# 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.