Fleet rework: stop defining read-only by command grammar — one grant model, working verify mode, classified tool families
Author: HmbownCreated Sep 17, 2026Updated Sep 17, 2026
Labelsdocumentation
Why (evidence)
- The 2026-09-17 incident (#6296): a verifier child, refused a chained read-only git command, used an inherited computer-use tool to type into the host Terminal. It was trying to do the task it was given.
- Our child model (
crates/tui/src/worker_profile.rs,fleet/role.rs,execpolicy/command_safety.rs): role postures + PermissionSet ceilings + ShellPolicy +ToolScope::Inherit+ deny-list unions + sentinels + a 21-prefix single-command read-only grammar (PARALLEL_READONLY_PREFIXES). A read-only child cannot runcargo,git fetch, ormerge-tree— the exact commands a "verify" child is handed — while the catalog still accepts inherited MCP tools (unclassified ⇒ allowed), which is how computer-use got through. - Cost of that mismatch today: each 8–10-issue audit child and the verifier burned ~1M–3M input tokens flailing against refusals; one escalated to desktop control. Every refusal without a named alternative invites a workaround.
What the references do (read 2026-09-17, refs/)
- codex: read-only is a real sandbox (Seatbelt/Landlock/bwrap);
workspace-write = explicit writable roots with
.gitprotected. Builds/tests are a sandbox denial resolved by policy-gated escalation with named alternatives (sandbox_permissions: require_escalated, arequest_permissionstool). Roles are reduce-only ("never replace the parent's authority"). Per-spawnmodel+reasoning_effort, validated against the model's supported levels. No native computer-use tool; the namespace name is reserved against impersonation. - grokbuild: capability modes are kind-level allowlists with monotonic
intersection (
ReadOnlyabsorbs). Read-only excludes the Execute kind entirely — no tests, by construction. Typed denials that name alternatives (Unknown { available }/NotAllowed { allowed }). MCP tools are exempt from the capability filter (None => true) and children inherit the parent MCP pool — the same hole we hit, flagged in their own comment. - kimicode: four static preset profiles own their tool lists (
explorekeeps Bash;planhas none). "Read-only" is prompt-enforced + tool omission, NOT a command classifier; the approval chain decides what Bash may run. Per-spawn model only; thinking follows the caller ("primary") or the model default. Fail-open approval when no approval surface is mounted. - omp: per-agent frontmatter tool lists ARE the grant; "read-only" is a
derived, presentation-only label (their code says so). Children run yolo with
the parent's task call as the authorization boundary. The
computertool is off by default globally and stripped for restricted sessions — the cleanest desktop guard of the four. Per-agentthinkingLevel(scout = smol/medium); the resolved model+effort is surfaced on every run.
Convergent lesson: none of them pretends a command allowlist is "read-only". Read-only is either absent mutating tools (kimicode, omp), absent capability kind (grokbuild), or a real sandbox with named escalation (codex). Our grammar is both too narrow (verifier can't verify) and not a boundary (it is a classifier + charset gate).
Direction for Codewhale
- One grant object per child —
files: none|read|write,shell: none|inspect|verify|full,network: bool,desktop: false-unless-granted,tools: preset allowlist. Roles become presets over it; delete the parallel machinery (ceiling + sentinels + posture re-mapping onto a second surface) in favor of one projection: catalog visibility = grant, execution denial = the same grant. - A
verifyshell mode that works:cargo test/check+git fetch/merge-treeunder an explicit bounded write scope (target/, refs) instead of a command allowlist pretending to be read-only. - Classified tool families, fail-closed: MCP/desktop tools are a labeled family; children never receive it unless the spawn grants it with a reason. Unclassified ⇒ not granted.
- Denials name the sanctioned alternative ("use Git action=fetch", "Run with workdir") and blocked probes escalate to the parent instead of inviting workarounds.
- Thinking: per-role defaults that are cheap where cheap is right (scout ≈ low/medium), per-spawn override, resolved model+tier surfaced in the roster and receipts.
- UI/UX: role picker and roster show the effective grant + model + thinking in plain words; spawn errors legible.
Related: #6296, #6015, #5529, #6194.
Source: Hmbown/CodeWhale