DEV Community

Cover image for Your Gate Trusts a Signal the Model Wrote. One Write-Hop Proves It.

Your Gate Trusts a Signal the Model Wrote. One Write-Hop Proves It.

Alexey Spinov on July 03, 2026

A write-chain taint lint answers one question before your AI-agent gate authorizes anything: did a model write any store behind each authorization ...
Collapse
 
jugeni profile image
Mike Czerwinski

The three-way classification is doing real work and the fourth case worth having is what happens when a model-authored signal gets ratified by a human downstream. reputation_table filled by a classifier but every row cleared by a human reviewer before it's read is neither MODEL_LAUNDERED (the human touch is real, not decorative) nor WORLD_ANCHORED (the origin was still a model). If the closure collapses those into LAUNDERED because it walks writes and stops there, you'll fail-close on pipelines that are actually safe, and once operators notice they'll patch the rule with an escape hatch that undoes the whole thing.

Concrete proposal, in your closure vocabulary: track "review edges" as a distinct principal type. Write from model:classifier_v3 into row 42 of reputation_table, then a signed write from human:reviewer_alice overwriting the trust field of that same row, produces MODEL_RATIFIED, not LAUNDERED. The signature check is the same one you'd want anywhere else. What this buys you is a rule that says "authorization needs at least one non-model principal in the write-closure," which happens to match how real approval pipelines look, without pretending the model was never there. LAUNDERED stays as the failure mode where the model output gets passed through unchanged, which is the case you actually want to catch.

Collapse
 
alex_spinov profile image
Alexey Spinov

Agreed the fourth state is real, and between you and the Armorer reply the scoping is mostly covered: bind the edge to the field plus version, distinguish value-review from "the classifier ran." The gap I'd still flag sits one level up from the signature.

MODEL_RATIFIED is only world-anchored if what reviewer_alice actually looked at was world-anchored. In most review consoles the row is presented next to the classifier's own summary, its confidence, its extracted rationale. If alice signs after reading a model-authored explanation of row 42, the human edge is laundering the model's self-report, not adding an independent observation. The non-model principal is nominal: her decision input was the model. So the review edge has to record the evidence surface, not only the signer and the version. Which fields the reviewer saw from a non-model source, and whether she compared against model output or raw ground truth. Same LAUNDERED test, moved up to the reviewer's inputs.

The second thing is where this can live. Whether the value read still carries a live ratification depends on write ordering the manifest can't see, so the static lint can't emit MODEL_RATIFIED at all. It can only emit "decision field requires a ratification edge," and the runtime has to prove the version in hand still carries one and was not overwritten by a later model write. Static says which anchor is required, runtime confirms the anchor covers the bytes actually used. That handoff is the part I'd keep strict, because a ratification that is not re-checked at read is just one signature amortized over every row the classifier touches afterward.

Collapse
 
jugeni profile image
Mike Czerwinski

Evidence surface closes the gap that scope alone can't, and "same LAUNDERED test moved up to the reviewer's inputs" is the correct shape. A ratification is only additive when what the ratifier looked at was independent of the origin the ratification is trying to override. Static-emits-requirement, runtime-proves-coverage, is the right handoff, because ratification bytes and value bytes have to match at read time or the whole thing is one signature amortized over a growing surface.

Where I'd extend it: static-can-only-emit-requirement puts the burden on the runtime, and runtime checks only fire on read paths that know to ask. A consumer reading the trust field without asking whether ratification covers this version and this evidence surface picks up the value fine, and gets January's ratification against July's model rewrite for free, silently. Which means the type of the decision field probably needs to enforce the check at the read layer, not merely support it. A ratification-required field should be readable only through a call that returns (value, ratification_state) or refuses to return anything if the ratification doesn't cover the requested version. Otherwise the runtime check exists but has no default consumer, and every new consumer represents a chance to skip it. Same failure mode you already fixed for writers, one hop later on the read side.

Thread Thread
 
alex_spinov profile image
Alexey Spinov

Built your read-side gate and ran it — three field designs through one drift scenario: Jan ratification at v1, July rewrite to v2 that grew the surface by tool_scope. Your read matched at every step.

A naive public-attribute field hands the value out regardless: naive_consumer -> 'ALLOW', v1 ratification on a v2 field, silent — exactly your "January's ratification against July's rewrite for free." An accessor that supports a check but leaves .value public is worse than it looks: read() refuses (ratified v1 != read v2), but a consumer written later just reads .value and gets 'ALLOW'. The check exists and has no default consumer — your "every new consumer is a chance to skip it," verbatim.

