"""INFD-WP-0002-T03 sitting bind preflight. Reports whether the compact sitting can be submitted to the live Stage 1 surface. Does not present, acknowledge, bind, or create approvals. """ from __future__ import annotations import argparse import json from pathlib import Path from urllib.error import HTTPError, URLError from urllib.request import HTTPRedirectHandler, Request, build_opener, urlopen from informed_decision.records import memo_from ROOT = Path(__file__).resolve().parents[1] / "docs" / "batches" / "2026-09-14" def load_sitting(root: Path = ROOT) -> dict: sitting = json.loads((root / "sitting.json").read_text()) memos = [] for name in ("credentials", "decisions"): index = json.loads((root / name / "index.json").read_text()) for row in index["ordinal"]: memo = memo_from(json.loads((root / name / row["memo"]).read_text())) memos.append( { "batch": name, "memo_id": memo.id, "approval_id": memo.approval_id, "principal": memo.binding.principal.id, "question": memo.question, } ) return {"sitting": sitting, "memos": memos} class _NoRedirect(HTTPRedirectHandler): def http_error_302(self, req, fp, code, msg, headers): raise HTTPError(req.full_url, code, msg, headers, fp) http_error_301 = http_error_303 = http_error_307 = http_error_308 = http_error_302 def _default_opener(): return build_opener(_NoRedirect()).open def probe_origin(origin: str, opener=None) -> dict: origin = origin.rstrip("/") fetch = opener or _default_opener() result = {"origin": origin, "healthz": None, "readyz": None, "auth_start": None} def get(path): req = Request(origin + path, method="GET") try: with fetch(req, timeout=10) as response: return _probe_row(response, getattr(response, "status", None) or response.getcode()) except HTTPError as exc: return _probe_row(exc, exc.code) except (URLError, TimeoutError, OSError) as exc: return {"http": None, "error": type(exc).__name__} result["healthz"] = get("/healthz") result["readyz"] = get("/readyz") result["auth_start"] = get("/auth/start") return result def _probe_row(response, status): headers = getattr(response, "headers", None) location = headers.get("Location") if headers else None raw = response.read(4096) try: body = json.loads(raw.decode("utf-8")) if raw else None except (UnicodeDecodeError, json.JSONDecodeError): body = None row = {"http": status} if location: row["location_host"] = location.split("/")[2] if "://" in location else location if isinstance(body, dict): row["status"] = body.get("status") if "reason" in body: row["reason"] = body.get("reason") return row def evaluate(bundle: dict, origin_probe: dict | None = None) -> dict: sitting = bundle["sitting"] memos = bundle["memos"] missing_act = [row["memo_id"] for row in memos if not row["approval_id"]] pending_principal = [row["memo_id"] for row in memos if row["principal"] == "pending-human-session"] gates = [] if sitting.get("status") != "draft-unsigned" or sitting.get("submitted") is True: gates.append("sitting_already_submitted") if len(memos) != 8: gates.append("unexpected_memo_count") if missing_act: gates.append("missing_act_binding") if pending_principal: gates.append("principal_not_live_subject") live_accept = None if origin_probe is not None: health = (origin_probe.get("healthz") or {}).get("http") ready = (origin_probe.get("readyz") or {}).get("http") start = (origin_probe.get("auth_start") or {}).get("http") if health != 200: gates.append("origin_healthz_not_ok") if start not in (302, 303): gates.append("auth_start_not_redirect") if ready != 200: gates.append("live_accept_closed") live_accept = "closed" else: live_accept = "open" ready_to_sit = not gates return { "kind": "informed-decision-sitting-preflight", "sitting_id": sitting.get("id"), "memo_count": len(memos), "missing_act_binding": missing_act, "principal_not_live_subject": pending_principal, "live_accept": live_accept, "gates": gates, "ready_to_sit": ready_to_sit, "agent_disposition": "forbidden", "origin_probe": origin_probe, } def main() -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--origin", help="Probe this Stage 1 origin; omit for file checks only") parser.add_argument("--receipt", type=Path, help="Write the JSON report") args = parser.parse_args() probe = probe_origin(args.origin) if args.origin else None report = evaluate(load_sitting(), probe) text = json.dumps(report, indent=2, ensure_ascii=False) + "\n" if args.receipt: args.receipt.write_text(text, encoding="utf-8") print(text, end="") return 0 if report["ready_to_sit"] else 2 if __name__ == "__main__": raise SystemExit(main())