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 <noreply@paperclip.ing>
This commit is contained in:
Dotta 2026-09-07 15:35:48 -05:00
parent 18ea040c34
commit cdb65a49ca
2 changed files with 41 additions and 2 deletions

View File

@ -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",

View File

@ -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