feat(control-plane): add authoritative native MCP fleet inventory (#949)

Every pre-existing runtime surface is per-process. gitea_get_runtime_context
and gitea_assess_master_parity describe only the server answering the call,
and gitea_assess_mcp_namespace_health accepts process, probe_result and
registered_tools from the caller, so it cannot constrain the caller. The
control-plane sessions table records allocator task sessions, not server
processes. Five self-reports of one revision therefore never proved that five
processes exist, that no sixth exists, or that all five share one client
cohort.

Add gitea_assess_fleet_inventory, a strictly read-only capability that takes
no evidence parameters. It combines two sources that must agree:

- a control-plane runtime registry row each server writes about itself,
  from the official entrypoint, immediately after the native transport bind;
- a process observation the answering server performs, never the caller.

A member is running only when both agree and the observed process predates
its registration, so configuration alone never counts and a recycled PID
cannot impersonate an exited server. Classification is a pure function of the
snapshot, so gitea-controller and gitea-reconciler return the same verdict.

Missing, duplicate, unexpected, stale and unregistered members are reported
in separate fields rather than collapsed into one error, because each needs a
different operator action. single_cohort is derived only from recorded cohort
identity and stays null when unknown, so matching revisions never establish a
cohort. Any unreadable registry, unavailable listing, unregistered process,
unknown cohort or unknown revision sets inventory_complete and
mutation_gate_satisfied false with a specific blocked_reason.

The capability performs no restart, reconnect, drain, lease mutation, issue
mutation or process termination, never terminates a duplicate, and never
manufactures restart evidence. The only signal sent is signal 0.

Also resolves the fleet namespace from the expected roster:
role_namespace_gate.infer_mcp_namespace recognises only author and reviewer
and echoes the profile name otherwise, which would have mislabelled the
controller, merger and reconciler members. Widening that shared helper is
controller role-metadata work owned by #950 and is deliberately not done here.

Schema v5 to v6 is additive and idempotent: one new table, no existing table,
tool signature or result field changed. Two control-plane tests that pinned
the schema version to the literal 5 now assert against SCHEMA_VERSION.

#950 (controller role metadata), #951 (restart receipts) and #952 (stale-lease
consistency) remain separate and untouched.

Tests: 118 new across tests/test_mcp_fleet_inventory.py,
tests/test_control_plane_db_server_runtimes.py and
tests/test_fleet_inventory_tool.py, covering every acceptance criterion
including the multi-LLM duplicate-server regression that motivated the issue.

