fix: 收窄 Artificer pika 配置范围

This commit is contained in:
Codex
2026-07-12 17:57:44 +02:00
parent 18017306b9
commit acb9674454
14 changed files with 50 additions and 874 deletions
+8 -451
View File
@@ -1,7 +1,7 @@
import { readFileSync, rmSync } from "node:fs";
import { describe, expect, test } from "bun:test";
import { parseAgentRunClientConfigYaml, resolveAgentRunAuth, runAgentRunCommand, stripAgentRunResourceWrapperArgs } from "./agentrun";
import { agentRunProviderCredentialRefs, agentRunProviderCredentialRefsForSelection, resolveAgentRunLaneTarget } from "./agentrun-lanes";
import { resolveAgentRunLaneTarget } from "./agentrun-lanes";
import { agentRunToolCredentialRefs } from "./agentrun/config";
import { placeholderAgentRunImage, renderAgentRunGitopsFiles } from "./agentrun-manifests";
import { collectLaneSecretSources, secretSyncScript } from "./agentrun/secrets";
@@ -979,101 +979,6 @@ describe("AgentRun default transport contract", () => {
});
describe("AgentRun YAML tool credential binding", () => {
test("maps the YAML source profile to one canonical AgentRun backend profile", async () => {
const { spec } = resolveAgentRunLaneTarget({ node: "NC01", lane: "nc01-v02" });
const pikaSecrets = spec.secrets.filter((secret) => secret.providerCredentialSourceProfile === "gpt.pika");
expect(pikaSecrets.map((secret) => ({
id: secret.id,
sourceMode: secret.sourceMode,
sourceRef: secret.sourceRef,
sourceKey: secret.sourceKey,
sourceFormat: secret.sourceFormat,
transform: secret.transform,
codexConfig: secret.codexConfig,
jsonValue: secret.jsonValue,
sourceProfile: secret.providerCredentialSourceProfile,
effectiveBackendProfile: secret.providerCredentialProfile,
targetRef: secret.targetRef,
}))).toEqual([
{
id: "provider-gpt-pika-auth-json",
sourceMode: "file",
sourceRef: "/root/.codex/auth.json.pika",
sourceKey: null,
sourceFormat: null,
transform: null,
codexConfig: null,
jsonValue: null,
sourceProfile: "gpt.pika",
effectiveBackendProfile: "gpt-pika",
targetRef: { namespace: "agentrun-v02", name: "agentrun-v02-provider-gpt-pika", key: "auth.json" },
},
{
id: "provider-gpt-pika-config",
sourceMode: "file",
sourceRef: "/root/.codex/config.toml.pika",
sourceKey: null,
sourceFormat: null,
transform: null,
codexConfig: null,
jsonValue: null,
sourceProfile: "gpt.pika",
effectiveBackendProfile: "gpt-pika",
targetRef: { namespace: "agentrun-v02", name: "agentrun-v02-provider-gpt-pika", key: "config.toml" },
},
]);
const renderedPikaSources = collectLaneSecretSources(spec).filter((source) => source.providerCredentialSourceProfile === "gpt.pika");
expect(renderedPikaSources.map((source) => ({
id: source.id,
sourceMode: source.sourceMode,
sourceRef: source.sourceRef,
sourceKey: source.sourceKey,
sourceFormat: source.sourceFormat,
transform: source.transform,
codexConfig: source.codexConfig,
jsonValue: source.jsonValue,
sourceProfile: source.providerCredentialSourceProfile,
effectiveBackendProfile: source.providerCredentialProfile,
targetRef: source.targetRef,
}))).toEqual(pikaSecrets.map((secret) => ({
id: secret.id,
sourceMode: secret.sourceMode,
sourceRef: secret.sourceRef,
sourceKey: secret.sourceKey,
sourceFormat: secret.sourceFormat,
transform: secret.transform,
codexConfig: secret.codexConfig,
jsonValue: secret.jsonValue,
sourceProfile: secret.providerCredentialSourceProfile,
effectiveBackendProfile: secret.providerCredentialProfile,
targetRef: secret.targetRef,
})));
const expected = [{
sourceProfile: "gpt.pika",
profile: "gpt-pika",
secretRef: {
namespace: "agentrun-v02",
name: "agentrun-v02-provider-gpt-pika",
keys: ["auth.json", "config.toml"],
},
}];
expect(agentRunProviderCredentialRefsForSelection(spec, "gpt.pika")).toEqual(expected);
expect(agentRunProviderCredentialRefs(spec, "gpt-pika")).toEqual(expected);
expect(agentRunProviderCredentialRefs(spec, "gpt.pika")).toEqual([]);
const invalidPath = `/tmp/unidesk-agentrun-invalid-profile-${process.pid}-${Date.now()}.yaml`;
try {
const invalid = readFileSync("config/agentrun.yaml", "utf8").replaceAll(" profile: gpt-pika", " profile: gpt.pika");
await Bun.write(invalidPath, invalid);
expect(() => resolveAgentRunLaneTarget(
{ node: "NC01", lane: "nc01-v02" },
{ ...process.env, AGENTRUN_CONTROL_PLANE_CONFIG: invalidPath },
)).toThrow("canonical AgentRun backend profile slug");
} finally {
rmSync(invalidPath, { force: true });
}
});
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");
@@ -1115,290 +1020,6 @@ describe("AgentRun YAML tool credential binding", () => {
expect(JSON.stringify({ githubSsh, sources })).not.toContain("/root/.ssh");
});
test("shares model overrides across Aipod render, create, and send without overriding provider authority", async () => {
const renderBodies: Record<string, any>[] = [];
const submittedBodies: Record<string, any>[] = [];
const sendBodies: Record<string, any>[] = [];
let renderedBackendProfile = "gpt-pika";
let renderedCredentialProfile = "gpt-pika";
let renderedSecretName = "agentrun-v02-provider-gpt-pika";
let resolutionModelOverride: string | null = null;
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") {
const input = await request.json() as Record<string, any>;
renderBodies.push(input);
const model = input.model ?? "gpt-5.6-terra";
const reasoningEffort = input.reasoningEffort ?? "xhigh";
const modelSource = input.model === undefined ? "aipod-spec-default" : "render-input-override";
const reasoningEffortSource = input.reasoningEffort === undefined ? "aipod-spec-default" : "render-input-override";
return Response.json({
ok: true,
data: {
queueTask: {
tenantId: "unidesk",
projectId: "pikasTech/unidesk",
queue: "commander",
lane: "v0.2",
providerId: "NC01",
backendProfile: renderedBackendProfile,
workspaceRef: input.workspaceRef ?? { kind: "opaque", path: "." },
executionPolicy: {
sandbox: "workspace-write",
approval: "never",
timeoutMs: 1_800_000,
network: "enabled",
secretScope: {
allowCredentialEcho: false,
providerCredentials: [{
profile: renderedCredentialProfile,
secretRef: {
namespace: "agentrun-v02",
name: renderedSecretName,
keys: ["auth.json", "config.toml"],
},
}],
},
},
payload: {
prompt: input.prompt ?? "fixture",
model,
reasoningEffort,
modelConfig: { model, reasoningEffort, modelSource, reasoningEffortSource, valuesPrinted: false },
},
},
dispatchDefaults: {
runnerJob: { namespace: "agentrun-v02" },
},
modelResolution: {
model: resolutionModelOverride ?? model,
reasoningEffort,
modelSource,
reasoningEffortSource,
modelOverridden: input.model !== undefined,
reasoningEffortOverridden: input.reasoningEffort !== undefined,
backendProfile: "gpt-pika",
backendProfileSource: "aipod-spec",
providerCredential: {
profile: "gpt-pika",
secretRef: {
namespace: "agentrun-v02",
name: "agentrun-v02-provider-gpt-pika",
keys: ["auth.json", "config.toml"],
valuesPrinted: false,
},
valuesPrinted: false,
},
valuesPrinted: false,
},
},
});
}
if (url.pathname === "/api/v1/queue/tasks") {
submittedBodies.push(await request.json() as Record<string, any>);
return Response.json({ ok: true, data: { id: "qt_gpt_pika", state: "pending" } });
}
if (url.pathname === "/api/v1/sessions/ses_gpt_pika/send") {
const body = await request.json() as Record<string, any>;
sendBodies.push(body);
return Response.json({
ok: true,
data: {
action: "session-send-plan",
dryRun: body.dryRun,
mutation: false,
sessionId: "ses_gpt_pika",
decision: "turn",
internalCommandType: "turn",
request: { method: "POST", path: url.pathname, createRunnerJob: true, valuesPrinted: false },
valuesPrinted: false,
},
});
}
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-gpt-pika-${process.pid}-${Date.now()}.yaml`;
const tempInputPath = `/tmp/unidesk-agentrun-gpt-pika-input-${process.pid}-${Date.now()}.json`;
try {
process.env.AGENTRUN_CLIENT_CONFIG = tempConfigPath;
await Bun.write(tempConfigPath, agentRunMinimalClientYaml.replace("http://agentrun.example.local:8080", server.url.href.replace(/\/$/u, "")));
const defaultPlan = JSON.parse(renderedTextOf(await runAgentRunCommand(null, [
"create", "task", "--aipod", "Artificer", "--prompt", "default fixture", "--dry-run", "-o", "json",
]))) as Record<string, any>;
expect(defaultPlan.aipodBinding).toMatchObject({
sourceProfile: "gpt.pika",
effectiveBackendProfile: "gpt-pika",
backendProfileSource: "aipod-spec",
model: "gpt-5.6-terra",
reasoningEffort: "xhigh",
modelSource: "aipod-spec-default",
reasoningEffortSource: "aipod-spec-default",
modelOverridden: false,
reasoningEffortOverridden: false,
valuesPrinted: false,
});
expect(defaultPlan.aipodBinding.providerCredentials).toEqual([{
profile: "gpt-pika",
secretRef: { name: "agentrun-v02-provider-gpt-pika", keys: ["auth.json", "config.toml"] },
valuesPrinted: false,
}]);
expect(renderBodies[0]).toEqual({ prompt: "default fixture" });
const override = await runAgentRunCommand(null, [
"create", "task", "--aipod", "Artificer", "--prompt", "override fixture",
"--model", "gpt-5.6-terra-alt", "--reasoning-effort", "xhigh", "-o", "json",
]);
expect(override.ok).toBe(true);
expect(renderBodies[1]).toMatchObject({
prompt: "override fixture",
model: "gpt-5.6-terra-alt",
reasoningEffort: "xhigh",
});
expect(renderBodies[1]?.backendProfile).toBeUndefined();
expect(renderBodies[1]?.executionPolicy).toBeUndefined();
expect(submittedBodies[0]).toMatchObject({
backendProfile: "gpt-pika",
payload: { model: "gpt-5.6-terra-alt", reasoningEffort: "xhigh" },
executionPolicy: {
secretScope: {
providerCredentials: [{
profile: "gpt-pika",
secretRef: { name: "agentrun-v02-provider-gpt-pika", keys: ["auth.json", "config.toml"] },
}],
},
},
});
const send = await runAgentRunCommand(null, [
"send", "session/ses_gpt_pika", "--aipod", "Artificer", "--prompt", "send fixture",
"--model", "gpt-5.6-terra-send", "--reasoning-effort", "medium", "--dry-run", "-o", "json",
]);
expect(send.ok).toBe(true);
const sendProjection = JSON.parse(renderedTextOf(send)) as Record<string, any>;
expect(sendProjection.aipodBinding).toMatchObject({
sourceProfile: "gpt.pika",
effectiveBackendProfile: "gpt-pika",
model: "gpt-5.6-terra-send",
reasoningEffort: "medium",
modelSource: "render-input-override",
reasoningEffortSource: "render-input-override",
modelOverridden: true,
reasoningEffortOverridden: true,
valuesPrinted: false,
});
expect(renderBodies[2]).toMatchObject({ model: "gpt-5.6-terra-send", reasoningEffort: "medium" });
expect(sendBodies[0]).toMatchObject({
dryRun: true,
run: { backendProfile: "gpt-pika" },
payload: { model: "gpt-5.6-terra-send", reasoningEffort: "medium" },
});
const render = await runAgentRunCommand(null, [
"aipod-specs", "render", "Artificer", "--model", "gpt-5.6-terra-render", "--reasoning-effort", "low", "-o", "json",
]);
expect(render.ok).toBe(true);
expect(renderBodies[3]).toEqual({ model: "gpt-5.6-terra-render", reasoningEffort: "low" });
const createHuman = renderedTextOf(await runAgentRunCommand(null, [
"create", "task", "--aipod", "Artificer", "--prompt", "human create", "--dry-run",
]));
const sendHuman = renderedTextOf(await runAgentRunCommand(null, [
"send", "session/ses_gpt_pika", "--aipod", "Artificer", "--prompt", "human send",
"--model", "gpt-5.6-terra-human", "--reasoning-effort", "medium", "--dry-run",
]));
const renderHuman = renderedTextOf(await runAgentRunCommand(null, [
"aipod-specs", "render", "Artificer", "--model", "gpt-5.6-terra-human", "--reasoning-effort", "medium",
]));
for (const human of [createHuman, sendHuman, renderHuman]) {
expect(human).toContain("sourceProfile=gpt.pika");
expect(human).toContain("effectiveBackendProfile=gpt-pika");
expect(human).toContain("model=");
expect(human).toContain("reasoningEffort=");
expect(human).toContain("valuesPrinted=false");
expect(human).not.toContain("secret-value");
}
expect(createHuman).toContain("modelSource=aipod-spec-default");
expect(sendHuman).toContain("modelSource=render-input-override");
expect(renderHuman).toContain("modelSource=render-input-override");
for (const helpArgs of [["create", "--help"], ["send", "--help"], ["aipod-specs", "render", "--help"]]) {
const help = renderedTextOf(await runAgentRunCommand(null, helpArgs));
expect(help).toContain("--model <name>");
expect(help).toContain("--reasoning-effort <level>");
}
resolutionModelOverride = "gpt-5.6-terra-conflict";
const resolutionRejected = await runAgentRunCommand(null, [
"create", "task", "--aipod", "Artificer", "--prompt", "resolution conflict", "-o", "json",
]);
expect(resolutionRejected.ok).toBe(false);
expect(renderedTextOf(resolutionRejected)).toContain("queueTask.payload.model must match modelResolution.model");
expect(submittedBodies).toHaveLength(1);
resolutionModelOverride = null;
const requestCountBeforeRejections = renderBodies.length;
for (const forbidden of [
["--backend-profile", "gpt-pika"],
["--profile", "gpt-pika"],
["--execution-policy-json", "{}"],
]) {
const rejected = await runAgentRunCommand(null, [
"create", "task", "--aipod", "Artificer", "--prompt", "rejected", ...forbidden, "-o", "json",
]);
expect(rejected.ok).toBe(false);
expect(renderedTextOf(rejected)).toContain("AipodSpec owns backendProfile");
}
await Bun.write(tempInputPath, JSON.stringify({ payload: { prompt: "nested", model: "gpt-5.6-terra-nested" } }));
const nestedRejected = await runAgentRunCommand(null, [
"create", "task", "--aipod", "Artificer", "--json-file", tempInputPath, "-o", "json",
]);
expect(nestedRejected.ok).toBe(false);
expect(renderedTextOf(nestedRejected)).toContain("payload.model");
await Bun.write(tempInputPath, JSON.stringify({ modelConfig: { model: "gpt-5.6-terra-nested" } }));
const modelConfigRejected = await runAgentRunCommand(null, [
"create", "task", "--aipod", "Artificer", "--json-file", tempInputPath, "-o", "json",
]);
expect(modelConfigRejected.ok).toBe(false);
expect(renderedTextOf(modelConfigRejected)).toContain("modelConfig");
expect(renderBodies).toHaveLength(requestCountBeforeRejections);
renderedBackendProfile = "gpt.pika";
renderedCredentialProfile = "gpt.pika";
const dottedRejected = await runAgentRunCommand(null, [
"create", "task", "--aipod", "Artificer", "--prompt", "dotted", "-o", "json",
]);
expect(dottedRejected.ok).toBe(false);
expect(renderedTextOf(dottedRejected)).toContain("backendProfile=gpt.pika");
expect(submittedBodies).toHaveLength(1);
renderedBackendProfile = "gpt-pika";
renderedCredentialProfile = "gpt-pika";
renderedSecretName = "agentrun-v01-provider-codex";
const secretRejected = await runAgentRunCommand(null, [
"create", "task", "--aipod", "Artificer", "--prompt", "wrong secret", "-o", "json",
]);
expect(secretRejected.ok).toBe(false);
expect(renderedTextOf(secretRejected)).toContain("is not the unique YAML binding");
expect(submittedBodies).toHaveLength(1);
} 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 });
rmSync(tempInputPath, { force: true });
}
});
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>[] = [];
@@ -1460,40 +1081,8 @@ describe("AgentRun YAML tool credential binding", () => {
toolCredentials: renderedToolCredentials(),
},
},
payload: {
prompt: "fixture",
model: "gpt-5.5",
reasoningEffort: "high",
modelConfig: {
model: "gpt-5.5",
reasoningEffort: "high",
modelSource: "aipod-spec-default",
reasoningEffortSource: "aipod-spec-default",
valuesPrinted: false,
},
},
payload: { prompt: "fixture" },
});
const modelResolution = {
model: "gpt-5.5",
reasoningEffort: "high",
modelSource: "aipod-spec-default",
reasoningEffortSource: "aipod-spec-default",
modelOverridden: false,
reasoningEffortOverridden: false,
backendProfile: "codex",
backendProfileSource: "aipod-spec",
providerCredential: {
profile: "codex",
secretRef: {
namespace: "agentrun-v02",
name: "agentrun-v01-provider-codex",
keys: ["auth.json", "config.toml"],
valuesPrinted: false,
},
valuesPrinted: false,
},
valuesPrinted: false,
};
const server = Bun.serve({
port: 0,
async fetch(request) {
@@ -1501,7 +1090,7 @@ describe("AgentRun YAML tool credential binding", () => {
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(), modelResolution } });
return Response.json({ ok: true, data: { queueTask: renderedTask() } });
}
if (url.pathname === "/api/v1/queue/tasks") {
submittedBodies.push(await request.json() as Record<string, any>);
@@ -1518,54 +1107,22 @@ describe("AgentRun YAML tool credential binding", () => {
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"]);
const accepted = await runAgentRunCommand(null, ["create", "task", "--aipod", "Artificer", "--prompt", "fixture", "--model", "gpt-explicit", "--reasoning-effort", "high", "-o", "json"]);
expect(accepted.ok).toBe(true);
expect(renderBodies).toHaveLength(1);
expect(submittedBodies).toHaveLength(1);
expect(renderBodies[0]?.backendProfile).toBeUndefined();
expect(renderBodies[0]?.executionPolicy).toBeUndefined();
expect(submittedBodies[0]?.executionPolicy?.secretScope?.toolCredentials?.map((credential: Record<string, unknown>) => `${credential.tool}/${credential.purpose}`)).toEqual([
"github/github-pr",
"unidesk-ssh/ssh-passthrough",
"github/github-ssh",
]);
const githubSsh = submittedBodies[0]?.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" },
}]);
expect(renderBodies[0]?.model).toBe("gpt-explicit");
expect(renderBodies[0]?.reasoningEffort).toBe("high");
expect(submittedBodies[0]).toEqual(renderedTask());
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(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);
expect(submittedBodies[1]).toEqual(renderedTask());
} finally {
server.stop(true);
if (previousConfig === undefined) delete process.env.AGENTRUN_CLIENT_CONFIG;