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
+164 -1
View File
@@ -32,7 +32,7 @@ from typing import Any, Iterator, Sequence
import dependency_graph
import gitea_audit
SCHEMA_VERSION = 5
SCHEMA_VERSION = 6
# Assignable work kinds only — raw monitoring incidents are never work items.
WORK_KINDS = frozenset({"issue", "pr"})
@@ -65,6 +65,33 @@ CREATE TABLE IF NOT EXISTS sessions (
status TEXT NOT NULL DEFAULT 'active'
);
-- Fleet-level MCP server runtime registry (#949). Distinct from ``sessions``:
-- a session is one allocator *task*, while a row here is one *server process*
-- that bound native MCP transport. Each server writes its own row and no other,
-- so a caller can never supply this evidence. Creating the table is itself the
-- v5→v6 migration: additive, idempotent, and it touches no existing table.
CREATE TABLE IF NOT EXISTS mcp_server_runtimes (
runtime_id TEXT PRIMARY KEY,
namespace TEXT NOT NULL,
profile TEXT,
role TEXT,
remote TEXT,
org TEXT,
repo TEXT,
repository_root TEXT,
pid INTEGER NOT NULL,
cohort_id TEXT,
cohort_source TEXT,
client_provenance TEXT,
boot_id TEXT,
startup_head TEXT,
daemon_start_head TEXT,
transport TEXT,
registered_at TEXT NOT NULL,
last_heartbeat_at TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'running'
);
CREATE TABLE IF NOT EXISTS work_items (
work_item_id INTEGER PRIMARY KEY AUTOINCREMENT,
remote TEXT NOT NULL,
@@ -910,6 +937,142 @@ class ControlPlaneDB:
rows = conn.execute(sql, params).fetchall()
return [dict(r) for r in rows]
# ── MCP server runtime registry (#949) ────────────────────────────────
_RUNTIME_COLUMNS: tuple[str, ...] = (
"runtime_id",
"namespace",
"profile",
"role",
"remote",
"org",
"repo",
"repository_root",
"pid",
"cohort_id",
"cohort_source",
"client_provenance",
"boot_id",
"startup_head",
"daemon_start_head",
"transport",
"registered_at",
"last_heartbeat_at",
"status",
)
def register_mcp_server_runtime(
self,
record: dict[str, Any],
*,
retention_seconds: int | None = None,
now: str | None = None,
) -> dict[str, Any]:
"""Record that *this* server process is running (#949).
Only the described process ever calls this, at native transport bind.
Two housekeeping deletions happen here — on the startup write path, never
on the read path — so the registry cannot grow without bound:
* rows that recorded the **same PID**, which the calling process now
owns and which therefore cannot still be a live server;
* rows older than *retention_seconds*.
Neither can hide a live duplicate: a concurrently running server holds a
different PID and writes a fresher row.
"""
stamp = now or _ts()
row = {key: record.get(key) for key in self._RUNTIME_COLUMNS}
row["runtime_id"] = str(record.get("runtime_id") or "").strip()
if not row["runtime_id"]:
raise ValueError("runtime_id is required (fail closed)")
row["namespace"] = str(record.get("namespace") or "").strip()
if not row["namespace"]:
raise ValueError("namespace is required (fail closed)")
if record.get("pid") is None:
raise ValueError("pid is required (fail closed)")
row["pid"] = int(record["pid"])
row["registered_at"] = record.get("registered_at") or stamp
row["last_heartbeat_at"] = record.get("last_heartbeat_at") or stamp
row["status"] = record.get("status") or "running"
columns = ", ".join(self._RUNTIME_COLUMNS)
placeholders = ", ".join("?" for _ in self._RUNTIME_COLUMNS)
values = [row[key] for key in self._RUNTIME_COLUMNS]
with self._tx() as conn:
conn.execute(
"DELETE FROM mcp_server_runtimes WHERE pid = ? AND runtime_id != ?",
(row["pid"], row["runtime_id"]),
)
if retention_seconds and retention_seconds > 0:
cutoff = _ts(
_utc_now() - timedelta(seconds=int(retention_seconds))
)
conn.execute(
"DELETE FROM mcp_server_runtimes WHERE registered_at < ?",
(cutoff,),
)
conn.execute(
f"INSERT OR REPLACE INTO mcp_server_runtimes({columns}) "
f"VALUES ({placeholders})",
values,
)
stored = conn.execute(
"SELECT * FROM mcp_server_runtimes WHERE runtime_id = ?",
(row["runtime_id"],),
).fetchone()
return dict(stored)
def heartbeat_mcp_server_runtime(self, runtime_id: str) -> None:
"""Refresh a runtime row's liveness timestamp.
Never called by the inventory read path: liveness is established from
process evidence so that reading the fleet mutates nothing (#949 AC11).
"""
with self._tx() as conn:
conn.execute(
"UPDATE mcp_server_runtimes SET last_heartbeat_at = ? "
"WHERE runtime_id = ?",
(_ts(), runtime_id),
)
def mark_mcp_server_runtime_stopped(self, runtime_id: str) -> None:
"""Mark a runtime row stopped (graceful shutdown bookkeeping)."""
with self._tx() as conn:
conn.execute(
"UPDATE mcp_server_runtimes SET status = 'stopped', "
"last_heartbeat_at = ? WHERE runtime_id = ?",
(_ts(), runtime_id),
)
def list_mcp_server_runtimes(
self,
*,
statuses: Sequence[str] | None = None,
limit: int = 500,
) -> list[dict[str, Any]]:
"""List runtime registry rows, newest registration first (read-only).
Ordering is fully deterministic: ties on ``registered_at`` break on
``runtime_id`` so two callers reading one snapshot see one order.
"""
clauses: list[str] = []
params: list[Any] = []
if statuses:
placeholders = ", ".join("?" for _ in statuses)
clauses.append(f"status IN ({placeholders})")
params.extend(statuses)
where = ("WHERE " + " AND ".join(clauses)) if clauses else ""
params.append(max(1, int(limit)))
with self._tx(immediate=False) as conn:
rows = conn.execute(
f"SELECT * FROM mcp_server_runtimes {where} "
"ORDER BY registered_at DESC, runtime_id ASC LIMIT ?",
params,
).fetchall()
return [dict(r) for r in rows]
# ── work items ────────────────────────────────────────────────────────
def upsert_work_item(