diff --git a/doc/sandbox-work-folders.md b/doc/sandbox-work-folders.md index 7c74b8b944..ca74021cc2 100644 --- a/doc/sandbox-work-folders.md +++ b/doc/sandbox-work-folders.md @@ -114,6 +114,10 @@ Sandbox Codex tool commands preserve the environment initialized by the adapter: login-shell execution and shell snapshots are disabled so image profiles cannot replace the managed Git PATH. This applies to CLI and ACP execution in both runner generations; local execution keeps its existing settings. +Native Codex ACP selects the provider's `agent-full-access` initial mode only +inside a validated external work-folder sandbox with an explicit `approve-all` +binding. This avoids starting an unsupported nested network namespace. Local +execution and the `approve-reads` / `deny-all` modes retain their existing policy. CLI state is separate from the four shared collections. A change of task, agent, responsible user, or project cannot reuse a sandbox with another binding. 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 0e1b38b1ae..b7b96c2e8e 100644 --- a/packages/paperclip-runner/src/drivers/acpx/runtime-sandbox.test.ts +++ b/packages/paperclip-runner/src/drivers/acpx/runtime-sandbox.test.ts @@ -49,6 +49,28 @@ describe("ACPX runtime sandbox", () => { .toBe("allow_login_shell = false\n\n[features]\nshell_snapshot = false\n"); }); + it.each(["approve-all", "approve-reads", "deny-all"] as const)( + "uses the external sandbox boundary only for explicitly approved Codex execution (%s)", async (permissionMode) => { + const fixture = await sandboxFixture("codex"); + const home = join(fixture.root, "home"); + const environment = { + HOME: home, PAPERCLIP_RUNNER_EXTERNAL_SANDBOX: "1", + INITIAL_AGENT_MODE: "agent-full-access", + ...Object.fromEntries(["task", "agent", "user", "project", "repos"].map((scope) => + [`PAPERCLIP_${scope.toUpperCase()}_DIR`, join(home, scope)])), + }; + const sandbox = await prepareAcpxRuntimeSandbox({ + binding: { ...fixture.binding, permissionMode }, agent: "codex", environment, + }); + expect(sandbox.launchEnvironment.INITIAL_AGENT_MODE).toBe(permissionMode === "approve-all" ? "agent-full-access" : undefined); + const local = await prepareAcpxRuntimeSandbox({ + binding: { ...fixture.binding, permissionMode }, agent: "codex", + environment: { ...environment, PAPERCLIP_RUNNER_EXTERNAL_SANDBOX: undefined }, + }); + expect(local.launchEnvironment.INITIAL_AGENT_MODE).toBeUndefined(); + }, + ); + it.each([ ["pi", "OPENROUTER_API_KEY", "pi-home"], ["claude", "ANTHROPIC_API_KEY", "claude-home"], diff --git a/packages/paperclip-runner/src/drivers/acpx/runtime-sandbox.ts b/packages/paperclip-runner/src/drivers/acpx/runtime-sandbox.ts index 7738627d7b..697d8b9be1 100644 --- a/packages/paperclip-runner/src/drivers/acpx/runtime-sandbox.ts +++ b/packages/paperclip-runner/src/drivers/acpx/runtime-sandbox.ts @@ -439,6 +439,13 @@ export async function prepareAcpxRuntimeSandbox(input: { ? { CODEX_HOME: agentHomeDirectory, NO_BROWSER: "1", + // The admitted external sandbox supplies OS isolation. Codex ACP's + // default mode otherwise starts a second network namespace, which + // cannot initialize inside Daytona. Only the host's explicit + // approve-all binding may select this provider mode. + ...(input.binding.permissionMode === "approve-all" + && externalWorkFolderEnvironment(input.environment ?? {}).HOME + ? { INITIAL_AGENT_MODE: "agent-full-access" } : {}), ...(launchEnvironment.CODEX_API_KEY || launchEnvironment.OPENAI_API_KEY ? { DEFAULT_AUTH_REQUEST: JSON.stringify({ methodId: "api-key" }) }