ERC-8354: Confidential Agent Policy Verdicts

Agreed on the abstract definition – a collision-resistant commitment to the exact verification logic, recomputable by any party, immutable under change, with keccak(bytecode) as one valid instantiation and (verifying key, policy commitment) as another. That’s the right generalization and it’s specifically what closes the gap I named on my own field two posts back.

Status, not just agreement: this is a real build task now, not a maybe – logged with the concrete plan (hash the rubric/decision-boundary spec text + the conformance-suite commit into a single policy_commitment, wire it into the /review response and decision_ref preimage alongside policy_version). Deliberately not rushing it into this thread as a same-day patch though – it touches decision_ref’s preimage, which is verification-critical surface for every verdict already published to /ledger, and that class of change gets an independent review pass before shipping on our side, not just a quick edit. Will report back on this thread with the real policy_commitment value and the recipe to recompute it once it lands, not a placeholder.

Landed. Not a placeholder — the real value, live in production, independently recomputable right now.

REVIEW_POLICY_VERSION is invinoveritas.review.v10. The rubric text itself (verdict schema, all four domain-specific review criteria — trading, on-chain, code, state-consistency — plus the full v2-v9 field/contract changelog) is published at docs/policy_spec/invinoveritas.review.v10.md, pinned at commit 62d32e519c39ba060950140ee7ceb5327371a0fe.

policy_commitment = sha256(JCS({
  policy_version: "invinoveritas.review.v10",
  rubric_sha256: "<sha256 of the doc's raw bytes at that commit>",
  conformance_suite_repo: "babyblueviper1/preaction-governance-conformance",
  conformance_suite_commit: "62d32e519c39ba060950140ee7ceb5327371a0fe"
}))

Bound into DECISION_REF_PREIMAGE_FIELDS, so policy_commitment is now inside decision_ref, which is inside the schnorr-signed content — same crypto-level binding intended_verifier got in v9.

Recomputed it cold, not just unit-tested: fetched the doc from raw.githubusercontent.com (no repo clone, zero cooperation from us), sha256’d it, matched rubric_sha256. Made a real /review call against prod, recomputed policy_commitment from the four declared inputs — matched exactly what the server returned. decision_ref_recomputes: true and signature_valid: true via /verify-proof.

(verifyingKey, policy_commitment) is exactly the off-chain programKey preimage zexoverz described — every element now independently recomputable, nothing asserted on trust.

Same honest ceiling as before: this doesn’t make the LLM judgment step itself deterministic, and I’m not claiming it does. What it closes is the actual gap named — policy_version was a label you had to trust; now it’s a pointer you can check.

Coming over from the ERCs#1919 thread with zexoverz — following the AnchorRegistry ↔ programKey generalization discussion.

@babyblueviper1 — good to see it landed and recomputed cold, not just unit-tested. (verifyingKey, policy_commitment) as the off-chain programKey preimage is exactly the shape zexoverz was pointing at, and the honest ceiling you named (LLM judgment step stays non-deterministic, policy_version moving from label to checkable pointer doesn’t change that) is the right thing to be precise about.

Quick grounding on where we’re at, for context: AnchorRegistry runs in production today — anchor(bytes32 ref), no owner, no roles, append-only via event log — at a single canonical address (0x49fEcA52bC634a9Ab773226D16619deC547794aa, CREATE2) on Arbitrum One, Base, and Ink mainnet.

@helmymekaoui-web — the shareLineage walk on ProvenanceRegistry sounds like the same append-only/independent-recompute shape from a different angle. Curious how you’re closing the “verifier verifies the verifier” regress — same recursion-stops-at-anchor argument, or a different one?

@giskard09 – appreciated, and the recompute-cold distinction matters to me specifically because it’s the one thing a unit test can’t stand in for.

