cycles-ap2-python — a runtime idempotency wrapper for AP2 payment mandates #262
Replies: 4 comments 7 replies
-
|
Follow-up: wrote up the keying decision and the implementation scars in more detail at runcycles.io/blog/ap2-open-mandate-consume-once-runtime-idempotency. The two questions in the original post (hash canonicalization, adapter shape) are still open from my end — keeping an eye on this thread. |
Beta Was this translation helpful? Give feedback.
-
|
Direct answer to your two open questions from running an adjacent consume-once layer in production. 1. Canonical input for JCS-canonical (RFC 8785) over the signed mandate body, SHA-256. Specifically: take the mandate JSON exactly as the credential provider returns it, run it through RFC 8785 canonicalisation, hash. Do not hash the JWS compact form (the base64url envelope adds an extra layer that breaks downstream interop — the hash changes if a verifier re-encodes the JWS even though the underlying payload is identical). We ship this for our APM (Agentic Payment Mandates) bilateral-delegation work. 10/10 byte-match against the APS v1 conformance vectors using The same canonical-hash surface composes upward into CTEF v0.3.x evidence envelopes (A2A #1734) and the Verascore evidence-schema-v0.1 ingest (#1631 + verascore.ai/docs/evidence-schema-v0.1). Keying your idempotency token on the same hash means downstream attestations and your 2. Adapter shape. We landed on a 4-method interface for the consume-once gate (very similar to your reserve/commit/release but with an explicit class IdempotencyStore(Protocol):
def reserve(key, content_hash, ttl) -> ReserveResult:
# ReserveResult.outcome: "new" | "replay" | "divergence"
# "divergence" = same key, different content_hash → caller gets IDEMPOTENCY_MISMATCH
# "replay" = same key, same content_hash → return cached response
def commit(key, response_body) -> None
def release(key, reason: ReleaseReason) -> None
def status(key) -> KeyStatusThe Our Happy to share the schema / middleware if it's a useful reference. The blog post you linked covers the same scar tissue from a different angle — particularly the "human-not-present" subtlety where a stale-plan replay looks identical to a legitimate retry until you compare content hashes. — AlgoVoi (chopmob-cloud) AlgoVoi (chopmob-cloud) -- Acquisition enquiries: https://docs.algovoi.co.uk/acquisition |
Beta Was this translation helpful? Give feedback.
-
|
Thanks @chopmob-cloud — both answers are exactly the level of detail I was hoping for. On Q1 (canonical input for The "do not hash the JWS compact form" point is the load-bearing one — JWS re-encoding by intermediaries is a real interop hazard, and getting that subtlety into adapter authors' minds early is worth more than the choice of canonical scheme itself. On Q2 (adapter shape). cycles-ap2-python today has reserve/commit/release on the Cycles plane (the Cycles protocol surfaces The "human-not-present, agent retrying via a stale plan" failure mode you describe is exactly the one Cycles' canonical IDEMPOTENCY_MISMATCH path was designed for. Good to hear that's holding up under production traffic on your side. Two clarifying questions on the references (just to make sure I'm following them correctly):
Neither changes the substance of the recommendations — just want to point readers at the right artifacts when I cite the conversation in the cycles-ap2-python docs. Thanks again for taking the time to write this up. |
Beta Was this translation helpful? Give feedback.
-
|
@amavashev — filed your "spec-published vectors" question as a formal Proposal so the conversation can land in front of maintainers rather than staying in show-and-tell: #265 — [Proposal] v0 conformance vectors for open_mandate_hash derivation (cross-impl validated) The issue lifts the derivation rule, the 7 v0 vectors, and the cross-impl validation table directly from this thread, with the scope/non-scope explicitly drawn so maintainers can decide on adoption without re-litigating canonicalisation choices. Happy to PR fixture files (instead of gist-linked) or add a deterministic build script if they prefer reproducible-by-construction over a static artefact. Will keep tracking your README citation in AlgoVoi (chopmob-cloud) -- Acquisition enquiries: https://docs.algovoi.co.uk/acquisition |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
Independent Python library that wraps an AP2 payment moment in a
reserve / commit / releaselifecycle: github.com/runcycles/cycles-ap2-python. Apache-2.0.Why. The spec calls out in §6 that "a shopping agent must avoid presenting subsequent open mandates without a rejection receipt to prevent multiple checkouts using the same open mandate." That's a runtime-state problem — idempotency, concurrency, consume-once — not a cryptographic one. Wanted to address it in a separate layer rather than push it into AP2 verification.
How. When an
open_mandate_hashis present, the wrapper keys the reservation's idempotency token on it (not ontransaction_id). Multiple checkouts spawned from one open mandate then share a single(tenant, endpoint, idempotency_key)bucket. Identical payloads replay; divergent payloads returnIDEMPOTENCY_MISMATCH. Either way, no second valid reservation.Scope. The library doesn't verify signatures, create mandates, or replace credential-provider verification — those stay with the AP2 SDK and the merchant flow.
Sharing in case it's a useful reference for anyone else working on this.
One question I’d welcome input on: for human-not-present flows, is there a preferred AP2-native canonical input for deriving
open_mandate_hash— signed mandate digest, canonical serialized mandate, or another stable identifier?Beta Was this translation helpful? Give feedback.
All reactions