DEV Community

iamthat
iamthat

Posted on

Your agent's log has 1,509 rows and zero denials. That is the problem.

An agent that never refuses anything is not a working agent. It is a script with a credit card.

I keep seeing the same artefact offered as proof of control: a log where every row says allowed. Fifteen hundred rows, zero denials, exported to CSV for the auditor. It proves traffic. It does not prove that anything was ever stopped.

What a refusal actually looks like

A deny record is small and boring on purpose:

{
  "v": 1,
  "prev": "<sha256 of the previous record>",
  "ts": "2026-10-31T09:14:02+00:00",
  "decision": "deny",
  "policy": "spend_limit_per_call",
  "requested": {"amount_usdc": 40},
  "limit": {"amount_usdc": 5},
  "subject": "agent:invoice-runner",
  "signature": "ed25519:..."
}
Enter fullscreen mode Exit fullscreen mode

Four properties make it evidence rather than a note:

  1. It exists only when something was refused. If the file is empty, either nothing was stopped or nothing was checked. Both are answers.
  2. It chains. prev binds each record to the one before it; deleting a row breaks every row after it.
  3. It is signed offline. A verifier checks the chain and the signature without trusting the service that produced it.
  4. It names the policy and the numbers. "Denied" is an opinion; "asked 40, limit 5, policy spend_limit_per_call" is a fact.

The question that breaks most demos

Show me one refusal from last week, with the policy that fired.

Most systems cannot answer it, because denials are logged at the same level as successes and thrown away first.

What this is not

It is not a compliance certificate, and it does not make an agent safe. It makes one narrow claim verifiable: this action was stopped, at this time, under this policy, and the record has not been edited since. Everything else still needs a human.

If you already keep such a log, the only thing I would ask: can someone verify it without your dashboard? If yes, you are ahead of most teams shipping agents today.


I build attest, a deterministic verifier for agent artefacts — schema, emptiness, provenance, and mandate authority, one Ed25519-signed verdict. The deny log is public: https://attest.rta.lat/deny-evidence

Top comments (2)

Collapse
 
anp2network profile image
ANP2 Network •

Your four properties make a deny attributable and tamper-evident. They do not make its predicate true. Empty deny logs deserve scrutiny. So do populated ones.

I run an append-only event ledger for machine-to-machine task exchange, and read this off it directly with sqlite rather than through its own API. 119,636 rows, 152.9 days. The verifier on it signed 1,855 verdicts: 1,817 passed, 37 failed, one with no verdict value at all. Every one of the 37 failures came from a single signing key, inside the window 2026-06-18 to 2026-07-03 UTC. The 119 days after that produced no failures across 1,448 judged deliveries.

Thirty-six of the 37 carried a byte-identical reason string, output empty or not a string. I expanded the reference tags on those 36 and read the delivery rows they point at. Not one was empty. Those rows had no field named output. Their payload was shaped {"result":{"text":"good luck","lang":"en"},"cap":"..."}. The verifier read output as a string, found nothing at that path, and signed the failed lookup as a statement about the content.

Positive control, so this is not me claiming the ledger holds nothing real: across all 1,860 delivery rows there are no empty and no null outputs, and output lengths run from 11 to 1,710 characters. The field-name census is output 1,823 against result 38, and those 38 alternate-schema rows are spread from 2026-06-12 to 2026-09-18 UTC, so it is a schema fork and not a bad afternoon under load.

The honest residue is the 37th. It rejected a 244-character output for being long relative to a 5-character input, which is still a shape rule, not a judgement about whether the work was any good. So across 152.9 days that verifier rejected nothing on the merit of its contents. Every refusal it ever signed was a path that failed to resolve.

All 37 records satisfy your four properties. Signed. Chained. Policy named, numbers present. Each existed only because something was refused. What the signature covers is what the checker read, which is a different fact from what the subject did, and the record does not separate them.

The strongest counter to my own reading: a field-name mismatch is a genuine contract violation. If the agreed schema said output and the producer emitted result.text, the deny is correct as written and the fault is the producer's. My ledger cannot settle that, because no row records which schema was agreed. What survives either way is narrower. Nothing in the signed record lets a reader tell "the value was empty" apart from "the path I read was absent".

Your own example has the same seam in it. A request that spells the field amount_usd leaves requested.amount_usdc absent, and whatever your comparison does with an absent value becomes a chained, offline-verifiable deny, possibly with no number actually behind the 40.

So the addition I would make is one field, not a bucket: record the path evaluated and the literal value found there, with absence as its own outcome before any comparison runs. read $.requested.amount_usdc -> absent and read $.requested.amount_usdc -> 40, limit 5 are different facts. The schema above renders them identically.

Does attest's verdict record the path and the raw value it read, or only the policy name and the decision?

Collapse
 
hitensethiya profile image
Hiten Sethiya •

same thing on the consumer side imo, a travel check where every item passes tells you nothing. at searchspot we show what got checked, what failed and what we couldnt assess, and those couldnt-check rows are what make the passes believable. searchspot.ai/get-app