From 37bb7d73437d9907da02a52e55c59bf15dc1cdea Mon Sep 17 00:00:00 2001 From: Ilya Grigorik Date: Fri, 31 Jul 2026 11:07:50 -0700 Subject: [PATCH 01/10] 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 6391c451c26a2ef3c8736425bfc2c68235e1c9c1 Mon Sep 17 00:00:00 2001 From: Ilya Grigorik Date: Mon, 3 Aug 2026 10:01:32 -0700 Subject: [PATCH 02/10] explain within-handler presentation preference Handler-level presentation preference is carried by map_order.payment_handlers; review on #657 noted the instrument grain was uncovered. The order of the business's advertised available_instruments array now carries the same suggestive within-handler preference, earliest first: platforms SHOULD consider it and MAY apply their own ordering, and the buyer-side context.payment[] preference remains distinct with the platform arbitrating. Completes handler-major presentation ordering at both grains. --- docs/specification/overview.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/specification/overview.md b/docs/specification/overview.md index 1cdccb7ab..bf94c36e8 100644 --- a/docs/specification/overview.md +++ b/docs/specification/overview.md @@ -1930,10 +1930,12 @@ 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. +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 From 35954af39d94167980f2f64593c2d966ca6566b0 Mon Sep 17 00:00:00 2001 From: Ilya Grigorik Date: Thu, 6 Aug 2026 10:24:00 -0700 Subject: [PATCH 03/10] exclude dictionaries from the ambient namespace The ambient `ucp` reservation applies to structured objects whose members are schema-defined fields. Dictionary keys are application data, so reserving `ucp` there conflicts with schemas such as `attribution`, where all values are strings. Limit the reservation to structured object scopes and treat `ucp` as ordinary data in dictionary containers. Keep structured objects used as dictionary values eligible. Narrow the closed-object authoring rule to the same boundary and align the central vocabulary description, overview, and glossary. --- docs/documentation/schema-authoring.md | 69 +++++++++++++++----------- docs/specification/glossary.md | 2 +- docs/specification/overview.md | 66 ++++++++++++++---------- source/schemas/ucp.json | 2 +- 4 files changed, 82 insertions(+), 57 deletions(-) diff --git a/docs/documentation/schema-authoring.md b/docs/documentation/schema-authoring.md index aa0d2300a..e6e69fe1c 100644 --- a/docs/documentation/schema-authoring.md +++ b/docs/documentation/schema-authoring.md @@ -186,40 +186,53 @@ The same registry structure appears in three contexts with different field requi ## 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 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 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. +- **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. Instance validation 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 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 + 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 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)). +- **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. ## The Entity Pattern diff --git a/docs/specification/glossary.md b/docs/specification/glossary.md index 978e74412..e112d573f 100644 --- a/docs/specification/glossary.md +++ b/docs/specification/glossary.md @@ -36,7 +36,7 @@ acronym in each specification Markdown file spells out the full term (e.g., | **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. | +| **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 bf94c36e8..3d92eb8d0 100644 --- a/docs/specification/overview.md +++ b/docs/specification/overview.md @@ -960,17 +960,21 @@ 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 — +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 same reservation applies at every nested object -scope. Schema authors **MUST NOT** define a member named `ucp` in domain -schemas or extensions. +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 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). +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 @@ -988,27 +992,35 @@ 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 +**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 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). +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 -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. +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. 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. +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 instance validation, +which treats ambient `ucp` members as ignored unknown objects. #### `map_order` @@ -1024,8 +1036,8 @@ signing — and `map_order` uses one. 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. +orders — both are protocol-namespace members. In a nested eligible structured +scope, it lives inside that scope's `ucp` member. For a sibling map field `` and its companion array `map_order.`: @@ -1080,8 +1092,8 @@ A business profile ordering its payment handlers: ``` `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`: +eligible structured scope, as when a Business orders the identity-provider +registry inside a capability's `config`: ```json diff --git a/source/schemas/ucp.json b/source/schemas/ucp.json index a78767ebd..c4c1085d2 100644 --- a/source/schemas/ucp.json +++ b/source/schemas/ucp.json @@ -58,7 +58,7 @@ "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.", + "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" } }, From 622102d460c8222df4540f10937357b4e027eb14 Mon Sep 17 00:00:00 2001 From: Ilya Grigorik Date: Thu, 6 Aug 2026 10:39:31 -0700 Subject: [PATCH 04/10] prohibit direct namespace recursion The existing idempotence rule prohibited any nested `ucp` member, which also excluded valid protocol namespaces on structured objects located beneath the root envelope, such as a capability's `config`. Limit the prohibition to a direct `ucp.ucp` child while keeping deeper structured objects eligible for their own protocol namespace. --- docs/specification/overview.md | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/docs/specification/overview.md b/docs/specification/overview.md index 3d92eb8d0..9b921bcbd 100644 --- a/docs/specification/overview.md +++ b/docs/specification/overview.md @@ -984,13 +984,9 @@ consumers — it is *not* extension space. Only UCP core defines members inside 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. +**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 From 2b7ba412f1262c7c4aa92dd28f35fac212b718bb Mon Sep 17 00:00:00 2001 From: Ilya Grigorik Date: Thu, 6 Aug 2026 11:40:28 -0700 Subject: [PATCH 05/10] define the ambient namespace validation contract UCP schemas are open by default, so ordinary JSON Schema validation accepts ambient `ucp` at eligible structured scopes without applying vocabulary rules. Recommend that UCP-aware tooling recognize and apply the central vocabulary. Require UCP-aware strict validators to continue allowing and validating ambient `ucp` while rejecting other unknown domain fields. Note that non-UCP-aware strict validators may reject it; this is an accepted compatibility limit. --- docs/documentation/schema-authoring.md | 9 +++++++++ docs/specification/overview.md | 8 ++++++++ 2 files changed, 17 insertions(+) diff --git a/docs/documentation/schema-authoring.md b/docs/documentation/schema-authoring.md index e6e69fe1c..9749291ea 100644 --- a/docs/documentation/schema-authoring.md +++ b/docs/documentation/schema-authoring.md @@ -204,6 +204,15 @@ for the normative rules. For schema authors this means: domain object. Instance validation treats an ambient `ucp` member as an ignored unknown object; validating its contents is a conformance-tooling concern. +- **Keep open schemas and strict tooling namespace-aware.** UCP schemas are + open by default, and ambient `ucp` is protocol metadata rather than a domain + field. Generic JSON tooling can parse the member; ordinary JSON Schema + validation accepts it as an undeclared property at an eligible structured + scope without applying UCP vocabulary rules. A UCP-aware strict validator + **MUST** allow the undeclared member and validate it against + `ucp.json#/$defs/members`, even when rejecting other unknown domain fields. + A non-UCP-aware strict validator may reject the member; this is an accepted + compatibility limit. - **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 diff --git a/docs/specification/overview.md b/docs/specification/overview.md index 9b921bcbd..93a08f406 100644 --- a/docs/specification/overview.md +++ b/docs/specification/overview.md @@ -1018,6 +1018,14 @@ carry no protocol-namespace obligation. 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. +**Schema processing.** UCP schemas are open by default. Ordinary JSON Schema +validation accepts an undeclared ambient `ucp` at an eligible structured scope +without applying UCP vocabulary rules; UCP-aware tooling **SHOULD** recognize +and apply `ucp.json#/$defs/members`. A UCP-aware strict validator **MUST** +allow and validate the member, even when rejecting other unknown domain fields. +A non-UCP-aware strict validator may reject it; this is an accepted +compatibility limit. + #### `map_order` JSON object members are unordered: member order is not guaranteed to survive From a59ff01866c0cd250bb78d91717a52e48df020ab Mon Sep 17 00:00:00 2001 From: Ilya Grigorik Date: Thu, 6 Aug 2026 12:15:21 -0700 Subject: [PATCH 06/10] clarify complete checkout signature coverage AP2 merchant authorization covers the JCS-canonicalized checkout without `ap2`, rather than the raw HTTP body bytes. Projecting the checkout through schema-recognized fields can therefore change the signed logical content. Clarify that verification and mandate construction use the complete checkout JSON and that removing any covered member invalidates the signature. --- docs/specification/ap2-mandates.md | 4 ++++ 1 file changed, 4 insertions(+) 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. From d19142736f7de1b45d235d4df0cb87d58dc96eb1 Mon Sep 17 00:00:00 2001 From: Ilya Grigorik Date: Thu, 6 Aug 2026 13:55:56 -0700 Subject: [PATCH 07/10] omit map_order from operation requests Mark both `map_order` properties with `ucp_request: "omit"`. Document per-member applicability and require repeated exposures to carry the same annotation. Request resolution now removes `map_order` while retaining the containing `ucp` envelope where its schema requires it. --- docs/documentation/schema-authoring.md | 6 ++++++ docs/specification/overview.md | 5 +++++ source/schemas/ucp.json | 8 ++++++-- 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/docs/documentation/schema-authoring.md b/docs/documentation/schema-authoring.md index 9749291ea..737d86dab 100644 --- a/docs/documentation/schema-authoring.md +++ b/docs/documentation/schema-authoring.md @@ -242,6 +242,12 @@ for the normative rules. For schema authors this means: 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 diff --git a/docs/specification/overview.md b/docs/specification/overview.md index 93a08f406..fd15894f1 100644 --- a/docs/specification/overview.md +++ b/docs/specification/overview.md @@ -1018,6 +1018,9 @@ carry no protocol-namespace obligation. 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. +**Vocabulary applicability.** Each registered protocol-namespace member defines +the document contexts and message directions where it applies. + **Schema processing.** UCP schemas are open by default. Ordinary JSON Schema validation accepts an undeclared ambient `ucp` at an eligible structured scope without applying UCP vocabulary rules; UCP-aware tooling **SHOULD** recognize @@ -1043,6 +1046,8 @@ At the root envelope, `map_order` appears directly beside the registries it orders — both are protocol-namespace members. In a nested eligible structured scope, it lives inside that scope's `ucp` member. +`map_order` does not apply to UCP operation requests. + For a sibling map field `` and its companion array `map_order.`: diff --git a/source/schemas/ucp.json b/source/schemas/ucp.json index c4c1085d2..0133c4a52 100644 --- a/source/schemas/ucp.json +++ b/source/schemas/ucp.json @@ -60,7 +60,10 @@ "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" } + "map_order": { + "$ref": "#/$defs/map_order", + "ucp_request": "omit" + } }, "additionalProperties": true }, @@ -104,7 +107,8 @@ "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`)." + "description": "Preferred key-traversal order for sibling map-valued registry fields in this object (`services`, `capabilities`, and `payment_handlers`).", + "ucp_request": "omit" }, "status": { "type": "string", From 4f1b2e6863daf7dce4c5e1645355cb5213e1f16b Mon Sep 17 00:00:00 2001 From: Ilya Grigorik Date: Thu, 6 Aug 2026 14:36:53 -0700 Subject: [PATCH 08/10] define map_order target resolution Nested `map_order` lives inside `ucp` while its target map sits on the object annotated by `ucp`. At the root, registries and `map_order` instead share the root envelope. Calling both relationships sibling fields left root domain maps ambiguous. Define nested lookup on the object containing `ucp` and root lookup inside the root envelope. Leave root domain fields such as `actions` out of scope and align the schema descriptions and processing rules. --- docs/specification/overview.md | 20 ++++++++++---------- source/schemas/ucp.json | 4 ++-- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/specification/overview.md b/docs/specification/overview.md index fd15894f1..0f0d4f15f 100644 --- a/docs/specification/overview.md +++ b/docs/specification/overview.md @@ -1039,21 +1039,21 @@ 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 eligible structured -scope, it lives inside that scope's `ucp` member. +`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 sibling map field `` and its companion array -`map_order.`: +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 sibling map `` in +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. @@ -1062,7 +1062,7 @@ For a sibling map field `` and its companion array 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 +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. Duplicate keys are invalid; consumers **SHOULD** honor the first diff --git a/source/schemas/ucp.json b/source/schemas/ucp.json index 0133c4a52..414617bc7 100644 --- a/source/schemas/ucp.json +++ b/source/schemas/ucp.json @@ -48,7 +48,7 @@ "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.", + "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.", "additionalProperties": { "type": "array", "items": { "type": "string" }, @@ -107,7 +107,7 @@ "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`).", + "description": "Preferred key-traversal order for sibling registry fields inside the root `ucp` envelope (`services`, `capabilities`, and `payment_handlers`).", "ucp_request": "omit" }, "status": { From 3d0ddd770767d2237502b49fc6e74d6f24ab6b8f Mon Sep 17 00:00:00 2001 From: Ilya Grigorik Date: Thu, 6 Aug 2026 14:53:48 -0700 Subject: [PATCH 09/10] recover from duplicate map_order entries `uniqueItems` allowed a repeated key in advisory ordering metadata to invalidate an entire profile or response, even though deterministic recovery is available. Allow repeated keys at the schema layer while prohibiting producers from emitting them. Require consumers to keep the containing document valid, honor the first occurrence, and ignore later repetitions. --- docs/specification/overview.md | 6 ++++-- source/schemas/ucp.json | 5 ++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/specification/overview.md b/docs/specification/overview.md index 0f0d4f15f..e35e5caf9 100644 --- a/docs/specification/overview.md +++ b/docs/specification/overview.md @@ -1065,8 +1065,10 @@ For a target map field `` and its companion array `map_order.`: 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. Duplicate keys are invalid; consumers **SHOULD** honor the first - occurrence of a key and ignore later duplicates. +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 diff --git a/source/schemas/ucp.json b/source/schemas/ucp.json index 414617bc7..71c190dce 100644 --- a/source/schemas/ucp.json +++ b/source/schemas/ucp.json @@ -48,11 +48,10 @@ "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.", + "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" }, - "uniqueItems": true + "items": { "type": "string" } } }, From 9db46ccbea43f3f81fa2c03bde320b3bfb13c860 Mon Sep 17 00:00:00 2001 From: Ilya Grigorik Date: Thu, 6 Aug 2026 22:22:02 -0700 Subject: [PATCH 10/10] move ambient validation to schema resolution Define UCP-aware schema resolution as the boundary that applies the central namespace vocabulary at eligible structured scopes and emits ordinary JSON Schema for standard validators and code generators. Keep the authoring guide focused on source-schema rules rather than repeating the resolution contract. --- docs/documentation/schema-authoring.md | 15 +++------------ docs/specification/overview.md | 22 +++++++++++++--------- 2 files changed, 16 insertions(+), 21 deletions(-) diff --git a/docs/documentation/schema-authoring.md b/docs/documentation/schema-authoring.md index 737d86dab..836dad08c 100644 --- a/docs/documentation/schema-authoring.md +++ b/docs/documentation/schema-authoring.md @@ -201,18 +201,9 @@ for the normative rules. For schema authors this means: 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. Instance validation treats an ambient `ucp` member as an - ignored unknown object; validating its contents is a conformance-tooling - concern. -- **Keep open schemas and strict tooling namespace-aware.** UCP schemas are - open by default, and ambient `ucp` is protocol metadata rather than a domain - field. Generic JSON tooling can parse the member; ordinary JSON Schema - validation accepts it as an undeclared property at an eligible structured - scope without applying UCP vocabulary rules. A UCP-aware strict validator - **MUST** allow the undeclared member and validate it against - `ucp.json#/$defs/members`, even when rejecting other unknown domain fields. - A non-UCP-aware strict validator may reject the member; this is an accepted - compatibility limit. + 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 diff --git a/docs/specification/overview.md b/docs/specification/overview.md index e35e5caf9..fc28243e7 100644 --- a/docs/specification/overview.md +++ b/docs/specification/overview.md @@ -1015,19 +1015,23 @@ 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 instance validation, -which treats ambient `ucp` members as ignored unknown objects. +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 schemas are open by default. Ordinary JSON Schema -validation accepts an undeclared ambient `ucp` at an eligible structured scope -without applying UCP vocabulary rules; UCP-aware tooling **SHOULD** recognize -and apply `ucp.json#/$defs/members`. A UCP-aware strict validator **MUST** -allow and validate the member, even when rejecting other unknown domain fields. -A non-UCP-aware strict validator may reject it; this is an accepted -compatibility limit. +**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`