Adds docs/architecture/mcp-restart-governance.md, the restart-governance/v1 ADR defining who may restart the MCP control plane and under what conditions. - Recovery ladder (reconnect -> rebind -> scoped restart -> full restart -> host) with restart stated as the last resort. - Authorization matrix across author/reviewer/merger/reconciler/controller/ operator/admin; no LLM worker role may perform or authorize a full or host restart. - v1 authority decision recorded: controller approval + automated safety gates; quorum deferred to a superseding ADR. - Break-glass path with pre-declared incident and mandatory post-hoc audit. - Ambiguous policy state denies restart. - Stable policy IDs RG-01..RG-08 for later enforcement code to bind to. Cross-links the ADR from docs/safety-model.md and docs/webui-deployment.md, and adds tests/test_mcp_restart_governance_docs.py asserting acceptance criteria 1-5. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
108 lines
3.7 KiB
Python
108 lines
3.7 KiB
Python
"""Documentation acceptance for the MCP restart governance ADR (#656).
|
|
|
|
Enforces issue #656 acceptance criteria:
|
|
|
|
* AC1 — policy document exists with an authorization matrix and the recorded
|
|
v1 decision (controller approval + automated safety gates).
|
|
* AC2 — restart is stated as a last resort with enumerated narrower recoveries.
|
|
* AC3 — a unilateral LLM full restart with affected sessions is forbidden.
|
|
* AC4 — break-glass conditions are listed.
|
|
* AC5 — the ADR is linked to #655, #652, #653, #630, #642, and is cross-linked
|
|
from the safety model and the web-console deployment boundary docs.
|
|
"""
|
|
from pathlib import Path
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
ADR = REPO_ROOT / "docs" / "architecture" / "mcp-restart-governance.md"
|
|
ADR_BASENAME = "mcp-restart-governance.md"
|
|
|
|
CROSS_LINK_DOCS = (
|
|
REPO_ROOT / "docs" / "safety-model.md",
|
|
REPO_ROOT / "docs" / "webui-deployment.md",
|
|
)
|
|
|
|
LINKED_ISSUES = ("#655", "#652", "#653", "#630", "#642")
|
|
POLICY_IDS = ("RG-01", "RG-02", "RG-03", "RG-04", "RG-05", "RG-06", "RG-07", "RG-08")
|
|
|
|
|
|
def _read(path: Path) -> str:
|
|
assert path.is_file(), f"missing {path.relative_to(REPO_ROOT)}"
|
|
return path.read_text(encoding="utf-8")
|
|
|
|
|
|
def test_ac1_adr_exists_with_matrix_and_v1_decision():
|
|
text = _read(ADR)
|
|
lower = text.lower()
|
|
assert text.lstrip().startswith("#"), "ADR lacks a title"
|
|
assert "#656" in text
|
|
assert "authorization matrix" in lower
|
|
# The matrix is a real table with the worker and privileged roles.
|
|
for role in ("author", "reviewer", "merger", "reconciler", "controller",
|
|
"operator", "admin"):
|
|
assert role in lower, f"authorization matrix missing role {role!r}"
|
|
# Recorded v1 decision.
|
|
assert "restart-governance/v1" in text
|
|
assert "controller approval" in lower and "automated safety gates" in lower
|
|
|
|
|
|
def test_ac2_restart_is_last_resort_with_narrower_recoveries():
|
|
text = _read(ADR)
|
|
lower = text.lower()
|
|
assert "last resort" in lower
|
|
# Enumerated narrower recoveries precede full restart on the ladder.
|
|
for rung in ("reconnect", "rebind", "scoped restart", "full restart",
|
|
"host"):
|
|
assert rung in lower, f"recovery ladder missing rung {rung!r}"
|
|
|
|
|
|
def test_ac3_forbids_unilateral_llm_full_restart_with_affected_sessions():
|
|
text = _read(ADR)
|
|
lower = text.lower()
|
|
assert "forbidden" in lower
|
|
assert "llm" in lower and "restart" in lower
|
|
assert "unilateral" in lower
|
|
# A worker role must not perform or authorize full/host restart.
|
|
assert "must not" in lower
|
|
|
|
|
|
def test_ac4_break_glass_conditions_listed():
|
|
text = _read(ADR)
|
|
lower = text.lower()
|
|
assert "break-glass" in lower
|
|
assert "incident" in lower
|
|
assert "audit" in lower
|
|
|
|
|
|
def test_ac5_adr_links_issue_lineage():
|
|
text = _read(ADR)
|
|
for issue in LINKED_ISSUES:
|
|
assert issue in text, f"ADR must link issue {issue}"
|
|
|
|
|
|
def test_ac5_safety_model_and_deployment_cross_link_adr():
|
|
for path in CROSS_LINK_DOCS:
|
|
text = _read(path)
|
|
assert ADR_BASENAME in text, (
|
|
f"{path.relative_to(REPO_ROOT)} must cross-link {ADR_BASENAME} "
|
|
f"(issue #656 acceptance criterion 5)"
|
|
)
|
|
|
|
|
|
def test_policy_ids_present_for_enforcement_code():
|
|
text = _read(ADR)
|
|
for pid in POLICY_IDS:
|
|
assert pid in text, f"policy id {pid} missing from ADR"
|
|
|
|
|
|
def test_failure_behavior_denies_on_ambiguity():
|
|
text = _read(ADR)
|
|
lower = text.lower()
|
|
assert "ambiguous" in lower and "deny" in lower
|
|
|
|
|
|
def test_cross_links_do_not_embed_secrets():
|
|
for path in (ADR,) + CROSS_LINK_DOCS:
|
|
text = _read(path)
|
|
for marker in ("ghp_", "BEGIN PRIVATE KEY", "Authorization: Bearer"):
|
|
assert marker not in text, f"{path} contains {marker!r}"
|