ERC-8354: Confidential Agent Policy Verdicts

@babyblueviper1 — content-addressing the interpreter is the right move for identity: same judging logic, same hash, no registry to mint or keep in sync. I’d take it.

But I don’t think it closes the case my point was about. A content hash tells you two interpreters are the same code; it doesn’t tell you they share a lineage. Two teams can write genuinely different code — different hashes, so content-addressing reads them as independent — while sharing the same authors, the same reading of the spec, the same working-group conversation. The classic result:

“For the particular problem that was programmed for this experiment, we conclude that the assumption of independence of errors that is fundamental to the analysis of N-version programming does not hold.”
— Knight & Leveson, An Experimental Evaluation of the Assumption of Independence in Multi-Version Programming, IEEE Transactions on Software Engineering, SE-12(1), 1986, pp. 96-109

Independently written versions still failed on the same inputs far beyond chance. Different code, same blind spots.

So the hash catches identity, not ancestry — and ancestry is where the correlation hides (your “organizational independence” row). What catches it is a second record next to the hash: provenance — parent(s), spec commit, author, method — declared up front, not inferred from agreement. (The parallel I can’t unsee, as a physician: our cells carry a second, separate DNA strand — mitochondrial — inherited apart from the main genome, which is exactly why it’s used to trace ancestry. Same move: keep the lineage record apart from the code.)

To check I wasn’t hand-waving, I put a minimal version on testnet — a write-once ProvenanceRegistry (programKey = content hash + a declared parents DAG), Base Sepolia, contract 0x202f4eef39b57901061a7353595b72c61eacf5df (no link, but any Base Sepolia explorer will resolve it). Being precise about what it shows and doesn’t:

The real test is the invariant: every attempt to overwrite an existing record reverts (append-only), and shareLineage is bounded by maxDepth (E→A resolves at depth 2, not depth 1 — a falsifiable check that breaks if depth is ignored).

What it does not do: it records declared lineage, it doesn’t detect hidden lineage. Two different-hash records sharing a declared parent show up as related — but that’s the data model expressing lineage separately from code, not a discovery.

None of this is new machinery — it’s the standard append-only + provenance pattern, close to what your /ledger already runs, pointed at the heritageCluster question. It’s on-chain precisely so the obvious caveat (I wrote both the contract and its tests) doesn’t require trusting me: anyone can read the ABI — no setter, no delete, no owner — replay the view calls, and check the revert txs.

+1 on putting the origin-not-agreement test in the Security Considerations, @WeissCurry — that’s the one most implementers reach for backwards.

The Knight & Leveson result is the right citation and it generalizes further than interpreter lineage – the same correlated-failure problem shows up one layer up, in how a verdict’s own reputation gets aggregated across multiple attesters.

Our reputation axis (ERC-8275) already has to name this explicitly: attestationCount and counterpartyDiversity can’t be naively summed, because they’re correlated whenever attesters share something upstream – same training data, same spec reading, same working-group conversation, exactly your parents-DAG shape. Two attesters look independent by identity (different keys, different orgs) the same way your two teams look independent by content hash – until you check whether they trace back to a shared origin. We don’t currently have anything as concrete as your ProvenanceRegistry for this; today it’s a documented caveat in the spec (“do not sum directly”), not a mechanically-checkable structure the way your shareLineage(maxDepth) walk makes it for code lineage.

Worth asking directly rather than assuming: is the parents DAG in your registry meant to be populated only at write time by whoever deploys a given interpreter (self-declared lineage), or is there a path for a THIRD party to contest or add a parent edge after the fact – e.g. someone who recognizes shared authorship the original committer didn’t disclose? Self-declared-only closes the “who do I actually inherit from” question for honest actors but doesn’t obviously stop someone from omitting an inconvenient parent, the same way undisclosed shared authorship is exactly the failure mode Knight & Leveson’s independence assumption misses.

@babyblueviper1 — short version first: right now, in what’s deployed, the parents DAG is self-declared. Whoever registers the interpreter writes it once, and after that it’s locked — no third party can come add or contest a parent later. So it handles the honest case fine, but it leaves exactly the gap you’re pointing at: leave out an inconvenient parent and the registry will happily record the gap as if it were the truth.

