fix: 保真渲染 AgentRun 多键工具凭据

This commit is contained in:
Codex
2026-07-12 09:23:08 +02:00
parent 9947210f63
commit 54f4379511
4 changed files with 392 additions and 40 deletions
+103 -1
View File
@@ -110,6 +110,20 @@ export interface AgentRunLaneSecretSpec {
readonly providerCredentialProfile: string | null;
}
export interface AgentRunToolCredentialSpec {
readonly id: string;
readonly tool: string;
readonly purpose: string;
readonly secretRef: {
readonly namespace: string;
readonly name: string;
readonly keys: readonly string[];
};
readonly projection:
| { readonly kind: "env"; readonly envName: string; readonly secretKey: string }
| { readonly kind: "volume"; readonly mountPath: string };
}
export interface AgentRunCodexConfigSpec {
readonly modelProvider: string;
readonly model: string;
@@ -273,6 +287,7 @@ export interface AgentRunLaneSpec {
readonly localPostgresExpectedAbsent: boolean;
};
readonly secrets: readonly AgentRunLaneSecretSpec[];
readonly toolCredentials: readonly AgentRunToolCredentialSpec[];
}
export interface AgentRunContainerResources {
@@ -543,6 +558,14 @@ export function agentRunLaneSummary(spec: AgentRunLaneSpec): Record<string, unkn
},
valuesPrinted: false,
})),
toolCredentials: spec.toolCredentials.map((credential) => ({
id: credential.id,
tool: credential.tool,
purpose: credential.purpose,
secretRef: credential.secretRef,
projection: credential.projection,
valuesPrinted: false,
})),
providerCredentials: agentRunProviderCredentialRefs(spec).map((credential) => ({
profile: credential.profile,
secretRef: credential.secretRef,
@@ -660,6 +683,7 @@ function parseLane(lane: string, node: AgentRunNodeSpec, input: Record<string, u
const database = recordField(input, "database", path);
const version = stringField(input, "version", path);
const statusMode = sourceStatusModeField(optionalStringField(source, "statusMode", `${path}.source`) ?? (version === "v0.2" ? "k3s-git-mirror" : "host-worktree"), `${path}.source.statusMode`);
const secrets = parseLaneSecrets(input, path);
const spec: AgentRunLaneSpec = {
lane,
nodeId: node.id,
@@ -723,9 +747,11 @@ function parseLane(lane: string, node: AgentRunNodeSpec, input: Record<string, u
repositories: parseGitMirrorRepositories(arrayField(gitMirror, "repositories", `${path}.gitMirror`), `${path}.gitMirror.repositories`),
},
database: parseDatabase(database, `${path}.database`),
secrets: parseLaneSecrets(input, path),
secrets,
toolCredentials: parseToolCredentials(input, path),
};
validateAgentRunLaneSourceAuthority(spec, path);
validateToolCredentialSources(spec, path);
return spec;
}
@@ -1022,6 +1048,82 @@ function parseLaneSecrets(input: Record<string, unknown>, path: string): AgentRu
return secrets;
}
function parseToolCredentials(input: Record<string, unknown>, path: string): AgentRunToolCredentialSpec[] {
const credentials = arrayField(input, "toolCredentials", path).map((value, index) => {
const credentialPath = `${path}.toolCredentials[${index}]`;
const secretRef = recordField(value, "secretRef", credentialPath);
const projection = recordField(value, "projection", credentialPath);
const kind = stringField(projection, "kind", `${credentialPath}.projection`);
const keys = stringArrayField(secretRef, "keys", `${credentialPath}.secretRef`);
if (keys.length === 0) throw new Error(`${credentialPath}.secretRef.keys must not be empty`);
if (new Set(keys).size !== keys.length) throw new Error(`${credentialPath}.secretRef.keys must not contain duplicates`);
const normalizedProjection = kind === "env"
? parseToolCredentialEnvProjection(projection, keys, `${credentialPath}.projection`)
: kind === "volume"
? parseToolCredentialVolumeProjection(projection, `${credentialPath}.projection`)
: null;
if (normalizedProjection === null) throw new Error(`${credentialPath}.projection.kind must be env or volume`);
return {
id: stringField(value, "id", credentialPath),
tool: credentialSlugField(value, "tool", credentialPath),
purpose: credentialSlugField(value, "purpose", credentialPath),
secretRef: {
namespace: stringField(secretRef, "namespace", `${credentialPath}.secretRef`),
name: stringField(secretRef, "name", `${credentialPath}.secretRef`),
keys,
},
projection: normalizedProjection,
} satisfies AgentRunToolCredentialSpec;
});
const ids = new Map<string, string>();
const identities = new Map<string, string>();
for (const [index, credential] of credentials.entries()) {
const credentialPath = `${path}.toolCredentials[${index}]`;
const priorId = ids.get(credential.id);
if (priorId !== undefined) throw new Error(`${credentialPath}.id duplicates ${priorId}.id (${credential.id})`);
ids.set(credential.id, credentialPath);
const identity = `${credential.tool}/${credential.purpose}`;
const priorIdentity = identities.get(identity);
if (priorIdentity !== undefined) throw new Error(`${credentialPath} duplicates ${priorIdentity} tool/purpose (${identity})`);
identities.set(identity, credentialPath);
}
return credentials;
}
function parseToolCredentialEnvProjection(input: Record<string, unknown>, keys: readonly string[], path: string): AgentRunToolCredentialSpec["projection"] {
const envName = stringField(input, "envName", path);
if (!/^[A-Z_][A-Z0-9_]{0,63}$/u.test(envName)) throw new Error(`${path}.envName must be an uppercase environment variable name`);
const secretKey = stringField(input, "secretKey", path);
if (!keys.includes(secretKey)) throw new Error(`${path}.secretKey must be present in secretRef.keys`);
return { kind: "env", envName, secretKey };
}
function parseToolCredentialVolumeProjection(input: Record<string, unknown>, path: string): AgentRunToolCredentialSpec["projection"] {
const mountPath = absolutePathField(input, "mountPath", path);
if (!mountPath.startsWith("/home/agentrun/") || mountPath.includes("..")) throw new Error(`${path}.mountPath must stay under /home/agentrun`);
return { kind: "volume", mountPath };
}
function credentialSlugField(input: Record<string, unknown>, key: string, path: string): string {
const value = stringField(input, key, path);
if (!/^[a-z0-9][a-z0-9._-]{0,63}$/u.test(value)) throw new Error(`${path}.${key} must be a lowercase credential slug`);
return value;
}
function validateToolCredentialSources(spec: AgentRunLaneSpec, path: string): void {
const declaredTargets = new Set(spec.secrets.map((secret) => `${secret.targetRef.namespace}\0${secret.targetRef.name}\0${secret.targetRef.key}`));
for (const [index, credential] of spec.toolCredentials.entries()) {
const credentialPath = `${path}.toolCredentials[${index}]`;
if (credential.secretRef.namespace !== spec.runtime.namespace) {
throw new Error(`${credentialPath}.secretRef.namespace must match ${path}.runtime.namespace (${spec.runtime.namespace})`);
}
const missingKeys = credential.secretRef.keys.filter((key) => !declaredTargets.has(`${credential.secretRef.namespace}\0${credential.secretRef.name}\0${key}`));
if (missingKeys.length > 0) {
throw new Error(`${credentialPath}.secretRef.keys have no matching YAML Secret source: ${missingKeys.join(", ")}`);
}
}
}
function parseCodexConfig(input: Record<string, unknown>, path: string): AgentRunCodexConfigSpec {
const providersRaw = recordField(input, "modelProviders", path);
const modelProviders = Object.entries(providersRaw).map(([id, raw]) => {