diff --git a/docs/specification/overview.md b/docs/specification/overview.md index 08cebef9..4b2469d4 100644 --- a/docs/specification/overview.md +++ b/docs/specification/overview.md @@ -33,11 +33,18 @@ Schema notes: ## Discovery, Governance, and Negotiation -UCP employs a server-selects architecture where the business (server) chooses -the protocol version and capabilities from the intersection of both parties' -capabilities. Both business and platform profiles can be cached by both parties, -allowing efficient capability negotiation within the normal request/response -flow between platform and business. +UCP separates protocol version compatibility from capability negotiation. +The business's profile at `/.well-known/ucp` describes capabilities for +its current protocol version. Businesses that support older protocol +versions **SHOULD** publish version-specific profiles and advertise them +via the `supported_versions` field — a map from protocol version to +profile URI, enabling platforms to discover the exact capabilities +for a specific protocol version. Capability negotiation follows a +server-selects architecture where the business (server) determines +the active capabilities from the intersection of both parties' +declared capabilities. Both business and platform profiles can be +cached by both parties, allowing efficient capability negotiation +within the normal request/response flow between platform and business. ### Namespace Governance @@ -264,6 +271,35 @@ This convention ensures: - **Verifiable**: Build-time checks can confirm each `extends` entry has a matching `$defs` key +##### Protocol Version Constraint + +Extension schemas **SHOULD** declare a `min_protocol_version` field +(alongside `name`, `title`, `description`) to indicate the minimum +UCP protocol version required by the extension: + +```json +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://acme.com/ucp/schemas/loyalty.json", + "name": "com.acme.shopping.loyalty", + "title": "Acme Loyalty Points", + "min_protocol_version": "2026-01-23", + "$defs": { ... } +} +``` + +The schema author — not the profile publisher — declares the minimum +protocol version requirement. The profile publisher selects and +advertises compatible versions in their profile. + +If `min_protocol_version` is present, platforms and businesses +**SHOULD** verify the negotiated protocol version is >= +`min_protocol_version` during schema resolution. Incompatible +extensions are excluded from the active capability set (see +[Resolution Flow](#resolution-flow)). If absent, the extension is +assumed to be compatible with the protocol version declared by the +profile. + #### Schema Resolution Convention To validate payloads, implementations resolve extension schemas as follows: @@ -286,8 +322,13 @@ Platforms **MUST** resolve schemas following this sequence: 2. **Negotiation**: Compute capability intersection (see [Intersection Algorithm](#intersection-algorithm)) 3. **Schema Fetch**: Fetch base schema and all active extension schemas -4. **Compose**: Merge schemas via `allOf` chains based on active extensions -5. **Validate**: Validate requests and responses against the composed schema +4. **Protocol Compatibility**: For each fetched extension + schema, if `min_protocol_version` is present, verify the negotiated + protocol version >= that value. Exclude incompatible capabilities and + re-prune orphaned extensions (steps 3-4 of the + [Intersection Algorithm](#intersection-algorithm)) +5. **Compose**: Merge schemas via `allOf` chains based on active extensions +6. **Validate**: Validate requests and responses against the composed schema ### Profile Structure @@ -402,6 +443,11 @@ used to verify signatures on webhooks and other authenticated messages from the business. See [Key Discovery](#key-discovery) for key lookup and resolution, and [Message Signatures](signatures.md) for signing mechanics. +Businesses that support older protocol versions **SHOULD** include a +`supported_versions` object mapping each older version to a +version-specific profile URI. See [Protocol Version](#protocol-version) +for details. + #### Platform Profile Platform profiles are similar and include signing keys for capabilities @@ -574,17 +620,23 @@ for a session: 1. **Compute intersection**: For each business capability, include it in the result if a platform capability with the same `name` exists. -2. **Prune orphaned extensions**: Remove any capability where `extends` is +2. **Select version**: For each capability in the intersection, compute the + set of version strings present in **both** the business and platform + arrays. If the set is non-empty, select the **highest** version + (latest date). If the set is empty (no mutual version), **exclude** the + capability from the intersection. + +3. **Prune orphaned extensions**: Remove any capability where `extends` is set but **none** of its parent capabilities are in the intersection. - For single-parent extensions (`extends: "string"`): parent must be present - For multi-parent extensions (`extends: ["a", "b"]`): at least one parent must be present -3. **Repeat pruning**: Continue step 2 until no more capabilities are removed +4. **Repeat pruning**: Continue step 3 until no more capabilities are removed (handles transitive extension chains). -The result is the set of capabilities both parties support, with extension -dependencies satisfied. +The result is the set of capabilities both parties support at mutually +compatible versions, with extension dependencies satisfied. #### Error Handling @@ -598,8 +650,8 @@ UCP negotiation can fail in two ways: These failure types require different handling: -- **Discovery failure** → transport error with optional `continue_url` -- **Negotiation failure** → UCP response with optional `continue_url` +- **Discovery or version failure** → transport error with optional `continue_url` +- **Capability negotiation failure** → UCP response with optional `continue_url` ##### Error Codes @@ -610,8 +662,8 @@ These failure types require different handling: | `invalid_profile_url` | Profile URL is malformed, missing, or unresolvable | 400 | -32001 | | `profile_unreachable` | Resolved URL but fetch failed (timeout, non-2xx) | 424 | -32001 | | `profile_malformed` | Fetched content is not valid JSON or violates schema | 422 | -32001 | +| `version_unsupported` | Platform's protocol version not supported | 422 | -32001 | | `capabilities_incompatible` | No compatible capabilities in intersection | 200 | result | -| `version_unsupported` | Platform's UCP version is not supported | 200 | result | **Signature Errors:** @@ -670,7 +722,20 @@ task through the standard web interface. } ``` - **Negotiation Failure (200):** + **Version Unsupported (422):** + + ```http + HTTP/1.1 422 Unprocessable Content + Content-Type: application/json + + { + "code": "version_unsupported", + "content": "Protocol version 2026-01-12 is not supported. This business supports versions 2026-01-11 and 2026-01-23.", + "continue_url": "https://merchant.com/cart" + } + ``` + + **Capabilities Incompatible (200):** ```http HTTP/1.1 200 OK @@ -684,9 +749,9 @@ task through the standard web interface. "messages": [ { "type": "error", - "code": "version_unsupported", - "content": "UCP version 2024-01-01 is not supported", - "severity": "requires_buyer_input" + "code": "capabilities_incompatible", + "content": "No compatible capabilities found", + "severity": "recoverable" } ], "continue_url": "https://merchant.com" @@ -730,7 +795,25 @@ task through the standard web interface. } ``` - **Negotiation Failure (JSON-RPC result):** + **Version Unsupported (JSON-RPC error):** + + ```json + { + "jsonrpc": "2.0", + "id": 1, + "error": { + "code": -32001, + "message": "Protocol version not supported", + "data": { + "code": "version_unsupported", + "content": "Protocol version 2026-01-12 is not supported. This business supports versions 2026-01-11 and 2026-01-23.", + "continue_url": "https://merchant.com/cart" + } + } + } + ``` + + **Capabilities Incompatible (JSON-RPC result):** ```json { @@ -745,9 +828,9 @@ task through the standard web interface. "messages": [ { "type": "error", - "code": "version_unsupported", - "content": "UCP version 2024-01-01 is not supported", - "severity": "requires_buyer_input" + "code": "capabilities_incompatible", + "content": "No compatible capabilities found", + "severity": "recoverable" } ], "continue_url": "https://merchant.com" @@ -1642,16 +1725,74 @@ Both businesses and platforms declare a single version in their profiles: ![High-level resolution flow sequence diagram](site:specification/images/ucp-discovery-negotiation.png) -Businesses **MUST** validate the platform's version and determine compatibility: +Version compatibility operates at two levels: the **protocol version** +and **capability versions**. The protocol version (`ucp.version`) +governs core protocol mechanisms — discovery, negotiation flow, +transport bindings, and signature requirements. Capability versions +govern the semantics of each feature independently, as defined in +[Independent Component Versioning](#independent-component-versioning). + +#### Protocol Version + +The `version` field declares the business's current protocol version. +The profile at `/.well-known/ucp` describes the capabilities, services, +and payment handlers available at that version. + +Businesses that support older protocol versions **SHOULD** declare a +`supported_versions` object mapping each older version to a profile +URI. Each URI points to a complete, self-contained profile for that +version — including its own capabilities, services, payment handlers, +and signing keys. When `supported_versions` is omitted, only +`version` is supported. + +```json +{ + "ucp": { + "version": "2026-01-23", + "supported_versions": { + "2026-01-11": "https://business.example.com/.well-known/ucp/2026-01-11" + } + } +} +``` + +##### Initial Service and Capability Discovery + +Platforms discover a business's capabilities through the following flow: + +1. Platform fetches `/.well-known/ucp` — this is the current version + profile. +2. If the platform's protocol version matches `version`: use this + profile directly. Proceed to capability negotiation. +3. If the platform's protocol version is a key in + `supported_versions`: fetch the profile at the mapped URI. This + profile describes the capabilities available at that protocol + version. Proceed to capability negotiation. +4. Otherwise: the business does not support the platform's protocol + version. Platforms **SHOULD NOT** send requests with an incompatible + version; businesses **MUST** respond with a `version_unsupported` + error. + +Version-specific profiles are leaf documents — they describe exactly +one protocol version and **MUST NOT** contain a `supported_versions` +field. -1. Platform declares version via profile referenced in request +##### Request-Time Validation + +Businesses **MUST** validate the platform's protocol version on +every request: + +1. Platform declares the protocol version it uses via the + `version` field in the profile referenced in the request. 2. Business validates: - - If platform version ≤ business version: Business **MUST** - process the request - - If platform version > business version: Business **MUST** return - `version_unsupported` error -3. Businesses **MUST** include the version used for processing in every - response. + - If the platform's `version` matches the business's `version` + or is a key in `supported_versions`: the request **MAY** + proceed to capability negotiation using the matching + version of the business profile. + - Otherwise: Business **MUST** return a `version_unsupported` + error. +3. Businesses **MUST** include the negotiated protocol version in + every response. Response with version confirmation: @@ -1668,20 +1809,34 @@ Response with version confirmation: } ``` -Version unsupported error: +Version unsupported error (protocol-level, returned before capability +negotiation): + +```http +HTTP/1.1 422 Unprocessable Content +Content-Type: application/json -```json { - "status": "requires_escalation", - "messages": [{ - "type": "error", - "code": "version_unsupported", - "content": "Version 2026-01-12 is not supported. This business implements version 2026-01-11.", - "severity": "requires_buyer_input" - }] + "code": "version_unsupported", + "content": "Protocol version 2026-01-12 is not supported. This business supports versions 2026-01-11 and 2026-01-23.", + "continue_url": "https://merchant.com/cart" } ``` +#### Capability Versions + +Capability versions are negotiated independently of the protocol +version. Each capability in the profile is an array. Multiple entries +for the same capability, each with a different `version`, advertise +support for multiple versions of that capability. The capability +intersection algorithm considers only capability versions supported +by both parties. + +Businesses **MUST** include only capabilities compatible with the +negotiated protocol version in their response. A capability that +depends on features introduced in a newer protocol version **MUST +NOT** be included when processing at an older protocol version. + ### Backwards Compatibility #### Backwards-Compatible Changes diff --git a/source/schemas/ucp.json b/source/schemas/ucp.json index d6cffac8..97100933 100644 --- a/source/schemas/ucp.json +++ b/source/schemas/ucp.json @@ -53,7 +53,10 @@ "type": "object", "required": ["version"], "properties": { - "version": { "$ref": "#/$defs/version" }, + "version": { + "$ref": "#/$defs/version", + "description": "Protocol version this profile describes. The profile's capabilities, services, and payment handlers are defined for this version." + }, "services": { "type": "object", "description": "Service registry keyed by reverse-domain name.", @@ -122,6 +125,15 @@ { "required": ["services", "payment_handlers"], "properties": { + "supported_versions": { + "type": "object", + "description": "Previous protocol versions this business supports, mapped to profile URIs. Businesses that support older protocol versions SHOULD advertise each version and link to its profile. Each URI points to a complete, self-contained profile for that version. When omitted, only `version` is supported.", + "propertyNames": { "$ref": "#/$defs/version" }, + "additionalProperties": { + "type": "string", + "format": "uri" + } + }, "services": { "additionalProperties": { "items": { "$ref": "service.json#/$defs/business_schema" }