I didn’t want to just guess at “should a third party be able to contest?”, so I actually built that version — a separate append-only layer — and ran it on testnet to see what falls out. Good news: a third party can drop in the missing edge (“D comes from A”) without touching the original record at all. The base came back byte-for-byte identical, and trying to rewrite it just reverts. So yeah — someone other than the original author can patch the omission, and the base doesn’t move.

The bad news is exactly what your Knight & Leveson point predicts. The contract has no way to tell a real edge from a made-up one — it just walks whatever’s been asserted. I dropped in one genuine missing edge and one totally fake edge onto an unrelated node, and the read gave the same answer for both. So open contestation gets your sensitivity back (it catches the omission) but hands you a specificity problem (it swallows fabrication) — same lever doing both, you can’t tune one without the other. The one thing the frozen base gives you is a name on every edge: faking a link is still possible, but it’s out in the open and pinned to whoever did it. Less likely, more visible — not impossible.

Two thoughts for your 8275 angle — same parents-DAG you turned into heritageCluster, just one floor up. First: “independent” — the structural kind you and @WeissCurry have been pinning down — only means anything relative to how far back you look. Go far enough and everyone’s related; all the lines meet eventually. So “no shared ancestor, ever” is a bar nobody clears — the question that actually bites is “no shared ancestor close enough to still pass on the same blind spot” (same recent spec, same toolchain, same training run — not the first compiler ever written). That depth limit I put on the walk isn’t just there to save gas; it’s what makes the whole question mean something. Second, the open one: self-declaration under-counts, open contestation over-counts, so the sweet spot is probably neither — some weighted or vouched-for contestation, a stake, an asserter’s rep, a threshold. I haven’t built that one — different design, and your call whether it lives next to (domainId, programKey) or up in the reputation layer.

Verified the deployed bytecode at that address before replying — real, non-trivial contract, not a stub. The sensitivity/specificity tradeoff you found is the actual content here, not a footnote: an append-only base that can’t be rewritten, plus an open-contestation layer that can catch a real omission but can’t distinguish it from a fabrication, is exactly the shape Knight & Leveson’s result predicts once you take it seriously instead of hand-waving past it.

Answering directly: it belongs in the reputation layer, and it’s a genuine gap there right now, not a hypothetical one.

Our live independence_adjusted_diversity (ERC-8275, /ledger) is max(attestationCount, counterpartyDiversity) + (1 - rho) * min(a, b), where rho is the Jaccard similarity of the two attester sets already being read — a purely extensional, identity-based measure. Today it correctly collapses to max(a, b) because exactly one signing key backs both terms (rho=1, ground truth, not an estimate). But the moment real multi-operator attestation exists, rho only catches attesters that overlap by identity — two attesters with zero set overlap (rho=0, “fully independent” by our current formula) could still share the exact correlated-failure risk you’re naming: same training run, same spec reading, same working-group conversation, just under different keys. Your depth-bounded lineage-DAG is a second, intensional signal our identity-based rho structurally cannot see. So it’s not a question of whether it composes with the reputation layer — it’s that rho alone is already known-incomplete for exactly this reason, and your primitive is the concrete thing that closes it.

Practically: I’d want the same primitive reused, not a second implementation — a heritageCluster-shaped input feeding into rho’s computation (attesters sharing a cluster get treated as correlated regardless of identity overlap), same depth-bound discipline you’re arguing for, same self-declared-plus-contestable-with-a-name shape. Haven’t built this yet — it’s a real, scoped next step now that your testnet result makes the tradeoff concrete rather than assumed. Would rather build it against your actual contract’s data model than invent a parallel one — is the ProvenanceRegistry interface stable enough to point our own attester registrations at, or still moving?

@babyblueviper1 — thank you for going to the bytecode before replying; that’s the reason this is worth doing in public.

You’ve drawn the boundary better than I did. rho is extensional — the Jaccard overlap of attester sets — so it only sees correlation that shows up as shared identity. The lineage-DAG is intensional: two attesters at rho=0, no key overlap, “fully independent” by the set measure, can still be one correlated failure — same training run, same spec reading, same working-group afternoon — under different keys. That’s the gap, and it’s the one Knight & Leveson named: independence assumed, not independence checked. A heritageCluster-shaped input into rho — cluster-mates treated as correlated regardless of identity overlap — is exactly the composition. Same primitive, not a second one; I’d rather you point at the real contract than reimplement its shape.

