Use the external sandbox for approved native Codex ACP runs

Select Codex ACP's full-access initial mode only for host-validated external work-folder environments with approve-all authority. Keep local and restrictive permission modes unchanged. The regression failed before the fix; all 50 ACP environment and runtime tests pass.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Dotta 2026-09-08 19:28:06 -05:00
parent fbf2494104
commit 8787dab6f3
3 changed files with 33 additions and 0 deletions

View File

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

View File

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

View File

@ -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" }) }