Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 10 additions & 47 deletions .changeset/profile-driven-negotiation.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,53 +2,16 @@
"@shopify/ucp-cli": minor
---

**A profile speaks exactly one UCP release, and the business must offer that release.**
`ucp-cli` now negotiates the newest exact UCP release shared with each Business, using the managed Profile by default. This build supports **2026-04-08** and **2026-08-25**; extension-key validation follows the selected release.

`ucp-cli` supports UCP **2026-04-08** and **2026-08-25**. This explicit set replaces the `2026-01-23`–`2026-08-25` range that `0.8.0` accepted; the profile selects which release it speaks when you create it:
Untouched Profiles generated by earlier releases continue working as named managed Profiles. Edited or custom-hosted Profiles retain their authored document and URL. `ucp profile init` remains available for intentional customization or release pinning, accepts one exact `--version`, and activates only with `--activate`; the former range flags and inert `profile publish` command are removed.

```sh
ucp profile init --name legacy --version 2026-04-08
ucp discover shop.example.com --profile legacy
```
Other observable changes:

Upgrading also raises the runtime floor to Node.js `22.19.0` (breaking change 7).

## Breaking changes

1. **The selected profile's release must exactly match a version the business offers.** `0.8.0` accepted a business service entry anywhere in its `2026-01-23`–`2026-08-25` range. Now a `2026-08-25` profile against a business publishing only `2026-04-08` fails with `PROTOCOL_VERSION_INCOMPATIBLE` before any commerce operation is sent — and when another local profile speaks a version the business offers, the error names that profile. A business offering neither supported release (for example, only `2026-01-23`) can no longer negotiate at all.

The match can also come through `supported_versions`: whenever the top-level `/.well-known/ucp` document is at any different version and its `supported_versions` entry links a document for your profile's release, `ucp-cli` fetches and validates that leaf. Extension-key validation follows the selected release: `2026-04-08` rejects hyphens and digit-leading reverse-domain segments; `2026-08-25` behavior is unchanged.

2. **A profile's local `profile.json` controls negotiation, whether its profile URL is a release default or a URL you own.** Its `ucp.version`, services, and capabilities are what `ucp-cli` uses. Requests advertise the profile URL, the business reads the copy served there, and `ucp doctor` compares the two. If an operation reports `AGENT_PROFILE_SERVICE_UNDECLARED`, declare that service in `profile.json` and make the profile URL serve the corrected file — switching to a URL you own if the current one cannot change.

Upgrading does not rewrite an existing `profile.json`, so a stock `0.8.0` file can report a non-failing `profile-drift` warning against the currently served document. `ucp profile init --name <name> --version <release> --force` replaces the file and discards local edits; pass `--profile-url` again when the URL is yours.

3. **`ucp profile init` takes `--version <release>`; the removed `--protocol-min` / `--protocol-max` flags now fail as unknown flags.** A stale `meta.protocol_versions` field in existing profiles is tolerated but ignored.

**`ucp profile publish` is removed** and invoking it fails as an unknown command. The `0.8.0` command never uploaded a document; publish `profile.json` through your hosting workflow.

4. **Bare `ucp discover` negotiates only the services both your agent profile and the business document declare.** Business-only services stay visible in the returned `profile`, are not contacted, get no `negotiated` entry — and no longer fail a bare `ucp discover` merely because they cannot negotiate. Explicitly requesting an undeclared service still raises `AGENT_PROFILE_SERVICE_UNDECLARED`.

Output gains `protocol` — fields `version`, `source` (`well-known` or `supported_versions`), and `businessProfileUrl` — plus `expectedCapabilities`, the intersection of agent profile and business declarations. `expectedCapabilities` is advisory; each response's `ucp.capabilities` remains authoritative.

5. **`ucp doctor` exits 1 when any check fails; warnings alone exit 0.** `--skip-network` skips exactly four checks: `protocol`, `profile-redirect`, `profile-drift`, and `profile-cache-control`.

Those four replace `0.8.0`'s `profile-url` check id — update scripts that match check ids. An unreachable profile URL or a version disagreement is a failure; non-version document drift and cache-control issues are warnings.

6. **`ucp --version` prints `ucp <cli-version> (UCP 2026-04-08, 2026-08-25)`** instead of `0.8.0`'s bare semver. Update scripts to read `<cli-version>` from the second field.

7. **Node.js `22.19.0` or later is required.** npm's default behavior reports the `engines` mismatch as an `EBADENGINE` warning and completes the install, so a successful install is not proof of compatibility.

`ucp doctor`'s `runtime` check now compares the full `major.minor.patch` and fails below the floor; `0.8.0` compared the major version only. Upgrade Node before invoking the CLI.

8. **Redirect responses are refused: UCP requires profile, identity, and schema documents to be served directly, with no redirect to follow.** `ucp-cli` refuses `301`, `302`, `303`, `307`, and `308` on every outbound UCP fetch, and applies the same policy to negotiated service endpoints, so the endpoint it calls is the endpoint the business document declares.

