216 lines
8.6 KiB
Python
216 lines
8.6 KiB
Python
#!/usr/bin/env python3
|
|
"""Probe read-only Argus KB diagnostics for current approved-proposal state."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import base64
|
|
import json
|
|
import urllib.error
|
|
import urllib.request
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
from typing import Any, Callable
|
|
from urllib.parse import urljoin
|
|
|
|
DEFAULT_BASE_URL = "http://77.42.65.182:8081"
|
|
REPORT_DIR = Path("docs/reports/leo-working-state-20260709")
|
|
DEFAULT_OUT = REPORT_DIR / "argus-kb-state-current.json"
|
|
DEFAULT_MARKDOWN_OUT = REPORT_DIR / "argus-kb-state-current.md"
|
|
|
|
|
|
FetchJson = Callable[[str, int], dict[str, Any]]
|
|
|
|
|
|
def fetch_json(url: str, timeout: int) -> dict[str, Any]:
|
|
request = urllib.request.Request(url, headers={"Accept": "application/json"})
|
|
try:
|
|
with urllib.request.urlopen(request, timeout=timeout) as response: # noqa: S310 - operator-supplied URL
|
|
return json.loads(response.read().decode("utf-8"))
|
|
except (urllib.error.URLError, TimeoutError, json.JSONDecodeError) as exc:
|
|
return {"_fetch_error": str(exc)}
|
|
|
|
|
|
def endpoint(base_url: str, path: str) -> str:
|
|
return urljoin(base_url.rstrip("/") + "/", path.lstrip("/"))
|
|
|
|
|
|
def decode_supplied_json(value: str | None) -> dict[str, Any] | None:
|
|
if not value:
|
|
return None
|
|
try:
|
|
return json.loads(base64.b64decode(value, validate=True).decode("utf-8"))
|
|
except Exception as exc: # noqa: BLE001 - retained as a structured fetch error
|
|
return {"_fetch_error": f"failed to parse supplied base64 JSON: {exc}"}
|
|
|
|
|
|
def supplied_payloads(args: argparse.Namespace) -> dict[str, dict[str, Any] | None]:
|
|
return {
|
|
"kb_proposals": decode_supplied_json(args.kb_proposals_json_base64),
|
|
"vital_signs": decode_supplied_json(args.vital_signs_json_base64),
|
|
"metrics": decode_supplied_json(args.metrics_json_base64),
|
|
}
|
|
|
|
|
|
def proposal_summary(kb_proposals: dict[str, Any]) -> dict[str, Any]:
|
|
packets = kb_proposals.get("packets") if isinstance(kb_proposals.get("packets"), list) else []
|
|
return {
|
|
"generated_at": kb_proposals.get("generated_at"),
|
|
"filters": kb_proposals.get("filters"),
|
|
"total": kb_proposals.get("total"),
|
|
"status_counts": kb_proposals.get("status_counts") or {},
|
|
"review_state_counts": kb_proposals.get("review_state_counts") or {},
|
|
"worker_applyable_count": kb_proposals.get("worker_applyable_count"),
|
|
"read_only": kb_proposals.get("read_only"),
|
|
"proposal_ids": [packet.get("proposal_id") for packet in packets],
|
|
"proposal_review_states": {
|
|
packet.get("proposal_id"): packet.get("review_state") for packet in packets if packet.get("proposal_id")
|
|
},
|
|
"proposal_applyability": {
|
|
packet.get("proposal_id"): {
|
|
"worker_applyable": packet.get("worker_applyable"),
|
|
"has_apply_payload": packet.get("has_apply_payload"),
|
|
"next_admin_action": packet.get("next_admin_action"),
|
|
}
|
|
for packet in packets
|
|
if packet.get("proposal_id")
|
|
},
|
|
}
|
|
|
|
|
|
def collect(args: argparse.Namespace, *, fetcher: FetchJson = fetch_json) -> dict[str, Any]:
|
|
urls = {
|
|
"kb_proposals": endpoint(args.base_url, "/api/kb-proposals"),
|
|
"vital_signs": endpoint(args.base_url, "/api/vital-signs"),
|
|
"metrics": endpoint(args.base_url, "/api/metrics"),
|
|
}
|
|
supplied = supplied_payloads(args)
|
|
raw = {name: payload if payload is not None else fetcher(urls[name], args.timeout) for name, payload in supplied.items()}
|
|
fetch_sources = {name: "supplied_base64_json" if payload is not None else "urllib" for name, payload in supplied.items()}
|
|
proposals = proposal_summary(raw["kb_proposals"])
|
|
proposal_total = proposals.get("total")
|
|
review_state_counts = proposals.get("review_state_counts") or {}
|
|
status_counts = proposals.get("status_counts") or {}
|
|
checks = {
|
|
"http_readback_ok": all("_fetch_error" not in payload for payload in raw.values()),
|
|
"approved_proposals_visible": isinstance(proposal_total, int)
|
|
and proposal_total > 0
|
|
and status_counts.get("approved") == proposal_total,
|
|
"approved_need_apply_payload": isinstance(proposal_total, int)
|
|
and review_state_counts.get("approved_needs_apply_payload") == proposal_total,
|
|
"no_worker_applyable_approved_proposals": proposals.get("worker_applyable_count") == 0,
|
|
"kb_proposals_endpoint_read_only": proposals.get("read_only") is True,
|
|
}
|
|
return {
|
|
"generated_at_utc": datetime.now(timezone.utc).isoformat(),
|
|
"mode": "argus_kb_state_readonly_http_probe",
|
|
"base_url": args.base_url,
|
|
"urls": urls,
|
|
"fetch_sources": fetch_sources,
|
|
"status": "pass" if all(checks.values()) else "fail",
|
|
"checks": checks,
|
|
"summary": {
|
|
"proposals": proposals,
|
|
"vital_signs": {
|
|
"claim_index_status": raw["vital_signs"].get("claim_index_status"),
|
|
"review_throughput": raw["vital_signs"].get("review_throughput"),
|
|
"funnel": raw["vital_signs"].get("funnel"),
|
|
"queue_staleness": raw["vital_signs"].get("queue_staleness"),
|
|
},
|
|
"metrics": {
|
|
"status_map": raw["metrics"].get("status_map"),
|
|
"source_map": raw["metrics"].get("source_map"),
|
|
"breakers": raw["metrics"].get("breakers"),
|
|
},
|
|
},
|
|
"raw": raw,
|
|
"claim_ceiling": (
|
|
"This is public Argus HTTP diagnostics evidence only. It proves the diagnostics surface can see "
|
|
"approved KB proposals and their applyability state; it does not prove Docker/Postgres row counts, "
|
|
"gateway systemd state, Telegram-visible reply quality, or production apply execution."
|
|
),
|
|
}
|
|
|
|
|
|
def write_json(path: Path, data: dict[str, Any]) -> None:
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
path.write_text(json.dumps(data, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
|
|
|
|
|
def write_markdown(path: Path, data: dict[str, Any]) -> None:
|
|
proposals = data["summary"]["proposals"]
|
|
lines = [
|
|
"# Argus KB State Probe",
|
|
"",
|
|
f"Generated UTC: `{data['generated_at_utc']}`",
|
|
f"Mode: `{data['mode']}`",
|
|
f"Status: `{data['status']}`",
|
|
f"Base URL: `{data['base_url']}`",
|
|
"",
|
|
"## Fetch Sources",
|
|
"",
|
|
]
|
|
lines.extend(f"- `{key}`: `{value}`" for key, value in sorted(data["fetch_sources"].items()))
|
|
lines.extend(
|
|
[
|
|
"",
|
|
"## Checks",
|
|
"",
|
|
]
|
|
)
|
|
lines.extend(f"- `{key}`: `{value}`" for key, value in sorted(data["checks"].items()))
|
|
lines.extend(
|
|
[
|
|
"",
|
|
"## Approved Proposal State",
|
|
"",
|
|
f"- Total: `{proposals.get('total')}`",
|
|
f"- Status counts: `{json.dumps(proposals.get('status_counts'), sort_keys=True)}`",
|
|
f"- Review state counts: `{json.dumps(proposals.get('review_state_counts'), sort_keys=True)}`",
|
|
f"- Worker-applyable count: `{proposals.get('worker_applyable_count')}`",
|
|
f"- Read-only endpoint: `{proposals.get('read_only')}`",
|
|
"",
|
|
"## Proposal IDs",
|
|
"",
|
|
]
|
|
)
|
|
lines.extend(f"- `{proposal_id}`" for proposal_id in proposals.get("proposal_ids") or [])
|
|
lines.extend(["", "## Claim Ceiling", "", data["claim_ceiling"], ""])
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
path.write_text("\n".join(lines), encoding="utf-8")
|
|
|
|
|
|
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--base-url", default=DEFAULT_BASE_URL)
|
|
parser.add_argument("--timeout", type=int, default=10)
|
|
parser.add_argument("--kb-proposals-json-base64")
|
|
parser.add_argument("--vital-signs-json-base64")
|
|
parser.add_argument("--metrics-json-base64")
|
|
parser.add_argument("--out", type=Path, default=DEFAULT_OUT)
|
|
parser.add_argument("--markdown-out", type=Path, default=DEFAULT_MARKDOWN_OUT)
|
|
return parser.parse_args(argv)
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
args = parse_args(argv)
|
|
data = collect(args)
|
|
write_json(args.out, data)
|
|
write_markdown(args.markdown_out, data)
|
|
print(
|
|
json.dumps(
|
|
{
|
|
"out": str(args.out),
|
|
"markdown_out": str(args.markdown_out),
|
|
"status": data["status"],
|
|
"checks": data["checks"],
|
|
},
|
|
indent=2,
|
|
sort_keys=True,
|
|
)
|
|
)
|
|
return 0 if data["status"] == "pass" else 2
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|