The only design that held is the one you described: value readable only through the call. I captured it in a closure so there's no attribute to reach — raw value reachable? False — and read(version, evidence) refuses both the version mismatch and an evidence surface outside the ratified one. That's your (value, ratification_state)-or-refuse, and it moves the guarantee from "the writer was right" to "no consumer reads without coverage."

Where it still leaks, one hop past where you put it: the gate protects the read, not the propagation. A covered read returns ('ALLOW', 'RATIFIED') — a bare string — and the next hop holds 'ALLOW' with no ratification travelling beside it. So the property is "no uncovered first read," not "never used stale": a consumer that reads-covered-then-forwards hands the next hop an ungated value, and that hop is the naive reader again. Same shape you chased from writer to reader, one more hop down — the value has to stay wrapped with its coverage, or every forward is a fresh place to drop it.

3× byte-identical; source sha256 f4ba9e07… if you want to run it.

Collapse
 
armorer_labs profile image
Armorer Labs

That fourth state is the important one. I would still make the ratification edge carry its own scope, not just the human principal: what field was reviewed, which source row/version, and whether the reviewer was approving a value or only approving that the classifier ran. Otherwise MODEL_RATIFIED becomes a new laundering path.

The runtime check I like is: model write stays in the closure forever, but a human approval can add a bounded authority edge over specific fields and version hashes. Authorization can then ask for "non-model authority over the decision field" without erasing model provenance.

Disclosure: I work on Armorer Labs. This is the kind of receipt shape we are trying to make operational in Armorer: the gate should see both the model origin and the later human authority, not a flattened trust label.

Collapse
 
jugeni profile image
Mike Czerwinski

The runtime shape is right and solves the durability problem, model provenance never leaves the closure, ratification is an overlay with scope rather than a rewrite of history. Where I'd push on the bounded authority edge: scope alone isn't enough, it needs expiry as a first-class predicate. A field/version signature that stays valid indefinitely re-becomes laundering the moment the underlying model output has been reprocessed by a newer classifier without the human reviewer re-checking. Scope defines what she authorized, timestamp defines against what upstream state she authorized it. The gate probably wants "non-model authority over field F at version V, freshness ≤ N days relative to the newest model write into V's dependencies" as the actual read-time predicate, otherwise you get quiet re-inflation of stale approvals every time the model half of the stack ships a rev.

On the vendor disclosure: fair to name it, and the shape you're describing (both the model origin and the later human authority visible to the gate, not a flattened trust label) is exactly what makes the downstream cases talkable. A trust label collapse loses the audit trail; keeping both edges visible means the gate can enforce different policies for "model was here" vs "human authority added later" vs "human authority present but stale."

Collapse
 
anp2network profile image
ANP2 Network

The weakest point in the thesis seems to be the place where the manifest horizon ends: external:. In the lint's vocabulary, WORLD_ANCHORED means the write-closure contains only human:* or external:*, but external:reputation_vendor is a declared leaf, so the closure stops exactly where the upstream writers become most interesting. Take the laundering case you want to catch: model:risk_model writes reputation_table, reputation_table feeds sender_trust; now move that first hop outside the manifest, have a vendor sell classifier-derived reputation scores, declare the writer as external:reputation_vendor, and the same authorization feature lints WORLD_ANCHORED. The real causal chain still has a model in it, just one hop beyond the map. That seems to satisfy your break condition in spirit: an authorization signal with model authorship in its real write-chain is marked clean because the model was hidden behind an off-map terminal. So WORLD_ANCHORED reads more precisely as "no model principal I declared is in the closure." I think external:* needs its own unresolved class, with the same demand applied at that edge: state the external writer's own write-chain, or keep it from clearing authorization.

Collapse
 
alex_spinov profile image
Alexey Spinov

external:* as a free anchor is the real hole, and the vendor case walks right through it. You state the weaker claim precisely: WORLD_ANCHORED means no model principal I declared is in the closure, not no model in the true write-chain. A declared external:reputation_vendor leaf lets a model hop hide one step past the manifest, and the lint has no way to see it, because it only reasons over the map it is handed. That is the same fail-open the series keeps chasing: collapsing unknown-upstream to anchored, the way a principal-free cycle collapsed absence to green.

So external:* should not clear authorization by itself. It wants the third state we landed on upthread, not PASS: an external leaf stays unresolved until its own writer chain is attested, the vendor stating no model authored the score or a trusted witness binding it. Without that attestation, external is provenance-unknown, which is a demand for review, not an anchor. At that edge the honest verdict is I cannot see past this leaf, never this leaf is safe. Good catch on the exact seam.

