From d5905a5489d9adf6598eceb4d5910cd33964b8db Mon Sep 17 00:00:00 2001 From: Dotta Date: Wed, 23 Sep 2026 06:56:13 -0700 Subject: [PATCH] fix(acpx): reject invalid credential environment callbacks Co-Authored-By: Paperclip --- docker/daytona-runner/Dockerfile | 2 +- .../test/acpx-codex-package-contract.test.mjs | 22 ++++++++++++ patches/acpx@0.13.1.patch | 35 ++++++++++--------- 3 files changed, 42 insertions(+), 17 deletions(-) diff --git a/docker/daytona-runner/Dockerfile b/docker/daytona-runner/Dockerfile index 90bc73adc3d..e3a5fa795df 100644 --- a/docker/daytona-runner/Dockerfile +++ b/docker/daytona-runner/Dockerfile @@ -28,7 +28,7 @@ COPY cli/package.json ./cli/package.json # The complete resolved lock (including transitive integrity hashes) is reviewed. # Reject registry-time drift BEFORE installing packages or running lifecycle code. # Refresh this digest together with source/provider dependency changes. -ARG PAPERCLIP_RUNNER_LOCK_SHA256=a297efff4b2dc0db5d8680793c1194efd2d8d3643f6935ab40d659b3fa34542e +ARG PAPERCLIP_RUNNER_LOCK_SHA256=0c06ab38c1d4b6b58e1c9f5ee85eb79f97cecd83e751b5d27c3b262faf5ab265 RUN printf '%s pnpm-lock.yaml\n' "${PAPERCLIP_RUNNER_LOCK_SHA256}" > /tmp/provider-lock.sha256 \ && sha256sum -c /tmp/provider-lock.sha256 \ && pnpm install --frozen-lockfile --filter '@paperclipai/paperclip-runner...' diff --git a/packages/paperclip-runner/test/acpx-codex-package-contract.test.mjs b/packages/paperclip-runner/test/acpx-codex-package-contract.test.mjs index 14e6018a8ba..3868dda6c5e 100644 --- a/packages/paperclip-runner/test/acpx-codex-package-contract.test.mjs +++ b/packages/paperclip-runner/test/acpx-codex-package-contract.test.mjs @@ -178,6 +178,28 @@ test("the ACPX patch fails closed on an invalid spawn environment", () => { ); }); +test("authentication rejects invalid isolated environments without host fallback", () => { + const addedSource = acpxPatch.split("\n") + .filter((line) => line.startsWith("+") && !line.startsWith("+++")) + .map((line) => line.slice(1)).join("\n"); + const start = addedSource.indexOf("function isPlainStringEnvironment(value)"); + const end = addedSource.indexOf("function buildAgentEnvironment(", start); + assert.ok(start >= 0 && end > start); + const hostEnvironment = { XAI_API_KEY: "host-credential-must-not-leak" }; + const resolveEnvironment = new Function( + "process", `${addedSource.slice(start, end)}; return resolveAgentEnvironment;`, + )({ env: hostEnvironment }); + for (const invalid of [undefined, null, [], "invalid", { XAI_API_KEY: 1 }]) { + assert.throws(() => resolveEnvironment(() => invalid), TypeError); + } + const isolated = {}; + assert.equal(resolveEnvironment(() => isolated), isolated); + assert.equal(resolveEnvironment(() => isolated).XAI_API_KEY, undefined); + assert.equal(resolveEnvironment(undefined), hostEnvironment); + assert.match(addedSource, /readEnvCredential\(method\.id, resolveAgentEnvironment\(this\.options\.spawnEnvironment\)\)/); + assert.match(addedSource, /resolveAgentEnvironment\(this\.options\.spawnEnvironment\)\)\.XAI_API_KEY/); +}); + test("the Codex patch enforces isolated instructions, tools, and skills", () => { for (const token of [ "PAPERCLIP_ACPX_ISOLATED_CONTEXT", diff --git a/patches/acpx@0.13.1.patch b/patches/acpx@0.13.1.patch index 7c8350f1e2c..cd662aba257 100644 --- a/patches/acpx@0.13.1.patch +++ b/patches/acpx@0.13.1.patch @@ -52,7 +52,7 @@ index d454fd7..991d21d 100644 if (typeof value === "string" && value.trim().length > 0) return value; } function protectedEnvKey(key) { -@@ -3135,8 +3137,18 @@ function promotePrefixedAuthEnvironment(env) { +@@ -3135,8 +3137,21 @@ function promotePrefixedAuthEnvironment(env) { } return protectedKeys; } @@ -63,17 +63,20 @@ index d454fd7..991d21d 100644 + const prototype = Object.getPrototypeOf(value); + return (prototype === Object.prototype || prototype === null) && Object.values(value).every((entry) => typeof entry === "string"); +} -+function buildAgentEnvironment(authCredentials, sessionEnv, spawnEnvironment) { ++function resolveAgentEnvironment(spawnEnvironment) { + let sourceEnvironment = process.env; + if (spawnEnvironment !== void 0) { + sourceEnvironment = spawnEnvironment(); + if (!isPlainStringEnvironment(sourceEnvironment)) throw new TypeError("ACPX spawn environment must be a plain record of string values"); + } -+ const env = { ...sourceEnvironment }; ++ return sourceEnvironment; ++} ++function buildAgentEnvironment(authCredentials, sessionEnv, spawnEnvironment) { ++ const env = { ...resolveAgentEnvironment(spawnEnvironment) }; const protectedAuthEnvKeys = promotePrefixedAuthEnvironment(env); if (authCredentials) for (const [methodId, credential] of Object.entries(authCredentials)) { addAuthCredentialEnvKeys(protectedAuthEnvKeys, methodId, credential); -@@ -3178,10 +3190,10 @@ function resolveConfiguredAuthCredential(methodId, authCredentials) { +@@ -3178,10 +3193,10 @@ function resolveConfiguredAuthCredential(methodId, authCredentials) { const configCredentials = authCredentials ?? {}; return configCredentials[methodId] ?? configCredentials[toEnvToken(methodId)]; } @@ -86,7 +89,7 @@ index d454fd7..991d21d 100644 stdio: [ "pipe", "pipe", -@@ -3961,10 +3973,13 @@ function resolveClientCapabilities(params) { +@@ -3961,10 +3976,13 @@ function resolveClientCapabilities(params) { ...params.elicitationModes.includes("url") ? { url: {} } : {} } } : {} }; @@ -102,7 +105,7 @@ index d454fd7..991d21d 100644 }; } function hasResponseField(response, field) { -@@ -4216,9 +4231,21 @@ var AcpClient = class { +@@ -4216,9 +4234,21 @@ var AcpClient = class { this.lastAgentExit = void 0; this.lastKnownPid = child.pid ?? void 0; this.attachAgentLifecycleObservers(child); @@ -124,7 +127,7 @@ index d454fd7..991d21d 100644 if (!this.options.verbose) return; process.stderr.write(chunk); }); -@@ -4253,7 +4280,12 @@ var AcpClient = class { +@@ -4253,7 +4283,12 @@ var AcpClient = class { geminiAcp: isGeminiAcpCommand(spawnCommand, args), copilotAcp: isCopilotAcpCommand(spawnCommand, args), claudeAcp: isClaudeAcpCommand(spawnCommand, args), @@ -138,7 +141,7 @@ index d454fd7..991d21d 100644 }; } logAgentLaunch(plan) { -@@ -4279,10 +4311,17 @@ var AcpClient = class { +@@ -4279,10 +4314,17 @@ var AcpClient = class { } async spawnAgentProcess(plan) { const spawnCommand = buildAgentSpawnCommand(plan.spawnCommand, plan.args, process.platform, plan.spawnOptions.env); @@ -158,7 +161,7 @@ index d454fd7..991d21d 100644 try { await waitForSpawn$1(spawnedChild); } catch (error) { -@@ -4422,7 +4461,10 @@ var AcpClient = class { +@@ -4422,7 +4464,10 @@ var AcpClient = class { const createPromise = this.runConnectionRequest(() => connection.newSession({ cwd: sessionCwd, mcpServers: this.options.mcpServers ?? [], @@ -170,25 +173,25 @@ index d454fd7..991d21d 100644 })); result = claudeAcp ? await withTimeout(createPromise, resolveClaudeAcpSessionCreateTimeoutMs()) : await createPromise; } catch (error) { -@@ -4864,7 +4906,7 @@ var AcpClient = class { +@@ -4864,7 +4909,7 @@ var AcpClient = class { } selectAuthMethod(methods) { for (const method of methods) { - const envCredential = readEnvCredential(method.id); -+ const envCredential = readEnvCredential(method.id, this.options.spawnEnvironment?.() ?? process.env); ++ const envCredential = readEnvCredential(method.id, resolveAgentEnvironment(this.options.spawnEnvironment)); if (envCredential) return { methodId: method.id, credential: envCredential, -@@ -4890,7 +4932,7 @@ var AcpClient = class { +@@ -4890,7 +4935,7 @@ var AcpClient = class { } readAgentSpecificEnvCredential(methodId) { if (!this.isGrokBuildAcpCommand() || methodId !== "xai.api_key") return; - const value = process.env.XAI_API_KEY; -+ const value = (this.options.spawnEnvironment?.() ?? process.env).XAI_API_KEY; ++ const value = (resolveAgentEnvironment(this.options.spawnEnvironment)).XAI_API_KEY; return typeof value === "string" && value.trim().length > 0 ? value : void 0; } selectAgentManagedAuthMethod(methodId) { -@@ -5028,6 +5070,12 @@ var AcpClient = class { +@@ -5028,6 +5073,12 @@ var AcpClient = class { attachAgentLifecycleObservers(child) { child.once("exit", (exitCode, signal) => { this.recordAgentExit("process_exit", exitCode, signal); @@ -201,7 +204,7 @@ index d454fd7..991d21d 100644 }); child.once("close", (exitCode, signal) => { this.recordAgentExit("process_close", exitCode, signal); -@@ -6435,6 +6483,27 @@ async function withConnectedSession(options) { +@@ -6435,6 +6486,27 @@ async function withConnectedSession(options) { //#region src/runtime/engine/prompt-turn.ts const SESSION_REPLY_IDLE_MS = 1e3; const SESSION_REPLY_DRAIN_TIMEOUT_MS = 5e3; @@ -229,7 +232,7 @@ index d454fd7..991d21d 100644 async function runPromptTurn(params) { try { const promptPromise = params.client.prompt(params.sessionId, params.prompt, params.onPromptRequestStarted, params.onElicitation); -@@ -6444,6 +6513,19 @@ async function runPromptTurn(params) { +@@ -6444,6 +6516,19 @@ async function runPromptTurn(params) { idleMs: SESSION_REPLY_IDLE_MS, timeoutMs: SESSION_REPLY_DRAIN_TIMEOUT_MS }).catch(() => {});