carousell/rctp

Spec ambiguities found while building v0.1

Rule (from AGENTS.md): when Draft 0.3 is ambiguous, open an issue labelled spec-ambiguity and implement the more restrictive reading. Each entry below names the reading v0.1 implements and links its tracking issue. The last column says how Draft 0.4 (adopted for v0.2, Appendix C.2) closes it: dissolved (the question no longer exists) or resolved (the draft states the rule). v0.1 code keeps its reading until v0.2 implements Draft 0.4 (#20).

ID Where Question v0.1 reading (more restrictive) Issue Draft 0.4 (adopted)
A-1 4, 9 How does the enforcement point receive the Seller Mandate it evaluates? The seller agent sends it on every seller-side call in RCTP-Mandate, covered by the request signature. The gateway re-verifies it each time. #2 Dissolved: key-bound access token; the platform holds grant and terms
A-2 3.2 Is sending the mandate to the enforcement point a "presentation" that needs SD-JWT+KB with a verifier nonce? Proof of possession is the RFC 9421 signature by the cnf key over a request covering rctp-mandate, with a single-use nonce and ±60 s freshness. Equivalent binding without a per-call nonce round trip. Revisit if editors require KB-JWT. #3 Dissolved: no credential is presented to the enforcement point
A-3 12 Which reason does a mandate get when it is unverifiable (bad issuer signature, untrusted issuer, unsupported vct, schema-invalid)? Refuse with 403 mandate_revoked: the agent's remedy (get a new mandate from the issuer) is the same. Propose a mandate_invalid code for Draft 0.4. A mandate whose cnf key or operator does not match the caller fails key binding: 401 signature_invalid. #4 Resolved: grant_invalid (401), grant_revoked, grant_expired
A-4 9.1, 12 Does a refusal record state the floor through counter when preset_counter equals the floor? counter is sent only when preset_counter is strictly above the floor. #5 Resolved: term set requires counter > floor
A-5 9.1 After the offer budget is spent, does an above-floor offer still get through? No. Every offer from that buyer operator on that listing gets 429 until the window frees a slot; otherwise 200-vs-429 reveals the floor. #6 Resolved: kept, counted per buyer agent with an operator ceiling
A-6 15.2, A.4 Which registered action does an inbound buyer offer evaluate? There is no offer.create. Record it as offer.accept (as draft example A.4 does) with the floor and budget checks; tier is not applied, a passing offer is delivered. #7 Resolved: new verb offer.create
A-7 4.1, 9 Is a listing in scope by ID, by category, or both? What if a listing has no price_floor? listing.create is checked against categories/excludes. Actions on an existing listing need its ID in listing_ids (when present) and its category not excluded. Offer actions on a listing with no price_floor entry are not_in_scope (fail closed). #8 Dissolved: scope is the seller's listings with terms; every term set has a floor
A-8 9.1 Does the floor apply to the seller's own counter? Yes: a seller counter below the floor is below_floor. #9 Resolved: yes, and for listing.reprice
A-9 10.1 Who may accept? Only the seller side, under offer.accept. A buyer who agrees to a counter re-offers at that amount, which runs the inbound checks. #10 Resolved: seller side only; a counter is an invitation
A-10 9.4, 12 Which reason for an approval response that arrives after expires_at? approval_invalid (9.4 lists expiry among its checks). The approval itself is reported as expired with reason approval_timeout. #11 Resolved: as v0.1
A-11 3.3, 16 V6 Status lists "SHOULD be cached for their lifetime", yet a revoked mandate must fail the next call. The reference gateway fetches the mandate status list on every signed call (cache lifetime 0, conditional GET). #12 Dissolved: grants are checked live, no status list
A-12 6.4, 7.3 Starter profile refunds: what must precede refunded? rejected by the buyer agent. Any other refund path waits for a later draft. #13 Resolved: not_as_described needs an attested, completed UCP return; not_received after deliver_within_days (10.3)
A-13 9.4 The draft names no way to register "the seller's passkey with the marketplace". Reference gateway: issuer-signed enrolment ticket (binding section 6.5). Not part of RCTP. #14 Resolved: out of scope; the marketplace account passkey is the approval key
A-14 9.1, 11, 7.3 The draft names no REST route to redeem an accepted offer's checkout, to write a checkpoint, or to attempt a fixed-forbid action. Binding section 12 defines POST /deals, POST /deals/{id}/checkpoints, POST /deals/{id}/contract and POST /account; the last two always answer 403 action_forbidden. #15 Resolved: section 12 routes POST /deals, /deals/{id}/events, /listings/{id}/terms; fixed forbids need no route

Found while binding Draft 0.4 (v0.2)

Same rule: each entry names the reading v0.2 implements (the more restrictive one) and links its tracking issue. Section numbers refer to Draft 0.4; the wire details are in v0.2-binding.md.

ID Where Question v0.2 reading (more restrictive) Issue
B-1 4.1, 5.3 Must a UCP profile key's kid be its JWK thumbprint? UCP allows any stable kid. Yes. The grant binds cnf.jkt, and the request keyid must equal it, so every profile key's kid is its RFC 7638 thumbprint; a key whose kid is not is unusable. #60
B-2 4.1 UCP makes RFC 9421 created optional and relies on Idempotency-Key for replay. Are time bounds required? Yes: created and expires are required, expires - created ≤ 300 s, ±60 s skew; plus UCP idempotency on every POST. #61
B-3 5.3, 13 RFC 7662 lets an inactive token say only active: false. How does the gateway tell grant_revoked and grant_expired from grant_invalid? The marketplace authorization server adds grant_status (active, revoked, expired) for tokens it issued, to the authenticated gateway only. Unknown tokens get grant_invalid. #62
B-4 6.1, 9.2 The listing approval challenge must bind terms_commitment, but the seller fills the terms in on the hosted page after the 202. When is the challenge computed? After the page submits the terms: POST /approve/{id}/terms validates them, fixes the salt and commitment, and only then issues the challenge. The 202 body for listing.create and listing.set_terms carries no challenge. #63
B-5 5.3, 9 After an approved ask, must the agent's access token itself still be current? No: the grant must still be active (not revoked or expired), checked by introspection; a lapsed access token alone does not stop the approved action. #65
B-6 6.2, 8.1 How are counterparty_requirements enforced before Party Credentials exist? Every offer on a listing that sets them is refused counterparty_requirements_unmet; no buyer can prove them in v0.2. #66
B-7 10.3(4) An event signed with the wrong observation: refused or stored without effect? Refused with 409 release_condition_unmet. #67
B-8 6.2, 10.2 Which event attests handover_inspection_passed? None in the Starter profile; a listing with that release condition also needs the buyer's accepted before release. #68
B-9 5.1 Is a seller one identity across operators? No: the seller pseudonym is pairwise per operator, so listing ownership and the approval passkey are per operator. #69

Found while binding Draft 0.5 (v0.3)

Same rule. Section numbers refer to Draft 0.5; the wire details are in v0.3-binding.md. B-1 to B-9 carry forward unchanged into v0.3; B-1 and B-2 apply to signed requests (binding 2.1).

ID Where Question v0.3 reading (more restrictive) Issue
B-10 4.1, 7.4(1) The checkout is bound to the buyer agent's key, but a buyer agent may authenticate without signing. Can it redeem? No: POST /deals is always signed by the key buyer_jkt names. A buyer agent that does not sign pays through the marketplace's UCP checkout. #101
B-11 5.3 A key-bound token on an unsigned request, or an unbound token on a signed one? Both 401 grant_invalid. #102
B-12 7.3(5) What does a marketplace without offer.counter answer to a counter? 403 action_forbidden; over MCP, tools/list omits counter_offer. #103
B-13 4.1(1) UCP requires UCP-Agent on every request, but a plugin on an agent host authenticates by OAuth and cannot add it. An OAuth call without UCP-Agent acts for the profile the marketplace registered for its client_id; a present UCP-Agent must match it. Signed calls still require UCP-Agent. #105

Source: spec/AMBIGUITIES.md