diff --git a/docs/documentation/core-concepts.md b/docs/documentation/core-concepts.md index 0e6f3e387..a8fbec62b 100644 --- a/docs/documentation/core-concepts.md +++ b/docs/documentation/core-concepts.md @@ -127,16 +127,18 @@ interact. ### Capabilities -Capabilities are standalone, independently versioned features that -a business declares it supports. They are the "verbs" of the protocol — -discrete units of functionality that platforms can discover, negotiate, and -invoke. +Capabilities are discrete, versioned features that a Business declares it +supports. They are the "verbs" of the protocol — units of functionality that +Platforms can discover, negotiate, and invoke. Each capability is identified by a reverse-domain name (e.g., `dev.ucp.shopping.checkout`) and carries a date-based version. Capabilities -are declared in the business's UCP profile at `/.well-known/ucp` and -negotiated and confirmed in every response so that the platform always knows -the active feature set for a given interaction. +are declared in the Business's UCP profile at `/.well-known/ucp`, negotiated +by exact version, and confirmed in every response so that the Platform always +knows the active feature set for a given interaction. UCP-authored `dev.ucp.*` +capabilities and extensions are versioned in lockstep with the specification: +each declares the date `D` of the UCP release it ships in. Third-party +extensions publish versions on their own cadence. The following are examples of capabilities defined in UCP — see the [Specification](../specification/overview.md) for the authoritative and @@ -191,9 +193,9 @@ up-to-date list. ### Services **Services** group the operations and events for a vertical under a -reverse-domain namespace (e.g., `dev.ucp.shopping`). -A service declares *what* functionality exists for that vertical; transport -bindings declare *how* it is accessed on the wire. +reverse-domain registry key (e.g., `dev.ucp.shopping`). The key identifies the +service: a service declares *what* functionality exists for that vertical, and +each entry in its array declares *how* it is accessed over a transport binding. A single service can be accessed via multiple transport bindings: @@ -204,12 +206,13 @@ A single service can be accessed via multiple transport bindings: | **A2A** | Agent Card | Agent-to-Agent protocol integrations | | **Embedded** | OpenRPC | Embedded integrations | -A business declares which transport bindings it supports within each service; -platforms pick whichever fits their context — an AI agent may prefer MCP, a -traditional web app may use REST. +A Business declares which transport bindings it supports within each service; +Platforms pick whichever fits their context — an AI agent may prefer MCP, a +traditional web app may use REST. Every UCP-defined service declares an explicit +`version` equal to the selected `ucp.version`, repeated on every entry since they +differ only by transport binding. Transport bindings have no separate version. -Service namespaces are also UCP's extensibility mechanism for new -verticals — e.g., `dev.ucp.hotels` may be introduced in the future. +Service namespaces are also UCP's extensibility mechanism for new verticals. Businesses opt in by declaring which services they support. ## Discovery & Capability Negotiation @@ -336,13 +339,13 @@ reject entities that fail it. Identifiers carry no fetched URL, and the `spec` normative algorithm. The `dev.ucp.*` namespace is reserved exclusively for capabilities governed by -the UCP Tech Council. Any vendor can define and publish capabilities under their -own domain — `org.acme.*` — without UCP maintainer approval. Vendor -capabilities follow the same extension model, meaning they can extend UCP base -capabilities (e.g., `org.acme.loyalty` extending `dev.ucp.shopping.checkout`) -or define entirely new ones. Because negotiation is always opt-in, vendor -capabilities only activate when both parties declare them, keeping the protocol -decentralized by design. +the UCP Tech Council responsible for the capability's domain. Any vendor can +define and publish capabilities under their own domain — `org.acme.*` — without +UCP maintainer approval. Vendor capabilities follow the same extension model, +meaning they can extend UCP base capabilities (e.g., `org.acme.loyalty` +extending `dev.ucp.shopping.checkout`) or define entirely new ones. Because +negotiation is always opt-in, vendor capabilities only activate when both parties +declare them, keeping the protocol decentralized by design. ## Payment Architecture @@ -438,20 +441,19 @@ Businesses publish their OAuth 2.0 server metadata at ## Versioning -UCP uses date-based version identifiers (`YYYY-MM-DD`). The version represents -the date of the last backwards-incompatible change. - -* **Non-breaking additions** do not increment the date. -* **Breaking changes** require a `!` prefix in the PR title and a 2-week - advance notice to the community before merging. -* Businesses that support older protocol versions **SHOULD** publish - version-specific profiles and advertise them via the `supported_versions` - field in their profile, enabling platforms to discover the exact capability - set for each supported version. - -Capability schemas carry their version inline, which enables independent -versioning — a discount extension can version on a different cadence than -the checkout capability it extends. - -See [Versioning](../versioning.md) for the release branch process and the -full breaking-change procedure. +UCP uses date-based version identifiers (`YYYY-MM-DD`). A UCP release `D` is a +snapshot of the core protocol — its services and transport bindings, +capabilities, extensions, and shared schemas — published together as one +internally compatible set. A profile's `ucp.version` selects that snapshot, and +the match is exact: an older date is available only when the Business +advertises it in `supported_versions`. A published snapshot can gain approved +backwards-compatible additions over time; copies fetched earlier remain valid. + +Every UCP-defined service, capability, and extension in release `D` declares +version `D`. Third-party extensions and payment handlers are versioned by their +authors, independently of UCP releases. + +See [Protocol Version](../specification/overview.md#protocol-version) for version +discovery, [Component Versioning and Release Snapshots](../specification/overview.md#component-versioning-and-release-snapshots) +for the normative release contract, and [Versioning](../versioning.md) for the +release-branch and backport process. diff --git a/docs/documentation/schema-authoring.md b/docs/documentation/schema-authoring.md index d12fc0ee7..2136a87c7 100644 --- a/docs/documentation/schema-authoring.md +++ b/docs/documentation/schema-authoring.md @@ -29,29 +29,35 @@ UCP schemas use standard JSON Schema fields plus UCP-specific metadata: | `$id` | JSON Schema | Schema's canonical URI for `$ref` resolution | All schemas | | `title` | JSON Schema | Human-readable display name | All schemas | | `description` | JSON Schema | Schema purpose and usage | All schemas | -| `name` | UCP | Reverse-domain identifier; doubles as registry key | Capabilities, services, handlers | -| `version` | UCP | Entity version (`YYYY-MM-DD` format) | Capabilities, services, payment handlers | +| `name` | UCP | Reverse-domain identifier; doubles as registry key | Capabilities, extensions, payment handlers | +| `version` | UCP | Entity version (`YYYY-MM-DD` format) | Capabilities, extensions, payment handlers | | `id` | UCP | Instance identifier for multiple configurations | Payment handlers only | +This table lists top-level metadata embedded in published schema documents; +profile registry declarations are covered in the sections below. + ### Why Self-Describing? -Capability schemas **must be self-describing**: when a platform fetches a schema, -it should determine exactly what capability and version it represents without -cross-referencing other documents. This matters because: +Capability and payment-handler schemas **must be self-describing**: when a +Platform fetches a schema, it should determine exactly what entity and version +it represents without cross-referencing other documents. This matters because: -1. **Independent versioning**: Capabilities may version independently. The schema - must declare its version explicitly—you can't infer it from the URL. +1. **Explicit version identity**: Capabilities, extensions, and payment handlers + declare their version explicitly; it cannot be inferred from the URL. + UCP-authored `dev.ucp.*` capabilities and extensions declare version `D` in + each UCP release `D`. Payment handlers publish on their authors' own schedules. 2. **Validation**: Validators can cross-check that a capability declaration's `schema` URL points to a schema whose embedded `name`/`version` match the declaration. Mismatches are authoring errors caught at build time. 3. **Developer experience**: When reading a schema file, integrators immediately - see what capability it defines without reverse-engineering the `$id` URL. + see what entity it defines without reverse-engineering the `$id` URL. 4. **Compact namespace**: The `name` field provides a standardized reverse-domain identifier (e.g., `dev.ucp.shopping.checkout`) that's more compact and semantic - than the full `$id` URL. + than the full `$id` URL. Service registry keys provide the same stable + identity. ### Why Both `$id` and `name`? @@ -77,9 +83,10 @@ breaking capability negotiation. The `version` field uses date-based versioning (`YYYY-MM-DD`) to enable: - **Capability negotiation**: Platforms request specific versions they support -- **Breaking change management**: New versions get new dates; old versions remain - valid and resolvable -- **Independent lifecycles**: Extensions can release on their own schedule +- **Certified release identity**: Each date identifies the entity's complete, + published schema closure +- **Independent author lifecycles**: Third-party extensions and payment handlers + can release on their authors' own schedules ## Schema Categories @@ -99,7 +106,14 @@ Examples: `checkout.json`, `fulfillment.json`, `discount.json`, `order.json` Define transport bindings that appear in `ucp.services{}` registries. Each transport (REST, MCP, A2A, Embedded) is a separate entry. -- **Top-level fields**: `$schema`, `$id`, `title`, `description`, `name`, `version` +- **Registry identity**: Reverse-domain service name used as the registry key +- **Entry fields**: `version`, `spec`, `schema`, `config`, and transport-specific fields +- **Versioning**: Every service entry declares an explicit `version`; in release + `D` it is `D`. Because each entry pairs the service with one transport binding, + that service `version` repeats on each entry, and transport bindings have no + separate version. The referenced OpenAPI/OpenRPC artifact carries its own + `info.version` as artifact metadata, not a negotiated version. See + [Component Versioning and Release Snapshots](../specification/overview.md#component-versioning-and-release-snapshots) - **Variants**: `platform_schema`, `business_schema` - **Transport requirements** (additional beyond the common base): - Platform profile (`platform_schema`): REST/MCP/Embedded require `schema` (OpenAPI/OpenRPC URL). A2A has no additional requirements. @@ -178,11 +192,11 @@ by `name` rather than arrays of objects with `name` fields. The same registry structure appears in three contexts with different field requirements: -| Context | Location | Required Fields | -| ------- | -------- | --------------- | -| Platform Profile | Advertised URI | `version`, `spec`, `schema` | -| Business Profile | `/.well-known/ucp` | `version`; may add `config` | -| API Responses | Checkout/order payloads | `version` (+ `id` for handlers) | +| Registry | Platform Profile | Business Profile | API Responses | +| -------- | ---------------- | ---------------- | ------------- | +| Services | `version`, `transport`, `spec`; `schema` for REST, MCP, and Embedded | `version`, `transport`; `endpoint` for REST, MCP, and A2A | `version`, `transport`; transport-specific `config` where applicable | +| Capabilities/extensions | `version`, `spec`, `schema` | `version`, `schema`; may add `config` | `version` | +| Payment handlers | `id`, `version`, `spec`, `schema` | `id`, `version`; may add `config` | `id`, `version` | ## The Entity Pattern @@ -288,14 +302,30 @@ values documented in the `description`. ## Versioning Strategy +### UCP Services (`dev.ucp.*`) + +In each UCP release `D`, every UCP-defined service entry declares version `D`. +See [Service Schemas](#service-schemas) for how the flattened registry represents +service versions and transport bindings. + ### UCP Capabilities (`dev.ucp.*`) -UCP-authored capabilities version with protocol releases by default. Individual -capabilities **may** version independently when needed. +In each UCP release `D`, every UCP-defined capability and extension declares +version `D`, even when its schema did not change directly. Declaring version `D` +does not replace negotiation: capabilities and extensions are still selected by +exact-version intersection. + +Profile selection, including profiles for older supported releases, is defined +in [Protocol Version](../specification/overview.md#protocol-version). + +### Third-Party Extensions and Payment Handlers -### Vendor Capabilities (`com.{vendor}.*`) +Third-party extensions and payment handlers publish versions on their authors' +own schedules. Their versions remain independent of the selected `ucp.version` +and are not constrained to `D`. UCP's `payment_handler.json` defines the shared +declaration shape, not a payment-handler implementation or its release cadence. -Capabilities outside `dev.ucp.*` version fully independently: +Third-party extensions version independently: ```json @@ -307,7 +337,9 @@ Capabilities outside `dev.ucp.*` version fully independently: } ``` -Vendor schemas follow the same self-describing requirements. +Extension version requirements belong in the extension schema document: +`requires.protocol` constrains the selected `ucp.version`, and +`requires.capabilities` constrains selected parent versions. ## Extensibility and Forward Compatibility diff --git a/docs/specification/overview.md b/docs/specification/overview.md index 0793cea17..dd3e17a5c 100644 --- a/docs/specification/overview.md +++ b/docs/specification/overview.md @@ -223,21 +223,14 @@ requires. ## Discovery, Governance, and Negotiation -UCP separates protocol version compatibility from capability negotiation. -The business's profile at `/.well-known/ucp` describes capabilities for -the protocol version it declares. 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. Version lifecycle, including when to deprecate -or remove older versions from `supported_versions`, is a business policy -decision. The protocol does not prescribe a deprecation schedule. -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. +UCP separates [protocol version selection](#protocol-version) from +[capability negotiation](#capability-versions). A Business advertises its current +protocol version and links to profiles for older supported versions. After the +Platform selects one exact version, the Business determines the active +capabilities from the versions both parties advertise. Version lifecycle, +including when to remove an older version, is a Business policy decision; UCP +does not prescribe a deprecation schedule. Business and Platform profiles can be +cached by both parties. ### Namespace Governance @@ -414,6 +407,15 @@ standard formats: - **A2A**: Agent Card Specification - **EP(embedded)**: OpenRPC (JSON format) +A service is identified by its reverse-domain registry key (e.g., +`dev.ucp.shopping`). In a profile, services are keyed by that name, and each +entry in `services[name][]` pairs the service with one transport binding and +declares the service `version`: in release `D` that version is `D`. This is the +service version, not a transport version — the binding has no separate version. +The OpenAPI or OpenRPC artifact a binding references carries its own +`info.version` as release metadata, not a separate version to negotiate. See +[Component Versioning and Release Snapshots](#component-versioning-and-release-snapshots). + #### Service Definition {{ extension_schema_fields('service.json#/$defs/platform_schema', 'overview') }} @@ -568,9 +570,14 @@ This convention ensures: ##### Version Requirements -Extension schemas **SHOULD** declare a `requires` object (alongside -`name`, `title`, `description`) to indicate the protocol and -capability versions required for correct operation: +Extension authors **SHOULD** declare a `requires` object in the extension +schema (alongside its `name`, `title`, and `description`) stating the versions +the extension depends on. A third-party extension schema declares its own +author-controlled `version`, which advances independently of `ucp.version`; +a UCP-authored `dev.ucp.*` extension declares version `D` in release `D`. +`requires.protocol` constrains the selected `ucp.version`, and +`requires.capabilities` constrains the selected versions of the named +capabilities: ```json @@ -578,6 +585,7 @@ capability versions required for correct operation: "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://acme.com/ucp/schemas/loyalty.json", "name": "com.acme.shopping.loyalty", + "version": "2026-06-15", "title": "Acme Loyalty Points", "requires": { "protocol": { "min": "2026-01-23" }, @@ -610,13 +618,16 @@ no upper bound: ``` Keys in `requires.capabilities` **MUST** be a subset of the -extension's `$defs` keys. If `requires` is present, platforms and -businesses **MUST** verify the negotiated protocol version and -capability versions satisfy the declared constraints during schema -resolution. Incompatible extensions are excluded from the active -capability set (see [Resolution Flow](#resolution-flow)). If -`requires` is absent, the extension is assumed to be compatible -with the versions declared by the profile. +extension's `$defs` keys. These ranges verify dependencies after exact +versions are selected; they do not select versions. The `ucp.version` is +fixed first by [profile selection](#protocol-version) and capability versions +by the [intersection algorithm](#intersection-algorithm); then, if `requires` +is present, Platforms and Businesses **MUST** verify that the selected +`ucp.version` and capability versions satisfy the declared constraints during +schema resolution. Incompatible extensions are excluded from the active +capability set (see [Resolution Flow](#resolution-flow)). If `requires` is +absent, the extension is assumed to be compatible with the versions declared +by the profile. #### Schema Resolution Convention @@ -1458,14 +1469,17 @@ and the Web Bot Auth interop signature shape, see #### Hosting -Both profiles must be reliably hosted. An unreliable or misconfigured -profile endpoint may prevent the other party from processing requests. +Profiles, and the schema and transport-description artifacts they reference, +must be reliably hosted. An unreliable or misconfigured endpoint may prevent +the other party from processing requests. -1. Profiles **MUST** be served over HTTPS. +1. Published artifacts **MUST** be served over HTTPS. 2. Profile endpoints **MUST NOT** use redirects (3xx). -3. Profile responses **MUST** include a `Cache-Control` header with - `public` and `max-age` of at least 60 seconds. Profiles **MUST NOT** - be served with `private`, `no-store`, or `no-cache` directives. +3. Published artifacts **MUST** include a `Cache-Control` header with + `public` and `max-age` of at least 60 seconds, and **MUST NOT** be + served with `private`, `no-store`, or `no-cache` directives. +4. Published artifacts **SHOULD** include a validator (`ETag` or + `Last-Modified`) so consumers can revalidate cached copies efficiently. Profiles represent a party's stable identity and capabilities. Profile URLs are expected to remain consistent across requests and not contain @@ -2635,12 +2649,13 @@ implementation guide, and examples. UCP uses date-based versioning in the format `YYYY-MM-DD`. This provides clear chronological ordering and unambiguous version comparison. -### Version Discovery and Negotiation - -UCP prioritizes strong backwards compatibility. Businesses implementing a -version **SHOULD** handle requests from platforms using that version or older. +### Versioned Profiles -Both businesses and platforms declare a single version in their profiles: +UCP prioritizes strong backwards compatibility. Each profile document declares +one core release version in `ucp.version`; its services, capabilities, +extensions, and payment handlers also carry explicit versions. A Business +advertises its current profile and links to profiles for older supported releases +through `supported_versions`; see [Protocol Version](#protocol-version). #### Example @@ -2672,16 +2687,15 @@ Both businesses and platforms declare a single version in their profiles: } ``` -### Version Negotiation +### Version Selection ![High-level resolution flow sequence diagram](site:specification/images/ucp-discovery-negotiation.png) -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 selection chooses one complete Business profile first. +Capability negotiation then selects active capabilities and extensions by exact +version intersection within that profile. The sections below define each step; +see [Component Versioning and Release Snapshots](#component-versioning-and-release-snapshots) +for release alignment and version ownership. #### Protocol Version @@ -2692,9 +2706,9 @@ 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. +version — whose `ucp.version` equals its map key and which includes +its own capabilities, services, payment handlers, and signing keys. +When `supported_versions` is omitted, only `version` is supported. ```json @@ -2714,23 +2728,35 @@ and signing keys. When `supported_versions` is omitted, only Platforms discover a business's capabilities through the following flow: -1. Platform fetches `/.well-known/ucp` — this is the current version +1. The 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. +2. The Platform selects a mutually supported protocol version from the + Business's `version` and `supported_versions` keys; Platforms **SHOULD** + prefer the most recent. If the selected version matches `version`, the + Platform uses this profile directly and proceeds to capability + negotiation. +3. If the selected version is a key in + `supported_versions`: fetch the profile at the mapped URI. The + Platform **MUST** verify that the fetched profile's `ucp.version` + equals the `supported_versions` key it selected; on mismatch the + Platform **MUST NOT** use that profile. Otherwise this profile + describes the capabilities available at that protocol version — + proceed to capability negotiation. +4. If no mutually supported version exists, 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. +A selected profile guarantees compatibility among its declared capabilities at +its declared version; across versions, resource representations may differ. +Platforms **MAY** run separate negotiations with the same Business at different +supported versions, and **SHOULD NOT** combine or intersect capabilities across +profiles within the same negotiation. + ##### Request-Time Validation Businesses **MUST** validate the platform's protocol version on @@ -2800,12 +2826,22 @@ notice. #### 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. +Capability compatibility is established only by exact version equality, not by +inferring compatibility from date order. Each capability in the profile is an +array; multiple entries with different `version` values advertise support for +multiple versions where the applicable publication policy permits it. When the +exact shared set contains more than one version, the intersection algorithm +orders those shared dates to select the latest one. Supported third-party +extension versions advance independently of `ucp.version`. UCP-authored +`dev.ucp.*` entries declare version `D` in release `D`: a profile for +`ucp.version = D` advertises version `D` for those entries. Older `dev.ucp.*` +versions are advertised only through `supported_versions` leaf profiles, never +as additional entries in a newer profile: the registry mechanism supports +multi-version arrays, but the core publication contract does not exercise them +for `dev.ucp.*` entries. Multi-version arrays therefore arise for third-party +extensions whose authors support multiple versions concurrently. 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 @@ -2814,9 +2850,18 @@ NOT** be included when processing at an older protocol version. ### Backwards Compatibility +UCP classifies changes as backwards-compatible or breaking and applies this +classification to its own components to govern how a UCP release advances. +Publication and backport policy for both classes is defined in +[Component Versioning and Release Snapshots](#component-versioning-and-release-snapshots). +The lists below describe which changes preserve and which break conforming +integrations; third-party extension and payment-handler authors control their own +version policy and can apply the same distinction on their own cadence. + #### Backwards-Compatible Changes -The following changes **MAY** be introduced without a new version: +The following changes are **backwards-compatible**: they do not break conforming +integrations. - Adding new non-required fields to responses - Adding new non-required parameters to requests @@ -2828,7 +2873,8 @@ The following changes **MAY** be introduced without a new version: #### Breaking Changes -The following changes **MUST NOT** be introduced without a new version: +The following changes are **breaking**: they break conforming integrations and +require a new component version. - Removing or renaming existing fields - Changing field types or semantics @@ -2838,26 +2884,82 @@ The following changes **MUST NOT** be introduced without a new version: - Modifying existing protocol flow or state machine - Changing the meaning of existing error codes -### Independent Component Versioning - -- UCP protocol versions independently from capabilities. -- Each capability versions independently from other capabilities. -- Capabilities **MUST** follow the same backwards compatibility rules as the - protocol. -- Businesses **MUST** validate capability version compatibility using the same - logic as what's described above. -- Transports **MAY** define their own version handling mechanisms. - -#### UCP Capabilities (`dev.ucp.*`) - -UCP-authored capabilities version with protocol releases by default. Individual -capabilities **MAY** version independently when breaking changes are required -outside the protocol release cycle. - -#### Vendor Capabilities (`com.{vendor}.*`) - -Capabilities outside the `dev.ucp.*` namespace version fully independently. -Vendors control their own release schedules and versioning strategy. +### Component Versioning and Release Snapshots + +A UCP release `D` is a snapshot of the core protocol — its services and transport +bindings, capabilities, extensions, and shared schemas — published and certified +together as internally compatible. Selecting `ucp.version` `D` selects that +snapshot. For a release `D`: + +1. `ucp.version` is `D`. +2. Every UCP-defined service declares `version` `D`. Each service entry pairs the + service with one transport binding, so every entry under a service repeats + that service `version`; transport bindings have no separate version. The + referenced OpenAPI/OpenRPC artifacts are published under `D` as release + metadata. +3. Every UCP-defined capability and extension declares `version` `D`, even when + its own schema did not change directly. +4. Shared schemas are published as part of the same snapshot. +5. UCP certifies the snapshot — components, bindings, and supported + compositions — together before publishing it. + +UCP **MAY** backport an approved backward-compatible change — a feature, or a +security, correctness, or interoperability fix — to a supported release and its +generated `D` artifacts. Breaking changes enter the next release and are not +backported by default; in exceptional cases, the Governance Council **MAY** +approve backporting a breaking change to a supported release, limited to +defects that compromise the security, correctness, or interoperability of the +release as published, and following the breaking-change notice process. UCP +**MUST** re-certify the snapshot before publishing any updated artifacts. + +A dated release works like a long-term-support channel: the date names a +compatibility line, and backported changes amend the published `D` snapshot in +place — `D` stays the same while its contents change, so parties that fetched +at different times can hold different copies of the same `D`. This skew is +safe by construction: backports default to the backwards-compatible class, so +an earlier copy is missing later additions, never in conflict with them. +Parties that fetch artifacts at runtime **SHOULD** follow standard HTTP +caching semantics, revalidating a cached copy once its `max-age` expires (see +[Hosting](#hosting)); refresh frequency is therefore controlled by the +publisher. Parties that consume artifacts at build time pick up amendments on +their own release cadence. An exceptional breaking backport +is announced through the breaking-change notice process and enforced at +request time by the amended contract; it invalidates earlier copies only +because the release was defective as published. + +A Business or Platform that selects `ucp.version` `D` **MUST** declare version +`D` on every `dev.ucp.*` service, capability, and extension entry in its +profile. A Platform that encounters a `dev.ucp.*` entry whose `version` +differs from the profile's `ucp.version` **MUST** reject that entry — treated +as not present and never activated — and **MAY** continue with the remaining +entries. An older release is selected only through a separate +`supported_versions` leaf profile whose own `ucp.version` is that older release +date (see [Protocol Version](#protocol-version)). Payment-handler versions are +controlled by their authors: UCP defines the shared declaration structure but +currently no concrete handler, so no handler version is constrained to `D`. +Declaring `version` `D` does not change negotiation: `dev.ucp.*` +capabilities and extensions are still selected by exact-version intersection (see +[Capability Versions](#capability-versions)). + +#### Third-party extensions (`com.{vendor}.*`, `org.{org}.*`) + +Third-party capabilities version independently of `ucp.version`: their authors +control both the version and the release cadence. The common form is an +**extension** over one or more UCP-defined root capabilities. A third-party +extension: + +- declares `extends` over one or more UCP-defined root capabilities, composed + via `allOf` (see [Extension Schema Pattern](#extension-schema-pattern)); +- uses the UCP-defined services and transport bindings selected by + `ucp.version`; and +- advertises exact extension versions in Business and Platform profiles and is + negotiated by exact-version intersection like every capability. + +A third-party extension's version is never tied to `ucp.version`. The author +declares the UCP releases and capability versions the extension needs through +`requires` (see [Version Requirements](#version-requirements)); those ranges +verify compatibility with the versions a profile selects, they do not select +versions. ## Glossary diff --git a/docs/versioning.md b/docs/versioning.md index 5366b5017..14a78e5f3 100644 --- a/docs/versioning.md +++ b/docs/versioning.md @@ -1,30 +1,43 @@ # Versioning -UCP uses date-based version identifiers following the format `YYYY-MM-DD` to -indicate the last date backwards incompatible changes were made. +This guide defines UCP's release-branch and backport process. UCP uses date-based +version identifiers in `YYYY-MM-DD` format; see +[Component Versioning and Release Snapshots](specification/overview.md#component-versioning-and-release-snapshots) +for the normative release model and +[Protocol Version](specification/overview.md#protocol-version) for profile +selection. -New development occurs on the `main` branch. We will maintain long-lived -branches for all supported releases of the spec. +New development occurs on the `main` branch. We maintain long-lived branches for +all supported releases of the spec so that a published snapshot `D` stays +available for reference and maintenance. Backport eligibility and approval — +approved backwards-compatible changes by default, breaking changes only through +exceptional Governance Council approval — are defined in +[Component Versioning and Release Snapshots](specification/overview.md#component-versioning-and-release-snapshots). +A backported change lands on the maintained `release/D` branch, and UCP +re-certifies the snapshot before publishing the updated artifacts. * When the Tech Council approves a new version of UCP, we will cut a new branch named `release/YYYY-MM-DD` directly from the current state of `main`. * We will implement a code freeze on the release branch the moment a - `release/YYYY-MM-DD` branch is cut. Only critical bug fixes should move - during this window. - * Critical issues discovered after cutting a release branch should be fixed + `release/YYYY-MM-DD` branch is cut. Only changes permitted by the backport + policy should move during this window. + * Approved backward-compatible changes discovered after cutting a release branch should be made in one of two ways: - 1. The fix is made on the release branch and merged to `main`. - 2. The fix is made on `main` and cherry-picked to the release branch. + 1. The change is made on the release branch and merged to `main`. + 2. The change is made on `main` and cherry-picked to the release branch. * Once finalized, we will merge the release branch into `main` and tag it (e.g., `git tag -a vYYYY-MM-DD`). We will use a GitHub Action to detect the new tag and automatically generate a release notes draft and upload artifacts. -* Unlike temporary feature branches, release/YYYY-MM-DD branches are long-lived - and correspond to specific versions of the spec for historical reference and - maintenance. +* Unlike temporary feature branches, `release/YYYY-MM-DD` branches are + long-lived and correspond to specific versions of the spec for historical + reference and maintenance. ## Breaking PRs * Breaking changes should include `!` in the PR title * Timing: We will announce the breaking change in Discussions 2 weeks before the change is merged. +* Security-sensitive fixes are the exception: the Governance Council sets + the disclosure timeline case by case, so a fix is not announced before it + is safe to disclose. diff --git a/hooks.py b/hooks.py index 012d71d98..c52356a2f 100644 --- a/hooks.py +++ b/hooks.py @@ -123,15 +123,18 @@ def _rewrite_version_urls(data, url_version): def _set_schema_version(data, version): - """Set version field for named entities (capabilities, services, handlers). + """Set versions for versioned entities and transport artifacts. - Named entities (schemas with top-level 'name' field) require version per - ucp.json#/$defs/entity. Build injects version so source files don't need it. + UCP-authored capability and extension schemas require version per + ucp.json#/$defs/entity. Build injects the release version only for dev.ucp.* + schemas published in the core release. Third-party extensions and payment + handlers retain their author-controlled versions; the UCP payment-handler + meta-schema has no name and defines only the shared declaration structure. - Additionally, for OpenAPI and OpenRPC transport specifications, set the - required info.version field. + For OpenAPI and OpenRPC transport specifications, set the required + info.version field as release artifact metadata. """ - if "name" in data: + if str(data.get("name", "")).startswith("dev.ucp."): data["version"] = version if ("openapi" in data or "openrpc" in data) and isinstance( @@ -430,7 +433,7 @@ def on_post_build(config): # Step 1: Resolve relative $ref to absolute URLs _process_refs(data, src_file.parent) - # Step 2: Inject version field for named entities + # Step 2: Inject versions for versioned entities and transport artifacts if schema_version: _set_schema_version(data, schema_version) diff --git a/source/schemas/service.json b/source/schemas/service.json index 88f645698..880875145 100644 --- a/source/schemas/service.json +++ b/source/schemas/service.json @@ -2,7 +2,7 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://ucp.dev/schemas/service.json", "title": "UCP Service", - "description": "Service binding for a specific transport. Each transport binding is a separate entry in the service array.", + "description": "Service declaration with one transport binding. Each transport binding is a separate entry in the service array; `version` identifies the service, not the transport.", "$defs": { "base": { diff --git a/source/schemas/ucp.json b/source/schemas/ucp.json index 79716bee7..fdf11e72c 100644 --- a/source/schemas/ucp.json +++ b/source/schemas/ucp.json @@ -8,7 +8,7 @@ "version": { "type": "string", "pattern": "^\\d{4}-\\d{2}-\\d{2}$", - "description": "UCP version in YYYY-MM-DD format." + "description": "Version identifier in YYYY-MM-DD format." }, "version_constraint": { @@ -34,7 +34,7 @@ "properties": { "protocol": { "$ref": "#/$defs/version_constraint", - "description": "Required protocol version." + "description": "Required range for the selected `ucp.version`." }, "capabilities": { "type": "object",