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>
This commit is contained in:
codex 2026-10-21 08:40:12 +02:00
parent 4c89148bba
commit 9edd3151ac
7 changed files with 337 additions and 6 deletions

View file

@ -6,12 +6,14 @@ 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
@ -27,15 +29,26 @@ def notifications(report, state, agent, founder):
return pending
def post(hub, recipient, payload):
def post(hub, recipient, payload, subject="Daily security flow"):
body = {"from_agent": "the-custodian", "to_agent": recipient,
"subject": "Daily security flow", "body": json.dumps(payload, sort_keys=True)}
"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")
@ -50,6 +63,8 @@ def main():
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)
@ -72,6 +87,20 @@ def main():
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)

View file

@ -0,0 +1,99 @@
#!/usr/bin/env python3
"""CUST-WP-0071-T05 missed/failed-run check for the weekly allocation assessment.
Flags `failed` when the latest receipt failed, and `missing` when no retained
report exists for the due ISO week. The current week falls due on Monday 12:00
Europe/Berlin; before then the previous week is checked. Week labels follow
weekly_allocation_incluster.week_of (ISO week of UTC time minus 6h).
Receipts source (inactive when unset, so harmless before the CronJob runs):
- a path to a copy of the job's receipts.jsonl (failed and missing), or
- `hub`: the job's delivery messages (from custodian-allocation-review to
the-custodian). Only retained weeks are posted, so a failed run shows as missing.
"""
from __future__ import annotations
import argparse
from datetime import datetime, timedelta, timezone
import json
from pathlib import Path
from urllib.parse import urlencode
from urllib.request import urlopen
from zoneinfo import ZoneInfo
BERLIN = ZoneInfo("Europe/Berlin")
OK = {"retained", "already-retained"}
WINDOW_END_HOUR = 6
def week_label(moment: datetime) -> str:
year, week, _ = (moment.astimezone(timezone.utc) - timedelta(hours=WINDOW_END_HOUR)).isocalendar()
return f"{year}-W{week:02d}"
def due_week(now: datetime) -> str:
local = now.astimezone(BERLIN)
monday = (local - timedelta(days=local.weekday())).replace(hour=12, minute=0, second=0, microsecond=0)
return week_label(monday if local >= monday else monday - timedelta(days=7))
def evaluate(receipts: list[dict], now: datetime) -> dict:
week = due_week(now)
ordered = sorted(receipts, key=lambda r: r.get("run_at") or "")
flags = []
if ordered and ordered[-1].get("status") == "failed":
flags.append({"flag": "failed", "week": ordered[-1].get("week"), "run_at": ordered[-1].get("run_at"),
"error": ordered[-1].get("error")})
if not any(r.get("week") == week and r.get("status") in OK for r in receipts):
flags.append({"flag": "missing", "week": week,
"detail": "no retained weekly allocation report for the due ISO week"})
return {"status": "flagged" if flags else "ok", "due_week": week, "flags": flags,
"recovery": "inspect CronJob custodian-assessment/custodian-allocation-review; rerun with "
"kubectl create job --from=cronjob/custodian-allocation-review (same-week rerun is idempotent)"
if flags else None}
def from_file(path: Path) -> list[dict]:
out = []
for line in path.read_text().splitlines():
try:
out.append(json.loads(line))
except ValueError:
continue
return out
def from_hub(hub: str, opener=urlopen) -> list[dict]:
query = urlencode({"to_agent": "the-custodian", "from_agent": "custodian-allocation-review", "limit": 50})
with opener(hub.rstrip("/") + "/messages/?" + query, timeout=30) as response:
messages = json.load(response)
out = []
for message in messages:
try:
body = json.loads(message.get("body") or "{}")
except ValueError:
continue
if body.get("week"):
out.append({"week": body["week"], "status": "retained", "run_at": message.get("created_at")})
return out
def check(source: str | None, hub: str, now: datetime | None = None, opener=urlopen) -> dict:
if not source:
return {"status": "inactive", "reason": "no weekly allocation receipts source configured"}
receipts = from_hub(hub, opener) if source == "hub" else from_file(Path(source))
return evaluate(receipts, now or datetime.now(timezone.utc))
def main():
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument("--receipts", help="receipts.jsonl path or 'hub'")
parser.add_argument("--hub", default="http://127.0.0.1:8000")
args = parser.parse_args()
result = check(args.receipts, args.hub)
print(json.dumps(result, indent=2))
return 1 if result["status"] == "flagged" else 0
if __name__ == "__main__":
raise SystemExit(main())

View file

