diff --git a/README.md b/README.md index b27e21dd..9e62daaf 100644 --- a/README.md +++ b/README.md @@ -733,7 +733,7 @@ All under `services.agent-box`: | `package` | selected agent default | Override package to run for every agent user. | | `installAgents` | all supported | Harnesses installed on the box (independent of what sessions run). | | `sessionLimit` | `null` (automatic) | Maximum running or queued sessions. Null derives roughly one slot per GiB of physical RAM; a positive integer overrides it. | -| `codexFullAccess` | `true` | Run codex with no approval prompts and no sandbox, box-wide, via `/etc/codex/config.toml`. The box is the sandbox. That file is codex's *system* config layer, so a user's own `~/.codex/config.toml` still overrides it — and it is the only path that reaches the app-server daemon behind a remote-controlled codex session. | +| `codexFullAccess` | `true` | Run codex with no approval prompts and no sandbox, box-wide, via `/etc/codex/config.toml`. The box is the sandbox. That file is codex's *system* config layer, so a user's own `~/.codex/config.toml` still overrides it — and it is the only path that reaches the app-server daemon behind a remote-controlled codex session. Opting a TUI session out (`skipPermissions = false`) switches it to codex's own bubblewrap sandbox, where `sudoAllowlist` stops working entirely (issue #726). | | `restartNotice` | `"auto"` | Whether a resumed Claude or Codex session gets the built-in "you were interrupted and automatically restarted" prompt. `"auto"` sends it for an unresolved hook lease, a webhook subscription filter, or a Claude turn cut mid-turn; `"always"` sends it whenever there is a concrete resume target; `"never"` never does. A Codex TUI uses its exact rollout target. Remote-controlled Codex can be targeted only after an app task creates a session subscription; the restarted daemon wakes that task with `codex queue`. A per-session `resumePrompt` overrides the built-in text. | | `remoteControlHost` | `fqdnOrHostName` | Host label for the `@` suffix of auto-derived Remote Control names. Empty -> falls back to the public `web.domain`, then the live kernel hostname. The AWS image sets it to the box's public sslip.io host. | | `users..sessions..*` | `{}` | Seed sessions (first boot only): per session `agent`, `skipPermissions`, `remoteControl`, `remoteControlName`, `workingDirectory`, `extraArgs`. Empty = the legacy per-user options below seed a session named `main`. | @@ -776,7 +776,13 @@ arbitrary command execution as the agent user. setuid and needs the euid transition) - a deliberate trade of a bit of containment for scoped elevation. - **Tight sudo:** whatever's in `sudoAllowlist` is the entire root-capable - surface. `NOPASSWD` only - no `SETENV`, no blanket sudo, no ALL. + surface. `NOPASSWD` only - no `SETENV`, no blanket sudo, no ALL. None of it + is reachable from a codex TUI session with `skipPermissions = false`, even + when `codexFullAccess` is `true` (that per-session override reaches a TUI + session only, not a remote-controlled one): it runs codex's own sandbox + instead, through an unprivileged bubblewrap user namespace where root is + never mapped, so sudo shows up owned by nobody:nogroup and refuses + outright regardless of the allowlist (issue #726). - **Login on everything a human reaches, brute-force damping (web deployments):** the terminal workspace, per-session terminals, settings, and the `//downloads/` file drop all sit behind the login (the CI tests diff --git a/modules/agent-box.nix b/modules/agent-box.nix index a6b8ec29..1e38fc17 100644 --- a/modules/agent-box.nix +++ b/modules/agent-box.nix @@ -749,6 +749,17 @@ let `systemctl` resolves through PATH to a Nix store path that won't match, silently falling back to asking for a password. + A Codex TUI session with `skipPermissions = false` runs Codex's OWN sandbox + instead, even when this box's `codexFullAccess` default is `true` (that + per-session override reaches a TUI session only, not a remote-controlled + one). That sandbox cannot run this at all: it execs commands inside an + unprivileged `bubblewrap` user namespace, where root is never mapped, so + `sudo` shows up owned by `nobody:nogroup` and refuses - "must be owned by + uid 0 and have the setuid bit set" - no matter what the sudoers file allows + (agent-box#726). That is every sudoAllowlist entry, not just this one; there + is no per-command workaround. Use a session with full access for this, or + reload caddy from a different session that has it. + @UPDATE_SECTION@## This platform has its own upstream repo The box itself - the terminal, session manager, webhook wiring, this @@ -13595,6 +13606,16 @@ in USER, shared by every remote thread, so there is nothing per-session to override. The codex TUI arms do honour it — the supervisor pins the restricted values back on the command line. + + A TUI session with skipPermissions = false runs codex's OWN + sandbox instead (sandbox_mode=workspace-write), which execs + commands inside an unprivileged bubblewrap user namespace. Root is + never mapped in that namespace, so sudo — including every + sudoAllowlist entry, not just the self-serve caddy reload — shows + up owned by nobody:nogroup and refuses outright (issue 726). That + opt-out trades away this option's autonomy AND the box's own sudo + escape hatch; there is no substitute privileged path for a session + in that mode yet. ''; }; @@ -25312,6 +25333,15 @@ if __name__ == "__main__": # exactly and a bare `systemctl` can resolve through PATH to one it will # not match — which asks for a password the agent does not have. # + # A Codex TUI session with skipPermissions = false runs Codex's OWN sandbox + # instead, even when this box's codexFullAccess default is true (that + # per-session override reaches a TUI session only, not a remote-controlled + # one). That sandbox cannot run this reload at all: it execs commands inside + # an unprivileged bubblewrap user namespace, where root is never mapped, so + # sudo shows up owned by nobody:nogroup and refuses regardless of the + # sudoers file (agent-box#726). Use a session with full access for this, or + # reload from one that has it. + # # Recommended snippet shape — reverse-proxy to a localhost port your # agent runs, NOT `file_server /home//...`. caddy.service has # ProtectHome=true, so it cannot read files under /home; use file_server @@ -25399,6 +25429,9 @@ if __name__ == "__main__": # # One import per user: Caddyfile's `import` directive only accepts a # single `*` per pattern, so we can't collapse this to `*/*.caddy`. + # + # A sandboxed Codex session can't run that reload at all — see the note at + # the top of this file (agent-box#726). '' + lib.concatMapStringsSep "" (name: "import /var/lib/agent-box-sites/${name}/*.caddy\n") (lib.attrNames cfg.users)); diff --git a/modules/agent-box.nix.in b/modules/agent-box.nix.in index 005f3d57..5e6206f4 100644 --- a/modules/agent-box.nix.in +++ b/modules/agent-box.nix.in @@ -1766,6 +1766,16 @@ in USER, shared by every remote thread, so there is nothing per-session to override. The codex TUI arms do honour it — the supervisor pins the restricted values back on the command line. + + A TUI session with skipPermissions = false runs codex's OWN + sandbox instead (sandbox_mode=workspace-write), which execs + commands inside an unprivileged bubblewrap user namespace. Root is + never mapped in that namespace, so sudo — including every + sudoAllowlist entry, not just the self-serve caddy reload — shows + up owned by nobody:nogroup and refuses outright (issue 726). That + opt-out trades away this option's autonomy AND the box's own sudo + escape hatch; there is no substitute privileged path for a session + in that mode yet. ''; }; diff --git a/modules/src/caddyfile-header.caddy b/modules/src/caddyfile-header.caddy index 05986afe..7ddae6e4 100644 --- a/modules/src/caddyfile-header.caddy +++ b/modules/src/caddyfile-header.caddy @@ -10,6 +10,15 @@ # exactly and a bare `systemctl` can resolve through PATH to one it will # not match — which asks for a password the agent does not have. # +# A Codex TUI session with skipPermissions = false runs Codex's OWN sandbox +# instead, even when this box's codexFullAccess default is true (that +# per-session override reaches a TUI session only, not a remote-controlled +# one). That sandbox cannot run this reload at all: it execs commands inside +# an unprivileged bubblewrap user namespace, where root is never mapped, so +# sudo shows up owned by nobody:nogroup and refuses regardless of the +# sudoers file (agent-box#726). Use a session with full access for this, or +# reload from one that has it. +# # Recommended snippet shape — reverse-proxy to a localhost port your # agent runs, NOT `file_server /home//...`. caddy.service has # ProtectHome=true, so it cannot read files under /home; use file_server diff --git a/modules/src/caddyfile-sites.caddy b/modules/src/caddyfile-sites.caddy index 0f2aeaf4..486de063 100644 --- a/modules/src/caddyfile-sites.caddy +++ b/modules/src/caddyfile-sites.caddy @@ -6,3 +6,6 @@ # # One import per user: Caddyfile's `import` directive only accepts a # single `*` per pattern, so we can't collapse this to `*/*.caddy`. +# +# A sandboxed Codex session can't run that reload at all — see the note at +# the top of this file (agent-box#726). diff --git a/modules/src/default-agents.md b/modules/src/default-agents.md index cf42a787..0b937b60 100644 --- a/modules/src/default-agents.md +++ b/modules/src/default-agents.md @@ -248,6 +248,17 @@ $HOME (caddy can't read /home). Use the full path shown, not bare `systemctl` resolves through PATH to a Nix store path that won't match, silently falling back to asking for a password. +A Codex TUI session with `skipPermissions = false` runs Codex's OWN sandbox +instead, even when this box's `codexFullAccess` default is `true` (that +per-session override reaches a TUI session only, not a remote-controlled +one). That sandbox cannot run this at all: it execs commands inside an +unprivileged `bubblewrap` user namespace, where root is never mapped, so +`sudo` shows up owned by `nobody:nogroup` and refuses - "must be owned by +uid 0 and have the setuid bit set" - no matter what the sudoers file allows +(agent-box#726). That is every sudoAllowlist entry, not just this one; there +is no per-command workaround. Use a session with full access for this, or +reload caddy from a different session that has it. + @UPDATE_SECTION@## This platform has its own upstream repo The box itself - the terminal, session manager, webhook wiring, this diff --git a/tests/golden/vm/etc/agent-box-guides/AGENTS.agent.md b/tests/golden/vm/etc/agent-box-guides/AGENTS.agent.md index 4f8d7573..bfce91da 100644 --- a/tests/golden/vm/etc/agent-box-guides/AGENTS.agent.md +++ b/tests/golden/vm/etc/agent-box-guides/AGENTS.agent.md @@ -304,6 +304,17 @@ $HOME (caddy can't read /home). Use the full path shown, not bare `systemctl` resolves through PATH to a Nix store path that won't match, silently falling back to asking for a password. +A Codex TUI session with `skipPermissions = false` runs Codex's OWN sandbox +instead, even when this box's `codexFullAccess` default is `true` (that +per-session override reaches a TUI session only, not a remote-controlled +one). That sandbox cannot run this at all: it execs commands inside an +unprivileged `bubblewrap` user namespace, where root is never mapped, so +`sudo` shows up owned by `nobody:nogroup` and refuses - "must be owned by +uid 0 and have the setuid bit set" - no matter what the sudoers file allows +(agent-box#726). That is every sudoAllowlist entry, not just this one; there +is no per-command workaround. Use a session with full access for this, or +reload caddy from a different session that has it. + ## This platform has its own upstream repo The box itself - the terminal, session manager, webhook wiring, this diff --git a/tests/golden/web/etc/agent-box-guides/AGENTS.agent.md b/tests/golden/web/etc/agent-box-guides/AGENTS.agent.md index afb462d9..5053c45c 100644 --- a/tests/golden/web/etc/agent-box-guides/AGENTS.agent.md +++ b/tests/golden/web/etc/agent-box-guides/AGENTS.agent.md @@ -622,6 +622,17 @@ $HOME (caddy can't read /home). Use the full path shown, not bare `systemctl` resolves through PATH to a Nix store path that won't match, silently falling back to asking for a password. +A Codex TUI session with `skipPermissions = false` runs Codex's OWN sandbox +instead, even when this box's `codexFullAccess` default is `true` (that +per-session override reaches a TUI session only, not a remote-controlled +one). That sandbox cannot run this at all: it execs commands inside an +unprivileged `bubblewrap` user namespace, where root is never mapped, so +`sudo` shows up owned by `nobody:nogroup` and refuses - "must be owned by +uid 0 and have the setuid bit set" - no matter what the sudoers file allows +(agent-box#726). That is every sudoAllowlist entry, not just this one; there +is no per-command workaround. Use a session with full access for this, or +reload caddy from a different session that has it. + ## Updating Update the box's software with: diff --git a/tests/golden/web/etc/caddy/caddy_config b/tests/golden/web/etc/caddy/caddy_config index 76cefbab..21b27975 100644 --- a/tests/golden/web/etc/caddy/caddy_config +++ b/tests/golden/web/etc/caddy/caddy_config @@ -10,6 +10,15 @@ # exactly and a bare `systemctl` can resolve through PATH to one it will # not match — which asks for a password the agent does not have. # +# A Codex TUI session with skipPermissions = false runs Codex's OWN sandbox +# instead, even when this box's codexFullAccess default is true (that +# per-session override reaches a TUI session only, not a remote-controlled +# one). That sandbox cannot run this reload at all: it execs commands inside +# an unprivileged bubblewrap user namespace, where root is never mapped, so +# sudo shows up owned by nobody:nogroup and refuses regardless of the +# sudoers file (agent-box#726). Use a session with full access for this, or +# reload from one that has it. +# # Recommended snippet shape — reverse-proxy to a localhost port your # agent runs, NOT `file_server /home//...`. caddy.service has # ProtectHome=true, so it cannot read files under /home; use file_server @@ -593,5 +602,8 @@ golden.example.org { # # One import per user: Caddyfile's `import` directive only accepts a # single `*` per pattern, so we can't collapse this to `*/*.caddy`. +# +# A sandboxed Codex session can't run that reload at all — see the note at +# the top of this file (agent-box#726). import /var/lib/agent-box-sites/agent/*.caddy import /var/lib/agent-box-sites/robot/*.caddy diff --git a/tests/native/expected/etc/agent-box-guides/AGENTS.agent.md b/tests/native/expected/etc/agent-box-guides/AGENTS.agent.md index 033ebea8..eefb3d8a 100644 --- a/tests/native/expected/etc/agent-box-guides/AGENTS.agent.md +++ b/tests/native/expected/etc/agent-box-guides/AGENTS.agent.md @@ -659,6 +659,17 @@ $HOME (caddy can't read /home). Use the full path shown, not bare `systemctl` resolves through PATH to a Nix store path that won't match, silently falling back to asking for a password. +A Codex TUI session with `skipPermissions = false` runs Codex's OWN sandbox +instead, even when this box's `codexFullAccess` default is `true` (that +per-session override reaches a TUI session only, not a remote-controlled +one). That sandbox cannot run this at all: it execs commands inside an +unprivileged `bubblewrap` user namespace, where root is never mapped, so +`sudo` shows up owned by `nobody:nogroup` and refuses - "must be owned by +uid 0 and have the setuid bit set" - no matter what the sudoers file allows +(agent-box#726). That is every sudoAllowlist entry, not just this one; there +is no per-command workaround. Use a session with full access for this, or +reload caddy from a different session that has it. + ## Updating Update the box's software with: diff --git a/tests/native/expected/etc/agent-box-guides/AGENTS.robot.md b/tests/native/expected/etc/agent-box-guides/AGENTS.robot.md index 033ebea8..eefb3d8a 100644 --- a/tests/native/expected/etc/agent-box-guides/AGENTS.robot.md +++ b/tests/native/expected/etc/agent-box-guides/AGENTS.robot.md @@ -659,6 +659,17 @@ $HOME (caddy can't read /home). Use the full path shown, not bare `systemctl` resolves through PATH to a Nix store path that won't match, silently falling back to asking for a password. +A Codex TUI session with `skipPermissions = false` runs Codex's OWN sandbox +instead, even when this box's `codexFullAccess` default is `true` (that +per-session override reaches a TUI session only, not a remote-controlled +one). That sandbox cannot run this at all: it execs commands inside an +unprivileged `bubblewrap` user namespace, where root is never mapped, so +`sudo` shows up owned by `nobody:nogroup` and refuses - "must be owned by +uid 0 and have the setuid bit set" - no matter what the sudoers file allows +(agent-box#726). That is every sudoAllowlist entry, not just this one; there +is no per-command workaround. Use a session with full access for this, or +reload caddy from a different session that has it. + ## Updating Update the box's software with: diff --git a/tests/native/expected/etc/agent-box/Caddyfile b/tests/native/expected/etc/agent-box/Caddyfile index b5fae9ff..90b9260e 100644 --- a/tests/native/expected/etc/agent-box/Caddyfile +++ b/tests/native/expected/etc/agent-box/Caddyfile @@ -11,6 +11,15 @@ # exactly and a bare `systemctl` can resolve through PATH to one it will # not match — which asks for a password the agent does not have. # +# A Codex TUI session with skipPermissions = false runs Codex's OWN sandbox +# instead, even when this box's codexFullAccess default is true (that +# per-session override reaches a TUI session only, not a remote-controlled +# one). That sandbox cannot run this reload at all: it execs commands inside +# an unprivileged bubblewrap user namespace, where root is never mapped, so +# sudo shows up owned by nobody:nogroup and refuses regardless of the +# sudoers file (agent-box#726). Use a session with full access for this, or +# reload from one that has it. +# # Recommended snippet shape — reverse-proxy to a localhost port your # agent runs, NOT `file_server /home//...`. caddy.service has # ProtectHome=true, so it cannot read files under /home; use file_server @@ -598,5 +607,8 @@ handle { # # One import per user: Caddyfile's `import` directive only accepts a # single `*` per pattern, so we can't collapse this to `*/*.caddy`. +# +# A sandboxed Codex session can't run that reload at all — see the note at +# the top of this file (agent-box#726). import /var/lib/agent-box-sites/agent/*.caddy import /var/lib/agent-box-sites/robot/*.caddy