Closes #949

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01NBaQKcRRKeKbZuhk72MhEf
This commit is contained in:
2026-07-27 23:25:45 -04:00
co-authored by Claude Opus 5
parent 82d71b7702
commit 92615f474b
12 changed files with 2718 additions and 3 deletions
+785
View File
@@ -0,0 +1,785 @@
"""Authoritative, read-only PRGS MCP fleet inventory (#949).
Every pre-existing runtime surface is *per process*. ``gitea_get_runtime_context``
and ``gitea_assess_master_parity`` describe only the server answering the call.
``gitea_assess_mcp_namespace_health`` accepts ``process``, ``probe_result`` and
``registered_tools`` **from the caller**, so it cannot constrain the caller. The
control-plane ``sessions`` table records allocator *task* sessions, not server
processes. Five independent self-reports of the same revision therefore never
proved that exactly five processes exist, that no sixth exists, or that all five
belong to one client cohort.
Evidence model
--------------
Two independent sources must agree before a fleet member counts as running:
``control-plane runtime registry``
A row each server writes **about itself** at native transport bind
(:func:`build_process_runtime_record`). No caller can supply it. It is
authoritative for identity: namespace, profile, role, repository binding,
cohort, and the revision the process started at.
``server-side process observation``
A process listing performed by the server answering the inventory call
(:func:`scan_mcp_server_processes`), never by the caller. It is
authoritative for existence and liveness, and it is the only source that
can show a process the registry does not know about.
A member is ``live`` only when a registry row has a matching, still-running
process whose start time precedes the registration (so a recycled PID cannot
impersonate a dead server). Anything the two sources cannot jointly establish
is reported as unknown and fails the mutation gate closed — configuration alone
never counts as a running member, and matching Git revisions never establish a
single cohort.
This module performs no restart, reconnect, lease mutation, issue mutation, or
process termination. The only signal it ever sends is ``signal 0`` liveness
probing, which delivers nothing to the target process.
"""
from __future__ import annotations
import os
import secrets
import subprocess
from datetime import datetime, timezone
from typing import Any, Iterable, Mapping, Sequence
# ── expected fleet ────────────────────────────────────────────────────────────
# The configured PRGS fleet. Each entry is one expected member; the roster is
# the definition of "expected" for missing/unexpected classification.
EXPECTED_PRGS_FLEET: tuple[dict[str, str], ...] = (
{"namespace": "gitea-author", "profile": "prgs-author", "role": "author"},
{
"namespace": "gitea-controller",
"profile": "prgs-controller",
"role": "controller",
},
{"namespace": "gitea-reviewer", "profile": "prgs-reviewer", "role": "reviewer"},
{"namespace": "gitea-merger", "profile": "prgs-merger", "role": "merger"},
{
"namespace": "gitea-reconciler",
"profile": "prgs-reconciler",
"role": "reconciler",
},
)
# Cohort identity supplied by a client that manages the whole fleet.
COHORT_ID_ENV = "GITEA_MCP_CLIENT_COHORT_ID"
# Registry rows older than this are pruned at *registration* time (a startup
# write), never on the read path. Dead rows inside the window are still reported
# as stale evidence rather than silently dropped.
RUNTIME_RETENTION_SECONDS = 7 * 24 * 3600
# Liveness classifications for a registry row.
LIVENESS_LIVE = "live"
LIVENESS_DEAD = "dead"
LIVENESS_PID_RECYCLED = "pid_recycled"
LIVENESS_UNOBSERVED = "unobserved"
LIVENESS_UNKNOWN = "unknown"
# Per-member health classifications.
HEALTH_RUNNING = "running"
HEALTH_MISSING = "missing"
HEALTH_DUPLICATE = "duplicate"
HEALTH_UNEXPECTED = "unexpected"
HEALTH_STALE = "stale"
HEALTH_UNKNOWN = "unknown"
EVIDENCE_AUTHORITY = "control_plane_runtime_registry+server_process_observation"
_MCP_PROCESS_MARKER = "mcp_server.py"
_LSTART_FORMAT = "%a %b %d %H:%M:%S %Y"
_ISO_FORMAT = "%Y-%m-%dT%H:%M:%SZ"
# ── time helpers ──────────────────────────────────────────────────────────────
def _utcnow() -> datetime:
return datetime.now(timezone.utc)
def iso_now() -> str:
return _utcnow().strftime(_ISO_FORMAT)
def _parse_iso(value: Any) -> datetime | None:
text = (str(value) if value is not None else "").strip()
if not text:
return None
if text.endswith("Z"):
text = text[:-1] + "+00:00"
try:
parsed = datetime.fromisoformat(text)
except ValueError:
return None
if parsed.tzinfo is None:
parsed = parsed.replace(tzinfo=timezone.utc)
return parsed.astimezone(timezone.utc)
def _int_or_none(value: Any) -> int | None:
try:
return int(value)
except (TypeError, ValueError):
return None
def _clean(value: Any) -> str | None:
text = (str(value) if value is not None else "").strip()
return text or None
# ── process-level evidence (server side only) ─────────────────────────────────
def probe_pid_alive(pid: int | None) -> bool | None:
"""Return whether *pid* exists. ``None`` when it cannot be determined.
Uses ``signal 0``, which performs a permission/existence check and delivers
nothing to the target. This module never sends a terminating signal.
"""
resolved = _int_or_none(pid)
if resolved is None or resolved <= 0:
return None
try:
os.kill(resolved, 0)
except ProcessLookupError:
return False
except PermissionError:
# The process exists but belongs to another user.
return True
except OSError:
return None
return True
def scan_mcp_server_processes(*, runner=subprocess.run) -> dict[str, Any]:
"""Observe running Gitea MCP server processes from this server process.
This is deliberately performed by the answering server, never by the caller:
a caller-supplied process list is exactly the input a fleet gate must not
trust. When the listing cannot be obtained the result reports
``available=False`` so the inventory fails closed instead of assuming that
no unregistered process exists.
"""
try:
proc = runner(
["ps", "-o", "pid,lstart,command", "-ax"],
capture_output=True,
text=True,
check=True,
)
except Exception as exc: # noqa: BLE001 - any failure means unknown evidence
return {
"available": False,
"processes": [],
"reason": f"process listing unavailable: {exc}",
}
processes: list[dict[str, Any]] = []
for raw_line in (proc.stdout or "").splitlines()[1:]:
line = raw_line.strip()
if not line or _MCP_PROCESS_MARKER not in line:
continue
parts = line.split(None, 6)
if len(parts) < 7:
continue
pid = _int_or_none(parts[0])
if pid is None:
continue
try:
naive = datetime.strptime(" ".join(parts[1:6]), _LSTART_FORMAT)
started_at = naive.astimezone(timezone.utc)
except (ValueError, OSError):
started_at = None
processes.append(
{
"pid": pid,
"started_at": started_at.strftime(_ISO_FORMAT) if started_at else None,
"command": parts[6],
}
)
processes.sort(key=lambda item: item["pid"])
return {"available": True, "processes": processes, "reason": None}
# ── cohort / registration record ──────────────────────────────────────────────
def namespace_for_profile(profile: str | None, *, default: str | None = None) -> str | None:
"""Map a configured profile to its fleet namespace.
Resolved from the expected roster rather than from name-shape heuristics.
``role_namespace_gate.infer_mcp_namespace`` only recognises author and
reviewer, so it returns the *profile* name for controller, merger, and
reconciler — which would label three of the five members with a namespace
that does not exist. Widening that helper is controller role-metadata work
and belongs to #950; the roster already carries the mapping this inventory
needs, so it is read from there.
A profile outside the roster falls back to *default* (typically the
caller's existing inference), so an unexpected member is still described
rather than dropped.
"""
cleaned = _clean(profile)
for entry in EXPECTED_PRGS_FLEET:
if entry["profile"] == cleaned:
return entry["namespace"]
return default if default is not None else cleaned
def derive_cohort_identity(env: Mapping[str, str] | None = None) -> dict[str, Any]:
"""Derive the client/cohort identity of this server process.
A cohort is the set of servers a single client launched together. The
parent process is the durable expression of that: an IDE/CLI client spawns
every namespace as its own child. A process whose parent has gone away
(reparented to init) cannot prove which cohort it belongs to, and says so
rather than guessing.
Revisions are deliberately not consulted here. Two servers built from the
same commit are not thereby one cohort (#949 AC7).
"""
source_env = os.environ if env is None else env
explicit = _clean(source_env.get(COHORT_ID_ENV))
if explicit:
return {"cohort_id": explicit, "cohort_source": "explicit_env"}
try:
ppid = os.getppid()
except OSError:
ppid = 0
if ppid and ppid > 1:
return {"cohort_id": f"ppid:{ppid}", "cohort_source": "parent_process"}
return {
"cohort_id": None,
"cohort_source": "unknown",
"cohort_reason": (
"parent process is unavailable or reparented to init; this server "
"cannot prove which client cohort launched it"
),
}
def build_process_runtime_record(
*,
namespace: str,
profile: str | None,
role: str | None,
remote: str | None = None,
org: str | None = None,
repo: str | None = None,
repository_root: str | None = None,
pid: int | None = None,
startup_head: str | None = None,
daemon_start_head: str | None = None,
transport: str | None = None,
client_provenance: str | None = None,
env: Mapping[str, str] | None = None,
boot_id: str | None = None,
registered_at: str | None = None,
) -> dict[str, Any]:
"""Build the row a server writes about itself at native transport bind.
Every field describes the *calling* process. Nothing here is caller-supplied
in the MCP sense: the only code that reaches this function is the official
entrypoint of the process being described.
"""
cohort = derive_cohort_identity(env)
resolved_pid = _int_or_none(pid)
if resolved_pid is None:
resolved_pid = os.getpid()
token = boot_id or secrets.token_hex(8)
return {
"runtime_id": f"{namespace}:{resolved_pid}:{token}",
"namespace": namespace,
"profile": _clean(profile),
"role": _clean(role),
"remote": _clean(remote),
"org": _clean(org),
"repo": _clean(repo),
"repository_root": _clean(repository_root),
"pid": resolved_pid,
"cohort_id": cohort["cohort_id"],
"cohort_source": cohort["cohort_source"],
"client_provenance": _clean(client_provenance) or "unknown",
"boot_id": token,
"startup_head": _clean(startup_head),
"daemon_start_head": _clean(daemon_start_head),
"transport": _clean(transport),
"registered_at": registered_at or iso_now(),
"status": "running",
}
# ── classification ────────────────────────────────────────────────────────────
def _expected_index(
expected_fleet: Sequence[Mapping[str, str]],
) -> dict[str, dict[str, str]]:
index: dict[str, dict[str, str]] = {}
for entry in expected_fleet:
profile = _clean(entry.get("profile"))
if profile:
index[profile] = dict(entry)
return index
def _sort_key(member: Mapping[str, Any]) -> tuple:
return (
str(member.get("namespace") or ""),
str(member.get("profile") or ""),
_int_or_none(member.get("pid")) or 0,
str(member.get("runtime_id") or ""),
)
def _normalize_row(
row: Mapping[str, Any],
*,
observed_by_pid: Mapping[int, Mapping[str, Any]],
process_scan_available: bool,
now: datetime,
) -> dict[str, Any]:
pid = _int_or_none(row.get("pid"))
member: dict[str, Any] = {
"runtime_id": _clean(row.get("runtime_id")),
"namespace": _clean(row.get("namespace")),
"profile": _clean(row.get("profile")),
"role": _clean(row.get("role")),
"remote": _clean(row.get("remote")),
"org": _clean(row.get("org")),
"repo": _clean(row.get("repo")),
"repository_root": _clean(row.get("repository_root")),
"pid": pid,
"cohort_id": _clean(row.get("cohort_id")),
"cohort_source": _clean(row.get("cohort_source")) or "unknown",
"client_provenance": _clean(row.get("client_provenance")) or "unknown",
"boot_id": _clean(row.get("boot_id")),
"startup_head": _clean(row.get("startup_head")),
"daemon_start_head": _clean(row.get("daemon_start_head")),
"transport": _clean(row.get("transport")),
"registered_at": _clean(row.get("registered_at")),
"last_heartbeat_at": _clean(row.get("last_heartbeat_at")),
"recorded_status": _clean(row.get("status")) or "unknown",
}
pid_alive = probe_pid_alive(pid)
member["pid_alive"] = pid_alive
observed = observed_by_pid.get(pid) if pid is not None else None
member["process_observed"] = bool(observed) if process_scan_available else None
if not process_scan_available:
# Existence cannot be corroborated; never upgrade to live on the
# registry's word alone.
member["liveness"] = LIVENESS_UNKNOWN
member["liveness_reason"] = (
"process observation unavailable; registry rows cannot be corroborated"
)
elif pid_alive is False:
member["liveness"] = LIVENESS_DEAD
member["liveness_reason"] = "recorded PID is not running"
elif pid_alive is None:
member["liveness"] = LIVENESS_UNKNOWN
member["liveness_reason"] = "PID liveness could not be determined"
elif observed is None:
member["liveness"] = LIVENESS_UNOBSERVED
member["liveness_reason"] = (
"recorded PID is not a running Gitea MCP server process"
)
else:
started_at = _parse_iso(observed.get("started_at"))
registered_at = _parse_iso(member["registered_at"])
if started_at and registered_at and started_at > registered_at:
member["liveness"] = LIVENESS_PID_RECYCLED
member["liveness_reason"] = (
"the process now holding this PID started after the registry row "
"was written; the registered server is gone"
)
else:
member["liveness"] = LIVENESS_LIVE
member["liveness_reason"] = None
heartbeat = _parse_iso(member["last_heartbeat_at"])
member["heartbeat_age_seconds"] = (
int((now - heartbeat).total_seconds()) if heartbeat else None
)
return member
def _binding_matches(
member: Mapping[str, Any], expected_binding: Mapping[str, Any] | None
) -> bool | None:
if not expected_binding:
return None
for field in ("remote", "org", "repo"):
expected = _clean(expected_binding.get(field))
if expected is None:
continue
actual = _clean(member.get(field))
if actual is None:
return None
if actual != expected:
return False
return True
def classify_fleet_inventory(
*,
runtime_rows: Iterable[Mapping[str, Any]],
process_scan: Mapping[str, Any] | None = None,
expected_fleet: Sequence[Mapping[str, str]] = EXPECTED_PRGS_FLEET,
expected_binding: Mapping[str, Any] | None = None,
registry_available: bool = True,
registry_error: str | None = None,
now: datetime | None = None,
answering_namespace: str | None = None,
) -> dict[str, Any]:
"""Classify a fleet snapshot. Pure: identical input yields identical output.
The verdict never depends on which namespace asked, so controller and
reconciler agree by construction; ``answering_namespace`` is reported as
metadata only.
"""
moment = now or _utcnow()
scan = dict(process_scan or {"available": False, "processes": [], "reason": None})
scan_available = bool(scan.get("available"))
observed_processes = list(scan.get("processes") or [])
observed_by_pid: dict[int, Mapping[str, Any]] = {}
for proc in observed_processes:
observed_pid = _int_or_none(proc.get("pid"))
if observed_pid is not None:
observed_by_pid[observed_pid] = proc
expected_index = _expected_index(expected_fleet)
members = [
_normalize_row(
row,
observed_by_pid=observed_by_pid,
process_scan_available=scan_available,
now=moment,
)
for row in (runtime_rows or [])
]
live = [m for m in members if m["liveness"] == LIVENESS_LIVE]
not_live = [m for m in members if m["liveness"] != LIVENESS_LIVE]
live_by_profile: dict[str, list[dict[str, Any]]] = {}
for member in live:
live_by_profile.setdefault(member["profile"] or "", []).append(member)
running_members: list[dict[str, Any]] = []
missing_members: list[dict[str, Any]] = []
duplicate_members: list[dict[str, Any]] = []
unexpected_members: list[dict[str, Any]] = []
binding_mismatches: list[dict[str, Any]] = []
role_mismatches: list[dict[str, Any]] = []
configured_members: list[dict[str, Any]] = []
for entry in expected_fleet:
profile = _clean(entry.get("profile")) or ""
instances = sorted(live_by_profile.get(profile, []), key=_sort_key)
configured_members.append(
{
"namespace": _clean(entry.get("namespace")),
"profile": profile,
"role": _clean(entry.get("role")),
"instance_count": len(instances),
"health": (
HEALTH_MISSING
if not instances
else HEALTH_RUNNING
if len(instances) == 1
else HEALTH_DUPLICATE
),
}
)
if not instances:
missing_members.append(
{
"namespace": _clean(entry.get("namespace")),
"profile": profile,
"role": _clean(entry.get("role")),
"health": HEALTH_MISSING,
"reason": (
"no live registry row corroborated by a running server "
"process"
),
}
)
continue
for instance in instances:
instance["health"] = (
HEALTH_RUNNING if len(instances) == 1 else HEALTH_DUPLICATE
)
instance["expected"] = True
running_members.append(instance)
if len(instances) > 1:
duplicate_members.append(
{
"namespace": _clean(entry.get("namespace")),
"profile": profile,
"role": _clean(entry.get("role")),
"health": HEALTH_DUPLICATE,
"instance_count": len(instances),
"pids": sorted(
i["pid"] for i in instances if i["pid"] is not None
),
"instances": instances,
"reason": (
"more than one live server is registered for this profile"
),
}
)
for member in sorted(live, key=_sort_key):
profile = member["profile"] or ""
expected_entry = expected_index.get(profile)
if expected_entry is not None:
expected_role = _clean(expected_entry.get("role"))
actual_role = member["role"]
if expected_role and actual_role and actual_role != expected_role:
role_mismatches.append(
{
"namespace": member["namespace"],
"profile": profile,
"pid": member["pid"],
"expected_role": expected_role,
"declared_role": actual_role,
"reason": "declared role does not match the configured profile",
}
)
expected_namespace = _clean(expected_entry.get("namespace"))
if (
expected_namespace
and member["namespace"]
and member["namespace"] != expected_namespace
):
role_mismatches.append(
{
"namespace": member["namespace"],
"profile": profile,
"pid": member["pid"],
"expected_namespace": expected_namespace,
"declared_role": member["role"],
"reason": (
"profile is served from a namespace it is not "
"configured for"
),
}
)
else:
member["health"] = HEALTH_UNEXPECTED
member["expected"] = False
unexpected_members.append(member)
match = _binding_matches(member, expected_binding)
member["repository_binding_matches"] = match
if match is False:
binding_mismatches.append(
{
"namespace": member["namespace"],
"profile": profile,
"pid": member["pid"],
"remote": member["remote"],
"org": member["org"],
"repo": member["repo"],
"expected": dict(expected_binding or {}),
"reason": "member is bound to a different repository",
}
)
elif match is None and expected_binding:
binding_mismatches.append(
{
"namespace": member["namespace"],
"profile": profile,
"pid": member["pid"],
"remote": member["remote"],
"org": member["org"],
"repo": member["repo"],
"expected": dict(expected_binding or {}),
"reason": "member did not record a complete repository binding",
}
)
stale_members: list[dict[str, Any]] = []
for member in sorted(not_live, key=_sort_key):
member["health"] = (
HEALTH_UNKNOWN if member["liveness"] == LIVENESS_UNKNOWN else HEALTH_STALE
)
stale_members.append(member)
registered_pids = {m["pid"] for m in live if m["pid"] is not None}
unregistered_processes: list[dict[str, Any]] = []
if scan_available:
for proc in observed_processes:
observed_pid = _int_or_none(proc.get("pid"))
if observed_pid is None or observed_pid in registered_pids:
continue
unregistered_processes.append(
{"pid": observed_pid, "started_at": proc.get("started_at")}
)
unregistered_processes.sort(key=lambda item: item["pid"])
# Cohort. Derived only from recorded cohort identity — never from revisions.
cohort_ids = {m["cohort_id"] for m in live}
cohort_unknown = any(cohort_id is None for cohort_id in cohort_ids)
known_cohorts = sorted(c for c in cohort_ids if c is not None)
if not live or cohort_unknown:
single_cohort: bool | None = None
mixed_cohort: bool | None = None
else:
single_cohort = len(known_cohorts) == 1
mixed_cohort = len(known_cohorts) > 1
# Revision spread. Reported independently of cohort; never used to infer it.
heads = {m["startup_head"] for m in live}
head_unknown = any(head is None for head in heads)
known_heads = sorted(h for h in heads if h is not None)
mixed_revision = (len(known_heads) > 1) if known_heads else None
exactly_one_per_profile = not missing_members and not duplicate_members
no_unexpected_members = not unexpected_members
incomplete_reasons: list[str] = []
if not registry_available:
incomplete_reasons.append(
registry_error or "the control-plane runtime registry could not be read"
)
if not scan_available:
incomplete_reasons.append(
str(scan.get("reason") or "server-side process observation unavailable")
)
if unregistered_processes:
pids = ", ".join(str(p["pid"]) for p in unregistered_processes)
incomplete_reasons.append(
f"running Gitea MCP server process(es) with no runtime registry row "
f"(PIDs: {pids}); the fleet contains members this inventory cannot "
f"describe"
)
if live and cohort_unknown:
incomplete_reasons.append(
"one or more live members did not record a client cohort identity; "
"matching revisions do not establish a single cohort"
)
if live and head_unknown:
incomplete_reasons.append(
"one or more live members did not record a startup revision"
)
if any(m["liveness"] == LIVENESS_UNKNOWN for m in members):
incomplete_reasons.append(
"liveness of one or more registry rows could not be determined"
)
inventory_complete = not incomplete_reasons
blocked_reasons: list[str] = list(incomplete_reasons)
if missing_members:
names = ", ".join(sorted(m["profile"] for m in missing_members))
blocked_reasons.append(f"expected fleet member(s) not running: {names}")
if duplicate_members:
names = ", ".join(sorted(d["profile"] for d in duplicate_members))
blocked_reasons.append(
f"duplicate server(s) registered for profile(s): {names}"
)
if unexpected_members:
names = ", ".join(
sorted(
str(m["profile"] or m["namespace"] or "?") for m in unexpected_members
)
)
blocked_reasons.append(f"unexpected fleet member(s) running: {names}")
if mixed_cohort:
blocked_reasons.append(
"live members span more than one client cohort: "
+ ", ".join(known_cohorts)
)
if mixed_revision:
blocked_reasons.append(
"live members started at different revisions: " + ", ".join(known_heads)
)
if binding_mismatches:
blocked_reasons.append(
"one or more live members are not bound to the expected repository"
)
if role_mismatches:
blocked_reasons.append(
"one or more live members declare a role or namespace that does not "
"match the configured profile"
)
mutation_gate_satisfied = bool(
inventory_complete
and exactly_one_per_profile
and no_unexpected_members
and single_cohort is True
and mixed_revision is False
and not binding_mismatches
and not role_mismatches
)
if not mutation_gate_satisfied and not blocked_reasons:
blocked_reasons.append(
"the fleet snapshot did not establish the exact-one-instance-per-"
"profile, single-cohort invariant"
)
return {
"success": True,
"read_only": True,
"evidence_authority": EVIDENCE_AUTHORITY,
"answering_namespace": _clean(answering_namespace),
"generated_at": moment.strftime(_ISO_FORMAT),
"inventory_complete": inventory_complete,
"incomplete_reasons": incomplete_reasons,
"configured_members": configured_members,
"running_members": sorted(running_members, key=_sort_key),
"missing_members": sorted(missing_members, key=_sort_key),
"duplicate_members": sorted(duplicate_members, key=_sort_key),
"unexpected_members": sorted(unexpected_members, key=_sort_key),
"stale_members": stale_members,
"unregistered_processes": unregistered_processes,
"repository_binding_mismatches": sorted(binding_mismatches, key=_sort_key),
"role_mismatches": sorted(role_mismatches, key=_sort_key),
"cohort_ids": known_cohorts,
"single_cohort": single_cohort,
"mixed_cohort": mixed_cohort,
"startup_revisions": known_heads,
"mixed_revision": mixed_revision,
"exactly_one_per_profile": exactly_one_per_profile,
"no_unexpected_members": no_unexpected_members,
"expected_member_count": len(expected_fleet),
"running_member_count": len(running_members),
"mutation_gate_satisfied": mutation_gate_satisfied,
"blocked_reason": blocked_reasons[0] if blocked_reasons else None,
"blocked_reasons": blocked_reasons,
"process_observation": {
"available": scan_available,
"observed_process_count": len(observed_processes),
"reason": scan.get("reason"),
},
"registry": {
"available": registry_available,
"row_count": len(members),
"error": registry_error,
},
"mutations_performed": [],
}
def summarize(result: Mapping[str, Any]) -> str:
"""One-line human summary of a classification result."""
if result.get("mutation_gate_satisfied"):
return (
f"fleet healthy: {result.get('running_member_count')} of "
f"{result.get('expected_member_count')} members running in a single "
f"cohort at one revision"
)
return f"fleet not provable: {result.get('blocked_reason')}"