From ea3a6d7e6fba0069fb01b216c4b92b5f583c6005 Mon Sep 17 00:00:00 2001 From: Dotta Date: Tue, 6 Oct 2026 13:10:25 -0500 Subject: [PATCH] fix: preserve Codex shell exclusions for configured secrets Co-Authored-By: Paperclip --- doc/AGENT-IDENTITY.md | 2 ++ .../src/acpx-engine/execute.test.ts | 5 +++-- .../adapter-utils/src/acpx-engine/execute.ts | 4 +++- .../codex-local/src/server/execute.test.ts | 5 +++-- .../codex-local/src/server/execute.ts | 7 ++++--- .../src/drivers/acpx/runtime-sandbox.test.ts | 20 +++++++++++++++++++ .../src/drivers/acpx/runtime-sandbox.ts | 4 +++- 7 files changed, 38 insertions(+), 9 deletions(-) diff --git a/doc/AGENT-IDENTITY.md b/doc/AGENT-IDENTITY.md index bc4d081543f..f8dabb3c54a 100644 --- a/doc/AGENT-IDENTITY.md +++ b/doc/AGENT-IDENTITY.md @@ -96,3 +96,5 @@ Do not use a disaster-recovery backup as a development clone when agents should have distinct identities. Native failure records and reports redact the assigned key before truncating diagnostics. Streaming output buffers settle at item or turn completion: short structural prefixes (such as a trailing dash) are preserved, longer interrupted key fragments become redaction markers, and pending buffers are cleared. Terminal events carry these settled `outputTails`; transcript projection displays them as deltas without changing the source event receipt. + +Codex shell delivery preserves its default `KEY`/`SECRET`/`TOKEN` name exclusions for other configured credentials. The exceptions are the three identity variables and `PAPERCLIP_API_KEY`: the latter is the short-lived, scoped run/bridge credential required by the Paperclip agent skill’s Bash/curl API calls. Disabling Codex’s automatic exclusions is paired with this explicit filtered allowlist; it does not admit arbitrary host environment variables. Provider authentication secrets can still reach the provider process without being newly exposed to shell commands by this feature. diff --git a/packages/adapter-utils/src/acpx-engine/execute.test.ts b/packages/adapter-utils/src/acpx-engine/execute.test.ts index 491eda13b17..faa2b6e7a0d 100644 --- a/packages/adapter-utils/src/acpx-engine/execute.test.ts +++ b/packages/adapter-utils/src/acpx-engine/execute.test.ts @@ -550,15 +550,16 @@ describe("shared ACPX engine runtime behavior", () => { ); }); - it("keeps assigned API and tool credentials in the identity-enabled Codex shell environment", async () => { + it("keeps identity and scoped API access without exposing configured service tokens to Codex shells", async () => { const { meta } = await runExecutor({ agent: "codex", env: { MY_SERVICE_TOKEN: "assigned-tool-token" } }, { authToken: "assigned-run-token", agentIdentity: { keyId: "sha256:test", publicKeyPem: "public", privateKeyPem: "private" }, }); const config = JSON.parse(String((meta[0]?.env as Record).CODEX_CONFIG)); expect(config.shell_environment_policy.include_only).toEqual(expect.arrayContaining([ - "PAPERCLIP_API_KEY", "MY_SERVICE_TOKEN", "PAPERCLIP_AGENT_PRIVATE_KEY", + "PAPERCLIP_API_KEY", "PAPERCLIP_AGENT_PRIVATE_KEY", ])); + expect(config.shell_environment_policy.include_only).not.toContain("MY_SERVICE_TOKEN"); expect(JSON.stringify(config)).not.toContain("assigned-run-token"); expect(JSON.stringify(config)).not.toContain("assigned-tool-token"); }); diff --git a/packages/adapter-utils/src/acpx-engine/execute.ts b/packages/adapter-utils/src/acpx-engine/execute.ts index a777a9b3867..725d733ab6c 100644 --- a/packages/adapter-utils/src/acpx-engine/execute.ts +++ b/packages/adapter-utils/src/acpx-engine/execute.ts @@ -2051,7 +2051,9 @@ async function buildRuntime(input: { ...(input.ctx.agentIdentity ? { identityEnvironmentKeys: [...new Set([ "PATH", "HOME", "LANG", "TMPDIR", "CODEX_HOME", "PAPERCLIP_AGENT_KEY_ID", "PAPERCLIP_AGENT_PUBLIC_KEY", "PAPERCLIP_AGENT_PRIVATE_KEY", - ...Object.keys(env), + // Preserve Codex's default secret-name exclusions. The short-lived + // Paperclip API token is required by the agent skill's Bash/curl calls. + ...Object.keys(env).filter(key => key === "PAPERCLIP_API_KEY" || !/key|secret|token/i.test(key)), ])] } : {}), requestedModel, requestedThinkingEffort, diff --git a/packages/adapters/codex-local/src/server/execute.test.ts b/packages/adapters/codex-local/src/server/execute.test.ts index 27da091b25c..7a1775c81cf 100644 --- a/packages/adapters/codex-local/src/server/execute.test.ts +++ b/packages/adapters/codex-local/src/server/execute.test.ts @@ -190,15 +190,16 @@ describe("codex execute — outbound auth copy-back restore contribution", () => }; } - it("keeps assigned API and tool credentials in the identity-enabled CLI shell", async () => { + it("keeps identity and scoped API access without exposing configured service tokens to CLI shells", async () => { vi.stubEnv("DATABASE_URL", "postgres://operator:host-password@host/private"); vi.stubEnv("HOST_DATABASE_PASSWORD", "host-password"); const { commandArgs } = await runTeardown({ sandboxAuth: "{}", hostAuth: "{}", withIdentity: true }); const policy = commandArgs.find(arg => arg.startsWith("shell_environment_policy.include_only=")); const keys = JSON.parse(policy!.slice(policy!.indexOf("=") + 1)); expect(keys).toEqual(expect.arrayContaining([ - "PAPERCLIP_API_KEY", "MY_SERVICE_TOKEN", "PAPERCLIP_AGENT_PRIVATE_KEY", + "PAPERCLIP_API_KEY", "PAPERCLIP_AGENT_PRIVATE_KEY", ])); + expect(keys).not.toContain("MY_SERVICE_TOKEN"); expect(keys).not.toContain("DATABASE_URL"); expect(keys).not.toContain("HOST_DATABASE_PASSWORD"); expect(commandArgs.join(" ")).not.toContain("assigned-run-token"); diff --git a/packages/adapters/codex-local/src/server/execute.ts b/packages/adapters/codex-local/src/server/execute.ts index 63c00368382..78e6594c519 100644 --- a/packages/adapters/codex-local/src/server/execute.ts +++ b/packages/adapters/codex-local/src/server/execute.ts @@ -1232,9 +1232,10 @@ export async function execute(ctx: AdapterExecutionContext): Promise key === "PAPERCLIP_API_KEY" || !/key|secret|token/i.test(key)), + ...identityNames, ])]; args.unshift("-c", "features.shell_snapshot=false", "-c", 'shell_environment_policy.inherit="all"', "-c", "shell_environment_policy.ignore_default_excludes=true", "-c", `shell_environment_policy.include_only=${JSON.stringify(shellKeys)}`); diff --git a/packages/paperclip-runner/src/drivers/acpx/runtime-sandbox.test.ts b/packages/paperclip-runner/src/drivers/acpx/runtime-sandbox.test.ts index fb80619a55d..53423467c92 100644 --- a/packages/paperclip-runner/src/drivers/acpx/runtime-sandbox.test.ts +++ b/packages/paperclip-runner/src/drivers/acpx/runtime-sandbox.test.ts @@ -52,6 +52,26 @@ describe("ACPX runtime sandbox", () => { expect((await stat(requirementsPath)).mode & 0o777).toBe(0o600); }); + it("keeps provider credentials outside identity-enabled Codex shell commands", async () => { + const fixture = await sandboxFixture("codex"); + const sandbox = await prepareAcpxRuntimeSandbox({ + binding: fixture.binding, agent: "codex", environment: { + PATH: process.env.PATH, + OPENAI_API_KEY: "provider-secret", MY_SERVICE_TOKEN: "configured-secret", + PAPERCLIP_API_KEY: "scoped-run-token", + PAPERCLIP_AGENT_KEY_ID: "sha256:test", PAPERCLIP_AGENT_PUBLIC_KEY: "public-identity", + PAPERCLIP_AGENT_PRIVATE_KEY: "private-identity", + }, + }); + const config = await readFile(join(sandbox.agentHomeDirectory, "config.toml"), "utf8"); + const keys = JSON.parse(config.split("\n").find(line => line.startsWith("include_only = "))!.slice("include_only = ".length)); + expect(keys).toEqual(expect.arrayContaining(["PAPERCLIP_API_KEY", "PAPERCLIP_AGENT_PRIVATE_KEY"])); + expect(keys).not.toContain("OPENAI_API_KEY"); + expect(keys).not.toContain("MY_SERVICE_TOKEN"); + expect(sandbox.launchEnvironment.OPENAI_API_KEY).toBe("provider-secret"); + for (const value of ["provider-secret", "configured-secret", "scoped-run-token", "private-identity"]) expect(config).not.toContain(value); + }); + it.each(["claude-sonnet-5", "sonnet", "custom-deployment-id"])( "preserves the requested Claude model %s in its isolated settings", async (model) => { diff --git a/packages/paperclip-runner/src/drivers/acpx/runtime-sandbox.ts b/packages/paperclip-runner/src/drivers/acpx/runtime-sandbox.ts index 78e6bdb3d9b..11df4137364 100644 --- a/packages/paperclip-runner/src/drivers/acpx/runtime-sandbox.ts +++ b/packages/paperclip-runner/src/drivers/acpx/runtime-sandbox.ts @@ -458,7 +458,9 @@ export async function prepareAcpxRuntimeSandbox(input: { `include_only = ${JSON.stringify([...new Set([ "PATH", "HOME", "LANG", "LANGUAGE", "TZ", "TMPDIR", "TEMP", "TMP", "CODEX_HOME", "PAPERCLIP_AGENT_KEY_ID", "PAPERCLIP_AGENT_PUBLIC_KEY", "PAPERCLIP_AGENT_PRIVATE_KEY", - ...Object.keys(input.environment), + // Provider/config secrets keep Codex's default shell exclusions; + // only Paperclip's scoped API token is required by Bash/curl skills. + ...Object.keys(input.environment).filter(key => key === "PAPERCLIP_API_KEY" || !/key|secret|token/i.test(key)), ])])}`, ] : []), "",