#489 Phase B + C — Permissions/Auto-mode + Config self-edit (design)
Author: Norbert · Date: 2026-06-16 · Status: design / awaiting NAO go Context: #489 "ultra-ideal TUI". Phases A (themes), D (markdown + syntax highlight + /resume + /export + /doctor + !shell), and E (visual theme picker) are shipped on mvp (tui 166/0). B and C remain — and unlike A/D/E they are not TUI-local: they touch Alice's server runtime and the /v1/chat protocol. This doc scopes them honestly so they can be executed as proper slices, not rushed cosmetics.
The architectural truth (why B/C aren't quick TUI toggles)
The Alice TUI is a thin client to an Alice server (local or remote via /endpoint). Tool execution happens server-side inside Alice's agentic loop — the client only streams the reply over an SSE-like channel. Verified: InputMode {Normal, AutoAccept, Plan} already exists in app.rs, is cycled by Shift+Tab and /mode, but is never sent to the server and gates nothing — it is a cosmetic label today.
Therefore real "permissions / approval" is a protocol feature, not a client flag: the server must be able to pause before a mutating tool, ask the client, and wait for an approve/deny. That spans server + protocol + client.
Phase B — Permissions / Auto-mode (approval protocol)
Mode semantics (align with Claude Code / opencode)
| Mode | Behaviour |
|---|---|
plan | Alice produces a plan and does not execute mutating tools. |
normal (default) | Each mutating tool call pauses for client approval. Read-only tools auto-run. |
auto-edit | File-edit tools auto-approved; other mutations (shell, network, deploy) still prompt. |
yolo | Everything auto-approved (current de-facto behaviour). |
Protocol (the core work)
- Request: client sends the active mode in the
/v1/chatrequest body ("interaction": {"approval_mode": "normal"}). Server change: parse + thread into the agentic loop's tool-dispatch gate. - Approval-request event: when the loop is about to run a tool that the mode requires approval for, it emits a new SSE event
{"type":"approval_request","id":"<uuid>","tool":"shell","summary":"rm -rf …","is_mutating":true}and suspends that turn awaiting a decision (with a timeout → auto-deny). - Decision channel: client replies via a small endpoint
POST /v1/chat/approve {"stream_id":…,"approval_id":…,"decision":"allow|deny|allow_always"}.allow_alwaysrecords a per-session standing rule for that tool/pattern. - Client overlay: a new
Overlay::Approval { id, tool, summary, … }rendered like the existing@-picker (Clear + centered_rect); keysaallow /ddeny /Aallow-always / Esc deny. Reuses Phase E's overlay plumbing. - Sensitive-file / param guard: server-side allowlist+denylist (e.g. always prompt for paths under
~/.ssh,.env*, deploy scripts) independent of mode — a safety floor thatyolostill cannot silently bypass for the denylist. - Budget kill-switch: per-turn token/time ceiling that force-suspends with an approval prompt when exceeded.
Sequencing (slices)
- B1 (server): thread
approval_modeinto the dispatch gate +is_mutatingclassification (theis_mutatingflag already exists from Phase 8N3) + the denylist safety floor. Emitapproval_request+ suspend;/v1/chat/approveresumes. Tests at the loop entry point. - B2 (client): send mode in the request;
Overlay::Approval+ key routing + the decision POST. Surfaceplan/auto-editdistinctly in the status line. - B3:
allow_alwaysstanding rules + budget kill-switch +/approvalsCLI noun to inspect/clear standing rules.
Risk: suspend/resume inside the streaming loop is the delicate part — must not deadlock a turn or leak a suspended stream (cf. the steer/coalesce/cancel races already fixed in 8OOO). B1 ships behind a default of yolo so existing behaviour is byte-identical until a client opts in.
Phase C — Config: oa.yaml surfaced + hot-reload + self-edit
What already exists (reuse, don't rebuild)
Phase 9 (#442) + #457 shipped unified `oa.yaml` + ConfigLoader + hot-reload. C is therefore mostly surfacing, not new infra:
- `/config` (TUI): read the resolved
oa.yaml, render it in a scrollable overlay (reuse Phase E overlay)./config get <key>//config set <key> <val>writes through ConfigLoader (which already hot-reloads watchers). - `alice-cli config get|set|path`: the same surface for scripts (the CLI already has many nouns; add
config). - Alice-self-edit (guarded): expose a
config_settool to Alice gated by the Phase B approval path + a hard denylist of keys she may never self-set (secrets, provider keys, auth, kill-switch). Every self-edit emits an audit line + (in non-yolo) an approval prompt. This is why C depends on B — the guardrail for self-edit is the permission system.
Sequencing
- C1:
/configview +alice-cli config get/path(read-only, zero-risk). - C2:
/config set+alice-cli config setthrough ConfigLoader (write + hot-reload), human-only. - C3:
config_settool for Alice, gated by B's approval + key denylist + audit.
Honest dependency + recommendation
- C1 is shippable now (read-only config surface, no B dependency) — a clean quick win if NAO wants visible progress before B.
- B is the larger lift (protocol + suspend/resume) and C3 depends on B.
- Recommended order: C1 → B1 → B2 → C2 → B3 → C3, each its own gated slice, B1 behind a
yolo-default so nothing regresses until opted in.
Awaiting NAO's steer: full sequence as above, or C1-first for an immediate read-only config win while B is designed deeper.