hermes-agent/gateway/readiness.py

117 lines
4.1 KiB
Python

"""Bounded, non-destructive readiness probes for authenticated health surfaces."""
from __future__ import annotations
import shutil
import sqlite3
from pathlib import Path
from typing import Any
import yaml
from hermes_constants import get_hermes_home
_DISK_DEGRADED_PERCENT = 90.0
def _check(status: str, detail: str | None = None, **extra: Any) -> dict[str, Any]:
result: dict[str, Any] = {"status": status}
if detail:
result["detail"] = detail
result.update(extra)
return result
def _probe_state_db(home: Path) -> dict[str, Any]:
path = home / "state.db"
if not path.exists():
return _check("ok", "not initialized")
try:
# A readiness probe must never compete with normal state writers. A
# read-only schema query still catches unreadable/corrupt databases
# without taking a write reservation on every health poll.
uri = f"file:{path.as_posix()}?mode=ro"
with sqlite3.connect(uri, uri=True, timeout=1.0) as conn:
conn.execute("PRAGMA query_only = ON")
conn.execute("SELECT name FROM sqlite_master LIMIT 1").fetchone()
return _check("ok")
except Exception as exc:
return _check("degraded", type(exc).__name__)
def _probe_config(home: Path) -> dict[str, Any]:
path = home / "config.yaml"
if not path.exists():
return _check("ok", "using defaults")
try:
raw = yaml.safe_load(path.read_text(encoding="utf-8"))
if raw is not None and not isinstance(raw, dict):
return _check("degraded", "top level is not a mapping")
return _check("ok")
except Exception as exc:
return _check("degraded", f"invalid config ({type(exc).__name__})")
def _probe_disk(home: Path) -> dict[str, Any]:
try:
usage = shutil.disk_usage(home)
used_pct = round((usage.used / usage.total) * 100, 1) if usage.total else 0.0
status = "degraded" if used_pct >= _DISK_DEGRADED_PERCENT else "ok"
return _check(status, used_percent=used_pct, free_bytes=usage.free)
except Exception as exc:
return _check("degraded", type(exc).__name__)
def _probe_gateway(runtime_status: dict[str, Any]) -> dict[str, Any]:
state = str(runtime_status.get("gateway_state") or "unknown")
platforms = runtime_status.get("platforms")
connected = 0
configured = 0
if isinstance(platforms, dict):
configured = len(platforms)
connected = sum(
1
for value in platforms.values()
if isinstance(value, dict)
and str(value.get("state") or value.get("status") or "").lower()
in {"connected", "running", "ok"}
)
status = "ok" if state in {"running", "draining"} else "degraded"
return _check(status, state=state, connected_platforms=connected, platforms=configured)
def collect_runtime_readiness(
*,
configured_model: str,
runtime_status: dict[str, Any] | None,
active_api_runs: int = 0,
process_completion_queue_depth: int = 0,
active_delegations: int = 0,
) -> dict[str, Any]:
"""Return bounded readiness diagnostics without mutating runtime state.
The detailed health endpoint is authenticated. Even there, probes expose
status and counts only: never config values, credentials, paths, commands,
queue payloads, or exception messages.
"""
home = get_hermes_home()
runtime = runtime_status if isinstance(runtime_status, dict) else {}
checks = {
"state_db": _probe_state_db(home),
"config": _probe_config(home),
"model": _check("ok" if str(configured_model or "").strip() else "degraded"),
"disk": _probe_disk(home),
"gateway": _probe_gateway(runtime),
"background_queues": _check(
"ok",
active_api_runs=max(0, int(active_api_runs)),
process_completions=max(0, int(process_completion_queue_depth)),
active_delegations=max(0, int(active_delegations)),
),
}
overall = "ok" if all(item.get("status") == "ok" for item in checks.values()) else "degraded"
return {"status": overall, "checks": checks}
__all__ = ["collect_runtime_readiness"]