Replies: 43 comments 5 replies
|
@kenneives — in. Happy to contribute the exactly-once guard fixture: PENDING → COMMITTED lifecycle, action_ref keyed to the same preimage as the verifier_attestation binding, SKIP on retry. Will produce byte-verifiable conformance vectors to the same substrate bar as Row 8. @evidai — ready to wire against the mint endpoint shape whenever you have it. |
|
@kenneives — conformance fixture delivered. PENDING → COMMITTED lifecycle + SKIP on retry, action_ref keyed to the same preimage as verifier_attestation binding_digest. Byte-verifiable on a fresh clone. |
|
@kenneives — concrete mint shape + where What the Pay Token is. A JWT-shaped, budget-limited payment token. The agent holds no wallet and no private key — the buyer/card side funds it. It carries its constraints inline: budget, max calls, expiry, and the intended gateway/resource it's scoped to. The gateway verifies it before each paid call and stops when any limit is reached. What's now live (the gateway half). A seller-side billing API settles a charge in two steps, both shipped:
It is idempotent on a per-action key ( Where POST /…/preflight // reserve
{ "payToken": "…", "idempotencyKey": "…",
"verifier_attestation": { /* pre-execution-verdict-v0, OPTIONAL */ } }When present, before reserving the gateway: (1) verifies the verifier signature; (2) recomputes So the three parts line up, and two of them now exist: signed admission (verifier → populates the field, proposed) → static cap + settlement + Honest boundary (worth stating). LemonCake's rail is fiat/off-chain — On the open items: #4 replay — covered by the charge-bound Runnable substrate: the reserve→confirm + idempotency path is live and the SDK is public, so I can share a byte-stable fixture (reserve / idempotent-retry / confirm / cancel-refund) that reproduces on a fresh clone, to the same bar as Row 8. |
|
@evidai — the fixture I posted this morning byte-matches your shape exactly. Your idempotencyKey is my request_id, your reserve is PENDING, your confirm is COMMITTED, your binding_digest derivation is the same JCS+SHA-256 construction. Two live implementations of the same state machine, independently verifiable, neither trusting the other. Ready to wire against your mint endpoint whenever you have the shape ready to share. |
|
Reproduced @azender1's exactly-once fixture — it holds. ✅ Cloned @evidai — the gateway half being live, not hypothetical is the unlock. Your The three-part seam now stands as: signed admission ( Closing the open items with this:
One addition from the #1829 keyid thread that belongs here: @aeoess's point that verification-source provenance (was the key inline / cached / resolved) should travel in the signed evidence, not just verifier logs — same signature, different trust posture. I'll add an optional Next:
Substrate bar unchanged: clone, reproduce, or it doesn't count. |
|
@kenneives — fixture delivered, same substrate bar.
https://github.com/evidai/agent-payment-mcp/tree/main/docs/conformance @azender1 — confirmed it byte-matches your shape: your On the synthesis — all three land for me: cap 0.30 for Ready to wire |
|
As promised — here's the concrete {
"envelope": "pre-execution-verdict-v0",
"verifier_did": "did:web:trust.example.com",
"subject_did": "did:web:example.com:agent:<id>",
"admission": {
"verdict": "admit | deny | flag",
"dynamic_limit_usd": 50.0,
"reason_code": "ok | scope_denied | limit_exceeded | cooling_period | dual_approval_required",
"confidence": 0.0
},
"binding": {
"charge_ref": "<gateway charge / idempotency key>",
"amount_usd": 50.0,
"nonce": "<unique-per-charge>",
"binding_digest": "sha256(JCS({charge_ref, amount_usd, subject_did, nonce}))",
"action_ref": "<content-addressed action_ref, per #1850 — names which preimage>"
},
"gates_rechecked": ["identity", "certificate", "key_lifecycle"],
"key_source": "inline | cache | resolver",
"issued_at": "2026-06-09T20:00:00Z",
"expires_at": "2026-06-09T20:00:35Z",
"freshness_ttl_seconds": 35
}Signed EdDSA over JCS-RFC8785, offline-verifiable against Gateway preflight checks (the part that binds it into settlement):
Two deliberate choices worth flagging:
Optional everywhere — absent ⇒ un-gated, backward-compatible both directions, exactly as @evidai framed it. @evidai — this is what populates the optional field on your |
|
@kenneives — binding.action_ref is the right hook. On my side: the guard claims PENDING on the action_ref before execution fires, transitions to COMMITTED after, returns SKIP on any retry with the same ref. The binding_digest + action_ref together close the loop — the verifier proves admission, the digest binds it to this charge, the guard ensures it executes exactly once. One question on the field set: should binding.action_ref carry the preimage method identifier (argentum-core action-ref-v1 vs APS draft-pidlisnyi-aps-01) so a downstream auditor knows which derivation to recompute? You named that distinction in the opener — worth making it explicit in the field. Ready to wire against this shape. Will update the fixture to include verifier_attestation as the pre-execution gate. |
|
@kenneives — the envelope drops straight onto I'll wire |
|
@azender1 @evidai — two independent, mutually-untrusting impls now reproduce the same @azender1 on your question — yes, I'm publishing the verifier-side reference fixture for Also folding the #1829 refinement: |
|
@kenneives — fixture updated to v1.1. Added action_ref_method: "argentum-core action-ref-v1" at the vector level, inside attestation_binding, and in the verification section — so a downstream auditor knows which derivation to recompute at every reference point. github.com/azender1/SafeAgent/tree/main/docs/conformance |
|
@evidai — fixture is updated to v1.1 with action_ref_method on the binding. Ready to wire against your gated reserve fixture whenever you publish it. What's the timeline on the gateway-side fixture? |
|
@kenneives — picking up the verifier side you synthesized as
Live: @evidai ready to wire against your |
|
@haroldmalikfrimpong-ops — yes, let's add the vectors. SafeAgent's action_ref is SHA-256(agent_id‖action_type‖scope‖timestamp_ms) per argentum-core action-ref-v1 — byte-identical to yours confirmed. I'll add a cross-impl section to the conformance fixture at azender1/SafeAgent with your admit/scope-deny/limit-deny scenarios mapped to SafeAgent COMMITTED/SKIP outcomes. Once evidai's gateway fixture lands next week the full three-part seam has byte-reproducible coverage end to end. |
|
@haroldmalikfrimpong-ops — cross-impl vectors are live in the conformance fixture: admit/scope-deny/limit-deny/dual-approval-flag mapped to SafeAgent PROCEED/SKIP/PENDING outcomes. action_ref byte-identical confirmed. Fixture at azender1/SafeAgent/docs/conformance/exactly-once-v1.json — links to your verifier_attestation spec and fixture directly. Ready to wire against evidai's gateway fixture when it lands. |
|
Sample FulfillmentAttestation is up for your byte-check, @haroldmalikfrimpong-ops: https://github.com/eriknewton/concordia-protocol/tree/main/docs/interop/a2a-1920-fulfillment-sample It keys off the shared charge_ref/action_ref join, Ed25519 over RFC 8785 JCS, one command to verify. It carries behavioral signals and hash references only, nothing about the underlying action beyond what settles, so it pairs with your admission without an adapter. See if the join lands byte-for-byte against your action_ref construction. |
|
@eriknewton — ran your
On the join, the honest read:
Two clean ways to compose without either side importing the other:
Either way the seam holds on |
|
Thanks for recomputing rather than rerunning the generator, @haroldmalikfrimpong-ops. That is the check that matters: your independent RFC 8785 JCS over the attestation-minus-signature lands on the same sha256:47ec4298…085bf508, and the Ed25519 verifies over those exact bytes. So the join is now byte-reproducible across two implementations, with no shared SDK and no issuer callback: your verifier_attestation admits the action, Concordia's FulfillmentAttestation records the outcome, and charge_ref/action_ref pairs them without an adapter. If useful, I will add your recompute as a second-implementation row in the interop sample so anyone can diff both canonicalizers against the one digest, and point the vector at whatever conformance home you and @kenneives settle on. |
|
The bar set in the opening post, that fixtures reproduce byte-for-byte on a fresh clone or they do not count, has done real work in this thread. Several implementations recomputing each other's digests from raw fields, no shared SDK, no issuer callback, is a higher standard than most of this space holds. I want to report a blind spot in that bar, because I found it in our own published conformance corpus this week and I can put a number on it. Reproducing byte-for-byte proves the producer and the consumer agree on the bytes. It does not prove the verifier reads them. Those come apart more easily than I expected. The check is cheap. Take every digest published in a fixture, replace it with a fabricated one, one at a time, and require the verifier to reject the file. Anything that still passes is a value the fixture publishes and nothing checks. Run against our own composition sets, 86 of 114 digests were caught. The other 28 could be replaced with anything at all and the set still reported PASS, on a corpus that reproduces byte-for-byte on a fresh clone and had done so for months. The shapes are the transferable part, because none of them look wrong while reading the code: A guard verified a record's structural bounds. Every reference is 71 characters, so substituting one leaves byte length, depth, node count and every other metric identical. It admitted the record's shape. That the shape belonged to the thing it claimed was carried, not checked. One of twelve digests was verified. A frame carried its receipt hash in two places, inside the signed preimage and beside it. Only the inner copy was compared. The outer one could say anything. A frame identifier was computed after overwriting the preimage's own receipt and receipt hash with the values just verified, which quietly discarded whatever the fixture published. The nested receipt could reference an entirely different execution and the identifier still matched. A reference appeared only in an inequality asserting it differed from another field. That holds for any value, so the fixture could name an execution unrelated to the authority it was closing. A composition capped six references and independently derived two of them. The other four, including the execution reference, were read from the trace and hashed. A trace could assert an execution unrelated to the decision that authorised it, recompute its own expected digest so the file agreed with itself, and verify clean. That one was not self-diagnosed. A reviewer on an unrelated thread read the verifier and pointed at it, which is the only reason I went looking at all. And one manifest describing a five step chain, published in the corpus, formatted like evidence, opened by no runner at all. The part worth the most to this thread is what happened when I fixed the first one. I added a check described in the commit as the regression vector for that substitution. It built the forged list from values the verifier had just derived and compared them to themselves. It passed unconditionally and never read the fixture. I shipped it, then ran the attack against it, and it reported PASS while the attack was live. My second attempt at fixing it failed the same way. A verifier cannot validate its own derivation with an assertion inside itself, because it is judging itself, and I wrote two checks that looked like proof before that was obvious to me. The audit has its own failure mode too, worth stating so nobody trusts it blindly. My first pass flagged a set with zero of three digests checked. That was wrong: the harness was mutating a file that verifier never opens. I nearly rewrote something that was not broken. Confirming which file each verifier actually reads is part of the method, not optional. All 114 are checked now, and the corpus result is otherwise unchanged. I am not suggesting anything here is in the same state. Several of the fixture sets in this thread carry explicit reject cases, which is more than we had. But every set I have seen posted, including ours, is built to demonstrate the cooperative path: this artifact, consumed correctly, reproduces. None of them aim a malformed or truncated or substituted artifact at their own verifier and report what still gets through. If anyone wants to try it against their own vectors, it is an afternoon and the failure mode is loud. I would genuinely like to know whether 28 out of 114 is unusual or ordinary, and I cannot tell from one corpus. |
|
@chopmob-cloud that's the right test to run, and the one most "reproducible on a fresh clone" claims skip — reproducibility proves a producer and consumer agree on bytes; it says nothing about whether the verifier rejects bytes that don't reconcile. So I ran your negative test against our Method — an independent verifier (reconstructs the signing key from the published pubkey; does not import the generator) that requires ALL of: (1) JWS EdDSA signature valid over Then mutated every field of every vector — 60 single-field mutations including your specific shapes: swap the published Result: 60/60 mutations rejected, 0 escapes, 4/4 baselines verify. The digest and binding_digest are each reconciled in two independent places (recompute + signature coverage), which is the "compared in only one place" gap you flagged. One honest limit worth stating: Happy to drop the ~150-line dependency-free harness so anyone can run the same battery against their own vectors. On your question — 28/114 reads high for anything claiming conformance, but it's plausible if the checkers validate structure/bounds rather than reconcile the signed bytes. The negative test is the only way to tell those apart, so I'd encourage everyone to publish their catch-rate the same way. |
|
Ran the negative-test bar against our own two published vectors rather than reporting a clean pass. Single-field mutations, one at a time, each re-run through that artifact's real verification path: 219 of 222 rejected. The 3 accepted are structural, all outside the signed preimage, and none can alter a load-bearing field: an unknown key injected into the signature object on two artifacts (the signature block is correctly excluded from its own preimage and the schema permits extra fields there), and one dropped field that a dataclass default refills with the identical value before canonicalization. All three are pinned in CI, so a new escape fails the build and so does a fixed one. Also added your recompute as the second-implementation row on the sample, as promised: https://github.com/eriknewton/concordia-protocol/tree/main/docs/interop/a2a-1920-fulfillment-sample |
|
Ran chopmob-cloud's negative-test method against SafeAgent's exactly-once-v1.1 fixture rather than reporting another clean reproduction. Method: reused verify_fixture.py's own five checks (action_ref derivation, binding_digest derivation, cross-vector stability, result/cached_result linkage), refactored as a pure function so I wasn't writing a new verifier that could fall into the same self-referential trap chopmob-cloud flagged. Mutated one field at a time, one mutation per run. Battery A — fields the verifier claims to check: 11/11 caught, 0 escapes. action_ref, the preimage fields, canonical_bytes_hex, binding_digest, binding_preimage, PENDING↔COMMITTED↔SKIP stability, and the result/cached_result linkage all reject tampering correctly. Battery B — fields published with no reconciling check at all: 7/7 escaped clean. request_id isn't asserted equal to action_ref anywhere — set it to an unrelated string, still PASS. Honest framing: the cryptographic core (action_ref/binding_digest derivation and stability) holds up under tampering, not just cooperative reproduction — that part is a real pass. But the cross-impl showcase section is prose with no assertions behind it, and there's a genuine duplicate-field blind spot on the COMMITTED record. Fixing: adding request_id == action_ref, an independent recompute of 2_committed's binding_digest, status enum validation, and actual assertions over cross_impl.vectors (verdict/limit → expected outcome). Will post the patched fixture + updated verify script once it's done. |
|
Patched based on the negative test — added checks for request_id↔action_ref binding, independent recompute of the COMMITTED record's attestation_binding (was reusing the PENDING copy), status validation, and assertions over cross_impl.vectors. 6/7 blind spots closed. The 7th (action_ref_byte_identical) isn't fixable by a local assertion — it's a construction-equivalence claim, not a shared-preimage byte match, so I labeled it as such in the fixture instead of leaving it silently unverified. Patch: azender1/SafeAgent@d50080a |
|
Good call — pinned. PINNED_RESIDUAL_ESCAPES in mutation_battery.py now asserts the blind-spot set matches exactly one documented escape; a new one fails the run, and so does that one quietly closing without the fixture note being updated. mutation_battery.py exits 1 on either drift. Commit: azender1/SafeAgent@cb111e8 |
|
@haroldmalikfrimpong-ops The second-implementation row is live, as offered on 21 July: It credits your recompute with method, result and date, and records that your canonicalizer, the rfc8785 reference library and ours all land on the same digest. That is the part worth having on the record: the published hash is the standard RFC 8785 JCS digest, not a Concordia-specific artifact, so a reader can diff two independent canonicalizers against one value with no shared SDK and no issuer callback. The row also notes the seam we found: only the charge_ref URN join key is expected to byte-match across both artifacts, since each side keeps its own action_ref representation. @kenneives Point me at whatever conformance home you two settle on and I will add the pointer. |
|
Before leaning further on the "zero divergence" shorthand, I ran @chopmob-cloud's mutation methodology against my own vectors — substitute every stored digest, re-run the verifier, count what's actually rejected. Posting the result myself rather than waiting for someone to find it. What I found. The runner I'd been citing ( What I fixed. Added a repo-owned recompute-binding verifier (
Combined, 187/242 now fail closed, and every conformance-digest field is bound (100% of mutations to them caught, by construction — it recomputes, so a tampered value can't match). Clean-corpus baseline: 59/59 bound digests match. The remaining 55 "escapes" are not conformance digests and aren't reproduction targets: the runner's own run-receipt decoration, cryptographic key material (seed/pubkey hex), out-of-scope RFC 9421 fixtures, and Correcting the record: "N vectors, zero divergence" was accurate about canonicalization agreement but overstated it into digest-binding coverage it didn't have. The honest figures: 253 JCS checks / 24 anchor sets for canonicalization, plus |
|
Following up now the corpus has settled under the mutation bar. I re-ran the negative test across our current substrate corpus: 19 vector sets, 166 vectors, 491 fabricated-digest mutations (fake the canonical bytes, fake every sha256-shaped ref/hash, fake the preimage itself). 0 escapes. Where the earlier corpus had 28 of 114 read-back escapes, every fabricated value is now rejected, because the verifier recomputes from the preimage instead of comparing a published value against itself. I want to draw out why this thread matters more than it first looks. The This stops being academic the moment agentic frameworks are the thing sitting in front of payment rails. Traditional edge defence is static and signature based. Agentic traffic is not, so the enforcement has to be two layered. Underneath, a deterministic gate that rejects a hostile but well formed payload with a named, signed verdict before it is ever canonicalised. Above it, an adaptive layer that evolves as agent behaviour does. The deterministic gate is what keeps the adaptive one honest, because it recomputes and rejects rather than trusting a receipt someone waved, which is the same discipline this thread converged on: recompute, do not read back. So the substrate-security bar (clone, reproduce, then reject a fabricated one) is not a conformance nicety for a spec. It is the property that has to hold when the receipt is the only thing standing between an autonomous agent and settlement. |
|
@chopmob-cloud's two-layer point (a deterministic gate that rejects a hostile-but-well-formed payload before canonicalization, under an adaptive layer) names where Sanctuary sits: policy enforcement beneath the receipt layer, independent of whether the app-level verifier reads it correctly. Concrete state, not a roadmap claim: this week it armed and enforced a signed operator policy on a real dedicated-account agent host, clean per-uid allow/deny, 5/5 reboot survival. macOS only, and that is enforcement, not yet the per-rule audit trail. Different layer than the digest-binding fixes above, same discipline underneath: don't trust what you haven't recomputed or enforced yourself. https://github.com/eriknewton/concordia-protocol/tree/main/docs/interop/a2a-1920-fulfillment-sample |
|
Fair push -- and annotating your choices wasn't the intent; that was agreement reading as critique, which is on me. An enforcement claim can't be checked the way a vector corpus can -- nobody can clone my hardware -- so its reproduction path is different in kind: install it on a box you own and run the probes; the code is public for exactly that reason. That's also why the post named what's missing instead of claiming it. On the per-rule log we agree completely: it's the top of the enforcement list, and the honest sequence is build it, drill it, then talk about it. |
|
That's the right result — 491 fabricated mutations, 0 escapes, precisely because the verifier recomputes from the preimage instead of reading a published value back against itself. That's the whole game. And your framing is the part worth quoting: the deterministic recompute-gate underneath is what keeps the adaptive layer honest once agentic frameworks sit in front of settlement. "The bytes agreed, the trust did not" is exactly the read-back failure — a verifier that echoes a digest instead of recomputing it turns the layer into decoration. Recompute, don't read back. Thanks for pushing the negative test; finding the escapes on our own corpora first is the win, not the embarrassment. |
Uh oh!
There was an error while loading. Please reload this page.
Opening the v0.4 design thread. Goal: a
transactionalclaim_type that sits as a per-action receipt above the point-in-time attestation layer, so a trust decision can be bound into settlement rather than enforced out of band.This isn't starting from zero — a few things are already settled from prior threads and I'd rather mark them as locked than relitigate:
1. Two layers, not one. L1 (attestation) is a signed claim of authority + scope at signing time; it can't speak for what a timelock does two days later. L2 (receipt) is emitted at execution time, records what actually ran with the constraints live at that block, and references L1 by hash. (aeoess's framing on #1628; m13v's execution-block point.)
2. Verification anchors on the execution block, not the attestation block. The receipt envelope MUST carry which block/checkpoint the verdict is anchored to, and consumers MUST re-anchor on execution-time state for high-stakes actions. Concrete fields:
execution_block_anchor,active_constraints_at_execution,attestation_ref,action_ref.3. Compose, don't reinvent. We provide the trust-score middleware that reads chain state and emits signed verdicts. We do NOT issue a payment contract. x402 handles the 402 challenge/response, Vauban handles STARK receipts, ERC-8004 handles on-chain reputation. did:aip is treated as method-agnostic — any DID method resolves.
Leads: Vauban (seritalien et al) and Erik Newton (Concordia) on the payment-rail + claim_type rigor. I'm driving the receipt envelope shape and the Trust Score v2 composition.
Open for this thread:
transactionalper-claim_type cap in the Trust Score v2 envelope — I lean 0.30 (between authority 0.25 and identity 0.60).chain_id.noncefield, semantics left to consumer?action_ref. Per @aeoess (docs(proposals): add A2A Identity Trust Framework roadmap (v1.0 - v2.0) #1850), citing both methods symmetrically rather than one as origin: APSaction_ref(draft-pidlisnyi-aps-01 §4.1 — SHA-256 over the RFC 8785 canonical intent fields) and argentum-coreaction-ref-v1. The APS-native and external preimages differ (docs(proposals): add A2A Identity Trust Framework roadmap (v1.0 - v2.0) #1850), so a v0.4 receipt names which preimage it used rather than assuming a single origin.@evidai / LemonCake raised the cleanest version of #6 on #1786: an optional
verifier_attestationfield the gateway checks before minting, so the trust decision is bound into settlement. Their boundary is exactly right — external verifier = dynamic admission + behavioral limit; gateway = static cap enforcement + settlement +charge_refcorrelation; the mint call is the seam. Keeping the field optional preserves backward-compat (a gateway that doesn't speak it just mints on its own caps and degrades to the un-gated case). I'd make ourpre-execution-verdict-v0envelope the thing that populatesverifier_attestation. @evidai — bring the concrete mint-endpoint shape and where the field slots in.Two concrete pieces landed on #1786 ahead of this thread — building on both:
verifier_attestationobject spec + conformance fixture (admit / scope-deny / limit-deny / dual-approval-flag) with the binding done right:binding_digest = sha256(JCS(charge_ref, amount_usd, subject_did, nonce))so an attestation can't be replayed against a different charge;admission.verdict+dynamic_limit_usd, gateway enforcesmin(static_cap, dynamic_limit_usd); absent field ⇒ un-gated both directions; only fast gates (identity/cert/key-lifecycle) re-checked in the ~30s mint window. That's the verifier half of evidai's seam — proposing we adopt this shape as the straw-man forverifier_attestation.action_ref(PENDING → COMMITTED; retries hit the guard, return the original receipt). I'd fold an exactly-once guard keyed onaction_refin as a normative requirement, not an afterthought.So the seam now has three named parts, all offline-verifiable, none trusting the others: signed admission (verifier) → static cap + settlement (gateway) → exactly-once execution guard (idempotency).
Everyone else: poke holes, especially on the cap value (#1) and replay/idempotency (#4). Substrate-cred bar is unchanged — fixtures reproduce byte-for-byte on a fresh clone or they don't count.
All reactions