the-custodian/scripts/security_flow_daily.py
codex 9edd3151ac CUST-WP-0071-T05: hub delivery per weekly report and daily missed/failed-run check
The in-cluster runner posts one State Hub message (custodian-allocation-review
-> the-custodian) per newly retained week, records the message id in
receipts.jsonl, never reposts, and retries an undelivered week. New
weekly_allocation_check.py flags a failed latest receipt or a missing report
for the due ISO week (Monday 12:00 Europe/Berlin); security_flow_daily.py
notifies once per distinct flag set and stays inactive until a receipts
source is configured. Founder decision 2026-10-21 (T05 follow-up answers).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 08:40:12 +02:00

108 lines
4.8 KiB
Python

#!/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()