fix(mcp-health): deterministic MCP namespace attachment (Closes #689)

This commit is contained in:
2026-07-25 18:52:46 -04:00
parent 2b4e43042a
commit a947bdc186
6 changed files with 455 additions and 14 deletions
+50
View File
@@ -33,6 +33,10 @@ class _SessionContext:
source: str
pid: int
canonical_repository_root: str | None = None
cohort_id: str | None = None
startup_sha: str | None = None
endpoint: str | None = None
config_fingerprint: str | None = None
def as_dict(self) -> dict[str, Any]:
return {
@@ -47,9 +51,14 @@ class _SessionContext:
"source": self.source,
"pid": self.pid,
"canonical_repository_root": self.canonical_repository_root,
"cohort_id": self.cohort_id,
"startup_sha": self.startup_sha,
"endpoint": self.endpoint,
"config_fingerprint": self.config_fingerprint,
}
# Process-local only — never a shared file (same rationale as mutation authority).
# The frozen value prevents partial mutation, while the lock makes first-bind and
# sanctioned rebind atomic across concurrent MCP calls.
@@ -72,6 +81,13 @@ def _reset_session_context_for_testing() -> None:
_SESSION_CONTEXT = None
def clear_session_context() -> None:
"""Purge process-session context and cohort bindings on disconnect."""
global _SESSION_CONTEXT
with _SESSION_CONTEXT_LOCK:
_SESSION_CONTEXT = None
def get_session_context() -> dict[str, Any] | None:
"""Return a detached snapshot of the bound context, or None if unbound."""
with _SESSION_CONTEXT_LOCK:
@@ -146,6 +162,10 @@ def bind_session_context(
expected_username: str | None = None,
source: str = "bind",
canonical_repository_root: str | None = None,
cohort_id: str | None = None,
startup_sha: str | None = None,
endpoint: str | None = None,
config_fingerprint: str | None = None,
) -> dict[str, Any]:
"""Atomically bind/re-bind context (the explicit activation path)."""
with _SESSION_CONTEXT_LOCK:
@@ -160,6 +180,10 @@ def bind_session_context(
expected_username=expected_username,
source=source,
canonical_repository_root=canonical_repository_root,
cohort_id=cohort_id,
startup_sha=startup_sha,
endpoint=endpoint,
config_fingerprint=config_fingerprint,
)
@@ -175,6 +199,10 @@ def _bind_session_context_unlocked(
expected_username: str | None,
source: str,
canonical_repository_root: str | None = None,
cohort_id: str | None = None,
startup_sha: str | None = None,
endpoint: str | None = None,
config_fingerprint: str | None = None,
) -> dict[str, Any]:
"""Store a complete immutable context while the caller holds the lock."""
global _SESSION_CONTEXT
@@ -190,6 +218,10 @@ def _bind_session_context_unlocked(
source=source,
pid=os.getpid(),
canonical_repository_root=(canonical_repository_root or "").strip() or None,
cohort_id=(cohort_id or "").strip() or None,
startup_sha=(startup_sha or "").strip() or None,
endpoint=(endpoint or "").strip() or None,
config_fingerprint=(config_fingerprint or "").strip() or None,
)
return _SESSION_CONTEXT.as_dict()
@@ -206,6 +238,10 @@ def seed_session_context_if_unbound(
expected_username: str | None = None,
source: str = "seed",
canonical_repository_root: str | None = None,
cohort_id: str | None = None,
startup_sha: str | None = None,
endpoint: str | None = None,
config_fingerprint: str | None = None,
) -> dict[str, Any]:
"""Atomically bind only when this process has no current context.
@@ -227,10 +263,15 @@ def seed_session_context_if_unbound(
expected_username=expected_username,
source=source,
canonical_repository_root=canonical_repository_root,
cohort_id=cohort_id,
startup_sha=startup_sha,
endpoint=endpoint,
config_fingerprint=config_fingerprint,
)
return _SESSION_CONTEXT.as_dict()
def assess_session_context(
*,
profile_name: str | None,
@@ -586,6 +627,10 @@ def mutation_context_audit_fields(
"session_identity": None,
"session_repository": None,
"session_org": None,
"session_cohort_id": None,
"session_startup_sha": None,
"session_endpoint": None,
"session_config_fingerprint": None,
}
return {
"session_context_bound": True,
@@ -598,9 +643,14 @@ def mutation_context_audit_fields(
"session_role_kind": data.get("role_kind"),
"session_context_source": data.get("source"),
"session_canonical_repository_root": data.get("canonical_repository_root"),
"session_cohort_id": data.get("cohort_id"),
"session_startup_sha": data.get("startup_sha"),
"session_endpoint": data.get("endpoint"),
"session_config_fingerprint": data.get("config_fingerprint"),
}
def _assessment(
proven: bool, reasons: list[str], ctx: Mapping[str, Any] | None
) -> dict[str, Any]: