feat(mode): the access cap — plan mode composes with the sandbox instead of banning bash
A ModeDefinition may declare access: the widest sandbox access shell commands run under while the mode holds, on the SANDBOX_MODES ladder. The bash seam gains the resolution point to hang it on: BashExecutor. resolveMode(session) folds override ?? default and dispatches the new bash/resolve-mode waterfall; dsh-tool-bash consults it at both the stamping site and the escalation baseline; dsh-mode's clamp listener takes the ladder minimum per call. Two independent log folds compose at read time — the mode never writes the sandbox knob, so the two switch in any order and the knob re-emerges intact on exit. The built-in plan definition ships access: read-only with the bash trio allowlisted CONDITIONALLY: both policy layers admit bash/bash_output/ bash_kill only while a confining executor is mounted (an unconfinable shell cannot honor the cap), and a bash call carrying sandbox_permissions under a cap is denied at the gate — no widening mid-mode; the widened step belongs in the plan. examples/plan-acp-agent swaps bash-local for sandbox-local + bash-sandbox (workspace-write default, clamped read-only inside plan) plus the approval seam; the re-recorded plan-mode arc runs a real cat inside plan under the clamped sandbox, and modes-advertise now pins the sandbox-mode and approval config options. RFC amended to the landed shape (access cap section, orthogonality FAQ, deferred item resolved into effects self-declaration).
This commit is contained in:
@@ -4,7 +4,7 @@ The coding agent as an ACP server with **session modes** composed — the live c
|
||||
|
||||
## What it demonstrates
|
||||
|
||||
`session/new` advertises the mode picker (`default` / `plan`); the editor's `session/set_mode` switches the session, applied at the next turn boundary. The default mode carries the full composition — `bash`, `read`/`write`/`edit`, `todo_write`, `ask_user_question` — while plan mode narrows it to the read-only allowlist (`read`, `todo_write`, `ask_user_question`, `exit_plan_mode` here — this tree loads no web tools) plus the plan-mode guidance section; every call outside the allowlist is denied at `tools/pre-execute` with a reason that steers it back to planning, and a blocking decision goes to the user through `ask_user_question`. The model leaves by presenting its plan through `exit_plan_mode`: the plan markdown renders as the tool's call card, the review question arrives as an elicitation form (approve / keep planning, free text welcome), and a keep-planning answer returns the feedback to the model verbatim.
|
||||
`session/new` advertises the mode picker (`default` / `plan`) plus the sandbox-mode and approval config options; the editor's `session/set_mode` switches the session, applied at the next turn boundary. The default mode carries the full composition — `bash` under a `workspace-write` sandbox, `read`/`write`/`edit`, `todo_write`, `ask_user_question` — while plan mode narrows it to the read-only allowlist plus the plan-mode guidance section. The bash tools STAY available in plan: plan's `access: read-only` cap clamps the sandbox resolution per call (a `bash/resolve-mode` waterfall listener), so exploration commands run for real while a write is denied by the sandbox itself — and the session's own sandbox-mode knob is never written, so it re-emerges intact on exit. Sandbox escalation (`sandbox_permissions`) is denied inside plan — the widened step belongs in the plan; in the default mode it raises a real `session/request_permission` prompt through the approval seam. Every call outside the allowlist is denied at `tools/pre-execute` with a reason that steers it back to planning, and a blocking decision goes to the user through `ask_user_question`. The model leaves by presenting its plan through `exit_plan_mode`: the plan markdown renders as the tool's call card, the review question arrives as an elicitation form (approve / keep planning, free text welcome), and a keep-planning answer returns the feedback to the model verbatim.
|
||||
|
||||
## Run
|
||||
|
||||
@@ -16,4 +16,4 @@ Drive it from Zed or any ACP client; the mode picker appears on the session. Swi
|
||||
|
||||
## Tests
|
||||
|
||||
`pnpm run test:snapshot` replays three scenarios keyless. `modes-advertise` (authored): the `modes` advertisement on `session/new`, both `session/set_mode` round-trips with their optimistic `current_mode_update`, and the loud rejection of an unknown mode id, as committed wire bytes. `plan-mode` (recorded, the header pin): the full arc — setMode(plan), the plan-shaped initial header, the plan presented via `exit_plan_mode`, a scripted elicitation approve, the boundary-flushed `mode/set` back and the widened fallback header, then a real edit under the restored toolset. `plan-mode-reject` (recorded): the keep-planning branch, whose corrective `isError` carries the reviewer's free-text feedback verbatim and leaves the session in plan mode. The gate's deny texts stay pinned at the unit tier (`packages/mode/mode/tests`).
|
||||
`pnpm run test:snapshot` replays three scenarios keyless (the recorded bash re-executes for real under the host's sandbox runner — Seatbelt on macOS, bwrap on Linux CI). `modes-advertise` (authored): the `modes` advertisement and both config options on `session/new`, both `session/set_mode` round-trips with their optimistic `current_mode_update`, and the loud rejection of an unknown mode id, as committed wire bytes. `plan-mode` (recorded, the header pin): the full arc — setMode(plan), the plan-shaped initial header, a real `cat` run inside plan under the clamped read-only sandbox, the plan presented via `exit_plan_mode`, a scripted elicitation approve, the boundary-flushed `mode/set` back and the widened fallback header, then a real edit under the restored toolset. `plan-mode-reject` (recorded): the keep-planning branch, whose corrective `isError` carries the reviewer's free-text feedback verbatim and leaves the session in plan mode. The gate's deny texts and the sandbox-denial marker stay pinned at the unit tier (`packages/mode/mode/tests`, `packages/bash/tool-bash/tests` — a recorded denial's stderr would be the backend's dialect and replay only where it was recorded).
|
||||
|
||||
Reference in New Issue
Block a user