Skip to content

feat: typed error responses and checkout conditional writes#628

Open
sakinaroufid wants to merge 4 commits into
Universal-Commerce-Protocol:mainfrom
sakinaroufid:spec-improvements
Open

feat: typed error responses and checkout conditional writes#628
sakinaroufid wants to merge 4 commits into
Universal-Commerce-Protocol:mainfrom
sakinaroufid:spec-improvements

Conversation

@sakinaroufid

Copy link
Copy Markdown
Contributor

Description

TL;DR: UCP is built on agents consuming machine-readable contracts, and the discovery profile points them straight at rest.openapi.json. But that file only modeled success responses, so every SDK, server stub and conformance test generated from it was blind to the error contract the docs promise, and error handling is exactly where autonomous agents need structure most. This PR declares the full error contract in the OpenAPI/OpenRPC definitions (4XX/5XX on all 13 operations, 409 on the state-changing ones, and search_catalog/lookup_catalog finally able to return the standard error envelope like every other op), so generated clients get typed errors and conformance can test failure paths. It also adds ETag/If-Match conditional writes to checkout so two writers on one session, say an agent on the API and a buyer in a trusted UI, can't silently overwrite each other's changes. No schema changes anywhere.


The REST service definition only declares the success response for each operation. The docs list a full status code contract (400 through 503, including 409 on idempotency-key reuse) with a JSON error body, but none of that exists in rest.openapi.json, so anything generated from it (clients, server stubs, contract tests) has no idea errors exist. I hit this while generating a client: the documented 409 comes back as an unmodeled exception instead of something you can parse the error envelope out of.

The first part fixes that. It adds reusable error response components carrying the standard envelope and wires them into every operation: 4XX/5XX on all 13 operations, an explicit 409 on the seven state-changing ones, and failed-delivery responses on the order event webhook. While in there I noticed search_catalog and lookup_catalog are the only two operations whose result has no error_response branch, in both the REST and MCP definitions, so they can't return the standard envelope at all. Every other operation can, including get_product in the same capability. Both results now use the same oneOf pattern as everything else, and the behavior is documented on the catalog pages.

The second part adds optimistic concurrency to checkout. Update Checkout is a full replacement, and a session can have more than one writer: an agent on the API plus a buyer parked in a trusted UI via continue_url, or the business repricing things server side. Two writers that read the same state and write different replacements lose one of the updates silently. Idempotency-Key doesn't help, it only dedupes identical retries of one request.

The fix is plain RFC 9110 conditional requests, no new schema fields. Businesses return a strong ETag on checkout responses. Platforms echo it in If-Match on Update, Complete and Cancel. On a mismatch the business applies nothing and returns 412 Precondition Failed with the standard envelope (precondition_failed, severity recoverable); the platform re-reads, reconciles and retries with the fresh tag. 428 Precondition Required is there for businesses that want to force conditional writes. Support is discovered from responses: if a platform never saw a tag it just writes unconditionally, so existing implementations keep working untouched. Only REST gets a binding for now, since MCP/A2A/Embedded have no natural place to carry the tag.

The conditional writes add normative behavior, so if that part needs to go through an enhancement proposal first I'm fine splitting it out into its own PR. The error declarations are just making the machine-readable contract match what the docs already say.

Category (Required)

Please select one or more categories that apply to this change.

  • Core Protocol: Changes to the base communication layer, global context, or breaking refactors. (Requires Technical Council approval)
  • Governance/Contributing: Updates to GOVERNANCE.md, CONTRIBUTING.md, or CODEOWNERS. (Requires Governance Council approval)
  • Capability: New schemas (Discovery, Cart, etc.) or extensions. (Requires Maintainer approval)
  • Documentation: Updates to README, or documentations regarding schema or capabilities. (Requires Maintainer approval)
  • Infrastructure: CI/CD, Linters, or build scripts. (Requires DevOps Maintainer approval)
  • Maintenance: Version bumps, lockfile updates, or minor bug fixes. (Requires DevOps Maintainer approval)
  • SDK: Language-specific SDK updates and releases. (Requires DevOps Maintainer approval)
  • Samples / Conformance: Maintaining samples and the conformance suite. (Requires Maintainer approval)
  • UCP Schema: Changes to the ucp-schema tool (resolver, linter, validator). (Requires Maintainer approval)
  • Community Health (.github): Updates to templates, workflows, or org-level configs. (Requires DevOps Maintainer approval)

Core Protocol covers the conditional writes; the error declarations are Capability plus Documentation.

Related Issues

None.

Checklist

  • I have followed the Contributing Guide (including Conventional Commits title requirements and ! for breaking changes).
  • I have updated the documentation (if applicable).
  • My changes pass all local linting and formatting checks.
  • I have added tests that prove my fix is effective or that my feature works.
  • New and existing unit tests pass locally with my changes.
  • (For Core/Capability) I have included/updated the relevant JSON schemas.
  • I have regenerated Python Pydantic models by running generate_models.sh under python_sdk.

The unchecked ones: the new conditional update examples (applied and rejected) are exercised by the docs example corpus rather than separate unit tests, and nothing under source/schemas/ changes. That's the point of putting the tag in HTTP fields, so there's also nothing to regenerate on the SDK side.

Screenshots / Logs (if applicable)

$ ucp-schema lint source/
✓ 96 files checked, all passed

$ python3 scripts/validate_examples.py --schema-base source/schemas/
279 passed, 0 failed, 0 errors, 47 skipped

$ python3 scripts/test_validate_examples.py
48 passed, 0 failed

mkdocs build --strict passes in both site modes and the link checker comes back clean. The doc generation macros only read success responses, so rendered pages don't change apart from the added notes and the two new catalog entity sections.

The service definitions only declared success responses, so anything
generated from rest.openapi.json had no typed error shape despite the
docs promising a full status code contract. Add reusable error response
components carrying the standard envelope and declare 4XX/5XX on all 13
operations, an explicit 409 on the seven state-changing ones, and
failed-delivery responses on the order event webhook. Also give
search_catalog and lookup_catalog the same error_response oneOf branch
every other operation already has, in both the REST and MCP definitions.

Checkout also gets optimistic concurrency: businesses return a strong
ETag on checkout responses, platforms echo it via If-Match on Update,
Complete and Cancel, and a mismatch returns 412 Precondition Failed
with the standard envelope so the platform can re-read, reconcile and
retry. No schema changes; the tag rides standard HTTP fields and
support is discovered from responses themselves.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants