Implements decision R6 from conductor-design.md §3 (landed in #275). This is the first slice of the reframe and the smallest.
Why
The conductor's tool surface today is an accident, not a decision. #createConductor (session-manager.ts:2733) builds a plain provider session with no capability role and no disallowedTools; allowedTools is a pre-approval list, not a restriction. So the conductor holds the backend's entire default toolset — Write, Edit, Bash, WebFetch — behind nothing but the canUseTool prompt, while its system prompt claims "You have NO tools to edit files or run commands yourself."
R6 retires the read-only-by-construction framing (it was never implemented: WRITE_TOOLS excludes Bash by design, and fleet_spawn(shape:"ship") obtains write on owner approval anyway). What replaces it is scope, not capability — which is both enforceable and strictly narrower than today's accident.
Scope
| Where |
Posture |
~/.codeoid-conductor (its own workdir — already a dedicated empty non-repo, non-$HOME dir) |
write auto-approved |
| Outside a git repo |
write approval-gated (today's default, made intentional) |
| Inside a git repo |
write never auto-approved; hard-denied where a live session holds that repo's worktree |
Bash inside a repo |
denied — it defeats every path scope above |
Where the enforcement goes (resolved, design §12 Q8/Q9)
In the gate, not in tool-safety.ts. The two questions are different kinds:
tool-safety.ts stays a pure classifier — is this verb read-only? — answerable from a tool name alone, no dependencies, no context. That purity is load-bearing for its tests and is why it can be trusted as a security boundary.
- The gate makes the contextual decision — may this session write this path right now? — which needs the workdir, the repo boundary, and the live worktree map.
A path field on the capability role was the alternative and it loses: it drags fleet state into the classifier.
Note: this is differentiated, not catch-up
The worktree-conflict check is deterministic because the daemon already knows which session owns which worktree (fleet_list returns it). A kernel sandbox has no concept of a session, so "don't write where another agent is working" is an invariant only a session-owning control plane can enforce. It is not sandbox parity work — peers with better sandboxes structurally cannot do it.
Verification
- Conductor can write notes in its own workdir with no prompt
- Conductor write into a repo with a live session's worktree is denied with a clear reason naming the owning session
Bash in a repo denied; Bash in its own workdir behaves per mode
fleet_list and WebFetch run silently under a non-claude provider
tool-safety.ts keeps its no-import property (there is already a test walking that graph for local-auth.ts — mirror it)
Implements decision R6 from conductor-design.md §3 (landed in #275). This is the first slice of the reframe and the smallest.
Why
The conductor's tool surface today is an accident, not a decision.
#createConductor(session-manager.ts:2733) builds a plain provider session with no capability role and nodisallowedTools;allowedToolsis a pre-approval list, not a restriction. So the conductor holds the backend's entire default toolset —Write,Edit,Bash,WebFetch— behind nothing but thecanUseToolprompt, while its system prompt claims "You have NO tools to edit files or run commands yourself."R6 retires the read-only-by-construction framing (it was never implemented:
WRITE_TOOLSexcludesBashby design, andfleet_spawn(shape:"ship")obtains write on owner approval anyway). What replaces it is scope, not capability — which is both enforceable and strictly narrower than today's accident.Scope
~/.codeoid-conductor(its own workdir — already a dedicated empty non-repo, non-$HOMEdir)Bashinside a repocanUseToolgateFLEET_TOOL_NAMES) into the auto-approved set — today they only auto-approve underclaudeviaallowedTools, so every other provider prompts onfleet_list(see Support non-Claude orchestrators for collaborative sessions (mountable fleet MCP) #245)WebSearch/WebFetchauto-approved for the conductor. An always-on assistant that prompts on every fetch is not an assistant, andNETWORK_TOOLSalready classifies both as read-only fetchesBashdenied inside a repo for the conductor roleWhere the enforcement goes (resolved, design §12 Q8/Q9)
In the gate, not in
tool-safety.ts. The two questions are different kinds:tool-safety.tsstays a pure classifier — is this verb read-only? — answerable from a tool name alone, no dependencies, no context. That purity is load-bearing for its tests and is why it can be trusted as a security boundary.A path field on the capability role was the alternative and it loses: it drags fleet state into the classifier.
Note: this is differentiated, not catch-up
The worktree-conflict check is deterministic because the daemon already knows which session owns which worktree (
fleet_listreturns it). A kernel sandbox has no concept of a session, so "don't write where another agent is working" is an invariant only a session-owning control plane can enforce. It is not sandbox parity work — peers with better sandboxes structurally cannot do it.Verification
Bashin a repo denied;Bashin its own workdir behaves per modefleet_listandWebFetchrun silently under a non-claudeprovidertool-safety.tskeeps its no-import property (there is already a test walking that graph forlocal-auth.ts— mirror it)