Child of umbrella #655 (governed MCP restart coordination); builds on the #657 restart-path inventory. Adds a central coordinator that evaluates live control-plane state before a restart and returns a blast-radius impact preview, so operators and the web console (#642/#652) can see what a restart would disrupt before concurrent LLM work is destroyed. Changes - restart_coordinator.py (new) — pure classification: inventory -> impact report DTO (RestartImpactReport/SessionImpact/LeaseImpact). Verdicts: safe / unsafe / override. Never restarts anything; fails closed on an incomplete inventory. - control_plane_db.py — additive ControlPlaneDB.list_sessions() read-only session inventory (the process-level unit a restart kills). - gitea_mcp_server.py — new dry-run MCP tool gitea_request_mcp_restart: gathers sessions/leases/terminal-lock from the #613 DB, calls the coordinator, returns the report. Override authority is read from the environment, never self-asserted (#630/#710 F1 pattern). Apply is gated by a later drain proof (non-goal here). - docs/mcp-restart-coordinator.md + docs/mcp-restart-impact-sample.json — doc and a real dry-run sample report. - docs/mcp-tool-inventory.md — register the new tool (inventory sync). - tests/test_restart_coordinator.py (new) — 15 tests: multi-session fixtures, deny-when-critical-section-open, fail-closed deny, override, terminal lock, stale heartbeat, JSON-serializable DTO, list_sessions. Tests: pytest tests/test_restart_coordinator.py -> 15 passed. Full suite: 13 failed / 4753 passed; all 13 reproduce identically on clean master @ef14622 (0 regressions). The residual test_issue_781 doc-registry failure is a pre-existing baseline gap for gitea_rebind_dirty_same_claimant_author_session (merged #864, undocumented on master) — out of scope for #658. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
96 lines
4.7 KiB
Markdown
96 lines
4.7 KiB
Markdown
# MCP restart coordinator and impact analysis (#658)
|
|
|
|
Before any sanctioned MCP restart, a central coordinator evaluates the live
|
|
control-plane state and produces an **impact preview** so operators and the web
|
|
console (#642 / #652) can see the blast radius *before* concurrent LLM work is
|
|
disrupted. Uncoordinated restarts destroy in-flight author/reviewer/merger work
|
|
and give operators no way to see what they are about to break.
|
|
|
|
This lands the coordinator + impact DTO + a dry-run MCP tool. It is the single
|
|
sanctioned entry point for restart evaluation post-#657 (which inventoried the
|
|
restart/reload/kill paths). The **mutative apply** path — actually performing a
|
|
restart — is a later child gated by a drain proof and is explicitly out of
|
|
scope here.
|
|
|
|
## Components
|
|
|
|
| Piece | Where | Responsibility |
|
|
|-------|-------|----------------|
|
|
| `restart_coordinator.evaluate_restart_impact` | `restart_coordinator.py` | Pure classification: inventory → impact report DTO. No I/O, no restart. |
|
|
| `RestartImpactReport` / `SessionImpact` / `LeaseImpact` | `restart_coordinator.py` | Console-facing DTO (`.as_dict()` is JSON-serializable). |
|
|
| `ControlPlaneDB.list_sessions` | `control_plane_db.py` | Read-only session inventory (the process-level unit a restart kills). |
|
|
| `gitea_request_mcp_restart` | `gitea_mcp_server.py` | MCP tool: gathers inventory from the #613 DB, calls the coordinator, returns the report. Dry-run only. |
|
|
|
|
## Dimensions evaluated
|
|
|
|
The coordinator classifies the inventory across the dimensions #658 requires:
|
|
|
|
- **Sessions** — every active MCP session; a restart terminates all of them.
|
|
Liveness = `status == active` **and** the owner pid is alive **and** the
|
|
heartbeat is fresh (default window 15 min). Dead/stale sessions do not count
|
|
toward blast radius.
|
|
- **Leases / locks** — control-plane leases joined with work items and their
|
|
freshness (`lease_lifecycle.classify_lease_freshness`). Only `active` (live
|
|
owner) leases are *disruptive*; expired / released / dead-process leases never
|
|
withhold a restart.
|
|
- **Issue / PR work** — the issues and PRs behind disruptive leases.
|
|
- **Mutations / critical sections** — a live lease carrying an author worktree
|
|
or a mutating phase (`implementing`, `publishing`, `merging`, …) is a
|
|
critical section a restart must not sever.
|
|
- **Terminal (merge) lock** — an active terminal lock always makes a restart
|
|
unsafe.
|
|
- **Prior recovery attempts** — narrower recovery already tried (e.g. sanctioned
|
|
client reconnects) is echoed so the operator sees the escalation history.
|
|
|
|
## Verdict
|
|
|
|
Exactly three verdicts, matching the acceptance criteria:
|
|
|
|
| Verdict | `allow_restart` | Meaning |
|
|
|---------|-----------------|---------|
|
|
| `safe` | `true` | No other live sessions, no live leases, no terminal lock. |
|
|
| `unsafe` | `false` | Live work would be disrupted and no operator override is present — **or** the inventory could not be completed (fail closed). |
|
|
| `override` | `true` | Live work present, but an operator override accepts the blast radius. |
|
|
|
|
`override_would_allow` tells the console whether an override path exists for the
|
|
current state. `blast_radius` is a `none` / `low` / `medium` / `high` severity
|
|
band derived from the affected session and work counts.
|
|
|
|
### Fail closed
|
|
|
|
If the control-plane inventory cannot be completed (DB unavailable, a listing
|
|
failed), `inventory_complete` is `false` and the verdict is `unsafe` / deny. An
|
|
incomplete evaluation must never green-light a restart.
|
|
|
|
### Operator override authority
|
|
|
|
Override authority is read from the environment variable
|
|
`GITEA_OPERATOR_RESTART_OVERRIDE_AUTHORIZATION` and **never** from a tool
|
|
argument. A worker session cannot set an environment variable on an
|
|
already-running daemon, so override cannot be self-asserted (same pattern as the
|
|
#630 daemon-maintenance authorization). The `request_override` tool argument only
|
|
expresses caller intent; it takes effect solely when the environment
|
|
authorization is present.
|
|
|
|
## The tool
|
|
|
|
```text
|
|
gitea_request_mcp_restart(remote, host, org, repo,
|
|
dry_run=True, request_override=False,
|
|
session_id=None, limit=200)
|
|
```
|
|
|
|
Read-only, dry-run, and it **never restarts anything**. `apply_supported` is
|
|
always `false`; passing `dry_run=False` performs no restart and reports that
|
|
apply is gated by a drain proof (a separate child).
|
|
|
|
## Audit
|
|
|
|
Every evaluation carries an `audit_record` (event, coordinator version, verdict,
|
|
allow decision, blast radius, counts, timestamp) so restart decisions are
|
|
auditable. No secrets flow through the coordinator — session ids, pids, and
|
|
profiles are operational metadata only.
|
|
|
|
A representative dry-run report is in
|
|
[`mcp-restart-impact-sample.json`](./mcp-restart-impact-sample.json).
|