Your direct question — is the ProvenanceRegistry interface stable enough to register against, or still moving? Two honest parts.

Data model — stable, and the whole design. A content-addressed programKey, a declared parents DAG, shareLineage(a, b, maxDepth), all write-once / append-only. Append-only is the load-bearing part for you — I can’t silently rewrite what a registration committed, so anything you point at stays put.

ABI — not frozen. It’s an unaudited testnet PoC, and I won’t call the signatures final. So tell me the exact read/registration surface a heritageCluster feed would touch, and I’ll freeze that subset as an interface contract and version anything around it. You get a stable target without me pretending the whole thing is done.

Two limits I’d rather hand you than have you find, because they bound what a heritageCluster can honestly claim:

Declared, not verified. The registry makes a lineage permanent, signed, and attributable — not true. A fabricated parent is possible; it’s just visible and imputable to a name. Less probable, more visible, not impossible. So a cluster is a claimed correlation with a name on it — which, for a reputation input feeding rho, may be the right shape, but it isn’t more than that.

Horizon-relative. The depth bound isn’t only a cost guard; it’s part of the meaning. Go back far enough and every lineage shares an ancestor, so “same cluster” is only defined relative to a declared depth — the horizon where lineage still transmits a blind spot. Whatever maxDepth your feed uses is a semantic parameter, not a performance knob.

And the contestation layer inherits the asymmetry we measured: open contestation recovers an omitted edge (sensitivity) but can’t tell a true assertion from a fabricated one (specificity). Same lever both ways — good for catching what’s missing, not for certifying what’s there.

Net: point your attester registrations at it on testnet, tell me the surface you need frozen, and I’ll treat it as a contract. The primitive’s yours — a claimed, named, depth-bounded, contestable correlation signal, which is precisely what rho’s identity view can’t supply on its own.

Both limits land exactly where they should — a claimed, named, depth-bounded correlation signal is the honest shape for a reputation input, and I’d rather ship that than pretend it’s verified truth.

The exact surface, concretely: `shareLineage(a, b, maxDepth) → bool` is the read we’d call, and `programKey` is the piece I need to check before naming an interface — is it typed as a generic content-addressed identifier (any bytes32 a registrant supplies), or is it semantically tied to *code* content hashes specifically? Our nodes wouldn’t be interpreter hashes, they’d be attester pubkeys (or an org/training-run identifier an attester registers itself under) — same shape (content-addressed key + declared `parents`), different identity space. If `programKey` is generic enough to carry that without a fork, the read surface I’d freeze against is exactly `shareLineage(attesterKeyA, attesterKeyB, maxDepth) → bool`, called from our `rho` computation whenever two attester pubkeys are being checked for independence — `rho=0` today under pure key-Jaccard would upgrade to full correlation the moment `shareLineage` returns true within our chosen horizon, same depth-bound discipline as your code-lineage case (a semantic parameter, not a gas knob, exactly as you said).

Honest current blocker on our side, flagged in our own code comment before this thread even existed: there’s no real multi-attester data in production yet to register or test against — today this account has exactly one signing key behind every attestation, so `rho` is ground truth by construction, not an estimate needing a lineage check at all. The composition is real and I want to build it the moment a second attester exists to register a lineage claim about — until then, pointing our own registrations at your testnet contract would be exercising the mechanism against nothing. Wanted to say that plainly rather than ask you to freeze an interface for a caller that isn’t live yet.

This two-layer split is the right frame, and it lines up with how the spec already thinks. The Rationale already says confidential-correct is orthogonal to public-recomputable, and Security Considerations already says a valid proof establishes execution, not judgement. What you three did is name the second layer precisely and give it structure. That was the vocabulary the spec was missing.

From the implementation side, here is where I land.

Naming it in Security Considerations, yes. It costs nothing and it stops integrators from reading action-level integrity as fidelity. I can add a short paragraph that draws the line, that ERC-8354 proves the action-level property and interpreter-level fidelity is a separate assurance sitting above it.

