diff --git a/docs/documentation/schema-authoring.md b/docs/documentation/schema-authoring.md index d12fc0ee7..11566573a 100644 --- a/docs/documentation/schema-authoring.md +++ b/docs/documentation/schema-authoring.md @@ -184,6 +184,43 @@ 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 UCP object scope for the protocol +namespace — the top-level envelope is its root manifestation. See +[The `ucp` Protocol Namespace](../specification/overview.md#the-ucp-protocol-namespace) +for the normative rules. For schema authors this means: + +- **Never mint a domain field named `ucp`.** Domain schemas and extensions + **MUST NOT** declare a property named `ucp`; the name always denotes the + protocol namespace. +- **Never declare the `ucp` member, either — with one exception, below.** + The member is UCP document grammar, defined centrally: any object scope + **MAY** carry it without the domain schema saying so, just as no schema + declares the name reservation. + Domain schemas and extensions do not add a `ucp` property. Instance + validation treats an ambient `ucp` member as an ignored unknown object; + validating its contents is a conformance-tooling concern. +- **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 consumer 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 keys are constrained by + `propertyNames` to reverse-domain names, which rejects `ucp` by + construction. This is why structural metadata about a map — such as + `map_order` — sits in the parent scope's protocol namespace rather than + inside the map itself. +- **Closed objects must declare `ucp` explicitly.** Closing an object does + not exempt it from the protocol grammar. When an object scope in a UCP + payload is validated with `additionalProperties: false`, its schema + **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 objects open (see + [Open Objects](#open-objects-additionalproperties)). + ## The Entity Pattern All capabilities, services, and handlers extend a common `entity` base schema: @@ -402,10 +439,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):** @@ -433,6 +467,73 @@ object fields: - **`minProperties`** — Empty objects (`{}`) are well-formed and harmless. Implementers should accept and process them as a no-op. +## The `request_constraints` Protocol Member + +Canonical placement, correspondence, lifecycle, and evaluation behavior is +defined in +[Request Constraints](../specification/overview.md#request-constraints). This +section defines how schema authors encode and adopt that behavior. + +### Namespace and encoding + +`request_constraints` is a response-only protocol member centrally registered +in `ucp.json#/$defs/members`. Host domain schemas do not declare it. Its +containing `ucp` object follows [The Reserved `ucp` Member](#the-reserved-ucp-member), +including the rule that a closed host object explicitly declares an optional +`ucp` property referencing `ucp.json#/$defs/members`. That declaration admits +the centrally registered vocabulary; the host schema does not add an individual +`request_constraints` property. Businesses and Platforms **MUST NOT** publish +`ucp.request_constraints` in discovery profiles, and Platforms **MUST NOT** +include it in requests. + +The shared `schemas/common/types/request_constraints.json` type has two closed +constraint positions: + +| Position | Executable keywords | Inert members | Shape | +| :-- | :-- | :-- | :-- | +| Object Constraint | `required`, `properties` | `title`, `description`, `$comment` | Closed | +| Value Constraint | `enum`, `const` | None | Closed | + +At an Object Constraint position, `required` is an array of unique strings and +`properties` maps request field names to Object or Value Constraints. An empty +Object Constraint is a valid no-op. Optional string `title` and `description` +members provide Business-authored display text that a Platform **MAY** present; +they never affect validity. + +`$comment` is separately permitted as an inert string. JSON Schema +[Core §8.3](https://json-schema.org/draft/2020-12/json-schema-core#section-8.3) +defines it as a reserved comment location that does not produce an annotation +result. It is not display text and does not affect validity. + +At a Value Constraint position, `enum` is a non-empty array of values unique +under JSON Schema equality and `const` is any JSON value. At least one is +present, and both apply when both are present. No other member is valid. + +### Adoption requirements + +For each adoption, document: + +- the authoritative Business response scopes eligible to carry + `ucp.request_constraints`; +- the parent logical object's corresponding representation in a later request; +- the target operation and operation-specific request schema; and +- the lifecycle under which a later authoritative representation supersedes an + earlier one and omission removes the constraint. + +The `ucp` object containing `request_constraints` annotates its parent logical +object, and the documented correspondence identifies that object's +representation in the later request. Central registration supplies the protocol +member at every eligible scope; host domain schemas do not redeclare it. + +Ensure that names in `required` and under `properties` exist at the +corresponding level of the intended composed, operation-specific request schema. +Before emission, a Business **MUST** validate the fragment against the shared +type and **MUST** ensure that every target name is valid in the actual composed +request schema. A Platform that chooses to evaluate the fragment **MUST** perform +the same checks first. A composed extension field is a valid target; a field +omitted from that request by `ucp_request`, including a response-only field, is +not. + ## Extension-Declared Action Types Every Action type is declared by an extension and becomes available only when diff --git a/docs/specification/glossary.md b/docs/specification/glossary.md index 716e41f7f..978e74412 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** | - | The reserved `ucp` member, available at every UCP object scope, carrying the protocol's metadata and structural annotations (e.g., `map_order`) for that scope. The top-level `ucp` envelope in profiles and responses is its root manifestation. | +| **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..d5d8982a2 100644 --- a/docs/specification/overview.md +++ b/docs/specification/overview.md @@ -31,6 +31,251 @@ Schema notes: unless otherwise specified - Amounts format: Minor units (cents) +## Request Constraints + +A static UCP request schema describes the inputs a Business generally accepts, +but a Business can accept a narrower set for one transaction. For example, an +agreed Line Item can be available only at quantity `100`, or a selected payment +path can require `billing_address` even though the general request shape does +not. + +`request_constraints` carries that transaction-specific restriction at +`ucp.request_constraints`. It is a response-only protocol member registered in +`ucp.json#/$defs/members`. Its value is the shared +[Request Constraints](site:schemas/common/types/request_constraints.json) type, +a bounded JSON Schema Draft 2020-12 fragment. The enclosing `ucp` object is the +ambient protocol namespace; see [The `ucp` Protocol Namespace](#the-ucp-protocol-namespace) +for its general rules. Businesses and Platforms **MUST NOT** publish +`ucp.request_constraints` in discovery profiles, and Platforms **MUST NOT** +include it in requests. + +### Guidelines + +#### Business + +A Business **MAY** emit `ucp.request_constraints` only in an authoritative +response scope that has a corresponding representation in a later request. +When it does, the Business **MUST** emit a fragment that conforms to the shared +Request Constraints type and **MUST** enforce it. + +#### Platform + +A Platform **MAY** use `ucp.request_constraints` to form or validate the +corresponding request input and to present its display text. A Platform **MAY** +instead ignore `ucp.request_constraints`. Doing so loses only early request +formation, validation, and display benefits. Business validation remains +authoritative, so correctness does not depend on Platform processing. + +### Correspondence and lifecycle + +The `ucp` object containing `request_constraints` annotates its parent logical +object. The fragment applies to that object's corresponding representation in +the later request, not to the response representation that carries it. The +request representation can have a different shape because response-only fields +are omitted by `ucp_request`. When existing operation semantics do not already +establish the correspondence, the adopting contract defines the corresponding +request representation, target operation, and operation-specific request +schema. + +For the same logical object, a later authoritative representation supersedes an +earlier one. If the later representation omits `ucp.request_constraints`, the +earlier constraint is removed. + +### Constraint language + +This Cart response shows a transaction-specific constraint in context. The +Business offers the Line Item only as the negotiated lot of `100`: + + +```json +{ + "ucp": { + "version": "{{ ucp_version }}" + }, + "id": "cart_123", + "line_items": [ + { + "id": "line_123", + "item": { + "id": "sku_123", + "title": "Bulk screws", + "price": 1200 + }, + "quantity": 100, + "totals": [ + {"type": "subtotal", "amount": 120000}, + {"type": "total", "amount": 120000} + ], + "ucp": { + "request_constraints": { + "title": "Contract quantity", + "description": "This item is available only in the negotiated quantity.", + "properties": { + "quantity": { + "const": 100 + } + } + } + } + } + ], + "currency": "USD", + "totals": [ + {"type": "subtotal", "amount": 120000}, + {"type": "total", "amount": 120000} + ] +} +``` + +The static Line Item schema accepts many positive quantities. For this response, +`properties` selects `quantity` in the corresponding request Line Item and +`const` narrows its accepted value to `100`. `title` and `description` are +optional display text and do not affect validity. + +The root is an **Object Constraint**. Under its `properties` member, each named +field maps to another Object Constraint or to a **Value Constraint**. Both +positions are closed: + +| Position | Executable members | Inert members | Shape and behavior | +| :-- | :-- | :-- | :-- | +| Object Constraint | `required`, `properties` | `title`, `description`, `$comment` | `required` is an array of unique field names. `properties` maps field names to Object or Value Constraints. An empty Object Constraint is a valid no-op. | +| Value Constraint | `enum`, `const` | None | `enum` is a non-empty array of values unique under JSON Schema equality. `const` is any JSON value. At least one is present; when both appear, both apply. | + +A Platform **MAY** present Business-authored `title` and `description` text; +neither member affects validity. `$comment` is an inert JSON Schema +Core comment string, not display text, and does not affect validity. + +A Business **MUST** validate each fragment against the shared closed shape +before emitting it. The Business **MUST** also ensure that every field name in +`required` or under `properties` exists at the corresponding level of the +actual composed, operation-specific request schema. A response-only field is +not a valid target. A Business **MUST NOT** emit an invalid fragment. + +A Platform that chooses to evaluate a fragment **MUST** perform those checks +first. If the fragment is invalid or cannot be processed safely, the Platform +**MAY** ignore it and rely on Business validation. The Platform **MUST NOT** +partially interpret a fragment. + +A Business **MUST** apply implementation-defined resource limits when validating +a fragment before emission and when enforcing it. The Business **MUST** bound +encoded size, nesting depth, node and member counts, and equality work such as +checking `enum` uniqueness. If it cannot validate a fragment within those +limits, the Business **MUST NOT** emit it. A Platform that chooses to evaluate a +fragment **MUST** apply equivalent local limits. If a limit is exhausted, the +Platform **MAY** ignore the fragment and rely on Business validation; it **MUST +NOT** partially interpret the fragment. + +### Validation and evaluation + +A Business **MUST** validate the corresponding submitted representation against +both the resolved request schema and the fragment, and **MUST** reject the +representation if either validation fails. A Platform **MAY** perform the same +two validations before submission. + +If `S` is the resolved request schema and `C` is the fragment, applying `C` +alongside `S` is validity-equivalent to `{ "allOf": [S, C] }`; an implementation +does not need to materialize a combined schema. + +The containing operation's existing outcomes and error contract apply. +`request_constraints` defines no new outcome or error code. + +### Examples + +#### Locked negotiated discount codes + +This Checkout response has the Discount extension active. Its fragment locks +the negotiated business-to-business discount-code array in the corresponding +Checkout update. + + +```json +{ + "ucp": { + "version": "{{ ucp_version }}", + "status": "success", + "capabilities": { + "dev.ucp.shopping.checkout": [ + {"version": "{{ ucp_version }}"} + ], + "dev.ucp.shopping.discount": [ + {"version": "{{ ucp_version }}"} + ] + }, + "payment_handlers": {}, + "request_constraints": { + "required": ["discounts"], + "properties": { + "discounts": { + "required": ["codes"], + "properties": { + "codes": {"const": ["ACME-X7Q9-L2M4"]} + } + } + } + } + }, + "id": "checkout_123", + "status": "incomplete", + "currency": "USD", + "line_items": [ + { + "id": "line_123", + "item": { + "id": "sku_123", + "title": "Bulk screws", + "price": 1200 + }, + "quantity": 24, + "totals": [ + {"type": "subtotal", "amount": 28800}, + {"type": "total", "amount": 28800} + ] + } + ], + "totals": [ + {"type": "subtotal", "amount": 28800}, + {"type": "total", "amount": 28800} + ], + "links": [ + { + "type": "terms_of_service", + "url": "https://business.example/terms" + } + ], + "discounts": { + "codes": ["ACME-X7Q9-L2M4"] + } +} +``` + +Because `discounts.codes` exists in the resolved Checkout update schema when the +Discount extension is active, it is a valid target. UCP centrally registers +`request_constraints`; the Discount schema does not declare it. + +#### Billing address on a submitted card instrument + +In this example, a Business emits `ucp.request_constraints` on an available +card instrument to require a field on the corresponding submitted card +instrument: + + +```json +{ + "type": "card", + "ucp": { + "request_constraints": { + "required": ["billing_address"] + } + } +} +``` + +The adopting payment-handler contract defines the corresponding submitted +instrument and its target request schema. The available instrument remains +availability metadata; `request_constraints` applies to the submitted +instrument. This illustration defines no card brands, credentials, availability +or options model, or payment-resolution behavior. + ## Actions An Action is an outstanding unit of extension-defined work for a Platform to @@ -958,6 +1203,152 @@ example: } ``` +### The `ucp` Protocol Namespace + +The member name `ucp` is reserved in every UCP object scope as the **protocol +namespace**. 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 same reservation applies at every nested object +scope. Schema authors **MUST NOT** define a member named `ucp` in domain +schemas or extensions. + +At each 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." + +**Idempotence.** Within `ucp`, protocol members appear directly: `ucp.ucp` +never exists. Producers **MUST NOT** emit a `ucp` member inside `ucp`, and no +UCP version will define one. Note that because the container is open, an +errant `ucp.ucp` — like any misspelled member name — still validates as an +unknown member that consumers ignore; schema validation does not reject such +instances. Guarding against them is an authoring-time concern, not a +wire-validation one. + +**Ambient vocabulary.** The protocol namespace is ambient: any object scope +in a UCP payload **MAY** carry a `ucp` member, 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. 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 consumer encountering a `ucp` member at any 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 +consumer that does not process it loses only that member's benefit, never +correctness. A consumer 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. At every other scope the member is optional and its absence is +always valid. Conformance to the vocabulary is defined by this +specification's processing rules, not by instance validation, which treats +ambient `ucp` members as ignored unknown objects. + +#### `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 sibling map-valued +fields of its containing scope. Each key of `map_order` names a sibling map +field; its value is an array of that map's keys in preferred traversal order. +At the root envelope, `map_order` appears directly beside the registries it +orders — both are protocol-namespace members. In a nested scope, it lives +inside that scope's `ucp` member. + +For a sibling 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 sibling map `` 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 sibling map. Consumers + **SHOULD** ignore entries naming keys that are not present, and entries + whose target field is absent or is not a map. +7. Duplicate keys are invalid; consumers **SHOULD** honor the first + occurrence of a key and ignore later duplicates. +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 +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 +2169,17 @@ 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. 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/common/types/request_constraints.json b/source/schemas/common/types/request_constraints.json new file mode 100644 index 000000000..82d9a64de --- /dev/null +++ b/source/schemas/common/types/request_constraints.json @@ -0,0 +1,72 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ucp.dev/schemas/common/types/request_constraints.json", + "title": "Request Constraints", + "description": "Closed, bounded JSON Schema Draft 2020-12 fragment emitted and authoritatively enforced by the Business through registered `ucp.request_constraints` to narrow a corresponding representation in a later request.", + "$ref": "#/$defs/object_constraint", + "$defs": { + "object_constraint": { + "type": "object", + "description": "A closed Object Constraint that applies standard Draft 2020-12 `required` and `properties` semantics to a corresponding submitted object.", + "properties": { + "required": { + "type": "array", + "ucp_request": "omit", + "description": "Names of fields required in the corresponding object of the later submitted representation.", + "uniqueItems": true, + "items": { + "type": "string" + } + }, + "properties": { + "type": "object", + "ucp_request": "omit", + "description": "Constraints for named fields of the corresponding object in the later submitted representation.", + "additionalProperties": { + "oneOf": [ + {"$ref": "#"}, + {"$ref": "#/$defs/value_constraint"} + ] + } + }, + "title": { + "type": "string", + "ucp_request": "omit", + "description": "Optional short display label." + }, + "description": { + "type": "string", + "ucp_request": "omit", + "description": "Optional display explanation." + }, + "$comment": { + "type": "string", + "ucp_request": "omit", + "description": "Inert JSON Schema Core comment." + } + }, + "additionalProperties": false + }, + "value_constraint": { + "type": "object", + "description": "A Value Constraint applies standard Draft 2020-12 `enum` and/or `const` semantics to the corresponding value in the later submitted representation. When both are present, both constraints apply.", + "anyOf": [ + {"required": ["enum"]}, + {"required": ["const"]} + ], + "properties": { + "enum": { + "type": "array", + "ucp_request": "omit", + "description": "A non-empty set of JSON values permitted for the corresponding submitted value.", + "minItems": 1, + "uniqueItems": true + }, + "const": { + "ucp_request": "omit" + } + }, + "additionalProperties": false + } + } +} diff --git a/source/schemas/ucp.json b/source/schemas/ucp.json index 79716bee7..71d5db1f1 100644 --- a/source/schemas/ucp.json +++ b/source/schemas/ucp.json @@ -46,6 +46,29 @@ "additionalProperties": true }, + "map_order": { + "type": "object", + "description": "Preferred key-traversal order for sibling map-valued fields of the containing scope. Each property names a sibling map field and its array lists that 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.", + "additionalProperties": { + "type": "array", + "items": { "type": "string" }, + "uniqueItems": true + } + }, + + "members": { + "type": "object", + "description": "Vocabulary and processing contract for the reserved `ucp` protocol-namespace member. Any object scope in a UCP payload MAY carry a `ucp` member conforming to this definition; 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" }, + "request_constraints": { + "$ref": "common/types/request_constraints.json", + "ucp_request": "omit" + } + }, + "additionalProperties": true + }, + "entity": { "type": "object", "description": "Shared foundation for all UCP entities.", @@ -83,6 +106,10 @@ "required": ["version"], "properties": { "version": { "$ref": "#/$defs/version" }, + "map_order": { + "$ref": "#/$defs/map_order", + "description": "Preferred key-traversal order for sibling map-valued registry fields in this object (`services`, `capabilities`, and `payment_handlers`)." + }, "status": { "type": "string", "enum": ["success", "error"],