feat: enforce MCP restart class permissions (#663)
This commit is contained in:
@@ -0,0 +1,232 @@
|
||||
"""Permission, drain, routing, and audit matrix for restart classes (#663)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from datetime import datetime, timezone
|
||||
|
||||
import restart_coordinator as rc
|
||||
|
||||
NOW = datetime(2026, 7, 24, 20, 0, tzinfo=timezone.utc)
|
||||
|
||||
|
||||
def _inventory() -> dict:
|
||||
return {
|
||||
"inventory_complete": True,
|
||||
"sessions": [
|
||||
{
|
||||
"session_id": "requester",
|
||||
"role": "author",
|
||||
"profile": "prgs-author",
|
||||
"pid": os.getpid(),
|
||||
"status": "active",
|
||||
"last_heartbeat_at": NOW.isoformat(),
|
||||
},
|
||||
{
|
||||
"session_id": "reviewer",
|
||||
"role": "reviewer",
|
||||
"profile": "prgs-reviewer",
|
||||
"pid": os.getpid(),
|
||||
"status": "active",
|
||||
"last_heartbeat_at": NOW.isoformat(),
|
||||
},
|
||||
],
|
||||
"leases": [
|
||||
{
|
||||
"lease_id": "review-lease",
|
||||
"session_id": "reviewer",
|
||||
"role": "reviewer",
|
||||
"phase": "reviewing",
|
||||
"work_kind": "pr",
|
||||
"work_number": 900,
|
||||
"worktree_path": "/tmp/review-900",
|
||||
"freshness": {"freshness": "active"},
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def _evaluate(
|
||||
restart_class: rc.RestartClass,
|
||||
*,
|
||||
role: str = "controller",
|
||||
permissions: tuple[str, ...] | None = None,
|
||||
approved: bool = True,
|
||||
operator: bool = True,
|
||||
**targets,
|
||||
):
|
||||
return rc.evaluate_restart_impact(
|
||||
_inventory(),
|
||||
now=NOW,
|
||||
requesting_session_id="requester",
|
||||
restart_class=restart_class,
|
||||
requester_role=role,
|
||||
requester_permissions=(
|
||||
permissions if permissions is not None
|
||||
else rc.permissions_for_role(role)
|
||||
),
|
||||
controller_approved=approved,
|
||||
operator_authorized=operator,
|
||||
**targets,
|
||||
)
|
||||
|
||||
|
||||
def test_policy_table_covers_exactly_all_nine_classes():
|
||||
assert set(rc.RESTART_CLASS_POLICIES) == set(rc.RestartClass)
|
||||
assert len(rc.RESTART_CLASS_POLICIES) == 9
|
||||
for restart_class, policy in rc.RESTART_CLASS_POLICIES.items():
|
||||
assert policy.restart_class is restart_class
|
||||
assert policy.required_permission
|
||||
assert policy.expected_blast_radius in {
|
||||
rc.BLAST_NONE, rc.BLAST_LOW, rc.BLAST_MEDIUM, rc.BLAST_HIGH
|
||||
}
|
||||
assert policy.drain_requirement
|
||||
assert policy.approval_requirement
|
||||
assert policy.audit_requirement
|
||||
assert policy.recovery_behavior
|
||||
|
||||
|
||||
def test_permission_matrix_allows_each_class_with_exact_permission():
|
||||
targets = {
|
||||
rc.RestartClass.WORKER_RESTART: {"target_session_id": "reviewer"},
|
||||
rc.RestartClass.ROLE_RUNTIME_RESTART: {"target_role": "reviewer"},
|
||||
rc.RestartClass.CONNECTOR_RESTART: {"target_connector": "github"},
|
||||
}
|
||||
for restart_class, policy in rc.RESTART_CLASS_POLICIES.items():
|
||||
report = _evaluate(
|
||||
restart_class,
|
||||
permissions=(policy.required_permission,),
|
||||
**targets.get(restart_class, {}),
|
||||
)
|
||||
assert report.permission_authorized, restart_class
|
||||
assert report.role_authorized, restart_class
|
||||
assert report.approval_satisfied, restart_class
|
||||
assert report.audit_record["restart_class"] == restart_class.value
|
||||
assert (
|
||||
report.audit_record["required_permission"]
|
||||
== policy.required_permission
|
||||
)
|
||||
|
||||
|
||||
def test_missing_or_nearby_permission_denies():
|
||||
report = _evaluate(
|
||||
rc.RestartClass.ROLE_RUNTIME_RESTART,
|
||||
permissions=("mcp.restart.worker.request",),
|
||||
target_role="reviewer",
|
||||
)
|
||||
assert report.verdict == rc.VERDICT_UNSAFE
|
||||
assert not report.allow_restart
|
||||
assert not report.permission_authorized
|
||||
assert any("missing required permission" in r for r in report.reasons)
|
||||
|
||||
|
||||
def test_unknown_restart_class_denies_fail_closed():
|
||||
report = rc.evaluate_restart_impact(
|
||||
_inventory(),
|
||||
now=NOW,
|
||||
restart_class="surprise_reboot",
|
||||
requester_role="admin",
|
||||
requester_permissions=("mcp.restart.host.request",),
|
||||
controller_approved=True,
|
||||
operator_authorized=True,
|
||||
)
|
||||
assert report.verdict == rc.VERDICT_UNSAFE
|
||||
assert not report.allow_restart
|
||||
assert report.restart_policy == {}
|
||||
assert any("unknown restart class" in r for r in report.reasons)
|
||||
|
||||
|
||||
def test_worker_roles_cannot_request_full_or_host_restart():
|
||||
for role in rc.WORKER_ROLES:
|
||||
granted = rc.permissions_for_role(role)
|
||||
assert "mcp.restart.full.request" not in granted
|
||||
assert "mcp.restart.host.request" not in granted
|
||||
report = _evaluate(
|
||||
rc.RestartClass.FULL_MCP_RESTART,
|
||||
role=role,
|
||||
permissions=granted,
|
||||
)
|
||||
assert not report.role_authorized
|
||||
assert not report.allow_restart
|
||||
|
||||
|
||||
def test_controller_approval_is_independent_of_permission():
|
||||
report = _evaluate(
|
||||
rc.RestartClass.WORKER_RESTART,
|
||||
approved=False,
|
||||
target_session_id="reviewer",
|
||||
)
|
||||
assert report.permission_authorized
|
||||
assert not report.approval_satisfied
|
||||
assert not report.allow_restart
|
||||
|
||||
|
||||
def test_narrow_classes_do_not_inherit_full_drain_or_peer_lease_block():
|
||||
for restart_class in (
|
||||
rc.RestartClass.CLIENT_RECONNECT,
|
||||
rc.RestartClass.SESSION_RECONNECT,
|
||||
rc.RestartClass.CONFIGURATION_RELOAD,
|
||||
):
|
||||
report = _evaluate(restart_class)
|
||||
assert not report.restart_policy["full_drain_required"]
|
||||
assert report.counts["leases_disruptive"] == 0
|
||||
assert report.counts["sessions_live_other"] == 0
|
||||
assert report.counts["critical_sections"] == 0
|
||||
assert report.counts["mutations"] == 0
|
||||
assert report.allow_restart, (restart_class, report.reasons)
|
||||
|
||||
|
||||
def test_client_reconnect_does_not_wait_for_unrelated_terminal_lock():
|
||||
inventory = _inventory()
|
||||
inventory["terminal_lock"] = {"terminal_pr": 901}
|
||||
report = rc.evaluate_restart_impact(
|
||||
inventory,
|
||||
now=NOW,
|
||||
requesting_session_id="requester",
|
||||
restart_class=rc.RestartClass.CLIENT_RECONNECT,
|
||||
requester_role="author",
|
||||
requester_permissions=rc.permissions_for_role("author"),
|
||||
)
|
||||
assert report.allow_restart
|
||||
assert report.terminal_lock is None
|
||||
|
||||
|
||||
def test_scoped_restart_only_counts_named_target():
|
||||
report = _evaluate(
|
||||
rc.RestartClass.ROLE_RUNTIME_RESTART,
|
||||
target_role="author",
|
||||
)
|
||||
assert report.counts["leases_disruptive"] == 0
|
||||
assert report.affected_prs == []
|
||||
assert report.allow_restart
|
||||
|
||||
reviewer = _evaluate(
|
||||
rc.RestartClass.ROLE_RUNTIME_RESTART,
|
||||
target_role="reviewer",
|
||||
)
|
||||
assert reviewer.counts["leases_disruptive"] == 1
|
||||
assert reviewer.affected_prs == [900]
|
||||
assert not reviewer.allow_restart
|
||||
|
||||
|
||||
def test_missing_scoped_target_denies_instead_of_widening():
|
||||
for restart_class in (
|
||||
rc.RestartClass.WORKER_RESTART,
|
||||
rc.RestartClass.ROLE_RUNTIME_RESTART,
|
||||
rc.RestartClass.CONNECTOR_RESTART,
|
||||
):
|
||||
report = _evaluate(restart_class)
|
||||
assert not report.allow_restart
|
||||
assert any("target required" in r for r in report.reasons)
|
||||
|
||||
|
||||
def test_only_full_and_host_classes_require_full_drain():
|
||||
requiring_full = {
|
||||
restart_class
|
||||
for restart_class, policy in rc.RESTART_CLASS_POLICIES.items()
|
||||
if policy.full_drain_required
|
||||
}
|
||||
assert requiring_full == {
|
||||
rc.RestartClass.FULL_MCP_RESTART,
|
||||
rc.RestartClass.HOST_RESTART,
|
||||
}
|
||||
Reference in New Issue
Block a user