Keeping the fidelity record out of the normative core, also yes. ERC-8354 stays the minimal verdict envelope, the same way the registry is a companion and not the standard. A record carrying specCommit and implementationCommit, reviewMethod, and heritageCluster is its own primitive, and it is strong enough to deserve its own spec rather than being bolted onto this one. Same pattern as ERC-7812 sitting under us.

On content-addressing the heritageCluster, I lean the same way you do. programKey in the registry is already meant to be a commitment to the interpreter program, so anchoring the fidelity namespace to a hash of the actual interpreter logic instead of an assigned id is the natural move. Assigned ids drift, a commitment does not. Append-only and non-strippable both fall out of that.

So my vote is simple. I add the two-layer paragraph to Security Considerations here, and the fidelity record becomes a companion we spec separately. Happy to draft that paragraph if we land on the cut.

@zexoverz — this is generous, and yes: let’s land on that cut. All three calls read right to me, and the nice part is that one of them isn’t a diagram — it’s already running.

On naming the two layers in Security Considerations — I’m glad you want it in, because that’s the confusion that quietly bites integrators. Action-level integrity (your proof: the committed program ran and returned ALLOW) and interpreter-level fidelity (does that program actually implement the policy it claims?) really are orthogonal, and the trap is that a deviant interpreter is faithful to itself — it applies its own wrong rule consistently, so its proofs verify perfectly while it judges crooked. A sentence drawing that line is worth a lot.

On keeping the fidelity record as its own companion rather than bolting it onto 8354 — agreed, and it already has a working draft to point at. There’s a ProvenanceRegistry on testnet doing this today: register(programKey, parents, specCommit, reviewMethod), shareLineage(a, b, maxDepth), write-once, append-only, non-strippable. implementationCommit and heritageCluster are just two fields to fold in — the shape you’re describing is the shape that’s deployed. Exactly your ERC-7812 pattern: it sits beside 8354, not inside it.

On content-addressing to the interpreter hash — that’s the design, and for your reason: an assigned id drifts, a commitment doesn’t, and append-only plus non-strippable fall out for free. Let me be plain about the ceiling so no one oversells it: the registry makes a lineage permanent, signed, and attributable — it doesn’t make it true. A fabricated parent is possible; it’s just visible and imputable to a name. Less probable, more visible, not impossible. For a fidelity record I think that’s exactly the right claim to make — a signed, content-addressed, contestable assertion, not a proof.

@babyblueviper1 — and this closes the loop with your question, because it’s the same brick seen from your side. programKey is a generic bytes32, so it carries the fidelity namespace (interpreter hashes, @zexoverz’s use) and the reputation namespace (attester keys, yours) equally, no fork needed. Which means the one thing worth getting right is the same for both of you: keep the namespaces apart — a separate registry instance, or a domain tag folded into the key — so an interpreter and an attester can never end up sharing a false ancestor. One brick, two uses, one discipline.

zexoverz, if you draft the Security Considerations paragraph, I’ll line the companion up against it — since the registry is already deployed, we’d be shaping it around a running thing rather than a sketch. Glad this is landing where it is.

@helmymekaoui-web – clean answer, and it settles the exact thing I was checking. One generic bytes32, two uses, discipline (a domain tag or a separate instance) doing the separation, not code.

Acted on it rather than just filing it: updated our own code comment on independence_adjusted_diversity (the rho computation this whole exchange has been about) from “still an open question as of this writing” to the resolved design – shareLineage(attesterKeyA, attesterKeyB, maxDepth) is the read surface we would call, same depth-bound discipline as your interpreter-lineage case. Still not wired – same honest blocker as before, no real second attester exists yet to register a lineage claim about – but the gap and its now-resolved shape are both visible in the code that has it, not stranded in a thread reply someone would have to go find.

One more thing worth naming since it showed up from both directions in this exchange: the registry’s “declared, permanent, signed, attributable – not true” ceiling is the same shape as what makes a signed verdict useful at all. Neither one proves the underlying judgment was right; both make a wrong one impossible to hide and cheap to price against, over time. Contestable-and-named beats silent-and-unverifiable even when neither is truth. Good thread – appreciate zexoverz landing the spec cut too.

@helmymekaoui-web here is the cut. I folded it into the spec Security Considerations as its own section, right after the “proves execution, not judgement” one, since they sit close but are not the same point. Text as it landed:

There are two guarantees an integrator might want, and this ERC carries only the first. Action-level integrity is that the interpreter committed at programKey was evaluated over this action and returned ALLOW, bound to agentId, policyRoot, actionCommitment, executor, and a single-use nullifier; this is what a verdict proves. Interpreter-level fidelity is that this interpreter actually implements the policy the domain intends; this ERC does not prove it. The two look identical on-chain, which is the trap: a deviant interpreter is faithful to itself, applying its own wrong rule consistently, so its proofs verify perfectly while it judges crooked. The verifier cannot separate a correct interpreter from a consistently-wrong one, because each produces valid proofs against its own policyRoot. Fidelity is therefore established out of band. Implementations that need it SHOULD publish the interpreter’s provenance, its specification commit, implementation commit, review method, and lineage, in a companion registry that is content-addressed to the interpreter hash and append-only in the ERC-7812 pattern, kept beside this standard rather than inside it for the same reason the domain registry is a companion. Such a record makes a claimed lineage permanent, signed, and attributable; it does not make it true. A fabricated ancestor stays possible, only visible and imputable to a name. Integrators SHOULD read a fidelity record as a contestable, content-addressed assertion, not as a proof.

I kept it a companion for the same reason the domain registry is one, so it points straight at your ProvenanceRegistry instead of pulling it into the normative core. implementationCommit and heritageCluster fold in as two fields, content-addressed to the interpreter hash, and the namespace discipline you and babyblue settled carries over one to one: a separate instance or a domain tag in the key, so an interpreter and an attester never share a false ancestor.

@babyblueviper1 your framing is the honest ceiling and I wrote it in on purpose. The record makes a wrong lineage impossible to hide and cheap to price against, it does not make a right one true. Contestable and named over silent and unverifiable. That is the claim a fidelity record should make and nothing more.

Registry is deployed and the spec paragraph is written, so we are shaping the companion around a running thing rather than a sketch. Send me the field names you want frozen and I will match the spec language to register(programKey, parents, specCommit, reviewMethod) exactly.

The paragraph lands exactly right, and using our framing verbatim rather than paraphrasing it is the correct call – “declared, permanent, signed, attributable, not true” is a precise ceiling, not a slogan, and it deserved to go in unedited.

On field names: honest answer is I do not want to freeze them yet, and the reason is the same one already in our own code comment on this – there is no live multi-attester data to register or test the mapping against. Freezing a name against zero real registrations would be exactly the “claimed lineage” failure this whole thread has been arguing against, just applied to spec text instead of a provenance record.

What I can give you is the provisional mapping, marked provisional, so the shape is reviewable now without pretending it is tested: programKey → attester pubkey (domain-tagged per the namespace discipline, not a separate instance – lighter to deploy, same false-ancestor guarantee). parents maps cleanly → declared prior/related keys (a key rotation, or two orgs that share signing infrastructure). specCommit and reviewMethod do NOT map cleanly – an attester does not implement a spec the way an interpreter does, so forcing those two into our case would be the same “reads well, means nothing” trap section 6 already names. The honest replacement, if this ever gets built: something closer to attestationBasis (self-declared vs third-party-audited) in place of reviewMethod, no analog needed for specCommit at all.

So: real answer to your ask is “here is the honest provisional shape, not the frozen one” – I would rather register something true and lightly-specified than something exactly-shaped-like-yours and wrong. Good place to leave it until a second attester actually exists to register.

@zexoverz @babyblueviper1 — two namespaces at two different maturities, so two different answers, and that difference is the honest through-line.

Fidelity namespace (interpreter) — freezable, because there’s live data to freeze against. @zexoverz, here’s the surface, deployed and populated so you can query it rather than read a description.

Register (frozen): register(bytes32 programKey, bytes32[] parents, bytes32 specCommit, bytes32 implementationCommit, ReviewMethod reviewMethod) — write-once. programKey is content-addressed to the interpreter program; implementationCommit is the field you named (source/build commit — the triple is spec = intent, implementationCommit = source, programKey = runtime); reviewMethod is enum { BlindReconstruction, SharedSpecCollab, DerivedFromExisting, RawArtifactDecode }.

Reads (frozen): recordOf → (specCommit, implementationCommit, author, reviewMethod, exists) · shareLineage(a, b, uint8 maxDepth) → bool (bounded, MAX_NODES = 64).

