From 37bb7d73437d9907da02a52e55c59bf15dc1cdea Mon Sep 17 00:00:00 2001 From: Ilya Grigorik Date: Fri, 31 Jul 2026 11:07:50 -0700 Subject: [PATCH 1/3] reserve `ucp` namespace, add map_order for registry ordering 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 --- docs/documentation/schema-authoring.md | 42 ++++++- docs/specification/glossary.md | 23 ++-- docs/specification/overview.md | 157 ++++++++++++++++++++++++ scripts/scaffolds/profile_response.json | 25 +++- source/schemas/ucp.json | 23 ++++ 5 files changed, 254 insertions(+), 16 deletions(-) diff --git a/docs/documentation/schema-authoring.md b/docs/documentation/schema-authoring.md index d12fc0ee7..aa0d2300a 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):** 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..1cdccb7ab 100644 --- a/docs/specification/overview.md +++ b/docs/specification/overview.md @@ -958,6 +958,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 +1924,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/ucp.json b/source/schemas/ucp.json index 79716bee7..a78767ebd 100644 --- a/source/schemas/ucp.json +++ b/source/schemas/ucp.json @@ -46,6 +46,25 @@ "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" } + }, + "additionalProperties": true + }, + "entity": { "type": "object", "description": "Shared foundation for all UCP entities.", @@ -83,6 +102,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"], From 17f96ae4eaf45381bf0cc753cfdadb800e41ec8b Mon Sep 17 00:00:00 2001 From: Ilya Grigorik Date: Thu, 30 Jul 2026 21:26:50 -0700 Subject: [PATCH 2/3] add response-carried request constraints Static request schemas describe the inputs a Business accepts generally, but cannot express narrower requirements for specific transaction. Platforms therefore cannot know before submission that a negotiated quantity is fixed or that an otherwise optional field is required. Add response-only `$requestConstraints` as a bounded Draft 2020-12 fragment that a Business emits and enforces against the corresponding later request representation. A Platform may use the fragment when forming or validating input, or ignore it and rely on the Business's existing validation errors. Keep the structural member ambient rather than adding an ordinary property to every carrier schema. Each adopting contract defines correspondence and lifecycle, while the shared type closes the vocabulary to `required`, `properties`, `enum`, and `const`, with optional display text and `$comment`. Examples demonstrate a Cart Line Item fixed at quantity 100, preservation of the company-scoped `ACME-X7Q9-L2M4` discount code, and a submitted card instrument that requires `billing_address`. --- docs/documentation/schema-authoring.md | 64 +++++ docs/specification/overview.md | 244 ++++++++++++++++++ .../common/types/request_constraints.json | 72 ++++++ 3 files changed, 380 insertions(+) create mode 100644 source/schemas/common/types/request_constraints.json diff --git a/docs/documentation/schema-authoring.md b/docs/documentation/schema-authoring.md index aa0d2300a..64ce4e3ec 100644 --- a/docs/documentation/schema-authoring.md +++ b/docs/documentation/schema-authoring.md @@ -467,6 +467,70 @@ object fields: - **`minProperties`** — Empty objects (`{}`) are well-formed and harmless. Implementers should accept and process them as a no-op. +## `$requestConstraints` Structural Vocabulary + +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 + +`$requestConstraints` is ambient UCP structural vocabulary, not a JSON Schema +keyword or an ordinary property of a response or request schema. UCP reserves +`$`-prefixed member names on domain objects for structural vocabulary. +Structural names use lower camel case; ordinary domain fields remain snake case. +A Business or Platform publishing an extension **MUST NOT** define an extension +member whose name begins with `$`. + +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 objects in authoritative Business responses that are eligible to carry + `$requestConstraints`; +- the 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 attachment identifies the constrained logical object, and the documented +correspondence identifies its representation in the later request. Do not +declare `$requestConstraints` under the ordinary `properties` of a response or +request schema. + +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/overview.md b/docs/specification/overview.md index 1cdccb7ab..2253e72a5 100644 --- a/docs/specification/overview.md +++ b/docs/specification/overview.md @@ -31,6 +31,250 @@ 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. + +`$requestConstraints` carries that transaction-specific restriction. It is +ambient UCP structural vocabulary, not a JSON Schema keyword or an ordinary +domain field. Its value is the shared +[Request Constraints](site:schemas/common/types/request_constraints.json) type, +a bounded JSON Schema Draft 2020-12 fragment. + +On UCP domain objects, member names beginning with `$` are reserved for UCP +structural vocabulary. Structural names use lower camel case, such as +`$requestConstraints`; ordinary UCP fields remain snake case. A Business or +Platform publishing an extension **MUST NOT** define an extension member whose +name begins with `$`. The `$comment` member admitted inside a Request Constraints +fragment is standard JSON Schema vocabulary, not an extension to the UCP +structural namespace. `$requestConstraints` is response-only and **MUST NOT** +appear in a request or discovery profile. + +### Guidelines + +#### Business + +A Business **MAY** attach `$requestConstraints` only to an object in an +authoritative response 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 `$requestConstraints` to form or validate the +corresponding request input. If the Platform cannot safely process a fragment, +it **MAY** ignore the fragment and rely on Business validation and the +containing operation's existing errors. + +### Correspondence and lifecycle + +The attachment identifies the constrained logical object. The fragment applies +to that object's representation in the later request, not to the response +object that carries it. That 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 `$requestConstraints`, 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} + ], + "$requestConstraints": { + "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. +`$requestConstraints` 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": {} + }, + "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"] + }, + "$requestConstraints": { + "required": ["discounts"], + "properties": { + "discounts": { + "required": ["codes"], + "properties": { + "codes": {"const": ["ACME-X7Q9-L2M4"]} + } + } + } + } +} +``` + +Because `discounts.codes` exists in the resolved Checkout update schema when the +Discount extension is active, it is a valid target. Ambient structural +vocabulary does not require the Discount schema to declare a carrier slot for +`$requestConstraints`. + +#### Billing address on a submitted card instrument + +A Business can attach `$requestConstraints` to an available card instrument to +require a field on the corresponding submitted card instrument: + + +```json +{ + "type": "card", + "$requestConstraints": { + "required": ["billing_address"] + } +} +``` + +The adopting payment-handler contract defines the corresponding submitted +instrument and its target request schema. The available instrument remains +availability metadata; `$requestConstraints` 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 diff --git a/source/schemas/common/types/request_constraints.json b/source/schemas/common/types/request_constraints.json new file mode 100644 index 000000000..e56eda767 --- /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": "A closed, bounded JSON Schema Draft 2020-12 fragment that constrains 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 + } + } +} From 3105ad5aa3e73605393f2be36efbefc75f1ce9f7 Mon Sep 17 00:00:00 2001 From: Ilya Grigorik Date: Fri, 31 Jul 2026 11:53:39 -0700 Subject: [PATCH 3/3] register request constraints in the `ucp` namespace Static request schemas cannot express transaction-specific narrowing, forcing Platforms to discover negotiated requirements only after submission fails. Register response-only `ucp.request_constraints` in the central protocol vocabulary and define a closed Draft 2020-12 fragment for object presence and value constraints. Attach constraints through the containing `ucp` scope so host schemas do not need carrier-specific declarations. Keep Platform processing optional and Business validation authoritative, preserving the namespace requirement that registered members remain safe to ignore. --- docs/documentation/schema-authoring.md | 35 +++--- docs/specification/overview.md | 113 +++++++++--------- .../common/types/request_constraints.json | 2 +- source/schemas/ucp.json | 6 +- 4 files changed, 82 insertions(+), 74 deletions(-) diff --git a/docs/documentation/schema-authoring.md b/docs/documentation/schema-authoring.md index 64ce4e3ec..11566573a 100644 --- a/docs/documentation/schema-authoring.md +++ b/docs/documentation/schema-authoring.md @@ -467,7 +467,7 @@ object fields: - **`minProperties`** — Empty objects (`{}`) are well-formed and harmless. Implementers should accept and process them as a no-op. -## `$requestConstraints` Structural Vocabulary +## The `request_constraints` Protocol Member Canonical placement, correspondence, lifecycle, and evaluation behavior is defined in @@ -476,12 +476,15 @@ section defines how schema authors encode and adopt that behavior. ### Namespace and encoding -`$requestConstraints` is ambient UCP structural vocabulary, not a JSON Schema -keyword or an ordinary property of a response or request schema. UCP reserves -`$`-prefixed member names on domain objects for structural vocabulary. -Structural names use lower camel case; ordinary domain fields remain snake case. -A Business or Platform publishing an extension **MUST NOT** define an extension -member whose name begins with `$`. +`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: @@ -510,24 +513,24 @@ present, and both apply when both are present. No other member is valid. For each adoption, document: -- the objects in authoritative Business responses that are eligible to carry - `$requestConstraints`; -- the corresponding representation in a later request; +- 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 attachment identifies the constrained logical object, and the documented -correspondence identifies its representation in the later request. Do not -declare `$requestConstraints` under the ordinary `properties` of a response or -request schema. +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 +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. diff --git a/docs/specification/overview.md b/docs/specification/overview.md index 2253e72a5..d5d8982a2 100644 --- a/docs/specification/overview.md +++ b/docs/specification/overview.md @@ -39,49 +39,46 @@ 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. -`$requestConstraints` carries that transaction-specific restriction. It is -ambient UCP structural vocabulary, not a JSON Schema keyword or an ordinary -domain field. Its value is the shared +`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. - -On UCP domain objects, member names beginning with `$` are reserved for UCP -structural vocabulary. Structural names use lower camel case, such as -`$requestConstraints`; ordinary UCP fields remain snake case. A Business or -Platform publishing an extension **MUST NOT** define an extension member whose -name begins with `$`. The `$comment` member admitted inside a Request Constraints -fragment is standard JSON Schema vocabulary, not an extension to the UCP -structural namespace. `$requestConstraints` is response-only and **MUST NOT** -appear in a request or discovery profile. +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** attach `$requestConstraints` only to an object in an -authoritative response 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. +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 `$requestConstraints` to form or validate the -corresponding request input. If the Platform cannot safely process a fragment, -it **MAY** ignore the fragment and rely on Business validation and the -containing operation's existing errors. +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 attachment identifies the constrained logical object. The fragment applies -to that object's representation in the later request, not to the response -object that carries it. That 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. +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 `$requestConstraints`, the +earlier one. If the later representation omits `ucp.request_constraints`, the earlier constraint is removed. ### Constraint language @@ -109,12 +106,14 @@ Business offers the Line Item only as the negotiated lot of `100`: {"type": "subtotal", "amount": 120000}, {"type": "total", "amount": 120000} ], - "$requestConstraints": { - "title": "Contract quantity", - "description": "This item is available only in the negotiated quantity.", - "properties": { - "quantity": { - "const": 100 + "ucp": { + "request_constraints": { + "title": "Contract quantity", + "description": "This item is available only in the negotiated quantity.", + "properties": { + "quantity": { + "const": 100 + } } } } @@ -178,7 +177,7 @@ 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. -`$requestConstraints` defines no new outcome or error code. +`request_constraints` defines no new outcome or error code. ### Examples @@ -202,7 +201,18 @@ Checkout update. {"version": "{{ ucp_version }}"} ] }, - "payment_handlers": {} + "payment_handlers": {}, + "request_constraints": { + "required": ["discounts"], + "properties": { + "discounts": { + "required": ["codes"], + "properties": { + "codes": {"const": ["ACME-X7Q9-L2M4"]} + } + } + } + } }, "id": "checkout_123", "status": "incomplete", @@ -234,44 +244,35 @@ Checkout update. ], "discounts": { "codes": ["ACME-X7Q9-L2M4"] - }, - "$requestConstraints": { - "required": ["discounts"], - "properties": { - "discounts": { - "required": ["codes"], - "properties": { - "codes": {"const": ["ACME-X7Q9-L2M4"]} - } - } - } } } ``` Because `discounts.codes` exists in the resolved Checkout update schema when the -Discount extension is active, it is a valid target. Ambient structural -vocabulary does not require the Discount schema to declare a carrier slot for -`$requestConstraints`. +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 -A Business can attach `$requestConstraints` to an available card instrument to -require a field on the corresponding 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", - "$requestConstraints": { - "required": ["billing_address"] + "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; `$requestConstraints` applies to the submitted +availability metadata; `request_constraints` applies to the submitted instrument. This illustration defines no card brands, credentials, availability or options model, or payment-resolution behavior. diff --git a/source/schemas/common/types/request_constraints.json b/source/schemas/common/types/request_constraints.json index e56eda767..82d9a64de 100644 --- a/source/schemas/common/types/request_constraints.json +++ b/source/schemas/common/types/request_constraints.json @@ -2,7 +2,7 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://ucp.dev/schemas/common/types/request_constraints.json", "title": "Request Constraints", - "description": "A closed, bounded JSON Schema Draft 2020-12 fragment that constrains a corresponding representation in a later request.", + "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": { diff --git a/source/schemas/ucp.json b/source/schemas/ucp.json index a78767ebd..71d5db1f1 100644 --- a/source/schemas/ucp.json +++ b/source/schemas/ucp.json @@ -60,7 +60,11 @@ "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" } + "map_order": { "$ref": "#/$defs/map_order" }, + "request_constraints": { + "$ref": "common/types/request_constraints.json", + "ucp_request": "omit" + } }, "additionalProperties": true },