fix: 支持动态 provider profile slug
This commit is contained in:
@@ -367,11 +367,7 @@ metadata:
|
||||
rules:
|
||||
- apiGroups: [""]
|
||||
resources: ["secrets"]
|
||||
verbs: ["create"]
|
||||
- apiGroups: [""]
|
||||
resources: ["secrets"]
|
||||
resourceNames: ["agentrun-v01-provider-codex", "agentrun-v01-provider-deepseek", "agentrun-v01-provider-minimax-m3", "agentrun-v01-provider-dsflash-go"]
|
||||
verbs: ["get", "patch", "update"]
|
||||
verbs: ["create", "get", "list", "patch", "update"]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: RoleBinding
|
||||
@@ -403,7 +399,6 @@ metadata:
|
||||
rules:
|
||||
- apiGroups: [""]
|
||||
resources: ["secrets"]
|
||||
resourceNames: ["agentrun-v01-provider-codex", "agentrun-v01-provider-deepseek", "agentrun-v01-provider-minimax-m3", "agentrun-v01-provider-dsflash-go"]
|
||||
verbs: ["get"]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import type { BackendProfile, JsonRecord } from "./types.js";
|
||||
|
||||
export const backendProfileIdPattern = /^[a-z0-9][a-z0-9-]{0,63}$/u;
|
||||
|
||||
export interface BackendProfileSpec {
|
||||
profile: BackendProfile;
|
||||
backendKind: "codex-app-server-stdio";
|
||||
@@ -13,7 +15,7 @@ export interface BackendProfileSpec {
|
||||
description: string;
|
||||
}
|
||||
|
||||
export const backendProfileSpecs: readonly BackendProfileSpec[] = [
|
||||
const builtinBackendProfileSpecs: readonly BackendProfileSpec[] = [
|
||||
{
|
||||
profile: "codex",
|
||||
backendKind: "codex-app-server-stdio",
|
||||
@@ -64,14 +66,40 @@ export const backendProfileSpecs: readonly BackendProfileSpec[] = [
|
||||
},
|
||||
];
|
||||
|
||||
export const backendProfiles = backendProfileSpecs.map((item) => item.profile) as readonly BackendProfile[];
|
||||
export const backendProfileSpecs = builtinBackendProfileSpecs;
|
||||
|
||||
export const backendProfiles = builtinBackendProfileSpecs.map((item) => item.profile) as readonly BackendProfile[];
|
||||
|
||||
export function defaultSecretNameForProfile(profile: string): string {
|
||||
return `agentrun-v01-provider-${profile}`;
|
||||
}
|
||||
|
||||
function dynamicBackendProfileSpec(profile: string): BackendProfileSpec {
|
||||
return {
|
||||
profile,
|
||||
backendKind: "codex-app-server-stdio",
|
||||
protocol: "codex-app-server-jsonrpc-stdio",
|
||||
transport: "stdio",
|
||||
command: "codex app-server --listen stdio://",
|
||||
status: "registered",
|
||||
requiredSecretKeys: ["auth.json", "config.toml"],
|
||||
defaultSecretName: defaultSecretNameForProfile(profile),
|
||||
profileIsolation: "profile-scoped-codex-home",
|
||||
description: `Dynamic Codex-compatible profile ${profile}`,
|
||||
};
|
||||
}
|
||||
|
||||
export function backendProfileSpec(profile: string): BackendProfileSpec | null {
|
||||
return backendProfileSpecs.find((item) => item.profile === profile) ?? null;
|
||||
if (!isBackendProfileSlug(profile)) return null;
|
||||
return builtinBackendProfileSpecs.find((item) => item.profile === profile) ?? dynamicBackendProfileSpec(profile);
|
||||
}
|
||||
|
||||
export function isBackendProfileSlug(value: string): boolean {
|
||||
return backendProfileIdPattern.test(value);
|
||||
}
|
||||
|
||||
export function isBackendProfile(value: string): value is BackendProfile {
|
||||
return backendProfileSpec(value) !== null;
|
||||
return isBackendProfileSlug(value);
|
||||
}
|
||||
|
||||
export function backendCapability(spec: BackendProfileSpec): JsonRecord {
|
||||
@@ -111,7 +139,7 @@ export function mergeBackendCapability(profile: string, storedCapabilities: Json
|
||||
}
|
||||
|
||||
export function backendCapabilities(): JsonRecord[] {
|
||||
return backendProfileSpecs.map(backendCapability);
|
||||
return builtinBackendProfileSpecs.map(backendCapability);
|
||||
}
|
||||
|
||||
export function backendCapabilitiesSqlValues(profiles?: readonly BackendProfile[]): string {
|
||||
@@ -121,7 +149,7 @@ export function backendCapabilitiesSqlValues(profiles?: readonly BackendProfile[
|
||||
if (!spec) throw new Error(`unknown backend profile for SQL seed: ${profile}`);
|
||||
return spec;
|
||||
})
|
||||
: backendProfileSpecs;
|
||||
: builtinBackendProfileSpecs;
|
||||
return specs.map((spec) => {
|
||||
const capabilities = JSON.stringify({
|
||||
backendKind: spec.backendKind,
|
||||
|
||||
+1
-1
@@ -28,7 +28,7 @@ export type FailureKind =
|
||||
export type RunStatus = "pending" | "claimed" | "running" | "completed" | "failed" | "blocked" | "cancelled";
|
||||
export type CommandState = "pending" | "acknowledged" | "completed" | "failed" | "cancelled";
|
||||
export type TerminalStatus = "completed" | "failed" | "blocked" | "cancelled";
|
||||
export type BackendProfile = "codex" | "deepseek" | "minimax-m3" | "dsflash-go";
|
||||
export type BackendProfile = string;
|
||||
export type QueueTaskState = "pending" | "running" | "completed" | "failed" | "blocked" | "cancelled";
|
||||
export type SessionExecutionState = "idle" | "running" | "terminal";
|
||||
export type SessionAttentionState = "active" | "unread" | "read";
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { createHash, randomUUID } from "node:crypto";
|
||||
import type { BackendProfile, CreateCommandInput, CreateQueueTaskInput, CreateRunInput, ExecutionPolicy, JsonRecord, JsonValue, QueueTaskState, ResourceBundleRef, SecretRef, SessionListState, SessionRef } from "./types.js";
|
||||
import { AgentRunError } from "./errors.js";
|
||||
import { backendProfileSpec, backendProfiles, isBackendProfile } from "./backend-profiles.js";
|
||||
import { backendProfileIdPattern, backendProfileSpec, isBackendProfile } from "./backend-profiles.js";
|
||||
|
||||
const backendProfilePatternText = String(backendProfileIdPattern);
|
||||
|
||||
const allowedTenants = new Set(["unidesk", "hwlab"]);
|
||||
const allowedToolCredentials = ["github", "unidesk-ssh"] as const;
|
||||
@@ -44,7 +46,7 @@ export function validateCreateRun(input: unknown): CreateRunInput {
|
||||
const tenantId = requiredString(record, "tenantId");
|
||||
if (!allowedTenants.has(tenantId)) throw new AgentRunError("tenant-policy-denied", `tenantId ${tenantId} is not allowed`, { httpStatus: 403 });
|
||||
const backendProfileValue = requiredString(record, "backendProfile");
|
||||
if (!isBackendProfile(backendProfileValue)) throw new AgentRunError("schema-invalid", `backendProfile ${backendProfileValue} is not supported in v0.1`, { httpStatus: 400, details: { allowedBackends: [...backendProfiles] } });
|
||||
if (!isBackendProfile(backendProfileValue)) throw new AgentRunError("schema-invalid", `backendProfile ${backendProfileValue} must be a lowercase slug`, { httpStatus: 400, details: { pattern: backendProfilePatternText } });
|
||||
const backendProfile = backendProfileValue as BackendProfile;
|
||||
const executionPolicy = validateExecutionPolicy(requiredRecord(record, "executionPolicy"));
|
||||
validateBackendSecretScope(backendProfile, executionPolicy);
|
||||
@@ -214,7 +216,7 @@ export function validateExecutionPolicy(record: JsonRecord): ExecutionPolicy {
|
||||
const item = asRecord(credential, "providerCredential");
|
||||
const profile = typeof item.profile === "string" ? item.profile.trim() : "";
|
||||
if (profile.length === 0) throw new AgentRunError("schema-invalid", "provider credential profile is required", { httpStatus: 400 });
|
||||
if (!isBackendProfile(profile)) throw new AgentRunError("schema-invalid", `provider credential profile ${profile} is not supported in v0.1`, { httpStatus: 400, details: { allowedBackends: [...backendProfiles] } });
|
||||
if (!isBackendProfile(profile)) throw new AgentRunError("schema-invalid", `provider credential profile ${profile} must be a lowercase slug`, { httpStatus: 400, details: { pattern: backendProfilePatternText } });
|
||||
const secretRef = asRecord(item.secretRef, "providerCredential.secretRef");
|
||||
if (typeof secretRef.name !== "string" || secretRef.name.length === 0) throw new AgentRunError("schema-invalid", "provider credential secretRef.name is required", { httpStatus: 400 });
|
||||
const keys = Array.isArray(secretRef.keys) ? secretRef.keys : [];
|
||||
@@ -327,7 +329,7 @@ export function validateCreateQueueTask(input: unknown): CreateQueueTaskInput {
|
||||
const tenantId = requiredString(record, "tenantId");
|
||||
if (!allowedTenants.has(tenantId)) throw new AgentRunError("tenant-policy-denied", `tenantId ${tenantId} is not allowed`, { httpStatus: 403 });
|
||||
const backendProfileValue = optionalString(record.backendProfile) ?? "codex";
|
||||
if (!isBackendProfile(backendProfileValue)) throw new AgentRunError("schema-invalid", `backendProfile ${backendProfileValue} is not supported in v0.1`, { httpStatus: 400, details: { allowedBackends: [...backendProfiles] } });
|
||||
if (!isBackendProfile(backendProfileValue)) throw new AgentRunError("schema-invalid", `backendProfile ${backendProfileValue} must be a lowercase slug`, { httpStatus: 400, details: { pattern: backendProfilePatternText } });
|
||||
const queue = optionalString(record.queue) ?? "default";
|
||||
const lane = optionalString(record.lane) ?? "default";
|
||||
const priorityValue = record.priority ?? 0;
|
||||
@@ -367,5 +369,5 @@ export function validateSessionListState(value: string): SessionListState {
|
||||
|
||||
export function validateBackendProfile(value: string): BackendProfile {
|
||||
if (isBackendProfile(value)) return value;
|
||||
throw new AgentRunError("schema-invalid", `backendProfile ${value} is not supported in v0.1`, { httpStatus: 400, details: { allowedBackends: [...backendProfiles] } });
|
||||
throw new AgentRunError("schema-invalid", `backendProfile ${value} must be a lowercase slug`, { httpStatus: 400, details: { pattern: backendProfilePatternText } });
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createHash, randomUUID } from "node:crypto";
|
||||
import { spawn } from "node:child_process";
|
||||
import { AgentRunError } from "../common/errors.js";
|
||||
import { backendProfileSpec, backendProfileSpecs } from "../common/backend-profiles.js";
|
||||
import { backendProfileSpec, backendProfileSpecs, isBackendProfileSlug } from "../common/backend-profiles.js";
|
||||
import type { AgentRunStore } from "./store.js";
|
||||
import type { BackendProfile, ExecutionPolicy, JsonRecord, JsonValue } from "../common/types.js";
|
||||
import { asRecord, validateBackendProfile } from "../common/validation.js";
|
||||
@@ -13,6 +13,7 @@ import { runnerJobStatusSummary } from "./runner-job-status.js";
|
||||
const defaultNamespace = "agentrun-v01";
|
||||
const credentialAnnotationPrefix = "agentrun.pikastech.local/provider-profile";
|
||||
const kubectlLastAppliedAnnotation = "kubectl.kubernetes.io/last-applied-configuration";
|
||||
const providerSecretNamePrefix = "agentrun-v01-provider-";
|
||||
|
||||
export interface ProviderProfileOptions {
|
||||
namespace?: string;
|
||||
@@ -40,7 +41,8 @@ interface RenderedConfig {
|
||||
}
|
||||
|
||||
export async function listProviderProfiles(options: ProviderProfileOptions = {}): Promise<JsonRecord> {
|
||||
const items = await Promise.all(backendProfileSpecs.map((spec) => providerProfileStatus(spec.profile, options)));
|
||||
const profiles = await listProviderProfileIds(options);
|
||||
const items = await Promise.all(profiles.map((profile) => providerProfileStatus(profile, options)));
|
||||
return { items, count: items.length, valuesPrinted: false };
|
||||
}
|
||||
|
||||
@@ -81,10 +83,8 @@ export async function setProviderProfileConfig(profileValue: string, body: unkno
|
||||
const delegatedBy = delegatedBySummary(record.delegatedBy);
|
||||
const namespace = profileNamespace(options);
|
||||
const existingSecret = await kubectlGetSecret(spec.defaultSecretName, namespace, options.kubectlCommand ?? "kubectl");
|
||||
if (!existingSecret) throw new AgentRunError("secret-unavailable", `provider profile ${profile} Secret is not configured`, { httpStatus: 404, details: { profile, secretRef: secretRefSummary(profile, namespace) } });
|
||||
const existingData = asOptionalRecord(existingSecret.data);
|
||||
const existingData = asOptionalRecord(existingSecret?.data);
|
||||
const authJsonData = dataKey(existingData, "auth.json");
|
||||
if (!authJsonData) throw new AgentRunError("secret-unavailable", `provider profile ${profile} auth.json is not configured`, { httpStatus: 400, details: { profile, secretRef: secretRefSummary(profile, namespace), key: "auth.json" } });
|
||||
const credentialHashSuffix = hashDataKey(existingData, "auth.json");
|
||||
const secretManifest: JsonRecord = {
|
||||
apiVersion: "v1",
|
||||
@@ -105,30 +105,28 @@ export async function setProviderProfileConfig(profileValue: string, body: unkno
|
||||
},
|
||||
},
|
||||
type: "Opaque",
|
||||
data: {
|
||||
"auth.json": authJsonData,
|
||||
"config.toml": base64Data(configToml),
|
||||
},
|
||||
data: providerProfileSecretData({ authJsonData, configToml }),
|
||||
};
|
||||
const applied = await kubectlUpsertSecret(secretManifest, options.kubectlCommand ?? "kubectl");
|
||||
return {
|
||||
action: "provider-profile-config-updated",
|
||||
mutation: true,
|
||||
profile,
|
||||
configured: true,
|
||||
configured: Boolean(authJsonData),
|
||||
secretRef: secretRefSummary(profile, namespace),
|
||||
resourceVersion: objectPath(applied, ["metadata", "resourceVersion"]),
|
||||
credentialHashSuffix,
|
||||
credentialHashSuffix: credentialHashSuffix ?? null,
|
||||
configHashSuffix: shortHash(configToml),
|
||||
updatedAt: objectPath(applied, ["metadata", "annotations", `${credentialAnnotationPrefix}-updated-at`]) ?? new Date().toISOString(),
|
||||
delegatedBy,
|
||||
requiresExternalBridgeUpdate: profile === "deepseek" || profile === "dsflash-go",
|
||||
requiresExternalBridgeUpdate: profileUsesMoonBridge(profile, configToml),
|
||||
configTomlPrinted: false,
|
||||
credentialValuesPrinted: false,
|
||||
valuesPrinted: false,
|
||||
pollCommands: {
|
||||
config: `./scripts/agentrun provider-profiles config ${profile}`,
|
||||
show: `./scripts/agentrun provider-profiles show ${profile}`,
|
||||
setKey: `./scripts/agentrun provider-profiles set-key ${profile} --key-stdin`,
|
||||
validate: `./scripts/agentrun provider-profiles validate ${profile} --wait --timeout-ms 120000`,
|
||||
},
|
||||
};
|
||||
@@ -180,7 +178,7 @@ export async function setProviderProfileCredential(profileValue: string, body: u
|
||||
updatedAt: objectPath(applied, ["metadata", "annotations", `${credentialAnnotationPrefix}-updated-at`]) ?? new Date().toISOString(),
|
||||
configSummary: rendered.config.configSummary,
|
||||
delegatedBy,
|
||||
requiresExternalBridgeUpdate: profile === "deepseek" || profile === "dsflash-go",
|
||||
requiresExternalBridgeUpdate: profileUsesMoonBridge(profile, rendered.config.configToml),
|
||||
valuesPrinted: false,
|
||||
pollCommands: {
|
||||
show: `./scripts/agentrun provider-profiles show ${profile}`,
|
||||
@@ -284,6 +282,46 @@ async function providerProfileStatus(profile: BackendProfile, options: ProviderP
|
||||
};
|
||||
}
|
||||
|
||||
async function listProviderProfileIds(options: ProviderProfileOptions): Promise<BackendProfile[]> {
|
||||
const profiles = new Set<BackendProfile>(backendProfileSpecs.map((spec) => spec.profile));
|
||||
const namespace = profileNamespace(options);
|
||||
const secrets = await kubectlListSecrets(namespace, options.kubectlCommand ?? "kubectl");
|
||||
for (const item of secrets) {
|
||||
const secretProfile = providerProfileFromSecret(item);
|
||||
if (secretProfile) profiles.add(secretProfile);
|
||||
}
|
||||
return [...profiles].sort(compareProviderProfiles);
|
||||
}
|
||||
|
||||
function compareProviderProfiles(left: string, right: string): number {
|
||||
const leftIndex = backendProfileSpecs.findIndex((item) => item.profile === left);
|
||||
const rightIndex = backendProfileSpecs.findIndex((item) => item.profile === right);
|
||||
if (leftIndex >= 0 || rightIndex >= 0) {
|
||||
if (leftIndex < 0) return 1;
|
||||
if (rightIndex < 0) return -1;
|
||||
if (leftIndex !== rightIndex) return leftIndex - rightIndex;
|
||||
}
|
||||
return left.localeCompare(right);
|
||||
}
|
||||
|
||||
function providerProfileFromSecret(secret: JsonRecord): BackendProfile | null {
|
||||
const metadata = asOptionalRecord(secret.metadata);
|
||||
const labels = asOptionalRecord(metadata?.labels);
|
||||
const annotatedProfile = stringPath(asOptionalRecord(metadata?.annotations), [`${credentialAnnotationPrefix}-profile`]);
|
||||
const labeledProfile = stringPath(labels, ["agentrun.pikastech.local/profile"]);
|
||||
const namedProfile = profileFromSecretName(stringPath(metadata, ["name"]));
|
||||
for (const candidate of [annotatedProfile, labeledProfile, namedProfile]) {
|
||||
if (candidate && isBackendProfileSlug(candidate)) return candidate;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function profileFromSecretName(name: string | null): string | null {
|
||||
if (!name || !name.startsWith(providerSecretNamePrefix)) return null;
|
||||
const profile = name.slice(providerSecretNamePrefix.length);
|
||||
return profile.length > 0 ? profile : null;
|
||||
}
|
||||
|
||||
function renderCredential(apiKey: string, config: RenderedConfig): { authJson: string; config: RenderedConfig } {
|
||||
const authJson = `${JSON.stringify(authPayload(apiKey))}\n`;
|
||||
return {
|
||||
@@ -292,6 +330,12 @@ function renderCredential(apiKey: string, config: RenderedConfig): { authJson: s
|
||||
};
|
||||
}
|
||||
|
||||
function providerProfileSecretData(input: { authJsonData?: string | null; configToml: string }): JsonRecord {
|
||||
const data: JsonRecord = { "config.toml": base64Data(input.configToml) };
|
||||
if (input.authJsonData) data["auth.json"] = input.authJsonData;
|
||||
return data;
|
||||
}
|
||||
|
||||
function authPayload(apiKey: string): JsonRecord {
|
||||
return { OPENAI_API_KEY: apiKey };
|
||||
}
|
||||
@@ -445,6 +489,10 @@ function defaultConfig(profile: BackendProfile): ProfileConfig {
|
||||
};
|
||||
}
|
||||
|
||||
function profileUsesMoonBridge(profile: BackendProfile, configToml: string): boolean {
|
||||
return profile === "deepseek" || profile === "dsflash-go" || configToml.includes("hwlab-deepseek-proxy.hwlab-v02.svc.cluster.local");
|
||||
}
|
||||
|
||||
function validateBaseUrl(profile: BackendProfile, value: string): void {
|
||||
let url: URL;
|
||||
try {
|
||||
@@ -525,7 +573,7 @@ function secretRefSummary(profile: BackendProfile, namespace: string): JsonRecor
|
||||
|
||||
function requiredSpec(profile: BackendProfile) {
|
||||
const spec = backendProfileSpec(profile);
|
||||
if (!spec) throw new AgentRunError("schema-invalid", `backendProfile ${profile} is not supported in v0.1`, { httpStatus: 400 });
|
||||
if (!spec) throw new AgentRunError("schema-invalid", `backendProfile ${profile} must be a lowercase slug`, { httpStatus: 400 });
|
||||
return spec;
|
||||
}
|
||||
|
||||
@@ -543,6 +591,16 @@ async function kubectlGetSecret(name: string, namespace: string, kubectlCommand:
|
||||
return parseKubectlObject(result.stdout, "secret");
|
||||
}
|
||||
|
||||
async function kubectlListSecrets(namespace: string, kubectlCommand: string): Promise<JsonRecord[]> {
|
||||
const result = await runKubectl(kubectlCommand, ["get", "secrets", "-n", namespace, "-o", "json"]);
|
||||
if (result.code !== 0) {
|
||||
throw new AgentRunError("infra-failed", `kubectl get secrets ${namespace} failed with code ${result.code}`, { httpStatus: 502, details: redactJson({ stderr: redactText(result.stderr.slice(-2000)), stdout: redactText(result.stdout.slice(-1000)) }) });
|
||||
}
|
||||
const parsed = parseKubectlObject(result.stdout, "secret-list");
|
||||
const items = Array.isArray(parsed.items) ? parsed.items : [];
|
||||
return items.filter((item): item is JsonRecord => typeof item === "object" && item !== null && !Array.isArray(item));
|
||||
}
|
||||
|
||||
async function kubectlUpsertSecret(manifest: JsonRecord, kubectlCommand: string): Promise<JsonRecord> {
|
||||
const name = stringPath(manifest, ["metadata", "name"]) ?? "<unknown>";
|
||||
const namespace = stringPath(manifest, ["metadata", "namespace"]) ?? defaultNamespace;
|
||||
|
||||
@@ -12,12 +12,9 @@ const secretText = "sk-selftest-provider-profile-secret";
|
||||
const selfTest: SelfTestCase = async (context) => {
|
||||
const gitopsRenderer = await readFile(path.join(context.root, "scripts/src/gitops-render.ts"), "utf8");
|
||||
assert.equal(gitopsRenderer.includes("agentrun-v01-mgr-provider-secret-manager"), true);
|
||||
assert.equal(gitopsRenderer.includes('verbs: ["create"]'), true);
|
||||
assert.equal(gitopsRenderer.includes('verbs: ["get", "patch", "update"]'), true);
|
||||
assert.equal(gitopsRenderer.includes('resourceNames: ["agentrun-v01-provider-codex", "agentrun-v01-provider-deepseek", "agentrun-v01-provider-minimax-m3", "agentrun-v01-provider-dsflash-go"]'), true);
|
||||
for (const profile of ["codex", "deepseek", "minimax-m3", "dsflash-go"]) {
|
||||
assert.equal(gitopsRenderer.includes(`agentrun-v01-provider-${profile}`), true);
|
||||
}
|
||||
assert.equal(gitopsRenderer.includes('verbs: ["create", "get", "list", "patch", "update"]'), true);
|
||||
assert.equal(gitopsRenderer.includes('resourceNames: ["agentrun-v01-provider-codex", "agentrun-v01-provider-deepseek", "agentrun-v01-provider-minimax-m3", "agentrun-v01-provider-dsflash-go"]'), false);
|
||||
assert.equal(gitopsRenderer.includes('resources: ["secrets"]'), true);
|
||||
|
||||
const fakeKubectl = path.join(context.tmp, "fake-provider-kubectl.js");
|
||||
const replacedSecretPath = path.join(context.tmp, "provider-secret-replace.json");
|
||||
@@ -42,11 +39,22 @@ if (args[0] === "get" && args[1] === "secret") {
|
||||
console.log(await stateFile.text());
|
||||
process.exit(0);
|
||||
}
|
||||
if (name === "agentrun-v01-provider-dsflash-go") {
|
||||
if (name === "agentrun-v01-provider-dsflash-go" || name === "agentrun-v01-provider-dsflash-go-cli-selftest") {
|
||||
console.error('Error from server (NotFound): secrets "' + name + '" not found');
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(JSON.stringify(fixtureSecret(name)));
|
||||
const fixture = fixtureSecret(name);
|
||||
await Bun.write(secretStatePath(name), JSON.stringify(fixture));
|
||||
console.log(JSON.stringify(fixture));
|
||||
process.exit(0);
|
||||
}
|
||||
if (args[0] === "get" && args[1] === "secrets") {
|
||||
const items = [fixtureSecret("agentrun-v01-provider-codex"), fixtureSecret("agentrun-v01-provider-deepseek"), fixtureSecret("agentrun-v01-provider-minimax-m3")];
|
||||
const dsflashState = Bun.file(secretStatePath("agentrun-v01-provider-dsflash-go"));
|
||||
if (await dsflashState.exists()) items.push(JSON.parse(await dsflashState.text()));
|
||||
const dynamicState = Bun.file(secretStatePath("agentrun-v01-provider-dsflash-go-cli-selftest"));
|
||||
if (await dynamicState.exists()) items.push(JSON.parse(await dynamicState.text()));
|
||||
console.log(JSON.stringify({ apiVersion: "v1", kind: "SecretList", items }));
|
||||
process.exit(0);
|
||||
}
|
||||
if (args[0] === "apply") {
|
||||
@@ -71,12 +79,10 @@ if (args[0] === "patch" && args[1] === "secret") {
|
||||
if (args[0] === "replace") {
|
||||
const text = await readStdin();
|
||||
const manifest = JSON.parse(text);
|
||||
if (manifest.metadata?.name === "agentrun-v01-provider-dsflash-go") {
|
||||
const stateFile = Bun.file(secretStatePath(manifest.metadata.name));
|
||||
if (!(await stateFile.exists())) {
|
||||
console.error('Error from server (NotFound): secrets "' + manifest.metadata.name + '" not found');
|
||||
process.exit(1);
|
||||
}
|
||||
const stateFile = Bun.file(secretStatePath(manifest.metadata.name));
|
||||
if (!(await stateFile.exists())) {
|
||||
console.error('Error from server (NotFound): secrets "' + manifest.metadata.name + '" not found');
|
||||
process.exit(1);
|
||||
}
|
||||
await Bun.write(${JSON.stringify(replacedSecretPath)}, JSON.stringify({ args, manifest }, null, 2));
|
||||
const annotations = manifest.metadata?.annotations ?? {};
|
||||
@@ -148,6 +154,27 @@ process.exit(1);
|
||||
assert.equal(Buffer.from(String(configData["auth.json"]), "base64").toString("utf8").includes("redacted-fixture"), true);
|
||||
assert.equal(Buffer.from(String(configData["config.toml"]), "base64").toString("utf8"), updatedConfigToml);
|
||||
|
||||
const dynamicProfile = "dsflash-go-cli-selftest";
|
||||
const dynamicConfigToml = "model_provider = \"opencode\"\nmodel = \"deepseek-v4-flash\"\nreview_model = \"deepseek-v4-flash\"\nbase_url = \"http://hwlab-deepseek-proxy.hwlab-v02.svc.cluster.local:4000/v1\"\n";
|
||||
const dynamicConfig = await client.put(`/api/v1/provider-profiles/${encodeURIComponent(dynamicProfile)}/config`, {
|
||||
configToml: dynamicConfigToml,
|
||||
delegatedBy: { system: "hwlab-v02", userId: "u-dynamic", username: "dynamic", requestId: "req-config-dynamic-selftest" },
|
||||
reason: "self-test-dynamic-config",
|
||||
}) as JsonRecord;
|
||||
assert.equal(dynamicConfig.profile, dynamicProfile);
|
||||
assert.equal(dynamicConfig.configured, false);
|
||||
assert.equal(dynamicConfig.credentialHashSuffix, null);
|
||||
const dynamicCreateRecord = JSON.parse(await readFile(createdSecretPath, "utf8")) as JsonRecord;
|
||||
const dynamicCreateManifest = dynamicCreateRecord.manifest as JsonRecord;
|
||||
assert.equal(((dynamicCreateManifest.metadata as JsonRecord).name), `agentrun-v01-provider-${dynamicProfile}`);
|
||||
const dynamicCreateData = dynamicCreateManifest.data as JsonRecord;
|
||||
assert.equal(Object.hasOwn(dynamicCreateData, "auth.json"), false);
|
||||
assert.equal(Buffer.from(String(dynamicCreateData["config.toml"]), "base64").toString("utf8"), dynamicConfigToml);
|
||||
|
||||
const dynamicShownAfterConfig = await client.get(`/api/v1/provider-profiles/${encodeURIComponent(dynamicProfile)}`) as JsonRecord;
|
||||
assert.equal(dynamicShownAfterConfig.configured, false);
|
||||
assert.equal(dynamicShownAfterConfig.failureKind, "secret-unavailable");
|
||||
|
||||
const updated = await client.put("/api/v1/provider-profiles/deepseek/credential", {
|
||||
apiKey: secretText,
|
||||
delegatedBy: { system: "hwlab-v02", userId: "u1", username: "tester", requestId: "req-selftest" },
|
||||
@@ -205,6 +232,30 @@ process.exit(1);
|
||||
assert.equal(dsflashShown.failureKind, null);
|
||||
assertNoSecretLeak(dsflashCreated);
|
||||
|
||||
const dynamicCredential = await client.put(`/api/v1/provider-profiles/${encodeURIComponent(dynamicProfile)}/credential`, {
|
||||
apiKey: secretText,
|
||||
delegatedBy: { system: "hwlab-v02", userId: "u3", username: "tester3", requestId: "req-dynamic-create-selftest" },
|
||||
reason: "self-test-dynamic-create",
|
||||
}) as JsonRecord;
|
||||
assert.equal(dynamicCredential.profile, dynamicProfile);
|
||||
assert.equal(dynamicCredential.resourceVersion, "rv-cleaned");
|
||||
assert.equal(dynamicCredential.requiresExternalBridgeUpdate, true);
|
||||
const dynamicReplaceRecord = JSON.parse(await readFile(replacedSecretPath, "utf8")) as JsonRecord;
|
||||
const dynamicReplaceManifest = dynamicReplaceRecord.manifest as JsonRecord;
|
||||
assert.equal(((dynamicReplaceManifest.metadata as JsonRecord).name), `agentrun-v01-provider-${dynamicProfile}`);
|
||||
const dynamicReplaceData = dynamicReplaceManifest.data as JsonRecord;
|
||||
assert.equal(Buffer.from(String(dynamicReplaceData["auth.json"]), "base64").toString("utf8").includes(secretText), true);
|
||||
assert.equal(Buffer.from(String(dynamicReplaceData["config.toml"]), "base64").toString("utf8"), dynamicConfigToml);
|
||||
const dynamicShown = await client.get(`/api/v1/provider-profiles/${encodeURIComponent(dynamicProfile)}`) as JsonRecord;
|
||||
assert.equal(dynamicShown.configured, true);
|
||||
assert.equal(dynamicShown.failureKind, null);
|
||||
|
||||
const listAfterDynamic = await client.get("/api/v1/provider-profiles") as JsonRecord;
|
||||
assert.equal(listAfterDynamic.count, 5);
|
||||
const dynamicListItems = (listAfterDynamic.items as JsonRecord[]) ?? [];
|
||||
assert.equal(dynamicListItems.some((item) => item.profile === dynamicProfile), true);
|
||||
assertNoSecretLeak(dynamicCredential);
|
||||
|
||||
await assert.rejects(
|
||||
() => client.put("/api/v1/provider-profiles/deepseek/credential", { apiKey: secretText, config: { baseUrl: "https://hyueapi.com/v1" } }),
|
||||
(error) => error instanceof Error && error.message.includes("not hyueapi.com"),
|
||||
@@ -228,7 +279,7 @@ process.exit(1);
|
||||
assert.equal(finalValidation.status, "completed");
|
||||
assert.equal(JSON.stringify(finalValidation).includes(secretText), false);
|
||||
assertNoSecretLeak(finalValidation);
|
||||
return { name: "provider-profile-management", tests: ["provider-profiles-list-redacted", "provider-profile-config", "provider-profile-set-key-redacted", "provider-profile-secret-replace-annotation-cleanup", "provider-profile-secret-create-upsert", "provider-profile-deepseek-moon-bridge", "provider-profile-manager-secret-rbac", "provider-profile-validation-runner-job"] };
|
||||
return { name: "provider-profile-management", tests: ["provider-profiles-list-redacted", "provider-profile-config", "provider-profile-set-key-redacted", "provider-profile-secret-replace-annotation-cleanup", "provider-profile-secret-create-upsert", "provider-profile-config-only-create", "provider-profile-dynamic-slug-roundtrip", "provider-profile-deepseek-moon-bridge", "provider-profile-manager-secret-rbac", "provider-profile-validation-runner-job"] };
|
||||
} finally {
|
||||
await new Promise<void>((resolve) => server.server.close(() => resolve()));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user