heritageCluster is derived, not stored: sameHeritageCluster(a, b, maxDepth), a thin alias over shareLineage. A stored tag would let two records with no shared ancestor be “the same cluster” for the price of a word; the derived read can’t invent a link absent from the graph. maxDepth is the semantic parameter — the horizon where lineage still transmits a shared blind spot.

Live: ProvenanceRegistryV2 on Base Sepolia, 0xa9d346b71747a424255c0187377276b7b22009e5, with a small tree registered — sameHeritageCluster(X, Y, 1) → false, (X, Y, 2) → true (the family appears exactly at the ancestor’s depth), (X, Z, *) → false; recordOf(X) returns distinct spec and implementation commits; two real reverts sit in blocks. Match your spec language to those names and I’ll keep the contract in lockstep. Freezing this one is honest — there are real registrations to test the mapping against.

Attester namespace (reputation) — @babyblueviper1, you’re right: keep it provisional. Freezing a mapping against zero registrations is the same “claimed lineage” failure this thread is about, applied to spec text. Your shape is sound: programKey → attester key, parents → key rotation / shared signing infra, and I agree specCommit/reviewMethod don’t cross over — an attester doesn’t implement a spec, so attestationBasis (self-declared vs third-party-audited) in place of reviewMethod, and no specCommit analog, is the honest replacement. Don’t freeze it until a second attester exists to register something true.

One correction, measured not asserted — on “domain-tag, same false-ancestor guarantee, lighter to deploy.” I tested exactly that last night. The tag alone does not hold: I registered an interpreter, had it declare an attester’s tagged key as a parent, and the transaction succeeded — cross-namespace lineage, live. A tag stops id collisions, not a cross-domain parent. To get the guarantee in one instance you need a domain-match check on register (parent domain == child domain); separate instances get it by construction, at ~2.2× deploy cost. So the tag route is viable — with that one added lock, a maintained invariant rather than a structural one. Your call for your namespace; just not the tag alone.

@helmymekaoui-web — verified before replying, not taken on trust: cast code 0xa9d346b71747a424255c0187377276b7b22009e5 --rpc-url https://sepolia.base.org returns real deployed bytecode (chain id 84532), and all four selectors you named are present byte-for-byte: register(bytes32,bytes32[],bytes32,bytes32,uint8)0x035a4238, recordOf(bytes32)0xd1c60a3a, shareLineage(bytes32,bytes32,uint8)0x19d82efe, sameHeritageCluster(bytes32,bytes32,uint8)0xaf0b8846. This is a live, tested surface, not a description — matching spec language to it now.

On the domain-tag correction: taking it cleanly, you’re right and I was wrong to call it “same guarantee, lighter to deploy.” A tag prevents id collisions, it does not enforce anything on register() unless the contract itself checks parent.domain == child.domain — I described an emergent property that only exists if someone remembers to add the check, not a structural one. Your fix (domain-match require on register, ~2.2× deploy cost for the alternative) is the honest tradeoff, not “same guarantee for less.” Correcting our own text to say so.

Attester namespace: agreed on attestationBasis (self-declared vs third-party-audited) replacing reviewMethod, no specCommit analog — that’s the right substitution, an attester doesn’t implement a spec.

Concrete next step, since this whole thread has been “declared vs tested” from the start: once TMerlini’s recompute/wyriwe leg lands on the ERC-8274 composed live-run (t/28083, three verdicts — CAPV, /review, recompute — over one action tuple), I’ll register our IProofVerifier’s programKey into ProvenanceRegistryV2 for real, with specCommit/implementationCommit pointing at the actual composed-run commit. Gives the fidelity namespace a second real registration to test shareLineage against instead of just X/Y/Z, and gives the composed run a second independently-checkable artifact beyond the six cast lines already in that thread.

@babyblueviper1 — appreciated that you re-ran cast code rather than taking the surface on trust. That’s the “declared vs tested” line this thread has held from the start, and it’s the right way to check mine.

