Repository navigation
Conversation
…ithin it project set --sandbox-policy records the read and write paths the orchestrator may add to one task's sandbox (task update --sandbox) or a rung's (ladder set --sandbox). Setting or widening it is owner-required; narrowing or clearing it is operational. Credentials, network hosts, the session bus and sockets stay owner-only whatever the policy lists, and anything outside it opens the owner's decision. Each grant records a setting audit, a sandbox grant event and sandbox evidence naming who granted it, the policy version and the items. task show and status list active widenings. A policy change revokes grants it no longer covers, and every dispatch judges them again. A worker's credential ask for sandbox items inside the policy goes to the orchestrator. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Bugbot couldn't run - usage limit reachedBugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit. A user or team admin can review and increase usage limits in the Cursor dashboard. (requestId: 04c4a007-2bff-43c7-951b-11666e676811) |
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Bugbot couldn't run - usage limit reachedBugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit. A user or team admin can review and increase usage limits in the Cursor dashboard. (requestId: 4b72a100-278b-4bee-b271-327f707740ed) |
There was a problem hiding this comment.
This is an auto review done by revuto.
I found one problem in how the owner-only sockets class is enforced. Credentials and the session bus are refused when a grant path holds them (within(d, p)). Sockets are only refused when the item is itself a socket or touches /run, /var/run or the runtime dir. A directory that contains a socket elsewhere passes sandbox.grant without the owner, and the check at dispatch time has the same gap. This goes against docs/state.md#sandbox-policy, which says "no policy hands one out".
| let socket = false; | ||
| try { socket = fs.statSync(p).isSocket(); } catch { /* not there yet */ } | ||
| if (socket || touches(r.sockets)) return 'sockets'; |
There was a problem hiding this comment.
This is an auto review done by revuto.
Granting a directory that holds a socket skips the owner. The sockets class only catches an item that is itself a socket (fs.statSync(p).isSocket()) or that touches /run, /var/run or the runtime dir. A directory holding a socket outside those places is not caught. Credentials and the bus, by contrast, are refused for any path that holds one.
So under a policy like /tmp/** or ~/**, the orchestrator can grant itself:
read /tmp/ssh-XXXX(an ssh-agent socket),/tmp/.X11-unixor/tmp/tmux-$UID(input injection, or commands run outside the sandbox),~/.colima/defaultor a podman machine dir (a docker-equivalent socket).
ownerOnly passes each of these, and live() at dispatch never notices either. Even a read grant is enough here: Linux connect() on a Unix socket is not blocked by a read-only mount. This repo already treats socket reach as path-based; see wholeMount in lib/agents.js, where a codex write path holding the runtime dir mounts the bus.
That breaks the #sandbox-policy promise that "no policy hands one out". A fix would be to also return sockets when the item is a directory that holds a socket. One way is a bounded walk or a find -type s-style scan. The other is to treat "holds a socket" as owner-only, with the same within(d, p) direction the credentials check uses. That judgement should also run again at dispatch.
There was a problem hiding this comment.
This is an auto review done by revuto.
The earlier owner-only socket finding is still open. Granting a directory containing a socket outside the recognized runtime locations can bypass owner approval.
| if (touches(r.bus)) return 'session_bus'; | ||
| let socket = false; | ||
| try { socket = fs.statSync(p).isSocket(); } catch { /* not there yet */ } | ||
| if (socket || touches(r.sockets)) return 'sockets'; |
There was a problem hiding this comment.
This is an auto review done by revuto.
[P1] Still open: directory grants bypass the owner-only socket check. fs.statSync(p).isSocket() checks only the granted path itself, and r.sockets contains only /run, /var/run and the runtime directory. Under a policy covering /tmp/**, a write grant for /tmp/ssh-XXXX therefore passes even when it contains an ssh-agent socket, exposing that socket without owner approval. live() uses the same check, so dispatch retains the grant. Reject directories containing sockets too, and add a regression covering both grant-time and dispatch-time checks.
…licy The sockets class caught only an item that was a socket itself or lay near /run, /var/run or the runtime directory. A granted directory holding an ssh-agent, X11, tmux or colima socket passed, and connect() needs no write access. ownerOnly now walks a granted directory for sockets and links to them, counting an unreadable directory as one that may hold a socket, and live() runs the same check at every dispatch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Bugbot couldn't run - usage limit reachedBugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit. A user or team admin can review and increase usage limits in the Cursor dashboard. (requestId: 2fc5617d-26a7-4e31-9796-dff70189a1cd) |
What
The owner sets a sandbox grant policy once. Inside it, the orchestrator widens a task's sandbox itself.
project set --sandbox-policy JSONrecordssandbox_policy:{ version, orchestrator_may_grant: { read, write }, owner_only }. Paths are absolute or start with~or<project>, with*,?and**globs. Setting it or adding a path issandbox.policy(owner-required). Removing paths ornullissandbox.policy.narrow(operational). The orchestrator can narrow or clear the policy but not widen it.task update ID --sandbox JSONis a per-task widening, stored assandbox_grantsrecords. Every later dispatch for that task adds it to the rung's sandbox. If every added item lies inside the policy and outside the owner-only classes, the command issandbox.grant(operational). Any other item opens the owner's decision as before, and itswhynames each item and the reason.ladder set RUNG --sandboxfollows the same rule, andproject.jsonsandbox_grantsrecords the rung items added under the policy.credentials(well-known stores under~, the state'shomes/brokers/ci-artifacts,owner_config_dir, anyenv_file, and any path holding one of these),network(domains),session_bus, andsockets(existing sockets, a directory that holds one at any depth or a link to one, such as an ssh-agent directory or/tmp/.X11-unix, a directory the check cannot read through,/run,/var/run, the runtime dir). A read grant counts too, sinceconnect()needs no write access. Per the scope change relayed by the orchestrator,orchestrator_may_grant.socketsis refused: the docker socket and the session bus stay owner-only.settingaudit, asandbox grantevent (by,role,policy_version,granted,removed) andsandboxevidence on the task.tower-crane evidence --type sandboxis refused.task showlistssandbox widenings:andstatuslists open tasks with active widenings.sandbox revokeevent and evidence. Every dispatch judges policy-backed grants again on canonical paths, links included. A grant that now falls outside, such as a granted path that has become a link into~/.ssh, is left out and named in a spawn notice.ask --kind credential --setting sandbox --sandbox JSON: if every item is inside the policy, the question goes to the orchestrator (technical: true,routed.sandbox_policy). Otherwise it stays with the owner.#sandbox-policysection (including the note that/run/docker.sockis root-equivalent), plus thesandbox_grantsfields, thesandboxevidence type and the new events. docs/cli.md regenerated. Both skills got one line each.Why
Owner direction, relayed 2026-10-21: the owner should be asked only when really needed. Path grants for a worker no longer wait on the owner. Docker-style host actions move to T468, so sockets stay owner-only here.
Verification
test/sandbox-policy.test.js:~/.sshand a socket under a listed/run/**), and narrowing versus widening.task showandstatuschecked, and dispatch shows T1 writes the path while T2 does not. A grant outside the policy is refused and opens a decision. The orchestrator cannot widen the policy. Each owner-only class stays the owner's. The worker ask is routed. A rung grant works. Narrowing revokes, and the next dispatch no longer writes the path.lib/sandbox-policy.jsreverted to the previous head (2 of 3 tests fail) and pass with it.test/authority,board-controls,owner-burden,project,commands,package,shared-files,evidence,decision-accessandstate-compatibility: all pass.authority,board-controls,evidence,project,render,settings,spawn-cmdpass;spawnpasses except 2 tests that bind a broker socket under a temp path too long for this sandbox (no short writable path here). The host tests gate runs them.test/sandbox-extensions,isolation,spawnand others fail the same 59 tests in the worker sandbox with or without this change. A claude spawn needs a temp directory under the real~/.cache, which the sandbox keeps read-only, so the dispatch checks in the new test use a codex stub. The host tests gate runs those files in full.npm run check:shared -- --base origin/mainpasses.Limits
ladder set.Note
High Risk
Changes sandbox confinement and who can widen agent filesystem/network access; misconfigured policy or grant logic could expose credential paths or host sockets.
Overview
Introduces a sandbox grant policy so the owner defines once which read/write paths the orchestrator may add without an approval, while credentials, network, session bus, and sockets stay owner-only regardless of policy.
project set --sandbox-policystores versionedsandbox_policywith glob paths (~,<project>,*,**). Widening the policy is owner-required; narrowing or clearing it is operational.task update --sandboxand matchingladder set --sandboxwiden one task or rung: additions fully inside the policy become operationalsandbox.grant; anything else still opens an owner decision. Grants are audited (sandbox grant/sandbox revokeevents,sandboxevidence on tasks), shown intask show/status, and re-checked at every dispatch—policy narrowing or path resolution changes revoke stale grants.ask --kind credential --setting sandbox --sandbox JSONroutes to the orchestrator when every requested item fits the policy. Newlib/sandbox-policy.jsimplements parsing, matching, dispatch merge into spawn settings, and board/settings UI hooks.Reviewed by Cursor Bugbot for commit 35cb9ea. Configure here.
Pre-submit review
project set --sandbox-policy(lib/project.js), test "the orchestrator grants paths inside the owner's policy" sets it as owner and checks version and owner_only: pass. Orchestrator per-task grant with audit and evidence: lib/tasks.js taskUpdate; the same test checks thesettingaudit (sandbox.grant), thesandbox grantevent,sandboxevidence,task show,status, and that T1's dispatch writes the path while T2's does not: pass. Per-rung grant:ladder set medium --sandboxand itssandbox_grantsrecord: pass. Outside the policy stays owner-required: refusal,{task, sandbox}escalation andwhy, no change to T1: pass. Owner-only classes never delegate: credentials, sockets (including a directory holding one), network and session_bus refused under~/**and/run/**, in the CLI tests and the variant table: pass. Orchestrator cannot widen the policy: two refusals that open decisions: pass. Narrowing revokes at next dispatch:sandbox revokeevents, task and rung removal, next dispatches without the paths; the drift test checks dispatch-time revocation for a link and for a socket that appears later: pass.--sandboxusage, flag, description andsandbox_grantsfield; docs regenerated,check:sharedpasses. Revuto P1 (directory holding a socket skips the owner): failure class is socket reach through a granted path that is not itself a socket. Siblings: a socket at depth, a link to a socket, an unreadable directory, read versus write grants, grant time versus dispatch time (live()), task versus rung grants (same ownerOnly). Table rows in test/sandbox-policy.test.js variant table plus the CLI drift test; both fail without the fix and pass with it.ci checks); its two socket findings are fixed in lib/sandbox-policy.js and show as no longer open.🤖 Generated with Claude Code