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
+76 -39
View File
@@ -579,7 +579,9 @@ export function agentRunExecutionPolicyWithLaneCredentials(basePolicy: Record<st
},
};
});
const toolCredentials = includeToolCredentials ? agentRunToolCredentialRefs(spec).map((credential) => {
const toolCredentialRefs = includeToolCredentials ? agentRunToolCredentialRefs(spec) : [];
if (includeToolCredentials) validateBasePolicyToolCredentials(basePolicy, toolCredentialRefs, target);
const toolCredentials = toolCredentialRefs.map((credential) => {
if (credential.secretRef.namespace !== spec.runtime.namespace) {
throw new AgentRunRestError("validation-failed", `toolCredential ${credential.tool} Secret ${credential.secretRef.name} is in namespace ${credential.secretRef.namespace}, expected target lane namespace ${spec.runtime.namespace}`);
}
@@ -588,11 +590,11 @@ export function agentRunExecutionPolicyWithLaneCredentials(basePolicy: Record<st
purpose: credential.purpose,
secretRef: {
name: credential.secretRef.name,
keys: [credential.secretRef.key],
keys: credential.secretRef.keys,
},
projection: credential.projection,
};
}) : [];
});
const secretScope = record(basePolicy.secretScope);
return {
...basePolicy,
@@ -604,45 +606,80 @@ export function agentRunExecutionPolicyWithLaneCredentials(basePolicy: Record<st
};
}
export const AGENTRUN_DEFAULT_TOOL_CREDENTIAL_IDS = new Set(["tool-github-pr-token", "tool-unidesk-ssh-token"]);
export function agentRunToolCredentialRefs(spec: AgentRunLaneSpec, options: { includeUnsupported?: boolean } = {}): Array<{ tool: string; purpose: string; projection: Record<string, unknown>; sourceId: string; secretRef: { namespace: string; name: string; key: string }; valuesPrinted: false }> {
return spec.secrets
.filter((secret) => secret.providerCredentialProfile === null && secret.id.startsWith("tool-"))
.map((secret) => {
const binding = agentRunToolCredentialBinding(secret.id, secret.targetRef.key);
return {
tool: binding.tool,
purpose: binding.purpose,
projection: binding.projection,
sourceId: secret.id,
secretRef: secret.targetRef,
valuesPrinted: false as const,
};
})
.filter((credential) => options.includeUnsupported === true || AGENTRUN_DEFAULT_TOOL_CREDENTIAL_IDS.has(credential.sourceId));
export interface AgentRunToolCredentialRef {
readonly tool: string;
readonly purpose: string;
readonly projection: Record<string, unknown>;
readonly sourceId: string;
readonly secretRef: { readonly namespace: string; readonly name: string; readonly keys: readonly string[] };
readonly valuesPrinted: false;
}
export function agentRunToolCredentialBinding(sourceId: string, key: string): { tool: string; purpose: string; projection: Record<string, unknown> } {
if (sourceId === "tool-github-pr-token") {
return {
tool: "github",
purpose: "github-pr",
projection: { kind: "env", envName: key, secretKey: key },
};
export function agentRunToolCredentialRefs(spec: AgentRunLaneSpec): AgentRunToolCredentialRef[] {
return spec.toolCredentials.map((credential) => ({
tool: credential.tool,
purpose: credential.purpose,
projection: credential.projection,
sourceId: credential.id,
secretRef: credential.secretRef,
valuesPrinted: false,
}));
}
function validateBasePolicyToolCredentials(basePolicy: Record<string, unknown>, laneCredentials: readonly AgentRunToolCredentialRef[], target: AgentRunSessionPolicyTarget): void {
const rawCredentials = record(basePolicy.secretScope).toolCredentials;
if (rawCredentials === undefined || rawCredentials === null) return;
if (!Array.isArray(rawCredentials)) {
throw new AgentRunRestError("validation-failed", "executionPolicy.secretScope.toolCredentials must be an array when lane tool credentials are enabled");
}
if (sourceId === "tool-unidesk-ssh-token") {
return {
tool: "unidesk-ssh",
purpose: "ssh-passthrough",
projection: { kind: "env", envName: key, secretKey: key },
};
const laneByIdentity = new Map(laneCredentials.map((credential) => [`${credential.tool}\0${credential.purpose}`, credential]));
const seen = new Set<string>();
for (const [index, rawCredential] of rawCredentials.entries()) {
const credential = record(rawCredential);
const tool = stringOrNull(credential.tool);
const purpose = stringOrNull(credential.purpose);
if (tool === null || purpose === null) {
throw new AgentRunRestError("validation-failed", `executionPolicy.secretScope.toolCredentials[${index}] must declare tool and purpose`);
}
const identity = `${tool}\0${purpose}`;
if (seen.has(identity)) {
throw new AgentRunRestError("validation-failed", `executionPolicy.secretScope.toolCredentials contains duplicate ${tool}/${purpose} identity`);
}
seen.add(identity);
const laneCredential = laneByIdentity.get(identity);
if (laneCredential === undefined) {
throw new AgentRunRestError("validation-failed", `executionPolicy toolCredential ${tool}/${purpose} is not declared by YAML lane ${target.spec.nodeId}/${target.spec.lane}`);
}
const secretRef = record(credential.secretRef);
const secretName = stringOrNull(secretRef.name);
const namespace = stringOrNull(secretRef.namespace) ?? target.spec.runtime.namespace;
if (secretName !== laneCredential.secretRef.name || namespace !== laneCredential.secretRef.namespace) {
throw new AgentRunRestError("validation-failed", `executionPolicy toolCredential ${tool}/${purpose} SecretRef conflicts with YAML lane ${target.spec.nodeId}/${target.spec.lane}`);
}
const keys = normalizedCredentialKeys(secretRef.keys);
const laneKeys = normalizedCredentialKeys(laneCredential.secretRef.keys);
if (keys === null || laneKeys === null || keys.length !== laneKeys.length || keys.some((key, keyIndex) => key !== laneKeys[keyIndex])) {
throw new AgentRunRestError("validation-failed", `executionPolicy toolCredential ${tool}/${purpose} keys conflict with YAML lane ${target.spec.nodeId}/${target.spec.lane}`);
}
if (!toolCredentialProjectionMatches(record(credential.projection), laneCredential.projection)) {
throw new AgentRunRestError("validation-failed", `executionPolicy toolCredential ${tool}/${purpose} projection conflicts with YAML lane ${target.spec.nodeId}/${target.spec.lane}`);
}
}
return {
tool: sourceId.replace(/^tool-/u, "").replace(/-token$/u, ""),
purpose: sourceId.replace(/^tool-/u, ""),
projection: {},
};
}
function normalizedCredentialKeys(value: unknown): string[] | null {
if (!Array.isArray(value) || value.some((item) => typeof item !== "string" || item.trim().length === 0)) return null;
const keys = value.map((item) => String(item)).sort();
return new Set(keys).size === keys.length ? keys : null;
}
function toolCredentialProjectionMatches(actual: Record<string, unknown>, expected: Record<string, unknown>): boolean {
if (actual.kind !== expected.kind) return false;
if (expected.kind === "env") {
return actual.envName === expected.envName && actual.secretKey === expected.secretKey;
}
if (expected.kind === "volume") return actual.mountPath === expected.mountPath;
return false;
}
export function renderAgentRunSessionPolicyExplanation(args: string[] = [], options: AgentRunRestTargetOptions = { node: null, lane: null }): string {
@@ -660,7 +697,7 @@ export function renderAgentRunSessionPolicyExplanation(args: string[] = [], opti
secretRef: credential.secretRef,
valuesPrinted: false,
}));
const toolCredentialSources = agentRunToolCredentialRefs(spec, { includeUnsupported: true });
const toolCredentialSources = agentRunToolCredentialRefs(spec);
return [
"KIND: session-policy",
`CONFIG: ${config.sourcePath}`,