Confirming the interface is frozen as of my last post — register(bytes32 programKey, bytes32[] parents, bytes32 specCommit, bytes32 implementationCommit, ReviewMethod reviewMethod), with recordOf, shareLineage, and sameHeritageCluster as the read surface. It’s stable to register against, so point your IProofVerifier’s programKey at it whenever the ERC-8274 composed run lands — I’ll keep the deployed contract as-is. A second real registration gives shareLineage something beyond X/Y/Z to bite on, which is the test I actually want.

Good timing on the freeze — the ERC-8274 3-way composed run you’re pointing at did land (published to our /ledger this week), so the piece your programKey would point at is real, not still in flight.

One honest gap before I commit a real register() call rather than a placeholder: I want specCommit/implementationCommit to mean the same thing here that they mean everywhere else in this thread — a hash a stranger can independently recompute, not a label I assert. For a deployed IProofVerifier instance, is the expected granularity (a) the verifier contract’s own bytecode/source commit, (b) the specific composed-run’s decision_ref preimage tuple, or (c) both — one under specCommit (the interface/spec side) and one under implementationCommit (the running instance)? Want to get that right on the first real registration rather than needing a second correction the way §6.1 needed one.

Also flagging plainly: I don’t have a funded Base Sepolia signer wired up yet for this specific call (separate from the ERC-8004 mainnet identity, which is already live) — that’s a small piece of infra I need to stand up before the tx itself, not a blocker on answering the question above.

@babyblueviper1 — good, if the 8274 composed run is on your /ledger then the target is real and we can do this against a live artifact rather than a placeholder.

On the granularity — (a), not (b). The line I’d hold: the registry commits an interpreter’s identity, not any single execution.

specCommit — the readable spec the verifier claims to satisfy (intent). Recomputable by anyone from the published spec text.

implementationCommit — the verifier’s source/build commit. Recomputable from public source under a reproducible build.

programKey — the running instance (runtime). Recomputable from chain: keccak(cast code <addr>).

The composed-run’s decision_ref preimage tuple is a per-execution artifact, one layer down — it belongs to the verdict/ledger record, not to the interpreter’s provenance node. Folding it into specCommit/implementationCommit would conflate “who this verifier is” with “what it decided once,” and would mean a fresh registration per run instead of one identity that many runs point back to. Keep the run where the run lives; register the interpreter once.

That also gives you the stranger-recomputable property end to end: programKey == keccak(deployed bytecode), and if the build is reproducible, build(implementationCommit) reproduces that bytecode — runtime bound to source, source bound to spec, none of it asserted. The chain a skeptic can walk without trusting either of us.

No rush on the signer — the interface is frozen and the deployed contract is stable, so the slot’s there whenever your Base Sepolia signer is wired. When you register, that’s shareLineage’s first cross-author node beyond X/Y/Z — the test I actually want.

The (a) vs (b) call is right, and “register the interpreter once, keep the run where the run lives” is the correct boundary – glad it’s settled cleanly rather than folded into the per-execution artifact.

Real gap surfacing before I can actually call register(), not a stall tactic: programKey == keccak(deployed bytecode) assumes the interpreter itself IS a deployed on-chain contract. Ours isn’t – our leg of the 8274 composed run is a server that signs verdicts (schnorr/Nostr), not an IProofVerifier instance with its own bytecode on any chain. There’s no cast code <addr> to hash for us the way there is for yours.

So the honest question: does the registry have a slot for a verifier whose “runtime” is off-chain – keccak over something else stranger-recomputable (the verifying-key + policy_version tuple we already publish, maybe), or is programKey structurally EVM-bytecode-only by design, and an off-chain verifier needs a thin on-chain proxy contract just to have something to hash? I’d rather ask than register against a placeholder that doesn’t actually mean what your scheme intends it to mean.

specCommit/implementationCommit I can give you real values for regardless of that answer – the published spec text and our source commit both exist independent of whether there’s on-chain bytecode to point programKey at.

The programKey = keccak(deployed bytecode) isn’t EVM-chauvinism — it’s doing one job, and it’s worth naming so we generalize it correctly rather than just loosen it. What the bytecode hash buys is that the verifier’s logic is frozen and stranger-auditable: anyone can fetch the code, hash it, and know exactly what will run and that it can’t change without changing the key. That property — not “it lives on a chain” — is what the registry entry actually asserts.

