feat(controller): expose instance-level fleet identity and health snapshots
Add a pure fleet snapshot assessor and a read-only controller/reconciler MCP tool so multi-instance fleets can be enumerated by client_instance_id without treating shared client_type as duplication. Trusted launcher instance IDs, classification (missing/unmanifested/collisions/historical), registry schema extensions, docs, and regression tests for #978. Closes #978 Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
+217
-6
@@ -15733,6 +15733,7 @@ _WORKER_HEARTBEAT_SUPERVISOR = None
|
||||
CLIENT_NAME_ENV = "GITEA_MCP_CLIENT"
|
||||
CLIENT_INSTANCE_ENV = "GITEA_MCP_CLIENT_INSTANCE"
|
||||
CLIENT_SESSION_ENV = "GITEA_MCP_CLIENT_SESSION"
|
||||
FLEET_RUN_ENV = "GITEA_MCP_FLEET_RUN_ID"
|
||||
|
||||
|
||||
def _worker_registry():
|
||||
@@ -15756,13 +15757,34 @@ def _worker_registry():
|
||||
|
||||
|
||||
def _client_identity_hints() -> dict:
|
||||
"""What the launcher told us about itself. Unset fields stay unset."""
|
||||
"""What the launcher told us about itself (#948 / #975 / #978).
|
||||
|
||||
``client_instance_id`` is established by the trusted application launcher
|
||||
once per launch and shared by all five namespace workers. When the launcher
|
||||
key is absent we still register under an explicit *legacy* placeholder so
|
||||
diagnostic reads work, but the registration is marked incomplete and cannot
|
||||
authorize multi-instance fleet mutation safety.
|
||||
"""
|
||||
import mcp_fleet_snapshot
|
||||
|
||||
client_name = (os.environ.get(CLIENT_NAME_ENV) or "").strip() or None
|
||||
raw_instance = (os.environ.get(CLIENT_INSTANCE_ENV) or "").strip() or None
|
||||
instance = mcp_fleet_snapshot.assess_instance_identity(raw_instance)
|
||||
# Legacy placeholder only when the launcher omitted the key — never invent a
|
||||
# trusted ID from PID proximity.
|
||||
if not instance["client_instance_id"]:
|
||||
placeholder = f"legacy-pid-{os.getpid()}"
|
||||
instance = mcp_fleet_snapshot.assess_instance_identity(
|
||||
placeholder, source="pid_fallback"
|
||||
)
|
||||
return {
|
||||
"client_name": (os.environ.get(CLIENT_NAME_ENV) or "").strip() or None,
|
||||
"client_instance_id": (os.environ.get(CLIENT_INSTANCE_ENV) or "").strip()
|
||||
or f"pid-{os.getpid()}",
|
||||
"client_name": client_name,
|
||||
"client_instance_id": instance["client_instance_id"],
|
||||
"instance_id_provenance": instance["provenance"],
|
||||
"instance_identity_trusted": instance["trusted"],
|
||||
"session_id": (os.environ.get(CLIENT_SESSION_ENV) or "").strip()
|
||||
or f"proc-{os.getpid()}-{_process_boot_head_sha or 'nohead'}",
|
||||
"fleet_run_id": (os.environ.get(FLEET_RUN_ENV) or "").strip() or None,
|
||||
}
|
||||
|
||||
|
||||
@@ -15794,20 +15816,43 @@ def _active_worker_identity() -> str | None:
|
||||
identity = mcp_worker_identity.generate_worker_identity(
|
||||
hints["client_name"], hints["session_id"]
|
||||
)
|
||||
role = _active_role_kind_safe()
|
||||
profile_name = (
|
||||
(os.environ.get(gitea_config.ENV_PROFILE) or "").strip() or None
|
||||
)
|
||||
# Namespace is the role namespace (author/reviewer/…) when known.
|
||||
namespace = role if role in {
|
||||
"author", "reviewer", "merger", "controller", "reconciler"
|
||||
} else None
|
||||
parity = None
|
||||
try:
|
||||
parity = _current_master_parity()
|
||||
except Exception:
|
||||
parity = None
|
||||
outcome = registry.register(
|
||||
worker_identity=identity,
|
||||
client_name=hints["client_name"],
|
||||
client_instance_id=hints["client_instance_id"],
|
||||
session_id=hints["session_id"],
|
||||
generation_id=generation,
|
||||
role=_active_role_kind_safe(),
|
||||
profile=(os.environ.get(gitea_config.ENV_PROFILE) or "").strip() or None,
|
||||
role=role,
|
||||
profile=profile_name,
|
||||
namespace=namespace,
|
||||
remote=(os.environ.get("GITEA_MCP_REMOTE") or "").strip() or None,
|
||||
repository_binding=PROJECT_ROOT,
|
||||
pid=os.getpid(),
|
||||
transport=native.get("bound_transport"),
|
||||
token_fingerprint=native.get("token_fingerprint"),
|
||||
pid_alive_probe=issue_lock_store.is_process_alive,
|
||||
fleet_run_id=hints.get("fleet_run_id"),
|
||||
process_identity=f"pid-{os.getpid()}",
|
||||
startup_revision=(parity or {}).get("startup_head")
|
||||
or _process_boot_head_sha,
|
||||
loaded_revision=(parity or {}).get("local_head")
|
||||
or (parity or {}).get("current_head"),
|
||||
parity_revision=(parity or {}).get("current_head"),
|
||||
live_revision=(parity or {}).get("live_remote_head"),
|
||||
instance_id_provenance=hints.get("instance_id_provenance"),
|
||||
)
|
||||
if outcome.get("registered"):
|
||||
_WORKER_IDENTITY = identity
|
||||
@@ -19373,6 +19418,172 @@ def gitea_validate_review_final_report(
|
||||
)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def gitea_snapshot_instance_fleet(
|
||||
remote: str = "dadeschools",
|
||||
host: str | None = None,
|
||||
org: str | None = None,
|
||||
repo: str | None = None,
|
||||
expected_manifest: list | dict | str | None = None,
|
||||
include_historical: bool = True,
|
||||
canonical_repository: str | None = None,
|
||||
expected_live_revision: str | None = None,
|
||||
) -> dict:
|
||||
"""Read-only: authoritative instance-level fleet identity and health snapshot (#978).
|
||||
|
||||
Controller and reconciler only. Returns a point-in-time snapshot of every
|
||||
registered namespace worker with instance attribution, heartbeat freshness,
|
||||
and structured classification (expected/missing/unmanifested, duplicate
|
||||
namespace workers, identity collisions, foreign repository, old revision,
|
||||
historical dead rows). Sharing only a client type is never a duplicate.
|
||||
|
||||
Does not grant any mutation capability. Does not scan process tables or
|
||||
open foreign databases for production evidence — the worker registry is
|
||||
the sole authority.
|
||||
|
||||
Args:
|
||||
remote: Known instance — 'dadeschools' or 'prgs'.
|
||||
host: Optional host override.
|
||||
org: Optional org override (audit context only).
|
||||
repo: Optional repo override (audit context only).
|
||||
expected_manifest: Optional approved fleet enrollment list
|
||||
(list of dicts with client_instance_id / client_type / fleet_run_id).
|
||||
When supplied, missing and unmanifested instances are classified.
|
||||
include_historical: Include released/superseded rows (default True);
|
||||
historical rows never make the live fleet unsafe by themselves.
|
||||
canonical_repository: Expected repository binding path/slug for
|
||||
foreign-repository classification.
|
||||
expected_live_revision: When set, workers whose recorded revisions
|
||||
differ are classified as old-revision.
|
||||
"""
|
||||
import json as _json
|
||||
|
||||
import mcp_fleet_snapshot
|
||||
|
||||
read_block = _profile_operation_gate("gitea.read")
|
||||
if read_block:
|
||||
return {
|
||||
"success": False,
|
||||
"read_only": True,
|
||||
"reasons": read_block,
|
||||
"permission_report": _permission_block_report("gitea.read"),
|
||||
}
|
||||
|
||||
profile = get_profile()
|
||||
role = _profile_role_kind(profile)
|
||||
if role not in {"controller", "reconciler"}:
|
||||
return {
|
||||
"success": False,
|
||||
"read_only": True,
|
||||
"allowed": False,
|
||||
"denied_role": role,
|
||||
"required_roles": ["controller", "reconciler"],
|
||||
"mutation_performed": False,
|
||||
"reasons": [
|
||||
f"gitea_snapshot_instance_fleet is restricted to controller and "
|
||||
f"reconciler roles; active role_kind is {role!r}. Author, "
|
||||
"reviewer, and merger profiles keep gitea.read for diagnosis "
|
||||
"elsewhere but do not receive this fleet surface, and no "
|
||||
"unrelated mutation permission is granted."
|
||||
],
|
||||
"exact_next_action": (
|
||||
"Re-run from a prgs-controller or prgs-reconciler namespace."
|
||||
),
|
||||
}
|
||||
|
||||
manifest: list | None = None
|
||||
if expected_manifest is not None:
|
||||
raw = expected_manifest
|
||||
if isinstance(raw, str):
|
||||
try:
|
||||
raw = _json.loads(raw)
|
||||
except Exception as exc:
|
||||
return {
|
||||
"success": False,
|
||||
"read_only": True,
|
||||
"reasons": [
|
||||
f"expected_manifest is not valid JSON: {type(exc).__name__}"
|
||||
],
|
||||
}
|
||||
if isinstance(raw, dict):
|
||||
# Accept {"instances": [...]} or a single instance object.
|
||||
if "instances" in raw and isinstance(raw["instances"], list):
|
||||
raw = raw["instances"]
|
||||
else:
|
||||
raw = [raw]
|
||||
if not isinstance(raw, list):
|
||||
return {
|
||||
"success": False,
|
||||
"read_only": True,
|
||||
"reasons": ["expected_manifest must be a list of instance objects"],
|
||||
}
|
||||
manifest = raw
|
||||
|
||||
registry = _worker_registry()
|
||||
if registry is None:
|
||||
return {
|
||||
"success": False,
|
||||
"read_only": True,
|
||||
"reasons": [
|
||||
"worker registry is unavailable; cannot produce an authoritative "
|
||||
"fleet snapshot (fail closed)"
|
||||
],
|
||||
"exact_next_action": (
|
||||
"Ensure GITEA_WORKER_REGISTRY_DB is writable and re-run after "
|
||||
"workers have registered."
|
||||
),
|
||||
}
|
||||
|
||||
try:
|
||||
if include_historical:
|
||||
records = registry.list_workers(status=None)
|
||||
else:
|
||||
records = registry.list_workers(status=mcp_worker_identity.STATUS_ACTIVE)
|
||||
except Exception as exc:
|
||||
return {
|
||||
"success": False,
|
||||
"read_only": True,
|
||||
"reasons": [
|
||||
f"failed to read worker registry: {type(exc).__name__}: {_redact(str(exc))}"
|
||||
],
|
||||
}
|
||||
|
||||
parity = None
|
||||
try:
|
||||
parity = _current_master_parity()
|
||||
except Exception:
|
||||
parity = None
|
||||
live_rev = expected_live_revision or (parity or {}).get("live_remote_head")
|
||||
canon = canonical_repository or PROJECT_ROOT
|
||||
|
||||
snapshot = mcp_fleet_snapshot.snapshot_instance_fleet(
|
||||
records,
|
||||
expected_manifest=manifest,
|
||||
pid_alive_probe=issue_lock_store.is_process_alive,
|
||||
canonical_repository=canon,
|
||||
expected_live_revision=live_rev,
|
||||
)
|
||||
snapshot["role_kind"] = role
|
||||
snapshot["profile"] = profile.get("profile_name")
|
||||
snapshot["remote"] = _effective_remote(remote)
|
||||
snapshot["repository"] = {
|
||||
"org": org,
|
||||
"repo": repo,
|
||||
"canonical_repository": canon,
|
||||
}
|
||||
snapshot["mutation_performed"] = False
|
||||
snapshot["permission_scope"] = {
|
||||
"read_only": True,
|
||||
"granted_operations": ["gitea.read"],
|
||||
"denied_unrelated_mutations": True,
|
||||
"note": (
|
||||
"This capability is strictly observational. It does not authorize "
|
||||
"branch, issue, PR, review, merge, or restart mutations."
|
||||
),
|
||||
}
|
||||
return snapshot
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def gitea_get_runtime_context(
|
||||
remote: str = "dadeschools",
|
||||
|
||||
Reference in New Issue
Block a user