Direct refusals raise `TRANSPORT_REDIRECT_REFUSED`, naming the status and the refused `Location`. A redirecting agent profile URL surfaces in `ucp doctor` as a single failing `profile-redirect` check. Serve documents directly and put each final HTTPS URL in the declaration that names it.

Fixed `checkout complete` body handling so it accepts input consistently with other write operations.

## Error codes

Eight codes are added relative to `0.8.0`, and none are removed: `AGENT_PROFILE_SCHEMA_INVALID`, `AGENT_PROFILE_SERVICE_UNDECLARED`, `AGENT_PROFILE_UNREACHABLE`, `AGENT_PROFILE_VERSION_MISMATCH`, `AGENT_PROFILE_VERSION_UNSUPPORTED`, `PROFILE_VERSION_MISMATCH`, `SERVICE_VERSION_INCOMPATIBLE`, and `TRANSPORT_REDIRECT_REFUSED`.

Within this added set, the `AGENT_PROFILE_*` codes identify your agent document — the one your requests advertise. `PROFILE_VERSION_MISMATCH` identifies the business document. `SERVICE_VERSION_INCOMPATIBLE` means the two sides' service declarations share no common version. `TRANSPORT_REDIRECT_REFUSED` is a redirect refused directly at the transport (breaking change 8).
- `ucp discover` reports the selected protocol and services declared by both Profiles.
- MCP mode ignores Profile and Business selections in `active.yaml`; explicit arguments and environment variables still apply.
- Business discovery and `tools/list` are cached separately between runs. `--refresh` bypasses both caches; `--dry-run` skips only the operation call.
- Doctor enforces the Node.js `22.19.0` runtime floor, exits nonzero on failed checks, and separately reports Profile redirects, drift, and cache policy.
- UCP document and declared endpoint redirects raise `TRANSPORT_REDIRECT_REFUSED` rather than being followed.
- Profile, version, and service failures now distinguish Profile-side and Business-side causes; see the error-code reference for recovery.
- `ucp checkout complete` now accepts and sends its required completion body like other write operations.
40 changes: 9 additions & 31 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ Designed agent-first. Structured JSON I/O on every command. Schema introspection
```sh
$> npm install -g @shopify/ucp-cli
$> ucp skills add
$> ucp profile init --name shopper
```
### 1. Find products

Expand Down Expand Up @@ -52,7 +51,7 @@ The merchant returns a cart with confirmed pricing and `continue_url` that the b
Need a shipping-cost preview before checkout? Inspect `ucp cart update --input-schema`; if the merchant accepts fulfillment destinations on cart update, send the buyer destination there to get merchant-provided estimates. Use checkout for the complete shipping/pickup option map and final selectable options.

### 3. Convert to checkout, configure handoff, complete
Some checkouts require additional buyer input or review that the agent can't negotiate on the buyer's behalf. When the merchant returns an escalation status with a continue URL, configure a custom hook and the CLI will call it to handle the handoff.
Some checkouts require additional buyer input or review that the agent can't negotiate on the buyer's behalf. When the merchant returns an escalation status with a continue URL, configure a custom hook and the CLI will call it to handle the handoff.

```sh
export UCP_ON_ESCALATION='jq -r .url | xargs open' # macOS; xdg-open on Linux
Expand Down Expand Up @@ -92,41 +91,20 @@ The skill packages the agent-facing operating model — when to search vs discov

A **business** is a URL — `https://shop.example.com`. The CLI fetches the business's UCP profile (cached on disk per spec TTL), negotiates the protocol version + transport, and dispatches operations against its endpoint. UCP CLI abstracts transport, service + capability negotiation, ..., and error handling.

### Choosing a protocol version
### Protocol releases

```sh
ucp --version # ucp 0.7.0 (UCP 2026-04-08, 2026-08-25)
ucp --version # ucp 0.9.0 (UCP 2026-04-08, 2026-08-25)
```

The parenthetical lists the UCP releases this CLI supports. Which one you speak is set by your **active profile** — a profile declares exactly one `ucp.version`, and merchants validate that exact version on every request. So switching version means switching profile:

```sh
ucp profile init --name legacy --version 2026-04-08 # create a profile pinned to an older release
ucp discover shop.example.com --profile legacy # ...and use it
```

Each profile has two halves, and one contract between them:

- `~/.ucp/profiles/<name>/profile.json` — the document the CLI declares. Its `ucp.version` is the release you speak; its services and capabilities are what you offer to negotiate.
- `meta.profile_url` — where that document lives on the web. The CLI sends this URL on every request, merchants fetch it, and they negotiate against whatever it serves.

**The two must say the same thing.** Keeping them equal is your job — `ucp doctor` is what checks: `protocol` fails when the URL is unreachable or serves a different `ucp.version` than you send, `profile-drift` warns when the rest of the document differs. `profile init` starts them in agreement by writing the release's published document to disk.

To advertise a capability set of your own, put the document at a URL you control. Nothing is signed, so **whoever controls that URL controls this agent's identity.**

