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
135 lines
7.3 KiB
Markdown
135 lines
7.3 KiB
Markdown
# 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.
|