fix: 支持动态 provider profile slug

This commit is contained in:
Codex
2026-06-08 04:19:58 +08:00
parent bda8e3bb1e
commit 509c2aa6fd
6 changed files with 181 additions and 47 deletions
+1 -6
View File
@@ -367,11 +367,7 @@ metadata:
rules: rules:
- apiGroups: [""] - apiGroups: [""]
resources: ["secrets"] resources: ["secrets"]
verbs: ["create"] verbs: ["create", "get", "list", "patch", "update"]
- 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"]
--- ---
apiVersion: rbac.authorization.k8s.io/v1 apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding kind: RoleBinding
@@ -403,7 +399,6 @@ metadata:
rules: rules:
- apiGroups: [""] - apiGroups: [""]
resources: ["secrets"] resources: ["secrets"]
resourceNames: ["agentrun-v01-provider-codex", "agentrun-v01-provider-deepseek", "agentrun-v01-provider-minimax-m3", "agentrun-v01-provider-dsflash-go"]
verbs: ["get"] verbs: ["get"]
--- ---
apiVersion: rbac.authorization.k8s.io/v1 apiVersion: rbac.authorization.k8s.io/v1
+34 -6
View File
@@ -1,5 +1,7 @@
import type { BackendProfile, JsonRecord } from "./types.js"; import type { BackendProfile, JsonRecord } from "./types.js";
export const backendProfileIdPattern = /^[a-z0-9][a-z0-9-]{0,63}$/u;
export interface BackendProfileSpec { export interface BackendProfileSpec {
profile: BackendProfile; profile: BackendProfile;
backendKind: "codex-app-server-stdio"; backendKind: "codex-app-server-stdio";
@@ -13,7 +15,7 @@ export interface BackendProfileSpec {
description: string; description: string;
} }
export const backendProfileSpecs: readonly BackendProfileSpec[] = [ const builtinBackendProfileSpecs: readonly BackendProfileSpec[] = [
{ {
profile: "codex", profile: "codex",
backendKind: "codex-app-server-stdio", 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 { 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 { export function isBackendProfile(value: string): value is BackendProfile {
return backendProfileSpec(value) !== null; return isBackendProfileSlug(value);
} }
export function backendCapability(spec: BackendProfileSpec): JsonRecord { export function backendCapability(spec: BackendProfileSpec): JsonRecord {
@@ -111,7 +139,7 @@ export function mergeBackendCapability(profile: string, storedCapabilities: Json
} }
export function backendCapabilities(): JsonRecord[] { export function backendCapabilities(): JsonRecord[] {
return backendProfileSpecs.map(backendCapability); return builtinBackendProfileSpecs.map(backendCapability);
} }
export function backendCapabilitiesSqlValues(profiles?: readonly BackendProfile[]): string { 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}`); if (!spec) throw new Error(`unknown backend profile for SQL seed: ${profile}`);
return spec; return spec;
}) })
: backendProfileSpecs; : builtinBackendProfileSpecs;
return specs.map((spec) => { return specs.map((spec) => {
const capabilities = JSON.stringify({ const capabilities = JSON.stringify({
backendKind: spec.backendKind, backendKind: spec.backendKind,
+1 -1
View File
@@ -28,7 +28,7 @@ export type FailureKind =
export type RunStatus = "pending" | "claimed" | "running" | "completed" | "failed" | "blocked" | "cancelled"; export type RunStatus = "pending" | "claimed" | "running" | "completed" | "failed" | "blocked" | "cancelled";
export type CommandState = "pending" | "acknowledged" | "completed" | "failed" | "cancelled"; export type CommandState = "pending" | "acknowledged" | "completed" | "failed" | "cancelled";
export type TerminalStatus = "completed" | "failed" | "blocked" | "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 QueueTaskState = "pending" | "running" | "completed" | "failed" | "blocked" | "cancelled";
export type SessionExecutionState = "idle" | "running" | "terminal"; export type SessionExecutionState = "idle" | "running" | "terminal";
export type SessionAttentionState = "active" | "unread" | "read"; export type SessionAttentionState = "active" | "unread" | "read";
+7 -5
View File
@@ -1,7 +1,9 @@
import { createHash, randomUUID } from "node:crypto"; import { createHash, randomUUID } from "node:crypto";
import type { BackendProfile, CreateCommandInput, CreateQueueTaskInput, CreateRunInput, ExecutionPolicy, JsonRecord, JsonValue, QueueTaskState, ResourceBundleRef, SecretRef, SessionListState, SessionRef } from "./types.js"; import type { BackendProfile, CreateCommandInput, CreateQueueTaskInput, CreateRunInput, ExecutionPolicy, JsonRecord, JsonValue, QueueTaskState, ResourceBundleRef, SecretRef, SessionListState, SessionRef } from "./types.js";
import { AgentRunError } from "./errors.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 allowedTenants = new Set(["unidesk", "hwlab"]);
const allowedToolCredentials = ["github", "unidesk-ssh"] as const; const allowedToolCredentials = ["github", "unidesk-ssh"] as const;
@@ -44,7 +46,7 @@ export function validateCreateRun(input: unknown): CreateRunInput {
const tenantId = requiredString(record, "tenantId"); const tenantId = requiredString(record, "tenantId");
if (!allowedTenants.has(tenantId)) throw new AgentRunError("tenant-policy-denied", `tenantId ${tenantId} is not allowed`, { httpStatus: 403 }); if (!allowedTenants.has(tenantId)) throw new AgentRunError("tenant-policy-denied", `tenantId ${tenantId} is not allowed`, { httpStatus: 403 });
const backendProfileValue = requiredString(record, "backendProfile"); 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 backendProfile = backendProfileValue as BackendProfile;
const executionPolicy = validateExecutionPolicy(requiredRecord(record, "executionPolicy")); const executionPolicy = validateExecutionPolicy(requiredRecord(record, "executionPolicy"));
validateBackendSecretScope(backendProfile, executionPolicy); validateBackendSecretScope(backendProfile, executionPolicy);
@@ -214,7 +216,7 @@ export function validateExecutionPolicy(record: JsonRecord): ExecutionPolicy {
const item = asRecord(credential, "providerCredential"); const item = asRecord(credential, "providerCredential");
const profile = typeof item.profile === "string" ? item.profile.trim() : ""; 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 (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"); 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 }); 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 : []; const keys = Array.isArray(secretRef.keys) ? secretRef.keys : [];
@@ -327,7 +329,7 @@ export function validateCreateQueueTask(input: unknown): CreateQueueTaskInput {
const tenantId = requiredString(record, "tenantId"); const tenantId = requiredString(record, "tenantId");
if (!allowedTenants.has(tenantId)) throw new AgentRunError("tenant-policy-denied", `tenantId ${tenantId} is not allowed`, { httpStatus: 403 }); if (!allowedTenants.has(tenantId)) throw new AgentRunError("tenant-policy-denied", `tenantId ${tenantId} is not allowed`, { httpStatus: 403 });
const backendProfileValue = optionalString(record.backendProfile) ?? "codex"; 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 queue = optionalString(record.queue) ?? "default";
const lane = optionalString(record.lane) ?? "default"; const lane = optionalString(record.lane) ?? "default";
const priorityValue = record.priority ?? 0; const priorityValue = record.priority ?? 0;
@@ -367,5 +369,5 @@ export function validateSessionListState(value: string): SessionListState {
export function validateBackendProfile(value: string): BackendProfile { export function validateBackendProfile(value: string): BackendProfile {
if (isBackendProfile(value)) return value; 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 } });
} }
+72 -14
View File
@@ -1,7 +1,7 @@
import { createHash, randomUUID } from "node:crypto"; import { createHash, randomUUID } from "node:crypto";
import { spawn } from "node:child_process"; import { spawn } from "node:child_process";
import { AgentRunError } from "../common/errors.js"; 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 { AgentRunStore } from "./store.js";
import type { BackendProfile, ExecutionPolicy, JsonRecord, JsonValue } from "../common/types.js"; import type { BackendProfile, ExecutionPolicy, JsonRecord, JsonValue } from "../common/types.js";
import { asRecord, validateBackendProfile } from "../common/validation.js"; import { asRecord, validateBackendProfile } from "../common/validation.js";
@@ -13,6 +13,7 @@ import { runnerJobStatusSummary } from "./runner-job-status.js";
const defaultNamespace = "agentrun-v01"; const defaultNamespace = "agentrun-v01";
const credentialAnnotationPrefix = "agentrun.pikastech.local/provider-profile"; const credentialAnnotationPrefix = "agentrun.pikastech.local/provider-profile";
const kubectlLastAppliedAnnotation = "kubectl.kubernetes.io/last-applied-configuration"; const kubectlLastAppliedAnnotation = "kubectl.kubernetes.io/last-applied-configuration";
const providerSecretNamePrefix = "agentrun-v01-provider-";
export interface ProviderProfileOptions { export interface ProviderProfileOptions {
namespace?: string; namespace?: string;
@@ -40,7 +41,8 @@ interface RenderedConfig {
} }
export async function listProviderProfiles(options: ProviderProfileOptions = {}): Promise<JsonRecord> { 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 }; 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 delegatedBy = delegatedBySummary(record.delegatedBy);
const namespace = profileNamespace(options); const namespace = profileNamespace(options);
const existingSecret = await kubectlGetSecret(spec.defaultSecretName, namespace, options.kubectlCommand ?? "kubectl"); 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"); 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 credentialHashSuffix = hashDataKey(existingData, "auth.json");
const secretManifest: JsonRecord = { const secretManifest: JsonRecord = {
apiVersion: "v1", apiVersion: "v1",
@@ -105,30 +105,28 @@ export async function setProviderProfileConfig(profileValue: string, body: unkno
}, },
}, },
type: "Opaque", type: "Opaque",
data: { data: providerProfileSecretData({ authJsonData, configToml }),
"auth.json": authJsonData,
"config.toml": base64Data(configToml),
},
}; };
const applied = await kubectlUpsertSecret(secretManifest, options.kubectlCommand ?? "kubectl"); const applied = await kubectlUpsertSecret(secretManifest, options.kubectlCommand ?? "kubectl");
return { return {
action: "provider-profile-config-updated", action: "provider-profile-config-updated",
mutation: true, mutation: true,
profile, profile,
configured: true, configured: Boolean(authJsonData),
secretRef: secretRefSummary(profile, namespace), secretRef: secretRefSummary(profile, namespace),
resourceVersion: objectPath(applied, ["metadata", "resourceVersion"]), resourceVersion: objectPath(applied, ["metadata", "resourceVersion"]),
credentialHashSuffix, credentialHashSuffix: credentialHashSuffix ?? null,
configHashSuffix: shortHash(configToml), configHashSuffix: shortHash(configToml),
updatedAt: objectPath(applied, ["metadata", "annotations", `${credentialAnnotationPrefix}-updated-at`]) ?? new Date().toISOString(), updatedAt: objectPath(applied, ["metadata", "annotations", `${credentialAnnotationPrefix}-updated-at`]) ?? new Date().toISOString(),
delegatedBy, delegatedBy,
requiresExternalBridgeUpdate: profile === "deepseek" || profile === "dsflash-go", requiresExternalBridgeUpdate: profileUsesMoonBridge(profile, configToml),
configTomlPrinted: false, configTomlPrinted: false,
credentialValuesPrinted: false, credentialValuesPrinted: false,
valuesPrinted: false, valuesPrinted: false,
pollCommands: { pollCommands: {
config: `./scripts/agentrun provider-profiles config ${profile}`, config: `./scripts/agentrun provider-profiles config ${profile}`,
show: `./scripts/agentrun provider-profiles show ${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`, 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(), updatedAt: objectPath(applied, ["metadata", "annotations", `${credentialAnnotationPrefix}-updated-at`]) ?? new Date().toISOString(),
configSummary: rendered.config.configSummary, configSummary: rendered.config.configSummary,
delegatedBy, delegatedBy,
requiresExternalBridgeUpdate: profile === "deepseek" || profile === "dsflash-go", requiresExternalBridgeUpdate: profileUsesMoonBridge(profile, rendered.config.configToml),
valuesPrinted: false, valuesPrinted: false,
pollCommands: { pollCommands: {
show: `./scripts/agentrun provider-profiles show ${profile}`, 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 } { function renderCredential(apiKey: string, config: RenderedConfig): { authJson: string; config: RenderedConfig } {
const authJson = `${JSON.stringify(authPayload(apiKey))}\n`; const authJson = `${JSON.stringify(authPayload(apiKey))}\n`;
return { 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 { function authPayload(apiKey: string): JsonRecord {
return { OPENAI_API_KEY: apiKey }; 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 { function validateBaseUrl(profile: BackendProfile, value: string): void {
let url: URL; let url: URL;
try { try {
@@ -525,7 +573,7 @@ function secretRefSummary(profile: BackendProfile, namespace: string): JsonRecor
function requiredSpec(profile: BackendProfile) { function requiredSpec(profile: BackendProfile) {
const spec = backendProfileSpec(profile); 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; return spec;
} }
@@ -543,6 +591,16 @@ async function kubectlGetSecret(name: string, namespace: string, kubectlCommand:
return parseKubectlObject(result.stdout, "secret"); 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> { async function kubectlUpsertSecret(manifest: JsonRecord, kubectlCommand: string): Promise<JsonRecord> {
const name = stringPath(manifest, ["metadata", "name"]) ?? "<unknown>"; const name = stringPath(manifest, ["metadata", "name"]) ?? "<unknown>";
const namespace = stringPath(manifest, ["metadata", "namespace"]) ?? defaultNamespace; const namespace = stringPath(manifest, ["metadata", "namespace"]) ?? defaultNamespace;
@@ -12,12 +12,9 @@ const secretText = "sk-selftest-provider-profile-secret";
const selfTest: SelfTestCase = async (context) => { const selfTest: SelfTestCase = async (context) => {
const gitopsRenderer = await readFile(path.join(context.root, "scripts/src/gitops-render.ts"), "utf8"); 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("agentrun-v01-mgr-provider-secret-manager"), true);
assert.equal(gitopsRenderer.includes('verbs: ["create"]'), true); assert.equal(gitopsRenderer.includes('verbs: ["create", "get", "list", "patch", "update"]'), 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"]'), false);
assert.equal(gitopsRenderer.includes('resourceNames: ["agentrun-v01-provider-codex", "agentrun-v01-provider-deepseek", "agentrun-v01-provider-minimax-m3", "agentrun-v01-provider-dsflash-go"]'), true); assert.equal(gitopsRenderer.includes('resources: ["secrets"]'), true);
for (const profile of ["codex", "deepseek", "minimax-m3", "dsflash-go"]) {
assert.equal(gitopsRenderer.includes(`agentrun-v01-provider-${profile}`), true);
}
const fakeKubectl = path.join(context.tmp, "fake-provider-kubectl.js"); const fakeKubectl = path.join(context.tmp, "fake-provider-kubectl.js");
const replacedSecretPath = path.join(context.tmp, "provider-secret-replace.json"); 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()); console.log(await stateFile.text());
process.exit(0); 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'); console.error('Error from server (NotFound): secrets "' + name + '" not found');
process.exit(1); 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); process.exit(0);
} }
if (args[0] === "apply") { if (args[0] === "apply") {
@@ -71,12 +79,10 @@ if (args[0] === "patch" && args[1] === "secret") {
if (args[0] === "replace") { if (args[0] === "replace") {
const text = await readStdin(); const text = await readStdin();
const manifest = JSON.parse(text); const manifest = JSON.parse(text);
if (manifest.metadata?.name === "agentrun-v01-provider-dsflash-go") { const stateFile = Bun.file(secretStatePath(manifest.metadata.name));
const stateFile = Bun.file(secretStatePath(manifest.metadata.name)); if (!(await stateFile.exists())) {
if (!(await stateFile.exists())) { console.error('Error from server (NotFound): secrets "' + manifest.metadata.name + '" not found');
console.error('Error from server (NotFound): secrets "' + manifest.metadata.name + '" not found'); process.exit(1);
process.exit(1);
}
} }
await Bun.write(${JSON.stringify(replacedSecretPath)}, JSON.stringify({ args, manifest }, null, 2)); await Bun.write(${JSON.stringify(replacedSecretPath)}, JSON.stringify({ args, manifest }, null, 2));
const annotations = manifest.metadata?.annotations ?? {}; 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["auth.json"]), "base64").toString("utf8").includes("redacted-fixture"), true);
assert.equal(Buffer.from(String(configData["config.toml"]), "base64").toString("utf8"), updatedConfigToml); 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", { const updated = await client.put("/api/v1/provider-profiles/deepseek/credential", {
apiKey: secretText, apiKey: secretText,
delegatedBy: { system: "hwlab-v02", userId: "u1", username: "tester", requestId: "req-selftest" }, delegatedBy: { system: "hwlab-v02", userId: "u1", username: "tester", requestId: "req-selftest" },
@@ -205,6 +232,30 @@ process.exit(1);
assert.equal(dsflashShown.failureKind, null); assert.equal(dsflashShown.failureKind, null);
assertNoSecretLeak(dsflashCreated); 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( await assert.rejects(
() => client.put("/api/v1/provider-profiles/deepseek/credential", { apiKey: secretText, config: { baseUrl: "https://hyueapi.com/v1" } }), () => 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"), (error) => error instanceof Error && error.message.includes("not hyueapi.com"),
@@ -228,7 +279,7 @@ process.exit(1);
assert.equal(finalValidation.status, "completed"); assert.equal(finalValidation.status, "completed");
assert.equal(JSON.stringify(finalValidation).includes(secretText), false); assert.equal(JSON.stringify(finalValidation).includes(secretText), false);
assertNoSecretLeak(finalValidation); 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 { } finally {
await new Promise<void>((resolve) => server.server.close(() => resolve())); await new Promise<void>((resolve) => server.server.close(() => resolve()));
} }