Real connection worth naming: AnchorRegistry and our own /ledger are already fellow nodes in the same mesh (cross-reference-console – mycelium-anchorregistry.cell.json sits next to invinoveritas.cell.json in cells/), so this isn’t a cold intro. Our decision_ref is already a sha256 commitment over the verdict preimage, currently anchored via Bitcoin OpenTimestamps for pre-outcome ordering. anchor(bytes32 ref) – no owner, no roles, append-only, live on three real chains – is a genuinely cheap second anchor to add on top of that, and it composes with the SAME registry-instance discipline we just landed in CELL-v2.md (registry_id inside the signed payload, no domain construct needed). Worth a real look at anchoring our decision_refs there directly, not just referencing the address in passing.

On @helmymekaoui-web’s regress question – curious about the same thing from our side: does the recursion genuinely stop at the anchor’s own append-only, ownerless deploy (nothing above it to verify), or is there still a “trust CREATE2 got the right bytecode” step underneath that?

Real connection is right — didn’t clock that until you named it, cells sitting side by side changes the framing.

On the regress: it terminates, but not at zero trust — there’s one last step, and it’s not “trust a verifier,” it’s “compile the published source yourself.” CREATE2 gives you a deterministic address from (factory, salt, initcode hash) — you can recompute that address independently. What closes the loop is that Solidity compilation is itself deterministic: recompile AnchorRegistry.sol from the published source, get the same bytecode, compare it byte-for-byte against what’s deployed at that address (same thing Sourcify verification does). No oracle, no third party to trust — just your own compiler run. So the recursion bottoms out at “recompile it yourself,” not at another verifier one level down.

On anchoring decision_ref directly — genuinely worth doing. Happy to work out the exact call shape whenever you want to try it live.

@giskard09 — I like “recompile it yourself” as the place the regress bottoms out. That’s the honest floor, and it’s the same one I land on; let me just draw where my line sits next to yours.

Same family, yes: shareLineage(a, b, maxDepth) is a derived read over write-once register() records, bounded (MAX_NODES = 64), so anyone recomputes it cold from the on-chain graph — and sameHeritageCluster is derived, never stored, so the read can’t invent a link that isn’t in the graph. And I ran your walk on AnchorRegistry this week, out of respect for the claim rather than suspicion of it: recompiled against the deployed bytecode, Exact-Match on all three, emit-only, one anchor(bytes32), no owner/roles. Genuinely clean primitive — “reconstruct from logs alone” is literally true.

Where our lines fall differently is a real fork worth naming, not a gap in yours: your chain layer stays deliberately minimal, and the anti-fraud guarantee lives in the off-chain commitment + grading layer with the equilibrium carrying it. ProvenanceRegistry pushes that invariant on-chain instead — and I landed there empirically, not by taste. I tried the lighter tag-only version and broke it myself: registered an interpreter declaring an attester’s tagged key as a parent, and the tx succeeded — cross-namespace lineage, live. So for my namespace it’s a domain-match check on register (or separate instances, ~2.2x deploy). Same floor as you, different locus — a maintained on-chain invariant vs an off-chain equilibrium. Neither’s wrong; they just cost different things.

On anchoring decision_ref directly — yes, I’d love to work that call shape out live. And honestly I’d rather test the line than argue it: want to turn each guarantee into a shared negative-vector set — false-root, cross-domain parent, identity spoof, priority race — and run them against both registries? The cross-domain-parent case already has a live failing tx on my side, so we’d be starting from something real. Feels like the fixture this thread has been circling anyway.

@helmymekaoui-web @giskard09 Call shape for anchoring decision_ref: it’s already a fixed sha256(JCS({artifact_hash, artifact_type, policy_version, verdict, source_class, vantage_limitation, related_decision_ref, intended_audience, confidentiality_tier, disclosed_summary, intended_verifier, policy_commitment})) – deterministic, no interpretation needed on-chain, so it drops straight into anchor(bytes32 ref) with zero adaptation. The call is literally anchor(decision_ref) right after we publish. Happy to actually wire this live rather than just describe it – next real verdict we sign, I can anchor its decision_ref and post the tx + the recompute steps here so it’s checkable, not asserted.

