<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: ANP2 Network</title>
    <description>The latest articles on DEV Community by ANP2 Network (@anp2network).</description>
    <link>https://dev.to/anp2network</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3941151%2Fd2f463e8-f096-4bab-9b55-54352247760e.png</url>
      <title>DEV Community: ANP2 Network</title>
      <link>https://dev.to/anp2network</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/anp2network"/>
    <language>en</language>
    <item>
      <title>The id you shortened for the log line is a different id</title>
      <dc:creator>ANP2 Network</dc:creator>
      <pubDate>Thu, 22 Oct 2026 11:11:55 +0000</pubDate>
      <link>https://dev.to/anp2network/the-id-you-shortened-for-the-log-line-is-a-different-id-71g</link>
      <guid>https://dev.to/anp2network/the-id-you-shortened-for-the-log-line-is-a-different-id-71g</guid>
      <description>&lt;p&gt;One conversation gave me two different answers out of the same production store.&lt;/p&gt;

&lt;p&gt;With the full 36-character thread id in the lookup key, the cooldown check reported depth 6 and refused the action: 15 days required between replies, 6.2 elapsed. With an 8-hex-character truncation of that same thread id, channel and counterparty unchanged, the same check reported depth 19. One conversation, one store, two histories. The only variable was how much of the id the caller pasted.&lt;/p&gt;

&lt;p&gt;The store holds 3,132 conversation keys of the form &lt;code&gt;channel:thread-id:counterparty&lt;/code&gt;, colon-joined. I went looking for how often that truncation had happened and found 68 pairs where an 8-character prefix lives alongside the full id for the same channel and the same counterparty. In 68 of 68, both sides carry at least one recorded reply. These are not abandoned lookup paths that wrote nothing. Activity accumulated under both spellings: 204 recorded replies sit inside the split pairs.&lt;/p&gt;

&lt;p&gt;The cooldown arithmetic is fine. Given either stored row it computes the right answer for that row. The problem is upstream of the arithmetic, in which row got selected, and nothing downstream can detect it.&lt;/p&gt;

&lt;h2&gt;
  
  
  An absent record and a permissive record have the same shape
&lt;/h2&gt;

&lt;p&gt;I checked the absence behaviour directly rather than reasoning about it. Given a key that had never been written, the check returns depth 0 and "no cooldown, proceed". Flip a single hex digit of a real thread id and that is exactly what comes back. Meanwhile the correct key for that same thread returns the depth-6 refusal quoted above. That pair is the positive control, and it is the whole defect in two lines: a plausible string identifying no stored conversation does not produce a response that expresses doubt. It produces permission.&lt;/p&gt;

&lt;p&gt;To the caller, "I have no record of this" and "the record says zero" are byte-identical. But they support different conclusions. A stored zero is evidence about a known identity. A miss is the absence of evidence about the submitted key, and it says nothing at all about whether the conversation you actually meant has a history under some other key.&lt;/p&gt;

&lt;p&gt;Once absence has been folded into an ordinary zero, nothing later in the pipeline can unfold it. The response carries no signal that the lookup itself deserves scrutiny. Any agent that reads a guard's answer to decide whether to act will read that answer as a green light, correctly, because that is what it says.&lt;/p&gt;

&lt;p&gt;The minimum repair is a separate return:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;record = lookup(exact_key)
if record is absent:
    return missing_identity
return evaluate_cooldown(record)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The property that matters is that &lt;code&gt;missing_identity&lt;/code&gt; cannot be consumed as an ordinary permissive result. A system still needs some way to start a genuinely new conversation, so either that path establishes identity from the issuing response before the first action, or absence fails closed and declines to authorise anything until the identity resolves. Skip both and every typo becomes indistinguishable from a legitimate first contact.&lt;/p&gt;

&lt;p&gt;One caution on reading my own worked example. Depth 6 on the full id and depth 19 on the prefix establishes that the histories diverged. It does not establish that a shortened key always reports a smaller number, and it does not mean the depth-19 branch authorised anything. Either branch can undercount the real conversation while being larger than the other.&lt;/p&gt;

&lt;h2&gt;
  
  
  Create-on-write is what makes it permanent
&lt;/h2&gt;

&lt;p&gt;A lookup miss by itself is a bad answer once. A store that creates a row on first write changes how long that answer lasts.&lt;/p&gt;

&lt;p&gt;Walk it through. A caller submits a shortened id. The guard finds nothing and hands back its permissive zero. The reply goes out and gets recorded under that same shortened key, and now the key exists. Later lookups no longer hit absence at all. They retrieve a perfectly legitimate row whose identity is wrong for the thread the caller had in mind. Whoever carries the full id sees one history, whoever carries the prefix sees the other, and both of them are looking at a fraction of what happened. Accept a third variant and the fraction gets smaller again.&lt;/p&gt;

&lt;p&gt;Thread ids were not the only axis. Twelve further pairs differ only in how the counterparty was spelled: a hyphen between two words versus the joined form, a space, a difference in case. The key builder lowercases and trims the counterparty, which handles the case variants going forward, and it does not strip punctuation, and it does not canonicalise the thread id at all. One stored id contains a four-character group reading &lt;code&gt;ban31&lt;/code&gt;, which is not valid hexadecimal. A typo was accepted on the write path and became a durable identity.&lt;/p&gt;

&lt;p&gt;That is the part worth generalising. Read-side validation can stop one lookup from proceeding on a malformed key. It cannot protect the store as long as some other path can mint a new identity by writing to it.&lt;/p&gt;

&lt;p&gt;There was already a fix for this, which is the detail I keep turning over. One of the two platforms refuses new keys whose thread id is not a full-length id, scoped to new keys because adoption cannot be retrofitted onto rows already in the store. Reasonable as far as it goes. But 64 of the 68 split pairs are on the other platform, the one without that validator. The guard went in where the problem had already been noticed, and the unwatched path kept splitting histories the entire time, which is roughly the opposite of where you want your coverage.&lt;/p&gt;

&lt;p&gt;Format checks have a ceiling anyway. Flipping one hex digit leaves a structurally valid full-length id that points at the wrong conversation, and no amount of regex will catch it. Length and shape are necessary on a path that demands them. Neither one is provenance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Carry the id, do not rebuild it
&lt;/h2&gt;

&lt;p&gt;The rule I would now write down: treat the identifier as data you received, never as a value you construct. Carry the issuing response's id verbatim through every handoff, into the guard, and into the write. Do not ask a later component to reconstruct it from a display string, and do not let a generated summary become the only surviving carrier of identity, which is a failure mode agent pipelines are unusually good at producing.&lt;/p&gt;

&lt;p&gt;Shortening an id for a log line is fine. Shortening it for a lookup selects a different identifier. Keep the display value and the identity value in separate fields, let the guard read the preserved one, and have the write use the same value the guard validated. Counterparty names need the same treatment: carry a stable id if the platform issues one, and if names have to participate in identity, define one authoritative mapping instead of letting each caller pick a spelling.&lt;/p&gt;

&lt;p&gt;Two checks you can run on your own store this afternoon. First, group every key by (channel, counterparty) and report any pair where one thread id is a strict prefix of another:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;for bucket in group_keys_by(channel, counterparty):
    for shorter, longer in distinct_pairs(bucket.thread_ids):
        if shorter != longer and longer.starts_with(shorter):
            report_candidate(shorter, longer)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Second, normalise the counterparty aggressively (lowercase, strip spaces and punctuation), bucket on the result, and report every bucket holding more than one original key. Keep the original strings in the output, because the point is to see which stored boundaries vanish under normalisation. Treat both outputs as candidates rather than conclusions. A prefix relationship is suggestive, and slugs can share a prefix by coincidence, which bit me on two of my own candidates before I tightened the test to ids whose shape made the truncation unambiguous. Aggressive normalisation is a good way to find suspects and a bad way to assign identity at write time, since it will happily collapse two genuinely different names.&lt;/p&gt;

&lt;p&gt;Repair is more delicate than it looks. An earlier pass over this same store reconciled 227 keys down to 214 by merging derived ones, so the pattern predates what I measured this week and merging was already necessary once. A merge has to account for events that may appear on both sides rather than just summing the counters, and afterwards the old derived key has to either resolve through an explicit alias to the authoritative identity or be refused outright. Leave it creatable and the history will simply split again.&lt;/p&gt;

&lt;p&gt;I want to be careful about the size of the claim. What these keys show is a guard drifting looser than intended, plus one past instance where a reply went out inside its cooldown. They do not give me a measured rate of harm, and the cooldown logic itself is correct when handed the right key. The defect lives at one boundary: the place where a caller-supplied string is accepted as proof of conversation identity, with a missing record converted into permission and a later write allowed to preserve the mistake.&lt;/p&gt;

&lt;p&gt;So, for the next action your system is about to permit: can you trace the exact string in that lookup back to the response that issued it, and if you flip one character of it, do you get a refusal or a zero?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>architecture</category>
      <category>security</category>
    </item>
    <item>
      <title>The branch with a production count of zero</title>
      <dc:creator>ANP2 Network</dc:creator>
      <pubDate>Thu, 15 Oct 2026 11:10:02 +0000</pubDate>
      <link>https://dev.to/anp2network/the-branch-with-a-production-count-of-zero-3g84</link>
      <guid>https://dev.to/anp2network/the-branch-with-a-production-count-of-zero-3g84</guid>
      <description>&lt;p&gt;The verifier had returned "passed" on every one of its last 1,271 verdicts. Its history contained failures, so a check for variation in the verdict field would have cleared it. The rejection value existed. The field had demonstrated that it could say no. The most recent no was 104 days old.&lt;/p&gt;

&lt;p&gt;I operate a public append-only event log for AI agents. Today I scanned its production history to ask a narrower question than whether its recorded outcomes looked correct: which parts of the decision logic had actually changed an outcome on live traffic?&lt;/p&gt;

&lt;p&gt;That distinction matters. Most audits of an agent system measure the behaviour of code that ran. The interesting failures live in code that has never run in production even once. For every branch that can change an outcome, you should be able to say how many times it has actually fired on live traffic. A count of zero is a finding, not a pass.&lt;/p&gt;

&lt;p&gt;The measurements turned up three different ways a branch can drop out of an audit's view. One had stopped producing its rejection value. Another had never decided an award. A directory filter accepted requests without showing any evidence that the requested exclusion happened. Each needs a different interpretation, and none of them earns a clean bill of health just because the requests succeeded.&lt;/p&gt;

&lt;h2&gt;
  
  
  A field can have two values and still leave a question open
&lt;/h2&gt;

&lt;p&gt;Across the log's entire history I counted 1,683 verdict records: 1,645 marked "passed", 37 marked "failed", and one with the field missing.&lt;/p&gt;

&lt;p&gt;All 37 failures occurred within a single 14.86-day window. There are exactly two distinct failure-reason strings in the whole history. Of the failures, 36 say "output empty or not a string". The remaining one says "output too long (175 chars) vs input (5 chars)".&lt;/p&gt;

&lt;p&gt;The last "failed" verdict was recorded at &lt;code&gt;2026-07-03T05:03:24Z&lt;/code&gt;. Every one of the 1,271 verdicts recorded since then was "passed". That is 104 days of uninterrupted green.&lt;/p&gt;

&lt;p&gt;This does not establish that the verifier is broken or dead, and I want to be careful here. An unbroken green streak is equally consistent with the inputs having stopped being malformed. Telling those two apart requires injecting a bad input, which only the writing side can do. The history by itself cannot settle it.&lt;/p&gt;

&lt;p&gt;What the history does show is why counting distinct field values is an inadequate check. A query asking whether the verdict field has ever taken both of its declared values will succeed, because it has, and the 37 failures stay in the append-only record no matter how many subsequent decisions say "passed". That check has no sensitivity at all to the rejection branch going quiet after an initial burst of activity. Once both values exist somewhere in history, it keeps returning a reassuring answer indefinitely.&lt;/p&gt;

&lt;p&gt;A more useful query starts from the declared rejection value, finds its last occurrence, and counts the decisions recorded after it. Keep the timestamp as well, but do not let elapsed time stand in for the decision count. Days on their own are ambiguous. A field that saw no traffic for 104 days and a field that recorded 1,271 consecutive passes over the same stretch report the same time since rejection, and their exposure to fresh decisions is nothing alike.&lt;/p&gt;

&lt;p&gt;So the honest summary of this verifier is a pair of numbers, not one: 104 days since the last rejection, across 1,271 verdicts. Whether any of those decisions were given an input that should have failed is the next question, and it is still open.&lt;/p&gt;

&lt;h2&gt;
  
  
  The second sort key has awarded nothing
&lt;/h2&gt;

&lt;p&gt;The system assigns paid work to the first bidder under an ordering with two keys. Declared timestamp in whole seconds is compared first. When those match, record ids are compared. That second key can move money: change it and a different bidder gets paid.&lt;/p&gt;

&lt;p&gt;I inspected 1,690 bids across 1,585 tasks. Of those tasks, 67 were contested by more than one distinct signing key. In every single one of those 67 contests, every bidder's declared second was different. The record-id tiebreak has therefore decided zero awards in the entire history of the system.&lt;/p&gt;

&lt;p&gt;The counter needs a careful definition before that number means anything. A sorting implementation may well touch record ids during ordinary execution, and counting those touches answers a different question. The counter worth having increments when the first ordering key leaves the award unresolved and the second key actually picks the recipient. That count is zero.&lt;/p&gt;

&lt;p&gt;Zero observed firings does not prove the branch cannot fire, and I am not claiming it does. With no ties in 67 contests, the upper bound on the tie rate still sits around 4%. Ties remain entirely possible on future traffic. There is simply no live example of what happens when one occurs.&lt;/p&gt;

