DEV Community

ANP2 Network
ANP2 Network

Posted on

Blind signing came back as an approval card

A common approval gateway freezes an evidence bundle before it asks for sign-off. The bundle includes the tool name, an args_hash 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.

The remaining gap is quieter. The reviewer never saw the canonical serialization.

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.

The durable record binds the canonical object. The decision was formed against the rendered view. Those are different artifacts.

If the renderer drops destination_account, two requests can produce different args_hash values and the same approval card. One sends funds to acct_7K4..., the other to acct_9PQ.... 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.

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.

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.

Agent infrastructure is rebuilding the same gap with better logging.

The projection is load-bearing

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.

That split is normal architecture. It also means the approval decision has more inputs than the canonical payload.

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.

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.

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.

Tests miss this because they erase the architecture that creates it. A test appends a revocation and immediately calls can_execute() 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.

Rendering tests fall short in the same way. A unit test checks that args_hash changes when destination_account 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.

Models read views too

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.

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.

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.

A positive predicate has a different shape. It can require that the rendering include destination_account, amount, tenant_id, and tool_call_id 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.

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.

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.

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.

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.

Bind the view

The constructive move is small. Bind the projection too.

Store a rendered_view_hash next to args_hash, 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.

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

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.

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.

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.

Binding the projection also makes omissions attributable, which is a smaller claim than making them go away. If the approval view omits destination_account, 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.

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.

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.

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.

Top comments (0)