From 03d92346960e76220087d20f552c18fa5985c1cb Mon Sep 17 00:00:00 2001 From: Dotta Date: Mon, 7 Sep 2026 15:35:48 -0500 Subject: [PATCH] Preserve safe Codex shell variables alongside GitHub credentials Codex filters explicit environment overrides through include_only. Retain the scoped home and standard executable path without allowing provider or host secrets into shell commands. Co-Authored-By: Paperclip --- .../codex/codex-security-config.test.ts | 35 +++++++++++++++++++ .../drivers/codex/codex-security-config.ts | 8 +++-- 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/packages/paperclip-runner/src/drivers/codex/codex-security-config.test.ts b/packages/paperclip-runner/src/drivers/codex/codex-security-config.test.ts index 8822ea665c..eb81589590 100644 --- a/packages/paperclip-runner/src/drivers/codex/codex-security-config.test.ts +++ b/packages/paperclip-runner/src/drivers/codex/codex-security-config.test.ts @@ -79,6 +79,41 @@ describe("Codex security configuration", () => { expect(serialized).not.toContain("!trusted-helper"); }); + it("preserves the scoped shell environment through the final GitHub allowlist", () => { + const scoped = { + HOME: "/home/daytona", + PAPERCLIP_TASK_DIR: "/home/daytona/task", + PAPERCLIP_AGENT_DIR: "/home/daytona/agent", + PAPERCLIP_USER_DIR: "/home/daytona/user", + PAPERCLIP_PROJECT_DIR: "/home/daytona/project", + PAPERCLIP_REPOS_DIR: "/home/daytona/repos", + PAPERCLIP_PRIMARY_REPO: "/home/daytona/repos/private-repo", + PAPERCLIP_WORKSPACE_CWD: "/home/daytona/repos/private-repo", + PAPERCLIP_RUNNER_EXTERNAL_SANDBOX: "1", + }; + const args = createIsolatedCodexAppServerArgs({ + ...scoped, PATH: "/usr/local/bin:/usr/bin:/bin", LANG: "C.UTF-8", + GITHUB_TOKEN: "github-secret", OPENAI_API_KEY: "provider-secret", + CODEX_HOME: "/home/daytona/.codex", DATABASE_URL: "host-secret", + }); + const prefix = "shell_environment_policy.include_only="; + const allowed = JSON.parse(args.find((arg) => arg.startsWith(prefix))!.slice(prefix.length)); + // Codex applies this allowlist AFTER its explicit environment overrides. + expect(allowed.sort()).toEqual([ + "GITHUB_TOKEN", "PATH", "LANG", "AGENT_HOME", ...Object.keys(scoped), + ].sort()); + expect(args.join("\n")).not.toContain("github-secret"); + expect(args.join("\n")).not.toContain("provider-secret"); + expect(args.join("\n")).not.toContain("host-secret"); + + const localArgs = createIsolatedCodexAppServerArgs({ + HOME: "/host/private", CODEX_HOME: "/host/codex", PATH: "/bin", + GITHUB_TOKEN: "github-secret", + }); + const localAllowed = JSON.parse(localArgs.find((arg) => arg.startsWith(prefix))!.slice(prefix.length)); + expect(localAllowed.sort()).toEqual(["GITHUB_TOKEN", "PATH"]); + }); + it("uses a read-only permission profile for plan mode", () => { expect(createSecuredCodexThreadParams("/workspace", "plan")).toMatchObject({ cwd: "/workspace", diff --git a/packages/paperclip-runner/src/drivers/codex/codex-security-config.ts b/packages/paperclip-runner/src/drivers/codex/codex-security-config.ts index 6e4bbae4da..b17ac40f4e 100644 --- a/packages/paperclip-runner/src/drivers/codex/codex-security-config.ts +++ b/packages/paperclip-runner/src/drivers/codex/codex-security-config.ts @@ -110,7 +110,8 @@ export function createIsolatedCodexAppServerArgs( ...readOnlyRoots.map((path) => `${tomlString(resolve(path))}="read"`), `":workspace_roots"={"."="read"}`, ].join(","); - const commandEnv = Object.entries(codexCommandEnvironment(source)) + const commandEnvironment = codexCommandEnvironment(source); + const commandEnv = Object.entries(commandEnvironment) .map(([key, value]) => `${key}=${tomlString(value)}`) .join(","); const defaultPermissionProfile = externalRunnerSandbox @@ -142,7 +143,10 @@ export function createIsolatedCodexAppServerArgs( ...(hasGitHubCredential ? [ "-c", - `shell_environment_policy.include_only=${JSON.stringify(inheritedGitHubKeys)}`, + // Codex applies include_only after set. Keep the explicit, safe + // command environment too, or GitHub-enabled shells lose PATH and + // the external sandbox's scoped HOME despite the overrides below. + `shell_environment_policy.include_only=${JSON.stringify([...new Set([...inheritedGitHubKeys, ...Object.keys(commandEnvironment)])])}`, ] : []), ...(commandEnv.length > 0