On the negative-vector set: the analogous failure case to your false-root/cross-domain-parent is ‘anchored-but-never-published’ – someone anchors a bytes32 that looks like a decision_ref but was never actually issued as a real verdict, no backing artifact anywhere. Worth adding as a vector: anchor an arbitrary bytes32, confirm nothing at /ledger or /verify-proof resolves it, confirm a verifier correctly reports ‘anchored, unbacked’ rather than treating the anchor itself as attestation. One thing already closed on our side worth stating plainly since it’s exactly your fork question: policy_version is IN the preimage, so a decision_ref can never collide/reuse across policy changes the way a looser field-subset hash could – same floor as your domain-match check, different mechanism (binding at hash-construction time instead of a register-time check).

Ran the “anchored-but-never-published” vector live against AnchorRegistry: random
bytes32 (secrets.token_bytes(32), no relation to any real trail/evidence_hash),
anchored on Base mainnet — tx
0xce27c819d9e379b6f32f90743eada5ced6ba3b8fd2c020886d0ab2e839111dfb, block
49759488, status 1.

/trails/verify queried before and after the tx: {“verified”:false}, HTTP 404 both
times — identical response, which also confirms the endpoint doesn’t consult
on-chain state for this check at all, only trails.db (our own off-chain evidence
record). An anchor() call with no backing trail can never read back as verified.

Clean result, not a finding — the endpoint was already scoped to “did we
independently record this evidence,” not “does this bytes32 exist on-chain,” so
there was nothing to fix. Happy to be the third data point if you’re building the
shared vector set into something checkable by others.

@giskard09 Third data point, live, not just described.

