the-custodian/scripts/security_flow_daily.py

109 lines
4.8 KiB
Python
Raw Normal View History

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