172 lines
5.8 KiB
Python
Executable File
172 lines
5.8 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Prove a real central-model decision through the authenticated Gateway."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
import stat
|
|
import sys
|
|
import time
|
|
from pathlib import Path
|
|
import uuid
|
|
|
|
from health_probe_common import (
|
|
ProbeError, atomic_receipt, gateway_base, load_config, read_token,
|
|
request_json, state_version,
|
|
)
|
|
|
|
PENDING_PATH = Path("/run/aios-health/central-gpt.pending.json")
|
|
|
|
|
|
def _load_pending(
|
|
path: Path, contract: str, *, owners: set[int] | None = None
|
|
) -> tuple[str, int] | None:
|
|
allowed_owners = {0} if owners is None else owners
|
|
if not path.exists():
|
|
return None
|
|
info = path.lstat()
|
|
if (
|
|
not path.is_absolute()
|
|
or stat.S_ISLNK(info.st_mode)
|
|
or not stat.S_ISREG(info.st_mode)
|
|
or stat.S_IMODE(info.st_mode) & ~0o640
|
|
or (os.name == "posix" and info.st_uid not in allowed_owners)
|
|
or path.resolve(strict=True) != Path(os.path.abspath(path))
|
|
):
|
|
raise ProbeError("unsafe_pending_marker")
|
|
value = json.loads(path.read_text(encoding="utf-8"))
|
|
if not isinstance(value, dict) or set(value) != {"contract_commit", "event_id", "state_version"}:
|
|
raise ProbeError("invalid_pending_marker")
|
|
event_id = value["event_id"]
|
|
version = value["state_version"]
|
|
if (
|
|
value["contract_commit"] != contract
|
|
or not isinstance(event_id, str)
|
|
or not event_id.startswith("health-central-")
|
|
or len(event_id) != 47
|
|
or not isinstance(version, int)
|
|
or version < 0
|
|
):
|
|
raise ProbeError("invalid_pending_marker")
|
|
return event_id, version
|
|
|
|
|
|
def _clear_pending(path: Path) -> None:
|
|
if not path.exists():
|
|
return
|
|
info = path.lstat()
|
|
if stat.S_ISLNK(info.st_mode) or not stat.S_ISREG(info.st_mode):
|
|
raise ProbeError("unsafe_pending_marker")
|
|
path.unlink()
|
|
|
|
|
|
def _safe_reason(exc: BaseException) -> str:
|
|
reason = str(exc)
|
|
if reason and len(reason) <= 64 and all(char.islower() or char.isdigit() or char == "_" for char in reason):
|
|
return reason
|
|
return "probe_error"
|
|
|
|
|
|
def run(config_path: Path, pending_path: Path = PENDING_PATH) -> int:
|
|
config = load_config(config_path)
|
|
endpoint = config["gateway_event_url"]
|
|
gateway_base(endpoint)
|
|
token = read_token(Path(config["gateway_token_file"]))
|
|
project = config["project_key"]
|
|
contract = config["contract_commit"]
|
|
pending = _load_pending(pending_path, contract)
|
|
is_new = pending is None
|
|
if is_new:
|
|
event_id = "health-central-" + uuid.uuid4().hex
|
|
version = state_version(endpoint, project, token)
|
|
atomic_receipt(
|
|
pending_path,
|
|
{"contract_commit": contract, "event_id": event_id, "state_version": version},
|
|
)
|
|
else:
|
|
event_id, version = pending
|
|
event = {
|
|
"schema_version": 1,
|
|
"project_key": project,
|
|
"contract_commit": contract,
|
|
"event_id": event_id,
|
|
"source_agent": "system",
|
|
"event_type": "central_gpt_health_probe",
|
|
"work_unit_id": "CENTRAL-GPT-HEALTH",
|
|
"status": "READY",
|
|
"state_version": version,
|
|
"implementation_commit": None,
|
|
"result_commit": None,
|
|
"blocker": None,
|
|
"evidence_refs": [],
|
|
"payload": {"probe": "side_effect_free", "nonce": event_id},
|
|
}
|
|
should_post = is_new
|
|
if not is_new:
|
|
try:
|
|
_, observed = request_json(endpoint + "/" + event_id, token)
|
|
except ProbeError as exc:
|
|
if str(exc) != "gateway_http_404":
|
|
raise
|
|
should_post = True
|
|
else:
|
|
observed = None
|
|
if should_post:
|
|
try:
|
|
status, _ = request_json(endpoint, token, method="POST", payload=event)
|
|
except ProbeError as exc:
|
|
if str(exc) == "gateway_fail_closed_409":
|
|
_clear_pending(pending_path)
|
|
raise
|
|
if status not in {200, 202}:
|
|
raise ProbeError("event_not_accepted")
|
|
observed = None
|
|
deadline = time.monotonic() + int(config["central_timeout_seconds"])
|
|
while time.monotonic() < deadline:
|
|
_, observed = request_json(endpoint + "/" + event_id, token)
|
|
if observed.get("status") in {"DECIDED", "TRIGGER_FAILED", "DEAD_LETTER"}:
|
|
break
|
|
time.sleep(float(config["poll_interval_seconds"]))
|
|
if isinstance(observed, dict) and observed.get("status") == "DEAD_LETTER":
|
|
_clear_pending(pending_path)
|
|
raise ProbeError("central_event_dead_letter")
|
|
if not isinstance(observed, dict) or observed.get("status") != "DECIDED" or observed.get("decision_applied") is not True:
|
|
raise ProbeError("central_decision_not_proven")
|
|
channel = observed.get("trigger_channel")
|
|
if channel not in {"workspace_agent", "responses_api"}:
|
|
raise ProbeError("central_channel_not_proven")
|
|
_clear_pending(pending_path)
|
|
now = int(time.time())
|
|
atomic_receipt(
|
|
Path(config["central_receipt_path"]),
|
|
{
|
|
"subject": "central_gpt",
|
|
"health": "HEALTHY",
|
|
"live_probe_succeeded": True,
|
|
"observed_at_epoch": now,
|
|
"event_id": event_id,
|
|
"decision_applied": True,
|
|
"state_version_after": observed["state_version_after"],
|
|
"workspace_agent": {"validated_live": channel == "workspace_agent"},
|
|
"responses_api": {"validated_live": channel == "responses_api"},
|
|
},
|
|
)
|
|
return 0
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--config", required=True, type=Path)
|
|
args = parser.parse_args()
|
|
try:
|
|
return run(args.config)
|
|
except (KeyError, OSError, ProbeError, ValueError) as exc:
|
|
print(f"CENTRAL_GPT_PROBE=BLOCKED reason={_safe_reason(exc)}", file=sys.stderr)
|
|
return 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|