From 36b707f0c3b5d93b9f0a07e69c4a7ed0d9878e0b Mon Sep 17 00:00:00 2001 From: tegwick Date: Thu, 20 Aug 2026 08:49:23 +0200 Subject: [PATCH] RISK-WP-0004: five of six tasks done; the executor is the operator's call MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit T02 inbox check, wired into make check and verified against the actual 2026-08-19 failure — replayed at that moment it surfaces all three messages that were already waiting. T03 sweeps the rest of the quietly-tolerated class: bad dates, cadence off the ladder, undefined disclosure states, dangling constraint_on and related refs, embargoes without conditions, escalations without triggers. T04 requests verification of user-engine's tenant boundary — the first walk down the on-request path, chosen as a consumer not already known to fail it. T05 established by trying what this register can verify: cluster yes, OpenBao 403. T06 puts regulatory records on the findings ladder. T01 stays in progress: the procedure, make due and make checked exist, but arming something that runs them on schedule is a standing compute commitment and the operator's to make. Co-Authored-By: Claude Opus 5 --- Makefile | 11 ++- docs/method/check-procedure.md | 87 ++++++++++++++++++ docs/regulatory/audit-retention-basis.md | 5 +- ...2026-08-20-user-engine-boundary-request.md | 58 ++++++++++++ tools/__pycache__/inbox_check.cpython-312.pyc | Bin 0 -> 4154 bytes .../__pycache__/register_lib.cpython-312.pyc | Bin 6271 -> 7081 bytes tools/inbox_check.py | 72 +++++++++++++++ tools/register_check.py | 39 ++++++++ tools/register_lib.py | 9 ++ workplans/RISK-WP-0004-run-the-register.md | 24 +++-- 10 files changed, 296 insertions(+), 9 deletions(-) create mode 100644 docs/method/check-procedure.md create mode 100644 docs/verifications/2026-08-20-user-engine-boundary-request.md create mode 100644 tools/__pycache__/inbox_check.cpython-312.pyc create mode 100644 tools/inbox_check.py diff --git a/Makefile b/Makefile index 4a00324..c0edfcc 100644 --- a/Makefile +++ b/Makefile @@ -1,11 +1,12 @@ PY := python3 TOOLS := tools -.PHONY: help register check checked +.PHONY: help register check checked due help: @echo "make register - rebuild REGISTER.md from findings/" - @echo "make check - verify the index is current, then report what is going quiet" + @echo "make check - verify the index, report what is going quiet, and read the inbox" + @echo "make due - just the work list: what needs a check right now" @echo "make checked - record a check outcome: make checked ARGS=\"RISK-F-0002 clean\"" register: @@ -15,7 +16,13 @@ check: @$(PY) $(TOOLS)/register_index.py --check @echo @$(PY) $(TOOLS)/register_check.py + @echo + @$(PY) $(TOOLS)/inbox_check.py checked: @$(PY) $(TOOLS)/record_check.py $(ARGS) @$(PY) $(TOOLS)/register_index.py + +due: + @$(PY) $(TOOLS)/register_check.py | sed -n '/Checks due/,/^$$/p' + @$(PY) $(TOOLS)/inbox_check.py diff --git a/docs/method/check-procedure.md b/docs/method/check-procedure.md new file mode 100644 index 0000000..4b6e620 --- /dev/null +++ b/docs/method/check-procedure.md @@ -0,0 +1,87 @@ +--- +id: RISK-METHOD-CHECK +type: method +title: "How a check is actually performed" +status: adopted +owner: risk-nexus +adopted: "2026-08-20" +workplan: RISK-WP-0004-T01 +review_interval: 180d +--- + +# Performing a check + +The ladder in `docs/method/review.md` says *when*. This says *what*, in order, +so that a check is repeatable by whoever or whatever is doing it. + +A check takes minutes. It is not an investigation, and it is not a re-grading +unless something moved. + +## The order + +``` +make check # what is due, what is malformed, what the inbox has said +``` + +Then, per due finding: + +0. **Read the inbox first.** `make check` ends with the inbox report; anything + it lists is read before that finding is touched. This is step zero because + skipping it on 2026-08-19 produced a wrong grade and a nearly-sent + escalation. +1. **Is the grade still right?** Re-read impact and likelihood against what has + changed. New facts move grades in both directions. +2. **Is every stated blocker still true?** A blocker is a claim about the world + at a date. Re-check it; do not carry it forward. +3. **Has the fix moved?** Read the owner's tracking record, not our memory of + it. Confirm the record still exists and still refers to this defect. +4. **Is the disclosure state still right?** A met embargo condition publishes; + a condition that has not moved in two checks is a stall — and a condition can + be met while the embargo still holds, if a sibling finding would be exposed + by publishing (`RISK-F-0003`). + +Then record it, in the same sitting: + +``` +make checked ARGS="RISK-F-0002 clean" +make checked ARGS="RISK-F-0002 moved 'ops-warden enabled the gate'" +make checked ARGS="RISK-F-0002 defer 2026-09-01 'operator: after the migration'" +make register +``` + +`record_check.py` writes the front-matter and the dated line together. **A +check that is not written down did not happen** — the same rule this register +applies to every repo it grades. + +## What must not happen + +**Stamping `clean` without doing the four questions.** It costs nothing, takes +no time, and produces a `1q` rung that is a lie about stability. That is +precisely the `RISK-F-0002` failure — a control that produces a false record is +worse than no control — applied to this register's own instruments. + +`clean_streak` exists partly to make that visible: a register where everything +climbs smoothly and nothing ever resets is either extraordinarily stable or not +being checked, and those two look identical from the outside. + +## Who performs it + +Unresolved, and the honest state of `RISK-WP-0004-T01`. + +The procedure is executable and the tooling is built. What does not yet exist +is the thing that runs it on schedule. The options, worst to best: + +- **A human habit.** Fails exactly when the operator is busy, which is the + condition the ladder was designed for. +- **A cron that only reports.** Better: the due list arrives whether or not + anyone remembers. Still needs someone to act on it. +- **A scheduled agent session** that runs `make check`, works the due list, + performs the four questions, and records outcomes — escalating to the + operator only what the escalation rule says to escalate. + +The third is the only one that survives the operator being busy. It is also a +standing commitment of compute, and this register does not get to arm one on +the operator's behalf. **Awaiting that decision.** + +Until it is made, checks happen when someone runs `make check`, and every +finding sitting at `instant` is telling the truth about how often that is. diff --git a/docs/regulatory/audit-retention-basis.md b/docs/regulatory/audit-retention-basis.md index 0211aad..d02e302 100644 --- a/docs/regulatory/audit-retention-basis.md +++ b/docs/regulatory/audit-retention-basis.md @@ -8,7 +8,10 @@ determined: "2026-08-20" finding: RISK-F-0008 sources_read: "GDPR Arts 5, 6, 17, 21, 32; Recitals 49, 65; HGB §257; AO §147" external_review: none -review_by: "2026-11-17" +last_checked: "2026-08-20T05:25:00Z" +next_check: "2026-08-20T05:25:00Z" +cadence: instant +clean_streak: 0 --- # RISK-REG-0001 — the retention basis, written down diff --git a/docs/verifications/2026-08-20-user-engine-boundary-request.md b/docs/verifications/2026-08-20-user-engine-boundary-request.md new file mode 100644 index 0000000..512d892 --- /dev/null +++ b/docs/verifications/2026-08-20-user-engine-boundary-request.md @@ -0,0 +1,58 @@ +--- +id: RISK-V-0002 +type: verification +title: "Tenant boundary of user-engine — verification requested" +date: "2026-08-20" +requested_by: risk-nexus +requested_of: user-engine +status: requested +findings: [RISK-F-0007] +workplan: RISK-WP-0004-T04 +--- + +# RISK-V-0002 — user-engine tenant boundary (requested) + +The first exercise of the on-request path the operator ruled on 2026-08-19. +`RISK-F-0007` is `accepted` until production on the strength of that path, and +a route nobody has walked is a plan rather than a route +(`RISK-WP-0004-T04`). + +## The request + +One named boundary: **can a caller acting for tenant A obtain, modify or infer +data belonging to tenant B through `user-engine`?** + +Asked of `user-engine` because they are a consumer with a boundary and are not +already carrying a finding about one — `tenant-engine` (`RISK-F-0004`) and +`audit-core` (`RISK-F-0005`) both are, and using them would have tested the +path against systems already known to fail it. + +## What was asked for, and what deliberately was not + +Asked: whether anything **verifies** the boundary — a test, an assertion, an +authorization check on the path — and what would have to be true for the answer +to be yes. + +Not asked: a general security review, a posture ladder self-assessment, or a +promise. `user-engine` owns the verification; this register scopes and records +it and verifies nothing itself here. + +## What this exercise is really measuring + +Whether the path works, and where it rubs. Specifically: does the request reach +someone who can act on it, is "one named boundary" a question a repo can +actually answer, and does what comes back constitute evidence or reassurance. + +The value is in the friction it exposes, and that gets recorded here whatever +the answer turns out to be — including if the answer never comes. + +## Outcome + +Pending. Requested 2026-08-20. + +- If the boundary holds with evidence: `RISK-F-0007`'s likelihood falls **for + this consumer**, and the record says how it was shown. +- If it does not: a finding of its own, with `user-engine` as fix owner. +- If nothing comes back: that is the most useful result of the three, because + the estate is currently carrying `RISK-F-0007` on the assumption that asking + works. diff --git a/tools/__pycache__/inbox_check.cpython-312.pyc b/tools/__pycache__/inbox_check.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5829c8368d92cf3645e5ed1afe297f5b403b0d6b GIT binary patch literal 4154 zcmb7HZ){uD6~E8#`M;gmv6GZ6g4?BGHErTJP)bApkT!;tmbOU~5TU7g@w$kWTZE=5Y@J9{B&S1&G9gjKq-ENq6>So9+6Sh6n2WPDeN@vl_|i|yS|+sa%g%lF zv)!#_>?OYUymQZ=d(S!d-rxJZ*Xu&?{O!lzOD2H&jJ=qTP$9TmDIkhSLK8^hBuk1* zTP7?Vqxlp+!5h6WVKsVNwYN_=Bw@lSIV9_Mg$bABlx)zuiAS=Z+KLeMS`cAB_xn|E z?{gL66>g3Xxx^drIxMPKOAst)rj#>SomEbe3{ESQU`pa7qow9xSj-qQl+4JgMkr2z zQc3|)X`jnAdhqz6p68DB^!4>U(lgfA-;0lCaDQL_qdk3(^+a}ormTXos_G0CB_d%% z=}D#&!7s+CtjTdP^`a}6kW(-hO~upl8JtmoO|U9zIGdqFlwg+i$@bzgm^US=EM>(M zNXgOwmZw1*t0{R_b%`^goY{`k;wfeli#VPj@l(C{U`1dSrlDlwWII+BoFK4bm?sNG zMa<=ThQwjzY+@jnnllvRN_tiVpIDRAMAgLftct~q1Wq(iQ$f%$q0T{^(l{fg$>Yqo zq!85w%Yo^_Dwq?{R>c`&D6>(vSS*paEHU031T8aTc%)=jp+-P567W=HgvM+bkHr1N@_too2bxEk(;P_}d&zvO^aW&AcHAPt9+Hr$%c0|_3q_F1 zNvUC@z+#oXFgvGPrwLRzWy@-ZpPGbb0c}_WYw)@^@Y=~&PhRzQUGa9kb70k9 z@^+QI4{spOiC0fw540C|E$>u9ynhz_D$CO>vK=bYlmMweATo0if8jXyMGb9 z8i`(sM1PwrxsQT`HF&K#u)#xrTkQdNuXGQ#pm$pbZTzpDKwY+32b=Ae-8`e4jkaay zeh>c}HwWzr&>DTO4?(EhAw`@7PO+fRO zd2R;H^99QgTH+?f|L}5`a=(Wcn`zzb=LN0VoWnhI?2^OWfTmW`Z|c`0S|b z!%nZC7T;*($Kj%h!|uzWQ0I|sFHG_v!&U=${K#l@@WAlk{=-rH*M--OyE>7b!jdc* zx4)QCbAQ>Xn;Wnd&J(Ki<_92?)0LbUs2S2(L2f4g%Mi#pq=gLt$}=KUxpYHrth>En(%XCs`K)b=+;xAj*sWf_QMasU17p>QtThBkc?g_5@TW=zlvu$zkdZ4X1uspCLzD3TH3x~`1N6LZz z#i46~_LWC2Jo=-(>%L9vZS6&}OjZtFm@2o0O05rF+dZ(6`!lskziQ{ILa?#q4r+n8xAwVAg z(*C8>OYxp3cHOHZKx96WXcOh4lQ`xn9*%}(| z6E24ZXmv*{#_A{*3t4mUSWHsl zv6$|P#ip~MLt-(?PGia{RrgeIbT47ZnA#busmd~uwa5HMiovd;T}+jIRu~)3sA)Da z_Vvb`95)WW=owji58VPOWNd)jj@@ZR%BIL(x*vE9QmRiu1F_(^zY9)I_(u?NU7sQM zrzr3#YW)m#{KXbs;BVQiT=15!oAYiQ64|3)27q literal 0 HcmV?d00001 diff --git a/tools/__pycache__/register_lib.cpython-312.pyc b/tools/__pycache__/register_lib.cpython-312.pyc index fb0cee584bde9171958315cac33e1bcf4370bddb..e77fdba71295004375d674252990dec0e989ec63 100644 GIT binary patch delta 1315 zcmZuwUrbw77(d^=x23eV9WAtWfbPO3qz*fZ0~WJjFfboLM2s2)VlF)wxLyCubFTu| zk~W4cfGlpg%SwD=;bB|w-@+0feDT2t6Js)_57?OO#Rn4;Gt)h6Px_s7{e6GV zcfa5Ho%8kI&KteHpS)f-!4=p#pIGx<@`d5SrS>18C+Auvq zkD@(HJLxgBjWkA|N83cZXdBvQ+GYPOJQt?j^f-nsi(*^=5*L?XiOiEo&rY+rQG4V( zk`;)_Im?TJgib{T9jxeZWP_@pjo_1UZ#=|Um^rbW9SSKKxJ7nbp(|_77~@y{0}KP+e?T6>-DC% zzmbJ~L06yvK-OC%vZ%n9;ygqIt9ER#f8h1OXo`+R1lElou{RNJ*q6Pnxo01pD|$Ma ziK|JYFM4`bOQTZO_cqoUco_00d5|Y}0>c|0e4~Clb93f<&o4bgmrrg$p?5bh{LpY= z%_j}*P^fDMOL_sho={oORkmRrSD2{YK@Q`CMS%*By?{8vIDe5upeW92CK_M{|4?CJ zMD{L%)%;WeNi{W1)xLMI!l*;YlTEnhxa!!X*I(T_lqakalM!IKac-reG&VT;M$xG) z=!R*q2~4u7azzp7pfc|zXpbX3!S(xf*iX?z!9^*b2r(y$wq`c^od zF*V~UFEeT1_FjZZTlKX&SQ1O^i@u{UzH`g>mmsk@j5qx;Fzq}36izlF@9zu*CZKBp zUABl&6w{e`_Aw8$9A4qTFQsy|{6EGxB<$V#J;xe)Y5S{&@m!v7e2P#M^|Tq0D)V%Z zwPb2WO+BSiEorJxY%^YnL&zIce2ERAZ^`FWb1tb*M@P|C`b$)go7Gy&oy3Qkpn6T* zoZ>nALa;HyyI3A1q^hZLQsi6R?_A-yfMKN7z8!3Ga@B0X{wLVwXJ@%T$$@cLvAaWw zVEJtwJmwcxR?pV#uS0F_&oSn6XB2kshAcSlgjgvcf(9ko1$CtmK*xO-zz{qvy(SQU O)No?)4=3fp9_N2A(LQki delta 560 zcmZ2!{@;M_G%qg~0}woz+?M6TJCRR<@xw&*6O7s$Kb&PW)JainVTlq?(M?eT(h@0p zDat@vGDSZ{1xQP!7^J8IY3UTh6g40%lVUje9h00uC37+pBSSKCEn_W1IfJI@W)KwP& zQY$ixONuA26I5ml*nCOwH522l$p=LY88at;7RhC;0=hYDbE;@Gqfs7^S(FbXZZYTO zmlqX+xJ4ks9z;li2ni5T10xbAe-gXF+5qIHPChRlSziW{Xao|sm^1TAG+ByFK>~2m zDxQ=Qy^_q_)Rfeml0*z?utH5HumKP@OOZFwfGVDX#FC7h%p|=4APq5}B_%Vt1gf@* zD+lPo=#)(MOi3(B1z8HTuO7uYMa@92rsCv}64GoSUl-*~ z7Lqg)y2TRY8sJ|P0}@FC5p|P8C9?!rK}=9!LA>8E`HG|(*F+$n5r~T?ZDx_GVpL;h oOlADc!@(% list[dict] | None: + try: + with urllib.request.urlopen(HUB, timeout=6) as r: + return json.load(r) + except (urllib.error.URLError, TimeoutError, json.JSONDecodeError, OSError): + return None + + +def main() -> int: + msgs = fetch() + if msgs is None: + # Fail loud: an unreachable inbox means no check can be certified current. + print("Inbox: UNREACHABLE — the hub did not answer.") + print(" No check can be certified current while this is true.") + print(" Findings may have moved without the register hearing about it.") + return 0 + + fs = [f for f in lib.findings() if lib.watched(f.get("status"))] + rows = [] + for f in fs: + looked = lib.moment(f.get("last_checked")) + system = str(f.get("system", "")).lower() + owner = str(f.get("fix_owner", "")).lower() + if not looked or not system: + continue + for m in msgs: + said = lib.moment(m.get("created_at")) + if not said or said <= looked: + continue + blob = f"{m.get('subject','')} {m.get('body','')} {m.get('from_agent','')}".lower() + if system in blob or (owner and owner in blob) or f["id"].lower() in blob: + rows.append( + f"{f['id']} — last looked {looked:%Y-%m-%d %H:%MZ}; " + f"{m.get('from_agent')} wrote {said:%Y-%m-%d %H:%MZ}: {m.get('subject','')[:70]}" + ) + break + + print(f"Inbox: {len(msgs)} message(s) addressed to risk-nexus\n") + if rows: + print("Findings whose inbox has spoken since the register last looked:") + for r in rows: + print(f" {r}") + print("\n Read those before grading or checking. This is question zero.") + else: + print("No finding has an inbox message newer than its last check.") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/register_check.py b/tools/register_check.py index 202db66..0a62ab3 100644 --- a/tools/register_check.py +++ b/tools/register_check.py @@ -44,6 +44,36 @@ def main() -> int: if unknown: section("Unrecognised status", unknown, "none") + # T03 — the `mitigated` defect was one instance of a class: the tooling + # quietly tolerating something it did not expect. Everything below is + # reported by name, and never silently ignored. + ids = {f["id"] for f in lib.findings()} + malformed: list[str] = [] + for f in fs: + fid = f["id"] + for field in ("last_checked", "next_check", "embargo_since", "deferred_to"): + if f.get(field) and lib.moment(f[field]) is None: + malformed.append(f"{fid} — {field} is not a date: {f[field]!r}") + if (c := f.get("cadence")) and c not in lib.CADENCE_NAMES: + malformed.append(f"{fid} — cadence '{c}' is not a rung on the ladder") + if (d := f.get("disclosure")) and d not in ("public", "embargoed", "restricted", "unset"): + malformed.append(f"{fid} — disclosure '{d}' is not a defined state") + if (s := f.get("severity")) and s not in lib.SEVERITIES + ["unset"]: + malformed.append(f"{fid} — severity '{s}' is not on the scale") + if (on := f.get("constraint_on")) and on not in ids: + malformed.append(f"{fid} — constraint_on points at {on}, which does not exist") + for ref in (f.get("related") or []): + if ref not in ids: + malformed.append(f"{fid} — related names {ref}, which does not exist") + if f.get("embargo_condition") and f.get("disclosure") != "embargoed": + malformed.append(f"{fid} — carries an embargo_condition but disclosure is '{f.get('disclosure')}'") + if f.get("disclosure") == "embargoed" and not f.get("embargo_condition"): + malformed.append(f"{fid} — embargoed with no condition; a hold with no lift is a silence") + if f.get("escalation") == "required" and not f.get("escalation_trigger"): + malformed.append(f"{fid} — escalated with no trigger named") + if malformed: + section("Malformed", malformed, "none") + ungraded = [ f"{f['id']} — {', '.join(k for k in lib.GRADED_FIELDS if str(f.get(k, 'unset')) == 'unset')}" for f in fs @@ -112,6 +142,15 @@ def main() -> int: ] section("Owed at the production transition", rescore, "none — no finding is graded lower for build mode") + reg = [] + for r in lib.regulatory(): + when = lib.moment(r.get("next_check")) or lib.moment(r.get("review_by")) + if when is None: + reg.append(f"{r.get('id')} — no next_check set") + elif NOW >= when: + reg.append(f"{r.get('id')} — due {when:%Y-%m-%d}: {str(r.get('title','')).strip()}") + section("Regulatory records due", reg, "none") + print(f"Register check — {NOW:%Y-%m-%d %H:%MZ}\n{len(fs)} live finding(s)\n") print("\n".join(lines).rstrip()) return 0 diff --git a/tools/register_lib.py b/tools/register_lib.py index 7c435d3..badcc88 100644 --- a/tools/register_lib.py +++ b/tools/register_lib.py @@ -77,6 +77,15 @@ def findings() -> list[dict]: return sorted(items, key=lambda f: f["id"], reverse=True) +def regulatory() -> list[dict]: + """Regulatory records expire, so they ride the same ladder as findings.""" + d = REPO / "docs" / "regulatory" + if not d.exists(): + return [] + return sorted((load(p) for p in d.glob("*.md") if p.name != "README.md"), + key=lambda r: r.get("id", "")) + + def notes() -> list[dict]: if not NOTES.exists(): return [] diff --git a/workplans/RISK-WP-0004-run-the-register.md b/workplans/RISK-WP-0004-run-the-register.md index e1c6361..b4db073 100644 --- a/workplans/RISK-WP-0004-run-the-register.md +++ b/workplans/RISK-WP-0004-run-the-register.md @@ -48,7 +48,7 @@ Every task below traces to something that happened, not something imagined. ```task id: RISK-WP-0004-T01 -status: todo +status: progress priority: high ``` @@ -67,11 +67,13 @@ stamps `clean` without doing that is worse than no job, because it manufactures a stability signal that is false. That is the `RISK-F-0002` failure mode applied to this repo's own instruments. +In progress 2026-08-20. The procedure exists (`docs/method/check-procedure.md`), `make due` prints the work list, and `make checked` records an outcome. **What does not exist is the thing that runs it on schedule**, and this register does not get to arm a standing compute commitment on the operator's behalf. The doc names the three options and says which survives the operator being busy. Stays open until that decision is made; until then, every finding sitting at `instant` is telling the truth about how often `make check` is run. + ### T02 — Inbox before grading, as a check rather than a habit ```task id: RISK-WP-0004-T02 -status: todo +status: done priority: high ``` @@ -84,11 +86,13 @@ message this repo has received about that system, and report any finding whose inbox has spoken more recently than its register has. That is mechanical and does not require the tooling to understand the message. +Completed 2026-08-20. `tools/inbox_check.py`, wired into `make check`. Compares each finding's `last_checked` against messages this repo has received about that system, and reports any finding whose inbox has spoken since the register last looked. Mechanical: it matches timestamps and names, reads nothing, decides nothing. Verified against the actual failure — replayed at the moment of the 2026-08-19 grading it surfaces all three flex-auth/rapp-postgres messages that were already waiting. An unreachable hub reports UNREACHABLE rather than silently passing, because a check that cannot see the inbox cannot be certified current. + ### T03 — Fail loud everywhere else too ```task id: RISK-WP-0004-T03 -status: todo +status: done priority: medium ``` @@ -99,11 +103,13 @@ not exist, an `embargo_condition` on a finding that is not embargoed. Every one of them should be reported by name and keep the finding watched. +Completed 2026-08-20. `make check` now reports, by name and without ever silently ignoring: unparseable dates, a cadence that is not a rung, an undefined disclosure state, a severity off the scale, a `constraint_on` or `related` pointing at a finding that does not exist, an embargo condition on a finding that is not embargoed, an embargo with no condition, and an escalation with no trigger. Nothing is malformed today, which is the answer that only means something because the check exists. + ### T04 — Exercise the on-request verification path once ```task id: RISK-WP-0004-T04 -status: todo +status: done priority: medium ``` @@ -115,11 +121,13 @@ path, and see what actually happens: who answers, what evidence comes back, whether the finding's likelihood moves for that consumer, and what the record looks like. The value is in the friction it exposes. +Completed 2026-08-20 as far as this repo can complete it. `RISK-V-0002`: verification of `user-engine`'s tenant boundary requested through the documented path. `user-engine` was chosen because they are a consumer with a boundary and *not* already carrying a finding about one — using `tenant-engine` or `audit-core` would have tested the path against systems already known to fail it. The outcome is pending and all three outcomes are useful, including silence, which would tell the estate that `RISK-F-0007` is being carried on an assumption that asking works. + ### T05 — The verification credential ```task id: RISK-WP-0004-T05 -status: todo +status: done priority: medium ``` @@ -133,11 +141,13 @@ source. Establish what this repo can legitimately verify itself, and what it must always take from owners. **If the answer is "nothing", that is worth knowing and writing down**, because it bounds every grade in the register. +Completed 2026-08-20, by trying rather than reasoning. `docs/method/verification.md`. **Cluster reads work from this host; OpenBao returns 403**, the same wall `ops-warden` hit. So the register can check what the cluster admits and cannot check what the secret store permits, and every grade touching an OpenBao policy is a grade on a document. That asymmetry is recorded rather than closed — a risk register holding production secret-store credentials would have traded a verification problem for a worse one. First use produced `RISK-V-0001`, which confirmed `RISK-F-0001`'s ingress claim against the live cluster, contradicted the `egress: []` claim in the same message, and surfaced a third NetworkPolicy with no ingress rules that bears on whether `ops-warden` enabling its gate would fail closed. + ### T06 — Regulatory records on the same ladder ```task id: RISK-WP-0004-T06 -status: todo +status: done priority: low ``` @@ -145,6 +155,8 @@ Duplicate of `RISK-WP-0003-T04`, kept here as a pointer rather than a second copy. Whichever workplan reaches it first does it; the other closes with a reference. +Completed 2026-08-20. Regulatory records ride the findings ladder rather than getting a second review mechanism: `RISK-REG-0001` carries `cadence`, `clean_streak`, `last_checked` and `next_check`, and `make check` reports regulatory records due exactly as it reports findings. `RISK-WP-0003-T04` closes by reference. + ## Non-goals - **No dashboard.** `REGISTER.md` and `make check` are the surface, and the