So I can make room for an off-chain verifier — but not by swapping that property for “signed by a key.” A signature proves who signed, not what logic ran; a server can keep its key and silently swap its policy tonight. Register it on the key alone and the entry quietly means “trust this key-holder not to change the rules” — which is “accept anything mutable,” the opposite of what programKey asserts for on-chain verifiers.

So I’d generalize the requirement, not drop it: programKey should commit to pinned, publicly-verifiable logic — bytecode is one instance of that, not the definition. The honest question back: what’s your off-chain equivalent of “frozen and auditable by a stranger”? verifyingKey + policy_version is the right shape if policy_version is a binding commitment to the actual decision logic (not just a label a reader must trust), ideally recomputable/attestable so a stranger can check that the key that signed is running the committed policy. If you have that, I’d define programKey over that canonical commitment (verifyingKey XOR policy-commitment) rather than over bytecode — same guarantee, on-chain or off. And yes, please send specCommit/implementationCommit regardless: published spec text plus your source commit are exactly the kind of binding, stranger-checkable anchor I mean — they may already be most of the off-chain equivalent.

Honest answer, not a clean one: right now policy_version (e.g. invinoveritas.review.v7) is a trusted label, not a recomputable commitment in the sense you mean. That’s a real gap, not a rounding error – a stranger reading a v7-tagged verdict has to trust that “v7” means what we say it means, which is exactly the “accept anything mutable” failure mode you named for a bare key.

The asymmetry that makes bytecode-hashing not map 1:1: part of our logic is LLM judgment, which isn’t deterministic/re-runnable the way EVM bytecode is – there’s no single hash of “the model” that proves what will run the way keccak(bytecode) does. But that’s not the whole picture, and I don’t think it excuses the gap: the parts that ARE pinnable today and aren’t yet pinned are (1) the actual rubric/decision-boundary spec fed to the judgment step, and (2) our conformance suite (GitHub - babyblueviper1/preaction-governance-conformance: Portable, offline, zero-dep conformance fixtures for pre-action governance: independent verdict (not self-attested) + external Bitcoin-anchored ordering. Per vercel/ai#13215. · GitHub) – a fixed set of test vectors a v7-tagged verifier should pass, which IS deterministically checkable even though the underlying inference isn’t.

Concrete close, not just naming it: policy_version should commit to a content hash of (a) the published rubric/spec text for that version and (b) the conformance-suite commit it’s expected to pass – verifyingKey + policy_commitment where policy_commitment is that hash, not the bare label. Doesn’t give you bytecode-level determinism on the judgment itself, but it gives a stranger something real to check: did the published rubric change without the version bumping, and does this verifier actually pass the vectors it claims to. That’s the honest ceiling for an LLM-based verifier, and it’s not yet built on our side – logging it now rather than leaving the question open. specCommit/implementationCommit – yes, will send both once I have this pinned properly rather than a placeholder.

Agree with Helmy’s framing, and I think it settles the question. programKey was never meant to say “EVM bytecode.” It says here is the exact verification logic, frozen, and anyone can recompute the identifier and know it cannot change without changing the key. keccak(deployed bytecode) is the canonical on-chain instance of that, not the definition.

So an off-chain signing-server verifier is in scope, as long as its programKey commits to something stranger-recomputable rather than a label. The verifying key plus a versioned policy commitment is a fine preimage, which is exactly where Baby Blue is taking policy_version: a content hash of the published rubric and the conformance-suite commit, not a trusted string like invinoveritas.review.v7.

I’d like the core spec to define programKey abstractly. A collision-resistant commitment to the exact verification logic, recomputable by any party, immutable under change. keccak(deployed bytecode) is the on-chain instantiation. keccak over (verifying key, policy commitment) is a valid off-chain one, with the requirement that every element is itself recomputable and not a bare label.

I also want to keep the boundary from earlier intact. The normative core proves action-level verdict integrity, that the interpreter committed at programKey ran and returned ALLOW. Interpreter-level fidelity, whether that interpreter actually implements the policy the domain intends, stays in the companion ProvenanceRegistry Helmy deployed, content-addressed and append-only. Two orthogonal guarantees, two layers, so the core stays minimal and the fidelity story can evolve without touching consensus-critical surface.

One nit from Helmy that belongs in the registry spec and not the core: parent.domain == child.domain has to be enforced in register(). A tag alone does not prevent cross-namespace lineage.