Files
Gitea-Tools/mcp_worker_identity.py
T
sysadminandClaude Opus 4.8 c0c6d14b73 feat(fleet): add CAS-protected stale worker retirement capability
Adds a sanctioned controller/reconciler capability that retires conclusively
stale MCP worker-registry rows through a dry-run-first, compare-and-swap
protected workflow (#980).

The #978 fleet snapshot is read-only, and its registry_revision is seeded with
snapshot_at at second precision, so a CAS gated on it can never pass. That
token is left unchanged; retirement gets its own stable registry_fingerprint
derived exclusively from canonical retirement-relevant registry content, plus a
candidate_fingerprint pinning the approved set. Identical contents observed at
different times produce the same token; row order never affects it; any
retirement-relevant change moves it.

WorkerRegistry.retire_stale_workers performs the whole decision inside one
BEGIN IMMEDIATE transaction: re-read rows, recompute the fingerprint from those
rows, recompute the eligibility plan from those rows (re-reading active workflow
leases), compare the candidate fingerprint, revalidate every target, then retire
each survivor with a guarded UPDATE asserting its status, heartbeat, generation,
session, fencing epoch, and pid are unchanged. Drift retires zero workers and
reports registry_revision_moved or candidate_set_moved; any exception rolls back
and reports transaction_failed, so a partial write is never reported as success.

Retirement requires a full conjunction: active status, complete registry fields,
parsable heartbeat, not live, pid_alive false, expired heartbeat, stale
ownership, canonical repository binding, no identity evidence shared with a live
or unprobeable worker, and no active workflow-lease ownership. Everything else
is preserved with a structured reason code. Retired rows become historical
rather than stale and keep their history; nothing is deleted.

Retirement does not repair untrusted live identity: live workers on legacy
instance identities are preserved and keep their legacy_incomplete_identity
blockers, so the result never claims the fleet became safe.

Exposed as gitea_plan_stale_worker_retirement and
gitea_apply_stale_worker_retirement, restricted to controller/reconciler role
kinds. The Gitea operation gate stays gitea.read because the mutation lands in
the local control-plane registry, matching the #601 lease lifecycle; no new
Gitea write permission is introduced and no author permission is broadened.

Tests: tests/test_issue_980_stale_worker_retirement.py (40 passed, 23 subtests),
including the regression test proving the old snapshot_at derivation moved the
token one second apart while the new registry CAS token does not. Full suite
from the branch worktree matches the master baseline exactly: 28 failed / 6206
passed vs 28 failed / 6166 passed, identical failure set.

Closes #980

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-07-30 15:22:30 -04:00

2192 lines
85 KiB
Python

"""Client/session-aware runtime ownership and provenance (#948).
Before this module the control plane had two disagreeing provenance models.
``gitea_get_runtime_context`` read the *live* process environment, while
``mcp_namespace_health.classify_namespace_probe`` read a filtered env summary
whose allowlist never carried the provenance keys — so one surface reported
``client_managed`` while the other reported ``manual_launch`` for the very same
process. Neither model could name *which* client or *which* session owned the
runtime, so a healthy daemon serving a second client looked like a duplicate.
This module is the single authority both surfaces now consult. The model it
implements:
* A **worker identity** is unique and is *not* a role or a profile. Roles and
profiles are reusable capability definitions that many live workers may share
concurrently; exclusivity belongs to an active lease target, never to a role.
* A **generation** is one daemon launch. Exactly one live session may claim a
generation at a time; a takeover mints a higher fencing epoch so the prior
session is fenced if it resumes.
* **Trusted evidence** is a live attachment record binding
``(client_instance_id, session_id, generation_id)`` to the running process.
A process environment flag is corroborating evidence at best: it is set by
whoever launched the process and says nothing about which session owns it
*now*, so it can never establish ownership on its own.
* Missing or self-contradictory evidence fails closed, and the refusal is
scoped to the affected worker identity rather than to every worker sharing
its profile.
"""
from __future__ import annotations
import atexit
import hashlib
import os
import re
import secrets
import sqlite3
import threading
import time
from contextlib import contextmanager
from datetime import datetime, timezone
from typing import Any, Callable, Iterator, Sequence
# --- Provenance verdicts -------------------------------------------------
#: A live attachment record binds this runtime to a client session.
PROVENANCE_CLIENT_SESSION = "client_session_attached"
#: Positive evidence that the process was launched outside any client session.
PROVENANCE_MANUAL = "manual_launch"
#: No trusted evidence either way — fails closed, distinct from "manual".
PROVENANCE_UNPROVEN = "unproven"
VALID_PROVENANCE = frozenset(
{PROVENANCE_CLIENT_SESSION, PROVENANCE_MANUAL, PROVENANCE_UNPROVEN}
)
# --- Evidence kinds ------------------------------------------------------
EVIDENCE_ATTACHMENT_RECORD = "live_attachment_record"
EVIDENCE_NATIVE_TRANSPORT = "bound_native_transport"
EVIDENCE_PROCESS_LIVENESS = "process_liveness"
#: Never sufficient alone. Named so a report can say *why* it was not enough.
EVIDENCE_ENV_FLAG = "process_env_flag"
#: Evidence kinds that, on their own, establish current session ownership.
SUFFICIENT_EVIDENCE = frozenset({EVIDENCE_ATTACHMENT_RECORD})
# --- Blocker kinds -------------------------------------------------------
BLOCKER_NONE = "none"
BLOCKER_NO_ATTACHMENT = "session_attachment_missing"
BLOCKER_CONFLICTING_SESSIONS = "conflicting_live_sessions"
BLOCKER_FENCED = "superseded_generation_fenced"
BLOCKER_IDENTITY_COLLISION = "worker_identity_collision"
BLOCKER_CONTRADICTORY = "contradictory_provenance_evidence"
# --- Client naming -------------------------------------------------------
#: Returned when no client name is supplied or the name is unusable. There is
#: deliberately no default vendor here: defaulting an unknown client to a
#: specific product emits reconnect steps for a UI the operator is not looking
#: at, which is the #948 hardcoded-``codex`` defect.
UNKNOWN_CLIENT = "unknown_client"
#: Canonical spellings for clients whose aliases we recognise. Absence from
#: this map is not an error — an unrecognised name is slugified and kept, so a
#: new client is named accurately rather than relabelled as a known one.
CLIENT_ALIASES: dict[str, str] = {
"codex": "codex",
"openai": "codex",
"openai_codex": "codex",
"claude": "claude_code",
"claude_code": "claude_code",
"claude_desktop": "claude_code",
"anthropic": "claude_code",
"gemini": "gemini",
"google": "gemini",
"antigravity": "antigravity",
"gemini_antigravity": "antigravity",
"grok": "grok",
"xai": "grok",
}
_SLUG_RE = re.compile(r"[^a-z0-9]+")
#: ``<llm-name>-<UTC-timestamp>-<short-sha>`` per the #948 scope clarification.
WORKER_IDENTITY_RE = re.compile(
r"^(?P<client>[a-z0-9][a-z0-9_]*)-(?P<ts>\d{8}T\d{6}Z)-(?P<digest>[0-9a-f]{12})$"
)
IDENTITY_TIMESTAMP_FORMAT = "%Y%m%dT%H%M%SZ"
IDENTITY_DIGEST_LENGTH = 12
# --- Registry defaults ---------------------------------------------------
REGISTRY_PATH_ENV = "GITEA_WORKER_REGISTRY_DB"
DEFAULT_REGISTRY_PATH = os.path.expanduser(
"~/.cache/gitea-tools/control-plane/worker_registry.sqlite3"
)
#: How long a registration stays live without a heartbeat. Chosen to outlast a
#: full-suite run inside one tool call while still releasing an abandoned
#: worker within a single operator coffee break.
DEFAULT_HEARTBEAT_TTL_SECONDS = 900.0
#: Optional operator override for the beat interval, in seconds. Clamped by
#: :func:`heartbeat_interval_for` so it can never be set at or above the TTL.
HEARTBEAT_INTERVAL_ENV = "GITEA_WORKER_HEARTBEAT_INTERVAL_SECONDS"
#: Never beat faster than this, so a misconfigured interval cannot turn the
#: supervisor into a busy sqlite writer.
MIN_HEARTBEAT_INTERVAL_SECONDS = 1.0
def heartbeat_interval_for(
ttl_seconds: float = DEFAULT_HEARTBEAT_TTL_SECONDS,
override: str | float | None = None,
) -> float:
"""Beat interval for *ttl_seconds*: one third of the TTL, capped at a half.
#975. A third means two consecutive beats can be lost before the row is
allowed to look stale, and the half-TTL cap is a hard ceiling so no
override can produce an interval that expires the registration it is
supposed to renew. That is the whole liveness contract: a healthy worker
stays owned, and a worker that has genuinely stopped beating still expires
on the configured TTL — this function never touches the TTL itself.
"""
try:
ttl = float(ttl_seconds)
except (TypeError, ValueError):
ttl = DEFAULT_HEARTBEAT_TTL_SECONDS
if not ttl > 0:
ttl = DEFAULT_HEARTBEAT_TTL_SECONDS
interval = ttl / 3.0
if override is not None:
candidate = str(override).strip()
if candidate:
try:
parsed = float(candidate)
except (TypeError, ValueError):
parsed = None
if parsed is not None and parsed > 0:
interval = parsed
ceiling = ttl / 2.0
if interval > ceiling:
interval = ceiling
if interval < MIN_HEARTBEAT_INTERVAL_SECONDS:
interval = min(MIN_HEARTBEAT_INTERVAL_SECONDS, ceiling)
return interval
#: Recorded-vs-presented pairs compared by :func:`_heartbeat_expectation_drift`.
_HEARTBEAT_EXPECTATION_FIELDS = (
"session_id",
"generation_id",
"client_name",
"pid",
)
def _heartbeat_expectation_drift(
record: dict[str, Any],
*,
expected_session_id: str | None = None,
expected_generation_id: str | None = None,
expected_client_name: str | None = None,
expected_pid: int | None = None,
) -> list[tuple[str, Any, Any]]:
"""Return ``(field, recorded, presented)`` for every mismatched expectation.
Only supplied expectations are compared, so a caller that presents nothing
gets the pre-#975 behaviour. ``client_name`` is compared through
:func:`normalize_client_name` so one application's namespaces stay one
client while genuinely different clients stay distinct.
"""
presented = {
"session_id": expected_session_id,
"generation_id": expected_generation_id,
"client_name": (
normalize_client_name(expected_client_name)
if expected_client_name is not None
else None
),
"pid": expected_pid,
}
drift: list[tuple[str, Any, Any]] = []
for field in _HEARTBEAT_EXPECTATION_FIELDS:
want = presented[field]
if want is None:
continue
got = record.get(field)
if field == "pid":
match = got is not None and int(got) == int(want)
else:
match = got == want
if not match:
drift.append((field, got, want))
return drift
STATUS_ACTIVE = "active"
STATUS_SUPERSEDED = "superseded"
STATUS_RELEASED = "released"
#: #980 terminal state for a registration whose owning process is conclusively
#: gone. Distinct from ``released`` (the worker said goodbye) and
#: ``superseded`` (a newer generation took over): ``retired`` records that the
#: *control plane* concluded the row was a stale orphan and retired it under a
#: compare-and-swap. Like every non-active status it is not live, so a retired
#: row counts as historical rather than stale in the #978 fleet snapshot.
STATUS_RETIRED = "retired"
_TRUE_VALUES = frozenset({"1", "true", "yes", "client_managed"})
_FALSE_VALUES = frozenset({"0", "false", "no", "manual", "manual_launch"})
#: Environment keys that historically stood in for provenance. They are read
#: only as corroboration; see :func:`env_provenance_signal`.
PROVENANCE_ENV_KEYS = (
"GITEA_CLIENT_MANAGED",
"GITEA_MCP_CLIENT_MANAGED",
"GITEA_SERVER_PROVENANCE",
"GITEA_MCP_SANCTIONED_DAEMON",
)
#: Explicit launch-provenance declarations, highest precedence first. Mirrors
#: the historical ``_is_client_managed_process`` precedence exactly, so
#: unifying the two models changes *who decides*, not *what is decided*.
LAUNCH_DECLARATION_ENV_KEYS = (
"GITEA_CLIENT_MANAGED",
"GITEA_MCP_CLIENT_MANAGED",
"GITEA_SERVER_PROVENANCE",
"GITEA_FORCE_CLIENT_MANAGED",
)
#: Env keys a client launcher sets that imply a non-terminal launch.
LAUNCH_CONFIG_ENV_KEYS = (
"GITEA_MCP_CONFIG",
"GITEA_MCP_PROFILE",
"GITEA_PROFILE_NAME",
)
# --- Launch provenance verdicts ------------------------------------------
#: Launch-dimension values. These keep the literals the pre-#948 surfaces
#: already published, so a consumer reading ``server_provenance`` sees the same
#: strings it always did.
LAUNCH_CLIENT_MANAGED = "client_managed"
LAUNCH_MANUAL = PROVENANCE_MANUAL
LAUNCH_UNPROVEN = PROVENANCE_UNPROVEN
# --- Session ownership verdicts ------------------------------------------
#: A live attachment record binds this runtime to exactly one client session.
OWNERSHIP_OWNED = "owned"
#: No live attachment record. Fails closed for ownership-scoped decisions.
OWNERSHIP_UNOWNED = "unowned"
#: More than one live session claims the generation. Fails closed.
OWNERSHIP_CONTESTED = "contested"
_SCHEMA_SQL = """
CREATE TABLE IF NOT EXISTS worker_registrations (
worker_identity TEXT PRIMARY KEY,
client_name TEXT NOT NULL,
client_instance_id TEXT NOT NULL,
session_id TEXT NOT NULL,
generation_id TEXT NOT NULL,
role TEXT,
profile TEXT,
namespace TEXT,
remote TEXT,
repository_binding TEXT,
pid INTEGER,
transport TEXT,
token_fingerprint TEXT,
started_at TEXT NOT NULL,
last_heartbeat_at TEXT NOT NULL,
heartbeat_ttl_seconds REAL NOT NULL,
fencing_epoch INTEGER NOT NULL,
status TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_worker_generation
ON worker_registrations(generation_id, status);
CREATE INDEX IF NOT EXISTS idx_worker_session
ON worker_registrations(session_id, status);
CREATE INDEX IF NOT EXISTS idx_worker_profile
ON worker_registrations(profile, status);
CREATE INDEX IF NOT EXISTS idx_worker_instance
ON worker_registrations(client_instance_id, status);
"""
#: #978 optional columns added without rewriting historical rows.
_SCHEMA_OPTIONAL_COLUMNS: tuple[tuple[str, str], ...] = (
("fleet_run_id", "TEXT"),
("authenticated_account", "TEXT"),
("process_identity", "TEXT"),
("startup_revision", "TEXT"),
("loaded_revision", "TEXT"),
("parity_revision", "TEXT"),
("live_revision", "TEXT"),
("instance_id_provenance", "TEXT"),
# #980 retirement bookkeeping. Deliberately outside the CAS fingerprint
# field set: they record *that* a retirement happened, and are written only
# by the retirement transaction itself.
("retired_at", "TEXT"),
("retired_by", "TEXT"),
("retirement_reason", "TEXT"),
)
class WorkerRegistryError(RuntimeError):
"""Raised for registry misuse that is a programming error, not a refusal."""
def _utc_now() -> datetime:
return datetime.now(timezone.utc)
def _ts(value: datetime) -> str:
return value.astimezone(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
def _parse_ts(value: str | None) -> datetime | None:
if not value:
return None
try:
return datetime.strptime(value, "%Y-%m-%dT%H:%M:%SZ").replace(
tzinfo=timezone.utc
)
except (TypeError, ValueError):
return None
def default_registry_path() -> str:
raw = (os.environ.get(REGISTRY_PATH_ENV) or DEFAULT_REGISTRY_PATH).strip()
return raw or DEFAULT_REGISTRY_PATH
# --- Identity ------------------------------------------------------------
def normalize_client_name(raw: str | None) -> str:
"""Canonicalise a client name without inventing one.
An empty or unusable name yields :data:`UNKNOWN_CLIENT` rather than a
specific product, so downstream reconnect guidance describes a UI the
operator is actually looking at (or says it does not know) instead of
always naming one vendor.
"""
text = _SLUG_RE.sub("_", (raw or "").strip().lower()).strip("_")
if not text:
return UNKNOWN_CLIENT
return CLIENT_ALIASES.get(text, text)
def worker_identity_digest(
client_name: str,
session_id: str,
timestamp_ns: int,
nonce: str,
) -> str:
"""Collision-resistant digest over client, session, high-resolution time, nonce."""
payload = f"{client_name}\x1f{session_id}\x1f{timestamp_ns}\x1f{nonce}"
return hashlib.sha256(payload.encode("utf-8")).hexdigest()[:IDENTITY_DIGEST_LENGTH]
def generate_worker_identity(
client_name: str | None,
session_id: str | None,
*,
timestamp_ns: int | None = None,
nonce: str | None = None,
now: datetime | None = None,
) -> str:
"""Mint ``<llm-name>-<UTC-timestamp>-<short-sha>`` (#948 AC27-29).
``timestamp_ns`` and ``nonce`` are injectable so a test can force a
collision deliberately; production callers omit both and get high-resolution
time plus a fresh random nonce.
"""
client = normalize_client_name(client_name)
session = (session_id or "").strip() or "no_session"
ts_ns = int(timestamp_ns if timestamp_ns is not None else time.time_ns())
seed = nonce if nonce is not None else secrets.token_hex(16)
stamp = (now or _utc_now()).astimezone(timezone.utc).strftime(
IDENTITY_TIMESTAMP_FORMAT
)
digest = worker_identity_digest(client, session, ts_ns, seed)
return f"{client}-{stamp}-{digest}"
def parse_worker_identity(identity: str | None) -> dict[str, Any]:
"""Structurally validate a worker identity."""
text = (identity or "").strip()
match = WORKER_IDENTITY_RE.match(text)
if not match:
return {
"valid": False,
"worker_identity": text or None,
"client_name": None,
"minted_at": None,
"digest": None,
"reasons": [
"worker identity must match <llm-name>-<UTC-timestamp>-<short-sha>, "
"e.g. gemini-20260729T061500Z-1a2b3c4d5e6f"
],
}
return {
"valid": True,
"worker_identity": text,
"client_name": match.group("client"),
"minted_at": match.group("ts"),
"digest": match.group("digest"),
"reasons": [],
}
def new_generation_id(pid: int | None = None) -> str:
"""Mint a boot epoch for one daemon launch."""
return f"gen-{int(pid if pid is not None else os.getpid())}-{secrets.token_hex(8)}"
# --- Environment signal --------------------------------------------------
def env_provenance_signal(env: dict[str, str] | None = None) -> dict[str, Any]:
"""Read the legacy env provenance flags as *corroborating* signal only.
Returns the raw reading plus an explicit statement that it does not
establish ownership. Callers must not branch on ``value`` alone; #948
requires a live attachment record for that.
"""
source = os.environ if env is None else env
present: dict[str, str] = {}
for key in PROVENANCE_ENV_KEYS:
raw = source.get(key)
if raw not in (None, ""):
present[key] = str(raw)
value: bool | None = None
for key in PROVENANCE_ENV_KEYS:
raw = (present.get(key) or "").strip().lower()
if not raw:
continue
if raw in _FALSE_VALUES:
value = False
break
if raw in _TRUE_VALUES:
value = True
break
return {
"evidence_kind": EVIDENCE_ENV_FLAG,
"keys_present": sorted(present),
"value": value,
"proves_session_ownership": False,
"reason": (
"process environment flags are set by whoever launched the process and "
"carry no session binding; they corroborate a live attachment record but "
"never establish current ownership on their own (#948)"
),
}
def assess_declared_launch_provenance(env: dict[str, str] | None = None) -> dict[str, Any]:
"""Launch provenance for a process we are observing from the outside.
Deliberately stricter than :func:`assess_launch_provenance`. For a *peer*
process there is no stdin to inspect, and launcher-configuration env is not
evidence of anything: a shell that exports ``GITEA_MCP_PROFILE`` passes it
to a hand-launched server too. So only an explicit declaration counts, and
its absence is reported as ``unproven`` rather than asserted as a manual
launch — naming a proof we do not have was the misdiagnosis #948 reports.
"""
source = os.environ if env is None else env
for key in LAUNCH_DECLARATION_ENV_KEYS:
raw = (source.get(key) or "").strip().lower()
if not raw:
continue
if raw in _FALSE_VALUES:
return {
"launch_provenance": LAUNCH_MANUAL,
"client_managed": False,
"decided_by": f"env:{key}",
"reasons": [f"{key}={raw!r} explicitly declares a manual launch"],
}
if raw in _TRUE_VALUES:
return {
"launch_provenance": LAUNCH_CLIENT_MANAGED,
"client_managed": True,
"decided_by": f"env:{key}",
"reasons": [
f"{key}={raw!r} explicitly declares a client-managed launch"
],
}
return {
"launch_provenance": LAUNCH_UNPROVEN,
"client_managed": False,
"decided_by": "no_declaration",
"reasons": [
"no explicit client-managed declaration on the observed process; "
"launcher-configuration env is not proof for a peer process because a "
"hand-launched server inherits it from the shell"
],
}
def assess_launch_provenance(
env: dict[str, str] | None = None,
*,
stdin_is_tty: bool | None = None,
declared_only: bool = False,
) -> dict[str, Any]:
"""Decide *how the process was launched* — a different question from ownership.
#948 separates two claims the old code ran together:
* **Launch provenance** — was this started by a client, or hand-launched from
a terminal? This is the #686 concern and it is what the environment can
legitimately answer, because the launcher is exactly what sets it.
* **Session ownership** — which live client session owns this runtime *now*?
The environment cannot answer that; only a live attachment record can.
Conflating them is what let one surface call a process ``client_managed``
while another called the same process ``manual_launch``. This function is now
the only implementation of the first claim, so every surface gets one answer.
``declared_only`` selects the strategy for an *observed peer* process, where
stdin cannot be inspected and launcher-config env proves nothing. Callers
assessing their own process leave it false.
"""
if declared_only:
return assess_declared_launch_provenance(env)
source = os.environ if env is None else env
reasons: list[str] = []
for key in LAUNCH_DECLARATION_ENV_KEYS:
raw = (source.get(key) or "").strip().lower()
if not raw:
continue
if raw in _FALSE_VALUES:
return {
"launch_provenance": LAUNCH_MANUAL,
"client_managed": False,
"decided_by": f"env:{key}",
"reasons": [f"{key}={raw!r} explicitly declares a manual launch"],
}
if raw in _TRUE_VALUES:
return {
"launch_provenance": LAUNCH_CLIENT_MANAGED,
"client_managed": True,
"decided_by": f"env:{key}",
"reasons": [f"{key}={raw!r} explicitly declares a client-managed launch"],
}
if stdin_is_tty:
return {
"launch_provenance": LAUNCH_MANUAL,
"client_managed": False,
"decided_by": "stdin_tty",
"reasons": [
"stdin is an interactive terminal, so this process cannot be "
"receiving client stdio"
],
}
present = [k for k in LAUNCH_CONFIG_ENV_KEYS if (source.get(k) or "").strip()]
if present:
return {
"launch_provenance": LAUNCH_CLIENT_MANAGED,
"client_managed": True,
"decided_by": "env:launcher_config",
"reasons": [
f"launcher-supplied configuration env present ({', '.join(present)}) "
"with no terminal on stdin"
],
}
reasons.append(
"no launch declaration, no launcher configuration env, and no terminal "
"evidence; launch provenance is unproven rather than assumed manual"
)
return {
"launch_provenance": LAUNCH_UNPROVEN,
"client_managed": False,
"decided_by": "no_evidence",
"reasons": reasons,
}
# --- Registry ------------------------------------------------------------
class WorkerRegistry:
"""Atomic, fail-closed registry of live client/session runtime ownership.
Backed by SQLite with ``BEGIN IMMEDIATE`` so two clients racing to register
the same identity, or to claim the same generation, resolve to exactly one
winner across processes.
"""
def __init__(self, db_path: str | None = None) -> None:
self.db_path = (db_path or default_registry_path()).strip()
parent = os.path.dirname(self.db_path)
if parent:
os.makedirs(parent, mode=0o700, exist_ok=True)
self._lock = threading.RLock()
self._init_schema()
# -- plumbing --
def _connect(self) -> sqlite3.Connection:
conn = sqlite3.connect(self.db_path, timeout=30)
conn.row_factory = sqlite3.Row
conn.execute("PRAGMA journal_mode = WAL")
return conn
@contextmanager
def _tx(self) -> Iterator[sqlite3.Connection]:
with self._lock:
conn = self._connect()
try:
conn.execute("BEGIN IMMEDIATE")
yield conn
conn.commit()
except Exception:
try:
conn.rollback()
except sqlite3.Error:
pass
raise
finally:
conn.close()
def _init_schema(self) -> None:
with self._lock:
conn = self._connect()
try:
conn.executescript(_SCHEMA_SQL)
existing = {
row[1]
for row in conn.execute(
"PRAGMA table_info(worker_registrations)"
).fetchall()
}
for name, decl in _SCHEMA_OPTIONAL_COLUMNS:
if name not in existing:
conn.execute(
f"ALTER TABLE worker_registrations ADD COLUMN {name} {decl}"
)
conn.commit()
finally:
conn.close()
@staticmethod
def _row_to_record(row: sqlite3.Row) -> dict[str, Any]:
return {key: row[key] for key in row.keys()}
# -- liveness --
@staticmethod
def is_live(
record: dict[str, Any] | None,
*,
now: datetime | None = None,
pid_alive: bool | None = None,
) -> dict[str, Any]:
"""Liveness from heartbeat freshness, corroborated by process evidence.
#948 AC7: abandoned generations are detected through authoritative
liveness evidence, not PID comparison alone. A fresh heartbeat is the
authority; ``pid_alive`` can only *withdraw* liveness, never grant it,
because a long-lived daemon PID outlives every session it serves.
"""
if not record:
return {"live": False, "reasons": ["no registration record"]}
if record.get("status") != STATUS_ACTIVE:
return {
"live": False,
"reasons": [f"registration status is {record.get('status')!r}"],
}
beat = _parse_ts(record.get("last_heartbeat_at"))
if beat is None:
return {"live": False, "reasons": ["registration has no parsable heartbeat"]}
ttl = float(record.get("heartbeat_ttl_seconds") or DEFAULT_HEARTBEAT_TTL_SECONDS)
age = ((now or _utc_now()) - beat).total_seconds()
fresh = age <= ttl
reasons: list[str] = []
if not fresh:
reasons.append(
f"heartbeat is {age:.0f}s old, past the {ttl:.0f}s liveness window"
)
if pid_alive is False:
reasons.append(f"recorded pid {record.get('pid')} is no longer running")
return {
"live": bool(fresh and pid_alive is not False),
"heartbeat_age_seconds": age,
"heartbeat_ttl_seconds": ttl,
"heartbeat_fresh": fresh,
"pid_alive": pid_alive,
"reasons": reasons,
}
# -- reads --
def get(self, worker_identity: str) -> dict[str, Any] | None:
conn = self._connect()
try:
row = conn.execute(
"SELECT * FROM worker_registrations WHERE worker_identity = ?",
(worker_identity,),
).fetchone()
return self._row_to_record(row) if row else None
finally:
conn.close()
def list_workers(
self,
*,
status: str | None = STATUS_ACTIVE,
generation_id: str | None = None,
profile: str | None = None,
role: str | None = None,
) -> list[dict[str, Any]]:
clauses: list[str] = []
params: list[Any] = []
if status:
clauses.append("status = ?")
params.append(status)
if generation_id:
clauses.append("generation_id = ?")
params.append(generation_id)
if profile:
clauses.append("profile = ?")
params.append(profile)
if role:
clauses.append("role = ?")
params.append(role)
where = f" WHERE {' AND '.join(clauses)}" if clauses else ""
conn = self._connect()
try:
rows = conn.execute(
f"SELECT * FROM worker_registrations{where} ORDER BY started_at",
params,
).fetchall()
return [self._row_to_record(r) for r in rows]
finally:
conn.close()
# -- writes --
def register(
self,
*,
worker_identity: str,
client_name: str | None,
client_instance_id: str,
session_id: str,
generation_id: str,
role: str | None = None,
profile: str | None = None,
namespace: str | None = None,
remote: str | None = None,
repository_binding: str | None = None,
pid: int | None = None,
transport: str | None = None,
token_fingerprint: str | None = None,
heartbeat_ttl_seconds: float = DEFAULT_HEARTBEAT_TTL_SECONDS,
now: datetime | None = None,
pid_alive_probe=None,
fleet_run_id: str | None = None,
authenticated_account: str | None = None,
process_identity: str | None = None,
startup_revision: str | None = None,
loaded_revision: str | None = None,
parity_revision: str | None = None,
live_revision: str | None = None,
instance_id_provenance: str | None = None,
) -> dict[str, Any]:
"""Atomically register one worker identity.
Fails closed on collision (#948 AC31): an identity already present is
never replaced, adopted, merged with, or corrupted — the caller is told
to mint a different identity and register that instead (AC32). The
existing registration is returned untouched so the caller can see what
it collided with.
#978 also fails closed when a *live* registration already holds the same
``client_instance_id`` for the same namespace: two workers of one
namespace cannot share one instance.
"""
parsed = parse_worker_identity(worker_identity)
if not parsed["valid"]:
return {
"success": False,
"registered": False,
"mutation_performed": False,
"blocker_kind": BLOCKER_IDENTITY_COLLISION,
"collision": False,
"reasons": parsed["reasons"],
"exact_next_action": (
"Mint a worker identity with generate_worker_identity() and "
"register that."
),
}
stamp = _ts(now or _utc_now())
proc_id = process_identity or (
f"pid-{int(pid)}" if pid is not None else None
)
with self._tx() as conn:
existing = conn.execute(
"SELECT * FROM worker_registrations WHERE worker_identity = ?",
(worker_identity,),
).fetchone()
if existing is not None:
record = self._row_to_record(existing)
pid_alive = (
pid_alive_probe(record.get("pid"))
if pid_alive_probe is not None
else None
)
liveness = self.is_live(record, now=now, pid_alive=pid_alive)
return {
"success": False,
"registered": False,
"mutation_performed": False,
"blocker_kind": BLOCKER_IDENTITY_COLLISION,
"collision": True,
"collision_kind": (
"active_worker" if liveness["live"] else "stale_registration"
),
"existing_registration": _public_record(record),
"existing_liveness": liveness,
"reasons": [
f"worker identity {worker_identity!r} is already registered "
f"(status={record.get('status')}, live={liveness['live']}); "
"registration is refused rather than replacing, adopting, or "
"merging with the existing worker (#948 AC31)"
],
"exact_next_action": (
"Generate a different worker identity and atomically register "
"that replacement; do not reuse or take over this one."
),
}
# #978: refuse a second live worker for the same (instance, namespace).
if namespace and client_instance_id:
peers = conn.execute(
"SELECT * FROM worker_registrations "
"WHERE client_instance_id = ? AND namespace = ? AND status = ?",
(client_instance_id, namespace, STATUS_ACTIVE),
).fetchall()
for peer_row in peers:
peer = self._row_to_record(peer_row)
pid_alive = (
pid_alive_probe(peer.get("pid"))
if pid_alive_probe is not None
else None
)
if self.is_live(peer, now=now, pid_alive=pid_alive)["live"]:
return {
"success": False,
"registered": False,
"mutation_performed": False,
"blocker_kind": BLOCKER_CONFLICTING_SESSIONS,
"collision": True,
"collision_kind": "duplicate_namespace_worker",
"existing_registration": _public_record(peer),
"reasons": [
f"client_instance_id {client_instance_id!r} already "
f"has a live worker for namespace {namespace!r} "
f"({peer.get('worker_identity')!r}); #978 fails closed "
"rather than registering a second worker"
],
"exact_next_action": (
"Stop the extra namespace worker, or use a distinct "
"client_instance_id for a separate application launch."
),
}
epoch = self._next_epoch(conn, generation_id)
conn.execute(
"""
INSERT INTO worker_registrations (
worker_identity, client_name, client_instance_id, session_id,
generation_id, role, profile, namespace, remote,
repository_binding, pid, transport, token_fingerprint,
started_at, last_heartbeat_at, heartbeat_ttl_seconds,
fencing_epoch, status,
fleet_run_id, authenticated_account, process_identity,
startup_revision, loaded_revision, parity_revision,
live_revision, instance_id_provenance
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
""",
(
worker_identity,
normalize_client_name(client_name or parsed["client_name"]),
client_instance_id,
session_id,
generation_id,
role,
profile,
namespace,
remote,
repository_binding,
int(pid) if pid is not None else None,
transport,
token_fingerprint,
stamp,
stamp,
float(heartbeat_ttl_seconds),
epoch,
STATUS_ACTIVE,
(fleet_run_id or "").strip() or None,
(authenticated_account or "").strip() or None,
proc_id,
(startup_revision or "").strip() or None,
(loaded_revision or "").strip() or None,
(parity_revision or "").strip() or None,
(live_revision or "").strip() or None,
(instance_id_provenance or "").strip() or None,
),
)
row = conn.execute(
"SELECT * FROM worker_registrations WHERE worker_identity = ?",
(worker_identity,),
).fetchone()
record = self._row_to_record(row)
return {
"success": True,
"registered": True,
"mutation_performed": True,
"blocker_kind": BLOCKER_NONE,
"collision": False,
"registration": _public_record(record),
"fencing_epoch": record["fencing_epoch"],
"reasons": [],
}
@staticmethod
def _next_epoch(conn: sqlite3.Connection, generation_id: str) -> int:
row = conn.execute(
"SELECT MAX(fencing_epoch) AS hi FROM worker_registrations "
"WHERE generation_id = ?",
(generation_id,),
).fetchone()
current = row["hi"] if row and row["hi"] is not None else 0
return int(current) + 1
def heartbeat(
self,
*,
worker_identity: str,
fencing_epoch: int,
now: datetime | None = None,
expected_session_id: str | None = None,
expected_generation_id: str | None = None,
expected_client_name: str | None = None,
expected_pid: int | None = None,
) -> dict[str, Any]:
"""Renew only the owning registration (#948 AC11).
A stale epoch is refused rather than silently renewed, so a superseded
session that resumes cannot heartbeat its way back into ownership.
#975 adds optional keyword-only *expectations*. Every one supplied must
match the recorded row or the renewal is refused. They exist because
identity plus epoch cannot express "renew the row I registered, and only
that row": a recycled PID, or a second session of the same client, would
otherwise be renewable by the wrong beater. They are fencing tokens,
never assertions — supplying one can only cause a refusal, never grant
anything, and omitting them preserves the pre-#975 behaviour exactly.
Drift reports the existing ``BLOCKER_FENCED`` rather than a new
``blocker_kind``, because consumers switch on that value.
"""
stamp = _ts(now or _utc_now())
with self._tx() as conn:
row = conn.execute(
"SELECT * FROM worker_registrations WHERE worker_identity = ?",
(worker_identity,),
).fetchone()
if row is None:
return {
"success": False,
"renewed": False,
"mutation_performed": False,
"blocker_kind": BLOCKER_NO_ATTACHMENT,
"reasons": [f"no registration for {worker_identity!r}"],
}
record = self._row_to_record(row)
drift = _heartbeat_expectation_drift(
record,
expected_session_id=expected_session_id,
expected_generation_id=expected_generation_id,
expected_client_name=expected_client_name,
expected_pid=expected_pid,
)
if drift:
return {
"success": False,
"renewed": False,
"mutation_performed": False,
"blocker_kind": BLOCKER_FENCED,
"expectation_drift": drift,
"reasons": [
"presented worker expectations do not match the recorded "
"registration, so this beater does not own the row: "
+ "; ".join(
f"{field} recorded {recorded!r}, presented {presented!r}"
for field, recorded, presented in drift
)
+ " (#975)"
],
}
if record["status"] != STATUS_ACTIVE:
return {
"success": False,
"renewed": False,
"mutation_performed": False,
"blocker_kind": BLOCKER_FENCED,
"reasons": [
f"registration status is {record['status']!r}; a superseded "
"registration cannot be renewed (#948 AC15)"
],
}
if int(record["fencing_epoch"]) != int(fencing_epoch):
return {
"success": False,
"renewed": False,
"mutation_performed": False,
"blocker_kind": BLOCKER_FENCED,
"recorded_epoch": int(record["fencing_epoch"]),
"presented_epoch": int(fencing_epoch),
"reasons": [
f"fencing epoch {fencing_epoch} does not match the recorded "
f"epoch {record['fencing_epoch']}; this session has been "
"superseded (#948 AC15/AC16)"
],
}
conn.execute(
"UPDATE worker_registrations SET last_heartbeat_at = ? "
"WHERE worker_identity = ?",
(stamp, worker_identity),
)
return {
"success": True,
"renewed": True,
"mutation_performed": True,
"blocker_kind": BLOCKER_NONE,
"last_heartbeat_at": stamp,
"fencing_epoch": int(fencing_epoch),
"reasons": [],
}
def claim_generation(
self,
*,
worker_identity: str,
generation_id: str,
now: datetime | None = None,
pid_alive_probe=None,
) -> dict[str, Any]:
"""Bind a generation to this worker, superseding only a dead claimant.
A generation held by another *live* session is refused: one daemon must
never be simultaneously claimed by conflicting live sessions. A
generation whose claimant is no longer live is taken over with a higher
fencing epoch, so stale ownership cannot permanently strand a healthy
daemon (#948 AC8/AC14).
"""
with self._tx() as conn:
mine = conn.execute(
"SELECT * FROM worker_registrations WHERE worker_identity = ?",
(worker_identity,),
).fetchone()
if mine is None:
return {
"success": False,
"claimed": False,
"mutation_performed": False,
"blocker_kind": BLOCKER_NO_ATTACHMENT,
"reasons": [
f"{worker_identity!r} is not registered; register before "
"claiming a generation"
],
}
holders = [
self._row_to_record(r)
for r in conn.execute(
"SELECT * FROM worker_registrations "
"WHERE generation_id = ? AND status = ? AND worker_identity != ?",
(generation_id, STATUS_ACTIVE, worker_identity),
).fetchall()
]
live_holders = []
for holder in holders:
pid_alive = (
pid_alive_probe(holder.get("pid"))
if pid_alive_probe is not None
else None
)
if self.is_live(holder, now=now, pid_alive=pid_alive)["live"]:
live_holders.append(holder)
if live_holders:
return {
"success": False,
"claimed": False,
"mutation_performed": False,
"blocker_kind": BLOCKER_CONFLICTING_SESSIONS,
"conflicting_owners": [_public_record(h) for h in live_holders],
"reasons": [
f"generation {generation_id!r} is already claimed by "
f"{len(live_holders)} live session(s): "
+ ", ".join(
f"{h['worker_identity']} (session {h['session_id']})"
for h in live_holders
)
+ "; a daemon must not be simultaneously claimed by "
"conflicting live sessions (#948)"
],
"exact_next_action": (
"Wait for the live owner's lease to lapse, or attach this "
"worker to its own generation. Do not kill the other process."
),
}
superseded = [h["worker_identity"] for h in holders]
for identity in superseded:
conn.execute(
"UPDATE worker_registrations SET status = ? "
"WHERE worker_identity = ?",
(STATUS_SUPERSEDED, identity),
)
epoch = self._next_epoch(conn, generation_id)
conn.execute(
"UPDATE worker_registrations SET generation_id = ?, fencing_epoch = ?, "
"status = ?, last_heartbeat_at = ? WHERE worker_identity = ?",
(
generation_id,
epoch,
STATUS_ACTIVE,
_ts(now or _utc_now()),
worker_identity,
),
)
row = conn.execute(
"SELECT * FROM worker_registrations WHERE worker_identity = ?",
(worker_identity,),
).fetchone()
record = self._row_to_record(row)
return {
"success": True,
"claimed": True,
"mutation_performed": True,
"blocker_kind": BLOCKER_NONE,
"registration": _public_record(record),
"fencing_epoch": record["fencing_epoch"],
"superseded_workers": superseded,
"reasons": (
[
f"took over generation {generation_id!r} from "
f"{len(superseded)} non-live claimant(s) with fencing epoch "
f"{record['fencing_epoch']}"
]
if superseded
else []
),
}
def release(
self, *, worker_identity: str, now: datetime | None = None
) -> dict[str, Any]:
"""Mark a registration released. Idempotent; never deletes history."""
with self._tx() as conn:
row = conn.execute(
"SELECT * FROM worker_registrations WHERE worker_identity = ?",
(worker_identity,),
).fetchone()
if row is None:
return {
"success": True,
"released": False,
"mutation_performed": False,
"reasons": [f"no registration for {worker_identity!r}"],
}
conn.execute(
"UPDATE worker_registrations SET status = ?, last_heartbeat_at = ? "
"WHERE worker_identity = ?",
(STATUS_RELEASED, _ts(now or _utc_now()), worker_identity),
)
return {
"success": True,
"released": True,
"mutation_performed": True,
"reasons": [],
}
def retire_stale_workers(
self,
*,
expected_registry_fingerprint: str,
expected_candidate_fingerprint: str,
worker_identities: Sequence[str],
fingerprint_fn: Callable[[list[dict[str, Any]]], str],
plan_fn: Callable[[list[dict[str, Any]]], dict[str, Any]],
retired_by: str | None = None,
retirement_reason: str = "",
now: datetime | None = None,
) -> dict[str, Any]:
"""Compare-and-swap retirement of conclusively stale registrations (#980).
The whole decision happens inside one ``BEGIN IMMEDIATE`` transaction:
the authoritative rows are re-read, the stable registry fingerprint is
recomputed from *those* rows, the eligibility plan is recomputed from
*those* rows, and only then are the approved targets retired — each
with a per-row guarded ``UPDATE`` that also asserts the row's identity,
liveness, and ownership columns are byte-identical to what the
revalidation just read. There is no window in which a safety check and
its matching write are separated by another statement, so a worker that
comes back to life, changes ownership, or is retired concurrently
cannot be deleted on the strength of a stale observation.
``fingerprint_fn`` and ``plan_fn`` are injected rather than imported so
the storage layer never depends on the decision layer; production wires
in :func:`mcp_fleet_retirement.registry_fingerprint` and
:func:`mcp_fleet_retirement.plan_stale_worker_retirement`, which is
exactly what the plan surface used.
Any exception — including a failure to commit — rolls the transaction
back and is reported as ``transaction_failed`` with zero retirements
and ``mutation_performed`` false, so a partial write can never be
reported as success.
"""
requested = [str(w) for w in (worker_identities or []) if str(w).strip()]
stamp = _ts(now or _utc_now())
result: dict[str, Any] | None = None
def _base(outcome: str) -> dict[str, Any]:
return {
"success": True,
"outcome": outcome,
"mutation_performed": False,
"retired": [],
"retired_count": 0,
"preserved": [],
"preserved_count": 0,
"requested_count": len(requested),
"expected_registry_fingerprint": expected_registry_fingerprint,
"expected_candidate_fingerprint": expected_candidate_fingerprint,
"acting_identity": retired_by,
"reasons": [],
}
if not requested:
outcome = _base("nothing_requested")
outcome["reasons"] = [
"no worker identities were supplied; nothing to retire"
]
return outcome
try:
with self._tx() as conn:
rows = [
self._row_to_record(r)
for r in conn.execute(
"SELECT * FROM worker_registrations"
).fetchall()
]
by_identity = {
str(row.get("worker_identity")): row for row in rows
}
current_registry_fingerprint = fingerprint_fn(rows)
if current_registry_fingerprint != expected_registry_fingerprint:
already = [
wid
for wid in requested
if str(
(by_identity.get(wid) or {}).get("status") or ""
)
== STATUS_RETIRED
]
idempotent = len(already) == len(requested)
result = _base(
"already_retired" if idempotent else "registry_revision_moved"
)
result["idempotent"] = idempotent
result["current_registry_fingerprint"] = (
current_registry_fingerprint
)
result["reasons"] = [
"the worker registry changed between plan and apply; "
"retiring zero workers"
if not idempotent
else "every requested registration is already retired; "
"safe no-op"
]
result["preserved"] = [
{
"worker_identity": wid,
"reason_code": "registry_revision_moved",
"detail": (
"aborted before any retirement: registry "
"fingerprint moved"
),
}
for wid in requested
]
result["preserved_count"] = len(requested)
return result
fresh_plan = plan_fn(rows)
current_candidate_fingerprint = fresh_plan.get(
"candidate_fingerprint"
)
if current_candidate_fingerprint != expected_candidate_fingerprint:
result = _base("candidate_set_moved")
result["current_registry_fingerprint"] = (
current_registry_fingerprint
)
result["current_candidate_fingerprint"] = (
current_candidate_fingerprint
)
result["reasons"] = [
"the retirement candidate set changed between plan and "
"apply; retiring zero workers"
]
result["preserved"] = [
{
"worker_identity": wid,
"reason_code": "candidate_set_moved",
"detail": (
"aborted before any retirement: candidate "
"fingerprint moved"
),
}
for wid in requested
]
result["preserved_count"] = len(requested)
return result
eligible = {
str(c.get("worker_identity")): c
for c in fresh_plan.get("candidates") or []
}
preserved_index = {
str(p.get("worker_identity")): p
for p in fresh_plan.get("preserved") or []
}
retired: list[dict[str, Any]] = []
preserved: list[dict[str, Any]] = []
for wid in requested:
row = by_identity.get(wid)
if row is None:
preserved.append(
{
"worker_identity": wid,
"reason_code": "registration_missing",
"detail": "no registration row with this identity",
}
)
continue
if wid not in eligible:
blocked = preserved_index.get(wid) or {}
preserved.append(
{
"worker_identity": wid,
"reason_code": blocked.get("reason_code")
or "not_in_current_plan",
"detail": blocked.get("detail")
or (
"revalidation immediately before retirement "
"no longer finds this worker eligible"
),
"evidence": blocked.get("evidence"),
}
)
continue
cursor = conn.execute(
"UPDATE worker_registrations "
"SET status = ?, retired_at = ?, retired_by = ?, "
" retirement_reason = ? "
"WHERE worker_identity = ? "
" AND status = ? "
" AND last_heartbeat_at = ? "
" AND generation_id = ? "
" AND session_id = ? "
" AND fencing_epoch = ? "
" AND IFNULL(pid, -1) = IFNULL(?, -1)",
(
STATUS_RETIRED,
stamp,
retired_by,
retirement_reason
or (eligible[wid].get("reason_code") or ""),
wid,
STATUS_ACTIVE,
row.get("last_heartbeat_at"),
row.get("generation_id"),
row.get("session_id"),
row.get("fencing_epoch"),
row.get("pid"),
),
)
if cursor.rowcount == 1:
retired.append(
{
"worker_identity": wid,
"reason_code": eligible[wid].get("reason_code"),
"retired_at": stamp,
"retired_by": retired_by,
"evidence": eligible[wid].get("evidence"),
}
)
else:
preserved.append(
{
"worker_identity": wid,
"reason_code": "row_changed_since_plan",
"detail": (
"guarded update matched no row; the "
"registration changed inside the retirement "
"transaction"
),
}
)
result = _base("applied")
result["mutation_performed"] = bool(retired)
result["retired"] = retired
result["retired_count"] = len(retired)
result["preserved"] = preserved
result["preserved_count"] = len(preserved)
result["current_registry_fingerprint"] = (
current_registry_fingerprint
)
result["current_candidate_fingerprint"] = (
current_candidate_fingerprint
)
result["retired_at"] = stamp if retired else None
except Exception as exc: # rolled back by _tx; report, never half-claim
failure = _base("transaction_failed")
failure["success"] = False
failure["reasons"] = [
"retirement transaction failed and was rolled back; zero "
f"registrations were retired: {type(exc).__name__}: {exc}"
]
failure["preserved"] = [
{
"worker_identity": wid,
"reason_code": "transaction_failed",
"detail": "transaction rolled back before any commit",
}
for wid in requested
]
failure["preserved_count"] = len(requested)
return failure
return result
def _public_record(record: dict[str, Any]) -> dict[str, Any]:
"""Registry row minus anything that should not travel to an LLM surface."""
return {
"worker_identity": record.get("worker_identity"),
"client_name": record.get("client_name"),
"client_instance_id": record.get("client_instance_id"),
"session_id": record.get("session_id"),
"generation_id": record.get("generation_id"),
"role": record.get("role"),
"profile": record.get("profile"),
"namespace": record.get("namespace"),
"remote": record.get("remote"),
"repository_binding": record.get("repository_binding"),
"pid": record.get("pid"),
"transport": record.get("transport"),
"started_at": record.get("started_at"),
"last_heartbeat_at": record.get("last_heartbeat_at"),
"heartbeat_ttl_seconds": record.get("heartbeat_ttl_seconds"),
"fencing_epoch": record.get("fencing_epoch"),
"status": record.get("status"),
"fleet_run_id": record.get("fleet_run_id"),
"authenticated_account": record.get("authenticated_account"),
"process_identity": record.get("process_identity"),
"startup_revision": record.get("startup_revision"),
"loaded_revision": record.get("loaded_revision"),
"parity_revision": record.get("parity_revision"),
"live_revision": record.get("live_revision"),
"instance_id_provenance": record.get("instance_id_provenance"),
}
# --- The single authoritative verdict ------------------------------------
def assess_provenance(
*,
registry: WorkerRegistry | None = None,
worker_identity: str | None = None,
generation_id: str | None = None,
env: dict[str, str] | None = None,
native_transport_bound: bool | None = None,
namespace: str | None = None,
profile: str | None = None,
role: str | None = None,
now: datetime | None = None,
pid_alive_probe=None,
stdin_is_tty: bool | None = None,
declared_only: bool = False,
) -> dict[str, Any]:
"""Resolve client/session provenance once, for every surface to reuse.
Runtime context, namespace health, namespace attachment, fleet inventory,
capability resolution, and typed reconnect assessment all call this, so the
contradiction that motivated #948 — one surface reporting ``client_managed``
while another reported ``manual_launch`` for the same process — cannot recur
by construction.
Two independent dimensions are reported:
``launch_provenance``
How the process was started. Answered from the environment, which is
legitimate because the launcher is what sets it. Preserves the #686
manual-launch wall unchanged.
``session_ownership``
Which live client session owns this runtime now. Answered *only* from a
live attachment record; no environment flag can establish it.
Missing evidence yields :data:`PROVENANCE_UNPROVEN`, which is deliberately
distinct from :data:`PROVENANCE_MANUAL`: "we cannot prove who owns this" is
not the same claim as "a human launched this from a terminal", and only the
latter justifies telling an operator to stop hand-launching servers.
"""
env_signal = env_provenance_signal(env)
launch = assess_launch_provenance(
env, stdin_is_tty=stdin_is_tty, declared_only=declared_only
)
evidence: list[str] = [EVIDENCE_ENV_FLAG] if env_signal["keys_present"] else []
reasons: list[str] = []
record: dict[str, Any] | None = None
liveness: dict[str, Any] = {"live": False, "reasons": ["no attachment record"]}
if registry is not None and worker_identity:
record = registry.get(worker_identity)
if record is not None:
pid_alive = (
pid_alive_probe(record.get("pid"))
if pid_alive_probe is not None
else None
)
liveness = registry.is_live(record, now=now, pid_alive=pid_alive)
if liveness["live"]:
evidence.append(EVIDENCE_ATTACHMENT_RECORD)
if pid_alive is not None:
evidence.append(EVIDENCE_PROCESS_LIVENESS)
if native_transport_bound:
evidence.append(EVIDENCE_NATIVE_TRANSPORT)
# Conflicting live claims on one generation are contradictory evidence: two
# sessions cannot both currently own the same daemon, so neither claim is
# trustworthy and the assessment fails closed rather than picking a winner.
conflicting: list[dict[str, Any]] = []
effective_generation = generation_id or (record or {}).get("generation_id")
if registry is not None and effective_generation:
for holder in registry.list_workers(generation_id=effective_generation):
if (
record is not None
and holder["worker_identity"] == record["worker_identity"]
):
continue
pid_alive = (
pid_alive_probe(holder.get("pid"))
if pid_alive_probe is not None
else None
)
if registry.is_live(holder, now=now, pid_alive=pid_alive)["live"]:
conflicting.append(_public_record(holder))
trusted = bool(set(evidence) & SUFFICIENT_EVIDENCE)
blocker = BLOCKER_NONE
if conflicting and trusted:
trusted = False
blocker = BLOCKER_CONTRADICTORY
reasons.append(
f"generation {effective_generation!r} is claimed by "
f"{len(conflicting) + 1} live sessions at once; contradictory ownership "
"evidence fails closed (#948)"
)
elif not trusted:
blocker = BLOCKER_NO_ATTACHMENT
if record is None:
reasons.append(
"no live client/session attachment record binds this runtime to a "
"session; a process environment flag alone does not prove current "
"session ownership (#948)"
)
else:
reasons.extend(
f"attachment record is not live: {r}" for r in liveness["reasons"]
)
if conflicting:
session_ownership = OWNERSHIP_CONTESTED
elif trusted:
session_ownership = OWNERSHIP_OWNED
else:
session_ownership = OWNERSHIP_UNOWNED
# The headline verdict. Session ownership decides it when proven; otherwise
# the launch dimension supplies the more specific of "hand-launched" versus
# "cannot tell", so remediation names the proof that is actually missing.
if session_ownership == OWNERSHIP_OWNED:
provenance = PROVENANCE_CLIENT_SESSION
elif launch["launch_provenance"] == PROVENANCE_MANUAL:
provenance = PROVENANCE_MANUAL
reasons.extend(launch["reasons"])
else:
provenance = PROVENANCE_UNPROVEN
client_name = normalize_client_name(
(record or {}).get("client_name")
or parse_worker_identity(worker_identity)["client_name"]
)
return {
"provenance": provenance,
# #948: the *launch* dimension, which is what this field has always
# meant to the #686 manual-launch wall. Keeping it bound to launch
# provenance means unifying the two models changes which code decides,
# not what gets decided, so no mutation that used to be permitted is
# newly refused.
"is_client_managed": bool(launch["client_managed"]),
"launch_provenance": launch["launch_provenance"],
"launch_decided_by": launch["decided_by"],
"launch_reasons": launch["reasons"],
# The *ownership* dimension, which no environment flag can establish.
"session_ownership": session_ownership,
"session_owned": session_ownership == OWNERSHIP_OWNED,
"trusted_evidence": trusted,
"evidence": sorted(set(evidence)),
"sufficient_evidence_kinds": sorted(SUFFICIENT_EVIDENCE),
"env_signal": env_signal,
"env_flag_only": bool(evidence) and not trusted and not conflicting,
"attachment": _public_record(record) if record else None,
"attachment_liveness": liveness,
"client_name": client_name,
"client_known": client_name != UNKNOWN_CLIENT,
"session_id": (record or {}).get("session_id"),
"client_instance_id": (record or {}).get("client_instance_id"),
"generation_id": effective_generation,
"worker_identity": (record or {}).get("worker_identity") or worker_identity,
"fencing_epoch": (record or {}).get("fencing_epoch"),
"conflicting_live_sessions": conflicting,
"blocker_kind": blocker,
"fail_closed": provenance != PROVENANCE_CLIENT_SESSION,
# #948 AC17/AC41: a refusal names the worker it applies to, never the
# profile, so unrelated healthy workers sharing that profile keep working.
"scope": {
"scope_kind": "worker_identity" if worker_identity else "generation",
"worker_identity": worker_identity,
"generation_id": effective_generation,
"namespace": namespace,
"profile": profile,
"role": role,
"profile_wide": False,
"note": (
"role and profile are reusable capability definitions; this verdict "
"binds only the named worker identity/generation"
),
},
"reasons": reasons,
}
# --- Cohort scoping ------------------------------------------------------
def classify_cohort(
records: list[dict[str, Any]],
*,
now: datetime | None = None,
pid_alive_probe=None,
) -> dict[str, Any]:
"""Separate genuine duplicates from valid concurrent workers (#948 AC43).
The pre-#948 scan flagged every additional process sharing a profile as a
duplicate, which is exactly the fleet-wide wall #948 exists to remove.
Sharing a role or profile is legitimate; reusing a *worker identity*, or two
live sessions claiming one generation, is not.
"""
live: list[dict[str, Any]] = []
for record in records:
pid_alive = (
pid_alive_probe(record.get("pid")) if pid_alive_probe is not None else None
)
if WorkerRegistry.is_live(record, now=now, pid_alive=pid_alive)["live"]:
live.append(record)
by_identity: dict[str, list[dict[str, Any]]] = {}
by_generation: dict[str, list[dict[str, Any]]] = {}
for record in live:
by_identity.setdefault(str(record.get("worker_identity")), []).append(record)
by_generation.setdefault(str(record.get("generation_id")), []).append(record)
duplicate_identities = sorted(k for k, v in by_identity.items() if len(v) > 1)
contested_generations = sorted(k for k, v in by_generation.items() if len(v) > 1)
shared_profiles = sorted(
{
str(r.get("profile"))
for r in live
if r.get("profile")
and sum(1 for o in live if o.get("profile") == r.get("profile")) > 1
}
)
blocked = bool(duplicate_identities or contested_generations)
reasons: list[str] = []
if duplicate_identities:
reasons.append(
f"worker identity reused by more than one live worker: "
f"{duplicate_identities}"
)
if contested_generations:
reasons.append(
f"generation claimed by more than one live session: {contested_generations}"
)
if shared_profiles and not blocked:
reasons.append(
f"profile(s) {shared_profiles} are shared by multiple live workers with "
"distinct identities, which is permitted and does not block mutations (#948)"
)
return {
"live_worker_count": len(live),
"live_workers": [_public_record(r) for r in live],
"duplicate_identities": duplicate_identities,
"contested_generations": contested_generations,
"shared_profiles": shared_profiles,
"profile_sharing_permitted": True,
"blocked": blocked,
"blocker_kind": (
BLOCKER_IDENTITY_COLLISION
if duplicate_identities
else BLOCKER_CONFLICTING_SESSIONS
if contested_generations
else BLOCKER_NONE
),
# Never profile-wide: the block names the offending identities only.
"blocked_worker_identities": sorted(
set(duplicate_identities)
| {
str(r.get("worker_identity"))
for gen in contested_generations
for r in by_generation[gen]
}
),
"reasons": reasons,
}
def scope_runtime_failure(
*,
failure_kind: str,
worker_identity: str | None = None,
generation_id: str | None = None,
profile: str | None = None,
role: str | None = None,
all_live_workers: list[dict[str, Any]] | None = None,
) -> dict[str, Any]:
"""Scope a stale-runtime/reconnect failure to one worker (#948 AC41).
Returns the set of workers actually affected and, separately, the healthy
workers that must keep operating. A stale generation affects only its own
ownership scope.
"""
workers = list(all_live_workers or [])
affected = [
_public_record(w)
for w in workers
if (worker_identity and w.get("worker_identity") == worker_identity)
or (
not worker_identity
and generation_id
and w.get("generation_id") == generation_id
)
]
affected_ids = {a["worker_identity"] for a in affected}
unaffected = [
_public_record(w)
for w in workers
if w.get("worker_identity") not in affected_ids
]
return {
"failure_kind": failure_kind,
"scope_kind": "worker_identity" if worker_identity else "generation",
"worker_identity": worker_identity,
"generation_id": generation_id,
"profile": profile,
"role": role,
"profile_wide": False,
"fleet_wide": False,
"affected_workers": affected,
"unaffected_workers": unaffected,
"unaffected_worker_count": len(unaffected),
"reasons": [
f"{failure_kind} is scoped to "
+ (
f"worker identity {worker_identity!r}"
if worker_identity
else f"generation {generation_id!r}"
)
+ f"; {len(unaffected)} other live worker(s) sharing this role/profile "
"are unaffected and remain able to mutate (#948 AC41)"
],
}
# --- Client-specific remediation ----------------------------------------
def reconnect_client_for(
provenance: dict[str, Any] | None = None,
*,
fallback: str | None = None,
) -> str:
"""Name the client to address reconnect guidance to.
Derived from the live attachment record, never a hardcoded vendor. When
nothing identifies the client, :data:`UNKNOWN_CLIENT` is returned so the
caller emits generic steps instead of steps for the wrong product.
"""
name = normalize_client_name((provenance or {}).get("client_name"))
if name != UNKNOWN_CLIENT:
return name
return normalize_client_name(fallback)
# --- Remote binding ------------------------------------------------------
def resolve_bound_remote(
*,
requested_remote: str | None,
bound_remote: str | None,
default_remote: str | None = None,
) -> dict[str, Any]:
"""Keep a namespace on the remote it is bound to (#948).
A tool default is not a decision. When a namespace is bound to a remote,
an omitted argument resolves to that binding rather than silently falling
through to the library default and hitting the wrong host; an argument that
contradicts the binding is refused rather than honoured.
"""
requested = (requested_remote or "").strip() or None
bound = (bound_remote or "").strip() or None
default = (default_remote or "").strip() or None
if bound and requested and requested != bound:
return {
"remote": bound,
"resolved_from": "session_binding",
"drifted": True,
"honoured_request": False,
"reasons": [
f"requested remote {requested!r} contradicts the session binding "
f"{bound!r}; the binding wins and the request is refused so a "
"PRGS-bound namespace never mutates another host (#948)"
],
}
if bound and not requested:
return {
"remote": bound,
"resolved_from": "session_binding",
"drifted": False,
"honoured_request": False,
"reasons": [
f"remote omitted; resolved to the session binding {bound!r} rather "
f"than the library default {default!r} (#948)"
],
}
if requested:
return {
"remote": requested,
"resolved_from": "explicit_argument",
"drifted": False,
"honoured_request": True,
"reasons": [],
}
return {
"remote": default,
"resolved_from": "library_default",
"drifted": False,
"honoured_request": False,
"reasons": [
"no remote requested and no session binding recorded; falling back to "
f"the library default {default!r}. Bind the session or pass remote "
"explicitly to avoid host drift (#948)."
],
}
# --- Production heartbeat lifecycle (#975) --------------------------------
#
# #948 delivered ``WorkerRegistry.heartbeat()`` and nothing ever called it, so
# ``last_heartbeat_at`` stayed pinned to ``started_at`` for every production
# worker and ``heartbeat_ttl_seconds`` stopped being a liveness window at all —
# it became a hard cap on how long any client could stay attached. This
# supervisor is the missing caller.
#
# It is a daemon thread rather than an asyncio task or a per-request refresh
# because the renewal has to survive an *idle* session: a client waiting on a
# lease with no tool call in flight must not lose ownership, which rules out
# request-driven refresh. Registration itself happens lazily inside a tool call
# on the server's event loop, and the registry performs blocking
# ``BEGIN IMMEDIATE`` sqlite writes, which must not run on that loop.
# ``daemon=True`` is deliberate: a hard kill takes the thread down with the
# process, so a dead worker still goes stale on the normal TTL.
#: Refusals that mean this worker no longer owns its row. Beating again could
#: only ever be an attempt to renew ownership it has already lost, so the
#: supervisor stops permanently instead of retrying.
TERMINAL_HEARTBEAT_BLOCKERS = frozenset({BLOCKER_FENCED, BLOCKER_NO_ATTACHMENT})
class WorkerHeartbeatSupervisor:
"""Periodically renew exactly one worker registration.
Contract:
* It never registers. A supervisor exists only for an already-registered
identity, so it cannot create a second registration or a second identity
system.
* Every beat presents the full expectation set, so it can renew only the row
matching this exact client name, worker identity, session, generation and
pid.
* A terminal refusal (fenced or missing) stops it permanently and records
why. A fenced session must never beat its way back into ownership.
* A transient failure (a locked database, say) is counted and the loop
continues, so one contended write does not silently end the heartbeat.
* Nothing here raises into a caller. ``beat_once`` returns its outcome and
the thread body swallows everything, because a heartbeat failure must
degrade to "not renewed" and never crash a tool call.
"""
def __init__(
self,
registry: WorkerRegistry,
*,
worker_identity: str,
fencing_epoch: int,
session_id: str | None = None,
generation_id: str | None = None,
client_name: str | None = None,
pid: int | None = None,
ttl_seconds: float = DEFAULT_HEARTBEAT_TTL_SECONDS,
interval_seconds: float | None = None,
clock=None,
) -> None:
self._registry = registry
self.worker_identity = worker_identity
self.fencing_epoch = int(fencing_epoch)
self.session_id = session_id
self.generation_id = generation_id
self.client_name = (
normalize_client_name(client_name) if client_name is not None else None
)
self.pid = int(pid) if pid is not None else None
self.ttl_seconds = float(ttl_seconds)
self.interval_seconds = (
heartbeat_interval_for(self.ttl_seconds)
if interval_seconds is None
else heartbeat_interval_for(self.ttl_seconds, interval_seconds)
)
#: Injectable so every TTL test uses controlled time and no test waits
#: for a real interval or a real TTL to elapse.
self._clock = clock or _utc_now
self._stop_event = threading.Event()
self._thread: threading.Thread | None = None
self._lock = threading.Lock()
self._atexit_registered = False
self.started = False
self.stopped_reason: str | None = None
self.beats_attempted = 0
self.beats_renewed = 0
self.transient_failures = 0
self.last_beat_at: str | None = None
self.last_result: dict[str, Any] | None = None
# -- one beat --
def beat_once(self, now: datetime | None = None) -> dict[str, Any]:
"""Renew once. Never raises; returns the registry outcome or a failure."""
if self.stopped_reason is not None:
return {
"success": False,
"renewed": False,
"beat_attempted": False,
"reasons": [
f"supervisor already stopped: {self.stopped_reason}"
],
}
stamp = now or self._clock()
self.beats_attempted += 1
try:
result = self._registry.heartbeat(
worker_identity=self.worker_identity,
fencing_epoch=self.fencing_epoch,
now=stamp,
expected_session_id=self.session_id,
expected_generation_id=self.generation_id,
expected_client_name=self.client_name,
expected_pid=self.pid,
)
except Exception as exc:
# Transient by assumption: an unexpected error is not proof this
# worker lost ownership, so it must not silently end the heartbeat.
self.transient_failures += 1
result = {
"success": False,
"renewed": False,
"transient": True,
"blocker_kind": None,
"reasons": [f"heartbeat raised {type(exc).__name__}: {exc}"],
}
self.last_result = result
return result
self.last_result = result
if result.get("renewed"):
self.beats_renewed += 1
self.last_beat_at = result.get("last_heartbeat_at") or _ts(stamp)
return result
blocker = result.get("blocker_kind")
if blocker in TERMINAL_HEARTBEAT_BLOCKERS:
self._stop_internal(
reason=(
f"refused with blocker_kind={blocker!r}: "
+ "; ".join(result.get("reasons") or [])
)
)
else:
self.transient_failures += 1
return result
# -- lifecycle --
def start(self) -> dict[str, Any]:
"""Start the beat thread. Idempotent; safe to call from a tool call."""
with self._lock:
if self.stopped_reason is not None:
return {
"started": False,
"reasons": [f"supervisor stopped: {self.stopped_reason}"],
}
if self._thread is not None and self._thread.is_alive():
return {"started": True, "already_running": True, "reasons": []}
self._stop_event.clear()
thread = threading.Thread(
target=self._run,
name=f"gitea-worker-heartbeat-{self.worker_identity}",
daemon=True,
)
self._thread = thread
self.started = True
if not self._atexit_registered:
# Orderly shutdown stops the heartbeat. A hard kill does not
# run this, which is correct: the row must then go stale.
atexit.register(self._atexit_stop)
self._atexit_registered = True
thread.start()
return {"started": True, "already_running": False, "reasons": []}
def _run(self) -> None:
while not self._stop_event.is_set():
# Wait first: registration already stamped a fresh heartbeat, so an
# immediate beat would be a redundant write on every server launch.
if self._stop_event.wait(self.interval_seconds):
return
try:
self.beat_once()
except Exception:
# beat_once is already total; this is the last-resort guard that
# keeps a supervisor thread from dying silently.
self.transient_failures += 1
if self.stopped_reason is not None:
return
def stop(self, reason: str = "stopped") -> dict[str, Any]:
"""Stop beating. Idempotent, and prompt because the loop waits on an Event."""
self._stop_internal(reason=reason)
thread = self._thread
if thread is not None and thread.is_alive():
thread.join(timeout=max(1.0, min(5.0, self.interval_seconds)))
return {
"stopped": True,
"reason": self.stopped_reason,
"thread_alive": bool(thread is not None and thread.is_alive()),
}
def _stop_internal(self, *, reason: str) -> None:
if self.stopped_reason is None:
self.stopped_reason = reason
self._stop_event.set()
if self._atexit_registered:
try:
atexit.unregister(self._atexit_stop)
except Exception:
pass
self._atexit_registered = False
def _atexit_stop(self) -> None:
try:
self.stop(reason="process exit")
except Exception:
pass
# -- observability --
def status(self) -> dict[str, Any]:
"""Read-only observability payload; safe to embed in a tool result."""
thread = self._thread
return {
"supervised": True,
"worker_identity": self.worker_identity,
"fencing_epoch": self.fencing_epoch,
"session_id": self.session_id,
"generation_id": self.generation_id,
"client_name": self.client_name,
"pid": self.pid,
"heartbeat_ttl_seconds": self.ttl_seconds,
"heartbeat_interval_seconds": self.interval_seconds,
"started": self.started,
"running": bool(
thread is not None
and thread.is_alive()
and self.stopped_reason is None
),
"stopped_reason": self.stopped_reason,
"beats_attempted": self.beats_attempted,
"beats_renewed": self.beats_renewed,
"transient_failures": self.transient_failures,
"last_heartbeat_at": self.last_beat_at,
"last_blocker_kind": (self.last_result or {}).get("blocker_kind"),
}