fix(controller): production client_instance_id launch path and instance-aware mutation gate
Remediate review 655 blockers on PR #979 (issue #978): B1 — Production launcher (mcp_application_launcher) mints one trusted client_instance_id per application launch and propagates it to all five namespace workers via GITEA_MCP_CLIENT_INSTANCE. launcher_entry and multi_namespace_launcher_entries use that path. Workers never invent a trusted ID; missing/malformed/user-supplied values fail closed. B2 — _check_mcp_runtimes_diagnostics is instance-aware: two legitimate instances sharing a profile are allowed when each has a distinct trusted client_instance_id; duplicate workers for the same (instance, profile) still fail closed. Worker identity/generation exported for peer scans. Tests cover shared ID across five namespaces, distinct launches, multi- instance same profile, same-instance duplicates, untrusted attribution, and the production launcher path. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
+145
-48
@@ -15734,6 +15734,9 @@ CLIENT_NAME_ENV = "GITEA_MCP_CLIENT"
|
||||
CLIENT_INSTANCE_ENV = "GITEA_MCP_CLIENT_INSTANCE"
|
||||
CLIENT_SESSION_ENV = "GITEA_MCP_CLIENT_SESSION"
|
||||
FLEET_RUN_ENV = "GITEA_MCP_FLEET_RUN_ID"
|
||||
INSTANCE_PROVENANCE_ENV = "GITEA_MCP_INSTANCE_PROVENANCE"
|
||||
WORKER_IDENTITY_ENV = "GITEA_MCP_WORKER_IDENTITY"
|
||||
GENERATION_ID_ENV = "GITEA_MCP_GENERATION_ID"
|
||||
|
||||
|
||||
def _worker_registry():
|
||||
@@ -15760,28 +15763,51 @@ def _client_identity_hints() -> dict:
|
||||
"""What the launcher told us about itself (#948 / #975 / #978).
|
||||
|
||||
``client_instance_id`` is established by the trusted application launcher
|
||||
once per launch and shared by all five namespace workers. When the launcher
|
||||
key is absent we still register under an explicit *legacy* placeholder so
|
||||
diagnostic reads work, but the registration is marked incomplete and cannot
|
||||
authorize multi-instance fleet mutation safety.
|
||||
once per launch and shared by all five namespace workers. Workers inherit
|
||||
that value from the production launcher env and never mint a trusted ID
|
||||
themselves. When the launcher key is absent or untrusted we still register
|
||||
under an explicit *legacy* placeholder so diagnostic reads work, but the
|
||||
registration is marked incomplete and cannot authorize multi-instance fleet
|
||||
mutation safety.
|
||||
"""
|
||||
import mcp_application_launcher as app_launcher
|
||||
import mcp_fleet_snapshot
|
||||
|
||||
inherited = app_launcher.inherit_or_refuse_client_instance(os.environ)
|
||||
client_name = (os.environ.get(CLIENT_NAME_ENV) or "").strip() or None
|
||||
raw_instance = (os.environ.get(CLIENT_INSTANCE_ENV) or "").strip() or None
|
||||
instance = mcp_fleet_snapshot.assess_instance_identity(raw_instance)
|
||||
# Legacy placeholder only when the launcher omitted the key — never invent a
|
||||
# trusted ID from PID proximity.
|
||||
if not instance["client_instance_id"]:
|
||||
placeholder = f"legacy-pid-{os.getpid()}"
|
||||
instance = mcp_fleet_snapshot.assess_instance_identity(
|
||||
placeholder, source="pid_fallback"
|
||||
)
|
||||
instance = {
|
||||
"client_instance_id": inherited.get("client_instance_id"),
|
||||
"provenance": inherited.get("provenance"),
|
||||
"trusted": bool(inherited.get("trusted")),
|
||||
"complete": bool(inherited.get("complete")),
|
||||
"launcher_sealed": bool(inherited.get("launcher_sealed")),
|
||||
}
|
||||
# Legacy placeholder only when the launcher omitted a usable key — never
|
||||
# invent a trusted ID from PID proximity or ordinary user env.
|
||||
if not instance["client_instance_id"] or not instance["trusted"]:
|
||||
if not instance["client_instance_id"]:
|
||||
placeholder = f"legacy-pid-{os.getpid()}"
|
||||
fallback = mcp_fleet_snapshot.assess_instance_identity(
|
||||
placeholder, source="pid_fallback"
|
||||
)
|
||||
instance = {
|
||||
"client_instance_id": fallback["client_instance_id"],
|
||||
"provenance": fallback["provenance"],
|
||||
"trusted": False,
|
||||
"complete": False,
|
||||
"launcher_sealed": False,
|
||||
}
|
||||
else:
|
||||
# Malformed / untrusted user-supplied value: keep it for diagnosis
|
||||
# but never mark trusted.
|
||||
instance["trusted"] = False
|
||||
instance["launcher_sealed"] = False
|
||||
return {
|
||||
"client_name": client_name,
|
||||
"client_instance_id": instance["client_instance_id"],
|
||||
"instance_id_provenance": instance["provenance"],
|
||||
"instance_identity_trusted": instance["trusted"],
|
||||
"instance_launcher_sealed": instance.get("launcher_sealed", False),
|
||||
"session_id": (os.environ.get(CLIENT_SESSION_ENV) or "").strip()
|
||||
or f"proc-{os.getpid()}-{_process_boot_head_sha or 'nohead'}",
|
||||
"fleet_run_id": (os.environ.get(FLEET_RUN_ENV) or "").strip() or None,
|
||||
@@ -15857,6 +15883,19 @@ def _active_worker_identity() -> str | None:
|
||||
if outcome.get("registered"):
|
||||
_WORKER_IDENTITY = identity
|
||||
_WORKER_GENERATION = generation
|
||||
# #978 B2: export worker/generation/instance into this process
|
||||
# env so peer process scans (and the instance-aware mutation
|
||||
# gate) can attribute live workers without treating shared
|
||||
# profiles as duplicates. Never overwrite a launcher-sealed
|
||||
# client_instance_id with a different value.
|
||||
os.environ[WORKER_IDENTITY_ENV] = identity
|
||||
os.environ[GENERATION_ID_ENV] = generation
|
||||
if hints.get("client_instance_id") and not (
|
||||
os.environ.get(CLIENT_INSTANCE_ENV) or ""
|
||||
).strip():
|
||||
os.environ[CLIENT_INSTANCE_ENV] = str(
|
||||
hints["client_instance_id"]
|
||||
)
|
||||
# #975: registration is the only moment identity and fencing
|
||||
# epoch are both known, so the heartbeat supervisor is started
|
||||
# here. Without it ``last_heartbeat_at`` never left
|
||||
@@ -22375,6 +22414,20 @@ def _check_mcp_runtimes_diagnostics(task: str, matching_profiles: list[str]) ->
|
||||
)
|
||||
peer_worker_identity = peer_env.get("GITEA_MCP_WORKER_IDENTITY")
|
||||
peer_generation = peer_env.get("GITEA_MCP_GENERATION_ID")
|
||||
# #978 B2: instance attribution for the live fleet gate. Two legitimate
|
||||
# application instances may share a profile when each has a distinct
|
||||
# trusted client_instance_id; only same-(instance, profile/namespace)
|
||||
# duplicates remain blocked.
|
||||
import mcp_fleet_snapshot as _fleet
|
||||
|
||||
peer_instance_raw = peer_env.get("GITEA_MCP_CLIENT_INSTANCE")
|
||||
peer_instance_assess = _fleet.assess_instance_identity(peer_instance_raw)
|
||||
peer_client_instance = (
|
||||
peer_instance_assess["client_instance_id"]
|
||||
if peer_instance_assess.get("trusted")
|
||||
else None
|
||||
)
|
||||
peer_instance_trusted = bool(peer_instance_assess.get("trusted"))
|
||||
|
||||
for env_match in re.finditer(r'\b(GITEA_[A-Z0-9_]+)=([^\s]+)', env_out):
|
||||
k, v = env_match.group(1), env_match.group(2)
|
||||
@@ -22392,6 +22445,9 @@ def _check_mcp_runtimes_diagnostics(task: str, matching_profiles: list[str]) ->
|
||||
"is_client_managed": is_client_managed,
|
||||
"worker_identity": peer_worker_identity,
|
||||
"generation_id": peer_generation,
|
||||
"client_instance_id": peer_client_instance,
|
||||
"instance_identity_trusted": peer_instance_trusted,
|
||||
"raw_client_instance_id": peer_instance_raw,
|
||||
}
|
||||
if profile not in all_profile_procs:
|
||||
all_profile_procs[profile] = []
|
||||
@@ -22399,44 +22455,85 @@ def _check_mcp_runtimes_diagnostics(task: str, matching_profiles: list[str]) ->
|
||||
|
||||
running_profiles = {}
|
||||
for profile, procs in all_profile_procs.items():
|
||||
# #948 AC40/AC43: sharing a profile is legitimate — profile is a
|
||||
# reusable capability definition, not a worker identity. What is *not*
|
||||
# legitimate is reusing one worker identity, or two live sessions
|
||||
# claiming one generation. Distinctness has to be proven, though:
|
||||
# processes carrying no identity evidence are indistinguishable, so
|
||||
# they stay classified as duplicates and keep the #686 wall intact.
|
||||
identified = [p for p in procs if p.get("worker_identity")]
|
||||
distinct_identities = {p["worker_identity"] for p in identified}
|
||||
all_identified = len(identified) == len(procs)
|
||||
duplicate_identity = len(identified) != len(distinct_identities)
|
||||
contested_generation = any(
|
||||
len({p["worker_identity"] for p in identified if p.get("generation_id") == gen}) > 1
|
||||
for gen in {p.get("generation_id") for p in identified if p.get("generation_id")}
|
||||
)
|
||||
|
||||
if len(procs) > 1 and (
|
||||
not all_identified or duplicate_identity or contested_generation
|
||||
):
|
||||
pids_str = ", ".join(str(p["pid"]) for p in procs)
|
||||
if duplicate_identity or contested_generation:
|
||||
detail = (
|
||||
"The same worker identity or generation is claimed more than once, "
|
||||
"so these are genuine duplicates rather than independent workers."
|
||||
)
|
||||
# #978 B2 / #948 AC40: sharing a profile is legitimate when each live
|
||||
# process belongs to a distinct trusted application instance. The
|
||||
# permanent model is instance-aware — not exactly_one_per_profile.
|
||||
# Fail closed only when:
|
||||
# * more than one live worker claims the same (client_instance_id,
|
||||
# profile/namespace), or
|
||||
# * worker identity / generation is reused, or
|
||||
# * multiple processes share a profile without trusted instance
|
||||
# evidence (indistinguishable → treat as duplicate).
|
||||
# Group by trusted client_instance_id (untrusted/missing → one bucket).
|
||||
by_instance: dict[str, list[dict]] = {}
|
||||
for p in procs:
|
||||
if p.get("instance_identity_trusted") and p.get("client_instance_id"):
|
||||
key = str(p["client_instance_id"])
|
||||
else:
|
||||
detail = (
|
||||
"Manual or duplicate launches defeat staleness detection and cannot "
|
||||
"receive client stdio."
|
||||
key = "__untrusted_or_missing__"
|
||||
by_instance.setdefault(key, []).append(p)
|
||||
|
||||
for instance_key, group in by_instance.items():
|
||||
if len(group) <= 1:
|
||||
continue
|
||||
identified = [p for p in group if p.get("worker_identity")]
|
||||
distinct_identities = {p["worker_identity"] for p in identified}
|
||||
all_identified = len(identified) == len(group)
|
||||
duplicate_identity = len(identified) != len(distinct_identities)
|
||||
contested_generation = any(
|
||||
len(
|
||||
{
|
||||
p["worker_identity"]
|
||||
for p in identified
|
||||
if p.get("generation_id") == gen
|
||||
}
|
||||
)
|
||||
reasons.append(
|
||||
f"stale-runtime: Duplicate MCP server process(es) detected for profile '{profile}' (PIDs: {pids_str}). "
|
||||
+ detail
|
||||
> 1
|
||||
for gen in {
|
||||
p.get("generation_id")
|
||||
for p in identified
|
||||
if p.get("generation_id")
|
||||
}
|
||||
)
|
||||
# Otherwise: several independently identified workers share one profile.
|
||||
# No reason is appended, deliberately. Every reason this function
|
||||
# returns is raised as a hard RuntimeError by its callers, so recording
|
||||
# legitimate concurrency here as "informational" would block exactly the
|
||||
# case #948 exists to permit.
|
||||
# Same trusted (instance, profile) with >1 live process is always a
|
||||
# duplicate-namespace-worker, even if worker identities differ —
|
||||
# one application launch gets exactly one worker per namespace.
|
||||
same_trusted_instance = instance_key != "__untrusted_or_missing__"
|
||||
if same_trusted_instance or (
|
||||
not all_identified or duplicate_identity or contested_generation
|
||||
):
|
||||
pids_str = ", ".join(str(p["pid"]) for p in group)
|
||||
if same_trusted_instance:
|
||||
detail = (
|
||||
f"More than one live worker for profile '{profile}' under "
|
||||
f"client_instance_id {instance_key!r} (duplicate namespace "
|
||||
"worker). Two legitimate application instances with "
|
||||
"distinct trusted client_instance_id values may share a "
|
||||
"profile; this collision does not."
|
||||
)
|
||||
elif duplicate_identity or contested_generation:
|
||||
detail = (
|
||||
"The same worker identity or generation is claimed more "
|
||||
"than once, so these are genuine duplicates rather than "
|
||||
"independent workers."
|
||||
)
|
||||
else:
|
||||
detail = (
|
||||
"Multiple processes share this profile without distinct "
|
||||
"trusted client_instance_id evidence, so they cannot be "
|
||||
"told apart from a duplicate MCP process. Relaunch via "
|
||||
"the production application launcher so each instance "
|
||||
"receives a trusted GITEA_MCP_CLIENT_INSTANCE."
|
||||
)
|
||||
reasons.append(
|
||||
f"stale-runtime: Duplicate MCP server process(es) detected "
|
||||
f"for profile '{profile}' (PIDs: {pids_str}). "
|
||||
+ detail
|
||||
)
|
||||
# Multiple trusted instances sharing one profile intentionally produce
|
||||
# no reason here. Callers raise every reason as a hard RuntimeError, so
|
||||
# treating legitimate multi-instance concurrency as "informational"
|
||||
# would re-introduce the profile-only wall #978 removes.
|
||||
client_procs = [p for p in procs if p["is_client_managed"]]
|
||||
if client_procs:
|
||||
client_procs.sort(key=lambda p: p["start_time"], reverse=True)
|
||||
|
||||
Reference in New Issue
Block a user