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 11 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.
There was a problem hiding this comment.
Yeah - it was the mixing case that I had in mind ... but - agreed - not a critical / blocking use case.
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.
|
Two comments coming to mind:
|
The ambient `ucp` reservation applies to structured objects whose members are schema-defined fields. Dictionary keys are application data, so reserving `ucp` there conflicts with schemas such as `attribution`, where all values are strings. Limit the reservation to structured object scopes and treat `ucp` as ordinary data in dictionary containers. Keep structured objects used as dictionary values eligible. Narrow the closed-object authoring rule to the same boundary and align the central vocabulary description, overview, and glossary.
The existing idempotence rule prohibited any nested `ucp` member, which also excluded valid protocol namespaces on structured objects located beneath the root envelope, such as a capability's `config`. Limit the prohibition to a direct `ucp.ucp` child while keeping deeper structured objects eligible for their own protocol namespace.
UCP schemas are open by default, so ordinary JSON Schema validation accepts ambient `ucp` at eligible structured scopes without applying vocabulary rules. Recommend that UCP-aware tooling recognize and apply the central vocabulary. Require UCP-aware strict validators to continue allowing and validating ambient `ucp` while rejecting other unknown domain fields. Note that non-UCP-aware strict validators may reject it; this is an accepted compatibility limit.
AP2 merchant authorization covers the JCS-canonicalized checkout without `ap2`, rather than the raw HTTP body bytes. Projecting the checkout through schema-recognized fields can therefore change the signed logical content. Clarify that verification and mandate construction use the complete checkout JSON and that removing any covered member invalidates the signature.
Mark both `map_order` properties with `ucp_request: "omit"`. Document per-member applicability and require repeated exposures to carry the same annotation. Request resolution now removes `map_order` while retaining the containing `ucp` envelope where its schema requires it.
Nested `map_order` lives inside `ucp` while its target map sits on the object annotated by `ucp`. At the root, registries and `map_order` instead share the root envelope. Calling both relationships sibling fields left root domain maps ambiguous. Define nested lookup on the object containing `ucp` and root lookup inside the root envelope. Leave root domain fields such as `actions` out of scope and align the schema descriptions and processing rules.
`uniqueItems` allowed a repeated key in advisory ordering metadata to invalidate an entire profile or response, even though deterministic recovery is available. Allow repeated keys at the schema layer while prohibiting producers from emitting them. Require consumers to keep the containing document valid, honor the first occurrence, and ignore later repetitions.
Define UCP-aware schema resolution as the boundary that applies the central namespace vocabulary at eligible structured scopes and emits ordinary JSON Schema for standard validators and code generators. Keep the authoring guide focused on source-schema rules rather than repeating the resolution contract.
Implement the reference schema-resolution behavior for Universal-Commerce-Protocol/ucp#657. Add a UCP-aware resolution path that applies the central namespace vocabulary at eligible structured object scopes while excluding dictionaries. Preserve namespace openness and directional applicability, reject direct `ucp.ucp`, and emit ordinary JSON Schema for existing validators and code generators. Keep helper references valid through nested resource IDs and selected definitions, extend strict traversal across child-instance applicators, and cover directionality, composition, dictionary values, forward compatibility, and collision handling.
|
@jamesandersen put together ucp-schema#48 as ref implementation: it boils down to a preprocessor pass that materializes @amithanda great feedback, ty! Addressed in followup commits -- ptal. |
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