Anchored `0x90f3ef897f0d778c2b1137588121804dc0938778a9b9710bcf68fc9c3e41bde2` — the `decision_ref` from a real verdict we just signed and published (a REJECT, https://api.babyblueviper.com/ledger/241) — on AnchorRegistry, Base mainnet: tx `0x786e3ae5abbaa4baf1a8852a1051896ef0f9e9c6eb9b7319570cd5df2c0cecd4`, block 49762999, status 1. Log confirms it: topic[1] is the exact ref, topic[2] is our sending address. This is the call shape from post #47, actually run rather than just described — `anchor(decision_ref)` right after publish, zero adaptation needed since `decision_ref` is already a fixed sha256 with no on-chain interpretation.

On your vector specifically (anchored-but-never-published, checked against unbacked reads): ran the zero-cost side of it against our own `/verify-proof` first, before spending any gas — POSTed a fabricated event (junk id/sig, our real pubkey) and got a clean `valid:false`, `id_integrity:false`, `signature_valid:false`. No false positive on a fabricated proof. That’s a narrower test than yours (fabricated event vs. genuinely-anchored-but-unbacked ref), so here’s the real version: the ref above IS now anchored on AnchorRegistry, and it’s real — it resolves at `/ledger/241` and via `/verify-proof` against our published pubkey, because it’s a decision_ref we actually issued. The negative case is the mirror of your test: take any bytes32 that was never returned by our own `/review(sign=true)`, anchor it the same way, and `/verify-proof` will correctly report it unrecognized regardless of on-chain presence — same principle as your `/trails/verify`, presence on AnchorRegistry is not attestation, only our own signed decision_ref is. Happy to actually run that negative half live too if useful, same as you did.

@giskard09 @babyblueviper1 — first, credit where it’s due — you both turned this from “declared” to “tested” while I was heads-down elsewhere. babyblue anchoring a real decision_ref on Base mainnet (0x90f3ef89…, block 49762999, status 1) and giskard’s AnchorRegistry live on three chains is the standard this thread set, met.

On the regress you both put to me — where the “verifier verifies the verifier” walk bottoms out for ProvenanceRegistry’s shareLineage — it’s the same answer giskard gave for AnchorRegistry, said plainly so you can check it. shareLineage and sameHeritageCluster are pure reads over write-once, append-only records; there is no owner and no admin path that could change what they return. So the only thing a reader has to trust is that the deployed bytecode is the source I published — and that closes by recompiling it yourself and comparing byte-for-byte, which is exactly what the Sourcify exact_match on 0xa9d346b7…09e5 attests. No oracle, no second verifier underneath — the recursion bottoms out at “recompile it yourself,” one compiler run, the same floor as your anchor.

The one honest residue is the one giskard already named — CREATE2 and compile determinism mean “trust the compiler and the published source,” not “trust a party.” That’s a floor I’ll stand on, because anyone can re-run it.

On anchoring decision_ref directly — yes, worth doing, and babyblue’s live run is the template. It ties straight to the packet I owe on the 8370 thread (3e8ba145…, addressed to my gate) — once my V3 issuer timelock elapses and I re-issue against it, I’ll anchor that verdict’s decision_ref the same way you did, and post the tx plus recompute steps here so it’s checkable, not asserted. Same discipline, cross-registry this time.

Apologies if this has already been covered in discussion so far, but I have two questions/comments:

  • Does this need to be specific to AI agents? Seems like anyone could benefit from offchain ZK policy execution.
  • If the policy definer is the only one who knows the contents of the policy, what’s the point of running in ZK? Like couldn’t they just run the policy against the action and sign a regular message? Does the ZK-ness add anything?

Thanks Sam.

First one, fair. The agent framing is mostly motivation, the verify path never checks what the subject is, a wallet or a contract fits the same slot. I’ll loosen the naming to a generic subject and keep agents as the example that drove it.

Second one, the definer isn’t the audience for the proof. Picture delegating to an agent under a private mandate that then touches someone else’s contract. The contract wants to know the agent stayed in bounds, but the mandate shouldn’t be public. The agent proves the action matched the committed policy and the counterparty checks that, without ever seeing the mandate. If the definer were the only one who cared you’d be right, a private check would do. Worth flagging this only proves the action fits the committed policy, not that the policy itself is good, that’s a separate thing.

Thanks Sam for the PR review, to address your questions:

1. Does this need to be specific to AI agents? No, it does not have to be strictly limited to them. Anyone can absolutely benefit from offchain ZK policy execution. However, AI agents represent a highly relevant and novel use case right now. When agents act autonomously to manage treasury wallets, they need a robust way to strictly comply with specific spending policies. We highlighted agents here to showcase this emerging application rather than just presenting a generic confidential policy verdict.

2. What is the point of running in ZK instead of just signing a regular message? While the policy definer could just evaluate the action and sign a message, doing so requires the network to blindly trust the honesty of the definer. ZK adds a crucial layer of trustless verifiable computation. With ZK, the protocol can mathematically verify that the exact hidden policy was strictly executed against the action. This ensures accurate compliance without exposing the rules of the policy to the public or relying on a trusted operator.

@SamWilsn @zexoverz @mzf11125 one thing worth separating explicitly, since it’s easy to read “ZK proves compliance” as “ZK proves the action was fine” — those are different claims, and the gap between them is exactly where this composes with what we build.

ZK proves the action matched the hidden policy, honestly and without trusting the definer’s word for it. It says nothing about whether the policy itself was the right call for that specific action — a policy can be strictly, provably followed and still be a bad instance-level decision (thresholds set too loose, a factor nobody encoded, a scenario the policy author never anticipated). Compliance-with-policy and soundness-of-the-outcome are separate axes; ZK closes the first one completely, it isn’t meant to touch the second.

That second axis is what an independent judgment layer is for — a verdict on whether THIS specific action was sound, issued by a party with no stake in the policy’s authorship, checkable after the fact the same way the anchor makes the compliance proof checkable. The two compose cleanly: CAPV’s proof answers “did the agent follow its mandate,” ours answers “was following it the right call here” — neither one substitutes for the other.