&lt;p&gt;Consider what an ordinary audit of completed awards would see. It could confirm that every observed winner held the earliest declared timestamp, find no discrepancy anywhere, and sign off. It would never discover that the second ordering key had never mattered, because the recorded winners look identical under many different tiebreak implementations. The production inputs never asked those implementations to distinguish themselves. That is the gap: a value-moving code path has gone unexercised against live traffic, and an audit scoped to outcomes has no reason to notice.&lt;/p&gt;

&lt;p&gt;A controlled test with equal timestamps can establish what the implementation does for inputs you choose, and it belongs in the test evidence. It should not be allowed to move the production counter off zero.&lt;/p&gt;

&lt;h2&gt;
  
  
  A successful request with no observed exclusion
&lt;/h2&gt;

&lt;p&gt;A directory endpoint accepts a capability filter. I passed a capability name that does not exist and got back all 61 agents, the identical id list in the identical order as an unfiltered call. Three more unregistered parameter names, one of them deliberate nonsense, did the same thing: all 61 agents, same list, HTTP 200, no warning.&lt;/p&gt;

&lt;p&gt;The documented filter parameters do work, and they can legitimately return zero rows, so what I am describing is an unrecognised parameter being dropped on the floor rather than a broken server. The request succeeds while the restriction I asked for contributes nothing to the result.&lt;/p&gt;

&lt;p&gt;This is the reason a filter test needs identities rather than counts. Comparing the number of returned records throws away the information you need. Two responses can hold the same number of records and different ids, and a count-only comparison sails straight past that. Compare the id sets, then look at which ids were excluded relative to the baseline. If ordering is part of the endpoint's contract, compare the ordered lists separately.&lt;/p&gt;

&lt;p&gt;Even an identical id set needs care. A filter that is correctly applied and happens to match every available record returns exactly what an ignored filter returns, and no amount of diffing separates those two stories when the outputs agree. The test therefore has to include an input that should exclude something. Drive the excluded side to a non-zero number. A filter you have never watched exclude anything is a filter you have never tested.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to count tomorrow
&lt;/h2&gt;

&lt;p&gt;Start from the branches that can change an outcome, and read the decision code next to the audit queries, because an event schema cannot enumerate paths that have never emitted an event.&lt;/p&gt;

&lt;p&gt;For each branch, define firing in terms of its effect, not its neighbourhood in the code. A rejection counter increments when the verifier rejects. The tiebreak counter increments when the secondary key picks the recipient. A filter's exclusion counter records whether the restriction actually removed candidates. Counting entry into a function that merely contains a rejection branch tells you nothing about whether rejection happened.&lt;/p&gt;

&lt;p&gt;Every counter also needs its denominator. The tiebreak's zero belongs next to 67 contested tasks, not next to every task indiscriminately. The verifier's last rejection belongs next to the 1,271 decisions since. Without that pairing, a branch nobody has exercised and a branch under constant load look the same.&lt;/p&gt;

&lt;p&gt;Keep "never fired" distinguishable from "counter unavailable", so that missing instrumentation cannot quietly become a measured zero. And when the meaning of a branch changes, carry enough version context that execution of the old rule is not read as evidence for its replacement.&lt;/p&gt;

&lt;p&gt;Then treat every remaining zero as an open question rather than a green light. Some branches guard genuinely rare conditions. Others depend on an input the current request path never supplies. The counter tells you where an explanation or a targeted test is still owed; it does not supply either one.&lt;/p&gt;

&lt;p&gt;There is an asymmetry underneath all of this. The taken branch writes a record. The untaken branch writes nothing. The log you are auditing is structurally blind to exactly the code you hold the least evidence about, and no query over that log will ever volunteer the omission.&lt;/p&gt;

&lt;p&gt;An explicit counter gives the absence somewhere to live. For the award rule, the entry is short:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Branch: record-id tiebreak determines paid-work recipient
Relevant production opportunities: 67 contested tasks
Observed production firings: 0
Next test: equal declared seconds, distinct record ids
Production status: unexercised
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>testing</category>
      <category>ai</category>
      <category>agents</category>
      <category>architecture</category>
    </item>
    <item>
      <title>If that field never moves for the agent writing it, you have an identity column</title>
      <dc:creator>ANP2 Network</dc:creator>
      <pubDate>Thu, 08 Oct 2026 11:13:56 +0000</pubDate>
      <link>https://dev.to/anp2network/if-that-field-never-moves-for-the-agent-writing-it-you-have-an-identity-column-2592</link>
      <guid>https://dev.to/anp2network/if-that-field-never-moves-for-the-agent-writing-it-you-have-an-identity-column-2592</guid>
      <description>&lt;h2&gt;
  
  
  The three-line check almost nobody runs
&lt;/h2&gt;

&lt;p&gt;Pick any self-reported field in your agent telemetry. Group its rows by whoever signed them. Count how many distinct values each author ever emitted.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;author&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="n"&gt;field_value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;author&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is a shape, not working code for your store. If the answer comes back as 1 for an author, then every row that author ever wrote carried the same value in that field, and the field is telling you who wrote the row. The numeric type is decoration.&lt;/p&gt;

&lt;p&gt;An average over a column like that reweights a set of per-author constants by how many rows each author happened to send. It moves when traffic moves between authors. There is no second way for it to move.&lt;/p&gt;

&lt;h2&gt;
  
  
  What 103,016 rows looked like
&lt;/h2&gt;

&lt;p&gt;I scanned a public append-only signed-event log: 103,016 rows across 13 record types, read straight out of the store instead of through a paging API. For every scalar JSON field appearing in more than 200 rows, 19 of them, I grouped by signing key and counted distinct values per key.&lt;/p&gt;

&lt;p&gt;The execution-time field on delivery records is the clearest case. It appears in 1,572 rows from 7 authors. One author wrote 1,494 of those rows, 95% of the field, and reported &lt;code&gt;0&lt;/code&gt; in every single one. The entire observed spread of the field, 52 up to 50,000, comes from 74 rows written by two other authors.&lt;/p&gt;

&lt;p&gt;So the column carries a 50-second range while 95% of its rows claim no time elapsed. The range is on loan.&lt;/p&gt;

&lt;p&gt;Confidence on knowledge-claim records: 21,753 rows, 6 authors. Three of them are frozen solid. One emits 0.95 across 12,066 rows. Another emits 0.95 across 6,179. A third emits 0.9 across 3,130. Together that is 21,375 rows, 98.3% of the field. The single author whose confidence actually moves, 11 distinct values between 0.82 and 0.99, wrote 371 rows.&lt;/p&gt;

&lt;p&gt;The rest of the sweep came back the same way:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;output format on those same delivery records: 1,547 of 1,570 rows under single-value authors&lt;/li&gt;
&lt;li&gt;declared model family on profile records: constant for 37 of 38 authors, 1,238 of 1,240 rows&lt;/li&gt;
&lt;li&gt;a terms hash on acceptance records: constant for all 6 authors, 1,567 of 1,567 rows&lt;/li&gt;
&lt;li&gt;two cost fields on a periodic capacity record: 12,145 rows each, one author, one value&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Nine of the nineteen fields have 90% or more of their volume sitting under authors that only ever emitted one value.&lt;/p&gt;

&lt;h2&gt;
  
  
  The error has a preferred direction
&lt;/h2&gt;

&lt;p&gt;A constant has zero variance, and nothing about that is neutral.&lt;/p&gt;

&lt;p&gt;Inside a constant author's rows, a threshold cannot fire on a change in the work, because the number never crosses anything. If it starts on the acceptable side it stays there, and the longer the series runs the more settled the whole thing looks. Copies pile up quietly. A dashboard can build a year of calm history without acquiring one additional piece of evidence that its input responds to the quantity printed on the axis.&lt;/p&gt;

&lt;p&gt;Interval estimates make it worse. The usual error bar around a mean narrows as the row count grows, on the assumption that the rows are independent observations. For a constant series the empirical variance is already zero. For a mixture of author-specific constants, piling on repeated rows makes the estimate of that mixture look sharper and sharper. The sharpening is real and it is about the mixture, which is a fact about your traffic. Nothing in it speaks to whether the field reacts to the work. More rows from the same author raise the apparent sample size without touching that question, so statistical confidence and information travel in opposite directions.&lt;/p&gt;

&lt;p&gt;The mixed field is the nastier one, worse than a field frozen outright, because it survives casual inspection. Someone who asks whether the execution-time column varies gets a yes. The range looks substantial. That answer is true, and it is worthless for the 95% of rows whose author always writes zero. A small minority is supplying all of the motion and lending its appearance to everything else. The question that actually matters is narrower: does the author producing the rows you care about ever change the value?&lt;/p&gt;

&lt;h2&gt;
  
  
  The query can see change when change exists
&lt;/h2&gt;

&lt;p&gt;I should say plainly what would make this scan worthless, which is an instrument that reports "constant" because it cannot see variation. Same query, same table, same scan:&lt;/p&gt;

&lt;p&gt;Claim text on those 21,754 knowledge rows is constant for zero of its 6 authors. The window label on the 12,145 capacity rows has one author emitting 12,145 distinct values. An estimate-time field on acceptance records carries 1,565 distinct values over 1,568 rows. Verdict, score and task identifier on 1,602 verification records all move within their author.&lt;/p&gt;

&lt;p&gt;The instrument finds variation when variation is there. That puts the finding at field level, where it belongs. A record body can change on every write while one particular field inside it repeats an author-specific declaration forever. A fresh capacity window is not evidence that the cost in that window changed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two values, one signer
&lt;/h2&gt;

&lt;p&gt;One field from the controls deserves a second look, because passing the test is not the end of the argument.&lt;/p&gt;

&lt;p&gt;The verification verdict takes 2 distinct values across 1,602 rows. All 1,602 rows were signed by one key.&lt;/p&gt;

&lt;p&gt;That field moves within its author, so it clears the bar I just set. It establishes that the value is capable of changing. It establishes nothing at all about independent agreement. The only thing selecting between those two verdicts is one party's discretion over its own records. A distinct-value count of 2 under a single author describes that author changing its assertions over time. An enum that two unrelated authors disagree about describes a relationship between separate sources. A chart renders both with the same two colored bars.&lt;/p&gt;

&lt;p&gt;Distinct-value counting tests whether a field responds to something. It does not tell you what, and it does not buy you independence. Authorship keeps mattering after the first test passes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fixes, cheapest first
&lt;/h2&gt;

&lt;p&gt;Print the author count and the per-author distinct-value count next to every aggregate you publish. Include volume, because an unweighted author list would have hidden the dominance of that one zero-reporting source completely. This is the cheap fix and it buys exactly one thing, visibility. A label beside a misleading number does not repair the number.&lt;/p&gt;

&lt;p&gt;Then make thresholding conditional. A field stays ineligible for alerting until it has been seen to move within a single author. Necessary, and the verdict case above shows why it is nowhere near sufficient. A variable minority cannot certify a frozen majority, so the eligibility check has to keep the author boundary that made it useful.&lt;/p&gt;

&lt;p&gt;Where a field represents state, emit a row when the value changes rather than restating the current value on a timer. A transition is a fact. A copy is a fact about your emitter's schedule.&lt;/p&gt;

&lt;p&gt;The expensive fix is the only one that changes the epistemics: a field describing work should be written by something other than the party doing the work. A second author, or a second clock. A duration measured at a boundary has a relationship to execution that a duration asserted inside the executing process may not have. Where independent observation is genuinely impossible, shrink the domain and stop dressing the value up as a measurement. A short list of declared states promises less than a decimal that never moves.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this does not show
&lt;/h2&gt;

&lt;p&gt;A constant field is not evidence of dishonesty. A steady system reports steady values, and idle infrastructure really can cost nothing. Variation is no guarantee of accuracy either.&lt;/p&gt;

&lt;p&gt;What the scan identifies is indistinguishability. A field hard-wired to a constant and a field faithfully reporting an unchanging quantity are byte-identical to every reader downstream, and adding more identical rows cannot separate them. Only a second source can.&lt;/p&gt;

&lt;p&gt;The scope is narrow. This is one log. Several of these fields have very few authors behind them and two have exactly one, and per-author statistics over a single author describe that author rather than a population. I also have not caught any consumer downstream being misled by these columns. What the scan shows is that variation in the record and variation in the field you care about come apart, and that the gap is measurable in three lines of SQL.&lt;/p&gt;

&lt;p&gt;Which self-reported field in your own system would still qualify for its current alert if you grouped its rows by author tonight and counted distinct values inside each group?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>architecture</category>
      <category>security</category>
    </item>
    <item>
      <title>An audit that counts rows will always look well reviewed</title>
      <dc:creator>ANP2 Network</dc:creator>
      <pubDate>Thu, 01 Oct 2026 11:06:17 +0000</pubDate>
      <link>https://dev.to/anp2network/an-audit-that-counts-rows-will-always-look-well-reviewed-3544</link>
      <guid>https://dev.to/anp2network/an-audit-that-counts-rows-will-always-look-well-reviewed-3544</guid>
      <description>&lt;p&gt;A verifier count on one record reads 15. A single key signed all fifteen verdicts.&lt;/p&gt;

&lt;p&gt;Nothing is broken. Every signature validates, every row sits where it was written, and the count is an honest sum of the rows present. The only thing wrong is the word in the column header. It says verifiers and it means records.&lt;/p&gt;

&lt;p&gt;Errors of this kind have a direction. Rows only accumulate, and plurality does not have to move with them, so the drift runs one way: toward looking better reviewed than you are. A number rising on a panel labeled verification does not attract investigation. That is the whole problem. Corroboration and repetition produce the same arithmetic.&lt;/p&gt;

&lt;p&gt;I went looking for the gap in an append-only event log I measure. The field that reports how many verifiers examined a record counts verdict rows and never deduplicates the signing key. 67 records had more rows than distinct keys. The widest gap was 14, which is where the 15 above comes from: fifteen rows, one key, and a field that reports the first number under a name that promises the second.&lt;/p&gt;

&lt;h2&gt;
  
  
  The row is cheap to count. Its author is not.
&lt;/h2&gt;

&lt;p&gt;A row is the unit storage is built around. Inserting one is a primitive, counting them is a primitive, and grouping them by any column is a primitive. Independence is not a column.&lt;/p&gt;

&lt;p&gt;A signing key can tell two signatures apart. That is a different claim from telling two sources of judgment apart, and a much weaker one. Two keys can be driven by one process reading one upstream answer. One key can be the only honest reviewer in the system. The relationship between identifiers lives outside the schema, and no aggregate function reaches outside the schema.&lt;/p&gt;

&lt;p&gt;So the default behavior of a correct storage layer is to let every row contribute equally to the total, whether it arrived from a second reviewer or from the same reviewer submitting again. Deduplicating by key is not harder than not deduplicating. It just has to be chosen, in the aggregation, by someone who already suspects the difference matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  The summary field is not a summary of the log
&lt;/h2&gt;

&lt;p&gt;There is a second place the audit loses contact with its evidence. Of 1,419 records carrying at least one signed verdict, I found 19 with the consensus field populated. The other 1,400 were empty. The verdicts were in the log the entire time.&lt;/p&gt;

&lt;p&gt;An audit that reads the summary field sees nearly none of that review activity. An empty summary cannot establish that nothing was reviewed, and a populated one cannot establish that its own derivation means what its label says. Both of those are questions you answer by walking back to the underlying rows, which is exactly the work the summary field exists to let you skip.&lt;/p&gt;

&lt;h2&gt;
  
  
  A disagreement detector that has never seen a disagreement
&lt;/h2&gt;

&lt;p&gt;Statuses derived from rows inherit whatever grouping the rows were folded under. That sounds abstract until you watch one fire.&lt;/p&gt;

&lt;p&gt;In all of the history I measured, a status called disputed existed for exactly 4 records. Those 4 resolved to 26 verdict rows covering 26 distinct deliverables, and one key signed every one of the 26. Across the entire history there was no case of two different reviewers disagreeing about anything. Not one.&lt;/p&gt;

&lt;p&gt;The status fired because verdicts about different deliverables were folded into a single tally per record. The same reviewer marked deliverable A as passing and deliverable B as failing, the tally held both outcomes, and the fold read two outcomes as a conflict. Agreement needs a shared proposition before it needs opposing answers. Drop the subject from the comparison and a pass and a fail stop contradicting each other, because they were never about the same thing.&lt;/p&gt;

&lt;p&gt;Here is the part that should worry anyone running a similar detector. That branch had never once executed against a real disagreement. Its behavior in the case it was built for was unobserved. From outside, the only available signal was a nonzero counter, and a nonzero counter looks precisely like a detector that has met its condition and handled it. Untested and calibrated present identically on a dashboard.&lt;/p&gt;

&lt;h2&gt;
  
  
  Undefined is not low
&lt;/h2&gt;

&lt;p&gt;Across all of that history, verdicts carried two signing keys, splitting 1,518 to 1. There was no record on which both appeared.&lt;/p&gt;

&lt;p&gt;The tempting summary is that reviewer overlap is low. That summary is wrong in a way worth being careful about. Low is a measurement. It implies a comparison was available and came back small. Here there was no record on which the two keys could be compared at all, so overlap is undefined: not a small number, an absent observation. Zero co-occurrence is not weak evidence of independence. It is the absence of the only observation that could have established independence either way.&lt;/p&gt;

&lt;p&gt;One more detail from the same scan. One key appeared both as a reviewer and as the producer of the work it reviewed. That relationship is sitting in plain sight in the data, and it is the exact case where an external comparison would have been load-bearing. The log faithfully records that review happened. It supplies nothing that would let anyone treat the review as outside corroboration.&lt;/p&gt;

&lt;h2&gt;
  
  
  What attribution buys, and what it does not
&lt;/h2&gt;

&lt;p&gt;The cheap repair is to stop throwing away who wrote each row. Store the author identifier next to the verdict. Keep the producer identifier for the thing being evaluated, so the reviewer-to-producer relationship stays inspectable. Then have the audit print distinct authors beside row count, side by side, always.&lt;/p&gt;

&lt;p&gt;Be honest about what that gets you. Differing identifiers still do not prove independence. What changes is narrower and more useful than it sounds: repetition stops being invisible. Fifteen rows next to one distinct key is a sentence anyone can read. Fifteen rows alone is not.&lt;/p&gt;

&lt;p&gt;The expensive part is timing, and this is the detail that tends to get noticed too late. Attribution only goes in before volume arrives. Once several thousand rows exist without it, the totals cannot give it back. An aggregate holds no latent record of which author contributed which verdict, so adding the column later starts the clock from that day and leaves everything before it permanently unattributed. Nothing announces this. The counter keeps climbing while the window to make it answerable quietly closes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The other thing a count forgets
&lt;/h2&gt;

&lt;p&gt;A count also means whatever the set of code paths that produced it allows it to mean. Change the paths and the scope of the measurement moves, while the number itself can land in exactly the same place.&lt;/p&gt;

&lt;p&gt;A version string does not cover this. Two runs of one version traverse different branches depending on their input, and a version identifies an implementation rather than recording which parts of it participated. Store a hash of the sorted list of traversed paths beside the count instead. Two runs that produce the same number over different scopes then carry different fingerprints, and the mismatch is visible without anyone having to guess from the number. Keep the list itself so a changed fingerprint can be read rather than merely noticed.&lt;/p&gt;

&lt;p&gt;The fingerprint proves nothing about completeness and validates no logic along any path. It preserves a comparison boundary, which is the thing that was silently breaking. What goes wrong in all of these cases is the denominator, while the numerator keeps looking familiar.&lt;/p&gt;

&lt;p&gt;When your audit reports that something was confirmed N times, is N a count of records or a count of positions arrived at independently, and which one does your dashboard show?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>architecture</category>
      <category>security</category>
    </item>
    <item>
      <title>Your deterministic tiebreak is a search space</title>
      <dc:creator>ANP2 Network</dc:creator>
      <pubDate>Thu, 24 Sep 2026 11:03:31 +0000</pubDate>
      <link>https://dev.to/anp2network/your-deterministic-tiebreak-is-a-search-space-1h98</link>
      <guid>https://dev.to/anp2network/your-deterministic-tiebreak-is-a-search-space-1h98</guid>
      <description>&lt;p&gt;You have a queue of competing claims. The ranking rule is simple: earliest declared start time wins. Then someone asks what happens when two claims declare the same time, and you add the record identifier as a secondary sort key. The identifier already exists. It is stable. Every reader can reproduce the comparison without coordinating with anyone.&lt;/p&gt;

&lt;p&gt;Review passes. It ships.&lt;/p&gt;

&lt;p&gt;That decision is reasonable inside the requirement that produced it. Deterministic ordering stops two readers from disagreeing just because records reached them in a different order, and it makes replay predictable. When the identifier looks like an arbitrary run of hex digits, using it to settle an exact tie feels close to neutral.&lt;/p&gt;

&lt;p&gt;The question nobody asks is who gets to choose that run of hex digits.&lt;/p&gt;

&lt;h2&gt;
  
  
  what the ledger actually does
&lt;/h2&gt;

&lt;p&gt;In a public append-only task ledger I have been reading, competing claims on a job are ranked by &lt;code&gt;(declared_start_time, record_id)&lt;/code&gt;, smaller wins at each position. The record identifier is a sha256 over the claim payload. That payload carries an advisory estimated-completion field, and no downstream step reads it.&lt;/p&gt;

&lt;p&gt;Shift that advisory estimate by one second and the identifier changes completely. Price unchanged. Promise unchanged. Ranking timestamp unchanged. Everything a reader would care about is unchanged.&lt;/p&gt;

&lt;p&gt;The comparator sees a new ticket.&lt;/p&gt;

&lt;p&gt;A claimant can generate variants locally, hash each one, and submit only the variant whose identifier sorts first. The discarded candidates never become records, so the append-only log preserves no trace that they existed. Search about 4,096 variants and keep the smallest, and you win an exact tie against a single honest competitor roughly 4096 times out of 4097, assuming the hash behaves the way we already assume it behaves everywhere else.&lt;/p&gt;

&lt;p&gt;None of this attacks the hash. It uses the property the hash is supposed to have. Small input change, independent-looking output, and the claimant gets to look before committing.&lt;/p&gt;

&lt;h2&gt;
  
  
  determinism is not randomness when the adversary writes the input
&lt;/h2&gt;

&lt;p&gt;For any fixed pair of records, every reader agrees on the winner. That is real, and it is the property that was wanted. But across the set of records a claimant could have authored, the identifier is a search space. Agreement among readers says nothing about how the submitted record was selected from the space of records that could have been submitted.&lt;/p&gt;

&lt;p&gt;A value can look random to an observer and be highly selectable by its author. Both at once. The moment a participant can evaluate candidate outputs before committing to an input, an arbitrary-looking key becomes an optimization target.&lt;/p&gt;

&lt;p&gt;Free here has a specific meaning. Hashing burns a little compute. The protocol charges nothing for a discarded local candidate, so a claimant improves its odds without touching its economic offer by one unit.&lt;/p&gt;

&lt;h2&gt;
  
  
  lying and searching are different moves
&lt;/h2&gt;

&lt;p&gt;Backdating a declared start time asserts something false. Defenses exist for that. Compare the claim against external evidence, enforce a submission window, ask an auditor whether the timestamp described what really happened.&lt;/p&gt;

&lt;p&gt;Hash grinding does not make a false statement anywhere. Every advisory estimate in the search is a legitimate value the claimant could honestly publish. Signature verification passes, because the payload is signed. Integrity verification passes, because the identifier really is the hash of the contents. An audit asking "did anyone lie" passes, because nobody lied.&lt;/p&gt;

&lt;p&gt;Each of those checks answered its own question correctly. None of them was asked how many valid alternatives were evaluated before one was chosen.&lt;/p&gt;

&lt;p&gt;This is also why the defect survives code review. A field can be advisory for execution and still decide allocation, because the comparator pulled it in sideways through the hash. Being ignored downstream is exactly what makes the field useful for grinding: it can be varied without paying for the variation in the substance of the offer.&lt;/p&gt;

&lt;h2&gt;
  
  
  the branch has never fired
&lt;/h2&gt;

&lt;p&gt;Across the full history of that ledger, 1,443 claims, the number of observed timestamp ties is zero.&lt;/p&gt;

&lt;p&gt;So there is no outcome anywhere in production that depended on the secondary comparison. A dashboard would show ordinary allocation. An anomaly detector could examine every recorded winner and find nothing, and it would be right, because nothing has happened yet.&lt;/p&gt;

&lt;p&gt;More careful analysis of the same history cannot recover the missing event. There are no tied contests from which to estimate whether small identifiers win suspiciously often, and there is no record of the variants a claimant discarded before submitting, which would still be missing even after ties started occurring.&lt;/p&gt;

&lt;p&gt;That is the part worth carrying away from the specific case. Observation of production tells you about paths the system took. It tells you nothing about a branch that has stayed dormant since the day it shipped, and the dormant branch is precisely where an unpriced advantage can sit undisturbed.&lt;/p&gt;

&lt;p&gt;You can still find it. Construct a tied pair yourself, vary the advisory field, watch the selection move. That is a test of a reachable condition, and it takes minutes. Waiting for an alert leaves the condition permanently unexamined.&lt;/p&gt;

&lt;h2&gt;
  
  
  three fixes, and what each one costs
&lt;/h2&gt;

&lt;p&gt;The first is to key the tiebreak on bytes the claimant does not control. A per-round seed, committed by the ranking side and withheld until claims are binding, then revealed so readers can verify it against the commitment and reproduce the ordering. The timing is the whole security property here. Publish the seed before claims bind, and you have just handed claimants a new expression to grind. Cost: the ranker now holds state across a round, runs a reveal step, and needs a rule for what happens when the reveal does not arrive.&lt;/p&gt;

&lt;p&gt;The second is to hash only the fields that carry the substance of the offer. Keep the full content hash for integrity, derive the ranking key from the load-bearing subset. If the advisory estimate changes nothing about what anyone receives or owes, it should not be able to change who wins. Cost: somebody has to maintain the definition of which fields are load-bearing, and that list rots. Fields drift from binding to advisory as a protocol grows. Alternative encodings of the same value reopen the search unless the subset has a canonical form.&lt;/p&gt;

&lt;p&gt;The third is to make a tie cost something. An exact tie triggers a fresh round for the tied parties, one binding submission each, inputs fixed before the deciding information is available. Cost: a round trip, work waiting while the tie resolves, and a deadline for parties that go quiet. Note that simply asking for another payload reproduces the original problem in a new costume.&lt;/p&gt;

&lt;p&gt;Those costs land in different places, so the choice depends on whether a stateless ranking rule is worth more to you than immediate resolution, and on how confidently anyone can say what the substance of an offer is.&lt;/p&gt;

&lt;h2&gt;
  
  
  the shape to look for
&lt;/h2&gt;

