diff --git a/packages/paperclip-runner/src/drivers/opencode/opencode-server-driver.test.ts b/packages/paperclip-runner/src/drivers/opencode/opencode-server-driver.test.ts index 89bb57f7aec..ddd01a4982e 100644 --- a/packages/paperclip-runner/src/drivers/opencode/opencode-server-driver.test.ts +++ b/packages/paperclip-runner/src/drivers/opencode/opencode-server-driver.test.ts @@ -2262,16 +2262,20 @@ describe("OpenCodeServerDriver", () => { const exitingFixture = join(root, "exit-before-health.mjs"); await writeFile( exitingFixture, - "#!/usr/bin/env node\nprocess.stderr.write(`credential=${process.env.OPENROUTER_API_KEY}\\nauthorization=super-secret-opencode-token\\n`);\nprocess.exit(17);\n", + "#!/usr/bin/env node\nimport { readFileSync } from 'node:fs';\nconst config = JSON.parse(readFileSync(`${process.env.XDG_CONFIG_HOME}/opencode/opencode.json`, 'utf8'));\nprocess.stderr.write(`credential=${process.env.OPENROUTER_API_KEY}\\ngateway=${config.provider.paperclip.options.apiKey}\\nauthorization=super-secret-opencode-token\\n`);\nprocess.exit(17);\n", { mode: 0o755 }, ); + const diagnostics: string[] = []; const driver = new OpenCodeServerDriver({ - model: "openrouter/deepseek/deepseek-v4-flash-0731", + model: "paperclip/team/model-alias", runtimeDirectory: root, command: exitingFixture, + onDiagnostic: (message) => { diagnostics.push(message); }, environment: { PATH: process.env.PATH, OPENROUTER_API_KEY: "fixture-key", + PAPERCLIP_AI_PROVIDER_KEY: "fixture-custom-gateway-key", + PAPERCLIP_AI_PROVIDER_URL: "https://gateway.example/v1", }, }); const error = await driver @@ -2289,6 +2293,41 @@ describe("OpenCodeServerDriver", () => { expect(error).toContain("stage=health"); expect(error).toContain("[REDACTED]"); expect(error).not.toContain("fixture-key"); + expect(error).not.toContain("fixture-custom-gateway-key"); expect(error).not.toContain("super-secret-opencode-token"); + expect(diagnostics.join("")).toContain("gateway=[REDACTED]"); + expect(diagnostics.join("")).not.toContain("fixture-custom-gateway-key"); + }); + + it.each(["network", "response"])("redacts custom gateway keys in %s errors", async (failure) => { + await chmod(fixture, 0o755); + const root = await mkdtemp(join(tmpdir(), "paperclip-opencode-driver-")); + const workspace = await mkdtemp(join(tmpdir(), "paperclip-opencode-workspace-")); + roots.push(root, workspace); + const key = "fixture-custom-gateway-key"; + const driver = new OpenCodeServerDriver({ + model: "paperclip/team/model-alias", + runtimeDirectory: root, + command: fixture, + environment: { + PATH: process.env.PATH, + PAPERCLIP_AI_PROVIDER_KEY: key, + PAPERCLIP_AI_PROVIDER_URL: "https://gateway.example/v1", + }, + fetch: async (input, init) => { + if (String(input).endsWith("/session") && init?.method === "POST") { + if (failure === "network") throw new Error(`Gateway rejected ${key}`); + return new Response(`Gateway rejected ${key}`, { status: 400 }); + } + return fetch(input, init); + }, + }); + const error = await driver.openSession({ + runId: "run-gateway-error", + normalizedSessionId: "gateway-error", + workingDirectory: workspace, + }).then(() => "provider unexpectedly started", (cause: unknown) => String(cause)); + expect(error).toContain("Gateway rejected [REDACTED]"); + expect(error).not.toContain(key); }); }); diff --git a/packages/paperclip-runner/src/drivers/opencode/opencode-server-driver.ts b/packages/paperclip-runner/src/drivers/opencode/opencode-server-driver.ts index 4a68460e053..ed4fc96b5a3 100644 --- a/packages/paperclip-runner/src/drivers/opencode/opencode-server-driver.ts +++ b/packages/paperclip-runner/src/drivers/opencode/opencode-server-driver.ts @@ -254,6 +254,7 @@ export class OpenCodeServerDriver implements HarnessDriver { recovered: false, reason: redact(String(error), [ this.#options.environment?.OPENROUTER_API_KEY, + this.#options.environment?.PAPERCLIP_AI_PROVIDER_KEY, ]), }; } @@ -1970,14 +1971,15 @@ async function startRuntime(input: { const assignedMcp = nativeMcpLaunchBinding( input.options.environment ?? process.env, ); - input.trace?.addSensitiveValues([ + const sensitiveValues = [ password, authHeader, bridge.secret, assignedMcp?.token, input.options.environment?.OPENROUTER_API_KEY, input.options.environment?.PAPERCLIP_AI_PROVIDER_KEY, - ]); + ].filter((value): value is string => Boolean(value)); + input.trace?.addSensitiveValues(sensitiveValues); const instructionRoot = input.options.runtimeContext?.instructions.bundle.rootPath; const [modelProvider, ...modelIdParts] = input.options.model.split("/"); @@ -2089,10 +2091,7 @@ async function startRuntime(input: { let diagnostics = ""; child.stderr?.on("data", (chunk) => { const raw = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)); - const redactedDiagnostic = redact(raw.toString("utf8"), [ - password, - input.options.environment?.OPENROUTER_API_KEY, - ]); + const redactedDiagnostic = redact(raw.toString("utf8"), sensitiveValues); diagnostics = `${diagnostics}${redactedDiagnostic}`.slice(-8_192); const frameId = input.trace?.frame({ direction: "provider_stderr", @@ -2155,10 +2154,7 @@ async function startRuntime(input: { process: child, bridge, trace: input.trace, - sensitiveValues: [ - password, - input.options.environment?.OPENROUTER_API_KEY, - ].filter((value): value is string => Boolean(value)), + sensitiveValues, close: async (closeInput = {}) => { await bridge.close().catch(() => {}); if (child.exitCode === null && child.signalCode === null && child.pid) {