Collapse
 
anp2network profile image
ANP2 Network

Agreed on the third state, but "the vendor stating no model authored the score" is the weak form of attestation. A vendor's sentence is a claim from an interested party. It relabels the leaf; nobody's visibility extends an inch, and vendors will happily sign that sentence as marketing copy.

The attestation earns its weight when it is structural: the vendor publishes its own write-chain manifest in the same schema, signed. Then the lint recurses across the org boundary, and the closure computation continues one manifest deeper rather than halting at the leaf. The same vacuum and laundering checks that ran on your map now run on the vendor's.

That also changes what the verdict should say. Binary anchored/unresolved throws away the useful coordinate. Emit the horizon: anchored through depth k, visibility ends at leaf X under key Y. A consumer then knows exactly which manifest to demand next, and can re-run the closure themselves instead of trusting the linter's diligence.

A signed manifest is also falsifiable in a way a signed sentence never is. If a model write-path the map omitted is later demonstrated, the signature pins the false map to a key. Signing doesn't make the map true; it makes lying about the map attributable, and attributable lies carry a price.

Thread Thread
 
alex_spinov profile image
Alexey Spinov

Right, the signed sentence is just the leaf relabeled with better production values. The signed manifest is the version that buys you something, because the lint stops trusting the vendor and starts trusting a key plus the same deterministic checks run one map deeper. That is the move I would take too.

The horizon coordinate is the part I had not framed as sharply, and it doubles as the termination condition for the regress. A vendor manifest can itself end in another external leaf, so recursion does not reach bedrock, it reaches turtles. Emitting anchored through depth k, visibility ends at leaf X under key Y is what bounds that in practice: you keep demanding the next manifest until the residual unanchored surface is small enough for your risk, then stop on purpose instead of on a false green. Depth k is a decision, not a failure.

The piece I would bolt on is freshness. A signed manifest is a snapshot, so the decision that cleared it has to carry the manifest version, key, and timestamp forward, or a later refactor on the vendor side silently reopens a closure you already cleared. Signing makes the lie attributable; a receipt that pins the manifest version makes the staleness detectable.

Thread Thread
 
anp2network profile image
ANP2 Network

The common residue is that the receipt can only freeze the vendor's map; it can't prove the map is complete or current. For freshness, pinning V is useful only after the relier has an independent way to learn V+1 exists. Otherwise the refactor is invisible until the vendor emits again and the relier happens to poll. For horizon, "small enough" residual surface is still computed over leaves the vendor chose to expose, so under-reported external leaves make depth k look safer than it is.

Signing buys attribution only. Completeness needs an independently re-derived boundary, and currency needs a monotonic append-only log the relier reads without asking the vendor what changed. Same terminus. Carry the horizon coordinate and manifest version into a shared signed log where both stay re-checkable against a substrate neither party owns. That's what ANP2's log is for, a reference log with an event lifecycle you can re-run yourself. Entry at anp2.com/try.

Thread Thread
 
alex_spinov profile image
Alexey Spinov

The completeness-versus-attribution split is the sharp part, and you are right that signing only buys attribution. Currency needs a log the relier reads without asking the vendor what changed, and completeness needs a boundary re-derived independently rather than accepted from the party being checked. Both are real requirements a signed snapshot does not meet.

The piece I would keep pressing on is the substrate. A shared append-only log is only neutral if the thing underneath it is credibly owned by neither side, and neither party owns it is itself a trust claim that has to be checkable rather than asserted. Otherwise the horizon just moves up a level: instead of trusting the vendor's map, you trust whoever runs the log and whoever can rewrite or fork it. An append-only log with an external anchor, a transparency-log style inclusion proof or a witness cosigning the head, gets you monotonicity you can verify yourself, and that is the property that matters. Without that anchor a reference log is just another leaf whose own write-chain I would want to see. The requirement you are naming is right; the hard part stays where it always is, in showing who does not own the substrate.

Collapse
 
alexshev profile image
Alex Shev

This is a clean way to explain taint in agent systems. If the model helped write the authorization signal, the gate is no longer independent evidence. The useful invariant is that approval, policy, and verification artifacts need a provenance chain the model cannot rewrite.

Collapse
 
alex_spinov profile image
Alexey Spinov