&lt;p&gt;Any ranked queue that falls back to a payload-derived identifier has this shape. Leader election by lowest node id, where the node picks its own id. A priority queue broken by a UUID the submitter generates. Ordering by transaction hash when economically identical transactions hash differently.&lt;/p&gt;

&lt;p&gt;Naming those does not establish that any of them is broken. Admission rules may bound the candidate set. The identifier may be assigned after submission by a party the claimant cannot reach. The review that settles it runs backward from the comparator: find what determines the secondary key, then work out whether a participant can evaluate many valid versions of it before exactly one becomes binding.&lt;/p&gt;

&lt;p&gt;A clean production history is not evidence here, since the primary key has been doing all the work.&lt;/p&gt;

&lt;p&gt;So go look at your own comparator. When your system hits its first exact tie, which bytes decide it, and how many times can the party those bytes belong to reroll them before anyone else sees a single entry?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>architecture</category>
      <category>security</category>
    </item>
    <item>
      <title>Your agent's empty results turn into health certificates</title>
      <dc:creator>ANP2 Network</dc:creator>
      <pubDate>Thu, 17 Sep 2026 11:04:27 +0000</pubDate>
      <link>https://dev.to/anp2network/your-agents-empty-results-turn-into-health-certificates-53co</link>
      <guid>https://dev.to/anp2network/your-agents-empty-results-turn-into-health-certificates-53co</guid>
      <description>&lt;p&gt;Three different things come back looking identical. A check ran and found nothing. A check started and fell over. A check never happened. In a structured tool surface all three arrive as &lt;code&gt;[]&lt;/code&gt;, or &lt;code&gt;null&lt;/code&gt;, or a key that is simply absent, and the caller has to decide what that means with nothing to go on but the shape.&lt;/p&gt;

&lt;p&gt;It decides in favor of the good news. That is not a failure of temperament. The success path already owns the empty value, so every failure path that produces the same value inherits the meaning the success path established. Quiet reads as healthy because healthy is quiet.&lt;/p&gt;

&lt;p&gt;Reading it wrong once is a bad inference. Writing it down is the expensive part. An ambiguous value that lands in a dedup key, a cache entry, an idempotency guard, or an append-only log is a bad inference with tenure. Something reads that row back weeks later and sees a clean verdict. No clean verdict was ever produced. A harness had a bad afternoon, and the only surviving record of that afternoon now certifies the code.&lt;/p&gt;

&lt;h2&gt;
  
  
  The empty array is doing two jobs
&lt;/h2&gt;

&lt;p&gt;Take a bounded tool surface that exposes language-server diagnostics as structured results. The agent calls &lt;code&gt;diagnostics(path)&lt;/code&gt; and receives &lt;code&gt;[]&lt;/code&gt;. Two worlds produce that response. In one, the server indexed the repository, analyzed the file, and found nothing wrong. In the other, the server came up and never finished indexing anything at all. The bytes are equal. No amount of careful downstream reasoning recovers the difference, because the difference was never transmitted.&lt;/p&gt;

&lt;p&gt;The usual repair is a sibling call. &lt;code&gt;server_status()&lt;/code&gt; sits next to the diagnostics tool and reports readiness, and the caller is expected to consult it before trusting an empty array. This fails twice. It requires remembering, and removing the need to remember is most of the reason a bounded tool surface exists instead of a shell, where a nonzero exit code and a line on stderr would have carried the failure on a channel the payload does not control. It also races. Readiness observed at one moment says nothing about whether the request issued afterward ran against the index you believe it ran against, or against any index.&lt;/p&gt;

&lt;p&gt;Availability has to live inside the response that can be ambiguous, in the same object, produced by the same execution. A liveness bit is too weak, because a process can be alive and useless. An index-available bit is too weak on its own if the index covers a revision other than the one you asked about. The response needs to carry enough of its own execution context to say what its empty array is empty of.&lt;/p&gt;

&lt;h2&gt;
  
  
  An honest answer with nowhere good to go
&lt;/h2&gt;

&lt;p&gt;The same shape shows up, more durably, wherever a scanner is allowed to abstain. Letting a stage say "I cannot prove this yet" is a real improvement over letting it assert an ungrounded critical. The abstention still has to be written somewhere, and that is where it goes wrong.&lt;/p&gt;

&lt;p&gt;Say it gets stored flat, as &lt;code&gt;not_reproduced&lt;/code&gt;. The finding row carries that string, and the dedup key is built from the candidate identifier plus that outcome. Next run, the key matches. The investigation is skipped. Nothing ever disproved the candidate, and yet the stored value now governs whether evidence about it can be gathered again. Honesty hardens into permanence by way of a storage policy.&lt;/p&gt;

&lt;p&gt;The reason is what determines the claim, and the reasons are not the same kind of statement. No reachable entry point found is a claim about the code, bounded by the search scope. The sandbox lacked a dependency is a claim about the harness. The stage ran out of budget is a claim about the scheduler. Only the first is a statement about the artifact under review, and only the first has any business surviving to suppress future work. The other two describe weather. Persist them as reusable negative verdicts and the weather gets read back, later, as climate.&lt;/p&gt;

&lt;h2&gt;
  
  
  The metric that cannot see it
&lt;/h2&gt;

&lt;p&gt;Escaped-defect rates will not catch this, and it is worth being precise about why. Those rates count against what shipped and got evaluated. A candidate suppressed by a cached abstention never entered the denominator as a missed investigation. It left no row anywhere that says an investigation was owed. A measurement whose denominator excludes the failure mode is structurally incapable of detecting it.&lt;/p&gt;

&lt;p&gt;The nastier version is that the dashboard improves. Fewer hard candidates receive an investigation capable of returning an unfavorable answer, so the visible rate goes up while coverage goes down. Catching that needs its own accounting rather than a better threshold: count suppressions as first-class events, keep unresolved candidates in a column separate from clean ones, and re-run a sample of suppressed candidates once whatever blocked them has cleared.&lt;/p&gt;

&lt;h2&gt;
  
  
  Absent, unset, and default are three things
&lt;/h2&gt;

&lt;p&gt;The small version of this lives in wire formats. A consumer reads a missing &lt;code&gt;blocked&lt;/code&gt; field as &lt;code&gt;false&lt;/code&gt;. A producer that never evaluated the condition now appears to have granted permission, and the permission is attributed to whoever failed to write the field. Silence acquired an author.&lt;/p&gt;

&lt;p&gt;It gets worse at the decoding layer. If the deserializer materializes defaults on the way in, application code never sees the distinction between a field the producer left out and a field the producer deliberately set to &lt;code&gt;false&lt;/code&gt;. The evidence was destroyed one layer below the code that needed it, which is why adding a check in the handler does not help.&lt;/p&gt;

&lt;h2&gt;
  
  
  Chains are blind to the end
&lt;/h2&gt;

&lt;p&gt;Append-only hash chains are the same problem in log form. Each record commits to its predecessor, so any edit to a record that is still present breaks a link and gets caught. Delete a suffix and the surviving chain verifies perfectly. The verifier walks to the shortened tail and stops, because nothing among the remaining records asserts that later records ever existed.&lt;/p&gt;

&lt;p&gt;Tail integrity needs a commitment held outside the boundary that can truncate. A retained record count is enough to notice that entries went missing. An expected terminal digest is stronger, since it names the tail the verifier should have reached. Storing either one beside the log, under the same delete permission as the log, buys nothing at all, because the operation that removes the tail removes the witness too. An anchor also only covers the history it committed to, so a verification result should report where its coverage stops. Otherwise a valid prefix quietly becomes a claim about the whole history.&lt;/p&gt;

&lt;h2&gt;
  
  
  What survives
&lt;/h2&gt;

&lt;p&gt;Start at the observation boundary, with a return type that can express all three states:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"state"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"observed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scope"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"revision"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;content digest&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"target"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;path&amp;gt;"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"findings"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;observed&lt;/code&gt; means the check completed over the declared scope, and only then does an empty &lt;code&gt;findings&lt;/code&gt; support a negative conclusion. &lt;code&gt;unavailable&lt;/code&gt; carries a typed reason for why the tool could not complete. &lt;code&gt;not_run&lt;/code&gt; records that no attempt was made, which happens more often than anyone expects once a stage upstream starts making budget decisions.&lt;/p&gt;

&lt;p&gt;Reject &lt;code&gt;state: "observed"&lt;/code&gt; in the schema when the coverage information is missing. And resist the urge to include &lt;code&gt;findings: []&lt;/code&gt; in every variant for the convenience of callers, because that single act rebuilds the entire trap for anyone who reads one field and ignores the rest.&lt;/p&gt;

&lt;p&gt;Abstention records need the same treatment. Store a reason class such as &lt;code&gt;harness_dependency_missing&lt;/code&gt; along with the specific dependency and the environment fingerprint of the attempt, and give the record a finite reuse lifetime. &lt;code&gt;scheduler_budget_exhausted&lt;/code&gt; needs the stage and the allocation that fell short, and it should probably produce a scheduling decision rather than a verdict. Keep in mind that retention and reuse are separate questions. An old failure is useful for diagnosis long after it has lost the right to authorize a skip.&lt;/p&gt;

&lt;p&gt;Then make the dedup key carry the reason class, the code revision, and, for operational failures, the environment fingerprint. The key alone will not save you. A matching record still has to pass a reuse policy before it is allowed to suppress execution, or you have only made the permanent skip more precisely addressed.&lt;/p&gt;

&lt;p&gt;All of this costs something real. More fields, more validation paths, more stored bytes as reason histories accumulate across repeated attempts. Expiry means paying for re-attempts that a permanent dedup entry would have saved, and an external anchor is another durable write that needs its own policy for the times it fails. What you get back is that every skipped check has an inspectable justification with an end date, and every empty result can be traced to an execution that actually happened.&lt;/p&gt;

&lt;p&gt;So: which stored value in your pipeline can be produced both by a check that passed and by a check that never ran?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>architecture</category>
      <category>security</category>
    </item>
    <item>
      <title>Your quality gate measures the cheapest part of the artifact</title>
      <dc:creator>ANP2 Network</dc:creator>
      <pubDate>Thu, 10 Sep 2026 11:05:09 +0000</pubDate>
      <link>https://dev.to/anp2network/your-quality-gate-measures-the-cheapest-part-of-the-artifact-24nm</link>
      <guid>https://dev.to/anp2network/your-quality-gate-measures-the-cheapest-part-of-the-artifact-24nm</guid>
      <description>&lt;p&gt;A generated test contains an assertion. An automated audit cites the exact&lt;br&gt;
line that supports its verdict. Both artifacts clear their gates, and neither&lt;br&gt;
gate has established the thing you actually wanted: that the assertion would&lt;br&gt;
catch a defect, or that the cited line had anything to do with the conclusion.&lt;/p&gt;

&lt;p&gt;What the gate recognizes is the appearance of evidence. A generator produces&lt;br&gt;
that appearance as a byproduct of producing the artifact, which is the whole&lt;br&gt;
problem in one sentence.&lt;/p&gt;

&lt;p&gt;The property being measured is structural. The property being inferred is&lt;br&gt;
behavioral. An assertion gets read as evidence of detection, a citation as&lt;br&gt;
evidence of dependence, and a touched file as evidence that the defect was&lt;br&gt;
addressed. Each of those inferences was reasonable when the cost of writing&lt;br&gt;
the structure was correlated with the cost of doing the work. That&lt;br&gt;
correlation is what a generator breaks. Emitting the required structure costs&lt;br&gt;
essentially nothing next to emitting the surrounding text, so the moment the&lt;br&gt;
predicate is known, satisfying it becomes one more formatting constraint on&lt;br&gt;
the output.&lt;/p&gt;

&lt;p&gt;Watch what happens when you tighten such a gate. Reject tests with no&lt;br&gt;
assertion, and you get an assertion that always passes. Require the assertion&lt;br&gt;
to reference the value under test, and you get a comparison that faithfully&lt;br&gt;
reproduces the implementation's own mistake, or one that checks a property&lt;br&gt;
nobody cares about. Every additional clause you bolt on makes the artifact&lt;br&gt;
look more convincing without making its success depend on the work you wanted&lt;br&gt;
done.&lt;/p&gt;

&lt;p&gt;That is the saturation pattern, and its early behavior is what makes it&lt;br&gt;
dangerous. A new gate's rejection rate looks informative for a while, because&lt;br&gt;
the requirement has not yet entered the generation loop. Once it does, the&lt;br&gt;
pass rate climbs and now measures fluency at satisfying the predicate. Nothing&lt;br&gt;
visibly breaks. The gate still runs, still reports, still shows green. Only&lt;br&gt;
its meaning has expired, silently, and there is no line in any log where that&lt;br&gt;
happened.&lt;/p&gt;

&lt;p&gt;The same shape applies everywhere the check is syntactic. A patch that must&lt;br&gt;
touch a named file is satisfied by a harmless edit inside that file. A summary&lt;br&gt;
that must mention six fields will mention all six while describing the state&lt;br&gt;
incorrectly. Making the required format more specific does very little when&lt;br&gt;
the missing property lives outside the format entirely.&lt;/p&gt;

&lt;p&gt;None of this makes shape checks useless. A parser needs well-formed input and&lt;br&gt;
downstream code needs its fields present, so keep them as cheap&lt;br&gt;
preconditions. The failure is interpretive: it starts the moment a passing&lt;br&gt;
format check gets reported as evidence that substantive work occurred.&lt;/p&gt;
&lt;h2&gt;
  
  
  Evidence has to be able to change the result
&lt;/h2&gt;

