feat: reserve the ucp protocol namespace, add map_order for registry ordering - #657
feat: reserve the ucp protocol namespace, add map_order for registry ordering#657igrigorik wants to merge 2 commits into
ucp protocol namespace, add map_order for registry ordering#657Conversation
UCP registries are reverse-DNS keyed JSON maps, and JSON object member order is not a protocol contract: RFC 8785 (JCS) canonicalization, used by UCP signing, sorts object member names. Businesses have no reliable way to declare preferred traversal order for registry keys, e.g. payment handler presentation order (#170, design discussion in #525). The model: the member name `ucp` is reserved at every object scope as the protocol namespace. The top-level envelope is the root manifestation of that reservation, not a special wrapper. The namespace is ambient: any object scope MAY carry a `ucp` member, domain schemas never declare it (it is document grammar, like the reservation itself), and its contents are defined exclusively by the vocabulary registered in ucp.json#/$defs/members. Consumers process the members they recognize and ignore the rest; a member is admitted to the vocabulary only if it is safe to ignore, so no member can be load-bearing for correctness. One exception: an object closed with additionalProperties:false must declare the optional `ucp` property explicitly. Future members register once and work at every scope immediately -- request constraints (#655) can rebase onto this vocabulary. The first member, map_order, declares key-traversal order for sibling map-valued fields, carried in an array because JCS preserves array element order. Partial lists are valid, unlisted keys remain valid and follow, and the list is not an allowlist. At the root envelope it orders the registries beside it: "ucp": { "payment_handlers": { "com.google.pay": [ ... ], "dev.shopify.shop_pay": [ ... ] }, "map_order": { "payment_handlers": ["dev.shopify.shop_pay", "com.google.pay"] } } At any deeper scope the same member rides the ambient `ucp` member, with no schema change to the host object -- e.g. ordering an identity provider registry inside a capability config: "config": { "providers": { "app.example.login": [ ... ], "com.google": [ ... ] }, "ucp": { "map_order": { "providers": ["app.example.login", "com.google"] } } } Refs #525, #170
| the context of the cart (e.g., removing "Buy Now Pay Later" for subscription | ||
| items, or filtering regional methods based on shipping address). | ||
|
|
||
| **Presentation Order:** Businesses **MAY** declare a preferred presentation |
There was a problem hiding this comment.
This is a tricky as per the comments on #170 and #176 citing that available instruments within handlers are really the presentation grain that a business may be most interested in ordering and map_order doesn't cover that (in current form). However, in practice this may still address a large number of use cases (where payment handlers are advertising more than one available instrument or where businesses are content to order at the handler level) riding on a broadly usable ordering mechanism.
There was a problem hiding this comment.
Pushed an update that clarifies this: 6391c451 — within a handler, the business's available_instruments array order carries the instrument presentation preference, so combined with map_order.payment_handlers the full presentation order can be derived (covers the #176 example: order the handlers, tokenizer advertises [card, bank]).
To be fair, this doesn't cover every case: a business can rank handlers, and rank instruments within each handler, but it can't mix them freely across handlers — e.g. card via gpay first, then bank via tokenizer, then back to paypal via gpay. I'd suggest we leave that as is though.
Handler-level presentation preference is carried by map_order.payment_handlers; review on #657 noted the instrument grain was uncovered. The order of the business's advertised available_instruments array now carries the same suggestive within-handler preference, earliest first: platforms SHOULD consider it and MAY apply their own ordering, and the buyer-side context.payment[] preference remains distinct with the platform arbitrating. Completes handler-major presentation ordering at both grains.
UCP represents extensible registries as JSON objects keyed by reverse-DNS names —
payment_handlers,services,capabilities. That shape is deliberate (decentralized governance, uniqueness by construction,propertyNamesenforcement), but JSON object member order is not a protocol contract: RFC 8785 canonicalization, which UCP signing relies on, sorts object member names. There is currently no reliable way for a Business to declare preferred traversal order for registry keys — the concrete driver is merchant-preferred payment-handler presentation order (#170; design discussion in #525).Proposed model
ucpis reserved at every object scope as the protocol namespace. The top-level envelope that profiles and responses already carry is not a special wrapper — it is the root manifestation of this reservation: a reserved member of the root object, grammatically identical to any nested scope carrying protocol annotations. Placement carries no semantics.ucpmember; domain schemas never declare it — the member is part of the UCP document grammar, like the name reservation itself. Its contents are defined exclusively by the vocabulary registered inucp.json#/$defs/members. Consumers process the members they recognize and MUST ignore the rest.additionalProperties: falsedoes not get an exemption from the grammar — it MUST declare the optionalucpproperty explicitly.Because registration is central, future members work at every scope the moment they land: #655's request constraints rebase onto this vocabulary as one property registration, immediately usable on any leaf object (e.g. per-line-item quantity constraints).
First member:
map_ordermap_orderdeclares preferred key-traversal order for sibling map-valued fields, carried in an array value because JCS preserves array element order while sorting object keys. Partial lists are valid; unlisted keys remain valid and follow via field-defined fallback or lexicographic order; the list is not an allowlist; absent means no order is declared, and consumers MUST NOT fall back to member order.At the root envelope, it orders the registries directly beside it:
{ "ucp": { "version": "2026-01-11", "payment_handlers": { "com.google.pay": [{ "id": "gpay", "version": "2026-01-11" }], "dev.shopify.shop_pay": [{ "id": "shop_pay", "version": "2026-01-11" }] }, "map_order": { "payment_handlers": ["dev.shopify.shop_pay", "com.google.pay"] } } }At any deeper scope, the same member rides the ambient
ucpmember — no schema change to the host object. Ordering an identity-provider registry inside a capability config:{ "config": { "providers": { "app.example.login": [{ "type": "oauth2" }], "com.google": [{ "type": "oauth2" }] }, "ucp": { "map_order": { "providers": ["app.example.login", "com.google"] } } } }For
payment_handlers, the declared order is the Business's presentation preference (#170): suggestive — the Platform SHOULD consider it and MAY apply its own ordering.Why this shape / alternatives considered in #525:
display_orderembedded rank (feat: add optional display_order to payment handlers #176): cannot attach to a map bucket whose value is an array; unnecessary where arrays already self-order.propertyNames— a breaking change for every shipped validator; typed maps become unions.payment_handlers_order): open-ended reserved-name sprawl;_ordersuffix collides with the commerce Order noun.$-prefixed member (original key ordering for UCP map registries #525 proposal): empirically taxed in codegen —$is an illegal identifier in Python/Go/Java/C#/Rust, and Pydantic's asymmetric alias defaults silently emit non-conforming payloads with stockdatamodel-code-generator.ucpis a clean identifier in every ecosystem, and the name is already de facto reserved (zero domain usages in the corpus).Compatibility
Additive. The envelope is open today; existing consumers ignore
map_order. No shipped registry is reshaped; registry maps stay pure and homogeneous (propertyNamesalready rejectsucpandmap_orderas registry keys — verified). Consumers that ignore the member lose nothing: order was never reliable before.Checklist