feat(mcp): gate Connected-but-unattached MCP namespaces (Closes #708)
The prior #708 slice added assess_connected_namespace_attachment() as a pure
decision function with no call site: nothing invoked it, so a session whose
role namespaces were Connected at the host but absent from the active session
tool surface still passed every mutation gate. Detection existed on paper only.
This makes it load-bearing.
Decision layer (mcp_namespace_health.py)
- assess_connected_namespace_attachment() gains secret-free telemetry
(connected/attached/required/missing counts, discovery cache hit and age,
reconnect_required, auto_attach_attempted, auto_recovered, error_type) and
reports reconnect_required, auto_recovered and startup_ordering_race.
- Startup ordering: a namespace whose connect completed after the session tool
snapshot cannot be in that snapshot, so parallel multi-role startup is
identified as its own race with the affected namespaces listed.
- attachment_gate_from_session() is a fail-closed gate keyed by
ATTACHMENT_GATED_TASKS. An unassessed namespace does not gate, matching #543
semantics, so this never fabricates a block.
- SANCTIONED_ATTACH_RECOVERY_TOOL names gitea_request_mcp_reconnect (#678) as
the only recovery.
Server wiring (gitea_mcp_server.py)
- New tool gitea_assess_mcp_namespace_attachment classifies the condition and
records a per-namespace verdict in _LIVE_NAMESPACE_ATTACHMENT.
- gitea_submit_pr_review and gitea_merge_pr now consult
_namespace_attachment_gate() alongside the existing #543 health gate, so both
fail closed while a required namespace is unattached.
- Watchdog check-in emits status only, never namespace contents.
The typed condition mcp_connected_namespaces_missing stays distinct from config
drift (#672), transport-closed (#584) and resolver EOF (#685). Recovery never
routes through direct imports, CLI or raw API mutation, profile hopping,
session-state overrides, or process kills.
Docs
- docs/mcp-namespace-health.md documents the tool arguments, startup ordering,
the fail-closed gate, and the telemetry contract.
- skills/llm-project-workflow/SKILL.md states Connected is not attached, and
that preflight proof is live tool visibility plus gitea_whoami on the role
namespace rather than host status alone.
- docs/mcp-tool-inventory.md lists the new tool.
- docs/remote-mcp/threat-model-anchors.json and threat-model.md: 21 #956 anchors
restamped for the line shift these additions caused in gitea_mcp_server.py.
Every anchor was re-derived from its recorded expect substring; none guessed.
Tests
- tests/test_issue_708_attachment_wiring.py (19 cases): typed detection, proof
mapping, reconnect-only next action, auto-attach success and failure,
reconnect rediscovery, multi-role startup ordering, telemetry including a
no-secret-leak assertion, fail-closed gate per role, unassessed and unmapped
tasks not gating, partial attachment gating only the affected role, and no
healthy verdict without attachment proof.
Verification
- tests/test_issue_708_attachment_wiring.py + test_issue_708_mcp_namespace_attachment.py: 24 passed
- namespace/session/runtime/review sweep: 427 passed, 12 subtests
- full suite head: 31 failed, 5885 passed, 6 skipped, 1047 subtests
- full suite base 17ba1ff035: 30 failed, 5862 passed, 6 skipped, 1047 subtests
- failing identifier sets match, plus tests/test_mirror_refs.py DryRunBanner,
which fails on the unmodified base in isolation and passes here: flaky, not a
regression from this branch.
- Gate proven by execution, not inspection: registering the assessment blocks
merge_pr and review_pr, and attaching the namespaces clears the block.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
@@ -10,71 +10,237 @@
|
||||
],
|
||||
"generated_against_commit": "a143cd065ba06e1a2bdc5143a19ec156e53650ef",
|
||||
"anchors": [
|
||||
{"anchor": "gitea_mcp_server.py:24728", "expect": "mcp_daemon_guard.bind_native_mcp_transport()"},
|
||||
{"anchor": "mcp_daemon_guard.py:49", "expect": "_PRODUCTION_TRANSPORTS = mcp_transport_config.SUPPORTED_TRANSPORTS"},
|
||||
{"anchor": "mcp_daemon_guard.py:195", "expect": "def bind_native_mcp_transport"},
|
||||
{"anchor": "irrecoverable_provenance.py:497", "expect": "def assess_transport_for_auth_mint"},
|
||||
{"anchor": "gitea_mcp_server.py:9132", "expect": "assess_transport_for_auth_mint()"},
|
||||
{"anchor": "gitea_mcp_server.py:9381", "expect": "assess_transport_for_auth_mint()"},
|
||||
{"anchor": "mcp_server.py:4", "expect": "The transport is selected by deployment configuration"},
|
||||
|
||||
{"anchor": "gitea_mcp_server.py:15415", "expect": "def _is_client_managed_process"},
|
||||
{"anchor": "gitea_mcp_server.py:15445", "expect": "def _provenance_mutation_block"},
|
||||
{"anchor": "gitea_mcp_server.py:15453", "expect": "unsupported_manual_launch"},
|
||||
{"anchor": "gitea_mcp_server.py:19004", "expect": "server_provenance"},
|
||||
{"anchor": "gitea_mcp_server.py:21445", "expect": "def _check_mcp_runtimes_diagnostics"},
|
||||
{"anchor": "gitea_mcp_server.py:21465", "expect": "\"ps\", \"-o\", \"pid,lstart,command\""},
|
||||
{"anchor": "gitea_mcp_server.py:21509", "expect": "\"ps\", \"eww\""},
|
||||
{"anchor": "gitea_config.py:1172", "expect": "RECOGNIZED_GITEA_ENV_KEYS"},
|
||||
{"anchor": "gitea_config.py:1233", "expect": "GITEA_CLIENT_MANAGED"},
|
||||
|
||||
{"anchor": "gitea_config.py:54", "expect": "ENV_PROFILE = \"GITEA_MCP_PROFILE\""},
|
||||
{"anchor": "gitea_config.py:97", "expect": "_REVIEW_MERGE_OPS"},
|
||||
{"anchor": "gitea_config.py:499", "expect": "repository authorization scope"},
|
||||
|
||||
{"anchor": "gitea_config.py:956", "expect": "def _keychain_token"},
|
||||
{"anchor": "gitea_config.py:974", "expect": "def resolve_token"},
|
||||
{"anchor": "gitea_config.py:1015", "expect": "def keychain_auth"},
|
||||
{"anchor": "gitea_config.py:294", "expect": "def _validate_identity_auth"},
|
||||
{"anchor": "mcp_daemon_guard.py:583", "expect": "def assert_keychain_access_allowed"},
|
||||
{"anchor": "gitea_mcp_server.py:19261", "expect": "def gitea_list_profiles"},
|
||||
{"anchor": "gitea_mcp_server.py:19312", "expect": "gitea_config.resolve_token(p)"},
|
||||
{"anchor": "gitea_mcp_server.py:19555", "expect": "def gitea_audit_config"},
|
||||
{"anchor": "gitea_mcp_server.py:19577", "expect": "service_summaries(config)"},
|
||||
|
||||
{"anchor": "gitea_config.py:704", "expect": "def resolve_service"},
|
||||
{"anchor": "gitea_config.py:837", "expect": "def service_summaries"},
|
||||
{"anchor": "gitea_config.py:851", "expect": "_keychain_token(auth.get(\"id\"))"},
|
||||
{"anchor": "gitea_mcp_server.py:17710", "expect": "\"jenkins-mcp\""},
|
||||
{"anchor": "gitea_mcp_server.py:17716", "expect": "external-mcp"},
|
||||
{"anchor": "gitea_mcp_server.py:17737", "expect": "\"glitchtip-mcp\""},
|
||||
{"anchor": "gitea_mcp_server.py:17742", "expect": "external-mcp"},
|
||||
{"anchor": "mcp_discoverability.py:9", "expect": "EXPECTED_JENKINS_TOOLS"},
|
||||
{"anchor": "mcp_discoverability.py:17", "expect": "EXPECTED_GLITCHTIP_TOOLS"},
|
||||
|
||||
{"anchor": "sentry_incident_bridge.py:36", "expect": "SENTRY_AUTH_TOKEN"},
|
||||
{"anchor": "sentry_incident_bridge.py:190", "expect": "def resolve_token"},
|
||||
{"anchor": "sentry_incident_bridge.py:289", "expect": "Authorization"},
|
||||
{"anchor": "sentry_observability.py:55", "expect": "SENTRY_DSN"},
|
||||
|
||||
{"anchor": "master_parity_gate.py:168", "expect": "def capture_startup_parity"},
|
||||
{"anchor": "master_parity_gate.py:255", "expect": "mutation_safe"},
|
||||
{"anchor": "gitea_mcp_server.py:19105", "expect": "def gitea_assess_master_parity"},
|
||||
|
||||
{"anchor": "gitea_mcp_server.py:193", "expect": "ACTIVE_WORKTREE_ENV"},
|
||||
{"anchor": "gitea_mcp_server.py:194", "expect": "AUTHOR_WORKTREE_ENV"},
|
||||
{"anchor": "gitea_mcp_server.py:2351", "expect": "/tmp/gitea_issue_lock.json"},
|
||||
{"anchor": "gitea_mcp_server.py:10897", "expect": "def gitea_bootstrap_author_issue_worktree"},
|
||||
{"anchor": "mcp_server.py:13", "expect": "/tmp/mcp_server_stderr.log"},
|
||||
|
||||
{"anchor": "issue_lock_store.py:26", "expect": "DEFAULT_LOCK_DIR"},
|
||||
{"anchor": "issue_lock_store.py:83", "expect": "def session_pointer_path"},
|
||||
{"anchor": "issue_lock_store.py:98", "expect": "def is_process_alive"},
|
||||
{"anchor": "mcp_session_state.py:27", "expect": "DEFAULT_STATE_DIR"},
|
||||
{"anchor": "control_plane_db.py:47", "expect": "DEFAULT_DB_PATH"},
|
||||
{"anchor": "control_plane_db.py:380", "expect": "mode=0o700"},
|
||||
{"anchor": "control_plane_db.py:386", "expect": "sqlite3.connect"},
|
||||
{"anchor": "control_plane_db.py:1145", "expect": "os.getpid()"},
|
||||
{"anchor": "gitea_mcp_server.py:12804", "expect": "owner_pid_alive"}
|
||||
{
|
||||
"anchor": "gitea_mcp_server.py:24848",
|
||||
"expect": "mcp_daemon_guard.bind_native_mcp_transport()"
|
||||
},
|
||||
{
|
||||
"anchor": "mcp_daemon_guard.py:49",
|
||||
"expect": "_PRODUCTION_TRANSPORTS = mcp_transport_config.SUPPORTED_TRANSPORTS"
|
||||
},
|
||||
{
|
||||
"anchor": "mcp_daemon_guard.py:195",
|
||||
"expect": "def bind_native_mcp_transport"
|
||||
},
|
||||
{
|
||||
"anchor": "irrecoverable_provenance.py:497",
|
||||
"expect": "def assess_transport_for_auth_mint"
|
||||
},
|
||||
{
|
||||
"anchor": "gitea_mcp_server.py:9175",
|
||||
"expect": "assess_transport_for_auth_mint()"
|
||||
},
|
||||
{
|
||||
"anchor": "gitea_mcp_server.py:9424",
|
||||
"expect": "assess_transport_for_auth_mint()"
|
||||
},
|
||||
{
|
||||
"anchor": "mcp_server.py:4",
|
||||
"expect": "The transport is selected by deployment configuration"
|
||||
},
|
||||
{
|
||||
"anchor": "gitea_mcp_server.py:15465",
|
||||
"expect": "def _is_client_managed_process"
|
||||
},
|
||||
{
|
||||
"anchor": "gitea_mcp_server.py:15495",
|
||||
"expect": "def _provenance_mutation_block"
|
||||
},
|
||||
{
|
||||
"anchor": "gitea_mcp_server.py:15503",
|
||||
"expect": "unsupported_manual_launch"
|
||||
},
|
||||
{
|
||||
"anchor": "gitea_mcp_server.py:19054",
|
||||
"expect": "server_provenance"
|
||||
},
|
||||
{
|
||||
"anchor": "gitea_mcp_server.py:21565",
|
||||
"expect": "def _check_mcp_runtimes_diagnostics"
|
||||
},
|
||||
{
|
||||
"anchor": "gitea_mcp_server.py:21585",
|
||||
"expect": "\"ps\", \"-o\", \"pid,lstart,command\""
|
||||
},
|
||||
{
|
||||
"anchor": "gitea_mcp_server.py:21629",
|
||||
"expect": "\"ps\", \"eww\""
|
||||
},
|
||||
{
|
||||
"anchor": "gitea_config.py:1172",
|
||||
"expect": "RECOGNIZED_GITEA_ENV_KEYS"
|
||||
},
|
||||
{
|
||||
"anchor": "gitea_config.py:1233",
|
||||
"expect": "GITEA_CLIENT_MANAGED"
|
||||
},
|
||||
{
|
||||
"anchor": "gitea_config.py:54",
|
||||
"expect": "ENV_PROFILE = \"GITEA_MCP_PROFILE\""
|
||||
},
|
||||
{
|
||||
"anchor": "gitea_config.py:97",
|
||||
"expect": "_REVIEW_MERGE_OPS"
|
||||
},
|
||||
{
|
||||
"anchor": "gitea_config.py:499",
|
||||
"expect": "repository authorization scope"
|
||||
},
|
||||
{
|
||||
"anchor": "gitea_config.py:956",
|
||||
"expect": "def _keychain_token"
|
||||
},
|
||||
{
|
||||
"anchor": "gitea_config.py:974",
|
||||
"expect": "def resolve_token"
|
||||
},
|
||||
{
|
||||
"anchor": "gitea_config.py:1015",
|
||||
"expect": "def keychain_auth"
|
||||
},
|
||||
{
|
||||
"anchor": "gitea_config.py:294",
|
||||
"expect": "def _validate_identity_auth"
|
||||
},
|
||||
{
|
||||
"anchor": "mcp_daemon_guard.py:583",
|
||||
"expect": "def assert_keychain_access_allowed"
|
||||
},
|
||||
{
|
||||
"anchor": "gitea_mcp_server.py:19311",
|
||||
"expect": "def gitea_list_profiles"
|
||||
},
|
||||
{
|
||||
"anchor": "gitea_mcp_server.py:19362",
|
||||
"expect": "gitea_config.resolve_token(p)"
|
||||
},
|
||||
{
|
||||
"anchor": "gitea_mcp_server.py:19675",
|
||||
"expect": "def gitea_audit_config"
|
||||
},
|
||||
{
|
||||
"anchor": "gitea_mcp_server.py:19697",
|
||||
"expect": "service_summaries(config)"
|
||||
},
|
||||
{
|
||||
"anchor": "gitea_config.py:704",
|
||||
"expect": "def resolve_service"
|
||||
},
|
||||
{
|
||||
"anchor": "gitea_config.py:837",
|
||||
"expect": "def service_summaries"
|
||||
},
|
||||
{
|
||||
"anchor": "gitea_config.py:851",
|
||||
"expect": "_keychain_token(auth.get(\"id\"))"
|
||||
},
|
||||
{
|
||||
"anchor": "gitea_mcp_server.py:17760",
|
||||
"expect": "\"jenkins-mcp\""
|
||||
},
|
||||
{
|
||||
"anchor": "gitea_mcp_server.py:17766",
|
||||
"expect": "external-mcp"
|
||||
},
|
||||
{
|
||||
"anchor": "gitea_mcp_server.py:17787",
|
||||
"expect": "\"glitchtip-mcp\""
|
||||
},
|
||||
{
|
||||
"anchor": "gitea_mcp_server.py:17792",
|
||||
"expect": "external-mcp"
|
||||
},
|
||||
{
|
||||
"anchor": "mcp_discoverability.py:9",
|
||||
"expect": "EXPECTED_JENKINS_TOOLS"
|
||||
},
|
||||
{
|
||||
"anchor": "mcp_discoverability.py:17",
|
||||
"expect": "EXPECTED_GLITCHTIP_TOOLS"
|
||||
},
|
||||
{
|
||||
"anchor": "sentry_incident_bridge.py:36",
|
||||
"expect": "SENTRY_AUTH_TOKEN"
|
||||
},
|
||||
{
|
||||
"anchor": "sentry_incident_bridge.py:190",
|
||||
"expect": "def resolve_token"
|
||||
},
|
||||
{
|
||||
"anchor": "sentry_incident_bridge.py:289",
|
||||
"expect": "Authorization"
|
||||
},
|
||||
{
|
||||
"anchor": "sentry_observability.py:55",
|
||||
"expect": "SENTRY_DSN"
|
||||
},
|
||||
{
|
||||
"anchor": "master_parity_gate.py:168",
|
||||
"expect": "def capture_startup_parity"
|
||||
},
|
||||
{
|
||||
"anchor": "master_parity_gate.py:255",
|
||||
"expect": "mutation_safe"
|
||||
},
|
||||
{
|
||||
"anchor": "gitea_mcp_server.py:19155",
|
||||
"expect": "def gitea_assess_master_parity"
|
||||
},
|
||||
{
|
||||
"anchor": "gitea_mcp_server.py:193",
|
||||
"expect": "ACTIVE_WORKTREE_ENV"
|
||||
},
|
||||
{
|
||||
"anchor": "gitea_mcp_server.py:194",
|
||||
"expect": "AUTHOR_WORKTREE_ENV"
|
||||
},
|
||||
{
|
||||
"anchor": "gitea_mcp_server.py:2351",
|
||||
"expect": "/tmp/gitea_issue_lock.json"
|
||||
},
|
||||
{
|
||||
"anchor": "gitea_mcp_server.py:10940",
|
||||
"expect": "def gitea_bootstrap_author_issue_worktree"
|
||||
},
|
||||
{
|
||||
"anchor": "mcp_server.py:13",
|
||||
"expect": "/tmp/mcp_server_stderr.log"
|
||||
},
|
||||
{
|
||||
"anchor": "issue_lock_store.py:26",
|
||||
"expect": "DEFAULT_LOCK_DIR"
|
||||
},
|
||||
{
|
||||
"anchor": "issue_lock_store.py:83",
|
||||
"expect": "def session_pointer_path"
|
||||
},
|
||||
{
|
||||
"anchor": "issue_lock_store.py:98",
|
||||
"expect": "def is_process_alive"
|
||||
},
|
||||
{
|
||||
"anchor": "mcp_session_state.py:27",
|
||||
"expect": "DEFAULT_STATE_DIR"
|
||||
},
|
||||
{
|
||||
"anchor": "control_plane_db.py:47",
|
||||
"expect": "DEFAULT_DB_PATH"
|
||||
},
|
||||
{
|
||||
"anchor": "control_plane_db.py:380",
|
||||
"expect": "mode=0o700"
|
||||
},
|
||||
{
|
||||
"anchor": "control_plane_db.py:386",
|
||||
"expect": "sqlite3.connect"
|
||||
},
|
||||
{
|
||||
"anchor": "control_plane_db.py:1145",
|
||||
"expect": "os.getpid()"
|
||||
},
|
||||
{
|
||||
"anchor": "gitea_mcp_server.py:12854",
|
||||
"expect": "owner_pid_alive"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user