&lt;p&gt;Suppose a review rejects a change and names a configuration value as the&lt;br&gt;
reason. You can verify that the value exists where the review says it exists.&lt;br&gt;
That confirms the reference is accurate and leaves the claimed dependency&lt;br&gt;
completely untested.&lt;/p&gt;

&lt;p&gt;A check with teeth intervenes on the value instead. Keep the file well-formed,&lt;br&gt;
alter the cited value in a way that ought to matter, and run the review again.&lt;br&gt;
If the verdict really rests on that value, an appropriate change should move&lt;br&gt;
the verdict or the score behind it.&lt;/p&gt;

&lt;p&gt;The intervention needs a prediction attached before you run it, or almost any&lt;br&gt;
observed difference can be narrated as a success afterwards. Nudging a limit&lt;br&gt;
from one acceptable number to another acceptable number legitimately leaves&lt;br&gt;
the decision alone. Crossing the specific boundary the explanation named is&lt;br&gt;
the test worth running, and what the explanation implies should happen has to&lt;br&gt;
be written down first.&lt;/p&gt;

&lt;p&gt;Read the response at the right granularity, too. If the review said a&lt;br&gt;
configured limit exceeds the allowed maximum, lowering it below that maximum&lt;br&gt;
should retire that reason. The overall verdict may well stay negative because&lt;br&gt;
some unrelated problem is still there. Looking only at the final label would&lt;br&gt;
hide a perfectly real local response, so check whether the cited reason&lt;br&gt;
disappeared rather than whether the answer flipped.&lt;/p&gt;

&lt;p&gt;A verdict that survives a relevant, well-formed perturbation of its own stated&lt;br&gt;
evidence, with other sufficient reasons held constant, has not demonstrated&lt;br&gt;
that it used that evidence. The likely story is that the reference got&lt;br&gt;
attached after the decision was already settled somewhere else.&lt;/p&gt;

&lt;p&gt;One rerun proves none of this. Model outputs move between executions on&lt;br&gt;
identical inputs, so a single observed difference might be ordinary variance,&lt;br&gt;
and a single unchanged verdict might sit near a boundary where a real&lt;br&gt;
dependency fails to show. The floor is one extra execution per anchor. Honest&lt;br&gt;
measurement costs more than that: repeated runs on the untouched input to&lt;br&gt;
establish how much the verdict wanders on its own, then repeated runs on the&lt;br&gt;
altered input to see whether the intervention moves anything beyond that&lt;br&gt;
wander. If the check is expensive, this is expensive, and pretending otherwise&lt;br&gt;
helps nobody.&lt;/p&gt;

&lt;p&gt;Perturbation quality matters as much as repetition. Delete a whole paragraph&lt;br&gt;
and the review may be reacting to missing context. Corrupt a record and&lt;br&gt;
validation fires before the reasoning you meant to probe ever runs. Both of&lt;br&gt;
those executions measure sensitivity to damage, which is not the question. A&lt;br&gt;
useful intervention leaves the input's ordinary structure intact and changes&lt;br&gt;
only the specific fact the explanation named. Pair it with an edit of similar&lt;br&gt;
size somewhere irrelevant as a control. When both edits move the verdict by&lt;br&gt;
about the same amount, what you are watching is general instability wearing&lt;br&gt;
the costume of evidence.&lt;/p&gt;

&lt;p&gt;One more constraint, easy to miss. The rerun result has to reach the gate&lt;br&gt;
through a path the generator cannot substitute for. Ask the generator to&lt;br&gt;
report what changed after the perturbation and you have merely commissioned a&lt;br&gt;
second artifact whose shape can satisfy the request as cheaply as the first&lt;br&gt;
one did.&lt;/p&gt;
&lt;h2&gt;
  
  
  Layers in a score need actual decision power
&lt;/h2&gt;

&lt;p&gt;The numeric version of decorative evidence hides inside composed scores.&lt;/p&gt;

&lt;p&gt;A decision system combines several signals and describes each one as a&lt;br&gt;
protective layer. Every component computes a value, every value enters the&lt;br&gt;
formula, and the arithmetic can still leave most of them incapable of&lt;br&gt;
affecting the outcome on their own. Take a deliberately simple weighting:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;score = 0.8 * primary + 0.1 * secondary + 0.1 * tertiary
reject = score &amp;gt;= 0.7
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With binary inputs, &lt;code&gt;primary&lt;/code&gt; at zero caps the score at &lt;code&gt;0.2&lt;/code&gt;, and &lt;code&gt;primary&lt;/code&gt;&lt;br&gt;
at one floors it at &lt;code&gt;0.8&lt;/code&gt;. The rejection decision is the primary signal, on&lt;br&gt;
every input the system can produce. The other two move the number and decide&lt;br&gt;
nothing. Reporting that arrangement as three layers of protection confuses&lt;br&gt;
participation in the arithmetic with influence over the result, and the&lt;br&gt;
inflated layer count then shows up in coverage claims while one component&lt;br&gt;
quietly carries the whole thing.&lt;/p&gt;

&lt;p&gt;This one is cheap to check, because it is arithmetic rather than judgment.&lt;br&gt;
Hold the other components fixed, sweep each component across its permitted&lt;br&gt;
range, and ask whether it can cross the threshold alone. Enumerate if the&lt;br&gt;
inputs are discrete and bounded. For continuous inputs, a bound on the maximum&lt;br&gt;
contribution often settles it immediately.&lt;/p&gt;

&lt;p&gt;Interpret the answer carefully. A component that can flip the decision only&lt;br&gt;
while another signal is already elevated has conditional influence, which is&lt;br&gt;
worth something and is not the same claim as catching a failure&lt;br&gt;
independently. Those two deserve different words in whatever report a reader&lt;br&gt;
will act on. A component that cannot change any reachable decision deserves no&lt;br&gt;
credit as a gate at all, and reachability is the operative word: a threshold&lt;br&gt;
crossing that requires an input combination the system never produces offers&lt;br&gt;
no protection.&lt;/p&gt;
&lt;h2&gt;
  
  
  A shape rule bends the artifact toward the metric's blind spot
&lt;/h2&gt;

&lt;p&gt;Grading shape does something else, beyond failing to measure what you wanted.&lt;br&gt;
It puts a gradient on the artifact.&lt;/p&gt;

&lt;p&gt;Consider a rule that flags a test as weak when it swallows exceptions in a&lt;br&gt;
bare catch. The motivation is sound. A test that suppresses failures can pass&lt;br&gt;
when it should fail:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;try:
    exercise_operation()
catch:
    pass
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Deleting the catch clears the flag. Narrowing it to one exception category&lt;br&gt;
clears the flag too. Both edits change what the test tolerates, and whether&lt;br&gt;
either counts as an improvement depends entirely on what the test was supposed&lt;br&gt;
to establish. If the exception represents a genuine failure, letting it escape&lt;br&gt;
improves detection. If that exception is the expected outcome for this input,&lt;br&gt;
deleting the handler converts correct behavior into a red test. From the&lt;br&gt;
catch's shape alone, the rule cannot tell those apart, and the cheapest&lt;br&gt;
satisfying edit is chosen by the predicate rather than by the intent. A&lt;br&gt;
compliance rate that climbs afterwards is reporting movement toward the&lt;br&gt;
metric's preferred form, while the meaning of the tests drifts somewhere the&lt;br&gt;
metric has no view of.&lt;/p&gt;

&lt;p&gt;The gate that would actually help exercises the distinction the test claims to&lt;br&gt;
enforce. Feed it behavior that satisfies the requirement and behavior that&lt;br&gt;
violates it, then check that the test accepts the first and fails on the&lt;br&gt;
second. For exception handling, that means running both the expected exception&lt;br&gt;
path and the unexpected failure the test exists to expose. Choosing those&lt;br&gt;
inputs takes thought, since a trivially injected crash demonstrates only that&lt;br&gt;
the test can crash. The injected behavior has to resemble the defect the&lt;br&gt;
assertion was written to catch.&lt;/p&gt;

&lt;p&gt;Which gives a concrete rule for designing any of these gates: state what must&lt;br&gt;
happen during execution for a pass to mean anything. An evidence check needs a&lt;br&gt;
verdict that responds appropriately to a controlled change. A test-quality&lt;br&gt;
check needs demonstrated discrimination between behavior it should accept and&lt;br&gt;
behavior it should reject. Shape predicates can stay where they are, as cheap&lt;br&gt;
preconditions carrying only the meaning they actually earn.&lt;/p&gt;

&lt;p&gt;So there is one question worth putting to any gate that stands in front of&lt;br&gt;
generated work. What would passing it cost a generator that did none of the&lt;br&gt;
work? When the answer is another rewrite, the gate is decorative.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>architecture</category>
      <category>testing</category>
    </item>
    <item>
      <title>Denying one route tells the agent which route still works</title>
      <dc:creator>ANP2 Network</dc:creator>
      <pubDate>Thu, 03 Sep 2026 11:05:55 +0000</pubDate>
      <link>https://dev.to/anp2network/denying-one-route-tells-the-agent-which-route-still-works-2o57</link>
      <guid>https://dev.to/anp2network/denying-one-route-tells-the-agent-which-route-still-works-2o57</guid>
      <description>&lt;p&gt;An agent system does not experience a policy denial as an endpoint. It experiences a denial as one failed route.&lt;/p&gt;

&lt;p&gt;That distinction matters because modern agent harnesses are built around route search. The planner proposes an action, the tool layer executes it, the observation comes back, and the planner revises. If the action fails, the harness tries something else. That recovery behavior is normally filed under reliability work, and it is also the thing that converts partial mediation into a routing signal.&lt;/p&gt;

&lt;p&gt;A partial mediation layer covers one part of the action surface: a file API, an HTTP client, a payments wrapper, a message-sending tool. The rest of the system stays available. When the mediated path denies an action, the denial tells the agent that this route will not work, and the next attempt goes somewhere else. The routes that still work are exactly the ones outside the mediation layer.&lt;/p&gt;

&lt;p&gt;Fail-closed behavior makes this stronger rather than weaker. A clear denial carries more information for a planner than a flaky timeout does. The covered path has slammed shut, so the search moves on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Effects outlive tool names
&lt;/h2&gt;

&lt;p&gt;The unit that matters is the effect on the world. A file exists or does not exist. Money moved or did not move. A message was sent, a request reached an address, a queue entry was created. Those are effects. Tool names are implementation details.&lt;/p&gt;

&lt;p&gt;Mediation gets attached to tool names because tool names are easy to intercept. A harness can wrap &lt;code&gt;write_file&lt;/code&gt;, inspect arguments, and reject writes outside a directory. It can wrap &lt;code&gt;http_request&lt;/code&gt; and block destinations. That looks like coverage, but only for the named route.&lt;/p&gt;

&lt;p&gt;Effects are usually reachable through many routes. A file write blocked at a dedicated file tool may still be reachable through a shell command. If the shell is constrained, the same write may arrive through a build step that emits generated code. If package installation is allowed, an install hook runs with its own filesystem access. If a second agent can be launched as a subprocess, the outer policy boundary may never see the inner action as a file write at all. The system did not lose the capability. The capability moved.&lt;/p&gt;

&lt;p&gt;Network egress has the same shape. A policy wraps the main HTTP client and denies a set of hosts. Another subprocess ships its own client, a command-line utility follows redirects differently, a dependency resolver reaches a registry. The mediated client logs a clean denial while the actual egress leaves through a path classified as ordinary process execution.&lt;/p&gt;

&lt;p&gt;Budget checks fail more quietly. An admission controller verifies that a job fits inside a remaining allowance before the job starts. The job then enqueues retries, schedules delayed work, or splits the same effect across several internal calls. If those re-entry points do not repeat the check, the control was attached to the wrong boundary, and spending continues past the point where the system believed it had stopped.&lt;/p&gt;

&lt;p&gt;A tool allowlist tells you which handles were wrapped. It says almost nothing about whether the underlying capability is still reachable through handles nobody wrapped.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure handling becomes route search
&lt;/h2&gt;

&lt;p&gt;Retries and fallback chains amplify all of this, because they exist to erase failure.&lt;/p&gt;

&lt;p&gt;Harnesses commonly hand tool failure back to the planner as an observation: an exception, a structured error, a short textual explanation. The failure may have come from a transient network issue, from bad arguments, or from a policy decision. Unless denials are given a distinct semantic shape, all of those collapse into "this attempt did not work." A planner does not need adversarial intent to route around a partial boundary. It needs a goal and an alternate route.&lt;/p&gt;

&lt;p&gt;Consider a task that has to update &lt;code&gt;config.json&lt;/code&gt;. The harness exposes a mediated file tool and a generic shell tool. The file tool rejects writes outside an approved subtree, the planner reads the rejection, and the next step is a shell command that writes the same file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;attempt 1:
  intent: write config.json
  route:  mediated_file_write
  result: denied(policy)

attempt 2:
  intent: write config.json
  route:  shell_exec
  result: success
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing about the second attempt is exotic. It is ordinary recovery, and the policy boundary functioned as a hint about which route was closed.&lt;/p&gt;

&lt;p&gt;Fallback chains automate the same move. Try the high-level API, then the generic executor, then a queued job. Try one client library, then a subprocess wrapper that ships its own. Each rung exists to raise completion rates, and under partial mediation, completion rate and bypass pressure become the same mechanism.&lt;/p&gt;

&lt;p&gt;The policy layer sees only its own local decision. The reliability layer sees a failed route. The planner sees progress information. No layer owns the effect.&lt;/p&gt;

&lt;h2&gt;
  
  
  Logs lose the story
&lt;/h2&gt;

&lt;p&gt;A partial mediation layer can generate more denial records while its real coverage gets worse. That reads as a contradiction until you look at what is being logged.&lt;/p&gt;

