diff --git a/docs/webui-restart-console.md b/docs/webui-restart-console.md new file mode 100644 index 0000000..12dd427 --- /dev/null +++ b/docs/webui-restart-console.md @@ -0,0 +1,102 @@ +# Web Console: restart status, impact preview, and approval state (#667) + +Phase 1 of the console restart surface. It consumes the #655 coordinator +substrate and displays it. It performs no restart, reload, drain, approval, or +process action, and it registers no write endpoint. + +Issue #667's rollout is explicit — *status views first, write approval after the +backend gates are green* — and this change delivers only the status half. + +## Surfaces + +| Path | Method | Purpose | +|------|--------|---------| +| `/runtime/restart` | GET | Restart status page | +| `/api/v1/system/restart/status` | GET | Same snapshot as JSON | + +Both accept an optional `restart_class` query parameter (default +`full_mcp_restart`). An unrecognised class is not an error: the coordinator +resolves it as unknown and fails closed, and the page shows the resulting deny. + +Neither path accepts `POST`; a write attempt returns `405`, and a test asserts +it. + +## What it shows + +* **Impact preview (#658)** — verdict, blast radius, affected sessions, leases, + critical sections, mutations, and the counts behind them, evaluated + `dry_run=True` against live control-plane state. +* **Drain proof (#661)** — verification of a supplied proof: valid, clean, + expired, tampered, and the reasons behind a refusal. +* **Post-restart reconcile (#662)** — the most recent completion proof, its + overall status, and which dimensions still require follow-up. +* **Restart classes (#663)** — the least-privilege matrix, with *you may + request* and *you may execute* computed for the viewing role rather than for a + generic operator. +* **Approval controls (#633)** — the authorization state of + `system.restart_namespace` and `system.reload_namespace`. +* **Break-glass (#664)** — declared and marked unavailable; see below. + +## Three rules this surface holds itself to + +A status page that is wrong is worse than one that is missing, because an +operator acts on it. Three properties are enforced by tests, and each was +verified by reverting the guard and watching a test fail. + +### An unreadable source reports unavailable, never green + +Every source carries its own `SourceStatus`. Nothing substitutes a default, +placeholder, or self-comparison for a reading that failed. An unreadable +control-plane database yields `inventory_complete: false`, which the coordinator +itself turns into a fail-closed verdict, and the page says the blast radius is +unknown rather than showing an empty affected-sessions table. + +An absent drain proof is reported as absent — not as a pass. The #661 gate +authorizes a restart only against a valid, unexpired, clean proof, so no proof +is precisely the state that gate denies on. + +### Authorization is asked the way execution would ask it + +Every probe passes `for_execution=True`. + +Asked without it, an admin is `allowed` for `system.restart_namespace`. On a +control surface that reads as a live button. Asked the way an execution attempt +would ask, the same principal is refused `phase_not_active`, because the console +is in Phase 1 and the action is Phase 2. This surface reports the second answer. + +`execution_enabled` is therefore `false` for every action and every role today, +and a test asserts that across the whole role matrix. + +### The control-plane database is opened read-only + +`ControlPlaneDB()` creates directories and runs migrations on construction — a +write. This surface never constructs one. It opens the sqlite file with +`mode=ro`, exactly as `webui/inventory.py` does, and treats a missing file as +missing authority rather than as an empty inventory. + +The test that protects this points at a path inside a directory that already +exists, so a read-write `connect` would really create the file. A nested +missing-directory path would have passed for the wrong reason. + +## Break-glass is declared, not offered + +The break-glass workflow (#664) is not available on this branch's base. The +panel is rendered to operator-class roles as **unavailable**, naming the issue +that tracks it. It is not silently omitted, because an operator who has been +told a governance path exists needs to see that it is not wired here; and it is +not rendered as a control, because there is nothing behind it. + +Unprivileged viewers see only a note that the surface is operator-class. + +## Redaction and escaping + +Every interpolated value passes through `_esc` (`html.escape(..., quote=True)`). +Free-form text and anything that can carry a filesystem path additionally passes +through `webui.inventory.scrub_text`, which redacts credential-shaped tokens +inside a string rather than only at its start. The impact payload is passed +through `webui.inventory.scrub` before rendering. + +## Linkage + +Parent #655 · extends #642 · consumes #658, #661, #662, #663 · RBAC #633 · +console #631 · vision #652 · roadmap #653 · break-glass #664.