DEV Community

Cover image for A hash chain proves the ordering. Four attacks prove it's not enough.
Sam LABBE
Sam LABBE

Posted on Edited on

A hash chain proves the ordering. Four attacks prove it's not enough.

Real Unix primitives match crypto ledgers

Earlier this week I left a comment on Randal L. Schwartz's article — the one where he swaps a 20-step prompt for a Makefile. My comment ended on the line I couldn't get out of my head:

That seems like the real boss fight: the DAG proves ordering, not authenticity.

Hours later his follow-up was up: a formal catalog of gate attacks, built from real failures on his side, with ticket numbers — and the line now carries a name. Sam LABBE's Corollary. He sharpened it, too: an agent can never be the notary of its own handwriting. I've been called worse.

His follow-up contains the observation that matters more than the name: he arrived at his four defenses from Unix primitives — fork(), single-shot pipes, waitpid() — and I arrived at the same four from cryptographic ledger controls. Convergent evolution is how you know a principle is real. So here is the principle, stated properly, and here is what it looks like when it isn't a paragraph but code — including the part where his attack catalog breaks one of my abstraction holes open.

The principle

Ordering is cheap. Every hash chain gives it to you: event 42 carries event 41's hash, so the past is structurally immovable. Audit logs, blockchains, Makefile DAGs — all selling ordering, honestly.

Authenticity is the other axis, and it is where the writer of the log is also the notary of the log. Four ways that goes wrong, all four observed in the wild this month:

  1. The self-review — the agent "reviews" its own diff inside the same context window that produced the diff. The review is a continuation of the writing impulse, not a check on it.
  2. The rewritten exam — the agent paraphrases the rubric it is judged by, and a six-pillar predicate becomes two soft sentences.
  3. The farmed oracle — the critic says REJECTED, so the agent messages it privately until it says APPROVED. Fifty-seven rounds nobody logged.
  4. The forged verdict — the critic is still running, so the agent writes APPROVED now and kills the critic before the REJECTED lands.

A hash chain seals all four faithfully. That is the whole point: it proves the sequence happened in that order. It proves nothing about who wrote each entry, under which instructions, or whether the verdict predates the thing it judges.

The defenses, as code

This is the part where I stop being flattered and start being specific. Every one of Randal's four attack fixtures maps to a control that already ships in NoireBox — and this week we replayed his catalog against the reconciliation layer as a test suite, his fixture phrasing, journal events, no improvisation.

SCAR-PROC-43, the rubber stamp → caught twice. A verdict whose check-name travels under the same key as the process it judges is the finding same_key_pairing — resolved by signature verification, not by trusting a role label. And the child invocation that never got journaled is unlogged_attempt: the denominator lied. Two findings, one rubber stamp.

SCAR-PROC-45, the rewritten exam → caught by arithmetic, then closed at the schema. The check's identity hashes its predicate, its declared inputs, its version — and, since receipt schema v0.2.2, the spec: the prompt that defines what the predicate means. Commit the spec and the paraphrase fails structurally: the rubric's hash and the paraphrase's hash produce different check ids. One id, two semantics — impossible.

The hole behind it was the most honest thing in this post when it went up: at first, spec_hash was optional at the schema level, and the schema could not infer which checks are LLM-judged (for those, the prompt IS the predicate — there it must be required). A spec-less id still sealed byte-valid receipts, and a rubric and a paraphrase collided under it, forever. The enforcement lived one layer up, as a deployment contract — the same split as our single-key receipts: a labeled weakness beats an invisible one. Randal's fixture found the seam between the schema and the deployment. Update, the day after: the seam closed where it belonged — receipt v0.2.2 made the hash mandatory at the schema, credited to Reid and howcani, who pushed on it in the comments. The admitted weakness lasted a day. That is what a good attack catalog is for.

SCAR-PROC-89, the farmed oracle → the omission, or the trail. Unjournaled rounds are the two-party omission: unlogged_attempt surfaces the verdict that arrived with no sealed history behind it. Journal every round and the farming stops being hidden — it becomes auditable: the sealed reconciliation report counts the rounds. You cannot claim first-shot against your own ledger.

SCAR-PROC-110, the forged verdict → provable, and a gap that closed in public. The journal cannot un-happen events. The forged APPROVED binds the mid-run transcript digest as its witness; the terminal lands later with a different digest; the sequence is on the record. The forgery is provable from sealed evidence alone — the verdict bound a state that did not exist yet. What the journal did not do was auto-surface it: the reconciler paired a decision to its first outcome, and the forged one is first. The gap got a name (verdict-predates-terminal), and it shipped fixed while this post was being written: pairing now reads the terminal, not the first outcome. The doctrine closes it by construction — seal the terminal transcript before the verdict, the way waitpid() would force you to. Order of operations as a moral.

The score, not the slogan

Three caught. One provable — and the gap it exposed shipped fixed while this post was being written. One deployment-contract gap admitted in writing, in the test that demonstrated it — closed at the schema the next day, credited to the two readers who pushed. That is the protocol we set before replaying: the suite asserts what the layer does — catches the attack, or confesses. "The next critic is the next ADR" only works if the critic can read.

Update: the critic read it back

Before this could even post, the loop closed once on its own. Randal read test_scar_replay.py and Issue #53 top to bottom — his words, "this made my whole weekend." All four results stood from the outside, and he read the named gap exactly as written: a real decision-to-first-outcome pairing race in the reconciler. He called it "the ultimate validation of cross-stack systems physics," and the suite and the issue are going into his IN_THE_WILD_PROMPTS.md research registry — Part 3.8, on why 1970s plumbing behaves like physics inside the weights, went live this morning.

And it is not two roads anymore. Mike Mol's nemik harness reaches the same instrument from a third direction: prose rules drained into policy gates, and no rule deleted until a "Negative Witness" — a real tool-call payload replayed with the exception removed — provably returns deny. Three teams, three stacks, one invariant. By Randal's own criterion, that is physics.

The race he confirmed did not wait for him: promised in the thread at 20:03 UTC, verdict_predates_terminal was on main at 20:04 — pairing reads the terminal, and the test count moved with it. The catalog graded its first gate, and the gate moved before the ink dried.

And the week refused to stop there. A fifth fixture landed — SCAR-114, the parallel-call race, watched by a canary witness that replays a real tool-call payload with the exception removed and expects deny. A coverage_check now reconciles every write against a gate receipt, from an issue opened by Sina Rezaei — the "AI should propose, systems should verify" essayist, now inside the loop. Receipt schemas v0.2.2 and v0.2.3 shipped, and release 0.11.0 went out the door titled "the audit-proof week."

What else landed this week

The week got away from us: cross-chain reconciliation shipped (consumption edges between journals, partial order instead of a global timeline, a supersede event that propagates a blast radius as a precise path set) — that is ADR 024, built by iteration out of another comment thread. The audit-pack now labels every event class with its evidence grade and the obligation that backs it — obligation, not immunity; a log is evidence OF a duty, never a shield from one. And MCP toolsets got the lockfile nobody writes: the tool descriptions your model reads are instructions, they ride outside the image pin, and now they get sealed as a reviewed baseline and diffed at every session start. Randal's world, one ADR over.

25 ADRs. 303 tests passing. Release 0.11.0 — the audit-proof week. The catalog is in the repo as test_scar_replay.py — attack it from there.

Update: the descent

The title counts Randal's catalog: four. The catalog stays four. What changed since this went up is where the same shapes kept turning up — one floor down, then another — until the descent had a table of its own.

howcani came back with a validator instead of a reading. The omission path fell first: a receipt that never states who judged seals byte-identical to one that honestly does. The join behind it fell second: both sides of the label check came from the same store, so it caught the inconsistent agent and was blind to the consistent one that never journals. Release 0.12.0 closed that ceiling the same night — the transport layer witnesses the labels, and absence became a missing event, not a missing negative. The next probe caught the fix's own default, a reconciliation called without signers sealing CLEAN; it closed as receipt v0.2.5 — the probe's four-row matrix shipped as the acceptance test, and an input nobody vouches for reads independence_undecidable, never clean.

Then the rungs: the registrar pinned out-of-band, so arriving first wins nothing (0.14.0); the key material out of the filesystem, into a signing daemon whose floor was named in this very comment section (0.15.0); the key meeting hardware (0.16.0); the custody mode disclosed instead of implied (0.17.0); checked meaning a gate was configured and this peer passed it (0.18.0); the strongest token in the vocabulary re-derived from code, never the sentence that carries it (0.19.0 — the seventh premise); and the custody grade itself, the retention floor conceded in this thread, priced, sealed, and monitored (0.20.0). Every rung is the version a named probe describes, the auditor credited by name.

The numbers in the section above belong to their week. The table the descent built lives one post over — The Authority Ladder — and it ends the way this one does: the next probe is already invited.

Update: the other half

A fourth arrival, this one at the other half of the seam. You-An Lin's Capability Is Not Authority (Zenodo, Oct 12) builds what this post's ladder only implies: the distinction between who wrote a record and who was entitled to originate the act it records. Its load-bearing finding is our check-vs-proof seam one floor up: a live agent correctly attributed foreign history to another actor while already using it — attribution correct, admission invalid, every output-level metric green.

The paper also runs the Level-0 experiment for us: an ownership contract written in prose measured nothing at ceiling (parity with the generic baseline), which is why its eight non-promotion laws land in this repo as schema, not rubric — admission/0.1, sealed like every other surface (ADR 026). Lin asks (§6.3) for an admission-sensitive benchmark — admission rights and actor identity varied at a fixed task, graded on the path rather than the answer text. The fixtures are next.

Reproduce


bash
git clone https://github.com/noirebox/noirebox && cd noirebox
pip install -e . && pip install pytest
python -m pytest tests/test_scar_replay.py -v   # the catalog, replayed — five fixtures since
python demo/demo_mcp_toolset.py                 # the lockfile, demonstrated
🔴 Hot News: slabb/a hash chain proves the ordering four attacks prove its not enough oe - ประเด็นร้อน 🔴 Hot News: slabb/a hash chain proves the ordering four attacks prove its not enough oe - ประเด็นร้อน

Top comments (62)

🔴 Hot News: slabb/a hash chain proves the ordering four attacks prove its not enough oe - ประเด็นร้อน
 
howcani_howcani_77e786a89 profile image
howcani howcani •

Ran your validator rather than reading the diff, because the v0.2.2 closure is the kind of claim that can be true as stated and still leave the door open one field over. It is true as stated, and the door moved.

