fix: 保真渲染 AgentRun 多键工具凭据
This commit is contained in:
@@ -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]) => {
|
||||
|
||||
Reference in New Issue
Block a user