Skip to content

fix!: repair profile schemas to pass validation & endpoint contract#429

Merged
igrigorik merged 6 commits into
mainfrom
fix/profile-schema
May 12, 2026
Merged

fix!: repair profile schemas to pass validation & endpoint contract#429
igrigorik merged 6 commits into
mainfrom
fix/profile-schema

Conversation

@igrigorik

Copy link
Copy Markdown
Contributor

Our schema validation and lint infrastructure exercises the services / payload surface — request roundtrips, codegen, fixture validation. The profile / discovery schemas and the MCP openrpc meta block have no equivalent validation pass:

  • The prose examples in docs/specification/ are not validated against their corresponding schemas.
  • No fixture roundtrips a real profile document through the top-level discovery schema.
  • The openrpc meta block is not cross-checked against the canonical Message Signatures spec.

As a result, a few schema issues snuck in undetected. Worse, our published profiles schemas are effectively broken / can't be validated due to broken oneOf and misplaced requires. See individual commits for specifics

  • c5fa47c: service.json platform_schema: drop over-strict endpoint requirement
  • 0100ed4: profile_schema.json: top-level oneOfanyOf (was unsatisfiable)
  • 78ea9b3: mcp.openrpc.json: remove stale RFC 7515 signature field

Technically, both oneOf and requires are breaking changes to the profile schemas, but they are un-breaking the current broken state, and so I propose we land and backport this to both 04-08 and previous releases.


Checklist

  • Core Protocol: Changes to the base communication layer, global context, or breaking refactors.
  • UCP Schema: Changes to the ucp-schema tool (resolver, linter, validator). (Requires Maintainer approval)
  • I have followed the Contributing Guide.
  • I have updated the documentation (if applicable).
  • My changes pass all local linting and formatting checks.

igrigorik added 3 commits May 10, 2026 12:51
  The platform_schema in service.json required `endpoint` for rest/mcp/a2a
  transports. This was structurally wrong: the protocol has no consumer that
  reads service-level endpoint on the platform side.

  - Negotiation algorithm (overview.md §"Capability Negotiation") reads
    version + extends from the platform profile, not endpoint.
  - Key discovery (overview.md §"Key Discovery") is verification-from-headers
    via UCP-Agent profile URL, not callable RPC against the platform.
  - Webhook delivery (business → platform) is modeled at the capability
    layer via `config.webhook_url` (see order.json#/platform_schema and
    order.md §"Order Event Webhook"), not at the service layer.

  The spec's own Platform Profile example (overview.md §"Platform Profile")
  omits endpoint and consequently fails to validate against this branch of
  its own schema. With this fix the canonical example validates.
  The top-level discriminator in profile_schema.json was `oneOf`, which
  requires data to match exactly one branch. The two branches (platform_profile,
  business_profile) have no required-vs-forbidden split and no type marker,
  so any document fully describing itself (e.g., a business profile that
  includes spec + schema URLs alongside endpoint) satisfies both branches
  and gets rejected with "must match exactly one schema in oneOf."

  Reproduction with ajv + JSON Schema 2020-12:

    Fixture                                | oneOf  | anyOf
    ---------------------------------------|--------|------
    Platform agent (spec+schema)           | PASS   | PASS
    Hosted-platform (spec+schema+endpoint) | FAIL   | PASS
    Business (endpoint only)               | PASS   | PASS
    Well-described business (all fields)   | FAIL   | PASS

  The second and fourth fixtures are the bug — both represent legitimate
  profiles that fail validation.

  Switching to `anyOf` matches what the data model actually expresses
  ("a profile is platform-shaped, business-shaped, or both"). Consumers
  that want self-describing parse can $ref the specific branch they expect
  based on where they dereferenced (UCP-Agent header → platform_profile;
  /.well-known/ucp → business_profile).
  The shopping MCP openrpc doc declared a `signature` field in the request
  meta object referencing RFC 7515 detached JWS. This contradicts the
  Message Signatures specification (docs/specification/signatures.md),
  which is unambiguous: UCP wire signing uses RFC 9421 (HTTP Message
  Signatures) + RFC 9530 (Content-Digest), carried in HTTP headers, not
  in the payload.

  The field was a leftover from an earlier signing-scheme exploration.
  RFC 7515 detached JWS still has one legitimate use in UCP — AP2 mandate
  signing — but that's payload-level by design and unrelated to request
  signing. It does not belong on MCP openrpc meta.
@igrigorik igrigorik added this to the Working Draft milestone May 10, 2026
@igrigorik igrigorik self-assigned this May 10, 2026
@igrigorik igrigorik requested review from a team as code owners May 10, 2026 22:42
@igrigorik igrigorik added the TC review Ready for TC review label May 10, 2026
Comment thread source/schemas/service.json Outdated

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"description": "Full service declaration for platform-level discovery. All
transports require `version`, `spec`, and `transport`. REST, MCP, and embedded
additionally require `schema`."

@amithanda

Copy link
Copy Markdown
Contributor

Thanks for catching this @igrigorik. I was wondering if there is a way for us to catch
these types of issues proactively in future via our existing ucp-schema based CI checks.