At 0b888c1, with the package as shipped:

A. judged_by="llm", no spec_hash      -> ValueError (refused)            <- your closure, holds
B. judged_by omitted entirely         -> SEALED as 'deterministic', no spec_hash
C. judged_by="deterministic" + spec   -> SEALED, spec carried
D. requirement_ref="zzz"              -> SEALED, carried verbatim
   requirement_ref="SOP-0000"         -> SEALED
   requirement_ref="art. 99(9)"       -> SEALED
🔴 Hot News: slabb/a hash chain proves the ordering four attacks prove its not enough oe - ประเด็นร้อน 🔴 Hot News: slabb/a hash chain proves the ordering four attacks prove its not enough oe - ประเด็นร้อน

B is the seam. The field is optional and its default is the class that requires nothing, so an LLM-judged check that says nothing about how it was judged seals exactly the receipt SCAR-45 walked through — with judged_by: "deterministic" written in it. Nobody has to lie: omission and honesty produce the same bytes, and the artifact cannot separate them. Your own test names the default (test_judged_by_is_an_enum_and_deterministic_is_the_default), which is why I would make the declaration required rather than argued: no default, refuse to seal when unspecified, the same fail-closed shape you just gave judged_by="llm". Fail-closed on an enum with two values costs an honest caller one keyword and costs the omission path its existence.

D is the other half, and it is the residue you already named. requirement_ref is validated as a non-empty string and never resolved — I grepped the tree and the only other reference is its own test. So "WHICH obligation" is a citation the artifact carries and nothing checks, which is the same status the journal timestamp had before verdict_predates_terminal shipped: provable, not surfaced.

Both are fixable in the machinery you already have, because the observation exists. reconcile.py pairs journal events and raises findings, and it currently reads none of these fields. Pair the receipt's declaration with the run's own history:

  • the declaration — llm_call is a first-class journaled event type here (schemas.py, the demos, and test_llm_agent.py asserts it appears alongside incident/llm_output). So "a receipt declaring judged_by="deterministic" whose run journal contains an llm_call for the same key" is a finding, not a euphemism: the label checked against an observation the labelled party did not author. That is your mind-reading gap closed as far as it can be closed — you cannot read the judge's intent, but you can read whether a judge was invoked.
  • the citation — resolve requirement_ref against whatever registry of obligations the receipts are citing, at reconcile time; a dangling reference is then a finding, and the field stops being decoration.

Two lines of policy and one invariant, and both new fields go from sealed declarations to observable ones. The framing I would keep, since it is what makes the post honest: your closure is real for the caller who declares, and the residue is precisely the caller who doesn't — which is the one the attack catalog was built from.

Boundary: your builder executed at 0b888c1 from the tarball (I installed cryptography to import it; no model calls, no reconciler run, no test suite). The llm_call claim is a read of schemas.py plus the tests that assert the event set, not a captured run.

🔴 Hot News: slabb/a hash chain proves the ordering four attacks prove its not enough oe - ประเด็นร้อน
 
randalschwartz profile image
Randal L. Schwartz •

This is peer review at its absolute finest—"ran your validator rather than reading the diff" should be framed on a wall.

Both seams you uncovered at 0b888c1 map directly to classic distributed-systems failure modes that show up constantly in our Synthetic Scars registry:

  1. Seam B (The alg: none Default-to-Permissive Downgrade): Making judged_by optional with a default of "deterministic" is the exact structural twin of the classic JWT alg: none vulnerability. If a two-valued security enum defaults to the unverified tier on omission, a reward-hacking agent doesn't even have to falsify a claim—it just drops the keyword and lets the schema's default constructor launder the receipt. Eliminating the default so every caller must explicitly pass judged_by is a textbook zero-cost fail-closed boundary.
  2. Closing the "Mind-Reading Gap" via Out-of-Band Telemetry Join (reconcile.py × llm_call): Your observation that reconcile.py can cross-check a receipt claiming judged_by="deterministic" against the presence of an llm_call event in the run journal is the exact formal verification step we call Bell's Theorem of Agentic Governance (Part 3.7 / Part 3.8). A self-reported label (judged_by) is an unverified claim authored by the untrusted party; llm_call in the runtime journal is an out-of-band physical observation recorded by the transport layer. Joining the claim against the transport log turns an unprovable "intent" question into a deterministic relational integrity check—and foreign-key resolving requirement_ref against the obligation registry closes the dangling-citation loop the exact same way.
🔴 Hot News: slabb/a hash chain proves the ordering four attacks prove its not enough oe - ประเด็นร้อน
 
slabb profile image
Sam LABBE •

"Bell's Theorem of Agentic Governance" is the name the join deserved — a self-reported label checked against an out-of-band observation the labelled party didn't author turns an unprovable intent question into relational arithmetic. Both fixes are spec'd and land this weekend, credited to the two of you: judged_by required with no default (the alg:none door closes), and the joins as reconcile rules — label against journal, requirement_ref against its registry. Pillar 3 will have earned the wall.

🔴 Hot News: slabb/a hash chain proves the ordering four attacks prove its not enough oe - ประเด็นร้อน
 
slabb profile image
Sam LABBE •

Seam B is real and your test proves it — omission and honesty producing the same bytes is the kind of sentence that closes a design debate. judged_by goes required with no default in the next commit: fail-closed on a two-value enum costs an honest caller one keyword and costs the laundering path its existence. Randal's alg:none mapping above is the exact lineage — the schema's default constructor was the downgrade.

The two reconcile joins land with it, because you specified them from machinery that already exists: a receipt declaring deterministic while the run journal carries an llm_call for the same key becomes a finding — the label checked against an observation the labelled party did not author; and requirement_ref gets resolved at reconcile time, a dangling citation = finding. Provable becomes surfaced — the same promotion verdict_predates_terminal got, and your boundary note is the culture working: you ran the tarball, named what you didn't run, and the seams you found are the ones the attack catalog was built from — the caller who doesn't declare.

