fix(mcp-health): deterministic MCP namespace attachment (Closes #689)
This commit is contained in:
@@ -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]:
|
||||
|
||||
Reference in New Issue
Block a user