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]>
147 lines
5.5 KiB
Python
147 lines
5.5 KiB
Python
"""Tests for the MCP restart-path inventory and guards (#657).
|
|
|
|
Covers:
|
|
* the registry is well-formed and every path is classified;
|
|
* unknown restart attempts fail closed (AC "fail closed on unknown restart");
|
|
* the previously-unguarded full-restart primitives stay guarded/absent
|
|
against the real source tree (AC "tests for at least one previously
|
|
unguarded path");
|
|
* pkill of the daemon is still classified as contamination (#630, AC3);
|
|
* the inventory doc and module stay in lock-step.
|
|
"""
|
|
|
|
import os
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
import mcp_restart_paths as rp
|
|
import runtime_recovery_guard
|
|
|
|
REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
DOC_PATH = os.path.join(REPO_ROOT, "docs", "mcp-restart-path-inventory.md")
|
|
|
|
|
|
class TestRegistryWellformed(unittest.TestCase):
|
|
def test_registry_is_wellformed(self):
|
|
# Must not raise.
|
|
rp.assert_registry_wellformed()
|
|
|
|
def test_every_path_has_valid_classification(self):
|
|
for path in rp.iter_restart_paths():
|
|
self.assertIn(path.classification, rp.VALID_CLASSIFICATIONS)
|
|
self.assertTrue(path.guard.strip(), path.path_id)
|
|
self.assertTrue(path.references, path.path_id)
|
|
self.assertTrue(path.locations, path.path_id)
|
|
|
|
def test_ids_are_unique(self):
|
|
ids = [p.path_id for p in rp.iter_restart_paths()]
|
|
self.assertEqual(len(ids), len(set(ids)))
|
|
|
|
def test_covers_every_classification(self):
|
|
present = {p.classification for p in rp.iter_restart_paths()}
|
|
self.assertEqual(present, set(rp.VALID_CLASSIFICATIONS))
|
|
|
|
|
|
class TestUnknownAttemptFailsClosed(unittest.TestCase):
|
|
def test_unknown_path_raises(self):
|
|
with self.assertRaises(rp.UnknownRestartPathError):
|
|
rp.assert_restart_attempt_registered("totally_novel_restart_hack")
|
|
|
|
def test_get_unknown_raises(self):
|
|
with self.assertRaises(rp.UnknownRestartPathError):
|
|
rp.get_restart_path("nope")
|
|
|
|
def test_registered_attempt_returns_path(self):
|
|
path = rp.assert_restart_attempt_registered("manual_daemon_kill")
|
|
self.assertEqual(path.classification, rp.CLASS_FORBIDDEN)
|
|
|
|
|
|
class TestDaemonNeverSelfReplaces(unittest.TestCase):
|
|
"""Previously-unguarded full-restart primitive: daemon self-replacement."""
|
|
|
|
def test_no_self_replacement_in_source(self):
|
|
# The live daemon modules must contain no os.execv/os.kill/os._exit
|
|
# self-restart call. Must not raise.
|
|
rp.assert_no_daemon_self_replacement(REPO_ROOT)
|
|
|
|
def test_scanner_flags_injected_violation(self):
|
|
# Guard the guard: prove the scanner catches a real self-replace call.
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
bad = Path(tmp) / "gitea_mcp_server.py"
|
|
bad.write_text(
|
|
"import os\n"
|
|
"def restart():\n"
|
|
" os.execv('/usr/bin/python', ['python'])\n",
|
|
encoding="utf-8",
|
|
)
|
|
found = rp.scan_daemon_self_replacement(tmp)
|
|
self.assertTrue(found)
|
|
with self.assertRaises(AssertionError):
|
|
rp.assert_no_daemon_self_replacement(tmp)
|
|
|
|
def test_scanner_ignores_comment_and_docstring_mentions(self):
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
ok = Path(tmp) / "gitea_mcp_server.py"
|
|
ok.write_text(
|
|
"import os\n"
|
|
"# NOT os.execv() to re-point the interpreter here.\n"
|
|
'"""Never calls os._exit to restart."""\n'
|
|
"value = 1\n",
|
|
encoding="utf-8",
|
|
)
|
|
self.assertEqual(rp.scan_daemon_self_replacement(tmp), [])
|
|
|
|
|
|
class TestLegacyAutoRestartHelperRemoved(unittest.TestCase):
|
|
"""Previously-unguarded full-restart path: _trigger_mcp_auto_restart."""
|
|
|
|
def test_helper_absent_in_source(self):
|
|
# Must not raise: helper was removed in #685.
|
|
rp.assert_auto_restart_helper_absent(REPO_ROOT)
|
|
|
|
def test_scanner_flags_reintroduced_helper(self):
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
bad = Path(tmp) / "mcp_server.py"
|
|
bad.write_text(
|
|
"def _trigger_mcp_auto_restart():\n return True\n",
|
|
encoding="utf-8",
|
|
)
|
|
with self.assertRaises(AssertionError):
|
|
rp.assert_auto_restart_helper_absent(tmp)
|
|
|
|
|
|
class TestPkillStaysForbidden(unittest.TestCase):
|
|
"""AC3: pkill of the daemon remains forbidden/contaminating (#630)."""
|
|
|
|
def test_manual_daemon_kill_registered_as_forbidden(self):
|
|
path = rp.get_restart_path("manual_daemon_kill")
|
|
self.assertEqual(path.classification, rp.CLASS_FORBIDDEN)
|
|
|
|
def test_pkill_classified_as_contamination(self):
|
|
assessment = runtime_recovery_guard.assess_recovery_command(
|
|
"pkill -f mcp_server.py"
|
|
)
|
|
self.assertTrue(assessment["contaminated"])
|
|
|
|
def test_read_only_probe_not_contamination(self):
|
|
assessment = runtime_recovery_guard.assess_recovery_command(
|
|
"ps aux | grep mcp_server"
|
|
)
|
|
self.assertFalse(assessment["contaminated"])
|
|
|
|
|
|
class TestInventoryDocInSync(unittest.TestCase):
|
|
def test_doc_exists(self):
|
|
self.assertTrue(os.path.exists(DOC_PATH), DOC_PATH)
|
|
|
|
def test_doc_mentions_every_path_id(self):
|
|
with open(DOC_PATH, encoding="utf-8") as handle:
|
|
doc = handle.read()
|
|
for path in rp.iter_restart_paths():
|
|
self.assertIn(path.path_id, doc, f"doc missing {path.path_id}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|