fix: 限定 Aipod 工具凭据最小集合

This commit is contained in:
Codex
2026-07-12 09:28:56 +02:00
parent 54f4379511
commit 1fbb3c9660
2 changed files with 77 additions and 32 deletions
+66 -27
View File
@@ -852,10 +852,45 @@ describe("AgentRun YAML tool credential binding", () => {
expect(JSON.stringify({ githubSsh, sources })).not.toContain("/root/.ssh");
});
test("preserves multi-key credentials across Aipod render and queue admission and rejects conflicts", async () => {
test("preserves explicit credential subsets across Aipod admission and rejects unknown identities or conflicts", async () => {
const renderBodies: Record<string, any>[] = [];
const submittedBodies: Record<string, any>[] = [];
let conflictingKeys = false;
let credentialMode: "all" | "subset" | "conflict" | "unknown" = "all";
const renderedToolCredentials = (): Record<string, unknown>[] => {
const credentials = [
{
tool: "github",
purpose: "github-pr",
secretRef: { name: "agentrun-v01-tool-github-pr", keys: ["GH_TOKEN"] },
projection: { kind: "env", envName: "GH_TOKEN", secretKey: "GH_TOKEN" },
},
{
tool: "unidesk-ssh",
purpose: "ssh-passthrough",
secretRef: { name: "agentrun-v01-tool-unidesk-ssh", keys: ["UNIDESK_SSH_CLIENT_TOKEN"] },
projection: { kind: "env", envName: "UNIDESK_SSH_CLIENT_TOKEN", secretKey: "UNIDESK_SSH_CLIENT_TOKEN" },
},
{
tool: "github",
purpose: "github-ssh",
secretRef: {
name: "agentrun-v01-tool-github-ssh",
keys: credentialMode === "conflict" ? ["id_ed25519", "known_hosts", "config"] : ["id_ed25519", "known_hosts"],
},
projection: { kind: "volume", mountPath: "/home/agentrun/.ssh" },
},
];
if (credentialMode === "subset") return credentials.slice(0, 1);
if (credentialMode === "unknown") {
return [{
tool: "artifact-store",
purpose: "publish",
secretRef: { name: "agentrun-artifact-store", keys: ["TOKEN"] },
projection: { kind: "env", envName: "ARTIFACT_TOKEN", secretKey: "TOKEN" },
}];
}
return credentials;
};
const renderedTask = () => ({
tenantId: "unidesk",
projectId: "pikasTech/unidesk",
@@ -875,29 +910,7 @@ describe("AgentRun YAML tool credential binding", () => {
profile: "codex",
secretRef: { name: "agentrun-v01-provider-codex", keys: ["auth.json", "config.toml"] },
}],
toolCredentials: [
{
tool: "github",
purpose: "github-pr",
secretRef: { name: "agentrun-v01-tool-github-pr", keys: ["GH_TOKEN"] },
projection: { kind: "env", envName: "GH_TOKEN", secretKey: "GH_TOKEN" },
},
{
tool: "unidesk-ssh",
purpose: "ssh-passthrough",
secretRef: { name: "agentrun-v01-tool-unidesk-ssh", keys: ["UNIDESK_SSH_CLIENT_TOKEN"] },
projection: { kind: "env", envName: "UNIDESK_SSH_CLIENT_TOKEN", secretKey: "UNIDESK_SSH_CLIENT_TOKEN" },
},
{
tool: "github",
purpose: "github-ssh",
secretRef: {
name: "agentrun-v01-tool-github-ssh",
keys: conflictingKeys ? ["id_ed25519", "known_hosts", "config"] : ["id_ed25519", "known_hosts"],
},
projection: { kind: "volume", mountPath: "/home/agentrun/.ssh" },
},
],
toolCredentials: renderedToolCredentials(),
},
},
payload: { prompt: "fixture" },
@@ -930,6 +943,11 @@ describe("AgentRun YAML tool credential binding", () => {
expect(accepted.ok).toBe(true);
expect(renderBodies).toHaveLength(1);
expect(submittedBodies).toHaveLength(1);
expect(renderBodies[0]?.executionPolicy?.secretScope?.toolCredentials?.map((credential: Record<string, unknown>) => `${credential.tool}/${credential.purpose}`)).toEqual([
"github/github-pr",
"unidesk-ssh/ssh-passthrough",
"github/github-ssh",
]);
for (const body of [renderBodies[0], submittedBodies[0]]) {
const githubSsh = body?.executionPolicy?.secretScope?.toolCredentials?.filter((credential: Record<string, unknown>) => credential.tool === "github" && credential.purpose === "github-ssh");
expect(githubSsh).toEqual([{
@@ -940,14 +958,35 @@ describe("AgentRun YAML tool credential binding", () => {
}]);
}
conflictingKeys = true;
credentialMode = "subset";
const subsetAccepted = await runAgentRunCommand(null, ["create", "task", "--aipod", "Artificer", "--prompt", "fixture", "-o", "json"]);
expect(subsetAccepted.ok).toBe(true);
expect(renderBodies).toHaveLength(2);
expect(submittedBodies).toHaveLength(2);
expect(submittedBodies[1]?.executionPolicy?.secretScope?.toolCredentials).toEqual([{
tool: "github",
purpose: "github-pr",
secretRef: { name: "agentrun-v01-tool-github-pr", keys: ["GH_TOKEN"] },
projection: { kind: "env", envName: "GH_TOKEN", secretKey: "GH_TOKEN" },
}]);
credentialMode = "conflict";
const rejected = await runAgentRunCommand(null, ["create", "task", "--aipod", "Artificer", "--prompt", "fixture", "-o", "json"]);
expect(rejected.ok).toBe(false);
expect(submittedBodies).toHaveLength(1);
expect(submittedBodies).toHaveLength(2);
const rejectedText = renderedTextOf(rejected);
expect(rejectedText).toContain("validation-failed");
expect(rejectedText).toContain("github/github-ssh keys conflict");
expect(Buffer.byteLength(rejectedText, "utf8")).toBeLessThan(2400);
credentialMode = "unknown";
const unknownRejected = await runAgentRunCommand(null, ["create", "task", "--aipod", "Artificer", "--prompt", "fixture", "-o", "json"]);
expect(unknownRejected.ok).toBe(false);
expect(submittedBodies).toHaveLength(2);
const unknownText = renderedTextOf(unknownRejected);
expect(unknownText).toContain("validation-failed");
expect(unknownText).toContain("artifact-store/publish is not declared by YAML lane");
expect(Buffer.byteLength(unknownText, "utf8")).toBeLessThan(2400);
} finally {
server.stop(true);
if (previousConfig === undefined) delete process.env.AGENTRUN_CLIENT_CONFIG;
+11 -5
View File
@@ -579,8 +579,10 @@ export function agentRunExecutionPolicyWithLaneCredentials(basePolicy: Record<st
},
};
});
const toolCredentialRefs = includeToolCredentials ? agentRunToolCredentialRefs(spec) : [];
if (includeToolCredentials) validateBasePolicyToolCredentials(basePolicy, toolCredentialRefs, target);
const laneToolCredentialRefs = includeToolCredentials ? agentRunToolCredentialRefs(spec) : [];
const toolCredentialRefs = includeToolCredentials
? selectBasePolicyToolCredentials(basePolicy, laneToolCredentialRefs, 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}`);
@@ -626,14 +628,16 @@ export function agentRunToolCredentialRefs(spec: AgentRunLaneSpec): AgentRunTool
}));
}
function validateBasePolicyToolCredentials(basePolicy: Record<string, unknown>, laneCredentials: readonly AgentRunToolCredentialRef[], target: AgentRunSessionPolicyTarget): void {
const rawCredentials = record(basePolicy.secretScope).toolCredentials;
if (rawCredentials === undefined || rawCredentials === null) return;
function selectBasePolicyToolCredentials(basePolicy: Record<string, unknown>, laneCredentials: readonly AgentRunToolCredentialRef[], target: AgentRunSessionPolicyTarget): AgentRunToolCredentialRef[] {
const secretScope = record(basePolicy.secretScope);
if (!Object.hasOwn(secretScope, "toolCredentials")) return [...laneCredentials];
const rawCredentials = secretScope.toolCredentials;
if (!Array.isArray(rawCredentials)) {
throw new AgentRunRestError("validation-failed", "executionPolicy.secretScope.toolCredentials must be an array when lane tool credentials are enabled");
}
const laneByIdentity = new Map(laneCredentials.map((credential) => [`${credential.tool}\0${credential.purpose}`, credential]));
const seen = new Set<string>();
const selected: AgentRunToolCredentialRef[] = [];
for (const [index, rawCredential] of rawCredentials.entries()) {
const credential = record(rawCredential);
const tool = stringOrNull(credential.tool);
@@ -664,7 +668,9 @@ function validateBasePolicyToolCredentials(basePolicy: Record<string, unknown>,
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}`);
}
selected.push(laneCredential);
}
return selected;
}
function normalizedCredentialKeys(value: unknown): string[] | null {