fix: 限定 Aipod 工具凭据最小集合
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user