Enumerate every code/script/host path that can restart, reload, reconnect,
kill, or force-recreate an MCP process, classify each, and link it to the
guard that constrains it.
- mcp_restart_paths.py: machine-readable registry (single source of truth)
with classifications (sanctioned_narrow / guarded_fail_closed / forbidden /
removed / host_residual) plus fail-closed guards:
* assert_restart_attempt_registered() -- unknown restart attempts fail closed
* assert_no_daemon_self_replacement() -- daemon never os.execv/os.kill/os._exit
itself (source-tree scan; comment/docstring mentions ignored)
* assert_auto_restart_helper_absent() -- keeps the #685-removed
_trigger_mcp_auto_restart from returning
* assert_registry_wellformed() -- every path classified, guarded, referenced
- docs/mcp-restart-path-inventory.md: complete inventory table linked from
#655; documents residual host behaviors (/mcp reconnect) and rollout.
- tests/test_mcp_restart_paths.py: 17 tests -- registry well-formedness,
unknown-attempt fail-closed, daemon-self-replacement scan (with injected
violation + comment/docstring negative case), legacy-helper-removed
regression, pkill-stays-contamination (#630), and doc/module lock-step.
No behavior change to existing modules; regression assertions codify invariants
that already hold (per #657 flag-free-before-hard-block rollout). Links
#652 #653 #655 #656.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
6.9 KiB
MCP restart / reload / kill path inventory (#657)
Complete inventory of every code, script, and host path that can restart, reload, reconnect, kill, or force-recreate an MCP process in this project, with each path classified and linked to the guard that constrains it.
This document is the human-readable companion to the machine-readable registry
in mcp_restart_paths.py. The two are kept in
lock-step by tests/test_mcp_restart_paths.py:
every path_id below must appear in this file, and the source guards are run
against the live tree.
Roadmap linkage: this inventory is the enumeration step of the restart governance work — parent #655, restart-governance ADR #656, vision #652, roadmap #653. Related detection/guard work: master-advance staleness #591/#420, side-effect-free resolver #685, transport flap #584, manual-kill contamination #630.
Classifications
| Classification | Meaning |
|---|---|
sanctioned_narrow_recovery |
One-shot, safe-by-construction recovery that never targets the running daemon. |
guarded_fail_closed |
Detects a restart-requiring condition, then fails mutations closed and emits reconnect guidance. Never self-restarts. |
forbidden |
A workflow-safety violation; where an LLM tool could invoke it, it is marked contamination. |
removed |
A previously-existing unguarded restart primitive that has been deleted; a regression guard keeps it absent. |
host_residual |
Behavior owned by the host/IDE, outside this process's control. Documented, not code-guarded here. |
The rule
No component may perform an unguarded full restart of the MCP daemon. The
in-process daemon (gitea_mcp_server.py, mcp_server.py,
role_session_router.py) must never replace or terminate its own process:
replacing the process after the host has wired up the stdio pipes desyncs the
JSON-RPC transport (observed with Antigravity/Cascade hosts). Recovery is owned
by the host/operator via a client reconnect — the daemon only ever detects
and fails closed.
Inventory
| path_id | Classification | Mechanism | Guard | Refs |
|---|---|---|---|---|
cli_venv_bootstrap_execv |
sanctioned_narrow_recovery | CLI wrapper scripts re-exec into venv/bin/python3 via os.execv, guarded by sys.executable != venv_python. |
One-shot pre-import bootstrap; runs before any MCP transport exists and only when not already on the venv interpreter; idempotent guard prevents a re-exec loop. | #657 |
daemon_self_replacement |
forbidden | The daemon replacing/terminating its own process (os.execv/os.kill/os._exit) to reload code. |
Forbidden by design; enforced against the source tree by assert_no_daemon_self_replacement(). |
#657, #584 |
legacy_auto_restart_helper |
removed | A helper (_trigger_mcp_auto_restart) that actively restarted the server from the read-only resolver path. |
Removed in #685; kept absent by assert_auto_restart_helper_absent(). |
#685, #657 |
config_touch_reload |
removed | Touching (utime) the MCP client config to make the host reload the server. | Removed from the resolver in #685: stale detection is report-only, never mutating config, spawning threads, or calling os._exit. |
#685, #657 |
master_advance_auto_restart |
guarded_fail_closed | On-disk master advancing past the running code. | master_parity_gate captures startup parity and blocks mutations while stale, emitting restart guidance; the process never self-restarts. |
#420, #591, #657 |
stale_runtime_resolver_reconnect |
guarded_fail_closed | The capability resolver detecting a stale serving process. | Report-only (#685): returns restart_required/stop_required and an exact reconnect action; no restart, thread, config touch, or os._exit. |
#685, #657 |
manual_daemon_kill |
forbidden | Shell kills of the daemon: pkill -f mcp_server.py, killall, broad pkill -f python sweeps, or kill <pid> of a daemon pid. |
Forbidden (#630): runtime_recovery_guard classifies these as contamination and gitea_record_daemon_process_kill_attempt writes a durable marker that fails later mutations closed. Operator maintenance authorization is read only from the environment. |
#630, #657 |
conflict_marker_infra_stop |
guarded_fail_closed | The daemon entrypoint scans for unresolved merge-conflict markers at startup and stops (sys.exit(1)). |
Fail-closed startup stop, not a restart: the process exits and waits for the operator to resolve conflicts and relaunch; never loops. | #657 |
ide_client_reconnect |
host_residual | A manual /mcp reconnect (or equivalent host action) that recreates the MCP client connection. |
Outside this process's control; the sanctioned recovery the gates point operators toward. No in-process code initiates it. | #584, #656, #657 |
profile_switch_runtime |
sanctioned_narrow_recovery | Switching the active execution profile at runtime (dynamic-profile mode). | In-process and restart-free: runtime_switching_supported is true, so a switch rebinds capability without recreating the process. |
#656, #657 |
Guards enforced in CI
tests/test_mcp_restart_paths.py asserts, against the live source tree:
- Registry well-formedness — every path has a valid classification, a non-empty guard description, references, and locations; ids are unique; all five classifications are represented.
- Unknown restart attempts fail closed —
assert_restart_attempt_registered()raisesUnknownRestartPathErrorfor any path id not in this inventory, so a novel/unnamed restart primitive cannot slip through silently. - Daemon never self-replaces —
assert_no_daemon_self_replacement()scans the daemon modules foros.execv/os.kill/os._exit/os.abortcalls (comment/docstring mentions are ignored) and finds none. - Legacy helper stays removed —
assert_auto_restart_helper_absent()confirms_trigger_mcp_auto_restarthas not returned. - pkill stays forbidden — a daemon
pkillcommand still classifies as contamination viaruntime_recovery_guard.
Residual host behaviors (outside process control)
/mcp reconnectin the IDE/host — the sanctioned recovery for stale-runtime, transport-flap (#584), and worktree-binding conditions. The daemon can only emit guidance toward it.- Host-level process management (the operator relaunching the daemon after a fail-closed stop, or after resolving merge conflicts).
These are documented rather than code-guarded because the process cannot observe or gate them from inside itself.
Rollout
Per #657, guards are introduced flag-free as regression assertions (they
codify invariants that already hold) before any hard runtime block is layered
on. When the restart coordinator (#655/#656) lands, registered paths gain a
coordinator token/capability check; unregistered attempts already fail closed
today via assert_restart_attempt_registered().