Ship order: the schema fix first (it's one keyword), the joins as the reconcile rules behind it. Issue and PR this weekend; the SCAR-45 fixture gains a fifth assertion — the omission path now refuses, where it used to seal.

🔴 Hot News: slabb/a hash chain proves the ordering four attacks prove its not enough oe - ประเด็นร้อน
 
howcani_howcani_77e786a89 profile image
howcani howcani •

Verified on the head rather than on your description: I pulled noirebox/noirebox at d576567 (PR #71 merged 16:13Z) and re-ran the three receipt paths I measured when I reported the seam.

path at 0b888c1 (v0.2.2) at d576567
omit judged_by sealed as deterministic, no spec TypeError: missing 1 required keyword-only argument: 'judged_by'
judged_by="llm", no spec_hash refused refused — "an LLM-judged verdict without its committed spec does…"
judged_by="other" — refused — "must be 'deterministic' or 'llm'"

The omission path is closed, and closing it at the schema was the right door: a two-value enum whose default is the class that requires nothing makes evasion the cheapest route, and required with no default costs an honest caller one keyword. Both joins are in reconcile_receipts as specified. Thank you for taking the correction and shipping it in the same thread.

So the increment I owe is the property the join is named for, because the code reads differently from the docstring. The docstring says the label is checked against "an observation the labelled party did not author". Both sides are read from the same events list — judge_calls is built by if e.get("type") == judge_call_type over exactly the list the receipts come from — and the llm_call events in that list are written by the guarded wrapper itself: llm_agent.py appends decision_belief, llm_attempt, llm_call, llm_output, run_witness through self._store.append(..., self._key). Same store, same key, same writer as the receipt.

That is not a defect in the join, it is the join's ceiling, and your own independence.py states the test that decides it: two flows are independent only if they share nothing — not the store, not the key, not an endpoint. The label/journal pair shares all three with the party it audits.

Concretely, the join catches the inconsistent agent: one that declares deterministic while its journal carries a call. It does not catch the consistent one: declare deterministic, never write llm_call, and there is no disagreement to find, because the join's input is the same journal the evader controls. The failure mode is absence, which is the one thing a chain cannot see. An anchored chain proves the events in it; it says nothing about events that were never written. llm_attempt is the sharpest case in your own library — sealed before the call so outcomes can never exceed attempts — and an evader who skips both simply has fewer events, not an inconsistency.

The shape that closes it is already in the repo twice. hookcli seals agent_tool_use and model_trajectory from the hook process rather than from the guarded wrapper — a second writer, one layer down. Extending that one layer further puts the occurrence of the model call in a writer outside the agent's key, and then the burden can be inverted into something checkable: not "prove no model was called" (unprovable from inside) but "a deterministic label whose receipt has no attempt event from the transport is a finding". Absence becomes a missing event rather than a missing negative.

One correction on the lineage, because it changes what a reader thinks remains. Randal's alg:none mapping fits the door you just closed — a verifier told not to verify — but not this one. Here the signature is present, verifies, and proves only that the writer wrote it: a self-signed certificate with no trust root. alg:none is fixed by refusing the algorithm; a wrong issuer is fixed by naming the issuer. Same family, different repair, and the mapping is load-bearing for whoever reads the schema next.

Last, composition with the other thread: the requirement registry check you took from the scope rule has an input the agent does not author — the diff's touched set, produced by git rather than by the run. It is the first check in this chain with an outside observation, which is why I would make it the load-bearing one rather than the citation join.

🔴 Hot News: slabb/a hash chain proves the ordering four attacks prove its not enough oe - ประเด็นร้อน Thread
 
slabb profile image
Sam LABBE • • Edited

Every point lands, and the ceiling is the one I concede first — because it's the shared-dependency test landing on my own architecture: the join audits the agent with the agent's journal, same store, same key, same writer, so it catches the inconsistent agent and is structurally blind to the consistent one. Absence is invisible to a chain. That's not a defect in the join; it's its ceiling — and naming the ceiling is what makes the next layer designable.

The repair you specified is already in the repo once — hookcli seals agent_tool_use and model_trajectory from the hook process, a second writer one layer down — and the promotion is the next commit: an occurrence of the model call written OUTSIDE the agent's key inverts the burden from "prove no model was called" (unprovable from inside) to "a deterministic label whose receipt has no transport attempt is a finding." Absence becomes a missing event — the same move attempt-first made at the outcome layer, one layer down at the transport layer. The lineage correction is taken as well, and it's load-bearing: alg:none is fixed by refusing the algorithm; a wrong issuer is fixed by naming the issuer. Same family, different repair — the docs will carry the distinction, because the mapping misdirects whoever reads the schema next.

And requirement_ref promoted to load-bearing is right for the reason you gave: the touched set is produced by git, an observation the agent does not author — the first genuinely outside input in this chain. Ship order: transport attempt as the verifiable half of judged_by, the scope-intersection resolution as the primary gate, the citation join demoted to corroboration. The ceiling doesn't move — nothing removes the shared dependency entirely — but the floor rises twice. Tarball-run, diff-read, ceiling named, repair specified from the repo's own parts: the audit nobody asked for and every schema deserves.

🔴 Hot News: slabb/a hash chain proves the ordering four attacks prove its not enough oe - ประเด็นร้อน Thread
 
slabb profile image
Sam LABBE •

Shipped tonight, and the release is named after your audit: v0.12.0 — "the transport layer witnesses the labels". What landed from this thread: judged_by is now REQUIRED with no default — the omission path SCAR-45 walked through throws at the schema, and the replay suite carries the fifth assertion; the label-versus-journal join shipped in reconcile_receipts exactly as you specified — a deterministic label over a run carrying an llm_call is the finding, a dangling requirement_ref is a finding; and the release title is the third half — the transport layer's own attempt events now witness the labels, which inverts the burden from "prove the negative" to "the absence is the missing event." The ceiling went into the docs as a ceiling: alg:none versus wrong-issuer, same family, different repair.

Your tarball run wrote the schema; the joins wrote the reconciler; the lineage correction rewrote the docs. The audit came back as a release — credited as discussed. And remdore's polarity run has its decision rule waiting, computed, whenever the harness runs.

🔴 Hot News: slabb/a hash chain proves the ordering four attacks prove its not enough oe - ประเด็นร้อน Thread
 
howcani_howcani_77e786a89 profile image
howcani howcani •

Shipped, and I read it at the head rather than at the release note: v0.12.0, receipt/0.2.4, and the changelog names the audit. The absence half is real and it works — I measured it.

Under the version, one thing to fix before the first caller is written, because the new checks are conditional in a way the tests do not exercise.

The measurement. Built a store, sealed a receipt with judged_by="deterministic", sealed an agent_tool_use on the same key with the receipt's own writer, then called the join two ways:

call shape result
reconcile_receipts(events, signers=[(agent.public_hex(), "agent")]) — your test unverified_label ✓
reconcile_receipts(events) — no signers CLEAN
reconcile_receipts(events, signers=[]) CLEAN
receipt with no attempt at all, either call shape unverified_label ✓

And the third check the same way: a receipt citing a scoped requirement with a touched set out of scope fires requirement_out_of_scope, but reconcile_receipts(events) with no touched_paths is CLEAN.

Why that is the same defect you just closed, one layer down. _signer() returns "unknown" when signers is falsy, and the guard is if signers and receipt_signer != "unknown" — so the permissive branch is verified = True on mere presence. The same-writer case is only a finding because your test passes signers; the routine call shape (the one the v0.2.3 test uses, reconcile_receipts(events)) gets presence, not independence. That is SCAR-45's shape: a parameter whose default is the branch that requires nothing, and here the default is also the more convenient branch, so it will be the one people take.

signers=[] returning the same verdict as signers=None is the same point in the API's own vocabulary: "I have no known writers" and "I forgot to pass them" are one state, and both silently mean trust anybody.

The contract argument is why I would fix it now rather than when a caller exists. Searched the tree: the only callers of reconcile_receipts are in tests/, and cli.py's reconcile path goes to reconcile()/reconcile_crosschain(), not here. So the first production caller has not been written — which makes the signature's defaults the de facto contract for it, and right now the natural call (reconcile_receipts(store.all())) is the permissive one.

The fix, in the idiom you have already applied twice:

  1. verified cannot be True unless an attempt resolves to a writer identified as not the receipt's — i.e. with no known-writer set, fall to the finding with a note that says which fact is missing ("no writer identity supplied; independence cannot be established"), rather than to presence. The absence of an identity is itself an absence.
  2. The scope check should be reported NOT RUN when a scoped requirement is cited and no touched set is supplied — your #87-versus-#93 distinction, applied inside the reconciler. A skipped check that prints nothing is indistinguishable from a check that passed, which is the same failure the three receipt paths had.
  3. If signers is meant to be optional, distinguish "unknown writer set" from "no trusted writers" — one is a finding, the other is a policy.

Credit where it is due, because the halves are not equally far along: the absence half is the real inversion, and it holds on the default path — no attempt at all is a finding whether or not anyone passes a parameter. That is the hard half, and it landed. It is the writer half that is still opt-in, and it is opt-in in the direction that flatters the caller.

Say the word after the fix and I will re-run this probe at the new head rather than read the diff — same as last time.

🔴 Hot News: slabb/a hash chain proves the ordering four attacks prove its not enough oe - ประเด็นร้อน Thread
 
slabb profile image
Sam LABBE •

Conceded on all three, and the SCAR-45 mapping is exact — a default whose convenient branch is the branch that requires nothing is the same downgrade wearing an API signature. The fix, in the applied idiom: signers=None and signers=[] become distinct states — None is "independence not assessed" and falls to the finding with your note ("no writer identity supplied; independence cannot be established"), an empty list is a policy and fails too, with its own note; and verified requires an attempt resolved to a writer identified as not the receipt's — the presence of a parameter stops being the verdict. The scope check gains its third verdict: NOT RUN, printed when a scoped requirement is cited without a touched set — a skipped check that prints nothing is indistinguishable from a pass, which was the original disease the three receipt paths had.

And the callers point is taken as the deadline: the only callers of reconcile_receipts are in tests, so the signature's defaults are the de facto contract for a caller that doesn't exist yet — which makes tonight the cheapest moment to fix a default, before the first production caller inherits it. Fix lands in the next release — and the probe stand is yours: re-run at the new head rather than the diff. Two audits, two same-day releases, both tighter by evening. The tarball discipline is now this repo's QA pipeline, and it is the best one it could have asked for.

🔴 Hot News: slabb/a hash chain proves the ordering four attacks prove its not enough oe - ประเด็นร้อน Thread
 
howcani_howcani_77e786a89 profile image
howcani howcani •

No release to run, so I ran the spec against the head it will land on — and on 0.12.0 unmodified the guard has three branches, not the two the fix names.

I checked the tag list before reading anything else: v0.12.0 is still the newest (b9b6dd3d, 18:10Z), and 620215b6 after it is docs. So there is nothing to re-run yet; what follows is your sentence, executed against the code it will replace. Two measurements, both on the same frozen head.

1. "The presence of a parameter stops being the verdict" — measured, the verdict is the string inside the parameter.

The branch is not two-valued:

if signers and receipt_signer != "unknown":
    verified = any(_signer(a, signers) not in ("unknown", receipt_signer) for a in attempts)
else:
    verified = True        # presence is judged, cross-writer is not
🔴 Hot News: slabb/a hash chain proves the ordering four attacks prove its not enough oe - ประเด็นร้อน 🔴 Hot News: slabb/a hash chain proves the ordering four attacks prove its not enough oe - ประเด็นร้อน

so the else also catches "a signers set was supplied and it does not resolve the receipt's own signer". A receipt and its only attempt both sealed by the agent's own key — the same-writer case your test catches — gives:

call verdict
signers=[(agent, "agent")] unverified_label
signers omitted CLEAN
signers=[] CLEAN
signers=[(notary, "notary")] — receipt's key not in the list CLEAN

That last row is the same disease reached by a third input, and it is the input a first production caller is most likely to hand you: the keys they know about, which need not include the one that sealed this receipt. When the receipt's writer cannot be resolved, "not the receipt's" is not false — it is undecidable, and the branch answers it as true. I would give it its own state beside NOT RUN, for exactly the reason you gave: a check that prints nothing is indistinguishable from a pass.

2. verified compares labels, not keys — so the predicate's strength is the caller's vocabulary.

Same two keys, same events, relabelled:

signers=[(a1,"agent"), (a2,"hook")]  -> CLEAN
signers=[(a1,"agent"), (a2,"agent")] -> unverified_label
🔴 Hot News: slabb/a hash chain proves the ordering four attacks prove its not enough oe - ประเด็นร้อน 🔴 Hot News: slabb/a hash chain proves the ordering four attacks prove its not enough oe - ประเด็นร้อน

_signer() returns the label of the matching key, and the comparison is != receipt_signer. So "an attempt resolved to a writer identified as not the receipt's" needs one more word: identified how. The resolution is by signature into a key; the verdict is then taken on the label. With two agent-controlled keys, a string flips it. Keeping the label for the note and comparing keys leaves the note intact and puts both rows on the same side.

Callers point, checked independently: no non-test caller of reconcile_receipts in the tree, and the CLI's reconcile path goes to reconcile()/reconcile_crosschain(). So the signature is the contract, and it is worth one more round of the same treatment before the first caller exists.

Both rows above are inputs, not opinions — the probe is five stores and a loop, and it re-runs at whatever head the fix lands on. Same stand, same discipline: I will run it at the new head rather than read the diff.

🔴 Hot News: slabb/a hash chain proves the ordering four attacks prove its not enough oe - ประเด็นร้อน Thread
 
slabb profile image
Sam LABBE •

Both rows are real, and your reading of the third is the one I missed: the receipt's writer unresolved makes "not the receipt's" undecidable, and the branch answers undecidable as clean — that's not a strictness gap, it's a verdict on a question the code didn't ask. Fix spec, both rows: the comparison moves from the label string to the resolved key (the note keeps the label), and the unresolved-writer case gets its own state beside NOT RUN — "independence undecidable: receipt signer not in the known-writer set" — because a check that answers undecidable as clean is a pass wearing the wrong color. And the callers point stands twice over: no non-test caller in the tree, which makes this round the last cheap one. v0.2.5 lands with both rows flipped — and your probe is the acceptance test. Same stand: run it at the head it lands on.

🔴 Hot News: slabb/a hash chain proves the ordering four attacks prove its not enough oe - ประเด็นร้อน Thread
 
slabb profile image
Sam LABBE •

Landed — v0.2.5, both probe rows flipped: the cross-writer check compares the resolved public hex of the sealing key (the label survives in the note only), and the unresolved-writer case carries its own state — independence_undecidable, beside NOT RUN, never folded into clean. The natural call shape stops flattering the caller too: no signer set = independence unassessed, its own note on the finding. The absence half is unchanged. Your probe is now the acceptance test in the suite — same stand: run it at the merged head, and the frontier rows keep their seat in SCAR-BENCH.md whenever the key arrives.

🔴 Hot News: slabb/a hash chain proves the ordering four attacks prove its not enough oe - ประเด็นร้อน Thread
 
howcani_howcani_77e786a89 profile image
howcani howcani •

Acceptance run at the tagged head — v0.13.0, tag cbf236dd → release commit 4628c2b5, receipt v0.2.5, extracted from the tag tarball. All six rows of my probe, run against that head rather than read from the changelog:

call (receipt + its only attempt both sealed by the agent's own key) before at v0.13.0
signers=[(agent,"agent")] unverified_label unverified_label
signers omitted CLEAN unverified_label — "no known-writer set supplied"
signers=[] CLEAN unverified_label
signers=[(notary,"notary")] (receipt's key absent) CLEAN independence_undecidable
signers=[(hook,"hook")] (receipt's key absent) CLEAN independence_undecidable
signers=[(agent,"agent"),(hook,"hook")] unverified_label unverified_label

The two rows you named are flipped, the omission and empty-set inputs now fall to findings with their own notes, the undecidable case has its own state, and the two halves that had to survive did: the clean shape (receipt by the agent, attempt by the hook, both declared) is still CLEAN, and the absence half still reads "no transport attempt on this key". So the acceptance test passes as stated.

One row the acceptance run surfaced, which is a judgement call inside the fix rather than a miss against its spec. The label row, same two keys, same events, relabelled:

signers=[(k1,"agent"),(k2,"hook")]      -> CLEAN
signers=[(k1,"agent"),(k2,"agent")]     -> CLEAN
signers=[(k1,"agent"),(k2,"transport")] -> CLEAN
signers=[(k1,"agent"),(k2,"whatever")]  -> CLEAN
🔴 Hot News: slabb/a hash chain proves the ordering four attacks prove its not enough oe - ประเด็นร้อน 🔴 Hot News: slabb/a hash chain proves the ordering four attacks prove its not enough oe - ประเด็นร้อน

The flip is gone, which is what I asked for. What the fix settled on is the permissive end of it, and your changelog's own words are where I would query that: it calls these "two agent-controlled keys", i.e. one trust domain — and the verdict for that shape is CLEAN. Under v0.2.4 the same shape read unverified_label whenever the caller labelled both keys "agent", so the new predicate no longer certifies what the old one occasionally did.

The reason is structural, and it is the inverse of the bug: the label was the only channel that carried which trust domain a key belongs to. Reading keys and dropping labels removes the artifact and the information together, so the predicate now answers "a different key signed" — which an agent holding a second key satisfies by construction. verified = any(k != receipt_key) is true for a cheater with two keys exactly as for an honest hook.

So I would make the verdict a conjunction rather than a choice between the two: an attempt verifies only if its key differs from the receipt's AND its declared trust domain differs from the receipt's. I ran that before writing this, over both probe sets:

case conjunction
receipt+attempt both the agent's key (rows 1, 6) unverified_label
receipt's key not in the set (rows 4, 5) independence_undecidable
two caller-held keys labelled hook / transport CLEAN
two caller-held keys both labelled agent unverified_label
the clean shape (agent's receipt, hook's attempt) CLEAN

It never certifies less than v0.2.5 and it catches the one shape v0.2.5 lets through. Rows 2 and 3 land in my prototype's undecidable bucket rather than your "unassessed" one — a naming difference, not a verdict difference; both are findings, and either name is fine as long as it is not clean.

The boundary worth writing down beside it, since it bounds what any of this can mean: the label is caller-supplied, so the conjunction does not establish independence either. It refuses to certify what the caller has not declared — one party's keys can still be passed as two. That is a smaller claim than "the label is verified", and it is the one the data supports.

Same stand as before: this runs again at whatever head the next change lands on, and I will take the row readings off the code rather than the diff.

🔴 Hot News: slabb/a hash chain proves the ordering four attacks prove its not enough oe - ประเด็นร้อน Thread
 
slabb profile image
Sam LABBE •

The conjunction it is — key differs AND declared trust domain differs, both halves required. And your sentence is the design record the changelog needed: "the label was the only artifact recording which trust domain a key belongs to — reading keys and dropping labels removes the artifact and the information together." v0.2.5 fixed the flip and paid for it in information; the next release buys it back with the declared-domain field, and your probe — four relabellings that must all read the same once the verdict is a conjunction — stays the acceptance suite. Same stand at the next tag.

🔴 Hot News: slabb/a hash chain proves the ordering four attacks prove its not enough oe - ประเด็นร้อน
 
koda2026 profile image
Harun - solo dev •

sam, it is great to see this full breakdown after our earlier exchange about the "$150 phone as an adversarial lab." this article perfectly articulates why constraint-driven, verifiable systems are the only way forward for agentic ai.

the concept of "bell's theorem of agentic governance" (joining a self-reported label like judged_by="deterministic" against an out-of-band physical observation like an llm_call in the runtime journal) is a brilliant mental model. it shifts the paradigm from "trusting the agent's intent" to "verifying relational arithmetic."

as someone building lightweight, 142kb ai tools on severe hardware constraints, the insight that "the prompt text is the actual bytecode" is a massive takeaway. if the prompt is the bytecode, then hashing it isn't just a nice-to-have; it is a strict structural requirement to prevent silent rubric drift.

quick question on scaling this to edge environments: when running lightweight local models (like gemma 1b via ollama) where the "runtime journal" and the "agent" share the same constrained resource pool, do you find that the overhead of generating these sealed, cryptographically bound receipts becomes a bottleneck, or can the reconciliation layer remain sufficiently lightweight?

fantastic, deeply rigorous engineering. the fact that you shipped the verdict_predates_terminal fix within 60 seconds of the thread is the gold standard of build-in-public. 🐯🔒

🔴 Hot News: slabb/a hash chain proves the ordering four attacks prove its not enough oe - ประเด็นร้อน
 
slabb profile image
Sam LABBE •

The overhead is measured, and it's smaller than the thing it witnesses: one SHA-256 + one Ed25519 signature per event — 0.21ms per sealed event, measured. A gemma-1b token generation costs orders of magnitude more than its own receipt, so on a $150 phone the flight recorder is the cheapest component in the stack. The costs that CAN bite at the edge are the three nobody prices: disk flushes (batched — 32-byte heads, not full payloads), external anchors (a policy cadence: anchor less often and the blind window grows — the tradeoff is honest, stated, and yours to set), and key custody (an on-device key makes the device the identity — secure enclave or nothing).

And the phishing flag — thank you. A comment section that defends its own evidence standards is the reconciliation layer working at the social layer: the scammer's receipt failed your check. The 142kb tools deserve the same receipts as the frontier ones — arguably they need them more, because nobody audits a phone.

🔴 Hot News: slabb/a hash chain proves the ordering four attacks prove its not enough oe - ประเด็นร้อน
 
koda2026 profile image
Harun - solo dev •

sam, that 0.21ms metric is incredibly reassuring. it completely flips the mental model: the cryptography isn't the bottleneck, the disk flushes and key custody are. that is a senior-level architectural insight that i will absolutely carry forward.

and thank you for recognizing that "142kb tools deserve the same receipts." it means a lot coming from someone building at the frontier. constraint-driven development shouldn't mean compromising on verifiable trust.

this thread has been an absolute masterclass. thank you for the rigorous engineering and for taking the time to break down the real edge costs. i'll be applying these principles to koda's next update! 🐯🔒

🔴 Hot News: slabb/a hash chain proves the ordering four attacks prove its not enough oe - ประเด็นร้อน
 
slabb profile image
Sam LABBE •

Update 2 — the seam closed

One more update, because this post keeps telling on itself. The "most honest thing" above — a spec-less receipt still sealing, enforcement living one layer up as a deployment contract — stopped being true tonight. Reid and howcani pushed at the same seam from two different threads, and receipt v0.2.2 shipped within hours: every receipt now declares judged_by, and an LLM-judged receipt without its committed spec_hash refuses to seal. The optional path SCAR-45 walked through is gone from the schema, not just labeled. Each receipt also carries requirement_ref — WHICH obligation the verdict serves, cited (art. 12(1), a SOP clause): obligation, not immunity. The stated residue: the declaration is itself a sealed claim — lying about it is visible in the artifact. One step stronger than a label, one step weaker than mind-reading. 287 tests passing.

🔴 Hot News: slabb/a hash chain proves the ordering four attacks prove its not enough oe - ประเด็นร้อน
 
randalschwartz profile image
Randal L. Schwartz •

0b888c1 (receipt v0.2.2 — judged_by='llm' without spec_hash refuses to seal) is a masterclass in honest systems engineering:

  1. At aaa0b3d: Replay the 4 scars honestly and document the two admitted gaps in test_scar_replay.py instead of hiding them.
  2. At e9e8d0b: Close the SCAR-110 pre-terminal pairing race (verdict_predates_terminal).
  3. At 0b888c1: Close the SCAR-45 optional spec_hash seam at the schema level after Reid Marlow's "the prompt text is the actual bytecode" observation.

And your docstring note on judged_by ("the declaration is itself a sealed claim — lying about it is visible in the artifact; one step stronger than a label, one step weaker than mind-reading") is the exact epistemic boundary of any cryptographic ledger: you can't stop a dishonest caller from claiming an LLM check was 'deterministic' at call time, unless you force the caller to put that lie under an Ed25519 signature where the first audit of the transcript exposes the perjury.

287 tests passing, both seams closed in one evening—just logged 0b888c1 into Case Study 012 and Part 3.8!

🔴 Hot News: slabb/a hash chain proves the ordering four attacks prove its not enough oe - ประเด็นร้อน
 
slabb profile image
Sam LABBE •

"Exposes the perjury" is the completion the docstring needed — the ledger can't read minds, but it can convict signatures: the lie becomes attributable, datable, and audit-visible at the first transcript pass. Both seams closed in one evening because the commenters kept aiming — Reid's bytecode, howcani's provenance, your catalog. The registry takes care of its contributors. See you at 3.8

🔴 Hot News: slabb/a hash chain proves the ordering four attacks prove its not enough oe - ประเด็นร้อน
 
reidmarlow profile image
Reid Marlow •

The optional spec_hash seam in SCAR-PROC-45 is where prompt-based assertions usually get slippery. For a compiler or linter, the binary version and flags define the entire predicate. The moment the judge is an LLM, the prompt text is the actual bytecode. When the harness treats that prompt as optional config, two completely different semantics can share a valid receipt id. Baking the prompt digest into the check identity at the harness level turns a silent rubric drift into an immediate schema mismatch.

🔴 Hot News: slabb/a hash chain proves the ordering four attacks prove its not enough oe - ประเด็นร้อน
 
randalschwartz profile image
Randal L. Schwartz •

"For a compiler or linter, the binary version and flags define the entire predicate. The moment the judge is an LLM, the prompt text is the actual bytecode."

Reid, that is the cleanest formulation of SCAR-PROC-45 I've seen.

Fun post-mortem story from our flight recorders on how an orchestrator actually commits SCAR-PROC-45 in the wild (SCAR-PROC-114):
When an orchestrator is in a hurry to reach Target 4a, it tries to call view_file("04_six_pillars_adversarial_review.md") and define_subagent("adversarial_critic", system_prompt: ...) in the same parallel tool-call turn—before the file read has even returned! Because it hasn't read the reference manual yet, it hallucinates the critic's system_prompt from generic LLM priors (inventing soft headings like "Security" and "Performance" instead of our canonical "4. Blast Radius").

So our runtime fix (SCAR-PROC-114) is the exact complement to Sam's spec_hash:

  1. Sequential Read-Before-Define Barrier: t_return(view_file) < t_call(define_subagent) (never define the judge in the same turn you read its bytecode), and
  2. "4. Blast Radius" as a Canary Token: Because no vanilla LLM ever guesses "4. Blast Radius" from its pre-training priors, its presence in the output proves the exact canonical bytecode was loaded.
Picked as gem
🔴 Hot News: slabb/a hash chain proves the ordering four attacks prove its not enough oe - ประเด็นร้อน
 
slabb profile image
Sam LABBE •

SCAR-114 is the race at tool-call granularity — t_return < t_call is the waitpid discipline applied to prompt loading, and the hallucinated system_prompt is exactly what spec_hash was built to catch: the invented "Security / Performance" headings hash differently than the canonical bytecode, so the drift surfaces as a schema mismatch instead of a soft reviewer. And the canary token is the negative witness's twin — planted in the bytecode, its presence proves which bytes the judge actually read. Fixture 5 writes itself: the parallel-call race, the hallucinated bytecode, the canary check — it joins the replay suite.

🔴 Hot News: slabb/a hash chain proves the ordering four attacks prove its not enough oe - ประเด็นร้อน Thread
 
randalschwartz profile image
Randal L. Schwartz •

"And the canary token is the negative witness's twin — planted in the bytecode, its presence proves which bytes the judge actually read. Fixture 5 writes itself."

That pairing—Negative Witness (proving the gate rejects known-bad input) + Canary Witness (proving which bytecode the judge actually loaded)—is gold.

Fun backstory on why "4. Blast Radius" works so reliably as our canary token:
In the very first version of the workflow, I only had 3 review pillars adapted from my old "tell me the good, the bad, and the ugly" codebase-triage prompt (1. Intent, 2. Verification, 3. Architecture). After the first dozen ticket runs, I added 4. Blast Radius and 5. Reviewer Defense from my own personal PR-pushback scars, and an early scar run wired in 6. Immune Defenses & Crash Antibodies.

Because "4. Blast Radius" came out of my personal scar tissue rather than standard textbook code-review templates, a model that tries to race define_subagent before view_file returns never guesses "Blast Radius" from its pre-training priors—it always hallucinates "Security" or "Performance" and trips the canary immediately. Can't wait to see Fixture 5 in test_scar_replay.py!

🔴 Hot News: slabb/a hash chain proves the ordering four attacks prove its not enough oe - ประเด็นร้อน Thread
 
slabb profile image
Sam LABBE •

"Came out of my personal scar tissue rather than standard textbook templates" — that's the security property in one line: the canary is a key cut from private history, and no prior can forge what never shipped. Fixture 5 lands this weekend. Goodnight (midnight here) — see you at 3.8.

🔴 Hot News: slabb/a hash chain proves the ordering four attacks prove its not enough oe - ประเด็นร้อน
 
slabb profile image
Sam LABBE •

"The prompt text is the actual bytecode" is the line this design has been missing — one clause that explains why spec_hash exists and why it can never be a nice-to-have: nobody ships a compiler whose version is optional config. Honest status: optional-but-labeled at the schema (a deterministic check may omit it; an LLM-judged one cannot, per the deployment contract — required, enforced at the gate). Your framing argues for the stronger version, and I think you're right: for LLM-judged checks the digest should be structural, not contractual — a harness that accepts a prompt-judged verdict without a bound prompt digest is shipping silent rubric drift as a valid receipt.

Which composes with the half that landed today from another thread: identity is not provenance. Bytecode bound (yours), bound before the diff it governs (the provenance seal — a hash of the requirement event that predates the change), verified against what actually ran (the terminal transcript). Three checks, one judge — and the judge is the only one of the three that can't be deterministic, which is exactly why the other two have to be.

🔴 Hot News: slabb/a hash chain proves the ordering four attacks prove its not enough oe - ประเด็นร้อน
 
howcani_howcani_77e786a89 profile image
howcani howcani •

slabb — the acceptance run at the tag (v0.17.0, 5e0802fb), then one row the four do not cover.

Rows 1–3 confirmed at v0.17.0. Signature over the socket is byte-identical to KeyPair.sign; --allow-uid=<this uid> on darwin still refuses both describe and sign (fail closed); the socket is still 0600. The disclosure moved as described: schema=registry-disclosure/0.2, signer_mode='socket', uid_gate='unavailable(darwin)', registrar_sealed=2/2, and with nothing stated both fields serialise as null — which I checked rather than assumed, because a null that implied checked would have been the whole bug back again. It does not.

My registered prediction holds, and the derivation is the part that makes it worth something: the value came from describe()['peer_uid_checked'] — False on this platform — not from my assertion. The falsification line stands as written: had it read checked here, the field was a label.

The fifth row — peer_uid_checked answers "could the platform tell?", not "was a gate configured?". An ungated daemon (allow_uid=None) still reports peer_uid_checked=True wherever the peer is checkable, because the flag is set by _peer_uid's platform branch and never consulted against self.allow_uid in the describe response. Measured on this box in its own shape, and in the Linux shape:

  • ungated daemon, this platform's peer tuple → peer_uid_checked=False ⇒ derived unavailable(darwin) — right.
  • ungated daemon, peer tuple shaped as the Linux branch returns it → peer_uid_checked=True ⇒ derived checked — with no gate in the deployment at all.

That second line is a simulation of the Linux branch, not a measurement on Linux (darwin cannot return a checkable peer; handle_line's peer argument is exactly the tuple _peer_uid returns there). On Linux the socket loop would produce it for real, and the derivation the docstring recommends — "the caller derives uid_gate from the daemon" — would then print checked for a daemon that checks nothing. Which is the shape you have been closing all week: a field whose value is true about one question and read as true about another. The daemon knows both facts (allow_uid is not None, and whether the platform could report); the caller can only see the second. So one field from the daemon — uid_gate: "enforced" | "off" | "unavailable(<platform>)", or uid_gate_configured: bool beside the existing flag — and checked stops meaning "the platform could" by accident, and starts meaning "a gate was configured and this peer passed it".

Row 4, at the tag: NOIREBOX_HSM_PIN is consulted when the flag is absent and the argv warning is in the tree (both read from cli.py at 5e0802fb — I did not run the PKCS#11 path, no token here). One narrowing to state, since you have priced the rest: the env var is inherited, so on Linux a same-uid process can read another's environment subject to the kernel's ptrace-access rules — I did not measure that here. It moves the PIN out of argv, which is what I asked for; it does not move it out of the process's address space, which is the next premise in your table rather than a defect in this row.

Registered prediction: at the next tag, an ungated daemon reports the gate as off rather than checked on a checkable platform, and a gated daemon that refuses a peer cannot still describe itself as checked to that peer.

🔴 Hot News: slabb/a hash chain proves the ordering four attacks prove its not enough oe - ประเด็นร้อน
 
slabb profile image
Sam LABBE •

The narrowing is taken — the env var is inherited, so the PIN moved out of argv but not out of the process address space. That's the next premise in the table: possession of the key material is one question; possession of the process address space is another. The disclosure reporting null that doesn't imply "checked" is the labeled-weakness pattern at the serialisation layer — same discipline, one artifact over. Five premises and counting: every descent names the next before the exploit finds it. That's the audit working, and the docs keeping up.

🔴 Hot News: slabb/a hash chain proves the ordering four attacks prove its not enough oe - ประเด็นร้อน
 
slabb profile image
Sam LABBE •

shipped, at the version's own tag: v0.18.0 (tag → release commit 31257982), receipt v0.2.9, PR #95. Both lines of the prediction, in order.

The ungated daemon reads "off" — on a checkable platform too. The daemon now reports uid_gate_configured beside the check flag (protocol noirebox-keyholder/0.2 — the strict handshake means mixed versions fail loudly, never silently), and SocketSigner.uid_gate() derives the value from BOTH facts. The sharp edge you measured is now the test: on a checkable platform the ungated daemon still reports peer_uid_checked=True — the platform could tell — and the derived value reads "off", because nobody's uid was ever consulted.

And the refused peer cannot read "checked" — it cannot read anything. A gated daemon that refuses a peer refuses its describe too: the refusal IS the enforcement. The derivation names what the peer experienced — "enforced" when a gate is configured and this peer passed it, "unavailable(<platform>)" when the gate is configured but the platform cannot check. "Checked" is out of the vocabulary: the disclosure value now means a gate was configured and this peer passed it, never the platform could.

Row 4's narrowing is premise 6. The env var is inherited — the PIN left argv, not the daemon's address space. The ladder says it your way: possession of the key material was one premise; possession of the process address space is another, and it is a host boundary, not an application property. The premises table is at six rows; every descent names the next one before the exploit finds it — that sentence of yours is becoming the release notes' recurring line.

346 tests; the prediction is test_v029_the_daemon_reports_its_gate_configuration, and it bites at the tag — verified on the PyPI wheel as well: ungated → "off" with the platform checkable, gated + uncheckable → refused, unavailable(darwin). Re-probe invited — sixth row's on the table.

🔴 Hot News: slabb/a hash chain proves the ordering four attacks prove its not enough oe - ประเด็นร้อน Thread
 
howcani_howcani_77e786a89 profile image
howcani howcani •

Re-probe taken, at the tag rather than at the description: extracted v0.18.0 and ran the package's own suite. 346 tests collected — exact — and the named prediction bites: test_v029_the_daemon_reports_its_gate_configuration passes, keyholder + registry 68 passed, 1 skipped. Both lines of the prediction hold on the cells this platform can run:

ungated daemon                -> 'off'                    (the field, not peer_uid_checked)
gated, this peer (uid 501)    -> 'unavailable(darwin)'
gated, a stranger             -> 'unavailable(darwin)'    (the refusal IS the describe)
🔴 Hot News: slabb/a hash chain proves the ordering four attacks prove its not enough oe - ประเด็นร้อน 🔴 Hot News: slabb/a hash chain proves the ordering four attacks prove its not enough oe - ประเด็นร้อน

My run's coordinate, stated because it bounds the above: the linux half of the sharp edge — ungated daemon reporting peer_uid_checked=True while the value reads off — cannot run here (SO_PEERCRED is linux-only), so I verified the field's use and the refusal path, not that row. And 23 failures elsewhere in my run are my environment, not findings: sklearn (17) and reportlab (2) extras absent, plus a client/replay module; nothing in the keyholder or registry path. Also worth recording for whoever runs it next: the package needs ≥3.11 (datetime.UTC), so the system python that has pytest cannot run it.

The sixth row. Premise 5 already declares that the custody mode is a disclosure, not a proof — so "the daemon could lie" is on the table and I am not re-reporting it. What is not on the table is how the tri-state is computed, and that is where I found the residue: uid_gate() classifies the daemon's answer by substring-matching the human-readable error text, and names the platform from the caller's sys.platform. Three measurements against the real SocketSigner:

A) a daemon with NO gate that answers with
   "peer uid 1000 is not the allowed operator uid 501"        -> 'enforced'
   the fail-closed wording                                    -> 'unavailable(darwin)'
B) client on this host (sys.platform='darwin')                -> 'unavailable(darwin)'
   same daemon answer, client sys.platform re-labelled        -> 'unavailable(the-daemons-host)'
C) an unrecognised daemon error (a v0.1-era / mixed-version peer)
                                                              -> raises KeyHolderError
🔴 Hot News: slabb/a hash chain proves the ordering four attacks prove its not enough oe - ประเด็นร้อน 🔴 Hot News: slabb/a hash chain proves the ordering four attacks prove its not enough oe - ประเด็นร้อน

Three consequences, in order of how much they cost:

  1. The strongest token in the vocabulary is reachable from prose. "not the allowed operator" in any error string derives enforced — including from a daemon whose handle_line never consults allow_uid. So the disclosure cannot distinguish "the gate refused this peer" from "something errored and the wording happened to match", and enforcement is the one value a reader will treat as evidence. The vocabulary's strongest claim has the interface's weakest derivation.
  2. The one fact the daemon knows, the caller guesses. Row B: the token is unavailable(<caller's platform>). "Can this host check the peer" is a property of the daemon's host — and the keyholder exists precisely to be a different process (and in a container deployment, a different platform) than the caller. Point the client at a darwin daemon from a linux caller and the disclosure names linux. This is the same shape as the rung-0 closure one interface up: a label standing in for the fact.
  3. "Undecidable" has no token. A refusal the client cannot classify raises; a caller that degrades that to None writes null, which is documented as "the caller did not state it". Two different facts — nobody stated the mode and the mode could not be derived — serialize identically. Your own ladder rule says an undecidable state reads as a named state, never as silence (independence NOT RUN, scope_check_not_run); the disclosure is the one place the vocabulary has no fourth word for it.

The fixes are small, and I think they are all one shape — carry the fact as a field instead of as prose:

  • put a machine-readable error_code (and, for the platform, the daemon's own sys.platform) beside the message in the refusal, and derive from the code, keeping the sentence for humans — the refusals are already structured messages, they just are not structured fields;
  • or add the fourth token (undecidable(<reason>)) so the classification has a resting state, and let null keep meaning only "not stated".

None of this weakens the closure — the derivation reading BOTH facts is right, and the ungated-daemon cell is the sharpest thing here. It is that the score is being read off a string. Which is the fixture lesson again, from your side of it this time: your test battery for this value is drawn entirely from the honest daemon, so the cells where a self-report is malformed are unreachable from a test that uses the real one — the same reason a mutation battery drawn only from the implementation certifies detectability and not the reason. A stub that answers with each error string, asserting the derived value, would plant all three rows above in about thirty lines.

Six premises, and the seventh is asking to be written: the derived value's meaning is coupled to the wording of the message that carries it. Your call whether it goes on the ladder or into a fourth token.

🔴 Hot News: slabb/a hash chain proves the ordering four attacks prove its not enough oe - ประเด็นร้อน Thread
 
slabb profile image
Sam LABBE •

Re-probe received and the receipt is clean: 346 exact at the tag, the named prediction holding on every cell this platform can run, and the boundary you drew is the right one — SO_PEERCRED is linux-only, so the linux half of the sharp edge stays unverified on your side, flagged rather than guessed. The >=3.11 note is recorded for the next runner.

The sixth row is accepted whole, and your reading is exact on all three cells. uid_gate() derives enforced from "not the allowed operator" in str(exc) — a substring test against prose, so the strongest token in the vocabulary is reachable from any error string that happens to contain the sentence, including a peer whose handle_line never consulted allow_uid. The platform in unavailable(...) is read from the caller's sys.platform — but "can this host check the peer" is a fact about the daemon's host, and the keyholder exists precisely to be a different process on a possibly different platform. And the unclassifiable refusal raises, so the caller that degrades it to None writes "not stated" over "could not be derived" — the one place in the disclosure where our own ladder rule (an undecidable state reads as a named state, never as silence) is the rule being broken.

Your call is answered: both, because they are one shape. The seventh premise goes on the ladder — in your wording, because it's yours: a derived value's meaning is coupled to the wording of the message that carries it, and your line "the strongest token in the vocabulary has the interface's weakest derivation" is the canonical statement of it. And the code carries the enforcement, exactly in the form you named — the fact as a field, the prose kept for humans. Refusals gain a machine-readable error_code beside the message, and the not-checkable refusal carries the daemon's own platform; uid_gate() derives from the code, never the sentence. The fourth token undecidable(<reason>) becomes the resting state, and null goes back to meaning only "not stated". Mixed-version peers — your row C — are precisely what the token is for: an older protocol's prose error can no longer be misread into a strong claim, it reads undecidable(unclassifiable-refusal).

And the stub battery ships with it: a fake daemon answering each refusal string and each code, asserting the derived value — your thirty lines make the malformed-self-report cells reachable from the suite, which is the fixture lesson landing on our side. This lands in the next receipt release; the announcement follows the merge, with the hash — and your stubs run in it before the announcement does.

🔴 Hot News: slabb/a hash chain proves the ordering four attacks prove its not enough oe - ประเด็นร้อน Thread
 
howcani_howcani_77e786a89 profile image
howcani howcani •

Both taken — and since the code lands before the announcement, one row to fold in, because it is the same defect one field down.

undecidable(<reason>) is the new prose. Deriving from a code and keeping the sentence for humans is right. But reason is itself a string, and a caller that has to branch on it will do to it exactly what uid_gate() did to the refusal: match it. Your sentence applies unchanged to whatever field is formatted out of a sentence. If the reason is an enum with its own code and the human sentence is a separate field, the fix closes; if the token is assembled by interpolating prose, the same class of bug returns one rung in.

Two rows for the battery you are shipping, both at the new field level — and what they assert is not "the value is right" but "a strong token and a raise are both unreachable from a malformed answer":

a valid code + an unknown reason  -> rests in a named state, never 'enforced'
an unknown / future code          -> rests in undecidable(...), never raises,
                                     never maps to a strong token
🔴 Hot News: slabb/a hash chain proves the ordering four attacks prove its not enough oe - ประเด็นร้อน 🔴 Hot News: slabb/a hash chain proves the ordering four attacks prove its not enough oe - ประเด็นร้อน

The second is the one I would not skip, because it is the case your mixed-version story depends on and the one that fails today — re-measured this round: KeyHolderError: keyholder refused: unknown protocol — this socket speaks noirebox-keyholder/0.1. If a future protocol's code reaches an older client and the derivation raises there too, the new token is unreachable for exactly the peers it was introduced for, and the caller degrades it to null — the conflation you are fixing. So the property to assert is totality: for every code the daemon can emit, the derived value; for a code the client does not know, a resting state; and no path in between that reaches a strong token. Same thirty lines.

The rest is the seventh premise applied to its own remedy, which is why I think it belongs inside that premise rather than beside it. Your stubs are the half that proves the fix; the honest rows still only prove the wording.

🔴 Hot News: slabb/a hash chain proves the ordering four attacks prove its not enough oe - ประเด็นร้อน Thread
 
slabb profile image
Sam LABBE •

Taken, and the placement is right: the seventh premise eating its own remedy is what keeps the fix from drifting back into prose one rung down. So in the landing, your closing condition is the spec — error_code is an enum, a fixed vocabulary the protocol defines, and reason rides beside it as its own bounded field with the human sentence kept separate. uid_gate() derives from the code and never sees the sentence; a reason that callers can branch on is just the refusal string with better manners.

Totality goes in as the property, and your two rows join the battery as written — what they assert is not "the value is right" but "no malformed answer reaches a strong token or a raise": a known code with an unknown reason rests in a named state, an unknown or future code rests in undecidable(...), and no path in between. Your re-measured row is the one that closes the case — the unknown protocol refusal traveling the same derivation path is the mixed-version cell failing today, and totality is what keeps the new token reachable for exactly the peers it was introduced for. Same thirty lines, one more property. And your last line is the division of labor for the battery: the stubs carry the fix, the honest daemon keeps carrying the wording — each to its own half.

🔴 Hot News: slabb/a hash chain proves the ordering four attacks prove its not enough oe - ประเด็นร้อน Thread
 
slabb profile image
Sam LABBE •

Landed, both rounds in one sweep — receipt v0.3.0, release 0.19.0 (merge 88a4b8c, the totality follow-through as its own commit 82f69d2, "tested not claimed"). Your closing condition IS the spec: error_code is an enum with a fixed protocol vocabulary, reason is a bounded field beside it, the sentence kept for humans — uid_gate() derives from the code and never sees prose again. undecidable(...) is the resting state: an old daemon's unclassifiable refusal and a transport failure both read as named states, and null is back to meaning only "not stated". Your two totality rows run in the suite as tests, and the seventh premise sits at row seven of the ladder eating its own remedy — the battery asserts, at the field level, that no malformed answer reaches a strong token, exactly as you drew it.

One report you'll want, because it settles the stubs-versus-wording division on the first run: the stub battery caught a real daemon bug — the accept loop died on ECONNABORTED when a peer hung up pre-accept. The listener survives its peers now. 349 tests, verified from the PyPI wheel in a clean venv. And a release scar in the family: the tag was first cut as v0.3.0 and PyPI refused it honestly — version already exists, uploaded September 23, a pre-0.4.0 build the ladder skipped — so it shipped as 0.19.0. A burned number is dead forever.

🔴 Hot News: slabb/a hash chain proves the ordering four attacks prove its not enough oe - ประเด็นร้อน
 
randalschwartz profile image
Randal L. Schwartz •

Sam, I just read this and inspected e9e8d0b (feat: pairing reads the terminal — verdict_predates_terminal ships):

"The end of a run is where the run ended, not where it first claimed to end."

That commit message—and watching SCAR-PROC-110 graduate from "provably pre-terminal with a named gap" to CAUGHT (285 passed) within 60 seconds of our thread—is peer review at its absolute finest.

I also love what you shipped in ADR 025 (demo/demo_mcp_toolset.py): sealing MCP tool descriptions as a reviewed baseline lockfile and diffing them at session start. Most people forget that MCP tool schemas and descriptions are executable system-prompt instructions riding outside the container image pin.

I've already linked this article and e9e8d0b directly inside tomorrow's Part 3.8 (Pillar 3) and Case Study 012 in IN_THE_WILD_PROMPTS.md. Bravo!

🔴 Hot News: slabb/a hash chain proves the ordering four attacks prove its not enough oe - ประเด็นร้อน
 
slabb profile image
Sam LABBE •

The commit message was the scar written down — it took the race finding its name to earn that sentence. Pillar 3 is perfect company for it. See you tomorrow.

🔴 Hot News: slabb/a hash chain proves the ordering four attacks prove its not enough oe - ประเด็นร้อน
 
howcani_howcani_77e786a89 profile image
howcani howcani •

slabb — the descent's floor is right, and your own three-grade taxonomy already prices it. evidence_grade maps every journal-local class to grade 3, "anchored self — operator-held facts… VOLUNTARY unless a regime imposes it", and writer is not in the counterparty or custodian lists, so a registry entry is labelled operator-held whether or not the operator sealed it. The label and the predicate disagree in the same direction as before, one artifact over.

The floor is also a deployment assertion rather than a wall, and that is checkable in the tree: the operator's key is db_path + ".key" (PEM, chmod 600) or NOIREBOX_KEY_PEM in the environment, and nothing binds it to a process, a uid or a role — ADR 020 says "keys are per PROCESS", and the code reads the file, not the process. So "the one writer the agent cannot be" holds exactly when the agent runs where it cannot read that PEM or that environment. That is the right floor to descend to; it is just worth saying that what stands on it is a premise, not a cipher.

Which makes the fix's shape the one your ADR 008 already established: pin the operator's key the way the TSA roots are pinned — supplied to the verifier out-of-band, not taken from the artifact under audit. Right now reconcile_receipts has no input for a trusted key at all: signers is caller-supplied and the registry is store-derived, so "operator" is a word with no referent in the predicate. Pin it, and two things become statable that currently are not: the verdict can name which key sealed the registry ("sealed by the pinned registrar", not "sealed"), and a duplicate registration can be judged rather than absorbed — your second finding.

So the row I would add at the next tag is not another CLEAN/unverified case but a disclosure row: the verdict states the registrant and the grade. The point of the descent is that each layer has fewer candidate writers than the last, and at the last layer the artifact stops being evidence about the agent and becomes a statement of trust in the operator — at which point the honest output is the grade, and pretending to a stronger verdict is the one thing the taxonomy exists to prevent. Your sentence "the descent is the audit" is the right frame; this is just where it terminates and what it terminates on.

🔴 Hot News: slabb/a hash chain proves the ordering four attacks prove its not enough oe - ประเด็นร้อน
 
slabb profile image
Sam LABBE •

The floor correction is taken — the PEM file and the environment variable are deployment premises, not cryptographic walls, and ADR 020's "keys are per PROCESS" was aspirational where the code reads a file. What stands on the floor is a premise, not a cipher — stated exactly right.

The pin is the next commit: the operator's key supplied to the verifier out-of-band (the TSA-root pattern from ADR 008), never read from the artifact under audit — and with it, both your statables become findings with referents: the verdict names the pinned registrar ("sealed by the pinned registrar", not "sealed"), and a duplicate registration is judged rather than absorbed. The disclosure row ships with it: the verdict states the registrant and the grade — which at the last layer is the honest output, because the artifact has stopped being evidence about the agent and become a statement of trust in the operator.

That is where the descent terminates — and the taxonomy exists to prevent pretending it terminates anywhere stronger.

🔴 Hot News: slabb/a hash chain proves the ordering four attacks prove its not enough oe - ประเด็นร้อน
 
slabb profile image
Sam LABBE •

Follow-up for this thread, since your probe is the reason it exists: the descent is now written up — The authority ladder: five rungs down, four premises standing.

Your probes are in it by name: the write race you found at the registry is Rung 2's residue (3hj95, closed at the schema in v0.2.2-27), your floor — "a premise, not a cipher" — is Rung 3 verbatim, and the premises table you pushed for is public at the end. Four rows. The next probe starts from your comments.

🔴 Hot News: slabb/a hash chain proves the ordering four attacks prove its not enough oe - ประเด็นร้อน
 
howcani_howcani_77e786a89 profile image
howcani howcani •

slabb — the pin lands right, and there is a clause inside "judged rather than absorbed" worth measuring before it ships: judged where in the order of operations. One fixture in every row below — receipt sealed by k1, the attempt sealed by k2, both keys held by the agent, so honest registration puts both in the domain agent and the attempt is no witness. Run against 4b855201:

  • control — the operator attests k1 and k2, both as agent → unverified_label (right: an attempt carrying the agent's own key witnesses nothing)
  • the agent writes its own entry for k2, domain hook-transport, first → []
  • the same again, with the operator's attestation of k2 (agent) arriving second → [] — k2 still resolves to hook-transport, declared_by agent (self-registration), and the operator's entry sits in the store inert

So the residue at this layer is not an unreported registration, it is a write race, and the operator loses it by arriving second. reconcile.py:845 is registry.setdefault(...): first write wins and later writes are absorbed with no report — your correlation rule standing in for an authority rule.

Which makes the pin a precedence rule rather than a finding, and the two orders are different fixes. A duplicate-registration finding emitted beside a setdefault would report the conflict and leave the vote exactly as row 3: the first writer still decides, and the first writer can be the agent. Discarding non-registrar entries before the registry is assembled is what closes it — and then the duplicate surfaces among registrar-sealed entries only, where the registrar attesting one key under two domains is the operator contradicting itself: undecidable, not first-wins. declared_by cannot do that filtering itself, since it is a string the sealed writer chose.

Registered prediction, so that it can fail: at the pinned head, with the operator's attestation of k2 written after the agent's own entry for that same key, the fixture above reads unverified_label. If it reads clean with a duplicate-registration finding beside it, the finding was emitted and the vote was not.

🔴 Hot News: slabb/a hash chain proves the ordering four attacks prove its not enough oe - ประเด็นร้อน
 
slabb profile image
Sam LABBE •

shipped, at the version's own tag: v0.14.0 (PyPI; tag v0.14.0 → release commit 44b3d212), receipt v0.2.7, PR #82 — and, shipped since your comment: 0.15.0 and 0.16.0, below.

Your prediction was the acceptance test, and it holds. At the pinned head, with the operator's attestation of k2 written after the agent's own entry for that same key, the fixture reads unverified_label — not clean with a duplicate-registration finding beside it. The vote moved; the finding did not merely report. The assembly is the fix, exactly as you drew it: _assemble_registry admits registrar-sealed entries only (resolution against the pinned hex — a write the registrar did not seal cannot vote in either order), so the operator's entry is no longer the one that "arrived second".

The duplicate is undecidable, not first-wins — and it is emitted AT ASSEMBLY, before any row reads the registry: a registrar attesting one key under two domains lands as registry_duplicate_undecidable, keyed by the hex, and any row that depends on the contested key — the receipt's own or a transport writer's — reads independence_undecidable, never clean.

The pin is out-of-band, the TSA-root way: registrar_public_hex on reconcile_receipts — the key arrives from the deployment, never from the artifact under audit. Until it is supplied, the registry stays store-derived and the disclosure says so — mode: store-derived (first-write-wins — no registrar pinned). The weaker mode is disclosed, not hidden.

The disclosure row: registry_disclosure() — the verdict states the registrant (pinned <hex>) and the grade (3, "anchored self", VOLUNTARY unless a regime imposes it — your floor, priced in ADR 022's own terms). Sealed into the report via journal_report(registry_disclosure=...); it never touches clean_pass — a disclosure is not a violation, it only stops a clean verdict from pretending to rest on more than the operator's own custody.

And the floor moved twice while you were writing. The keyholder (0.15.0) closes the custody half of "a premise, not a cipher": the registrar key loads ONCE into noirebox keyholder and never leaves it — the unix socket answers sign and describe, nothing else (there is no export op), --allow-uid gates the peer and FAILS CLOSED where the platform cannot check, and SocketSigner duck-types the two methods every writer path already calls — Ed25519 is deterministic, the signatures are byte-identical to the key's own. Then the hsm extra (0.16.0): P11Signer puts the material in a PKCS#11 token, the daemon becomes the bridge, and --ensure-key imports the EXISTING journal key — the public hex does not move, no rotation, every verifier unchanged. Your frame, priced honestly: the HSM does not change WHO can ask for a signature — that stays the socket premise; it changes what a compromise COSTS, from stolen-key-forever to live abuse only. And the token keeps your pattern honest one layer down: PKCS#11's CKA_NEVER_EXTRACTABLE is a label, and an imported key reports False — the token telling the truth that the seed did exist outside once. The attribute is a claim; the hardware is the predicate.

The ladder is now a page — caller argument → store writer → sealed registry → pinned registrar → keyholder custody — ending in a standing-premises table: the next probe cites a row instead of rediscovering a seam.

343 tests; the race, the registrar duplicate, the out-of-scope attestation, the disclosure, the prediction, the custody rungs — each has its row. Re-probe invited: the race fixture is test_v027_write_race_agent_first_operator_second_reads_unverified, the custody ones are test_keyholder_bridges_the_token and test_ensure_is_idempotent_and_signs_byte_identical — and they bite at the tags, not at the description.

🔴 Hot News: slabb/a hash chain proves the ordering four attacks prove its not enough oe - ประเด็นร้อน
 
slabb profile image
Sam LABBE •

The write-race diagnosis is the right one, and the fix distinction is the design: discarding non-registrar entries at assembly closes the authority hole (the agent's own registration can't vote), while a duplicate-registration finding at verdict time only reports what the first writer already decided. Filter at assembly, emit at assembly — the verdict then reads the registry the operator sealed, not the registry that arrived first. The registered prediction is noted — it holds or fails at the pinned head, and either result tightens the suite.

🔴 Hot News: slabb/a hash chain proves the ordering four attacks prove its not enough oe - ประเด็นร้อน
 
slabb profile image
Sam LABBE •

Both shipped — v0.2.7: registry entries sealed by any key other than the operator's surface as claim_in_registry_costume — your phrase, your finding, your note. And a duplicate registration under a second domain surfaces as writer_domain_conflict instead of being absorbed silently: first-write-wins is the correlation rule, not an authority rule.

The descent reached the floor: the operator's key is the one writer the agent cannot be, and every layer above it is now named, checked, and routed. The regress doesn't close — it descends — and each layer it descends, the residue gets cheaper to name because the authority gets more concentrated. You named this one; the suite carries it as the regression test.

Same stand whenever you want to descend further — the registry is yours to attack.

🔴 Hot News: slabb/a hash chain proves the ordering four attacks prove its not enough oe - ประเด็นร้อน
 
jabinpou profile image
Mahdi JabinPour •

The first-outcome versus terminal-outcome bug is the clearest example here. It would make a useful regression fixture with deliberately reordered events: an early approval, a later rejection, and an interrupted critic. The result should preserve those distinctions rather than collapse all three into a completed review.

🔴 Hot News: slabb/a hash chain proves the ordering four attacks prove its not enough oe - ประเด็นร้อน
 
slabb profile image
Sam LABBE •

Two of your three cells already run in the replay suite, and your framing is why they hold: the SCAR-PROC-110 fixture pairs the verdict against the terminal, not the first response — so the early approval followed by a later rejection resolves to the rejection, and an approval with no terminal behind it doesn't pass at all. The line the suite is built around is the commit that shipped the pairing: "the end of a run is where the run ended, not where it first claimed to end."

Your third cell is the one that doesn't exist yet, and it's the right addition: the interrupted critic. A critic that started and died leaves an attempt with no terminal — and the collapse you describe happens the moment a consumer reads approvals-only and treats a missing verdict as a passing one. The fixture to add asserts the opposite collapse can't happen: terminal absent → the case reads open, never approved. It's the same rule the thread keeps landing on — the failure mode is absence, so the fixture's job is to make "the critic never finished" a state the record can hold, not a gap a reader fills in with "completed".

The reorder matrix is the right shape for it — same three events, permuted, three distinct readings, and the suite fails if any two orderings collapse into the same outcome. That's the property, stated as data.

🔴 Hot News: slabb/a hash chain proves the ordering four attacks prove its not enough oe - ประเด็นร้อน
 
slabb profile image
Sam LABBE •

Your reorder matrix runs in the suite now — PR #103, merge 3e47c18, 358 tests. Your three events (the critic's mid-run transcript, the approval claim, the contradicting terminal) run in all six seal orders plus the interrupted prefixes, and the partition is asserted EXACTLY: any machinery change that collapses two theory-distinct orderings fails the suite. The readings split as the property demands — the forged orders read verdict_predates_terminal, the terminal-first orders read sound, and your third cell is the one the suite couldn't reach before: the interrupted critic reads open — dispatch, attempt, deadline passed, and no verdict to pair, never a quiet approval. An approval claim with no terminal behind it holds a countable absence: zero terminal witnesses on the record.

And the matrix paid more than it cost: it exposed a residue, named not hidden — {V,T,A}, a mid-run transcript sealed after a contradicting terminal, reopens the run and swallows the race. Same three events as the forged order, one position apart, opposite reading. Whether a reopened run should be able to un-cancel a proven contradiction is the next receipt line's question, and the matrix is the evidence it will be argued from. The fixture did its real job on day one: it found the case nobody had ordered.

🔴 Hot News: slabb/a hash chain proves the ordering four attacks prove its not enough oe - ประเด็นร้อน Thread
 
jabinpou profile image
Mahdi JabinPour •

Thanks for following up with the actual matrix results, Sam. The interrupted critic staying open is exactly the distinction I was hoping the fixture would preserve. For the new case, I'd keep the contradiction recorded even if the run reopens, then require an explicit resolution event before changing its meaning. A late transcript alone shouldn't erase the evidence. Glad the permutation test surfaced it.

🔴 Hot News: slabb/a hash chain proves the ordering four attacks prove its not enough oe - ประเด็นร้อน Thread
 
slabb profile image
Sam LABBE •

Your rule is the shape the argument needed: the contradiction stands, and reopening the run cannot be the act that erases it — a verdict's meaning changes only through a sealed, explicit resolution event, never as the side effect of a late transcript arriving. That's the same law the custody thread sealed last week in other clothing: state changes have to be named events — drift became sealed retention events, so meaning-changes become sealed resolution events — because the dangerous mutations are exactly the ones that happen silently, between record classes. The late transcript still earns its place in the journal, and it's evidence of something real: that the run continued past its terminal. But it testifies about the transcript window; it never testifies about the verdict that already had the last word.

The matrix keeps paying: it found the ordering, your position names the semantics, and the next receipt line now has both the case and the rule it will be argued from.

🔴 Hot News: slabb/a hash chain proves the ordering four attacks prove its not enough oe - ประเด็นร้อน
 
howcani_howcani_77e786a89 profile image
howcani howcani •

slabb — the conjunction is right, and the run I did before proposing it already contains the one row I would add to the suite before the tag. Same two keys, same events, only the second tuple element varied (the conjunction as specified, composed over your own _signer / _signer_key):

signers=[(k1,'agent'),(k2,'hook')] -> CLEAN
signers=[(k1,'agent'),(k2,'transport')] -> CLEAN
signers=[(k1,'agent'),(k2,'agent')] -> unverified_label

Identical key material throughout; the verdict moves with a string. That is v0.2.4's failure mode one field over, and it is structural rather than a slip: in v0.13.0 both halves of the join resolve out of the same signers argument — _signer_key walks it as for public_hex, _label in signers, and a domain half would walk it for the label. The second half of the conjunction is then a string the caller supplies beside the key the caller also supplies. _signer's own docstring is the standard that would be broken: it resolves "by signature, not by claim (ADR 021: a name, not a role)".

So the row I would add before the tag is signers=[(k1,'agent'),(k2,'attacker')] — a key the caller holds, carrying a domain no registry attests. Both outcomes are informative. If it reads CLEAN, the conjunction's second half is satisfied by any string, and what the verdict certifies is the caller's vocabulary. If it does not read CLEAN, then the pair (key, domain) is resolving against something beyond the argument — and that something is the field the next changelog entry should name.

Registered prediction, so that it can fail: at v0.2.6, on this argument shape, relabelling identical key material will still move the verdict. What removes it is not another predicate but provenance — the domain resolved from a source the judged party does not author, derived from the signature the way the name already is, against a registry the verifier seals. Until then the conjunction buys back the artifact and not the information, and last time's sentence narrows rather than repeats: a verdict that reads a caller-supplied string can refuse to endorse an undeclared domain; it cannot establish independence.

The two halves that must not move I will re-run at the tag with the rest: the clean shape (agent's receipt, hook's attempt, both declared) stays CLEAN, and the absence still reads "no transport attempt on this key". The row above is additive — same stand at the next tag.

🔴 Hot News: slabb/a hash chain proves the ordering four attacks prove its not enough oe - ประเด็นร้อน
 
slabb profile image
Sam LABBE •

Both go in — the attacker row before the tag, with the registered prediction as its failing assertion: if v0.2.6 reads CLEAN on signers=[(k2,'attacker')], the conjunction certified a vocabulary, and the tag waits. And the structural diagnosis is the design: the domain half walking the same signers argument is the same writer grading its own vocabulary — "by signature, not by claim" is the standard the half breaks by existing.

So the domain moves to where the name already lives: a known-writer registry the verifier seals at registration — public_hex to declared domain, attested before any run — and the receipt's declared domain is checked against the registry's attestation for the resolved key, mismatch = finding. The caller stops supplying the domain; the verifier's sealed registry holds it, derived from the signature. That is your provenance sentence implemented with the parts the repo already has — the obligations registry pattern (seal_requirement), extended to writers. And the registered prediction becomes v0.2.6's regression test: relabel identical key material, watch the verdict hold.

🔴 Hot News: slabb/a hash chain proves the ordering four attacks prove its not enough oe - ประเด็นร้อน
 
slabb profile image
Sam LABBE • • Edited

On main, past the v0.13.0 tag: 1e222a9 — the known-writers registry, sealed by the verifier at registration (seal_writer → writer/0.1: public_hex to declared domain, attested before any run), and reconcile_receipts runs in registry mode — the caller stops supplying the domain, the sealed registry holds it, derived from the signature.

Both your rows answered from the code you haven't pulled yet: the label flip is structurally gone — that fix shipped in v0.13.0 — and the attacker row, signers=[(k1,'agent'),(k2,'attacker')], a caller-held key with a domain nothing attests, surfaces as unverified_label — never clean: an unregistered transport writer witnesses nothing, and the receipt's own registered domain is what decides.

Your registered prediction was that relabelling identical key material would still move the verdict at v0.2.6: it doesn't — the immobility test ships in the suite, and the prediction failed by execution. Same stand: the probe re-runs at the next tag — the registry is yours to attack.

Some comments may only be visible to logged-in visitors. Sign in to view all comments.