Files
Gitea-Tools/docs/mcp-fleet-inventory.md
T
sysadminandClaude Opus 5 92615f474b 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
2026-07-27 23:25:45 -04:00

7.3 KiB

Authoritative MCP fleet inventory (#949)

gitea_assess_fleet_inventory is the read-only native capability that answers a question no other surface could: is exactly one server running for each configured PRGS profile, and do they all belong to one client cohort?

Why the existing surfaces were not enough

Surface What it proves Why it cannot prove the fleet
gitea_get_runtime_context Profile, identity, workspace binding of the process answering the call Says nothing about the other four namespaces
gitea_assess_master_parity Startup vs current vs live revision of that same process Five self-reports of one revision do not establish five processes, nor the absence of a sixth
gitea_assess_mcp_namespace_health Whether one named namespace can invoke one tool Accepts process, probe_result and registered_tools from the caller — a capability whose inputs come from the party it constrains is not evidence
control-plane sessions table Allocator task sessions A session is a unit of work, not a server process; nothing recorded that a server exists

Before this capability, satisfying a strict five-process/single-cohort gate required shell process inspection, cached JSON, or source reading — none of which are sanctioned workflow evidence.

Evidence model

Two independent sources must agree before a member counts as running.

1. Control-plane runtime registry — table mcp_server_runtimes. Each server writes exactly one row about itself, from the official entrypoint, immediately after the native MCP transport bind. Only a transport-bound process reaches that line, and no MCP caller can reach it at all. The row is authoritative for identity: namespace, profile, role, repository binding, cohort, startup revision, transport, and PID.

2. Server-side process observation — a process listing performed by the server answering the inventory call, never by the caller. It is authoritative for existence and liveness, and it is the only source that can reveal a running server the registry does not know about.

A member is live only when a registry row has a matching running process whose start time precedes the registration — so a recycled PID cannot impersonate a server that has since exited.

Deliberate non-inferences

  • Configuration is not existence. A configured profile with no live corroborated row is missing, never running (AC8).
  • Matching revisions are not a cohort. single_cohort is derived only from recorded cohort identity. Five members at one revision with an unknown cohort yield single_cohort: null and a closed gate, never true (AC7).
  • Parent-client status is not member health. Each member is classified from its own evidence.

Cohort identity comes from GITEA_MCP_CLIENT_COHORT_ID when the client sets it, otherwise from the parent process that launched the server. A server whose parent has gone away (reparented to init) reports an unknown cohort rather than guessing.

Result shape

Top-level verdict fields:

Field Meaning
inventory_complete All evidence was obtainable. False whenever anything below is unknown.
incomplete_reasons Every distinct reason completeness failed.
configured_members The five expected members with their instance counts and health.
running_members Live, corroborated instances of expected profiles.
missing_members Expected profiles with no live instance.
duplicate_members Expected profiles with more than one live instance, with every PID.
unexpected_members Live members outside the expected roster. Never folded into duplicates.
stale_members Registry rows whose process is dead, unobserved, PID-recycled, or unknown.
unregistered_processes Running MCP server processes with no registry row.
repository_binding_mismatches Live members bound to another repository, or with an incomplete binding.
role_mismatches Live members whose declared role or namespace contradicts the configured profile.
single_cohort / mixed_cohort true/false, or null when cohort evidence is unknown.
mixed_revision / startup_revisions Revision spread across live members.
exactly_one_per_profile No missing and no duplicate expected members.
mutation_gate_satisfied The full invariant held.
blocked_reason / blocked_reasons Why the gate is closed; the first is the headline.
mutations_performed Always [].

Ordering is deterministic — every list sorts by namespace, profile, PID, then runtime id — so two callers reading one snapshot see identical structure.

How controller and reconciler gates consume it

Classification is a pure function of the snapshot. The answering namespace is reported as answering_namespace metadata and never affects the verdict, so gitea-controller and gitea-reconciler return the same result for the same fleet. Neither is privileged over the other.

Consume it like this:

  1. Call gitea_assess_fleet_inventory from gitea-controller or gitea-reconciler (any namespace holding gitea.read may call it).
  2. If mutation_gate_satisfied is true, the exact-one-instance-per-profile, single-cohort, single-revision invariant is proven; proceed.
  3. Otherwise stop and report blocked_reason verbatim. Distinguish the cases — they need different operator actions:
    • missing_members — the client did not launch that namespace; reconnect it.
    • duplicate_members — a second client or a manual launch is running that profile; the operator quits the extra client. This capability never terminates a process.
    • unexpected_members — an unconfigured PRGS server is live; investigate before trusting any gate.
    • unregistered_processes — a server is running code that predates this capability, or failed to register; the inventory is incomplete by construction and must not be reported as healthy.
    • mixed_cohort / mixed_revision — the fleet is not one coherent unit.
    • single_cohort: null — cohort evidence is missing; this is unknown, not healthy.

Never treat inventory_complete: false as a soft warning. It means the inventory cannot describe the whole fleet, which is exactly the state the mutation gate exists to refuse.

Read-only guarantees

The capability performs no restart, reconnect, drain, lease mutation, issue mutation, repository write, or process termination, and it never manufactures historical restart evidence. The only signal it sends is signal 0, a liveness/permission check that delivers nothing to the target process. Registry writes happen exclusively on the startup path of the process being described, never on this read path.

Scope boundaries

This capability is observation only. Related concerns live elsewhere and are deliberately not absorbed here:

  • #950 — controller role metadata and capability-routing consistency.
  • #951 — durable synchronization and restart receipts.
  • #952 — stale-lease inspection, dashboard, and executor consistency.
  • #900 — cohort lifecycle supervision (draining and reaping superseded cohorts) is a mutating transition that would consume this evidence.
  • #948 — the client/session/generation ownership model that this evidence feeds.