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
+62
View File
@@ -306,6 +306,10 @@ controlPlane:
name: agentrun-v02-mgr-db
key: DATABASE_URL
localPostgresExpectedAbsent: true
toolCredentials:
- extends: controlPlane.templates.toolCredentials.githubPr
- extends: controlPlane.templates.toolCredentials.unideskSsh
- extends: controlPlane.templates.toolCredentials.githubSsh
secrets:
- id: manager-api-key
sourceRef: hwlab/nc01-v03-admin.env
@@ -478,6 +482,8 @@ controlPlane:
profile: dsflash-go
- extends: controlPlane.templates.secrets.githubPrRawToken
- extends: controlPlane.templates.secrets.unideskSshToken
- extends: controlPlane.templates.secrets.githubSshPrivateKey
- extends: controlPlane.templates.secrets.githubSshKnownHosts
extends: controlPlane.templates.agentrunV02Lane
variables:
NODE: NC01
@@ -533,6 +539,46 @@ controlPlane:
argoNamespace: argocd
argoApplication: "${argoApplication}"
repoURL: http://git-mirror-http.devops-infra.svc.cluster.local:8080/pikasTech/agentrun.git
toolCredentials:
githubPr:
id: github-pr
tool: github
purpose: github-pr
secretRef:
namespace: agentrun-v02
name: agentrun-v01-tool-github-pr
keys:
- GH_TOKEN
projection:
kind: env
envName: GH_TOKEN
secretKey: GH_TOKEN
unideskSsh:
id: unidesk-ssh
tool: unidesk-ssh
purpose: ssh-passthrough
secretRef:
namespace: agentrun-v02
name: agentrun-v01-tool-unidesk-ssh
keys:
- UNIDESK_SSH_CLIENT_TOKEN
projection:
kind: env
envName: UNIDESK_SSH_CLIENT_TOKEN
secretKey: UNIDESK_SSH_CLIENT_TOKEN
githubSsh:
id: github-ssh
tool: github
purpose: github-ssh
secretRef:
namespace: agentrun-v02
name: agentrun-v01-tool-github-ssh
keys:
- id_ed25519
- known_hosts
projection:
kind: volume
mountPath: /home/agentrun/.ssh
secrets:
githubPrRawToken:
id: tool-github-pr-token
@@ -551,3 +597,19 @@ controlPlane:
namespace: agentrun-v02
name: agentrun-v01-tool-unidesk-ssh
key: UNIDESK_SSH_CLIENT_TOKEN
githubSshPrivateKey:
id: tool-github-ssh-private-key
sourceMode: file
sourceRef: deploy-ssh/github.com/id_ed25519
targetRef:
namespace: agentrun-v02
name: agentrun-v01-tool-github-ssh
key: id_ed25519
githubSshKnownHosts:
id: tool-github-ssh-known-hosts
sourceMode: file
sourceRef: deploy-ssh/github.com/known_hosts
targetRef:
namespace: agentrun-v02
name: agentrun-v01-tool-github-ssh
key: known_hosts
+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]) => {
+151
View File
@@ -2,7 +2,9 @@ import { readFileSync, rmSync } from "node:fs";
import { describe, expect, test } from "bun:test";
import { parseAgentRunClientConfigYaml, resolveAgentRunAuth, runAgentRunCommand, stripAgentRunResourceWrapperArgs } from "./agentrun";
import { resolveAgentRunLaneTarget } from "./agentrun-lanes";
import { agentRunToolCredentialRefs } from "./agentrun/config";
import { placeholderAgentRunImage, renderAgentRunGitopsFiles } from "./agentrun-manifests";
import { collectLaneSecretSources, secretSyncScript } from "./agentrun/secrets";
function renderedTextOf(value: unknown): string {
if (typeof value !== "object" || value === null) throw new Error("expected rendered CLI result");
@@ -808,6 +810,155 @@ describe("AgentRun default transport contract", () => {
});
});
describe("AgentRun YAML tool credential binding", () => {
test("aggregates managed GitHub SSH source keys into one volume credential and one sync target", () => {
const { spec } = resolveAgentRunLaneTarget({ node: "NC01", lane: "nc01-v02" });
const githubSsh = agentRunToolCredentialRefs(spec).filter((credential) => credential.tool === "github" && credential.purpose === "github-ssh");
expect(githubSsh).toEqual([{
sourceId: "github-ssh",
tool: "github",
purpose: "github-ssh",
secretRef: {
namespace: "agentrun-v02",
name: "agentrun-v01-tool-github-ssh",
keys: ["id_ed25519", "known_hosts"],
},
projection: { kind: "volume", mountPath: "/home/agentrun/.ssh" },
valuesPrinted: false,
}]);
const sources = collectLaneSecretSources(spec).filter((source) => source.targetRef.name === "agentrun-v01-tool-github-ssh");
expect(sources.map((source) => ({ id: source.id, sourceMode: source.sourceMode, sourceRef: source.sourceRef, targetRef: source.targetRef }))).toEqual([
{
id: "tool-github-ssh-private-key",
sourceMode: "file",
sourceRef: "deploy-ssh/github.com/id_ed25519",
targetRef: { namespace: "agentrun-v02", name: "agentrun-v01-tool-github-ssh", key: "id_ed25519" },
},
{
id: "tool-github-ssh-known-hosts",
sourceMode: "file",
sourceRef: "deploy-ssh/github.com/known_hosts",
targetRef: { namespace: "agentrun-v02", name: "agentrun-v01-tool-github-ssh", key: "known_hosts" },
},
]);
const syncScript = secretSyncScript(spec, sources.map((source, index) => ({ targetRef: source.targetRef, value: `fixture-${index}` })));
const payloadMatch = syncScript.match(/^payload_b64='([^']+)'$/mu);
expect(payloadMatch).not.toBeNull();
const syncManifest = JSON.parse(Buffer.from(payloadMatch?.[1] ?? "", "base64").toString("utf8")) as Array<{ targetRef: { namespace: string; name: string; key: string } }>;
expect(syncManifest.map((item) => item.targetRef)).toEqual(sources.map((source) => source.targetRef));
expect(new Set(syncManifest.map((item) => `${item.targetRef.namespace}/${item.targetRef.name}`))).toEqual(new Set(["agentrun-v02/agentrun-v01-tool-github-ssh"]));
expect(syncScript).toContain("groups.setdefault(key");
expect(JSON.stringify({ githubSsh, sources })).not.toContain("/root/.ssh");
});
test("preserves multi-key credentials across Aipod render and queue admission and rejects conflicts", async () => {
const renderBodies: Record<string, any>[] = [];
const submittedBodies: Record<string, any>[] = [];
let conflictingKeys = false;
const renderedTask = () => ({
tenantId: "unidesk",
projectId: "pikasTech/unidesk",
queue: "commander",
lane: "v0.1",
backendProfile: "codex",
providerId: "NC01",
workspaceRef: { kind: "opaque", path: "." },
executionPolicy: {
sandbox: "workspace-write",
approval: "never",
timeoutMs: 1_800_000,
network: "enabled",
secretScope: {
allowCredentialEcho: false,
providerCredentials: [{
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" },
},
],
},
},
payload: { prompt: "fixture" },
});
const server = Bun.serve({
port: 0,
async fetch(request) {
const url = new URL(request.url);
expect(request.headers.get("authorization")).toBe("Bearer secret-value");
if (url.pathname === "/api/v1/aipod-specs/Artificer/render") {
renderBodies.push(await request.json() as Record<string, any>);
return Response.json({ ok: true, data: { queueTask: renderedTask() } });
}
if (url.pathname === "/api/v1/queue/tasks") {
submittedBodies.push(await request.json() as Record<string, any>);
return Response.json({ ok: true, data: { id: "qt_fixture", state: "queued" } });
}
return new Response("not found", { status: 404 });
},
});
const previousConfig = process.env.AGENTRUN_CLIENT_CONFIG;
const previousKey = process.env.HWLAB_API_KEY;
process.env.HWLAB_API_KEY = "secret-value";
const tempConfigPath = `/tmp/unidesk-agentrun-tool-credential-test-${process.pid}-${Date.now()}.yaml`;
try {
process.env.AGENTRUN_CLIENT_CONFIG = tempConfigPath;
await Bun.write(tempConfigPath, agentRunMinimalClientYaml.replace("http://agentrun.example.local:8080", server.url.href.replace(/\/$/u, "")));
const accepted = await runAgentRunCommand(null, ["create", "task", "--aipod", "Artificer", "--prompt", "fixture", "-o", "json"]);
expect(accepted.ok).toBe(true);
expect(renderBodies).toHaveLength(1);
expect(submittedBodies).toHaveLength(1);
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([{
tool: "github",
purpose: "github-ssh",
secretRef: { name: "agentrun-v01-tool-github-ssh", keys: ["id_ed25519", "known_hosts"] },
projection: { kind: "volume", mountPath: "/home/agentrun/.ssh" },
}]);
}
conflictingKeys = true;
const rejected = await runAgentRunCommand(null, ["create", "task", "--aipod", "Artificer", "--prompt", "fixture", "-o", "json"]);
expect(rejected.ok).toBe(false);
expect(submittedBodies).toHaveLength(1);
const rejectedText = renderedTextOf(rejected);
expect(rejectedText).toContain("validation-failed");
expect(rejectedText).toContain("github/github-ssh keys conflict");
expect(Buffer.byteLength(rejectedText, "utf8")).toBeLessThan(2400);
} finally {
server.stop(true);
if (previousConfig === undefined) delete process.env.AGENTRUN_CLIENT_CONFIG;
else process.env.AGENTRUN_CLIENT_CONFIG = previousConfig;
if (previousKey === undefined) delete process.env.HWLAB_API_KEY;
else process.env.HWLAB_API_KEY = previousKey;
rmSync(tempConfigPath, { force: true });
}
});
});
describe("AgentRun runner API key SecretRef", () => {
test("renders manager runtime authority from the YAML runner SecretRef", () => {
const { spec } = resolveAgentRunLaneTarget({ node: "NC01", lane: "nc01-v02" });
+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}`,