#!/usr/bin/env python3 """Daily decision sync and security report, with per-recipient delivery checkpoints. Dry-run by default. --apply is the scheduled mode. No credentials are stored. """ import argparse import fcntl import json import os from pathlib import Path import subprocess import sys from urllib.request import Request, urlopen from security_flow_report import DEFAULT_REPOS import weekly_allocation_check SCRIPTS = Path(__file__).resolve().parent def notifications(report, state, agent, founder): pending = [] ready = {key: report[key] for key in ("READY", "UNBLOCKED")} if ready != state.get("agent"): pending.append(("agent", agent, ready)) # ATTENDED is the daily operator reminder; empty lists never message the founder. if report["ATTENDED"]: pending.append(("founder", founder, {"ATTENDED": report["ATTENDED"]})) return pending def post(hub, recipient, payload, subject="Daily security flow"): body = {"from_agent": "the-custodian", "to_agent": recipient, "subject": subject, "body": json.dumps(payload, sort_keys=True)} request = Request(hub.rstrip("/") + "/messages/", data=json.dumps(body).encode(), headers={"Content-Type": "application/json"}, method="POST") with urlopen(request, timeout=30) as response: return json.load(response)["id"] def allocation_notice(result, state): """CUST-WP-0071-T05: notify once per distinct flag set; clear when healthy.""" if result.get("status") != "flagged": if result.get("status") == "ok": state.pop("allocation", None) return None if result["flags"] == state.get("allocation"): return None return {"WEEKLY_ALLOCATION": result} def save(path, data): temporary = path.with_suffix(path.suffix + ".tmp") temporary.write_text(json.dumps(data, indent=2) + "\n") temporary.replace(path) def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--root", default=str(Path.home())) parser.add_argument("--repos", default=",".join(dict.fromkeys(["the-custodian", *DEFAULT_REPOS]))) parser.add_argument("--hub", default="http://127.0.0.1:8000") parser.add_argument("--agent", default="the-custodian") parser.add_argument("--founder", default="human") parser.add_argument("--state-dir", type=Path, default=Path.home() / ".local/state/custodian-security-flow") parser.add_argument("--allocation-receipts", default=os.environ.get("CUSTODIAN_ALLOCATION_RECEIPTS"), help="weekly allocation receipts.jsonl path or 'hub'; unset keeps the check inactive") parser.add_argument("--apply", action="store_true") args = parser.parse_args() args.state_dir.mkdir(parents=True, exist_ok=True) with (args.state_dir / "run.lock").open("w") as lock: fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB) command = [sys.executable, str(SCRIPTS / "sync_resolved_decisions.py"), "--root", args.root, "--hub", args.hub] subprocess.run(command + (["--apply"] if args.apply else []), check=True) raw = subprocess.check_output([sys.executable, str(SCRIPTS / "security_flow_report.py"), "--root", args.root, "--repos", args.repos, "--json"], text=True) report = json.loads(raw) state_path = args.state_dir / "delivery.json" state = json.loads(state_path.read_text()) if state_path.exists() else {} save(args.state_dir / "report.json", report) for key, recipient, payload in notifications(report, state, args.agent, args.founder): if not args.apply: print(json.dumps({"dry_run": True, "to_agent": recipient, "report": payload})) continue message_id = post(args.hub, recipient, payload) state[key] = payload state[key + "_message_id"] = message_id save(state_path, state) # A later recipient failure does not replay earlier sends. try: allocation = weekly_allocation_check.check(args.allocation_receipts, args.hub) except (OSError, ValueError) as exc: # never break the security flow allocation = {"status": "error", "error": type(exc).__name__} notice = allocation_notice(allocation, state) if notice and not args.apply: print(json.dumps({"dry_run": True, "to_agent": args.agent, "report": notice})) elif notice: state["allocation_message_id"] = post(args.hub, args.agent, notice, "Weekly allocation assessment check") state["allocation"] = allocation["flags"] if args.apply: save(state_path, state) if allocation["status"] != "inactive": print(json.dumps({"weekly_allocation": allocation}, sort_keys=True)) print(raw) if __name__ == "__main__": main()