fix: address PR #587 REQUEST_CHANGES for MCP namespace health (#543)

- Distinguish client_namespace vs offline_spawn probe sources; only IDE
  client probes prove namespace health and feed mutation gates.
- Record assessments in session; gate gitea_submit_pr_review and
  gitea_merge_pr when client-namespace health is unhealthy/non-proven.
- Mark test_mcp_conn.py offline-only; align recovery docs to client
  reconnect (no PID-kill/config-touch as canonical recovery).
- Rebase onto current master so #590 ledger isolation keeps
  TestMergePR.test_unknown_profile_blocks green.
This commit is contained in:
2026-07-09 18:44:04 -04:00
parent af9f1c5944
commit ebd06b73c9
7 changed files with 341 additions and 94 deletions
+54 -5
View File
@@ -2825,6 +2825,35 @@ _TERMINAL_REVIEW_ACTIONS = frozenset({"approve", "request_changes"})
# remote + profile identity with TTL.
_REVIEW_DECISION_LOCK: dict | None = None
# Session-scoped live MCP namespace health assessments (#543).
# Keyed by namespace name. Only client_namespace entries authorize mutations.
_LIVE_NAMESPACE_HEALTH: dict[str, dict] = {}
def _record_live_namespace_health(assessment: dict | None) -> None:
"""Store a namespace health assessment for mutation gates."""
if not isinstance(assessment, dict):
return
ns = str(assessment.get("namespace") or "").strip()
if not ns:
return
_LIVE_NAMESPACE_HEALTH[ns] = {
"namespace": ns,
"healthy": bool(assessment.get("healthy")),
"ide_namespace_proven": bool(assessment.get("ide_namespace_proven")),
"probe_source": assessment.get("probe_source"),
"blocks_merge_workflow": bool(assessment.get("blocks_merge_workflow")),
"error_type": assessment.get("error_type"),
"required_tool": assessment.get("required_tool"),
}
def _live_namespace_health_gate(task: str) -> list[str]:
"""Fail closed on recorded broken/non-client IDE namespace health (#543)."""
return mcp_namespace_health.mutation_gate_from_session(
task, _LIVE_NAMESPACE_HEALTH
)
def _decision_lock_binding(lock: dict | None = None) -> dict:
"""Resolve key fields for durable decision-lock storage."""
@@ -3515,6 +3544,11 @@ def _evaluate_pr_review_submission(
reasons.extend(workflow_blockers)
reasons.extend(review_workflow_load.recovery_handoff_without_replay())
return result
if live:
ns_gate = _live_namespace_health_gate("review_pr")
if ns_gate:
reasons.extend(ns_gate)
return result
if action not in _REVIEW_ACTIONS:
reasons.append(
@@ -4616,6 +4650,12 @@ def gitea_merge_pr(
reasons.extend(review_workflow_load.recovery_handoff_without_replay())
return result
# Gate 0b — recorded live MCP namespace health must not be broken (#543).
ns_gate = _live_namespace_health_gate("merge_pr")
if ns_gate:
reasons.extend(ns_gate)
return result
# Gate 1 — valid merge method (no API call on a bad method).
if do not in _MERGE_METHODS:
reasons.append(
@@ -9203,16 +9243,22 @@ def gitea_assess_mcp_namespace_health(
config_path: str | None = None,
profile: str | None = None,
configured: bool = True,
probe_source: str | None = None,
) -> dict:
"""Read-only: classify live MCP namespace health for required Gitea tools.
"""Classify MCP namespace health for required Gitea tools (#543).
Static FastMCP registration is not enough to prove a namespace works: IDE
clients can keep a registered tool list while live calls fail with
``client is closing: EOF``. This diagnostic accepts static registration
evidence plus live invocation evidence and returns fail-closed guidance for
reviewer/merger workflows.
``client is closing: EOF``. Pass live IDE invocation evidence with
``probe_source='client_namespace'``. Offline subprocess probes
(``test_mcp_conn.py``) must use ``probe_source='offline_spawn'`` and never
count as IDE proof.
Assessments are recorded in the session so live
``gitea_submit_pr_review`` / ``gitea_merge_pr`` can fail closed when a
client-namespace probe reported unhealthy.
"""
return mcp_namespace_health.classify_namespace_probe(
result = mcp_namespace_health.classify_namespace_probe(
namespace,
required_tool=required_tool,
registered_tools=registered_tools,
@@ -9221,7 +9267,10 @@ def gitea_assess_mcp_namespace_health(
config_path=config_path,
profile=profile,
configured=configured,
probe_source=probe_source,
)
_record_live_namespace_health(result)
return result
@mcp.tool()