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
+182
View File
@@ -196,6 +196,7 @@ RECONCILER_WORKTREE_ENV = "GITEA_RECONCILER_WORKTREE"
import namespace_workspace_binding as nwb # noqa: E402
import canonical_repository_root as crr # noqa: E402 # #706 cross-repo canonical root
import mcp_namespace_health # noqa: E402
import mcp_fleet_inventory # noqa: E402 # #949 authoritative fleet inventory
import stale_binding_recovery # noqa: E402
# Worktree env bindings inherited from the parent environment at daemon boot
@@ -18811,6 +18812,116 @@ def gitea_assess_master_parity(
return out
def _fleet_namespace_for_profile(profile_name: str | None) -> str | None:
"""Fleet namespace for *profile_name* (#949).
``role_namespace_gate.infer_mcp_namespace`` recognises only author and
reviewer and returns the profile name for everything else, so it cannot
name the controller, merger, or reconciler namespaces. The fleet roster
carries that mapping; this defers to it and keeps the existing inference as
the fallback for profiles outside the roster. Widening the shared helper is
controller role-metadata work owned by #950.
"""
return mcp_fleet_inventory.namespace_for_profile(
profile_name,
default=role_namespace_gate.infer_mcp_namespace(profile_name),
)
@mcp.tool()
def gitea_assess_fleet_inventory(
remote: str = "dadeschools",
host: str | None = None,
org: str | None = None,
repo: str | None = None,
) -> dict:
"""Read-only: authoritative inventory of the running PRGS MCP fleet (#949).
Every other 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`` takes ``process`` and
``probe_result`` from the caller and so cannot constrain it. This tool takes
**no evidence parameters at all**: it combines the control-plane runtime
registry, which each server writes about itself at native transport bind,
with a process observation performed by this server. A member counts as
running only when both agree, so configuration alone never counts as a
running server and a caller cannot supply the answer.
Classification is a pure function of the snapshot, so ``gitea-controller``
and ``gitea-reconciler`` return the same verdict for the same fleet;
``answering_namespace`` is metadata and never changes it.
Fails closed. Any unreadable registry, unavailable process listing,
unregistered server process, unknown cohort, or unknown startup revision
sets ``inventory_complete`` false and ``mutation_gate_satisfied`` false with
a specific ``blocked_reason``. Matching Git revisions never establish a
single cohort ``single_cohort`` is derived only from recorded cohort
identity and stays ``null`` when that is unknown.
Strictly read-only: no restart, reconnect, lease mutation, issue mutation,
repository write, or process termination, and duplicates are reported and
never terminated. The only signal sent is ``signal 0`` liveness probing,
which delivers nothing to the target process. ``mutations_performed`` is
always an empty list.
Args:
remote: Known instance 'dadeschools' or 'prgs'. Declares which
repository binding fleet members are expected to carry.
host: Override the Gitea host.
org: Override the expected owner/organization binding.
repo: Override the expected repository binding.
Returns:
dict with 'inventory_complete', 'configured_members', 'running_members',
'missing_members', 'duplicate_members', 'unexpected_members',
'stale_members', 'unregistered_processes', 'single_cohort',
'mixed_cohort', 'mixed_revision', 'exactly_one_per_profile',
'mutation_gate_satisfied', 'blocked_reason', and per-member evidence.
"""
read_block = _profile_operation_gate("gitea.read")
if read_block:
return {
"success": False,
"read_only": True,
"inventory_complete": False,
"mutation_gate_satisfied": False,
"blocked_reason": "the active profile may not read control-plane state",
"reasons": read_block,
"permission_report": _permission_block_report("gitea.read"),
"mutations_performed": [],
}
ctx = session_ctx.get_session_context() or {}
expected_binding = {
"remote": remote or ctx.get("remote"),
"org": org or ctx.get("org"),
"repo": repo or ctx.get("repository"),
}
db, db_errors = _control_plane_db_or_error()
runtime_rows: list[dict] = []
registry_error: str | None = None
if db is None:
registry_error = "; ".join(db_errors) or "control-plane DB unavailable"
else:
try:
runtime_rows = db.list_mcp_server_runtimes(statuses=("running",))
except Exception as exc: # noqa: BLE001 - unreadable registry is unknown evidence
registry_error = f"runtime registry could not be read: {_redact(str(exc))}"
result = mcp_fleet_inventory.classify_fleet_inventory(
runtime_rows=runtime_rows,
process_scan=mcp_fleet_inventory.scan_mcp_server_processes(),
expected_binding=expected_binding,
registry_available=registry_error is None,
registry_error=registry_error,
answering_namespace=_fleet_namespace_for_profile(_active_profile_name(host)),
)
result["expected_repository_binding"] = expected_binding
result["summary"] = mcp_fleet_inventory.summarize(result)
return result
# #781: documented in the canonical review workflow as a tool reviewers call
# before workflow load, but the registration decorator had been lost, so the
# documented inventory named something no namespace could reach.
@@ -24344,6 +24455,72 @@ def gitea_quarantine_contaminated_review(
}
def _register_fleet_runtime(transport: str = "stdio") -> dict | None:
"""Record this server process in the control-plane runtime registry (#949).
Called once from the official entrypoint immediately after the native
transport bind, so the row can only ever describe the process writing it.
This is the evidence ``gitea_assess_fleet_inventory`` reads; without it the
fleet is unprovable, but a failure here must never prevent the server from
serving, so every error is swallowed after being logged to stderr.
"""
try:
profile_name = gitea_config.selected_profile_name()
try:
profile = get_profile()
except Exception: # noqa: BLE001 - identity resolution must not block boot
profile = {}
allowed = (profile or {}).get("allowed_operations") or []
forbidden = (profile or {}).get("forbidden_operations") or []
role = _role_kind(allowed, forbidden) if allowed else None
ctx = session_ctx.get_session_context() or {}
env = os.environ
provenance = (
"client_managed"
if (
(env.get("GITEA_CLIENT_MANAGED") or "").strip().lower()
in {"1", "true", "yes", "client_managed"}
or (env.get("GITEA_MCP_CLIENT_MANAGED") or "").strip().lower()
in {"1", "true", "yes", "client_managed"}
or (env.get("GITEA_SERVER_PROVENANCE") or "").strip()
== "client_managed"
)
else "manual_launch"
)
record = mcp_fleet_inventory.build_process_runtime_record(
namespace=_fleet_namespace_for_profile(profile_name),
profile=profile_name,
role=role,
remote=ctx.get("remote"),
org=ctx.get("org"),
repo=ctx.get("repository"),
repository_root=PROJECT_ROOT,
startup_head=_process_boot_head_sha,
daemon_start_head=_process_boot_head_sha,
transport=transport,
client_provenance=provenance,
env=env,
)
db, errors = _control_plane_db_or_error()
if db is None:
sys.stderr.write(
f"--- fleet runtime registration skipped: {'; '.join(errors)} ---\n"
)
return None
db.register_mcp_server_runtime(
record,
retention_seconds=mcp_fleet_inventory.RUNTIME_RETENTION_SECONDS,
)
sys.stderr.write(
f"--- fleet runtime registered: {record['runtime_id']} "
f"(cohort {record['cohort_id']}) ---\n"
)
return record
except Exception as exc: # noqa: BLE001 - never block startup
sys.stderr.write(f"--- fleet runtime registration failed: {exc} ---\n")
return None
# ── Entry point ───────────────────────────────────────────────────────────────
if __name__ == "__main__":
@@ -24353,6 +24530,11 @@ if __name__ == "__main__":
# native transport; offline imports / standalone scripts fail closed.
mcp_daemon_guard.mark_sanctioned_daemon()
mcp_daemon_guard.bind_native_mcp_transport(transport="stdio")
# #949: record this process in the control-plane runtime registry. Only a
# transport-bound server reaches this point, so the row is authoritative
# evidence that this namespace is actually running — evidence no caller of
# gitea_assess_fleet_inventory can supply.
_register_fleet_runtime(transport="stdio")
# Lock this session's launch profile into the environment so child CLI
# processes (e.g. review_pr.py) can detect and refuse profile
# side-channel overrides (#199).