Files
Gitea-Tools/docs/mcp-restart-path-inventory.md
sysadminandClaude Opus 4.8 3428fb4190 feat(mcp-health): inventory and guard MCP restart/reload/kill paths (#657)
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]>
2026-07-24 00:57:33 -04:00

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:

  1. 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.
  2. Unknown restart attempts fail closedassert_restart_attempt_registered() raises UnknownRestartPathError for any path id not in this inventory, so a novel/unnamed restart primitive cannot slip through silently.
  3. Daemon never self-replacesassert_no_daemon_self_replacement() scans the daemon modules for os.execv/os.kill/os._exit/os.abort calls (comment/docstring mentions are ignored) and finds none.
  4. Legacy helper stays removedassert_auto_restart_helper_absent() confirms _trigger_mcp_auto_restart has not returned.
  5. pkill stays forbidden — a daemon pkill command still classifies as contamination via runtime_recovery_guard.

Residual host behaviors (outside process control)

  • /mcp reconnect in 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().