Skip to content

feat(checkout): mandate-evaluation extension for AP2 TTL at admission (resolves #512)#540

Open
arnaud-92 wants to merge 2 commits into
Universal-Commerce-Protocol:mainfrom
arnaud-92:merchantstamp/mandate-evaluation-ep
Open

feat(checkout): mandate-evaluation extension for AP2 TTL at admission (resolves #512)#540
arnaud-92 wants to merge 2 commits into
Universal-Commerce-Protocol:mainfrom
arnaud-92:merchantstamp/mandate-evaluation-ep

Conversation

@arnaud-92

@arnaud-92 arnaud-92 commented Jun 23, 2026

Copy link
Copy Markdown

Description

Adds a vendor-namespace Checkout capability extension, com.merchantstamp.mandate-evaluation
(docs/specification/mandate-evaluation.md), defining when AP2 mandate liveness is evaluated
relative to the UCP Checkout state machine and how that admission decision is made auditable.
Resolves the complete_in_progress TTL ambiguity reported in #512, landing the design converged in
#512 / #515 with @aeoess, @chopmob-cloud and @GarethCOliver.

Normative model

  • Mandate liveness is evaluated exactly once, at admission of Complete Checkout.
    • Valid at admission → authorization frozen; MAY enter complete_in_progress; MUST run to
      terminal; later expiry MUST NOT abort or shorten the settlement window.
    • Expired at admission → MUST NOT enter complete_in_progress; terminates canceled with the
      existing mandate_expired error.
  • mandate_expired is admission-time only — it MUST NOT fire once in complete_in_progress
    (the normative fix for [Bug]: Undefined behavior when AP2 mandate TTL expires during complete_in_progress #512).

Compose on AP2, don't fork. No new lifecycle status, error code, receipt envelope, signature
algorithm, or key-discovery mechanism. The outcome is an AP2-aligned terminal receipt (Success,
or Error + mandate_expired); admission evidence rides as a namespaced extension field
(evaluated_at, reference, checkout_id, mandate_exp, result) on that receipt. evaluated_at
and mandate_exp are Unix epoch integer seconds, consistent with AP2 core.

Binding by content — recomputable tuple {reference, checkout_id, evaluated_at}; no forward
receipt_id. Digest rules kept in two separate cases (SHA-256 over the exact signed
serialization for the artifact handle, with compact-vs-detached JWS pinned per representation;
JCS-then-SHA-256 for unsigned recomputable content). Signing reuses the existing UCP/AP2
Message Signatures profile (ES256 required, ES384/512 optional, JCS, signing_keys[]).

Scope. Point-of-use admission + post-admission lifecycle only. Pre-TTL withdrawal / revocation /
status propagation — including the closed-enum EXPIRED cancellation receipt — is out of scope and
lives on a separate track.

Category (Required)

  • Capability: New schemas (Discovery, Cart, etc.) or extensions. (Requires Maintainer approval)
  • Documentation: Updates to README, or documentations regarding schema or capabilities. (Requires Maintainer approval)

Related Issues

Resolves #512
Refs #515

Checklist

  • I have followed the Contributing Guide (including Conventional Commits title requirements and ! for breaking changes).
  • I have updated the documentation (if applicable).
  • My changes pass all local linting and formatting checks.
  • I have added tests that prove my fix is effective or that my feature works.
  • New and existing unit tests pass locally with my changes.
  • (For Core/Capability) I have included/updated the relevant JSON schemas.
  • I have regenerated Python Pydantic models by running generate_models.sh under python_sdk.

Notes

Working-Draft vendor extension; conformance tests to follow in
Universal-Commerce-Protocol/conformance. Open items tracked in §7 of the doc (receipt-addressing
conformance vectors, validity-window commitment, receipt delivery placement, FIDO push-down of
evaluated_at, DCQL/OpenID4VP capability-query alignment).

cc @GarethCOliver @aeoess @chopmob-cloud — opening as text to iterate on, per
#515 (comment).

@arnaud-92 arnaud-92 requested review from a team as code owners June 23, 2026 10:07
@arnaud-92 arnaud-92 changed the title Add mandate-evaluation extension (com.merchantstamp.mandate-evaluatio… feat(checkout): mandate-evaluation extension for AP2 TTL at admission (resolves #512) Jun 23, 2026
@arnaud-92

Copy link
Copy Markdown
Author

@aeoess — good catch, and it deserves to be pinned explicitly so two implementers can't diverge.

reference is the AP2 mandate reference — the hash of the closed mandate credential, computed per AP2's existing rule — never the checkout reference. The checkout/session identity rides in its own checkout_id field. They are two distinct named fields in the tuple, so reference is never overloaded to mean the checkout.

Why the mandate ref is the one that binds: the admission decision answers "was this grant live at the point of use," so the evidentiary anchor has to be the mandate being evaluated — reference carries that. checkout_id scopes the decision to the flow, which is what enforces mutual exclusivity per checkout and lets a verifier reconstruct the link. Both are required, but they answer different questions: reference = which authorization grant, checkout_id = which flow.

To remove the ambiguity, I'll tighten §5.1/§5.3 in docs/specification/mandate-evaluation.md to state:

  • reference — the AP2 mandate reference; hash of the closed mandate credential, per AP2's existing definition. MUST NOT be the checkout_hash (which hashes the inner checkout_jwt, equal to the Payment Mandate transaction_id).
  • checkout_id — the UCP checkout/session identifier.
  • binding tuple = {reference, checkout_id, evaluated_at}, where reference and checkout_id are two independent identifiers and are never conflated.

This and the compact-vs-detached JWS pinning (§6.1) are exactly the two implementer footguns worth nailing before the fields freeze — thanks for flagging both.

@aeoess

aeoess commented Jun 24, 2026

Copy link
Copy Markdown

That resolves it.

Using reference as the AP2 mandate reference, meaning the hash of the closed mandate credential, while keeping checkout_id as the separate flow identity is the right split. The admission decision binds to the grant being evaluated, and the per-checkout exclusivity scoping lives in its own field.

The §5.1 / §5.3 wording you laid out, with reference explicitly not being the checkout_hash and the binding tuple keeping the two as independent identifiers, closes the ambiguity.

Pinning compact versus detached JWS per representation in §6.1 closes the other one. Those were the two places where two conformant implementers could have diverged on the same logical receipt.

Both are pinned now. Thanks for tightening the text.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Undefined behavior when AP2 mandate TTL expires during complete_in_progress

2 participants