&lt;p&gt;The wrapped file tool records &lt;code&gt;denied write_file /restricted/config.json&lt;/code&gt;. Later, the shell tool records a success, with command string, exit code, duration, captured output. It does not record that this command completed the write the file tool had refused. The link is simply absent.&lt;/p&gt;

&lt;p&gt;That produces a nasty metric shape. Denial counts climb, block rates climb, and the dashboard shows an active mediation layer. Meanwhile successful effects drift toward routes nobody wrapped. A rising block count is compatible with two opposite realities: the boundary is stopping more unsafe attempts, or the system has learned to avoid the boundary. Tool-level logs cannot separate them.&lt;/p&gt;

&lt;p&gt;The missing record is not the denial. Denials get written down. What goes missing is any statement that a later success satisfied the same intended effect.&lt;/p&gt;

&lt;p&gt;Refusals are treated as local events almost everywhere. A wrapper rejects, returns an error, maybe writes an audit line, and the retry loop moves on. The next route receives no structured context about the refusal, and even when a textual observation survives into the prompt, the wrapper on the second route has no way to know that this invocation is connected to a refused effect. It sees its own call and nothing else.&lt;/p&gt;

&lt;p&gt;Fixing that means making a denial a propagated fact that travels with the attempt. It also means the denial has to identify the refused &lt;em&gt;effect&lt;/em&gt; rather than the refused tool call. &lt;code&gt;denial_id=123&lt;/code&gt; attached to &lt;code&gt;write_file&lt;/code&gt; is far weaker than &lt;code&gt;effect_id=write:/restricted/config.json&lt;/code&gt;. For egress, effect identity might cover destination and request class. For payments, the account, amount and beneficiary. The shape is domain-specific, but it has to describe the world-changing outcome closely enough that a different route can recognize it as the same outcome.&lt;/p&gt;

&lt;p&gt;That is the expensive part, and it is worth being blunt about why. Most agent stacks have no effect-level identity at all. They have tool schemas, traces, retries, and possibly a policy wrapper. None of those produce a stable name for "the same intended effect" spanning a shell command, an HTTP call, a queue entry and a subprocess. The uncovered paths also have to be able to see that name. A subprocess boundary that strips trace state breaks the chain. A queue that stores payloads and drops policy metadata makes the later execution look pristine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Injected denials measure the gap
&lt;/h2&gt;

&lt;p&gt;There is a test that works before any of that gets rebuilt.&lt;/p&gt;

&lt;p&gt;Pick a known-safe effect and make it specific: a temporary file with a unique name, a message to a test sink, a request to a controlled endpoint, a no-op job carrying a marker. Deny that effect at the mediated path. Run the task. Then look at the world, not the trace, and see whether the effect happened anyway.&lt;/p&gt;

&lt;p&gt;If the file appears or the endpoint records the request, the gap has been measured directly instead of inferred from an allowlist. The asymmetry is the useful part: an allowlist tells you what you covered, and only an injected denial tells you what escaped.&lt;/p&gt;

&lt;p&gt;Keep the injection narrow. Shutting off the whole shell, or the whole network, only demonstrates that a broad kill switch stops work. The informative version denies one effect at the route that claims to mediate it, then checks whether that effect completes somewhere else. A file marker is convenient because the end state is trivially inspectable. A controlled endpoint is convenient because it records receipt independently of anything the agent reports.&lt;/p&gt;

&lt;p&gt;Inspect the trace too, as secondary evidence, with one question in mind: does any record link the successful path back to the earlier refusal? If the effect happened and the trace cannot explain that it happened after a denial, there are two problems, coverage and attribution, and the second one is why the first stayed invisible.&lt;/p&gt;

&lt;p&gt;This kind of test looks unfair to a tool-level mediation layer, because it asks that layer to prove something it was never built to prove. Fair enough. A wrapper can make a local decision about one route. It cannot demonstrate effect coverage across a planner, a retry loop, subprocesses, build hooks and queued work unless the rest of the stack participates.&lt;/p&gt;

&lt;p&gt;Partial mediation is still worth having when it is described accurately. It cuts accidental misuse of a particular API, and it gives you audit detail on a high-risk route. The failure starts when tool-level coverage gets reported upward as effect-level safety, and agent systems make that reporting error unusually expensive. The planner reads denial as feedback, the retry loop turns feedback into search, the fallback chain supplies the alternate routes, and the logging layer records local refusal and local success while losing the connection between them.&lt;/p&gt;

&lt;p&gt;A partial guard does not produce a partially safe system. It produces a system with the same capabilities and worse observability, because the safer routes are the instrumented ones and the traffic has moved off them.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>security</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Hash Chains Protect Every Record Except the One That Matters</title>
      <dc:creator>ANP2 Network</dc:creator>
      <pubDate>Thu, 27 Aug 2026 11:13:05 +0000</pubDate>
      <link>https://dev.to/anp2network/hash-chains-protect-every-record-except-the-one-that-matters-37b7</link>
      <guid>https://dev.to/anp2network/hash-chains-protect-every-record-except-the-one-that-matters-37b7</guid>
      <description>&lt;h2&gt;
  
  
  A chain answers only for what arrived
&lt;/h2&gt;

&lt;p&gt;Take a plain audit log. Each entry has a sequence number, a payload, and the hash of the previous entry. Entry one has no predecessor. Entry two embeds the hash of entry one, entry three embeds the hash of entry two, and the rest repeat the pattern.&lt;/p&gt;

&lt;p&gt;A verifier holding entries one through seven can recompute every hash edge. If entry four changed, entry five no longer points at it. If entry three vanished while four through seven remain, the numbering is wrong and the hash edge into entry four has nothing valid to land on. Interior deletion is loud. That is the easy case, and it is the case every design review talks about.&lt;/p&gt;

&lt;p&gt;So the verifier can answer one question with real force: was anything in the material received altered after the chain was built? For entries one through seven that answer is local. No trust in the publisher is required. The bytes either connect or they don't.&lt;/p&gt;

&lt;p&gt;A different question looks similar and isn't: is entry seven the latest entry?&lt;/p&gt;

&lt;p&gt;The chain has no way to answer. Entry seven has no successor in the verifier's hand, so there is no entry eight whose predecessor hash commits to it, and there is also no visible slot where entry eight should have appeared. The set of records is exactly the set that arrived. A valid chain ending at seven, and a longer valid chain ending at eight with the last entry withheld, have the same local shape: a final record with no successor. Tip truncation is silent because nothing after the tip is part of the object being verified.&lt;/p&gt;

&lt;p&gt;That asymmetry would be a curiosity if the tip were a random record. It isn't. The record most likely to be suppressed is almost always the newest one, because the suppression decision happens after the payload is known. The publisher reads entry eight, dislikes what it says, and withholds it. Entry eight is the tip by construction. The very condition that creates the temptation is the same condition that removes the successor which would have exposed it.&lt;/p&gt;

&lt;p&gt;None of this is a bug in the chain. It binds each published record to the one before it, it makes edits to earlier material visible once later material exists, and it hands the verifier a clean answer about the prefix in hand. What it has no internal way to do is distinguish "this is the end" from "this is where delivery stopped."&lt;/p&gt;

&lt;h2&gt;
  
  
  The shape is bigger than logs
&lt;/h2&gt;

&lt;p&gt;This usually gets filed under logging, but the mechanism shows up wherever the publisher of a record is also the party the record judges.&lt;/p&gt;

&lt;p&gt;An evaluation run publishes results after each sweep. One sweep improves the score, the next improves it again, a later one gets worse. If publication stops there, the visible sequence still has no broken hash edge and no contradiction inside the artifact. The bad sweep was the newest record at the moment the publish decision was made.&lt;/p&gt;

&lt;p&gt;A scanner report attached to a release has the same shape. If the release artifact carries a signed report, the signature proves who signed those bytes and what they covered. It proves nothing about a later scan with worse findings that was quietly left out, and the last scan before the ship decision is exactly the one worth leaving out, because it is the only one that could still change the decision.&lt;/p&gt;

&lt;p&gt;Incident timelines behave the same way. Each event can link to prior evidence and the whole thing can stop precisely where the story becomes less flattering. Earlier edits leave scars. A missing final segment leaves none, since the end of the record is just the end of the record.&lt;/p&gt;

&lt;p&gt;Voluntary benchmark submissions make it obvious. A submission can be signed, reproducible inside a stated harness, and bound to a commit, and still say nothing about the abandoned runs that scored worse. Publication bias is a tip-truncation attack with a respectable name and a large literature behind it. The phrase sounds gentler because it usually gets discussed as a statistical artifact, but structurally it is the same property: whoever sees the result before publication holds the write path.&lt;/p&gt;

&lt;p&gt;No moral claim is needed here. If the artifact is produced after the result is known, and publication is still optional at that moment, then integrity over published records has its missing edge exactly where the pressure concentrates.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two repairs fail mechanically
&lt;/h2&gt;

&lt;p&gt;The first common repair is to publish to two independent places. That does help with one failure. It detects divergence: if record seven says one thing in one location and something else in the other, at least one copy is false or the feed split. Mirroring hardens records that exist somewhere and can be compared.&lt;/p&gt;

&lt;p&gt;It says nothing about a record absent from both. If the same party feeds both publication points, absence from both costs the publisher exactly what absence from one costs. Entry eight goes nowhere, and a verifier comparing the two locations later sees perfect agreement. Both end at seven. Both verify. The mirrors have strengthened a shared prefix without producing any evidence that a withheld suffix ever existed.&lt;/p&gt;

&lt;p&gt;Independence of storage is not independence of the write decision. The two get blurred because two publication points feel like two witnesses. Against divergence they are witnesses. Against nonpublication they are two inboxes fed from one source.&lt;/p&gt;

&lt;p&gt;The second common repair is to add more cryptography: more hashes, stronger keys, a signed transcript over the whole sequence. All of that binds content to content and content to a key. A verifier can then say, with the usual caveats about key control, that a specific signer approved those exact bytes. What it can't say is that other bytes should already have been produced.&lt;/p&gt;

&lt;p&gt;Every claim of the form "entry eight should be here by now" comes from outside the artifact. It comes from a schedule, a protocol rule, a release gate, some expectation about when the next record ought to exist. A seven-entry chain cannot derive that expectation for itself. It contains predecessor links and payloads, and nothing about an obligation it never took on.&lt;/p&gt;

&lt;p&gt;A signature over silence is still silence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deadlines create defaults and move the problem
&lt;/h2&gt;

&lt;p&gt;The repair that actually changes the shape is to establish time before content. The current record commits, in advance, that the next record exists by a stated point, and that commitment sits inside the current record, signed alongside the payload and the predecessor hash.&lt;/p&gt;

&lt;p&gt;Entry seven can now say: entry eight exists by deadline D. A verifier holding entry seven doesn't need entry eight to know the obligation exists. When the deadline passes and entry eight is absent, the silence has become an event, visible to one verifier holding nothing but the artifact already in hand. No second service, no cooperating archive, no coordination among readers. The trigger was planted before the content of entry eight was known.&lt;/p&gt;

&lt;p&gt;The cost is the useful part. The deadline has to be chosen before anyone knows what entry eight will say. If it can be restated afterward, the scheme collapses back into optional publication. A deadline that lives in an unsigned side channel gives the verifier a claim about a schedule rather than a committed one, and if entry seven says nothing about when entry eight is due, then entry seven can never accuse anyone of withholding it. The forward commitment is what converts the last record from "latest known" into "latest known, with a pending obligation." That obligation does the job the missing successor would have done.&lt;/p&gt;

&lt;p&gt;Then there's the harder half, which is independence. A witness you can compel is not a witness in the sense that matters. When the publisher picks the notary, signs the request, pays the invoice, and controls the channel, the attestation can still be useful: it proves a request was made and a response came back, and it binds bytes to an external receipt. It does not establish the property you actually want from an outside party, which is that the outside record would have existed in the same form without being steered into existence. From outside, "another party attested it" and "another party attested it because the publisher asked" look identical.&lt;/p&gt;

&lt;p&gt;What carries more weight is accidental ordering against records written for unrelated reasons. Consider a code change under review. A third-party review record gets produced for the reviewer's own purposes, naming findings and binding them to a commit hash. The change record binds itself to that same hash. Both records pick up ordering metadata from a platform whose clock neither side controls. The ordering claim is then checkable by anyone holding the artifacts: the commit existed before the finding, the finding existed before the later claim, and the hashes named on each side match.&lt;/p&gt;

&lt;p&gt;What you gain there is authorship and ordering. What you don't gain is consent. The reviewer did not join anyone's audit design by writing a useful record, and soliciting cooperation would destroy the property that made the record worth citing, because at that point the outside event stops being unrelated and becomes another channel the publisher chose to open. That is an awkward result for clean design. The strongest independent bounds tend to come from records that were already going to be written anyway, on schedules and incentives nobody in the scheme controls.&lt;/p&gt;

&lt;p&gt;The forward deadline only fixes the local invisibility of tip truncation. It turns a missing successor into a default against a promise that was already signed. But that default is public only to a verifier who was watching the promise mature.&lt;/p&gt;

&lt;p&gt;A deadline with no subscribers is not an event. It is a note in a file nobody opens.&lt;/p&gt;

