Repository navigation
plat 491
PLAT-491 — A Crew/Code chat's built-in shell runs as the app account with platform secrets in its environment and the app's Docker socket
| Field | Value |
|---|---|
| State | open |
| Priority | P1 |
| Product | sandbox |
| Area | secrets |
| Summary | open. |
| Coordination | Value |
|---|---|
| State | open (found 2026-10-05 by a read-only probe on Excellence, release agents-f429a8cc; nothing changed; no secret value was printed, no container was run) |
| Priority | P1 |
| Owner | security-sandbox |
Owner asked "can we do some check in native bash". Through the MCP connection (as the owner) a scratch Crew (smoke-test, owner's own, no secrets) was asked to run a fixed script with its BUILT-IN shell (exec_command/Bash) and the same script with the bridge shell (execute_shell_command) as control. The script prints names, yes/no and HTTP codes only. (The Crew declined a first version that touched another Crew's data and made a scratch folder; that version was dropped, not pushed.)
| Target | Built-in shell | Bridge shell | Who refused |
|---|---|---|---|
| identity |
agents uid 990, member of EVERY slot group (slot01..50, slotshared) |
slot03 |
n/a |
open /srv/agents/.env, state/vault/agent.env
|
refused although access() says readable |
refused | Landlock (built-in), Unix (bridge) |
| list state/, docs root, _users, Workflow, vault state dir, app home, slots/run, releases, state/cli-runtimes/v1 | ALLOWED (folder names only; ListPaths: ["/"] in internal/clisandbox/landlock.go, by design) |
refused | n/a |
/srv/confida (other product) |
refused | refused | Unix |
| write in docs root, state/ | refused by a real touch (docs root); access() says writable |
refused | Landlock / Unix |
| tmux on the slot socket | blocked by the slot tmux shim ("Permission denied") | cannot connect | shim / hidden folder |
loopback /health: 24001, 8080 |
200 | 200 | none |
docker info |
WORKS, via DOCKER_HOST=unix:///run/user/990/docker.sock (the APP account's rootless Docker) |
works (the slot's own Docker) | none |
| environment (names) | includes SUPABASE_SERVICE_ROLE_KEY, SUPABASE_ANON_KEY, GOG_KEYRING_PASSWORD, ADMIN_USERS, AUTH_ALLOWED_EMAILS, CAPLAYER_SERVICE_TOKEN_FILE, SSH_AUTH_SOCK, GATEWAY_*, DOCKER_HOST (not AUTH_SECRET, not ACCESS_PASSWORD) |
ADMIN_USERS, AUTH_ALLOWED_EMAILS, GATEWAY_*, DOCKER_HOST, MCP_API_TOKEN (its own bridge token); no Supabase keys, no keyring password |
n/a |
- The CLI process inherits the agent's environment (PATH, NVM, SYSTEMD, ... all present), so
envin the built-in shell can print the Supabase service-role key and the gog keyring password to anyone who can get that CLI to run a command (a Crew/Code user, or a prompt injected into their chat). - Landlock does not govern
connect()on pathname Unix sockets, so the app account's own Docker daemon is usable from inside the lock: a container with a bind mount runs outside it and could read whatagentscan read (its.env, state, and, through the slot groups it belongs to, every user's tree). Onlydocker infowas run; no container was started. - The app account is in every slot group, so Unix permissions do not separate users for this shell; Landlock is the only boundary. Reads of files were refused by Landlock; names are listable.
- Recorded decisions this touches (owner's call): native tools on in
full(2026-10-03/04, PLAT-385..419), PLAT-364 "keep both tool sets", PLAT-446 "Crew turns run as the app account", slots keep Docker (2026-10-04).
Writes into other users' trees; reading with the Supabase key; running a container; whether other CLIs (Claude, Cursor, Muse, Pi) show the same environment (this run was Codex); RTS and Confida (RTS app Docker socket exists at /run/user/999).
- Build the CLI's environment from an allowlist (or strip a denylist of platform secrets) at launch, as the bridge shell already does (
BuildSafeEnvironment); the CLI needs a handful of variables, not the service-role key. Closes the env exposure for every native tool. - Remove the app Docker from the app-account CLI: unset
DOCKER_HOSTAND hide/run/user/<app uid>in the CLI's mount namespace (the private-roots mechanism of PLAT-480 F1, not yet in the provider launcher), or run Crew CLIs as the user's slot (the slot CLI canary exists:AGENTWORKS_SLOT_CLI_USERS), which also removes the slot-group membership problem; conflicts with PLAT-446. - Disable the native shell per CLI (Codex
features.shell_tool=false; Claude print-mode denylist, interactive--toolslist; Muse allowlist; Agy hook; Cursor: in full-native mode its preToolUse deny hook exempts Shell (cursorFullNativeAllowedTools) and cli.json allowsShell(*), so dropping both would deny it. CORRECTION 2026-10-05: an earlier version of this ticket said Cursor had no deny-all mode; that came from a peer note ("none that I verified"), not from the code, and the code shows it is possible; untested live). Owner rejectedmcp_onlyfor chats (2026-10-05). Does not by itself remove the env (file tools may read/proc/self/environ) so it needs option 1 anyway.
workspace/security/landlock_runner_linux.go: ScrubPlatformSecretEnv runs at the launcher's final exec, the one place every confined CLI passes through (not the six adapters). Deploy self-test row cli-launcher-env-scrub (full level) plants canary values under the platform names, runs env through the launcher and requires them gone and an ordinary variable kept. Container e2e: passes with the new launcher; with the OLD launcher (origin/main before the change, E2E_OLD_IS_ENV_SCRUB_BASELINE=1) the row FAILS and names the leaked variables. Left: deploy (RTS, Excellence, Confida; owner's go each), a live check from a real Crew's built-in shell that env no longer shows the names, the Docker socket (option 2), and the native-shell switch (option 3, asked of ai-work-2c by the owner). The macOS local app (Seatbelt) does not go through this launcher.
Owner decision (see DECISIONS 2026-10-05): the CLI's built-in shell is off in Full mode on every CLI; native file read/edit, skills and
subagents stay; the bridge shell is the only shell. One switch, off by default: AGENTWORKS_CLI_NATIVE_SHELL=on restores it
(nativeshell.Enabled() in the provider). The prompt says so (native-shell-off section) so a model does not conclude it has no shell.
| CLI | Provider commit | Mechanism | Real CLI result |
|---|---|---|---|
| Codex 0.160.0 | 1073ab3 |
shell_tool and unified_exec disabled |
no shell tool offered; native edit worked; on restored shell |
| Claude Code 2.1.289 | 68ca08f | Bash/PowerShell/Monitor/BashOutput/KillShell dropped from --tools; --disallowedTools when tools=default |
no Bash call possible incl. subagent; edit worked; on restored |
| Muse 1.4.2 | 01d351e | shell tools out of the allowlist baked into the hook |
bash refused with reason naming execute_shell_command; edit worked; on restored |
| Agy 1.2.16 | 24f86bc | PreToolUse hook denies run_command/send_command_input
|
denied with reason; write_to_file worked; on restored |
| Cursor 2026.10.01 | f740613 | preToolUse deny + beforeShellExecution deny; Shell(*) dropped from allow |
live 2026-10-05 with the RTS key (CURSOR_API_KEY, process only): interactive and structured, switch off: shell refused, native Write worked; interactive on: shell ran. Structured on: the shell is "Rejected" by Cursor itself, as it was BEFORE this change (checked on the previous commit): --print never ran the built-in shell in Full mode |
| Pi | n/a | already bridge-only | n/a |
Existing live tests that drive the built-in shell now set the switch on. cli-sandbox-contract needs the server started with the switch on
(noted in the command).
- The bridge shell in the SAME chat was not driven live on any CLI (stubs only); a real Crew chat on an isolated server (
id -unrefused, native edit works,execute_shell_commandworks) was not run, nor the three contracts (they need the switch on). - Codex structured and Claude print paths: argument construction only, not live. Claude: a skill declaring
allowed-tools: Bashnot tested. - No deploy: Excellence and RTS need their own live check and the owner's go per server.
PLAT-491, P1, open. Found by a read-only probe on Excellence: the Crew's built-in shell runs as the app account (member of every slot group), can print the Supabase service-role key and gog keyring password from its environment by name, and can use the app's rootless Docker.
Auto-synced from docs/ on main. Edit there, not here.