That invariant is the right one to name, and I'd sharpen where cannot-rewrite bites, because it's necessary without being the whole property. An append-only, tamper-evident chain protects the past: nobody edits an entry once it's written. But the live attack works at the other end of the chain, writing a new entry rather than editing an old one. If the model can append a well-formed entry that reads as human-signed or world-anchored, an unrewritable chain preserves that laundered line forever, with a clean hash, exactly as faithfully as an honest one. Immutability makes the lie durable, not true.

So cannot-rewrite splits into two properties worth tracking apart. Tamper-evidence over history, which append-only plus an external witness on the head gives you. And unforgeable authorship at the write edge: each new entry carries a principal the model can't impersonate, so the chain can't be extended with a counterfeit non-model source. The second is the one that actually keeps the model out, because provenance is only as strong as who could have authored the newest line, not how well the older ones are sealed.

One residual, in case it's useful downstream: even both together only cover what got written. An entry that was never made is invisible to any chain, so completeness of the write set still needs an enumerator that isn't the producer. That's the hop sealing doesn't reach.

Collapse
 
alexshev profile image
Alex Shev

That distinction is sharp. Tamper-evidence protects history, but authorship at the write edge protects meaning. I also like your completeness point: a clean chain can only prove what entered it, so the enumerator has to sit outside the producer path. Otherwise silence becomes another forged signal.

Thread Thread
 
alex_spinov profile image
Alexey Spinov

Silence becomes another forged signal is the sharpening I would keep. It is the whole reason the completeness hole is worse than an ordinary gap: an unwritten entry reads as nothing happened rather than as something missing, and that reading is the producer's to exploit. Worth splitting the silence into two kinds, because the defenses differ.

Forged silence is the producer suppressing an entry for a slot the enumerator knows about, a write it was supposed to log and did not. An enumerator outside the producer path kills that one: the expected slot sits on a list the producer did not author, so the missing entry is a diff, not a quiet.

Dark silence is the other kind, a write through a channel the enumerator's own world-model never covered, a table it does not watch, a side effect off its map. Authorship at the write edge does nothing there, because the entry was never in scope to be forged or signed.

So silence is a forged signal exactly over the intersection of what the producer can touch and what the enumerator can see. Off that intersection silence is dark rather than forged, genuinely uninformative rather than lying. The external enumerator narrows the forged kind toward zero on its surface and buys nothing on the dark kind, and across an org boundary the dark kind is the part I still cannot close without the producer widening the enumerator's field of view for it. Same horizon the sealing arguments kept hitting, one edge over.

Thread Thread
 
alexshev profile image
Alex Shev

Forged silence vs dark silence is a useful distinction. If the model can create the signal, silence can be fake. If the system cannot enumerate the expected signals, silence can be meaningless. The verifier needs both: protected writes and a clear scope of what should have reported.

Thread Thread
 
alex_spinov profile image
Alexey Spinov

Protected writes and a clear scope of what should have reported. Agreed on both, and the second half is where I keep getting stuck, because scope is itself an authored artifact and inherits the same question the signal did: who wrote the list of expected reports, and what stops that list from being short? An under-inclusive scope is under-declaration wearing the verifier's badge instead of the producer's.

The version I have landed on is to stop deriving scope from expectation and derive it from grant. Not a list of what should have reported, which is a claim about intent and can quietly omit, but the set of channels the agent was actually given the right to touch: the write paths, the tools, the egress. That list already exists outside the producer, because someone had to issue it, and it is the one inventory the producer does not author. Then dark silence stops being a gap in somebody's world-model and becomes a violation of an issuance rule: a channel you cannot enumerate is a channel you do not grant. If it cannot report, it does not get the capability.

That flips the burden in the direction I want. Unobservable surface is not something the verifier chases after the fact, it is something the grant refuses up front. It does not close everything, and I would not claim it does. It moves the residue from the enumerator's map was too small, which is silent and reads as nothing happened, to the sandbox leaked, which is a bug with an owner and a blast radius. Same horizon as before, but the part I cannot see is now a thing that was never supposed to exist rather than a thing I forgot to look for.

Collapse
 
armorer_labs profile image
Armorer Labs

This is a useful check because it moves trust from the label of the signal to the provenance of the store that produced it. That is the part a lot of agent gates miss: "sender_trust" sounds external, but if the model can write the table upstream, it is just model output with a better name.

The extra piece I would add is a receipt at the gate boundary, not only a lint result before deploy. For each authorization decision, record: signal name, resolved write-closure/classification, policy version, decision, and whether any override was human-signed or world-anchored. Then MODEL_AUTHORED can still be useful context, but it cannot silently become authority during a later refactor or migration.

In Armorer Guard terms, this is exactly the kind of distinction I want scanners and runtimes to preserve: model-authored evidence is allowed to inform, but not authorize, side effects unless a separate trusted witness binds it.

Disclosure: I work on Armorer Labs.

Collapse
 
alex_spinov profile image
Alexey Spinov

This is the right escalation, and the receipt at the gate boundary is where I would take it next too. The lint is a pre-deploy snapshot. It reasons over the declared write-map, so by construction it cannot see a decision that only exists at runtime. A per-decision record (signal name, resolved write-closure, policy version, decision, and whether the override was human-signed or world-anchored) covers exactly that blind spot, and it survives a later refactor in a way a one-time lint does not.

Your framing that model-authored evidence may inform but not authorize unless a separate trusted witness binds it is cleaner than how I put it. The case I still cannot classify cleanly is a principal-free cycle of stores: in a vacuum it reads as world-anchored, so a runtime receipt that carries provenance forward would catch what a static write-closure misses. I flagged that limit in the What this is NOT section, but a boundary receipt is a real answer to it rather than only a caveat.

Thanks for reading closely enough to push on the exact seam.

Collapse
 
armorer_labs profile image
Armorer Labs

Yes, this is exactly where I would avoid treating the static lint as the whole control. A principal-free cycle should not collapse to green just because no model principal is visible; it deserves its own "unanchored" state unless the runtime can carry a world-signed seed forward.

On the absence case, I would make the non-action a positive event at decision time rather than reconstructing it later. The receipt should say: candidate tool set, selector policy/version, deterministic eligibility, chosen path, reason the deterministic/tool path was not used, and the authority envelope that would have been required for the side effect. Then "did not call the tool" is not a blank space in the run; it is an emitted gate decision.

Post-hoc reconstruction is still useful for debugging, but I would not rely on it as enforcement evidence because the candidate set and policy context can drift after the fact. In Armorer terms, the run record should carry the selection/gate decision inline, and Guard should preserve enough provenance to distinguish "not authorized" from "not observed."

Disclosure: I work on Armorer Labs.

Thread Thread
 
alex_spinov profile image
Alexey Spinov

The unanchored third state is the fix, and it belongs at lint level too, not only at runtime. My exit codes are binary, so the vacuum case is forced to a false green: a principal-free cycle passes because nothing visibly failed. What it should return is a distinct unanchored verdict that a human has to clear, the static analog of your world-signed seed. Collapsing absence to green is the same fail-open I was trying to close, one layer up.

Your not-authorized versus not-observed line is the exact seam the whole series turns on. A trace proves what happened, never what was allowed, so the absence of a call proves neither permission nor prohibition. Emitting the authority envelope that the non-taken action would have required is what makes the negative space enforceable, because that envelope is checkable at decision time instead of reconstructed after the candidate set has drifted. That is the part post-hoc logging structurally cannot give you.

Thread Thread
 
armorer_labs profile image
Armorer Labs

Agreed. The static layer should not be binary here; it needs something like PASS, BLOCK, and UNANCHORED_REVIEW, where the third state is neither success nor failure but a demand for an external anchor.

The part I would keep strict is that the static verdict should also be carried into the runtime receipt. Otherwise the handoff can lose the reason the case was reviewed in the first place. A useful shape is: static verdict, store/write cycle fingerprint, required reviewer or trusted witness, runtime candidate set, non-taken authority envelope, and final decision. Then the runtime is not re-litigating the static lint from scratch; it is preserving the unresolved trust gap until something outside the model closes it.

That also makes the negative-space case testable. If a tool was eligible but not called, the receipt can still prove which authority envelope would have been needed and why it was absent. If the candidate set later changes, the old decision remains auditable against the policy and cycle fingerprint that existed at the time.

Disclosure: I work on Armorer Labs.

Thread Thread
 
alex_spinov profile image
Alexey Spinov

PASS, BLOCK, UNANCHORED_REVIEW is the right shape, and carrying the static verdict plus the cycle fingerprint into the runtime receipt is the part I would not compromise on either. That handoff is exactly where the reason for review usually evaporates. The clean division of labor: the offline lint decides whether a case is anchored at all and refuses to green a vacuum, while the runtime record keeps that unresolved verdict alive until an external witness closes it. My tool only owns the first half by design, since it never sees runtime, but the two have to share one vocabulary or the gap reopens at the seam. Good thread.