8.8 KiB
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 + the MCP tool. It is the single sanctioned entry point for restart evaluation post-#657 (which inventoried the restart/reload/kill paths).
The drain-proof hard gate now executes inside this tool (#661, via PR #882):
an apply request (dry_run=False) is evaluated against a drain proof here and
denied when that proof is missing, expired, unclean, tampered with, or stale.
It is no longer a separate child operation. What remains a later child is only
the execution step — actually stopping and restoring a process. This tool
still never restarts anything: apply_supported is always false and
restart_performed is always false.
The coordinator now routes every request through the restart-class policy
matrix defined for #663. See
mcp-restart-classes.md for permissions, expected
blast radius, scoped drain and approval requirements, audit fields, and
recovery behavior for all nine classes.
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, and on dry_run=False runs the #661 drain-proof hard gate. Never restarts a process. |
drain_proof.gate_apply_restart |
drain_proof.py |
The #661 hard gate: verifies a drain proof against the current impact fingerprint, or records an authorized break-glass bypass. |
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 == activeand 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). Onlyactive(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
gitea_request_mcp_restart(remote, host, org, repo,
dry_run=True, request_override=False,
session_id=None, limit=200,
restart_class="full_mcp_restart",
target_session_id=None, target_role=None,
target_connector=None,
drain_proof_json=None,
request_break_glass=False)
It never restarts anything: apply_supported is always false and
restart_performed is always false.
Dry-run versus apply
| Call | Behavior |
|---|---|
dry_run=True (default) |
Read-only impact preview. No drain proof is required or consulted. |
dry_run=False |
The #661 drain-proof hard gate runs in this tool. The outcome is reported under apply_gate / apply_authorized; a denial also returns a durable incident descriptor. Still no restart. |
Authorization ordering
An apply requires both authorizations, and they are independent:
- Restart-class authorization (#663) — the requester's role and permissions
must allow the requested class, the class's approval requirement must be
satisfied, and any target-scoped class must name its target. Failing any of
these makes
allow_restartfalse. - Drain-proof gate (#661) — a valid, unexpired, clean proof bound to the current impact fingerprint, or an authorized break-glass.
apply_authorized is the conjunction: gate.allow and allow_restart. A clean
drain proof therefore cannot override a class or requester-role denial, and a
denied class never reports an authorized apply. apply_gate carries
drain_gate_allow and restart_class_authorized so a denial is attributable to
the authorization that produced it.
Break-glass
Break-glass bypasses the drain proof only — never the restart-class matrix.
It is honoured solely when request_break_glass is set and the environment
carries GITEA_BREAKGLASS_RESTART_AUTHORIZATION; like operator override, the
tool argument expresses caller intent and cannot be self-asserted by a worker
session. break_glass_requested and break_glass_authorized are both reported,
so a bypass is never silent.
Break-glass Restart Workflow (gitea_break_glass_restart, #664)
The dedicated MCP tool gitea_break_glass_restart provides the privileged emergency break-glass restart workflow when graceful drain cannot complete:
- Role Authorization (#664 AC1): Ordinary LLM worker roles (
author,reviewer,merger,reconciler) are denied fail-closed. Privilegedcontrollerrole or explicitGITEA_BREAKGLASS_RESTART_AUTHORIZATIONis required. - Required Parameters (#664 AC2):
reason: Mandatory non-empty string (min 10 characters).confirmation: Must equal exactly"I_ACKNOWLEDGE_BREAK_GLASS_MCP_RESTART_DISRUPTION".impact_ack: Must beTrue.
- Automatic Incident Creation (#664 AC3): Creates a Gitea incident issue (
[INCIDENT] Break-glass MCP restart invoked by ...) detailing the reason, timestamp, disrupted sessions, and linking#652 #653 #655 #630 #658 #662 #664. - Immutable Audit Entry: Records an immutable audit log entry under
event="break_glass_mcp_restart". - Mandatory Reconciliation (#664 AC4): Sets
reconciliation_required=Truerequiring post-restart reconciliation viagitea_reconcile_after_restart(#662).
Fail closed on apply
A missing, malformed, expired, unclean, tampered, or fingerprint-stale drain
proof denies the apply and returns an incident descriptor. An unknown restart
class denies before any of this. Ambiguity always denies.
Audit
Every evaluation carries an audit_record (event, coordinator version, verdict,
restart class, required permission, 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.