@ -15,6 +15,12 @@ the Monday 08:00 Europe/Berlin schedule in both CET and CEST and keeps the
window start as close as possible to the seven-day Prometheus retention edge. A week whose report is already
retained is a no-op success (idempotent retry); a failed attempt leaves no
report, so the Job's retry or the next manual run collects again.
Delivery (founder decision 2026-10-21, "Hub message + repo tasks"): after a
week is retained, one State Hub message goes from custodian-allocation-review
to the-custodian with a short summary, report path and sha256. The message id
is recorded in receipts.jsonl; a week that already has a message id is never
posted again, and a retained-but-undelivered week is posted by the next run.
"""
from __future__ import annotations
@ -170,6 +176,61 @@ def append_receipt(data: Path, receipt: dict) -> None:
print(json.dumps(receipt, sort_keys=True))
def delivered_message(data: Path, week: str) -> str | None:
path = data / "receipts.jsonl"
if not path.exists():
return None
for line in reversed(path.read_text().splitlines()):
try:
entry = json.loads(line)
except ValueError:
continue
if entry.get("week") == week and entry.get("message_id"):
return entry["message_id"]
return None
def summary(receipt: dict) -> dict:
report = json.loads(Path(receipt["report"]).read_text())
decisions: dict[str, int] = {}
for item in report["recommendations"]:
decisions[item["decision"]] = decisions.get(item["decision"], 0) + 1
return {"week": receipt["week"], "report": receipt["report"], "report_sha256": receipt["report_sha256"],
"capture_end": report["period"]["capture_end"], "workloads": len(report["workloads"]),
"namespaces_with_metrics": len(report["namespace_context"]),
"recommendations": dict(sorted(decisions.items())),
"comparison_compatible": report["comparison"]["compatible"],
"measurement_gaps": len(report["measurement_gaps"]),
"failed_queries": sorted(n for n, q in report["queries"].items() if q.get("response_status") != "success"),
"next_step": "Custodian session reviews and records owner-repo tasks (ADR-001); no automatic resizing."}
def hub_post(hub: str, body: dict) -> str:
request = urllib.request.Request(hub.rstrip("/") + "/messages/", data=json.dumps(body).encode(),
headers={"Content-Type": "application/json"}, method="POST")
with urllib.request.urlopen(request, timeout=30) as response:
return str(json.load(response)["id"])
def deliver(data: Path, receipt: dict, hub: str | None, post=hub_post) -> tuple[dict, bool]:
"""Post one hub message per retained week; returns (receipt, ok)."""
if receipt.get("status") not in {"retained", "already-retained"}:
return receipt, True
existing = delivered_message(data, receipt["week"])
if existing:
return {**receipt, "delivery": "already-posted", "message_id": existing}, True
if not hub:
return {**receipt, "delivery": "not configured"}, True
content = summary(receipt)
body = {"from_agent": "custodian-allocation-review", "to_agent": "the-custodian",
"subject": f"Weekly allocation assessment {receipt['week']}",
"body": json.dumps(content, sort_keys=True)}
try:
return {**receipt, "delivery": "posted", "message_id": post(hub, body)}, True
except (OSError, ValueError, KeyError) as exc:
return {**receipt, "delivery": f"failed: {type(exc).__name__}"}, False
def run(args, *, now: datetime, get, query) -> dict:
sources = {
"weekly_allocation_report": args.source_dir / "weekly_allocation_report.py",
@ -242,6 +303,7 @@ def parser() -> argparse.ArgumentParser:
p.add_argument("--inventory", type=Path, default=env("ASSESSMENT_INVENTORY") and Path(env("ASSESSMENT_INVENTORY")))
p.add_argument("--reviews", type=Path, default=env("ASSESSMENT_REVIEWS") and Path(env("ASSESSMENT_REVIEWS")))
p.add_argument("--prometheus-url", default=env("PROMETHEUS_URL", "http://telemetry-prometheus.telemetry.svc:9090"))
p.add_argument("--hub-url", default=env("STATE_HUB_URL"), help="State Hub API; unset disables delivery")
p.add_argument("--executor", default=env("ASSESSMENT_EXECUTOR", "unspecified"))
p.add_argument("--max-age-hours", type=float, default=float(env("ASSESSMENT_MAX_AGE_HOURS", "48")))
return p
@ -264,8 +326,9 @@ def main() -> int:
"week": label, "run_at": iso(now), "status": "failed",
"error": f"{type(exc).__name__}: {exc}"[:500]})
return 1
receipt, delivered = deliver(args.data_dir, receipt, args.hub_url)
append_receipt(args.data_dir, receipt)
return 0
return 0 if delivered else 1 # undelivered: Job retry posts it (report already retained)
if __name__ == "__main__":