#!/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 from pathlib import Path import subprocess import sys from urllib.request import Request, urlopen from security_flow_report import DEFAULT_REPOS 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): body = {"from_agent": "the-custodian", "to_agent": recipient, "subject": "Daily security flow", "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 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("--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. print(raw) if __name__ == "__main__": main()