All three bugs in this PR lived in the same uncovered space: schema logic for
discovery and profile documents. ucp-schema is already the right tool and already
wired into CI — the lint job in docs.yml runs ucp-schema lint source/ on every
PR touching source/**. But lint checks schema files for syntax, broken $ref
chains, and annotation errors only; it does not exercise schema logic. ucp-schema validate
covers the service payload surface (checkout, cart, order) via
--op create|update|read — profile documents have no operation concept and are not
covered.

The result: ucp-schema lint source/ passes profile_schema.json and service.json
cleanly both before and after this PR, with no signal that either had a bug. I verified
this locally by applying the PR diff, running lint against both states, and getting
identical clean output each time.


Suggestion: close the discovery/profile validation blind spot in ucp-schema and CI

Recommended approach — Embed examples in schemas, extend ucp-schema lint to validate them

JSON Schema 2020-12 includes a standard examples keyword (meta-data annotation
vocabulary), and this repo already uses it — error_code.json, warning_code.json,
and card_credential.json all carry examples arrays today. However, examples is
annotation-only by spec: entries are never validated against the declaring schema
automatically. I confirmed this by injecting an integer into a type: string schema's
examples array — ucp-schema lint passes it cleanly with no error.

The recommendation is therefore to extend ucp-schema lint to add what the spec does
not: explicitly validate each entry in an examples array against the schema or
$defs sub-schema that declares it. Once that capability exists, adding examples to
profile_schema.json and service.json turns the existing lint CI step into a live
correctness check — no new files, no new CI steps.

For profile_schema.json, examples would be added at the top level and within each
$defs branch:

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://ucp.dev/schemas/discovery/profile.json",
  "anyOf": [
    { "$ref": "#/$defs/platform_profile" },
    { "$ref": "#/$defs/business_profile" }
  ],

  "examples": [
    {
      "ucp": {
        "version": "2026-01-23",
        "services": {
          "dev.ucp.shopping": [{
            "version": "2026-01-23",
            "transport": "rest",
            "schema": "https://ucp.dev/2026-01-23/services/shopping/rest.openapi.json"
          }]
        },
        "capabilities": {
          "dev.ucp.shopping.checkout": [{ "version": "2026-01-23" }]
        },
        "payment_handlers": {}
      },
      "signing_keys": [{ "kid": "key-1", "kty": "EC", "crv": "P-256" }]
    }
  ],

  "$defs": {
    "platform_profile": {
      "examples": [
        { "ucp": { "version": "2026-01-23", "services": { "...": "..." }, "payment_handlers": {} } },
        { "ucp": { "...": "..." }, "signing_keys": [{ "kid": "key-1", "kty": "EC" }] }
      ]
    },
    "business_profile": {
      "examples": [
        { "ucp": { "version": "2026-01-23", "services": { "...": "..." }, "payment_handlers": {} } }
      ]
    }
  }
}

The top-level example with signing_keys and capabilities populated is the exact
document that exposed this PR's oneOf bug — it satisfies both platform_profile and
business_profile branches simultaneously, which oneOf rejects and anyOf accepts.
Had lint been validating this example when oneOf was introduced, CI would have failed
on that PR rather than this one.

What this requires from ucp-schema (two additions to lint):

  1. Validate examples during lint — when processing a schema file, validate each
    entry in examples arrays (both top-level and within $defs) against the schema or
    sub-schema that declares it. Report failures as errors, not warnings.

  2. Require examples on discovery/meta schemas — add a lint rule that flags
    profile_schema.json, service.json, capability.json, and payment_handler.json
    as errors if they carry no examples. This makes it structurally impossible to ship
    a schema change without coverage: you either update an existing example that breaks,
    or lint blocks the PR.

What this requires in this repo:

Add examples entries to source/discovery/profile_schema.json,
source/schemas/service.json, and their $defs branches. The existing
ucp-schema lint source/ CI step picks up coverage automatically once the tool
validates them — no new files, no new CI configuration.

Why this is more failure-proof than the two other alternatives that I considered:

External fixture files (tests/fixtures/profile/) and annotated spec examples
(<!-- ucp-schema: validate-against ... --> in docs/specification/*.md) both work
but share the same structural weakness: the tests live in a different file from the
schema. A contributor opening profile_schema.json to make a change has no
in-their-face signal that something elsewhere needs updating. The coupling is social,
not structural — the same conditions that let these bugs through.

With embedded examples the tests and schema are in the same file. You cannot miss
them, and a lint rule that requires examples on discovery schemas means you cannot
ship a change without coverage.

What do you think?

Comment thread source/schemas/service.json
@ptiper

ptiper commented May 11, 2026

Copy link
Copy Markdown
Contributor

Thanks Ilya. @amithanda, agree we need CI to catch this. I opened a new issue in the .github repo to track: Universal-Commerce-Protocol/.github#21

  Two prose surfaces still described the pre-fix endpoint contract:

    - source/schemas/service.json: platform_schema.description listed
      `endpoint` as required on REST/MCP/A2A — contradicting the schema
      after the endpoint-requirement fix earlier in this PR.
    - docs/documentation/schema-authoring.md: the Service Schemas section
      listed transport requirements flat (no platform-vs-business split)
      with `endpoint` required on REST/MCP/A2A across the board.

  Both now reflect the actual contract: platform profiles require
  `schema` on REST/MCP/Embedded with A2A base-only; business profiles
  require `endpoint` on REST/MCP/A2A with Embedded base-only.
@igrigorik

Copy link
Copy Markdown
Contributor Author

@amithanda good catch on the prose -- ty! Pushed a fix.

I agree that we need to validate and enforce this in CI. Worth nothing that all of the above bugs have associated examples inlined in our spec text, but today we do not validate them — the examples were shaped right, the schemas did not match the intent. As a first pass, I'd suggest we finish and land the pattern we're exploring in #359.

  <!-- ucp-schema: validate discovery/profile.json#/$defs/platform_profile -->
  { "ucp": { ... }, "signing_keys": [...] }

I think above would have caught these bugs. We can certainly explore adding and validating examples in the schemas as you suggested as followup as well. That said, I would not block this PR on either, let's run them in parallel?

@amithanda

Copy link
Copy Markdown
Contributor

@amithanda good catch on the prose -- ty! Pushed a fix.

I agree that we need to validate and enforce this in CI. Worth nothing that all of the above bugs have associated examples inlined in our spec text, but today we do not validate them — the examples were shaped right, the schemas did not match the intent. As a first pass, I'd suggest we finish and land the pattern we're exploring in #359.

  <!-- ucp-schema: validate discovery/profile.json#/$defs/platform_profile -->
  { "ucp": { ... }, "signing_keys": [...] }

I think above would have caught these bugs. We can certainly explore adding and validating examples in the schemas as you suggested as followup as well. That said, I would not block this PR on either, let's run them in parallel?

@igrigorik - Yes, Agree that validating and enforcing via CI checks can come in a follow up PR and we should move ahead with the changes in this PR.

I am also open to back porting these to previous versions given this is more of a bug.

@igrigorik igrigorik merged commit 3d95c87 into main May 12, 2026
11 checks passed
@igrigorik igrigorik deleted the fix/profile-schema branch May 12, 2026 20:47
igrigorik added a commit that referenced this pull request May 20, 2026
* fix: add missing ucp_agent parameter to create_cart REST operation (#362)

create_cart is the only REST operation (out of 14) missing the required
UCP-Agent header parameter. All other operations — including get_cart,
update_cart, and cancel_cart — include it. The spec states "All requests
MUST include the UCP-Agent header."

* fix!: repair profile schemas to pass validation & endpoint contract (#429)

* fix(service): drop endpoint requirement from platform schema

  The platform_schema in service.json required `endpoint` for rest/mcp/a2a
  transports. This was structurally wrong: the protocol has no consumer that
  reads service-level endpoint on the platform side.

  - Negotiation algorithm (overview.md §"Capability Negotiation") reads
    version + extends from the platform profile, not endpoint.
  - Key discovery (overview.md §"Key Discovery") is verification-from-headers
    via UCP-Agent profile URL, not callable RPC against the platform.
  - Webhook delivery (business → platform) is modeled at the capability
    layer via `config.webhook_url` (see order.json#/platform_schema and
    order.md §"Order Event Webhook"), not at the service layer.

  The spec's own Platform Profile example (overview.md §"Platform Profile")
  omits endpoint and consequently fails to validate against this branch of
  its own schema. With this fix the canonical example validates.

* fix(discovery): replace top-level oneOf with anyOf in profile schema

  The top-level discriminator in profile_schema.json was `oneOf`, which
  requires data to match exactly one branch. The two branches (platform_profile,
  business_profile) have no required-vs-forbidden split and no type marker,
  so any document fully describing itself (e.g., a business profile that
  includes spec + schema URLs alongside endpoint) satisfies both branches
  and gets rejected with "must match exactly one schema in oneOf."

  Reproduction with ajv + JSON Schema 2020-12:

    Fixture                                | oneOf  | anyOf
    ---------------------------------------|--------|------
    Platform agent (spec+schema)           | PASS   | PASS
    Hosted-platform (spec+schema+endpoint) | FAIL   | PASS
    Business (endpoint only)               | PASS   | PASS
    Well-described business (all fields)   | FAIL   | PASS

  The second and fourth fixtures are the bug — both represent legitimate
  profiles that fail validation.

  Switching to `anyOf` matches what the data model actually expresses
  ("a profile is platform-shaped, business-shaped, or both"). Consumers
  that want self-describing parse can $ref the specific branch they expect
  based on where they dereferenced (UCP-Agent header → platform_profile;
  /.well-known/ucp → business_profile).

* fix(mcp): drop stale signature field from openrpc meta

  The shopping MCP openrpc doc declared a `signature` field in the request
  meta object referencing RFC 7515 detached JWS. This contradicts the
  Message Signatures specification (docs/specification/signatures.md),
  which is unambiguous: UCP wire signing uses RFC 9421 (HTTP Message
  Signatures) + RFC 9530 (Content-Digest), carried in HTTP headers, not
  in the payload.

  The field was a leftover from an earlier signing-scheme exploration.
  RFC 7515 detached JWS still has one legitimate use in UCP — AP2 mandate
  signing — but that's payload-level by design and unrelated to request
  signing. It does not belong on MCP openrpc meta.

* docs(schemas): update prose to match transport contract

  Two prose surfaces still described the pre-fix endpoint contract:

    - source/schemas/service.json: platform_schema.description listed
      `endpoint` as required on REST/MCP/A2A — contradicting the schema
      after the endpoint-requirement fix earlier in this PR.
    - docs/documentation/schema-authoring.md: the Service Schemas section
      listed transport requirements flat (no platform-vs-business split)
      with `endpoint` required on REST/MCP/A2A across the board.

  Both now reflect the actual contract: platform profiles require
  `schema` on REST/MCP/Embedded with A2A base-only; business profiles
  require `endpoint` on REST/MCP/A2A with Embedded base-only.

---------

Co-authored-by: Patrick R. Jordan <patrick.r.jordan@gmail.com>
Co-authored-by: Ilya Grigorik <ilya@grigorik.com>
jingyli added a commit to jingyli/ucp that referenced this pull request May 21, 2026
…rsal-Commerce-Protocol#440)

* fix: add missing ucp_agent parameter to create_cart REST operation (Universal-Commerce-Protocol#362)

create_cart is the only REST operation (out of 14) missing the required
UCP-Agent header parameter. All other operations — including get_cart,
update_cart, and cancel_cart — include it. The spec states "All requests
MUST include the UCP-Agent header."

* fix!: repair profile schemas to pass validation & endpoint contract (Universal-Commerce-Protocol#429)

* fix(service): drop endpoint requirement from platform schema

  The platform_schema in service.json required `endpoint` for rest/mcp/a2a
  transports. This was structurally wrong: the protocol has no consumer that
  reads service-level endpoint on the platform side.

  - Negotiation algorithm (overview.md §"Capability Negotiation") reads
    version + extends from the platform profile, not endpoint.
  - Key discovery (overview.md §"Key Discovery") is verification-from-headers
    via UCP-Agent profile URL, not callable RPC against the platform.
  - Webhook delivery (business → platform) is modeled at the capability
    layer via `config.webhook_url` (see order.json#/platform_schema and
    order.md §"Order Event Webhook"), not at the service layer.

  The spec's own Platform Profile example (overview.md §"Platform Profile")
  omits endpoint and consequently fails to validate against this branch of
  its own schema. With this fix the canonical example validates.

* fix(discovery): replace top-level oneOf with anyOf in profile schema

  The top-level discriminator in profile_schema.json was `oneOf`, which
  requires data to match exactly one branch. The two branches (platform_profile,
  business_profile) have no required-vs-forbidden split and no type marker,
  so any document fully describing itself (e.g., a business profile that
  includes spec + schema URLs alongside endpoint) satisfies both branches
  and gets rejected with "must match exactly one schema in oneOf."

  Reproduction with ajv + JSON Schema 2020-12:

    Fixture                                | oneOf  | anyOf
    ---------------------------------------|--------|------
    Platform agent (spec+schema)           | PASS   | PASS
    Hosted-platform (spec+schema+endpoint) | FAIL   | PASS
    Business (endpoint only)               | PASS   | PASS
    Well-described business (all fields)   | FAIL   | PASS

  The second and fourth fixtures are the bug — both represent legitimate
  profiles that fail validation.

  Switching to `anyOf` matches what the data model actually expresses
  ("a profile is platform-shaped, business-shaped, or both"). Consumers
  that want self-describing parse can $ref the specific branch they expect
  based on where they dereferenced (UCP-Agent header → platform_profile;
  /.well-known/ucp → business_profile).

* fix(mcp): drop stale signature field from openrpc meta

  The shopping MCP openrpc doc declared a `signature` field in the request
  meta object referencing RFC 7515 detached JWS. This contradicts the
  Message Signatures specification (docs/specification/signatures.md),
  which is unambiguous: UCP wire signing uses RFC 9421 (HTTP Message
  Signatures) + RFC 9530 (Content-Digest), carried in HTTP headers, not
  in the payload.

  The field was a leftover from an earlier signing-scheme exploration.
  RFC 7515 detached JWS still has one legitimate use in UCP — AP2 mandate
  signing — but that's payload-level by design and unrelated to request
  signing. It does not belong on MCP openrpc meta.

* docs(schemas): update prose to match transport contract

  Two prose surfaces still described the pre-fix endpoint contract:

    - source/schemas/service.json: platform_schema.description listed
      `endpoint` as required on REST/MCP/A2A — contradicting the schema
      after the endpoint-requirement fix earlier in this PR.
    - docs/documentation/schema-authoring.md: the Service Schemas section
      listed transport requirements flat (no platform-vs-business split)
      with `endpoint` required on REST/MCP/A2A across the board.

  Both now reflect the actual contract: platform profiles require
  `schema` on REST/MCP/Embedded with A2A base-only; business profiles
  require `endpoint` on REST/MCP/A2A with Embedded base-only.

---------

Co-authored-by: Patrick R. Jordan <patrick.r.jordan@gmail.com>
Co-authored-by: Ilya Grigorik <ilya@grigorik.com>
jingyli added a commit that referenced this pull request May 22, 2026
…changes into 2026-04-08 version (#433)

* update line item ids to differentiate from item ids (#112)

* fix: standardize package registries in lockfile (#368)

Regenerated the lockfile to ensure all dependencies are resolved from the
default public registries configured in pyproject.toml. This removes
environment-specific registry URLs that were inadvertently included.

* Update signature requirements in documentation (format only) (#242)

Split the notes in different lines to fix rendering

Co-authored-by: Guillaume V. <4216770+ptiper@users.noreply.github.com>

* docs: fix inconsistencies in specification examples (#363)

* fix: add missing ucp_agent parameter to create_cart REST operation

create_cart is the only REST operation (out of 14) missing the required
UCP-Agent header parameter. All other operations — including get_cart,
update_cart, and cancel_cart — include it. The spec states "All requests
MUST include the UCP-Agent header."

* docs: fix inconsistencies in specification examples

Fix five documentation bugs where examples diverged from schema
definitions:

- Fix CheckoutCom.svg fallback text showing "Chewy" instead of
  "Checkout.com" (docs/index.md, 2 occurrences)
- Fix invalid checkout status "ready_for_payment" → "ready_for_complete"
  (buyer-consent.md)
- Fix wrong PostalAddress field "address_street" → "street_address"
  (embedded-checkout.md)
- Fix singular/plural JSONPath "method[0]" → "methods[0]"
  (checkout-rest.md)
- Replace deprecated "risk_signal" with "signals" using reverse-domain
  keys (processor-tokenizer-payment-handler.md)

* docs: fix accuracy issues in documentation pages (#365)

- Replace hardcoded version "2026-01-11" with {{ ucp_version }} template
  variable in playground.md (3 UCP version fields; third-party URLs
  left unchanged)
- Add EP (Embedded Protocol) to transport examples list in
  core-concepts.md, matching the four transports defined in the spec
- Update roadmap to reflect that product discovery, cart, and post-order
  management are now part of the specification

* docs: remove entity wrapper from MCP response examples (#360)

MCP response examples incorrectly nested the UCP payload under a
"checkout", "cart", or "order" key inside structuredContent. The
OpenRPC result.name field is descriptive metadata — it does not
create a wrapper key in the JSON-RPC wire format (per the OpenRPC
spec, name only affects params in by-name mode, not results).

This aligns success response examples with error responses, which
already placed the UCP envelope directly in structuredContent
without a wrapper.

Addresses the same issue as the closed #239 — that fix was deferred
to #216, which corrected error responses but left success responses
wrapped.

* fix: render Total and Totals fields on schema reference page (#352)

The Total and Totals sections on the specification reference page
rendered empty tables because `_render_table_from_schema` entered
the `allOf` branch for schemas that have both `properties` and
`allOf` validation constraints (if/then, contains). The allOf items
contained no renderable content, so no table rows were emitted.

Two fixes in `_render_table_from_schema`:

1. Only enter the `allOf` rendering branch when `properties` is
   empty (`and not properties`). This lets Total's three fields
   (type, display_text, amount) render through the normal property
   iteration path.

2. Treat array-typed schemas (like Totals) the same as scalar types
   in the description fallback, since their top-level `allOf` only
   carries `contains` constraints, not composition. Totals now
   renders its description text, consistent with Amount and Signed
   Amount.

Made-with: Cursor

* docs: Correct profile examples in docs Playground widget (#236)

* Correct payment handler profile example in playground

* Remove trailing space from playground.md

---------

Co-authored-by: Guillaume V. <4216770+ptiper@users.noreply.github.com>

* docs: Modernize text diagram in signatures.md (#331)

* fix: Fix inconsistent schema on documentation examples & discounts extension schema (#371)

* Documentation fix to discounts extension to make sure it is compliant with the spec.

* Clean up unsupported field in checkout examples for quantity-related errors.

* Fix discount schema to use UCP specific annotation instead of readOnly.

* docs: add extensibility and forward compatibility guidelines (#290)

- Added "Extensibility and Forward Compatibility" section to the schema
  authoring guide.
- Defined standards for Open vs. Closed Enumerations to prevent breaking
  changes in code-generated clients.
- Added guidance on avoiding 'additionalProperties: false' to ensure
  forward compatibility for objects.
- Clarified the impact of modern code generators (e.g., Quicktype) on
  schema-to-type validation.
- Updated the comprehensive capability schema example to align with
  these new extensibility rules.

Co-authored-by: Guillaume V. <4216770+ptiper@users.noreply.github.com>

* docs: add centralized glossary and acronym standards (#241)

- Organize terms by category (Commerce, Payments, etc.).
- Define financial acronyms previously used but not expanded.
- Establish guidelines for first-use acronyms in Markdown files.

Ref: #214 (comment)

Co-authored-by: Ilya Grigorik <ilya@grigorik.com>

* docs: enrich core-concepts with comprehensive UCP protocol overview (#336)

* feat: attribution field for platform referral context (#391)

* feat: attribution field for platform referral context

  Adds top-level `attribution` carrying platform-emitted referral and
  conversion-event context (campaigns, click IDs, source/medium markers) —
  the agentic counterpart of URL query parameters in browser-based flows.

  - Core field, not an extension. Attribution is informational; its
    presence or absence does not affect protocol behavior, so capability
    negotiation earns nothing. No registry entry, no extension declaration.
  - Open string-keyed map. UCP does not prescribe attribution models,
    vocabularies, or touchpoint logic. Platforms use their existing
    conventions (GA4 campaign parameters, click identifiers like
    gclid / fbclid / ttclid).
  - Lives on cart, checkout, and catalog requests as platform-provided
    input; appears on order as a business-emitted snapshot of the
    originating checkout's attribution.

* drop direct-identifier clause

What counts as "identifying" depends on jurisdiction and on the
agent's and business's data context. Any enumeration is also
incomplete. UCP can't usefully encode this at the schema level.

The privacy paragraph already carries the compliance posture, the
schema description positively scopes the field, and `buyer` is the
canonical home for direct identifiers.

* docs: document min/max property-count default

By default, UCP schemas do not set `minProperties` or `maxProperties`
on object fields. maxProperties caps are deferred to implementers —
the protocol does not define them because any specific limit requires
judgment calls that inevitably hit exceptions; implementers should
impose their own constraints with clear error feedback. minProperties
is omitted because empty objects are well-formed and harmless;
implementers process them as a no-op.

* docs: Fix typos and improve formatting in index and versioning (#416)

* fix(docs): omit deprecated checkout id from playground payload (#332)

Removes checkoutResponse.id from the update payload in playground.md. This field is marked as deprecated_required_to_omit for update requests (as the ID is now passed in the URL path). It was previously only commented as deprecated without actually removing the property from the payload object.

* docs: adds descriptions to links in llms-txt (#419)

* fix!: Cherrypick critical schema fixes into 2026-04-08 version (#440)

* fix: add missing ucp_agent parameter to create_cart REST operation (#362)

create_cart is the only REST operation (out of 14) missing the required
UCP-Agent header parameter. All other operations — including get_cart,
update_cart, and cancel_cart — include it. The spec states "All requests
MUST include the UCP-Agent header."

* fix!: repair profile schemas to pass validation & endpoint contract (#429)

* fix(service): drop endpoint requirement from platform schema

  The platform_schema in service.json required `endpoint` for rest/mcp/a2a
  transports. This was structurally wrong: the protocol has no consumer that
  reads service-level endpoint on the platform side.

  - Negotiation algorithm (overview.md §"Capability Negotiation") reads
    version + extends from the platform profile, not endpoint.
  - Key discovery (overview.md §"Key Discovery") is verification-from-headers
    via UCP-Agent profile URL, not callable RPC against the platform.
  - Webhook delivery (business → platform) is modeled at the capability
    layer via `config.webhook_url` (see order.json#/platform_schema and
    order.md §"Order Event Webhook"), not at the service layer.

  The spec's own Platform Profile example (overview.md §"Platform Profile")
  omits endpoint and consequently fails to validate against this branch of
  its own schema. With this fix the canonical example validates.

* fix(discovery): replace top-level oneOf with anyOf in profile schema

  The top-level discriminator in profile_schema.json was `oneOf`, which
  requires data to match exactly one branch. The two branches (platform_profile,
  business_profile) have no required-vs-forbidden split and no type marker,
  so any document fully describing itself (e.g., a business profile that
  includes spec + schema URLs alongside endpoint) satisfies both branches
  and gets rejected with "must match exactly one schema in oneOf."

  Reproduction with ajv + JSON Schema 2020-12:

    Fixture                                | oneOf  | anyOf
    ---------------------------------------|--------|------
    Platform agent (spec+schema)           | PASS   | PASS
    Hosted-platform (spec+schema+endpoint) | FAIL   | PASS
    Business (endpoint only)               | PASS   | PASS
    Well-described business (all fields)   | FAIL   | PASS

  The second and fourth fixtures are the bug — both represent legitimate
  profiles that fail validation.

  Switching to `anyOf` matches what the data model actually expresses
  ("a profile is platform-shaped, business-shaped, or both"). Consumers
  that want self-describing parse can $ref the specific branch they expect
  based on where they dereferenced (UCP-Agent header → platform_profile;
  /.well-known/ucp → business_profile).

* fix(mcp): drop stale signature field from openrpc meta

  The shopping MCP openrpc doc declared a `signature` field in the request
  meta object referencing RFC 7515 detached JWS. This contradicts the
  Message Signatures specification (docs/specification/signatures.md),
  which is unambiguous: UCP wire signing uses RFC 9421 (HTTP Message
  Signatures) + RFC 9530 (Content-Digest), carried in HTTP headers, not
  in the payload.

  The field was a leftover from an earlier signing-scheme exploration.
  RFC 7515 detached JWS still has one legitimate use in UCP — AP2 mandate
  signing — but that's payload-level by design and unrelated to request
  signing. It does not belong on MCP openrpc meta.

* docs(schemas): update prose to match transport contract

  Two prose surfaces still described the pre-fix endpoint contract:

    - source/schemas/service.json: platform_schema.description listed
      `endpoint` as required on REST/MCP/A2A — contradicting the schema
      after the endpoint-requirement fix earlier in this PR.
    - docs/documentation/schema-authoring.md: the Service Schemas section
      listed transport requirements flat (no platform-vs-business split)
      with `endpoint` required on REST/MCP/A2A across the board.

  Both now reflect the actual contract: platform profiles require
  `schema` on REST/MCP/Embedded with A2A base-only; business profiles
  require `endpoint` on REST/MCP/A2A with Embedded base-only.

---------

Co-authored-by: Patrick R. Jordan <patrick.r.jordan@gmail.com>
Co-authored-by: Ilya Grigorik <ilya@grigorik.com>

* feat!: identity linking OAuth 2.0 foundation with capability-driven scopes (#354) (#431)

* refactor: Update terminology and clarify roles in UCP documentation

- Replace "consumer surfaces/platforms" with "consumer platforms" and "businesses" with "business platforms" for consistency.
- Enhance the definitions of consumer and business platforms, emphasizing their roles in capability consumption and exposure.
- Revise key goals and responsibilities to reflect updated terminology and clarify the interaction dynamics within the UCP framework.
- Introduce a new section on capabilities, detailing their structure and examples to improve understanding of UCP's functionality.

* docs: update core concepts and capabilities in UCP documentation

- Clarified the role of Payment & Credential Providers to emphasize the secure handling of sensitive user data.
- Enhanced the description of Agentic Commerce to include various modalities for AI agents.
- Revised terminology for distinct actors in the UCP framework to improve clarity.
- Updated capability negotiation process to specify version selection and mutual agreement.
- Improved examples and descriptions for capabilities and transport bindings to align with current standards.

* docs: refine terminology and clarify roles in UCP documentation

- Updated terminology to replace "consumer platforms" with "clients" and "business platforms" with "providers" for consistency and clarity.
- Enhanced descriptions of the roles and responsibilities of clients and providers in the UCP framework.
- Revised key goals and capabilities to reflect the updated terminology and improve understanding of UCP's functionality.

* docs: improve formatting and clarity in UCP core concepts documentation

* docs: minor fix to doc and line wrap

* docs: update terminology and clarify roles in UCP documentation

- Replaced "Client" with "Platform" and "Provider" with "Business" for consistency.

* docs: enhance identity linking capability in UCP documentation

- Updated the identity linking specification to clarify the role of platforms and businesses in buyer-authenticated commerce experiences.
- Introduced a new JSON schema for identity linking, detailing the configuration for capabilities that require buyer identity.
- Revised the overview and general guidelines sections to reflect the updated terminology and structure for identity linking capabilities.
- Added new error code for identity requirements in the shopping types schema.

* chore: revert core-concepts.md to upstream version

Restores docs/documentation/core-concepts.md to match
Universal-Commerce-Protocol/ucp upstream main. The local changes
belong to a separate PR and should not be included here.

* docs: update identity linking terminology in specifications

- Renamed the `required` field to `auth_required` in the identity linking specification and JSON schema to enhance clarity regarding buyer identity requirements.

* fix: schema convention, field naming, iss validation

  Four targeted fixes to prepare this PR for backport to 04/08 and
  clean stacking of the delegated IdP follow-up.

  ## 1. Nest $defs under capability name (convention alignment)

  Restructures the schema to match the established pattern required by
  the composition algorithm: `ext_schema["$defs"][root.name]`

  Before:
    $defs:

  After:
    $defs:
      capability_identity_config
      dev.ucp.common.identity_linking:
        platform_schema, business_schema

  Why: capability-scoped schemas live under the capability's reverse-domain
  name so future tooling can resolve them predictably as
  `schema#/$defs/{capability-name}/business_schema`.

  ## 2. Fix `required` → `auth_required` in overview.md

  The business profile example in `overview.md` used `"required": true`
  while the schema and spec text use `"auth_required"`. Anyone copying the
  overview example would hit a validation error.

  ## 3. Remove top-level `version` field from schema

  No other capability schema in the repo carries a top-level `version`
  field — version lives on the capability entry in the UCP profile, not
  on the schema file itself. Removed for consistency with `checkout.json`,
  `fulfillment.json`, `cart.json`, etc.

  ## 4. Tighten `iss` validation language

  Removed the "if present" hedge in two places (For Platforms bullet and
  Account Linking Flow step 3). Since the spec requires businesses to
  MUST return `iss` in every authorization response, the hedge was
  unnecessary and could be read as making `iss` validation conditional.

  ## 5. $comment updated to reflect unified providers model

  The schema-level `$comment` previously described `providers` and
  `mechanisms` as two separate reserved extension points. Updated to
  describe a single `providers` map with a `type` discriminator defaulting
  to `oauth2` — aligning with feedback on #354 that these are the same
  concept (a trust-anchored identity source with a discovery mechanism
  and proof protocol), not separate keys. This is $comment-only — no
  schema behavior change — and gives the follow-up IdP PR a clean model
  to add `providers` onto without rewriting the Future Extensibility
  section.

* docs:Updated the identity linking specification to clarify the support for both delegated identity providers and non-OAuth authentication mechanisms through a unified `config.providers` extension point.

* docs: Update identity linking terminology from "buyer" to "user" across multiple files and update RFC links

- Changed references from "buyer" to "user" in identity-linking documentation to reflect updated terminology.
- Updated the description in the identity linking schema to specify user-scoped features instead of buyer-scoped.
- Adjusted related documentation sections to ensure consistency in terminology and clarify user authentication requirements.

* refactor: flatten scopes to wire-format-keyed map

  Restructures identity linking scope configuration based on TC discussion.

  `config.capabilities` → `config.scopes`. Flat map keyed by wire-format
  scope token (`{capability}:{scope}`); keys are exactly what platforms put
  into OAuth `scope=` parameters.

  ```json
  "config": {
    "scopes": {
      "dev.ucp.shopping.order:read":   {},
      "dev.ucp.shopping.order:manage": {}
    }
  }

  - auth_required removed. Scope presence in the map is the signal:
  listed → user auth required; not listed → public/agent-auth access.
  - Each value is an open scope_policy object for per-scope auth
  constraints (e.g. min_acr, max_token_age) and future metadata. Platforms
  MUST ignore unrecognized fields.
  - New scope_token $def with regex enforcing {reverse-domain}:{name}
  format; removes ambiguity around bare-capability scopes.

* Add scopes section for checkout capability

* docs: Enhance identity linking specification with new error handling and token validation requirements

- Added requirements for handling `insufficient_scope` errors, including the need for businesses to return specific messages identifying missing scopes.
- Updated the `identity_required` section to clarify conditions under which it should be triggered.
- Introduced the `UCP-Identity-Token` header for user identity tokens when using platform credentials.

* docs: Update identity linking specification to include loopback redirect handling

* docs: Add scopes sections for cart, order, and catalog capabilities

* docs: clarify OAuth 2.0 usage and drop UCP-Identity-Token

* support public clients via PKCE

  The MUST on client_secret_basic excluded native, desktop, and on-device
  agent runtimes (RFC 8252 §8.5 — public clients cannot keep a client_secret)
  and also blocked stronger asymmetric methods (private_key_jwt RFC 7523,
  tls_client_auth RFC 8705). The IdP support we want to land next requires
  asymmetric crypto for JWT bearer assertions.

  Replaces the single-method MUST with RFC-8414-driven negotiation:
    - Confidential clients SHOULD prefer asymmetric methods; MAY use
      client_secret_basic.
    - Public clients (RFC 8252 §8.5) MUST use 'none' and rely on PKCE
      with S256 as proof-of-possession; MUST NOT embed a client_secret.
    - Businesses declare methods in token_endpoint_auth_methods_supported;
      SHOULD support an asymmetric method; MAY support 'none' for public
      clients. PKCE S256 required when 'none' is advertised.
    - Distinct error codes: invalid_client for auth-method failures,
      invalid_grant for PKCE failures.

* docs: normalize capability scope sections

  Per TC discussion default access is a merchant policy decision, not a
  spec mandate. UCP defines well-known scopes; merchants decide what auth
  is required for non-scoped operations.

    - Remove default-access framing from capability specs. Each section
      now states only the well-known scopes the capability defines.
    - Tighten scope descriptions to a consistent shape:
      "<operations gated> — <data or behavior unlocked>".
    - Hoist protocol-level rules (declaration, derivation, well-known vs
      custom extension) into identity-linking.md, where they live once.
      Capability specs link to that section instead of duplicating.
    - Switch the identity-linking.md B2B walkthrough from
      dev.ucp.shopping.checkout:create to :manage. The well-known scope
      fits the "no guest checkout" narrative more cleanly (gates all
      checkout ops, not just the entry point).

* Add an optional description field to the scope_policy object in the identity_linking.json schema.

* document identity_optional and relation to scope description

  * `scope_policy.description`: $ref shared `description.json` type for
    multi-format text (plain/markdown/html). Cross-domain ref to
    shopping/types is the minimal change; promoting the type to
    common/types is a follow-up.

  * New `## Optional Authentication` section + `identity_optional`
    info-severity code. Decoupled from per-scope `description` by
    design: identity_optional is a runtime per-request notice;
    description is static per-scope context for OAuth consent.

  * `insufficient_scope` example fixed: response lists the FULL required
    scope set, not the delta (per Amit's TC restatement). Platform
    computes the diff and uses incremental authorization to avoid
    redundant consent prompts. continue_url wording aligned.

  * Schema descriptions: drop "public or agent-authenticated access"
    framing. TC consensus is that UCP does not prescribe a default;
    merchants decide access policy for non-scoped operations.

  * `message_info.json` `code`: register known info codes via JSON
    Schema `examples` array AND inline names in the description.
    Tooling (autocomplete, codegen) and humans both served.

* docs: extract and document well-known info+warning mesages codes

From the boyscout 'leave it better than you found it' rulebook...

 - Copy _error pattern as standalone file for _info and _warning
 - Include info and warning codes in reference docs

* adopt WWW-Authenticate Bearer challenges

Replace the pre-baked OAuth `continue_url` pattern with RFC 6750 §3
WWW-Authenticate challenges, plus RFC 9728 Protected Resource Metadata
pointers. Resolves David's TC flag.

Why: pre-baking an authorization URL forces the merchant to own
parameters it can't sensibly own — PKCE code_challenge, state,
redirect_uri, client_id are all client-side concerns. Native/agent
clients per RFC 8252 construct their own authorization request anyway,
so the pre-baked URL was either ignored or rewritten. Standard OAuth
client libraries parse WWW-Authenticate Bearer challenges automatically;
custom continue_url parsing was UCP-specific dead weight.

* For Platforms: MUST process WWW-Authenticate Bearer challenges per
  RFC 6750 §3 on 401/403; extract scope parameter; SHOULD follow
  resource_metadata pointer per RFC 9728. Bumped Bearer Authorization
  bullet with RFC 6750 §2.1 reference.
* For Businesses: MUST emit Bearer challenge on identity_required
  (401) and insufficient_scope (403). RFC 9728 SHOULD bullet upgraded
  to reference /.well-known/oauth-protected-resource and integration
  with WWW-Authenticate.
* identity_required: full normative restructure (status code, header,
  body). realm MUST be issuer URI; error="invalid_token" when token
  present-but-bad; error SHOULD be omitted when no token (RFC 6750
  §3.1). resource_metadata SHOULD. continue_url retained for non-OAuth
  onboarding flows ONLY; explicit MUST NOT for pre-baked OAuth URLs.
* insufficient_scope: full normative restructure. realm + error +
  scope (full required set, not delta) MUST. resource_metadata SHOULD.
  Pre-baked OAuth continue_url removed entirely.
* Security Considerations: new "Authentication challenges" bullet.
  Platforms MUST drive flow from structured scope/error params;
  error_description is hint-only and MUST NOT control flow. realm MUST
  match issuer URI for cross-protection-space correlation.

* docs: add UCP and OAuth architecture explainer

  Document the architectural split between UCP and OAuth (RFC 8414)
  responsibilities that the rest of the spec relies on but never
  articulates. Anchors the four moving parts:

  * UCP config.scopes — hard gates (required auth)
  * OAuth scopes_supported — accepted scope vocabulary
  * Diff (scopes_supported ∖ config.scopes) — optional layer
  * UCP messages[] — runtime contextual hints (e.g., identity_optional)

* Three corrections to identity-linking.md following review of the WWW-Authenticate Bearer changes:

1. Add no-token identity_required example

The existing example only showed the token-present-but-expired case (error="invalid_token"). RFC 6750 §3.1 says error SHOULD be omitted when no token was presented — the more common case for a first request to a gated operation. Added a second labeled example for the no-token case so implementers aren't led to emit error="invalid_token" unconditionally.

2. Fix identity_optional section direction

The section intro described identity_optional as "a mechanism for the platform to inform the buyer." The direction was reversed — the business emits this code in its response; the platform receives it and may present it to the user. Corrected to accurately describe the emitter and receiver.

3. Simplify identity_optional to remove misleading description coupling

The section stated that per-scope description fields convey context for optional authentication, and that businesses SHOULD populate them when emitting identity_optional. This is incorrect: description is a field on scope_policy objects in config.scopes, which is the hard-gate (required) layer. Scopes relevant to identity_optional are by definition in the optional layer (scopes_supported ∖ config.scopes) and have no corresponding UCP schema field for descriptions. Removed the two-mechanisms paragraph and the SHOULD guidance on populating descriptions, as both described a mechanism that doesn't exist for optional-layer scopes. Upgraded identity_optional emission from MAY to SHOULD, since content on the message is the only available value prompt mechanism.

---------

Co-authored-by: Amit Handa <amithanda@google.com>
Co-authored-by: Ilya Grigorik <ilya@grigorik.com>

---------

Co-authored-by: James Thompson <thompson.tomo@outlook.com>
Co-authored-by: Guillaume V. <4216770+ptiper@users.noreply.github.com>
Co-authored-by: Dario Guzik <dario@guzik.com.ar>
Co-authored-by: Patrick R. Jordan <patrick.r.jordan@gmail.com>
Co-authored-by: James Andersen <james.j.andersen@gmail.com>
Co-authored-by: Nachiket Torwekar <nachiket.torwekar@gmail.com>
Co-authored-by: Dylan Koch <dkoch74@gmail.com>
Co-authored-by: Gauresh G Pai <107191770+gaureshpai@users.noreply.github.com>
Co-authored-by: Greg Smith <smithgrg@amazon.com>
Co-authored-by: Ilya Grigorik <ilya@grigorik.com>
Co-authored-by: Amit Handa <amithanda@google.com>
Co-authored-by: pemamian <pemamian@google.com>
Co-authored-by: Nicholas James Hall <55357993+nicholasjameshall@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

TC review Ready for TC review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants