diff --git a/apps/daemon/src/runtimes/defs/codex.ts b/apps/daemon/src/runtimes/defs/codex.ts index f13b2318b..23ada6158 100644 --- a/apps/daemon/src/runtimes/defs/codex.ts +++ b/apps/daemon/src/runtimes/defs/codex.ts @@ -48,6 +48,10 @@ export function codexNeedsDangerFullAccessSandbox( platform: NodeJS.Platform = process.platform, env: NodeJS.ProcessEnv = process.env, ): boolean { + // Operator override for deployments where Codex cannot create its + // workspace-write sandbox, for example unprivileged Linux containers. + // Only danger-full-access is accepted; unknown values keep the default path. + if (env.OD_CODEX_SANDBOX?.trim() === 'danger-full-access') return true; if (platform === 'win32') return true; // WSL reports `linux` but Codex still hits the Windows read-only // workspace-write sandbox path when launched from there (#2834). diff --git a/apps/daemon/tests/runtimes/registry-and-args.test.ts b/apps/daemon/tests/runtimes/registry-and-args.test.ts index e47d08080..25817e1fe 100644 --- a/apps/daemon/tests/runtimes/registry-and-args.test.ts +++ b/apps/daemon/tests/runtimes/registry-and-args.test.ts @@ -103,33 +103,37 @@ test('local agent profiles skip explicit unknown baseAgent without falling back' }); test('codex args disable plugins when OD_CODEX_DISABLE_PLUGINS is 1', () => { - process.env.OD_CODEX_DISABLE_PLUGINS = '1'; + withEnvSnapshot(['OD_CODEX_DISABLE_PLUGINS', 'OD_CODEX_SANDBOX'], () => { + process.env.OD_CODEX_DISABLE_PLUGINS = '1'; + delete process.env.OD_CODEX_SANDBOX; - withPlatform('darwin', () => { - const args = codex.buildArgs('', [], [], {}, { cwd: '/tmp/od-project' }); + withPlatform('darwin', () => { + const args = codex.buildArgs('', [], [], {}, { cwd: '/tmp/od-project' }); - assert.deepEqual(args.slice(0, 11), [ - 'exec', - '--json', - '--skip-git-repo-check', - '--sandbox', - 'workspace-write', - '-c', - 'sandbox_workspace_write.network_access=true', - '-c', - 'default_permissions=":workspace"', - '--disable', - 'plugins', - ]); + assert.deepEqual(args.slice(0, 11), [ + 'exec', + '--json', + '--skip-git-repo-check', + '--sandbox', + 'workspace-write', + '-c', + 'sandbox_workspace_write.network_access=true', + '-c', + 'default_permissions=":workspace"', + '--disable', + 'plugins', + ]); + }); }); }); test('codex args use workspace-write sandbox on macOS and Linux', () => { - delete process.env.OD_CODEX_DISABLE_PLUGINS; + withEnvSnapshot(['OD_CODEX_DISABLE_PLUGINS', 'OD_CODEX_SANDBOX', 'WSL_DISTRO_NAME'], () => { + delete process.env.OD_CODEX_DISABLE_PLUGINS; + delete process.env.OD_CODEX_SANDBOX; - for (const platform of ['darwin', 'linux'] as const) { - withPlatform(platform, () => { - withEnvSnapshot(['WSL_DISTRO_NAME'], () => { + for (const platform of ['darwin', 'linux'] as const) { + withPlatform(platform, () => { delete process.env.WSL_DISTRO_NAME; const args = codex.buildArgs('', [], [], {}, { cwd: '/tmp/od-project' }); assert.equal(args.includes('--full-auto'), false); @@ -149,15 +153,15 @@ test('codex args use workspace-write sandbox on macOS and Linux', () => { true, ); }); - }); - } + } + }); }); test('codex args use danger-full-access sandbox on WSL because workspace-write stays read-only', () => { - delete process.env.OD_CODEX_DISABLE_PLUGINS; - withPlatform('linux', () => { - withEnvSnapshot(['WSL_DISTRO_NAME'], () => { + withEnvSnapshot(['OD_CODEX_DISABLE_PLUGINS', 'OD_CODEX_SANDBOX', 'WSL_DISTRO_NAME'], () => { + delete process.env.OD_CODEX_DISABLE_PLUGINS; + delete process.env.OD_CODEX_SANDBOX; process.env.WSL_DISTRO_NAME = 'Ubuntu'; assert.equal(codexNeedsDangerFullAccessSandbox('linux', process.env), true); const args = codex.buildArgs('', [], [], {}, { cwd: '/tmp/od-project' }); @@ -173,6 +177,50 @@ test('codex args use danger-full-access sandbox on WSL because workspace-write s }); }); +test('codex args allow OD_CODEX_SANDBOX danger-full-access override on Linux', () => { + withPlatform('linux', () => { + withEnvSnapshot(['OD_CODEX_DISABLE_PLUGINS', 'OD_CODEX_SANDBOX', 'WSL_DISTRO_NAME'], () => { + delete process.env.OD_CODEX_DISABLE_PLUGINS; + process.env.OD_CODEX_SANDBOX = 'danger-full-access'; + delete process.env.WSL_DISTRO_NAME; + + assert.equal(codexNeedsDangerFullAccessSandbox('linux', process.env), true); + const args = codex.buildArgs('', [], [], {}, { cwd: '/tmp/od-project' }); + assert.deepEqual(args.slice(0, 5), [ + 'exec', + '--json', + '--skip-git-repo-check', + '--sandbox', + 'danger-full-access', + ]); + assert.equal( + args.includes('sandbox_workspace_write.network_access=true'), + false, + ); + }); + }); +}); + +test('codex args ignore unknown OD_CODEX_SANDBOX values', () => { + withPlatform('linux', () => { + withEnvSnapshot(['OD_CODEX_DISABLE_PLUGINS', 'OD_CODEX_SANDBOX', 'WSL_DISTRO_NAME'], () => { + delete process.env.OD_CODEX_DISABLE_PLUGINS; + process.env.OD_CODEX_SANDBOX = 'workspace-write'; + delete process.env.WSL_DISTRO_NAME; + + assert.equal(codexNeedsDangerFullAccessSandbox('linux', process.env), false); + const args = codex.buildArgs('', [], [], {}, { cwd: '/tmp/od-project' }); + assert.deepEqual(args.slice(0, 5), [ + 'exec', + '--json', + '--skip-git-repo-check', + '--sandbox', + 'workspace-write', + ]); + }); + }); +}); + test('codex args use danger-full-access sandbox on Windows because workspace-write blocks PowerShell', () => { // Codex CLI's workspace-write sandbox mode on Windows lacks a working // OS-level sandbox and falls back to a policy that rejects shell @@ -180,48 +228,57 @@ test('codex args use danger-full-access sandbox on Windows because workspace-wri // The agent cannot list files or run any shell-backed tool under that // policy. danger-full-access is Codex CLI's documented Windows-compatible // mode (issue #1721). - delete process.env.OD_CODEX_DISABLE_PLUGINS; + withEnvSnapshot(['OD_CODEX_DISABLE_PLUGINS', 'OD_CODEX_SANDBOX'], () => { + delete process.env.OD_CODEX_DISABLE_PLUGINS; + delete process.env.OD_CODEX_SANDBOX; - withPlatform('win32', () => { - const args = codex.buildArgs('', [], [], {}, { cwd: '/tmp/od-project' }); + withPlatform('win32', () => { + const args = codex.buildArgs('', [], [], {}, { cwd: '/tmp/od-project' }); - assert.deepEqual(args.slice(0, 5), [ - 'exec', - '--json', - '--skip-git-repo-check', - '--sandbox', - 'danger-full-access', - ]); - // The workspace-write-scoped network override is meaningless under - // danger-full-access and must not appear on Windows. - assert.equal(args.includes('workspace-write'), false); - assert.equal( - args.includes('sandbox_workspace_write.network_access=true'), - false, - ); - assert.equal(args.includes('default_permissions=":workspace"'), true); + assert.deepEqual(args.slice(0, 5), [ + 'exec', + '--json', + '--skip-git-repo-check', + '--sandbox', + 'danger-full-access', + ]); + // The workspace-write-scoped network override is meaningless under + // danger-full-access and must not appear on Windows. + assert.equal(args.includes('workspace-write'), false); + assert.equal( + args.includes('sandbox_workspace_write.network_access=true'), + false, + ); + assert.equal(args.includes('default_permissions=":workspace"'), true); + }); }); }); test('codex args keep plugins enabled when OD_CODEX_DISABLE_PLUGINS is unset', () => { - delete process.env.OD_CODEX_DISABLE_PLUGINS; + withEnvSnapshot(['OD_CODEX_DISABLE_PLUGINS', 'OD_CODEX_SANDBOX'], () => { + delete process.env.OD_CODEX_DISABLE_PLUGINS; + delete process.env.OD_CODEX_SANDBOX; - withPlatform('darwin', () => { - const args = codex.buildArgs('', [], [], {}, { cwd: '/tmp/od-project' }); + withPlatform('darwin', () => { + const args = codex.buildArgs('', [], [], {}, { cwd: '/tmp/od-project' }); - assert.equal(args.includes('--disable'), false); - assert.equal(args.includes('plugins'), false); + assert.equal(args.includes('--disable'), false); + assert.equal(args.includes('plugins'), false); + }); }); }); test('codex args keep plugins enabled when OD_CODEX_DISABLE_PLUGINS is not 1', () => { - process.env.OD_CODEX_DISABLE_PLUGINS = 'true'; + withEnvSnapshot(['OD_CODEX_DISABLE_PLUGINS', 'OD_CODEX_SANDBOX'], () => { + process.env.OD_CODEX_DISABLE_PLUGINS = 'true'; + delete process.env.OD_CODEX_SANDBOX; - withPlatform('darwin', () => { - const args = codex.buildArgs('', [], [], {}, { cwd: '/tmp/od-project' }); + withPlatform('darwin', () => { + const args = codex.buildArgs('', [], [], {}, { cwd: '/tmp/od-project' }); - assert.equal(args.includes('--disable'), false); - assert.equal(args.includes('plugins'), false); + assert.equal(args.includes('--disable'), false); + assert.equal(args.includes('plugins'), false); + }); }); }); diff --git a/deploy/.env.example b/deploy/.env.example index 788be0ce8..978ac3f29 100644 --- a/deploy/.env.example +++ b/deploy/.env.example @@ -22,3 +22,6 @@ OPEN_DESIGN_MEM_LIMIT=384m # Node.js heap cap inside the container. NODE_OPTIONS=--max-old-space-size=192 +# Optional Codex CLI sandbox override. Set to danger-full-access only when +# Codex fails with workspace-write sandbox setup errors inside the container. +OD_CODEX_SANDBOX= diff --git a/deploy/README.md b/deploy/README.md index 1983779b7..844a87c24 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -58,6 +58,20 @@ The image intentionally does not bundle Claude/Codex/Gemini CLI binaries. Keep those outside the image, or build a separate private runtime layer if a server deployment needs local code-agent CLIs installed in the container. +If you install Codex inside an unprivileged Linux container and it fails while +creating its `workspace-write` sandbox, opt into Codex's full-access mode for +all Codex runs in that deployment: + +```bash +OD_CODEX_SANDBOX=danger-full-access docker compose up -d --no-build +``` + +Only the exact value `danger-full-access` is supported; unknown values are +ignored. Use this only for trusted, single-user deployments. It lets Codex run +without the workspace-write sandbox, which is useful when the container host +blocks unprivileged user namespaces, but it gives the Codex process broader +filesystem access inside the container. + ## Publish to Docker Hub ```bash diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml index c999cd873..f786dbd7a 100644 --- a/deploy/docker-compose.yml +++ b/deploy/docker-compose.yml @@ -16,6 +16,7 @@ services: OD_PORT: 7456 OD_WEB_PORT: ${OPEN_DESIGN_PORT:-7456} OD_API_TOKEN: ${OD_API_TOKEN:?Please run 'openssl rand -hex 32' to generate one, and set it in your .env file.} + OD_CODEX_SANDBOX: ${OD_CODEX_SANDBOX:-} ports: - "127.0.0.1:${OPEN_DESIGN_PORT:-7456}:7456" volumes: