diff --git a/doc/AGENT-IDENTITY.md b/doc/AGENT-IDENTITY.md index 0e379b905d7..8860a613f45 100644 --- a/doc/AGENT-IDENTITY.md +++ b/doc/AGENT-IDENTITY.md @@ -97,7 +97,23 @@ 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. +Legacy Codex shell delivery includes explicitly configured task variables, including +resolved secret references such as `SLACK_BOT_TOKEN`. Both ACP and CLI execution +use the same name-only allowlist, including runs without an agent identity. +Codex login keys (`OPENAI_API_KEY`, `CODEX_API_KEY`), managed provider keys, +observability credentials, and private runtime transport credentials remain +outside that service-secret opt-in. Unassigned host secrets are not added. +The three identity variables and the short-lived, scoped `PAPERCLIP_API_KEY` +retain their runtime delivery. Secret values remain in the child environment; +the shell policy contains only variable names. Removing a configured variable +removes its shell access at the next launch. + +Assigning a service credential through the adapter's environment grants the +run's shell commands access to that credential. It is not a brokered credential +that is hidden from the agent: a model-directed command can read it. Assign only +credentials the run is authorized to use, with the service permissions it needs. +The shell allowlist protects unassigned host and provider credentials; it does +not isolate an assigned service credential from the run's own commands. Cloud customer-success inspection can consume this existing identity together with strict active-run authority. See [inspection support](CUSTOMER-SUCCESS-INSPECTION.md). diff --git a/maps/agent-access-map/runs-adapters.md b/maps/agent-access-map/runs-adapters.md index 23d452382f4..209f2e61072 100644 --- a/maps/agent-access-map/runs-adapters.md +++ b/maps/agent-access-map/runs-adapters.md @@ -114,6 +114,16 @@ remaining operations before qualifying the whole capability. Gaps: Environment test path checks creation/configuration policy; missing provider auth must use its owner setup flow. +Scoped Legacy Codex shell delivery: resolved task configuration can explicitly +assign service-secret variables to shell tools through ACP or CLI execution. +The existing configuration and secret-access checks still authorize that binding; +the shell allowlist grants no extra company or vault access. Codex login keys, +observability credentials, private runtime transport credentials, and unassigned +host secrets retain their separate paths. Source/control evidence: +[shared shell policy](../../packages/adapter-utils/src/codex-shell-environment.ts) +and the adapter executor regressions. Live delivery remains a separate check +under `runs-adapters/validation`. + Scoped source review: [test authorization and resolution](../../server/src/routes/agents.ts) requires same-company `agents:create`, saved-agent update access when supplied, allowed environment selection, and normal secret/responsible-user checks. An diff --git a/maps/feature-map/runs-adapters.md b/maps/feature-map/runs-adapters.md index ef9b83201dc..181fb514009 100644 --- a/maps/feature-map/runs-adapters.md +++ b/maps/feature-map/runs-adapters.md @@ -56,6 +56,13 @@ models/authentication and unsupported Legacy-only options must fail visibly. Manual: Select the intended adapter and account, refresh model choices, and run its environment test. Save, reload, then execute a small task. Repeat with a deliberately unavailable account and verify the failure remains visible. +For Legacy Codex, assign a service secret such as `SLACK_BOT_TOKEN` through agent +configuration and run a task that checks its presence without printing its value. +Check both ACP and CLI execution. Remove the binding and repeat on the next run. +The assigned secret must reach shell tools; an unassigned host token and Codex's +own login keys must remain unavailable. Verify the same delivery without an +agent identity. Shell-policy metadata must contain names rather than values. + Choose Codex as the harness; the UI displays its name/icon for both runners. Advanced identifies the automatic default and offers Paperclip Runner or Legacy. New Codex agents default to the native runner on supported macOS or GNU/Linux diff --git a/packages/adapter-utils/src/acpx-engine/execute.test.ts b/packages/adapter-utils/src/acpx-engine/execute.test.ts index 81830e29a16..d11ca17b494 100644 --- a/packages/adapter-utils/src/acpx-engine/execute.test.ts +++ b/packages/adapter-utils/src/acpx-engine/execute.test.ts @@ -669,7 +669,7 @@ describe("shared ACPX engine runtime behavior", () => { fastMode: true, }); - expect(JSON.parse(String((meta[0]?.env as Record).CODEX_CONFIG))).toEqual({ + expect(JSON.parse(String((meta[0]?.env as Record).CODEX_CONFIG))).toMatchObject({ model: "gpt-5.6-sol", model_reasoning_effort: "high", service_tier: "fast", @@ -683,18 +683,33 @@ describe("shared ACPX engine runtime behavior", () => { ); }); - 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", "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"); + it.each([true, false])("delivers configured service tokens to Codex shells while isolating host and provider credentials (identity: %s)", async (withIdentity) => { + const root = await makeTempRoot(); + vi.stubEnv("PAPERCLIP_HOME", root); + vi.stubEnv("HOST_SERVICE_TOKEN", "host-token-canary"); + vi.stubEnv("DATABASE_URL", "postgres://host/private"); + try { + const { meta } = await runExecutor({ agent: "codex", stateDir: path.join(root, "state"), env: { + CODEX_HOME: path.join(root, "codex-home"), + SLACK_BOT_TOKEN: "assigned-tool-token", OPENAI_API_KEY: "provider-key-canary", + OTEL_EXPORTER_OTLP_HEADERS: "authorization=export-secret-canary", + } }, { + ...(withIdentity ? { + 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).toContain("SLACK_BOT_TOKEN"); + if (withIdentity) expect(config.shell_environment_policy.include_only).toEqual(expect.arrayContaining([ + "PAPERCLIP_API_KEY", "PAPERCLIP_AGENT_PRIVATE_KEY", + ])); + for (const key of ["HOST_SERVICE_TOKEN", "DATABASE_URL", "OPENAI_API_KEY", "OTEL_EXPORTER_OTLP_HEADERS"]) { + expect(config.shell_environment_policy.include_only).not.toContain(key); + } + expect(JSON.stringify(config)).not.toContain("assigned-run-token"); + expect(JSON.stringify(config)).not.toContain("assigned-tool-token"); + } finally { vi.unstubAllEnvs(); } }); it("forwards arbitrary Codex model IDs verbatim without picker-dependent session config", async () => { @@ -728,7 +743,7 @@ describe("shared ACPX engine runtime behavior", () => { }, }); - expect(JSON.parse(String((meta[0]?.env as Record).CODEX_CONFIG))).toEqual({ + expect(JSON.parse(String((meta[0]?.env as Record).CODEX_CONFIG))).toMatchObject({ model: "gpt-runtime", approval_policy: "never", service_tier: "fast", @@ -743,7 +758,7 @@ describe("shared ACPX engine runtime behavior", () => { env: { CODEX_CONFIG: "not-json" }, }); - expect(JSON.parse(String((meta[0]?.env as Record).CODEX_CONFIG))).toEqual({ + expect(JSON.parse(String((meta[0]?.env as Record).CODEX_CONFIG))).toMatchObject({ model: "gpt-runtime", }); expect(logs).toContainEqual({ @@ -771,10 +786,15 @@ describe("shared ACPX engine runtime behavior", () => { ]); }); - it("does not inject CODEX_CONFIG or session config when Codex overrides are absent", async () => { + it("bounds Codex shell inheritance even without model overrides or an agent identity", async () => { const { configOptions, meta } = await runExecutor({ agent: "codex" }); - expect((meta[0]?.env as Record).CODEX_CONFIG).toBeUndefined(); + const config = JSON.parse(String((meta[0]?.env as Record).CODEX_CONFIG)); + expect(config.model).toBeUndefined(); + expect(config.features.shell_snapshot).toBe(false); + expect(config.shell_environment_policy).toMatchObject({ inherit: "all", ignore_default_excludes: true }); + expect(config.shell_environment_policy.include_only).toContain("PATH"); + expect(config.shell_environment_policy.include_only).not.toContain("HOST_SERVICE_TOKEN"); expect(configOptions).toEqual([]); }); diff --git a/packages/adapter-utils/src/acpx-engine/execute.ts b/packages/adapter-utils/src/acpx-engine/execute.ts index 2065537d83a..b48c1414ca1 100644 --- a/packages/adapter-utils/src/acpx-engine/execute.ts +++ b/packages/adapter-utils/src/acpx-engine/execute.ts @@ -1,6 +1,7 @@ import { isOpenRouterUrl, openRouterClaudeHeaders } from "@paperclipai/shared/openrouter-attribution"; import { prepareAcpxCodexObservabilityHome } from "../codex-observability-home.js"; -import { AGENT_OBSERVABILITY_ENV_KEYS, applyObservabilityEnvironment } from "../agent-observability.js"; +import { codexShellEnvironmentKeys } from "../codex-shell-environment.js"; +import { applyObservabilityEnvironment } from "../agent-observability.js"; import { cancellableSandboxStartup } from "./startup-cancellation.js"; import { emitStartupReuse, environmentStartupReuse, summarizeStartupReuse, type StartupReuseReason } from "../startup-reuse.js"; import { withAdapterExecutionPhase, type AdapterExecutionPhase } from "../execution-phase.js"; @@ -1545,10 +1546,10 @@ function buildCodexStartupConfig(input: { requestedModel: string; requestedThinkingEffort: string; fastMode: boolean; - identityEnvironmentKeys?: string[]; + shellEnvironmentKeys?: string[]; }): { value: string | null; invalidExistingConfig: boolean } { const hasRuntimeConfig = Boolean( - input.requestedModel || input.requestedThinkingEffort || input.fastMode || input.identityEnvironmentKeys, + input.requestedModel || input.requestedThinkingEffort || input.fastMode || input.shellEnvironmentKeys, ); if (!hasRuntimeConfig) return { value: null, invalidExistingConfig: false }; @@ -1579,10 +1580,10 @@ function buildCodexStartupConfig(input: { }, } : {}), - ...(input.identityEnvironmentKeys ? { + ...(input.shellEnvironmentKeys ? { features: { ...parseObject(existing.features), ...(input.fastMode ? { fast_mode: true } : {}), shell_snapshot: false }, shell_environment_policy: { ...parseObject(existing.shell_environment_policy), - inherit: "all", ignore_default_excludes: true, include_only: input.identityEnvironmentKeys }, + inherit: "all", ignore_default_excludes: true, include_only: input.shellEnvironmentKeys }, } : {}), }), invalidExistingConfig, @@ -2119,15 +2120,10 @@ async function buildRuntime(input: { env.GEMINI_MODEL = requestedModel; } if (acpxAgent === "codex") { + const shellEnvironmentKeys = codexShellEnvironmentKeys(env, resolvedAdapterEnv, Boolean(input.ctx.agentIdentity)); const codexStartupConfig = buildCodexStartupConfig({ existingConfig: env.CODEX_CONFIG, - ...(input.ctx.agentIdentity ? { identityEnvironmentKeys: [...new Set([ - "PATH", "HOME", "LANG", "TMPDIR", "CODEX_HOME", - "PAPERCLIP_AGENT_KEY_ID", "PAPERCLIP_AGENT_PUBLIC_KEY", "PAPERCLIP_AGENT_PRIVATE_KEY", - // 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 => !(AGENT_OBSERVABILITY_ENV_KEYS as readonly string[]).includes(key) && (key === "PAPERCLIP_API_KEY" || !/key|secret|token/i.test(key))), - ])] } : {}), + shellEnvironmentKeys, requestedModel, requestedThinkingEffort, fastMode, diff --git a/packages/adapter-utils/src/codex-shell-environment.test.ts b/packages/adapter-utils/src/codex-shell-environment.test.ts new file mode 100644 index 00000000000..4ee1104982c --- /dev/null +++ b/packages/adapter-utils/src/codex-shell-environment.test.ts @@ -0,0 +1,36 @@ +import { expect, it } from "vitest"; +import { codexShellEnvironmentKeys } from "./codex-shell-environment.js"; + +it("admits assigned service secrets by name while retaining unassigned secret exclusions", () => { + const assigned = { SLACK_BOT_TOKEN: "slack-canary", SERVICE_API_KEY: "key-canary", APP_SECRET: "secret-canary" }; + const names = codexShellEnvironmentKeys({ ...assigned, HOST_API_TOKEN: "host-canary" }, assigned, false); + expect(names).toEqual(expect.arrayContaining(Object.keys(assigned))); + expect(names).not.toContain("HOST_API_TOKEN"); + for (const value of Object.values(assigned)) expect(JSON.stringify(names)).not.toContain(value); +}); + +it("keeps Codex authentication, observability and runtime transports out of the service-secret opt-in", () => { + const configured = { + SLACK_BOT_TOKEN: "assigned", OPENAI_API_KEY: "openai", CODEX_API_KEY: "codex", + PAPERCLIP_AI_PROVIDER_KEY: "managed", OTEL_EXPORTER_OTLP_HEADERS: "authorization=export", + PAPERCLIP_GITHUB_BROKER_TOKEN: "broker", PAPERCLIP_NATIVE_MCP_TOKEN: "mcp", + }; + const names = codexShellEnvironmentKeys(configured, configured, true); + expect(names).toContain("SLACK_BOT_TOKEN"); + for (const key of Object.keys(configured).filter(key => key !== "SLACK_BOT_TOKEN")) expect(names).not.toContain(key); +}); + +it("does not turn a configured glob into permission to inherit other secrets", () => { + const configured = { "*TOKEN*": "wildcard", "SERVICE[AB]_KEY": "pattern" }; + const names = codexShellEnvironmentKeys({ ...configured, HOST_TOKEN: "host" }, configured, true); + expect(names).not.toContain("*TOKEN*"); + expect(names).not.toContain("SERVICE[AB]_KEY"); + expect(names).not.toContain("HOST_TOKEN"); +}); + +it("removes the secret opt-in when a binding is removed or has not been resolved", () => { + const environment = { SLACK_BOT_TOKEN: "old-provider-environment" }; + expect(codexShellEnvironmentKeys(environment, {}, false)).not.toContain("SLACK_BOT_TOKEN"); + expect(codexShellEnvironmentKeys(environment, { SLACK_BOT_TOKEN: { type: "secret_ref" } }, false)).not.toContain("SLACK_BOT_TOKEN"); + expect(codexShellEnvironmentKeys(environment, {}, true)).not.toContain("SLACK_BOT_TOKEN"); +}); diff --git a/packages/adapter-utils/src/codex-shell-environment.ts b/packages/adapter-utils/src/codex-shell-environment.ts new file mode 100644 index 00000000000..4f52e76ece1 --- /dev/null +++ b/packages/adapter-utils/src/codex-shell-environment.ts @@ -0,0 +1,31 @@ +import { AGENT_OBSERVABILITY_ENV_KEYS } from "./agent-observability.js"; + +const SECRET_NAME = /key|secret|token/i; +const ENVIRONMENT_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/; +const PROVIDER_ONLY_KEYS: ReadonlySet = new Set([ + "OPENAI_API_KEY", "CODEX_API_KEY", "PAPERCLIP_AI_PROVIDER_KEY", + ...AGENT_OBSERVABILITY_ENV_KEYS, +]); +const IDENTITY_KEYS = ["PAPERCLIP_AGENT_KEY_ID", "PAPERCLIP_AGENT_PUBLIC_KEY", "PAPERCLIP_AGENT_PRIVATE_KEY"]; + +/** Only resolved adapter configuration can opt a service secret into shell access. + * The launch environment here is Paperclip-built, never ambient process.env. + * Return names only: values must stay out of provider config and argv. + */ +export function codexShellEnvironmentKeys( + environment: Record, + configuredEnvironment: Record, + hasAgentIdentity: boolean, +): string[] { + const configuredSecrets = new Set(Object.entries(configuredEnvironment) + .filter(([key, value]) => ENVIRONMENT_NAME.test(key) && typeof value === "string" && typeof environment[key] === "string" + && !/^PAPERCLIP_(?:GITHUB_|RUNNER_|NATIVE_|ACPX_)/i.test(key) + && SECRET_NAME.test(key) && !PROVIDER_ONLY_KEYS.has(key.toUpperCase())) + .map(([key]) => key)); + return [...new Set([ + "PATH", "HOME", "LANG", "LANGUAGE", "LC_ALL", "LC_CTYPE", "TZ", "TMPDIR", "TEMP", "TMP", "CODEX_HOME", + ...Object.keys(environment).filter(key => ENVIRONMENT_NAME.test(key) && !PROVIDER_ONLY_KEYS.has(key.toUpperCase()) + && (key === "PAPERCLIP_API_KEY" || configuredSecrets.has(key) || !SECRET_NAME.test(key))), + ...(hasAgentIdentity ? IDENTITY_KEYS : []), + ])].sort(); +} diff --git a/packages/adapters/codex-local/src/server/acp.test.ts b/packages/adapters/codex-local/src/server/acp.test.ts index 1927afca93f..07a6fd24ccc 100644 --- a/packages/adapters/codex-local/src/server/acp.test.ts +++ b/packages/adapters/codex-local/src/server/acp.test.ts @@ -780,11 +780,16 @@ describe("codex_local ACP lane", () => { expect(runtimes[0]?.setConfigInputs).toEqual([]); expect(meta[0]?.commandNotes?.join("\n")).toContain("Prepared ACPX Codex skill home"); expect(meta[0]?.env?.CODEX_HOME).toBe(path.join(root, "codex-home")); - expect(JSON.parse(String(meta[0]?.env?.CODEX_CONFIG))).toEqual({ + expect(JSON.parse(String(meta[0]?.env?.CODEX_CONFIG))).toMatchObject({ model: "gpt-5.5", model_reasoning_effort: "high", service_tier: "fast", features: { fast_mode: true }, + shell_environment_policy: { + inherit: "all", + ignore_default_excludes: true, + include_only: expect.arrayContaining(["PATH", "CODEX_HOME"]), + }, }); }); diff --git a/packages/adapters/codex-local/src/server/execute.remote.test.ts b/packages/adapters/codex-local/src/server/execute.remote.test.ts index 50de0f2d703..e359306af16 100644 --- a/packages/adapters/codex-local/src/server/execute.remote.test.ts +++ b/packages/adapters/codex-local/src/server/execute.remote.test.ts @@ -392,7 +392,7 @@ describe("codex remote execution", () => { expect(runChildProcess).toHaveBeenCalledTimes(1); const call = runChildProcess.mock.calls[0] as unknown as [string, string, string[]] | undefined; - expect(call?.[2]).toEqual([ + expect(call?.[2].slice(call[2].indexOf("exec"))).toEqual([ "exec", "--json", "--dangerously-bypass-approvals-and-sandbox", @@ -465,7 +465,7 @@ describe("codex remote execution", () => { expect(runChildProcess).toHaveBeenCalledTimes(1); const call = runChildProcess.mock.calls[0] as unknown as [string, string, string[]] | undefined; - expect(call?.[2]).toEqual([ + expect(call?.[2].slice(call[2].indexOf("exec"))).toEqual([ "exec", "--json", "--dangerously-bypass-approvals-and-sandbox", @@ -546,7 +546,7 @@ describe("codex remote execution", () => { const call = runChildProcess.mock.calls[0] as unknown as | [string, string, string[], { env: Record; remoteExecution?: { remoteCwd: string } | null }] | undefined; - expect(call?.[2]).toEqual([ + expect(call?.[2].slice(call[2].indexOf("exec"))).toEqual([ "exec", "--json", "--dangerously-bypass-approvals-and-sandbox", diff --git a/packages/adapters/codex-local/src/server/execute.test.ts b/packages/adapters/codex-local/src/server/execute.test.ts index 7a1775c81cf..1a1519caf85 100644 --- a/packages/adapters/codex-local/src/server/execute.test.ts +++ b/packages/adapters/codex-local/src/server/execute.test.ts @@ -118,7 +118,7 @@ describe("codex execute — outbound auth copy-back restore contribution", () => ); } - async function runTeardown(input: { sandboxAuth: string; hostAuth: string; onProviderStopped?: () => Promise; withIdentity?: boolean }) { + async function runTeardown(input: { sandboxAuth: string; hostAuth: string; onProviderStopped?: () => Promise; withIdentity?: boolean; env?: Record }) { const rootDir = await mkdtemp( path.join(os.tmpdir(), "paperclip-codex-copyback-e2e-"), ); @@ -159,7 +159,7 @@ describe("codex execute — outbound auth copy-back restore contribution", () => engine: "cli", // External CODEX_HOME (outside the managed company tree) so no managed // seeding rewrites auth.json before teardown; equals the shared host home. - env: { CODEX_HOME: sharedHostHome, ...(input.withIdentity ? { MY_SERVICE_TOKEN: "assigned-tool-token" } : {}) }, + env: { CODEX_HOME: sharedHostHome, ...input.env }, }, context: { paperclipWorkspace: { @@ -190,16 +190,19 @@ describe("codex execute — outbound auth copy-back restore contribution", () => }; } - it("keeps identity and scoped API access without exposing configured service tokens to CLI shells", async () => { + it.each([true, false])("delivers configured service tokens to CLI shells while isolating host and provider credentials (identity: %s)", async (withIdentity) => { 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 { commandArgs } = await runTeardown({ sandboxAuth: "{}", hostAuth: "{}", withIdentity, + env: { SLACK_BOT_TOKEN: "assigned-tool-token", OPENAI_API_KEY: "provider-key-canary" }, + }); 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([ + const keys = policy ? JSON.parse(policy.slice(policy.indexOf("=") + 1)) : []; + expect(keys).toContain("SLACK_BOT_TOKEN"); + if (withIdentity) expect(keys).toEqual(expect.arrayContaining([ "PAPERCLIP_API_KEY", "PAPERCLIP_AGENT_PRIVATE_KEY", ])); - expect(keys).not.toContain("MY_SERVICE_TOKEN"); + expect(keys).not.toContain("OPENAI_API_KEY"); 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 581f1384f4c..fbba81bc533 100644 --- a/packages/adapters/codex-local/src/server/execute.ts +++ b/packages/adapters/codex-local/src/server/execute.ts @@ -114,6 +114,7 @@ import { createCodexAcpExecutor, resolveCodexExecutionEngineForRun, } from "./acp.js"; +import { codexShellEnvironmentKeys } from "@paperclipai/adapter-utils/codex-shell-environment"; const __moduleDir = path.dirname(fileURLToPath(import.meta.url)); const executeCodexAcp = createCodexAcpExecutor(); @@ -1291,18 +1292,9 @@ 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)}`); - } + const shellKeys = codexShellEnvironmentKeys(env, envConfigStrings, Boolean(ctx.agentIdentity)); + 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)}`); const commandNotesWithFastMode = execArgs.fastModeIgnoredReason == null ? commandNotes