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>
108 lines
4.8 KiB
Python
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()
|