kb://architecture/TUI_PERMISSIONS_CONFIG_PHASE_BC_2026-06-16active2026-06-17

#489 Phase B + C — Permissions/Auto-mode + Config self-edit (design)

openalicedesignalice-core

#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)

ModeBehaviour
planAlice 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-editFile-edit tools auto-approved; other mutations (shell, network, deploy) still prompt.
yoloEverything auto-approved (current de-facto behaviour).

Protocol (the core work)

  1. Request: client sends the active mode in the /v1/chat request body ("interaction": {"approval_mode": "normal"}). Server change: parse + thread into the agentic loop's tool-dispatch gate.
  2. 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).
  3. Decision channel: client replies via a small endpoint POST /v1/chat/approve {"stream_id":…,"approval_id":…,"decision":"allow|deny|allow_always"}. allow_always records a per-session standing rule for that tool/pattern.
  4. Client overlay: a new Overlay::Approval { id, tool, summary, … } rendered like the existing @-picker (Clear + centered_rect); keys a allow / d deny / A allow-always / Esc deny. Reuses Phase E's overlay plumbing.
  5. 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 that yolo still cannot silently bypass for the denylist.
  6. Budget kill-switch: per-turn token/time ceiling that force-suspends with an approval prompt when exceeded.

Sequencing (slices)

  • B1 (server): thread approval_mode into the dispatch gate + is_mutating classification (the is_mutating flag already exists from Phase 8N3) + the denylist safety floor. Emit approval_request + suspend; /v1/chat/approve resumes. Tests at the loop entry point.
  • B2 (client): send mode in the request; Overlay::Approval + key routing + the decision POST. Surface plan/auto-edit distinctly in the status line.
  • B3: allow_always standing rules + budget kill-switch + /approvals CLI 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:

  1. `/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).
  2. `alice-cli config get|set|path`: the same surface for scripts (the CLI already has many nouns; add config).
  3. Alice-self-edit (guarded): expose a config_set tool 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: /config view + alice-cli config get/path (read-only, zero-risk).
  • C2: /config set + alice-cli config set through ConfigLoader (write + hot-reload), human-only.
  • C3: config_set tool 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.