diff --git a/docs/documentation/schema-authoring.md b/docs/documentation/schema-authoring.md index d12fc0ee7..836dad08c 100644 --- a/docs/documentation/schema-authoring.md +++ b/docs/documentation/schema-authoring.md @@ -184,6 +184,62 @@ The same registry structure appears in three contexts with different field requi | Business Profile | `/.well-known/ucp` | `version`; may add `config` | | API Responses | Checkout/order payloads | `version` (+ `id` for handlers) | +## The Reserved `ucp` Member + +The member name `ucp` is reserved at every structured UCP object scope — an +object whose members are schema-defined fields — for the protocol namespace. +The top-level envelope is its root manifestation. Dictionary containers are +excluded because their keys are data rather than fields. See +[The `ucp` Protocol Namespace](../specification/overview.md#the-ucp-protocol-namespace) +for the normative rules. For schema authors this means: + +- **Never mint a structured domain field named `ucp`.** Schema authors + **MUST NOT** declare a domain field named `ucp` in a structured object; the + name denotes the protocol namespace there. +- **Never declare the ambient `ucp` member, either — with one exception, + below.** The member is UCP document grammar, defined centrally. A Business + or Platform **MAY** include it in any eligible structured scope without the + domain schema saying so, just as no schema declares the name reservation. + Schema authors **MUST NOT** add a `ucp` property to an open structured + domain object. Ordinary instance validation against an open UCP source + domain schema treats an ambient `ucp` member as an ignored unknown object; + validating its contents is a conformance-tooling concern. +- **Dictionary keys remain data.** A dictionary container cannot host the + protocol namespace. Schema authors **MUST NOT** model a dictionary key named + `ucp` as that namespace; the key is governed by the dictionary's key and + value schemas and is ordinary data. A structured object used as a + dictionary value remains an eligible scope and follows these rules. For + example, `attribution` is a dictionary of string values, so its key `ucp` is + ordinary attribution data. +- **The vocabulary grows only in UCP core.** New protocol members are added + in `ucp.json#/$defs/members`, never in domain or extension schemas, and a + member is admitted only if it is safe to ignore — a Business or Platform + that does not process it loses only that member's benefit, never + correctness. The container is open for forward compatibility with future + UCP versions, not as an extension point. +- **Registry maps can never host `ucp`.** Registry maps are dictionaries, so + they cannot host the protocol namespace. Their reverse-domain + `propertyNames` constraints additionally reject the literal `ucp` key. + This is why structural metadata about a registry map — such as `map_order` + — sits in the parent structured scope's protocol namespace rather than + inside the map itself. +- **Closed structured objects must declare `ucp` explicitly.** Closing a + structured object does not exempt it from the protocol grammar. When a + schema author defines a structured object scope in a UCP payload with + `additionalProperties: false`, the author **MUST** declare an optional + `ucp` property referencing `ucp.json#/$defs/members` so the ambient member + remains representable there. This is the one exception to the + never-declare rule above — and one more reason to leave structured objects + open (see [Open Objects](#open-objects-additionalproperties)). This + exception applies only to structured object scopes; closing a dictionary + does not create an ambient namespace there. +- **Declare member applicability.** Schema authors **MUST** annotate every + property registered in `ucp.json#/$defs/members` with `ucp_request` (`omit`, + `optional`, or `required`, as appropriate). They **MUST** repeat the + annotation wherever the same member is exposed elsewhere in `ucp.json`. An + explicit ambient `ucp` property follows the applicability of its containing + schema and is not automatically omitted from requests. + ## The Entity Pattern All capabilities, services, and handlers extend a common `entity` base schema: @@ -402,10 +458,7 @@ Marking an object as closed preemptively prevents any future non-breaking additi protocol, what would otherwise be a backward-compatible field addition (e.g., adding a "gift_message" field to an order) becomes a breaking change for any client validating against a closed schema. -By default, JSON Schema is open and ignores unknown properties. Authors should leave this keyword omitted except in rare -circumstances: polymorphic discriminators (where strictness prevents oneOf validation ambiguity), security-critical -payloads (where unknown fields may indicate tampering), or protocol envelopes (where strictness is useful to catch -typos in core metadata like the `ucp` block). +By default, JSON Schema is open and ignores unknown properties. Authors should leave this keyword omitted except in rare circumstances: polymorphic discriminators (where strictness prevents oneOf validation ambiguity) or security-critical payloads (where unknown fields may indicate tampering). The `ucp` protocol namespace itself is deliberately open (tolerant readers ignore unrecognized members); typo discipline there is an authoring-time concern, not a wire-validation one. **Anti-Pattern (Prevents adding new fields without a reversion):** diff --git a/docs/specification/ap2-mandates.md b/docs/specification/ap2-mandates.md index 3d305d232..bfa219c20 100644 --- a/docs/specification/ap2-mandates.md +++ b/docs/specification/ap2-mandates.md @@ -255,6 +255,10 @@ immediately over the same HTTP connection. Mandates are different: JCS ensures that semantically identical JSON produces byte-identical output, making signatures reproducible across implementations and time. +Verification and mandate construction operate on the complete checkout JSON, +not a projection of schema-recognized fields; removing any member covered by +`merchant_authorization` changes the JCS payload and invalidates the signature. + **AP2-Specific Rule:** When computing the business's `merchant_authorization` signature, exclude the `ap2` field entirely. This ensures future AP2 fields are automatically handled. diff --git a/docs/specification/glossary.md b/docs/specification/glossary.md index 716e41f7f..e112d573f 100644 --- a/docs/specification/glossary.md +++ b/docs/specification/glossary.md @@ -26,17 +26,18 @@ acronym in each specification Markdown file spells out the full term (e.g., ## Protocol -| Term | Acronym | Definition | -| :------------------------------ | :------ | :-------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Action** | - | Outstanding extension-defined work for a Platform to process; appears only in the response-only `actions` map of adopting capabilities. | -| **Agent Payments Protocol** | AP2 | An open protocol designed to enable AI agents to securely interoperate and complete payments autonomously. UCP leverages AP2 for secure payment mandates. | -| **Agent2Agent Protocol** | A2A | An open standard for secure, collaborative communication between diverse AI agents. UCP can use A2A as a transport layer. | -| **Capability** | - | A standalone core feature that a business supports (e.g., Checkout, Identity Linking). Capabilities are the fundamental "verbs" of UCP. | -| **Credential Provider** | CP | A trusted entity (like a digital wallet) responsible for securely managing and executing the user's payment and identity credentials. | -| **Extension** | - | An optional capability that augments another capability via the `extends` field. Extensions appear in `ucp.capabilities[]` alongside core capabilities. | -| **Model Context Protocol** | MCP | A protocol standardizing how AI models connect to external data and tools. UCP capabilities map 1:1 to MCP tools. | -| **Profile** | - | A JSON document hosted by businesses and platforms at a well-known URI, declaring their identity, supported capabilities, and endpoints. | -| **Universal Commerce Protocol** | UCP | The standard defined in this document, enabling interoperability between commerce entities via standardized capabilities and discovery. | +| Term | Acronym | Definition | +| :------------------------------ | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| **Action** | - | Outstanding extension-defined work for a Platform to process; appears only in the response-only `actions` map of adopting capabilities. | +| **Agent Payments Protocol** | AP2 | An open protocol designed to enable AI agents to securely interoperate and complete payments autonomously. UCP leverages AP2 for secure payment mandates. | +| **Agent2Agent Protocol** | A2A | An open standard for secure, collaborative communication between diverse AI agents. UCP can use A2A as a transport layer. | +| **Capability** | - | A standalone core feature that a business supports (e.g., Checkout, Identity Linking). Capabilities are the fundamental "verbs" of UCP. | +| **Credential Provider** | CP | A trusted entity (like a digital wallet) responsible for securely managing and executing the user's payment and identity credentials. | +| **Extension** | - | An optional capability that augments another capability via the `extends` field. Extensions appear in `ucp.capabilities[]` alongside core capabilities. | +| **Model Context Protocol** | MCP | A protocol standardizing how AI models connect to external data and tools. UCP capabilities map 1:1 to MCP tools. | +| **Profile** | - | A JSON document hosted by businesses and platforms at a well-known URI, declaring their identity, supported capabilities, and endpoints. | +| **Protocol Namespace** | - | Reserved `ucp` member for protocol metadata and annotations in structured UCP objects with schema-defined fields. Dictionaries are excluded: `ucp` is key data there; structured object values remain eligible. Top-level envelope is its root. | +| **Universal Commerce Protocol** | UCP | The standard defined in this document, enabling interoperability between commerce entities via standardized capabilities and discovery. | ## Commerce diff --git a/docs/specification/overview.md b/docs/specification/overview.md index 0793cea17..fc28243e7 100644 --- a/docs/specification/overview.md +++ b/docs/specification/overview.md @@ -958,6 +958,179 @@ example: } ``` +### The `ucp` Protocol Namespace + +The member name `ucp` is reserved as the **protocol namespace** in every +structured UCP object scope — an object whose members are schema-defined +fields. The top-level `ucp` member that profiles and responses carry — +described in [Profile Structure](#profile-structure) above — is not a special +wrapper; it is the root manifestation of this reservation: a reserved member +of the root object. The reservation does not apply to a dictionary container, +whose keys are data rather than fields. A dictionary key named `ucp` is +ordinary data. A structured object used as a dictionary value remains an +eligible scope. Schema authors **MUST NOT** define a domain field named `ucp` +in structured object schemas or extensions. + +At each eligible structured scope, `ucp` carries the protocol's statements +about that scope: protocol metadata at the root (version, services, +capabilities, payment handlers) and structural annotations such as +[`map_order`](#map_order). + +**Openness.** The `ucp` container is open. Consumers **MUST** ignore members +inside `ucp` that they do not recognize (tolerant reader). Openness exists so +documents produced under a newer UCP version remain readable by older +consumers — it is *not* extension space. Only UCP core defines members inside +`ucp`, and extension authors **MUST NOT** place extension data there. An +unrecognized member inside `ucp` means "defined by a newer UCP version," +never "extension data." + +**No direct recursion.** Producers **MUST NOT** emit a `ucp` member as a direct +child of another `ucp` member (`ucp.ucp`). Structured objects beneath it, such +as a capability's `config`, remain eligible for their own `ucp` member. + +**Ambient vocabulary.** The protocol namespace is ambient within structured +UCP objects: a Business or Platform **MAY** include a `ucp` member at any +eligible structured scope, and its contents are defined exclusively by UCP +core's vocabulary — the member is part of the UCP document grammar, like the +name reservation itself. The reservation stops at a dictionary container. A +Business or Platform **MUST NOT** interpret a dictionary key named `ucp` as +the protocol namespace; the key and its value are ordinary dictionary data. +For example, `attribution` is a dictionary of string values, so an attribution +key named `ucp` is ordinary attribution data, not a protocol-namespace member. +Guidance for schema authors on working within this reservation lives in the +Schema Authoring Guide's +[The Reserved `ucp` Member](/documentation/schema-authoring/#the-reserved-ucp-member) +section. A Business or Platform encountering a `ucp` member at an eligible +structured scope processes the members it recognizes, each per its own +definition, and **MUST** ignore unrecognized members (see *Openness* above). +A member is admitted to the vocabulary only if it is safe to ignore: a +Business or Platform that does not process it loses only that member's +benefit, never correctness. A Business or Platform that ignores `map_order`, +for example, simply traverses the map unordered — the status quo before +ordering existed. + +**Scope determines obligations.** At the root of profiles and responses, the +`ucp` envelope additionally carries the required handshake members exactly as +specified elsewhere in this document — this section changes none of those +obligations. A Business or Platform **MAY** omit the member at every other +eligible structured scope. Dictionary containers are not eligible scopes and +carry no protocol-namespace obligation. Conformance to the vocabulary is +defined by this specification's processing rules, not by ordinary instance +validation against open UCP source schemas; that validation treats ambient +`ucp` members as ignored unknown objects. + +**Vocabulary applicability.** Each registered protocol-namespace member defines +the document contexts and message directions where it applies. + +**Schema processing.** UCP source schemas are open by default, so ordinary +validation against them may accept ambient `ucp` without applying the protocol +vocabulary. For a selected message direction, a UCP-aware resolver **MUST** +produce a resolved schema that recognizes and validates ambient `ucp` at every +eligible structured scope against the central vocabulary in +`ucp.json#/$defs/members`, subject to that vocabulary's applicability in the +selected direction. The `ucp` namespace remains open to unrecognized members +for forward compatibility, even if the resolved schema rejects other unknown +domain fields. The result is ordinary JSON Schema that standard validators and +code generators can consume. + +#### `map_order` + +JSON object members are unordered: member order is not guaranteed to survive +parsing, and +[RFC 8785](https://www.rfc-editor.org/rfc/rfc8785.html){ target="_blank" } +(JSON Canonicalization Scheme), which UCP signing relies on, sorts object +member names while preserving array element order. An array +value is therefore the only order carrier that survives canonicalization and +signing — and `map_order` uses one. + +`map_order` declares a preferred key-traversal order for map-valued fields in +the scope annotated by its containing `ucp` member. At a nested scope, each +key of `map_order` names a map field on the object that contains `ucp`. At the +document root, each key instead names a sibling map field inside the root `ucp` +envelope. Root domain fields outside `ucp`, such as a checkout response's +`actions`, are not targets. Each value is an array of the target map's keys in +preferred traversal order. + +`map_order` does not apply to UCP operation requests. + +For a target map field `` and its companion array `map_order.`: + +1. Producers **MUST NOT** rely on JSON object member order for UCP map-valued + registries; `map_order` is the order carrier. +2. `map_order.` contains keys from the target map field `` in + preferred traversal order. +3. The order array **MAY** be partial: listed keys are traversed first, in + array order. +4. Unlisted map keys remain valid and available; consumers traverse them + after the listed keys, using the field-defined fallback order or, if the + field defines none, lexicographic order. +5. The order array is not an allowlist: consumers **MUST NOT** interpret + omission of a key as removal, ineligibility, or reduced support. +6. Producers **SHOULD** list only keys present in the target map. Consumers + **SHOULD** ignore entries naming keys that are not present, and entries + whose target field is absent or is not a map. +7. Producers **MUST NOT** list the same map key more than once in an order + array. Consumers **MUST NOT** reject the containing document solely because + an order array repeats a map key. Consumers that process `map_order` + **MUST** honor the first occurrence and ignore later repetitions. +8. If `map_order`, or its entry for a field, is absent, no order is declared; + consumers **MUST NOT** fall back to object member order. +9. A field's own specification defines what ordered traversal *means* for it + (presentation, negotiation priority, and so on) — `map_order` carries + order and nothing else. + +Rules 3–5 are a deliberate divergence from conventions in which unlisted keys +are an error or are dropped: partial lists are always valid, and unlisted +keys are always retained. + +A business profile ordering its payment handlers: + + +```json +{ + "ucp": { + "version": "{{ ucp_version }}", + "services": { ... }, + "payment_handlers": { + "com.google.pay": [ + { "id": "gpay", "version": "{{ ucp_version }}" } + ], + "dev.shopify.shop_pay": [ + { "id": "shop_pay", "version": "{{ ucp_version }}" } + ] + }, + "map_order": { + "payment_handlers": ["dev.shopify.shop_pay", "com.google.pay"] + } + } +} +``` + +`map_order` is scope-generic — the same mechanism orders sibling maps at any +eligible structured scope, as when a Business orders the identity-provider +registry inside a capability's `config`: + + +```json +{ + "providers": { + "app.example.login": [ + {"type": "oauth2", "auth_url": "https://login.example.app"} + ], + "com.google": [ + {"type": "oauth2", "auth_url": "https://accounts.google.com"} + ] + }, + "ucp": { + "map_order": {"providers": ["app.example.login", "com.google"]} + } +} +``` + +What an order *means* remains per-field (rule 9); the one traversal semantics +defined today is the business's presentation preference for +`payment_handlers` — see [Payment Handlers](#payment-handlers). + ### Platform Advertisement on Request Platforms **MUST** communicate their profile URI with each request to enable @@ -1778,6 +1951,19 @@ governing body. 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 +order for their advertised handlers via `map_order.payment_handlers` in their +profile and response envelopes (see +[The `ucp` Protocol Namespace](#the-ucp-protocol-namespace)). The preference +is suggestive: it communicates the business's preferred presentation — +typically a conversion or risk judgment — and platforms **SHOULD** take it +into account but **MAY** apply their own ordering. The same suggestive +preference applies within a handler: the order of the business's advertised +`available_instruments` array communicates preferred instrument presentation, +earliest first. It is distinct from, and does not override, the buyer-side +preference a platform submits in `context.payment[]` (buyer-preferred +handlers, on the request side); the platform arbitrates between the two. + **Available Instrument Resolution:** Within each active handler, both the platform and the business independently advertise `available_instruments` — the set of instrument types and constraints each party supports. The business is diff --git a/scripts/scaffolds/profile_response.json b/scripts/scaffolds/profile_response.json index 75b96dfcf..826a1d85c 100644 --- a/scripts/scaffolds/profile_response.json +++ b/scripts/scaffolds/profile_response.json @@ -13,7 +13,30 @@ ] }, "capabilities": {}, - "payment_handlers": {} + "payment_handlers": { + "com.google.pay": [ + { + "id": "gpay", + "version": "2026-01-01", + "spec": "https://payments.google.com/gpay/specification", + "schema": "https://payments.google.com/gpay/schema.json" + } + ], + "dev.shopify.shop_pay": [ + { + "id": "shop_pay", + "version": "2026-01-01", + "spec": "https://shopify.dev/ucp/shop-pay", + "schema": "https://shopify.dev/ucp/shop-pay.json" + } + ] + }, + "map_order": { + "payment_handlers": [ + "dev.shopify.shop_pay", + "com.google.pay" + ] + } }, "keys": [ { diff --git a/source/schemas/ucp.json b/source/schemas/ucp.json index 79716bee7..71c190dce 100644 --- a/source/schemas/ucp.json +++ b/source/schemas/ucp.json @@ -46,6 +46,27 @@ "additionalProperties": true }, + "map_order": { + "type": "object", + "description": "Preferred key-traversal order for map-valued fields in the scope annotated by the containing `ucp` member. In a nested scope, each key names a map-valued field on the object containing `ucp`; at the document root, each key names a sibling map-valued field inside the root `ucp` envelope. Each array lists its target map's keys in preferred order. Lists may be partial: unlisted keys remain valid and are appended using the field-defined fallback or lexicographic order. This member is not an allowlist; consumers ignore unknown or absent target fields. Repeated map keys in an order array do not invalidate the containing payload; consumers use the first occurrence and ignore later repetitions.", + "additionalProperties": { + "type": "array", + "items": { "type": "string" } + } + }, + + "members": { + "type": "object", + "description": "Vocabulary and processing contract for the reserved `ucp` protocol-namespace member. Any structured UCP object scope—an object whose members are schema-defined fields—MAY carry a `ucp` member conforming to this definition. The reservation does not apply to dictionary scopes, where keys are data: a dictionary key named `ucp` is ordinary data. A structured object used as a dictionary value remains an eligible scope. Consumers process recognized members and MUST ignore unrecognized ones. At the document root, the `ucp` envelope additionally carries the handshake members defined by this schema. Open for forward compatibility; not an extension point — only UCP core registers members, and every member MUST be safe to ignore.", + "properties": { + "map_order": { + "$ref": "#/$defs/map_order", + "ucp_request": "omit" + } + }, + "additionalProperties": true + }, + "entity": { "type": "object", "description": "Shared foundation for all UCP entities.", @@ -83,6 +104,11 @@ "required": ["version"], "properties": { "version": { "$ref": "#/$defs/version" }, + "map_order": { + "$ref": "#/$defs/map_order", + "description": "Preferred key-traversal order for sibling registry fields inside the root `ucp` envelope (`services`, `capabilities`, and `payment_handlers`).", + "ucp_request": "omit" + }, "status": { "type": "string", "enum": ["success", "error"],