From 0f032696c3ed9326ce038db3206d4a40e1445ca0 Mon Sep 17 00:00:00 2001 From: codex Date: Sun, 11 Oct 2026 20:47:18 +0200 Subject: [PATCH] Record round-2 founder decisions; add deterministic flow, requalify and decision-sync scripts Co-Authored-By: Claude Opus 5.5 --- .../2026-10-11-security-unblock-decisions.md | 36 +++++++ scripts/security_flow_report.py | 97 +++++++++++++++++++ scripts/sync_resolved_decisions.py | 58 +++++++++++ 3 files changed, 191 insertions(+) create mode 100644 scripts/security_flow_report.py create mode 100644 scripts/sync_resolved_decisions.py diff --git a/docs/decisions/2026-10-11-security-unblock-decisions.md b/docs/decisions/2026-10-11-security-unblock-decisions.md index 007655fe..36bfd736 100644 --- a/docs/decisions/2026-10-11-security-unblock-decisions.md +++ b/docs/decisions/2026-10-11-security-unblock-decisions.md @@ -51,3 +51,39 @@ The founder authorized demoting off-critical-path wait tasks in the security repos to `flavor: residual`, or cancelling them. Changes are listed in the assessment history file. + +## Round 2 + +### Standing build-mode change authorization — **adopted** (to be encoded in policy-nexus) + +**Pre-authorized:** any non-destructive live change on Railiance that has (1) a reviewed diff or dry-run, +(2) a declared rollback, (3) a named abort owner, and (4) emits a receipt. An agent or the attended +window may execute it without a separate founder go. Conditional approvals execute once their listed +evidence exists. + +**Still founder-gated:** destructive deletion of data or hosts; changes to access or secret policy +*scope* (who can read or do what); new spend ceilings; public publication. + +Resolves: `877c268b` (ops-warden fetch pin rollout, including retroactive acceptance of the install +that already ran), `0d6792b6` (INFD-WP-0005-T05 requester custody, executes under the rule), +`e071b34e` (FLEX-WP-0020-T11 old checkout cleanup: conditional on the listed soak evidence; local +checkout removal is not destructive to data held elsewhere), and `ab85ae8a` (RPF-WP-0035-T08 factory +audit acceptance: conditional on the archive readback and the revocation receipt). + +**Re-arm at production close-down:** narrow the rule to the production change process. + +### D10 remainder — Identity record and MFA (`cef97ccd`) — **accepted in principle** + +The MFA-required journeys specified in USER-WP-0027/0028 are accepted. The founder's real-factor +live acceptance (U05–U08, P04–P06) is an attended-window item. Its findings are normal defects, +not decisions. + +### D4 + D5 — Destructive cleanup (`5d8dcdb5`) and production risk (`2fd5849c`) — **accept + conditional** + +- The single-node / shared-control-plane risk is accepted as a build-mode risk. **Re-arm before customers.** +- The single-node outage exercise and the native clock verification are authorized. +- The CoulombCore / railiance-master destructive cleanups (NK-WP-0022-T08, RMASTER-WP-0020-T08, + RAIL-HO-WP-0011-T08) are approved as a batch. **They execute only after one passing railiance01 + restore/restart drill receipt and expired retention windows.** +- The State Hub primary stop and the core-hub cutover follow the standing change rule. +- Hall of Helix publication is not covered. It stays with consent decision `8a095daa`. diff --git a/scripts/security_flow_report.py b/scripts/security_flow_report.py new file mode 100644 index 00000000..f29c03de --- /dev/null +++ b/scripts/security_flow_report.py @@ -0,0 +1,97 @@ +#!/usr/bin/env python3 +"""Deterministic flow report for the NetKingdom security estate. + +Replaces agent sessions that poll wait tasks. Reads workplan files only (no hub, +no network) and prints four lists: + + READY todo/progress tasks in non-residual workplans: what agents should work on + UNBLOCKED wait tasks whose every depends_on target is done/cancel: requalify to todo + ATTENDED needs_human wait tasks: the attended-window checklist for the operator/founder + STALLED wait tasks qualified only by blocked_on message-from (owner reply), oldest first by file + +Usage: security_flow_report.py [--repos a,b,c] [--json] +""" +import argparse, glob, json, os, re + +DEFAULT_REPOS = ("net-kingdom railiance-platform access-engine secrets-engine ops-mason " + "ops-warden key-cape user-engine tenant-engine gate-house kings-guard " + "policy-nexus rapp-policy-nexus").split() +TERMINAL = {"done", "cancel"} + + +def field(block, key): + m = re.search(rf"^{key}:\s*(.*)$", block, re.M) + return m.group(1).strip().strip('"') if m else "" + + +def load(root, repos): + tasks, wps = {}, {} + for r in repos: + for f in glob.glob(os.path.join(root, r, "workplans", "**", "*.md"), recursive=True): + text = open(f, encoding="utf-8", errors="ignore").read() + fm = text.split("---")[1] if text.startswith("---") else "" + wp = {"repo": r, "file": os.path.relpath(f, root), + "status": field(fm, "status"), "flavor": field(fm, "flavor"), + "title": field(fm, "title")} + for b in re.findall(r"```task\n(.*?)```", text, re.S): + tid = field(b, "id") + if not tid: + continue + deps = re.findall(r"[A-Z][A-Z0-9-]+-T\d+|[A-Z][A-Z0-9-]+-WP-\d+", field(b, "depends_on")) + tasks[tid] = {"id": tid, "status": field(b, "status"), "deps": deps, + "human": field(b, "needs_human") == "true", + "blocked_on": field(b, "blocked_on"), + "reason": field(b, "blocking_reason"), + "priority": field(b, "priority"), "wp": wp} + return tasks + + +def classify(tasks): + def dep_done(d): + if d in tasks: + return tasks[d]["status"] in TERMINAL + # workplan-level dependency: every known task of that workplan is terminal + own = [t for t in tasks.values() if t["id"].startswith(d + "-T")] + return bool(own) and all(t["status"] in TERMINAL for t in own) + + out = {"READY": [], "UNBLOCKED": [], "ATTENDED": [], "STALLED": []} + for t in tasks.values(): + wp = t["wp"] + if wp["status"] in ("finished", "archived") or wp["flavor"] == "residual": + continue + if t["status"] in ("todo", "progress"): + out["READY"].append(t) + elif t["status"] == "wait": + if t["human"]: + out["ATTENDED"].append(t) + elif t["deps"] and all(dep_done(d) for d in t["deps"]): + out["UNBLOCKED"].append(t) + elif not t["deps"] and t["blocked_on"].startswith("message-from"): + out["STALLED"].append(t) + prio = {"high": 0, "medium": 1, "low": 2} + for k in out: + out[k].sort(key=lambda t: (prio.get(t["priority"], 3), t["id"])) + return out + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--root", default=os.path.expanduser("~")) + ap.add_argument("--repos", default=",".join(DEFAULT_REPOS)) + ap.add_argument("--json", action="store_true") + a = ap.parse_args() + res = classify(load(a.root, a.repos.split(","))) + if a.json: + print(json.dumps({k: [{x: t[x] for x in ("id", "status", "priority", "reason", "blocked_on")} + | {"repo": t["wp"]["repo"]} for t in v] for k, v in res.items()}, indent=1)) + return + for k, v in res.items(): + print(f"\n## {k} ({len(v)})") + for t in v: + extra = t["blocked_on"] if k == "STALLED" else t["reason"] + print(f"- {t['id']} [{t['status']}/{t['priority'] or '-'}] {t['wp']['repo']}" + + (f" — {extra[:140]}" if extra else "")) + + +if __name__ == "__main__": + main() diff --git a/scripts/sync_resolved_decisions.py b/scripts/sync_resolved_decisions.py new file mode 100644 index 00000000..5202b8ce --- /dev/null +++ b/scripts/sync_resolved_decisions.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 +"""Resolve open hub decisions that a repo decision log already records as resolved. + +Repo files are authoritative (ADR-001); the hub is a read model. This scans +~/*/decisions/*.md for fenced yaml blocks with `id`, `title`, `status: resolved` +and resolves the open hub decision whose title matches exactly (whitespace- +normalized, case-insensitive). Uses the sanctioned POST /decisions/{id}/resolve. + +Usage: sync_resolved_decisions.py [--apply] [--hub http://127.0.0.1:8000] +""" +import argparse, glob, json, os, re, urllib.request + + +def norm(s): + return re.sub(r"\s+", " ", s.strip().strip("'\"")).lower() + + +def repo_resolved(root): + out = {} + for f in glob.glob(os.path.join(root, "*", "decisions", "*.md")): + text = open(f, encoding="utf-8", errors="ignore").read() + for b in re.findall(r"```yaml\n(.*?)```", text, re.S): + g = lambda k: (re.search(rf"^{k}:\s*(.*)$", b, re.M) or [None, ""])[1] + if g("status").strip() in ("resolved", "accepted") and g("title"): + out[norm(g("title"))] = (g("id").strip(), os.path.relpath(f, root)) + return out + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--hub", default="http://127.0.0.1:8000") + ap.add_argument("--root", default=os.path.expanduser("~")) + ap.add_argument("--apply", action="store_true") + a = ap.parse_args() + known = repo_resolved(a.root) + data = json.load(urllib.request.urlopen(f"{a.hub}/decisions/?status=open&limit=500")) + data = data.get("items", data) if isinstance(data, dict) else data + hits = 0 + for d in data: + t = norm(d["title"]) + m = known.get(t) or next((v for k, v in known.items() + if min(len(k), len(t)) >= 40 and (k.startswith(t) or t.startswith(k))), None) + if not m: + continue + hits += 1 + rid, path = m + print(f"{d['id'][:8]} {rid} {path} | {d['title'][:70]}") + if a.apply: + body = json.dumps({"rationale": f"Resolved in repo decision log {path} ({rid}); hub synced to repo state.", + "decided_by": "bernd"}).encode() + req = urllib.request.Request(f"{a.hub}/decisions/{d['id']}/resolve", data=body, + headers={"Content-Type": "application/json"}, method="POST") + urllib.request.urlopen(req).read() + print(f"{hits} open hub decisions match resolved repo records" + ("" if a.apply else " (dry run)")) + + +if __name__ == "__main__": + main()