79 lines
3.3 KiB
Python
79 lines
3.3 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
|
|
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()
|