```sh
ucp profile init --name mine --profile-url https://you.example/agent.json
# edit ~/.ucp/profiles/mine/profile.json
# upload it to that URL yourself — scp, S3, whatever you host with
ucp doctor # confirms the two agree
```
The parenthetical lists the UCP releases this CLI supports. For each Business, the CLI uses the newest release both sides support. Intentional release pinning and custom Profiles are covered in [`skills/ucp/references/SETUP.md`](skills/ucp/references/SETUP.md). Profile documents are unsigned, so **whoever controls the advertised URL controls the agent's identity.**

Two scopes for picking which business an operation targets:

- **Global catalog (no `--business`)** — for product discovery across thousands of merchants. Each result names its merchant via `seller.domain`.
- **Per-merchant (`--business <url>`)** — for cart, checkout, order, or catalog operations scoped to a single merchant.
- **Per-merchant (`--business <url>`)** — for cart, checkout, order, or catalog operations scoped to a single merchant.

**Live introspection so the agent never guesses.** Both `discover` and `--input-schema` make a real network call to the merchant; they're not static doc lookups. The schema you get back is whatever the merchant currently advertises — including extensions they've added since you last shopped there. Merchants stay in authoritative control of their own schemas; they can evolve, deprecate, or extend without coordinated releases against the CLI or the agent. Capability negotiation is real: the agent and merchant agree on what to use based on what's actually offered right now.
**Live introspection so the agent never guesses.** Both `discover` and `--input-schema` read the merchant's own advertised schemas rather than static docs; results are cached briefly, and `--refresh` forces a fresh fetch. The schema you get back is whatever the merchant currently advertises — including extensions they've added since you last shopped there. Merchants stay in authoritative control of their own schemas; they can evolve, deprecate, or extend without coordinated releases against the CLI or the agent. Capability negotiation is real: the agent and merchant agree on what to use based on what's actually offered right now.

```sh
ucp discover --business https://<seller-domain> # what operations are offered
Expand All @@ -135,7 +113,7 @@ ucp cart update --input-schema --business https://<seller-domain> # cart-s
ucp checkout update --input-schema --business https://<seller-domain> # full fulfillment option map/final fields
```

**Every response carries a `cta`.** The CLI is context-aware — it tracks where you are in the flow and surfaces the next-best step(s) as structured recommendations the agent should consider. Successful responses point forward (cart created → here are the checkout / refine / search-more commands); error responses point at recovery (schema validation failed → here's the `--input-schema` command to introspect first). The agent doesn't have to memorize the operating model; the CLI threads it through.
**Most responses carry a `cta`.** The CLI is context-aware — it tracks where you are in the flow and surfaces the next-best step(s) as structured recommendations the agent should consider. Successful responses point forward (cart created → here are the checkout / refine / search-more commands); recoverable errors point at recovery (schema validation failed → here's the `--input-schema` command to introspect first). The agent doesn't have to memorize the operating model; the CLI threads it through. Errors themselves stay flat — `code` and `message` always, `retryable` and `cta` optional — so branch on the full `code` and read a CTA as advice.

```sh
$ ucp cart create --business https://shop.example.com \
Expand Down Expand Up @@ -238,7 +216,7 @@ ucp cart update <id> --business https://<seller-domain> \
--dry-run
```

Builds and validates the request, prints the exact payload that would hit the wire (including auto-injected `meta.idempotency-key` and `meta.ucp-agent`), skips the network call. Cart and checkout updates are full-replace: carry forward request-shaped line items, using `line_items[].id` only for existing lines and `line_items[].item.id` for the underlying item/variant. Useful for debugging payloads or confirming a mutation before issuing it.
Builds and validates the request, prints the exact payload that would hit the wire (including auto-injected `meta.idempotency-key` and `meta.ucp-agent`), and skips the operation call — discovery still runs first, so a cold or `--refresh`ed cache can still hit the network. Cart and checkout updates are full-replace: carry forward request-shaped line items, using `line_items[].id` only for existing lines and `line_items[].item.id` for the underlying item/variant. Useful for debugging payloads or confirming a mutation before issuing it.

### Custom request headers (auth, tenancy, tracing)

Expand All @@ -251,7 +229,7 @@ UCP requests attach a built-in `User-Agent: @shopify/ucp-cli/<version>`. Overrid

Higher source wins per header name (case-insensitive); non-conflicting headers from every source ship. Empty values unset for that scope. Framing headers the dispatcher owns (`Content-Type`, `Accept`, `Host`, `Connection`, hop-by-hop, `MCP-Protocol-Version`) are silently dropped from all user sources. Sensitive header values (`Authorization`, `Cookie`, and any name ending in `-Token`, `-Key`, `-Secret`, `-Password`) are redacted in verbose traces (`UCP_VERBOSE=1`).

Persistent setup, modeled on git's `[http]` / `[http "<URL>"]`:
Persistent setup needs a named Profile (see [`skills/ucp/references/SETUP.md`](skills/ucp/references/SETUP.md)), and is modeled on git's `[http]` / `[http "<URL>"]`:

```json
{
Expand Down
3 changes: 2 additions & 1 deletion biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
"!**/.husky",
"!**/coverage",
"!**/local",
"!**/src/core/generated"
"!**/src/core/generated",
"!**/test/fixtures/legacy-profiles"
]
},
"formatter": {
Expand Down
Loading