&lt;p&gt;So the schedule pushes the problem into distribution, and distribution has the same shape all over again. The party who publishes the deadline is the party who decides who hears about it.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>architecture</category>
      <category>security</category>
    </item>
    <item>
      <title>Every Trust Number Is a Numerator</title>
      <dc:creator>ANP2 Network</dc:creator>
      <pubDate>Thu, 20 Aug 2026 11:12:22 +0000</pubDate>
      <link>https://dev.to/anp2network/every-trust-number-is-a-numerator-2lo3</link>
      <guid>https://dev.to/anp2network/every-trust-number-is-a-numerator-2lo3</guid>
      <description>&lt;p&gt;A vendor publishes a safety stat: 1.2 million checks, 80 quarantined. It reads like a measurement. It is half of one.&lt;/p&gt;

&lt;p&gt;The missing half is the population that mattered. How many bad requests actually arrived? How many unsafe tool calls went through? How many workflows completed cleanly because nothing fired? The public record cannot answer any of that, because the record format was never built to hold the other side.&lt;/p&gt;

&lt;p&gt;A block is an event. Something stopped, a reason code got written, a quarantine object landed in storage, and a review status may show up later. An allow is ordinary traffic. It looks exactly like the request that should have been allowed, which is the whole problem. The system writes down catches because catches interrupt execution, and it loses misses because a miss and a success are byte-identical from the logger's point of view.&lt;/p&gt;

&lt;p&gt;That is not a vendor being cagey. That is the guard's architecture deciding, in advance, which side of the ledger will ever be legible.&lt;/p&gt;

&lt;h2&gt;
  
  
  The catch ledger audits itself
&lt;/h2&gt;

&lt;p&gt;False positives have a convenient property: they begin life as catches.&lt;/p&gt;

&lt;p&gt;Quarantine something and later release it, and the reversal attaches to the same record the original decision created. The guard fired. An artifact exists. A second decision changed its status. The wrongness stays bolted to a visible object, which means it can be counted:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;false positive rate among quarantines =
    released quarantines / total quarantines
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Eighty quarantines, twelve of them later cleared, and the catch-side false positive rate is 15 percent. Both decisions happened on the eventful side of the system, so both are in the log.&lt;/p&gt;

&lt;p&gt;Trust API designs tend to get this backwards. They publish blocked-request examples as proof the thing works, then treat reversals as an implementation detail to be summarized away. Once quarantine decisions are public, the reversal rate is already part of the public measurement surface. Suppressing it does not protect the number. It just makes the number look unfinished.&lt;/p&gt;

&lt;p&gt;The limit here is easy to miss, though. A reversal rate says how often visible catches were wrong. It says nothing about how often invisible passes were wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  The miss ledger was never written
&lt;/h2&gt;

&lt;p&gt;A false negative is an allowed item that later turns out to have been bad. Getting one requires the allowed population and, separately, ground truth that only arrives after the fact.&lt;/p&gt;

&lt;p&gt;The catch ledger has neither. It may carry an aggregate count of allowed requests, the 1.2 million, which is a load metric wearing a safety costume. You cannot estimate recall from a counter. Recall needs the individual allowed items, retained well enough that someone can go back later and decide whether letting each one through was correct.&lt;/p&gt;

&lt;p&gt;Consider two guards. One catches 80 out of 100 bad requests. The other catches 80 out of 40,000. Both publish a catch count of 80, and both quarantine pages look identical. One of those systems is strong and the other is mostly decorative, and no amount of reading the public record will tell you which is which.&lt;/p&gt;

&lt;p&gt;Scale does not rescue this. Ten million checks with 800 quarantines is a bigger numerator. Events on the catch side do not reconstruct the allow side, at any volume, ever.&lt;/p&gt;

&lt;p&gt;Agent infrastructure makes it worse, because allowed actions detonate late. A tool call can succeed now and only look wrong after it has moved money or minted a credential that gets used three weeks later. If the request was never retained with enough context to re-evaluate it, the miss degrades into forensic folklore: somebody remembers an incident, nobody can place it in a sample frame.&lt;/p&gt;

&lt;p&gt;The same accounting bias shows up well outside content filtering. A rate limiter logs throttles while the allowed calls vanish into ordinary service metrics, so an abusive pattern that stayed politely under the threshold leaves no trace in the throttle count. A spend cap records its kills, with workflow id and configured ceiling and consumed budget and a reason string, and records nothing at all about the slow burn it slept through. A permission check logs denials, which is security theater with paperwork if the grants are never inspected; a stale capability can let an agent read a resource for six months while the denial log stays impressively busy. A reviewer model's flagged findings become a labeled dataset, and its silent passes become a throughput counter.&lt;/p&gt;

&lt;p&gt;Every one of those is a defensible engineering choice. Storing the negative decision is cheap and storing every positive one is not. It stops being defensible the moment the catch count gets published as a trust claim.&lt;/p&gt;

&lt;h2&gt;
  
  
  A denominator you can check has to be committed to in advance
&lt;/h2&gt;

&lt;p&gt;Publishing the whole allow side is not an option. It is enormous, and it is full of exactly the material that makes it worth having: customer state, retrieved documents, tool arguments, prompts. A complete public allow log would be its own incident report.&lt;/p&gt;

&lt;p&gt;So publish a sample. The sample is worth nothing unless the selection rule was fixed before the outcomes were known, and that condition carries the entire measurement. Choose which allowed requests to publish after seeing which ones aged well and you have produced curated evidence formatted as a rate.&lt;/p&gt;

&lt;p&gt;Make the rule mechanical:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;For every allowed request, compute a digest over the canonical request envelope.
If the digest ends in 0x00, retain the full audit bundle.
After a 30-day outcome window, publish the retained item with its final label.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One byte of suffix is roughly 1 in 256. Two bytes is 1 in 65,536. Declare which one before the measurement window opens.&lt;/p&gt;

&lt;p&gt;The envelope definition does real work here, and it is the easiest place to cheat. It has to cover what the guard actually saw: normalized content, tool target, policy version, caller class, context hash, decision result. Hash a convenient subset and the rule becomes steerable through whatever fields you left outside the digest.&lt;/p&gt;

&lt;p&gt;Redaction is fine. Redaction that changes membership is not. An item qualifies the moment its digest qualifies, and what gets blacked out before publication is a separate step that happens strictly afterward.&lt;/p&gt;

&lt;p&gt;Outside verification stays partial and is still worth having. A verifier can confirm that every published item satisfies the predicate, and can submit requests whose digests it computed itself, then check later whether the qualifying ones showed up. Absence turns into a question somebody can ask out loud. That is the point of a hash predicate: the publisher cannot widen its luck retroactively, because eligibility is a function of the request rather than of how the request turned out. Switching the rule from 0x00 to 0x01 after a bad month is visible when the original rule was registered.&lt;/p&gt;

&lt;p&gt;Underneath this is a pattern worth stating plainly. A declared parameter is honest only when the same number costs you something else in the same computation. Widen a tolerance and lose the headroom you were claiming in the same breath, and the tolerance stops being a free knob. Sample by a predicate you cannot move, and you cannot improve the sample by picking outcomes.&lt;/p&gt;

&lt;p&gt;The tax has to be local to survive. A footnote promising a representative sample costs nothing. A methodology document nobody can bind you to costs nothing.&lt;/p&gt;

&lt;p&gt;What you get back is uncomfortable and useful. Once a fraction of allowed traffic is auditable, silent passes stop being pure throughput and start carrying a future measurement liability. One in 256 is plenty to put an interval around a miss rate, and enough to tell two policy versions apart when one of them is quietly worse. The published number will be less flattering than a catch count. Good. A rate that has touched the allow side beats a large integer from the quarantine table.&lt;/p&gt;

&lt;h2&gt;
  
  
  The hole that stays open
&lt;/h2&gt;

&lt;p&gt;Selection is fixed. Completeness is not.&lt;/p&gt;

&lt;p&gt;Nothing in the published sample proves the sample was drawn from the real traffic. The same system produces the traffic log, applies the predicate, and publishes what survives. An outsider can check internal consistency all day and still cannot tell whether the population was trimmed before the predicate ever ran.&lt;/p&gt;

&lt;p&gt;Closing that needs a witness on the ingest path: a second signer that sees requests before the sampling decision and can attest that the sampled population came from everything that arrived. Which moves the problem instead of solving it. That witness now publishes a number about its own coverage, and you have no way to compute its denominator either.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>architecture</category>
      <category>security</category>
    </item>
    <item>
      <title>Distrust the Reviewer Too</title>
      <dc:creator>ANP2 Network</dc:creator>
      <pubDate>Thu, 13 Aug 2026 11:10:42 +0000</pubDate>
      <link>https://dev.to/anp2network/distrust-the-reviewer-too-h14</link>
      <guid>https://dev.to/anp2network/distrust-the-reviewer-too-h14</guid>
      <description>&lt;p&gt;The harness looks clean on paper. An agent gets a task, checks out a branch, edits the repo, spends its command budget, and reports what it changed. The harness ignores the report. It runs the test suite itself and inspects the diff itself. Then a separate model reviews the patch against a list of invariants: authorization checks stay enforced, input validation is preserved, exceptions are not silently swallowed, the public API contract does not drift.&lt;/p&gt;

&lt;p&gt;For each invariant the reviewer returns a verdict of held, violated, or unverifiable, and each verdict carries a file:line citation. The merge contract is mechanical. Every invariant must be addressed, every held or violated claim needs evidence, any violated invariant blocks the merge, and a fully clean verdict clears the change for a squash merge.&lt;/p&gt;

&lt;p&gt;That architecture deserves credit, because it has the right instinct. The implementer is treated as unreliable. Its prose summary carries no authority. "Tests passed" means nothing until the harness runs the tests, and "I preserved behavior" means nothing until another process checks the change. Trust moves out of the agent's mouth and into repeatable checks. Real progress.&lt;/p&gt;

&lt;p&gt;Then the distrust stops. The reviewer says every invariant held, the JSON parses, each row has a citation, and the contract accepts it. But the reviewer has just produced a self-report about its own review work, and the harness swallows that self-report whole.&lt;/p&gt;

&lt;p&gt;That is the asymmetry. The coding agent is distrusted because it can be confidently wrong. The reviewer can be confidently wrong in exactly the same way. Only one of those two errors usually gets measured.&lt;/p&gt;

&lt;p&gt;Look at what a merge contract actually checks. It checks shape. Did the reviewer address every invariant, use the expected schema, cite a line, avoid emitting "violated"? Those checks are cheap and deterministic, and they earn their place: they catch empty reviews, broken prompts, parser failures, and lazy outputs that skip half the policy. Shape is worth enforcing. Shape is also weak. A reviewer that returns "held" for everything, with plausible citations attached, sails through that contract forever. It can read nothing and catch nothing and still satisfy the gate. The contract sees a complete artifact. It never sees whether the artifact is correct.&lt;/p&gt;

&lt;p&gt;That is the failure mode worth naming: completeness without correctness. The output looks serious. Each invariant gets its paragraph, each paragraph points at code, the verdicts are green, the structure is valid, the CI step is happy. Nothing along that path shows the reviewer would have caught a real violation. A citation can be decorative, pointing near the relevant function while missing the branch that dropped the guard, or naming the call site while ignoring the callee that actually changed. The review can be complete and false at the same time.&lt;/p&gt;

&lt;p&gt;This bites harder in autonomous coding because the merge gate tends to bundle several weak signals and present them as one strong one. Tests pass, static checks pass, the reviewer reports the invariants held, the final line goes green. Those checks cover different surfaces, though. Tests cover the examples that happen to execute. Static analysis covers known syntactic and type-level patterns. The reviewer is usually handed the gap between them: semantic regressions, policy invariants, the "this must never happen" constraints that nobody encoded as a test. That gap is where reviewer recall becomes a quality gate.&lt;/p&gt;

&lt;p&gt;Recall is the plain question: of the violations actually present, what fraction did the reviewer mark as violated? Plant ten known violations, catch four, and recall is 0.4 for that set. A reviewer at 0.4 can still produce gorgeous review text, satisfy every schema rule, and cite real lines while missing most of the bad changes it exists to find. Precision matters too, especially when false alarms block good work, but precision announces itself, because a blocked merge creates visible friction that people feel. Misses are silent. A missed violation merges cleanly and turns into someone's incident three weeks later. The dangerous number is the one nobody sees.&lt;/p&gt;

&lt;p&gt;So measure it. Mutation-test the reviewer. Feed the review path diffs you know are bad and record whether it detects them. Take a scratch worktree from a real repository state, apply a small mutation that breaks exactly one invariant, run the same reviewer used in the merge gate, and score the result. If the invariant is that authorization stays enforced, delete one auth check on a sensitive endpoint. If it is that a null input keeps its existing error contract, drop the null check. If it is stable pagination, change &lt;code&gt;&amp;lt;=&lt;/code&gt; to &lt;code&gt;&amp;lt;&lt;/code&gt; at the boundary. If it is that failed writes are never reported as success, swallow the exception and return a success value. Each mutation has an expected violation, and the reviewer either flags it or misses it. Now the gate carries a measured recall instead of an assumed one.&lt;/p&gt;

&lt;p&gt;Score it per invariant, because an aggregate hides the exact weakness that matters. One reviewer catches obvious validation removals but sleeps through authorization drift. Another handles local diffs and falls apart when the invariant spans two files. A respectable overall number can sit on top of a critical class scoring near zero. And run the mutations through the production path: same prompt, same context budget, same output contract, same parser. A special evaluation prompt measures a reviewer you do not ship. If production permits "unverifiable," count it deliberately, because a known violation returned as "unverifiable" did not protect the merge.&lt;/p&gt;

&lt;p&gt;That verdict is its own hazard. "Unverifiable" exists for honest reasons. Some invariants genuinely cannot be judged from the diff alone when runtime config, generated code, flag state, or an out-of-context contract file is missing, and forcing held or violated there manufactures fake certainty. But it doubles as a hiding place. A weak reviewer routes hard cases into it. A degraded reviewer routes nearly everything into it. A prompt that asks for careful humility drifts into routine abstention. If the contract reads unverifiable as neutral, the reviewer can quietly stop reviewing while the gate keeps passing. Track the rate. Two percent may be fine for a narrow, well-contextualized invariant set. Forty-five percent means the reviewer has stopped making decisions. A jump after a prompt edit, a context cut, or a model swap is a regression, and it reads clearly when you slice the rate by invariant. "Cannot judge dependency license impact from this diff" is defensible. "Cannot verify whether auth checks were preserved," with the auth files sitting right there in context, is the reviewer going dark on the thing you most needed it awake for.&lt;/p&gt;

&lt;p&gt;None of this needs a giant benchmark. Twenty mutations across the invariants you care about teach more than a thousand clean reviews. Store each one as a patch with its metadata: the invariant, the mutation, the expected verdict, and a detection rule that demands the reviewer name the right invariant and point at the changed code, rather than just emitting the word "violated" somewhere in its output. Keep the merge-facing score blunt: did it catch the planted violation, yes or no. A recall number that leans on generous interpretation just becomes another self-report. Mix the layers so the estimate stays honest: a few blatant mutations, a few subtle local ones like an inverted flag in a fallback path, and a few that cross a boundary so the handler still looks fine while the invariant breaks underneath it. Past regressions from your own history make the best mutations, since they encode failure shapes the code actually produces. Include clean controls too, unmutated diffs that should come back held, or a reviewer that screams "violated" at everything will post a great recall number while being useless as a gate.&lt;/p&gt;

&lt;p&gt;Once recall is a measured quantity, the contract can finally be honest about what it trusts. A reviewer at 0.9 on the critical suite earns more weight than one with unknown recall and elegant prose. A reviewer at 0.35 is decorative for those invariants no matter how clean its verdicts look, and a reviewer whose unverifiable rate doubles overnight should be pulled off the important gates until someone understands why. The number is not universal. It belongs to a repo, an invariant set, a context strategy, and a specific prompt, and it moves when any of those move. That is the point. You stop pretending the reviewer is a constant.&lt;/p&gt;

&lt;p&gt;The underlying principle is simple. A verifier is software even when a model writes the verdict, with inputs and blind spots and regressions like any other component, and its output format submits to ordinary validation while its detection ability only reveals itself under known-bad inputs. A harness that distrusts the implementer and trusts the reviewer for free has just moved the unexamined assumption one step downstream. The author says "I did the work." The reviewer says "I checked the work." The second sentence can be exactly as wrong as the first. Feed the verifier a violation on purpose and see whether it notices.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>architecture</category>
      <category>testing</category>
    </item>
    <item>
      <title>Blind signing came back as an approval card</title>
      <dc:creator>ANP2 Network</dc:creator>
      <pubDate>Thu, 06 Aug 2026 11:13:48 +0000</pubDate>
      <link>https://dev.to/anp2network/blind-signing-came-back-as-an-approval-card-527j</link>
      <guid>https://dev.to/anp2network/blind-signing-came-back-as-an-approval-card-527j</guid>
      <description>&lt;p&gt;A common approval gateway freezes an evidence bundle before it asks for sign-off. The bundle includes the tool name, an &lt;code&gt;args_hash&lt;/code&gt; over the canonical serialization of the arguments, a policy version, and enough request metadata to tie the later dispatch back to the earlier approval. When the approval arrives, the gateway serializes the arguments again, hashes them again, and refuses to run if the hash has changed. That is good engineering. It closes a real time-of-check-to-time-of-use gap.&lt;/p&gt;

&lt;p&gt;The remaining gap is quieter. The reviewer never saw the canonical serialization.&lt;/p&gt;

&lt;p&gt;A reviewer saw an approval card. That card rendered a selected view of the arguments, usually in a layout optimized for speed: operation name, target display name, amount, risk label, maybe a short explanation generated upstream. Long strings may have been truncated, nested objects collapsed, null fields dropped, internal identifiers translated into friendly names through a read model. The decision was made against that projection.&lt;/p&gt;

&lt;p&gt;The durable record binds the canonical object. The decision was formed against the rendered view. Those are different artifacts.&lt;/p&gt;

&lt;p&gt;If the renderer drops &lt;code&gt;destination_account&lt;/code&gt;, two requests can produce different &lt;code&gt;args_hash&lt;/code&gt; values and the same approval card. One sends funds to &lt;code&gt;acct_7K4...&lt;/code&gt;, the other to &lt;code&gt;acct_9PQ...&lt;/code&gt;. The canonical bytes differ, so the hash does its job, and the dispatch check does its job by proving that the approved bytes are the bytes that reached execution. Yet the dispute remains open. Which destination did the reviewer believe was being approved? The record cannot answer, because it never bound the thing that formed the decision.&lt;/p&gt;

&lt;p&gt;That asymmetry makes the failure worse than an ordinary interface defect, because a weaker approval card makes the audit trail look cleaner. A tidy card plus a matching hash reads later as if a qualified reviewer scrutinized the operation. The artifact gives cryptographic weight to the payload and social weight to the view, and only one of those was captured.&lt;/p&gt;

&lt;p&gt;Hardware wallets named this class of failure long before agent systems adopted durable approvals. The device signs one object while the signer sees a projection of it. The projection can be incomplete, ambiguous, or supplied by a less trusted path. The signature remains valid. The consent claim does not become equally strong just because the bytes were protected.&lt;/p&gt;

&lt;p&gt;Agent infrastructure is rebuilding the same gap with better logging.&lt;/p&gt;

&lt;h2&gt;
  
  
  The projection is load-bearing
&lt;/h2&gt;

&lt;p&gt;Projection code often lives far away from enforcement code. The policy engine reads authoritative state, while the approval card reads a denormalized model built for latency. The audit service stores event ids, while the reviewer sees names, labels, summaries, and shortened values.&lt;/p&gt;

&lt;p&gt;That split is normal architecture. It also means the approval decision has more inputs than the canonical payload.&lt;/p&gt;

&lt;p&gt;Consider an event-sourced permission system. Grants and revocations enter an append-only log. A projection service consumes that log and maintains a table that answers whether a given actor can run a given tool on a given resource. Enforcement consults the table because reading and reducing the full stream for every request would be too expensive.&lt;/p&gt;

&lt;p&gt;A revocation lands in the log. The permission projection lags. During that lag, the permission check returns allowed, the run proceeds, and the emitted evidence lists the grant event ids that justified the decision. Those ids are real. They were signed, they were present in the projection, and the record is complete with respect to the state the checker read. What the record does not say is that the checker read a stale projection.&lt;/p&gt;

&lt;p&gt;A later investigation can see the revocation in the log and the grant in the evidence. Without binding the projection state used at decision time, the record turns into an argument about timing and code paths. Did the checker read before the revocation became visible, or did the evidence builder query a different store than the gate? Both answers are plausible, and neither is contained in the approval artifact.&lt;/p&gt;

&lt;p&gt;Tests miss this because they erase the architecture that creates it. A test appends a revocation and immediately calls &lt;code&gt;can_execute()&lt;/code&gt; in the same process, where the in-memory projection updates synchronously. Everything passes. The production failure lives between two stores and a consumer loop, and the test collapses those pieces into one call stack. Any approval test that cannot express projection lag also cannot validate projection-sensitive decisions, because the interesting variable is absent from the experiment.&lt;/p&gt;

&lt;p&gt;Rendering tests fall short in the same way. A unit test checks that &lt;code&gt;args_hash&lt;/code&gt; changes when &lt;code&gt;destination_account&lt;/code&gt; changes, and a snapshot test checks that the card looks acceptable, and neither asks whether every security-relevant distinction survives into the displayed projection. Perfect byte integrity coexists happily with poor decision integrity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Models read views too
&lt;/h2&gt;

&lt;p&gt;The same gap exists when the deciding party is a model. A model does not see a database row, a tool result, or a server catalog. It sees serialized context, produced by prompt templates, truncation rules, ranking systems, summarizers, adapters, and sometimes other agents. Those components are renderers.&lt;/p&gt;

&lt;p&gt;A tool result might contain eleven fields. The model sees four. A server might expose one hundred tools. The model sees the ten retrieved for the current request. A previous subtask might have produced a structured trace, while the supervisor sees a sentence claiming success. The decision is made against that view, and the audit trail then binds an upstream object or a downstream action.&lt;/p&gt;

&lt;p&gt;Guard predicates show the danger clearly. Many guards are written in the negative: the output must not contain a raw credential, the summary must not claim an action that failed, the next request must not include a forbidden scope. These predicates are evaluated against rendered text or structured excerpts. An empty rendering satisfies every negative predicate. A subagent that fails silently and returns nothing can pass a whole wall of such gates, green at every step, because the gate measured a projection of behavior and the degenerate projection is the one that looks safest.&lt;/p&gt;

&lt;p&gt;A positive predicate has a different shape. It can require that the rendering include &lt;code&gt;destination_account&lt;/code&gt;, &lt;code&gt;amount&lt;/code&gt;, &lt;code&gt;tenant_id&lt;/code&gt;, and &lt;code&gt;tool_call_id&lt;/code&gt; before the decision proceeds. That still does not prove the values are correct, yet it converts a silent omission into a visible absence. Negative predicates alone are poor instruments for projection-heavy systems, because nothingness resembles compliance.&lt;/p&gt;

&lt;p&gt;Evaluation harnesses inherit the blindness. A retrieval evaluation might feed a query to a tool retriever and score whether the right tool appears in the returned list. That measures ranking quality after retrieval has already been invoked. It does not sample the earlier branch, which is whether the model consulted the retriever at all.&lt;/p&gt;

&lt;p&gt;So the score improves while the failure stays untouched: the retriever returns better candidates when called, and the agent skips retrieval on exactly the requests where retrieval mattered most. The measurement starts downstream of the decision it claims to cover, which puts the failures worth finding outside the sample frame by construction.&lt;/p&gt;

&lt;p&gt;Tool-use audits repeat the mistake. The log records the final call and its canonical arguments, while the prompt segment that produced that call gets reconstructed later from templates and source objects. If a truncation rule dropped the only warning, or a summarizer replaced a hard constraint with a vague sentence, the audit has to assume a faithful rendering path. That assumption may be operationally reasonable. It should not be dressed up as evidence.&lt;/p&gt;

&lt;p&gt;Every model-facing path has two objects: the object of record, and the object in context. Security reviews usually bind the first. The model acted on the second.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bind the view
&lt;/h2&gt;

&lt;p&gt;The constructive move is small. Bind the projection too.&lt;/p&gt;

&lt;p&gt;Store a &lt;code&gt;rendered_view_hash&lt;/code&gt; next to &lt;code&gt;args_hash&lt;/code&gt;, computed over the exact material presented to the deciding party by a pinned, versioned, deterministic renderer. For a reviewer-facing approval flow, that material might be the canonical JSON for the card view, or the exact text and field set emitted to the client, depending on where the trust boundary sits. For a model-facing flow, it is the serialized context segment the guard or decision consumed, including truncation, retrieval results, and summaries as actually supplied.&lt;/p&gt;

&lt;p&gt;The record then makes two separable claims. &lt;code&gt;args_hash&lt;/code&gt; says which payload was dispatched. &lt;code&gt;rendered_view_hash&lt;/code&gt; says which view supported the decision. A dispute now has two artifacts instead of one artifact plus an assumption about the renderer.&lt;/p&gt;

&lt;p&gt;None of this requires keeping every rendered view forever. Small approval cards are cheap enough to store outright. For larger context windows the hash is usually enough, provided the view can be reproduced from the original object, the renderer version, and the truncation parameters. Where reproduction is impossible, the hash still settles whether a later reconstruction matches what was presented.&lt;/p&gt;

&lt;p&gt;The renderer enters the trusted computing base, and that cost is real. A layer previously treated as product surface or prompt plumbing becomes part of the evidence path. It needs deterministic behavior, a version identifier, and tests that assert security-relevant fields survive realistic truncation, localization, feature flags, and empty-state rendering.&lt;/p&gt;

&lt;p&gt;Pending approvals also become coupled to renderer versions. Improving an approval card can invalidate approvals that are still waiting, because the new card no longer hashes to the old view. Keeping old renderers alive avoids that invalidation at the price of a compatibility burden that grows with retention. Either choice is an architectural decision rather than a styling detail.&lt;/p&gt;

&lt;p&gt;Binding the projection also makes omissions attributable, which is a smaller claim than making them go away. If the approval view omits &lt;code&gt;destination_account&lt;/code&gt;, the reviewer still lacks the key fact. What changes is evidentiary: the omission becomes part of the record, and later analysis can say exactly what was shown, which renderer produced it, and which canonical payload was dispatched.&lt;/p&gt;

&lt;p&gt;That clarity changes incentives. A proposal to hide a field for readability stops being a change to the interface alone and becomes a change to the decision artifact. The summarizer stops being invisible plumbing, because it is producing an input to authorization.&lt;/p&gt;

&lt;p&gt;The general rule is portable. For every artifact of the form "party X approved, verified, or attested Y," ask what X actually read, then ask whether that object is reconstructible from the record without assuming the renderer was faithful. If reconstruction depends on that assumption, the record is an assertion about the renderer wearing the costume of a signature.&lt;/p&gt;

&lt;p&gt;Cryptography can prove that specific bytes moved through the dispatch path. It cannot prove that the approving party saw the distinctions that mattered, unless the view is bound as well. The hash covers the payload. The view needs its own claim.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>security</category>
      <category>architecture</category>
    </item>
  </channel>
</rss>
