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
8 changes: 8 additions & 0 deletions CHANGE_LOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Change Log

## 20/09/26 at 16:15:19 by [Ran Yahalom](mailto:ranya@wix.com)

- Made Section 5.3.4 authoritative over the Section 5.1.1 `pending`/`confirm` cell, so `confirm` from `pending` is legal only in manual confirmation mode and an auto-mode booking that is not already `confirmed` MUST be rejected with `invalid_transition`. That closes the contradiction that would otherwise let a platform confirm an unpaid UCP-Native leftover sitting in `pending`
- Marked REST `POST /bookings/{booking_id}/confirm` and MCP `usp_bookings_confirm` as `business_only`, stripping platform `USP-Agent`/`_meta` and booking-scoped credentials, because a scoped credential that can confirm would still grant a free appointment on that unpaid leftover
- Added flow vector `105-confirmation-mode-guard` and a coherence check for the `business_only` access class so the cell cannot drift untested again

---

## 20/09/26 at 14:50:25 by [Ran Yahalom](mailto:ranya@wix.com)

- Withdrew the Section 7.5 cancel-checkout terminal-state invariant, the `cancellation.reason_code` field with its `checkout_abandoned` value, the `canceled_at` cross-read stability requirement, the buyer-notification suppression rule, the Section 5.2 note on the missing terminal path, and flow vector `105-cancel-checkout-abandoned-booking`. The hazard they addressed rested on the claim that a booking left `pending` by an abandoned checkout stays confirmable, and Section 5.3.4 restricts `confirm` to business-initiated manual-mode approval, so the claim does not hold as stated
Expand Down
32 changes: 4 additions & 28 deletions openapi/usp-rest.json
Original file line number Diff line number Diff line change
Expand Up @@ -614,7 +614,7 @@
"BookingScopedCredential": {
"type": "http",
"scheme": "DPoP",
"description": "Resource-scoped capability credential issued at creation, authorizing get/update/cancel/reschedule/PII-bearing operations on that single resource independent of platform identity. The resource is a booking, a waitlist entry, or a feed subscription. Registry registrations are NOT in this set: they are authorized at platform tier against the registering platform's bound jkt, since a registry entry has exactly one owner and needs no separate credential type. Transmitted as 'Authorization: DPoP <bsc_...>'. Shape defined at schemas/profile.json#/$defs/BookingScopedCredential and returned by the creation responses. The DPoP scheme replaces Bearer for two reasons. It removes this credential from the Authorization: Bearer slot it previously shared with OAuth2Bearer and ApiKey, where a server could not tell from the request alone which scheme the caller intended. And it stops a business silently accepting a sender-constrained credential as a plain bearer token. When the credential carries a cnf confirmation key it is sender-constrained and MUST be presented together with a PlatformKeyPop proof over the bound key - the requirement object listing both is that case, and OpenAPI cannot express the 'only when cnf is present' condition, so the credential-alone alternative remains listed for unbound legacy credentials while the MUST is carried in specification.md Section 10.1.6. A business that issued a cnf-bearing credential MUST NOT accept it without a proof, whatever mechanism the caller declares. Mechanism identifier shared with schemas/profile.json#/$defs/AuthorizationMechanism and with openrpc/usp-mcp.json components.x-usp-securitySchemes.",
"description": "Resource-scoped capability credential issued at creation, authorizing get/update/cancel/reschedule/PII-bearing operations on that single resource independent of platform identity. It does not authorize confirm. The resource is a booking, a waitlist entry, or a feed subscription. Registry registrations are NOT in this set: they are authorized at platform tier against the registering platform's bound jkt, since a registry entry has exactly one owner and needs no separate credential type. Transmitted as 'Authorization: DPoP <bsc_...>'. Shape defined at schemas/profile.json#/$defs/BookingScopedCredential and returned by the creation responses. The DPoP scheme replaces Bearer for two reasons. It removes this credential from the Authorization: Bearer slot it previously shared with OAuth2Bearer and ApiKey, where a server could not tell from the request alone which scheme the caller intended. And it stops a business silently accepting a sender-constrained credential as a plain bearer token. When the credential carries a cnf confirmation key it is sender-constrained and MUST be presented together with a PlatformKeyPop proof over the bound key - the requirement object listing both is that case, and OpenAPI cannot express the 'only when cnf is present' condition, so the credential-alone alternative remains listed for unbound legacy credentials while the MUST is carried in specification.md Section 10.1.6. A business that issued a cnf-bearing credential MUST NOT accept it without a proof, whatever mechanism the caller declares. Mechanism identifier shared with schemas/profile.json#/$defs/AuthorizationMechanism and with openrpc/usp-mcp.json components.x-usp-securitySchemes.",
"x-usp-mechanism": "booking_scoped_credential"
},
"OAuth2Bearer": {
Expand Down Expand Up @@ -2550,11 +2550,10 @@
"Bookings"
],
"summary": "Confirm Booking",
"description": "Business-initiated confirmation for manual-mode bookings (specification.md Section 5.3.4). Not a platform continuation method: a platform principal and a booking_scoped_credential MUST NOT authorize this call. Authentication of the business caller is deployment-defined and is not a platform AuthorizationPolicy mechanism. Auto-mode bookings that are not already confirmed MUST be rejected with invalid_transition at HTTP 200 in messages[].",
"operationId": "confirmBooking",
"x-usp-access": "business_only",
"parameters": [
{
"$ref": "#/components/parameters/USP-Agent"
},
{
"$ref": "#/components/parameters/IdempotencyKey"
},
Expand Down Expand Up @@ -2630,30 +2629,7 @@
"$ref": "#/components/responses/ServiceUnavailable"
}
},
"security": [
{
"HttpMessageSignature": []
},
{
"PlatformKeyPop": []
},
{
"BookingScopedCredential": [],
"PlatformKeyPop": []
},
{
"BookingScopedCredential": []
},
{
"OAuth2Bearer": []
},
{
"ApiKey": []
},
{
"MutualTLS": []
}
]
"security": []
}
},
"/bookings/{booking_id}/cancel": {
Expand Down
17 changes: 2 additions & 15 deletions openrpc/usp-mcp.json
Original file line number Diff line number Diff line change
Expand Up @@ -1766,22 +1766,9 @@
{
"name": "usp_bookings_confirm",
"summary": "Confirm Booking",
"description": "Business-initiated confirmation for manual mode bookings. Maps to POST /bookings/{booking_id}/confirm. Privileged operation on an existing resource (Section 10.1.6): MUST authenticate as for privileged_platform, and SHOULD prefer a retained booking_scoped_credential when the business accepts that mechanism (see _meta.usp.authorization). When that credential was issued carrying cnf it is sender-constrained: a platform_key_pop proof bound to cnf.jkt is MANDATORY, and the business MUST reject the credential presented without one rather than accept it as a bearer token, whatever mechanism the caller declares.",
"description": "Business-initiated confirmation for manual-mode bookings. Maps to POST /bookings/{booking_id}/confirm (specification.md Section 5.3.4). x-usp-access: business_only. Not a platform continuation method: a platform principal and a booking_scoped_credential MUST NOT authorize this call. Authentication of the business caller is deployment-defined. Auto-mode bookings that are not already confirmed MUST be rejected with invalid_transition at HTTP 200 in messages[].",
"paramStructure": "by-name",
"params": [
{
"name": "_meta",
"required": true,
"schema": {
"type": "object",
"properties": {
"usp": {
"$ref": "#/components/schemas/McpUspMetaPrivilegedScoped"
}
}
},
"description": "MCP metadata for a privileged scoped operation (existing booking, hold, waitlist entry, or registry registration). _meta.usp.profile is REQUIRED. Authenticate per Section 10.1.6; prefer booking_scoped_credential via _meta.usp.authorization when available, together with the platform_key_pop proof in _meta.usp.authorization.proof that its cnf binding requires."
},
{
"name": "booking_id",
"required": true,
Expand Down Expand Up @@ -1841,7 +1828,7 @@
"$ref": "#/components/errors/USPProtocolError"
}
],
"x-usp-access": "privileged_scoped"
"x-usp-access": "business_only"
},
{
"name": "usp_bookings_cancel",
Expand Down
18 changes: 10 additions & 8 deletions site-docs/security.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ verifier alike:
- Verifiers **MUST** support `ES256`; signers **SHOULD** default to it. ECDSA
values use fixed-width raw `r||s` encoding, not DER.

See [Section 9.1.4](https://github.com/wix/universal-scheduling-protocol/blob/master/specification.md#914-request-signing)
See [Section 9.1.4](../specification.md#914-request-signing)
in the normative specification.


Expand All @@ -90,13 +90,15 @@ payment confirmation) **SHOULD** use an idempotency key:

This requirement applies in **both** deployment modes: it is not inherited
from UCP in UCP-Native Mode. See
[Section 10.1.6](https://github.com/wix/universal-scheduling-protocol/blob/master/specification.md#1016-platform-authentication-for-privileged-operations)
[Section 10.1.6](../specification.md#1016-platform-authentication-for-privileged-operations)
for the full normative text and rationale.

- **Public operations** (catalog, availability, profile discovery) **MAY**
remain unauthenticated.
- **Privileged operations** (booking create/update/confirm/cancel/reschedule,
holds, waitlist actions, payment-adjacent completion, registry writes, and
- **Privileged operations** (booking create/update/cancel/reschedule,
business-authenticated confirmation of a booking which a platform principal
**MUST NOT** invoke, holds, waitlist actions, payment-adjacent completion,
registry writes, and
any response carrying buyer personal data) **MUST** be authenticated.
- Every request to a privileged operation **MUST** carry a `USP-Agent` (or
`UCP-Agent`) header on REST, or `_meta.usp.profile` on MCP, that resolves to
Expand Down Expand Up @@ -125,15 +127,15 @@ for the full normative text and rationale.
compromised database can act on the booking. With it, the value alone is
useless.
- Businesses declare which mechanisms they require in an `authorization` policy
([`schemas/profile.json`](https://github.com/wix/universal-scheduling-protocol/blob/master/schemas/profile.json) `$defs/AuthorizationPolicy`
([`schemas/profile.json`](https://usp-protocol.dev/schemas/profile.json) `$defs/AuthorizationPolicy`
/ `$defs/AuthorizationMechanism`), published **top-level** in a Standalone
`/.well-known/usp` profile and as **`config.authorization` on the
`dev.usp-protocol.services` service binding** in a UCP-Native `/.well-known/ucp`
profile. USP does not add top-level members to a UCP profile document; it
declares only under its own `dev.usp-protocol.*` namespace authority, and `config` is
the member UCP defines for entity-specific settings. The same mechanism set is expressed in
[`openapi/usp-rest.json`](https://github.com/wix/universal-scheduling-protocol/blob/master/openapi/usp-rest.json) `components.securitySchemes`
and [`openrpc/usp-mcp.json`](https://github.com/wix/universal-scheduling-protocol/blob/master/openrpc/usp-mcp.json)
[`openapi/usp-rest.json`](https://usp-protocol.dev/schemas/openapi/usp-rest.json) `components.securitySchemes`
and [`openrpc/usp-mcp.json`](https://usp-protocol.dev/schemas/openrpc/usp-mcp.json)
`components.x-usp-securitySchemes`; MCP may present credentials on the HTTP
layer (when MCP is over HTTP) or via `_meta.usp.authorization`.
- On rejection, businesses **SHOULD** return `401` with `WWW-Authenticate`
Expand Down Expand Up @@ -270,7 +272,7 @@ USP is designed to minimize PCI-DSS scope for implementations:
| Buyer authorizes the charge on a trusted, deterministic surface | Inherited from UCP checkout | Required | USP spec §8.5 |
| Privileged-op authentication (some accepted mechanism) | Required | Required | USP spec §10.1.6 |
| HTTP Message Signatures (recommended default mechanism) | Available | Available | RFC 9421 |
| Booking-scoped capability credential | Available (#134, #162) | Available (#134, #162) | USP spec §10.1.6 |
| Booking-scoped capability credential | Available | Available | USP spec §10.1.6 |
| OAuth 2.0 (one accepted mechanism) | Available | Available | RFC 6749 |
| DPoP token binding | Inherited | Recommended | RFC 9449 |
| Rate limiting | Inherited | Recommended | draft-ietf-httpapi-ratelimit-headers |
Expand Down
7 changes: 5 additions & 2 deletions site-docs/specification/booking.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ The lifecycle diagram shows which statuses can follow which. It does not say whi

| Current status | `confirm` | `cancel` | `reschedule` | `confirm-payment` | `update` |
|-------------------|-----------|----------|--------------|-------------------|----------|
| `pending` | Yes | Yes | SHOULD | No | Yes |
| `pending` | Yes (manual) | Yes | SHOULD | No | Yes |
| `requires_action` | No | Yes | MAY | Yes | Yes |
| `confirmed` | Yes (idempotent) | Yes | Yes | No | Yes |
| `in_progress` | No | No | No | No | Yes |
Expand All @@ -69,6 +69,7 @@ The cells implementations diverge on:

- **`reschedule` from `pending`** is `SHOULD`. A business that does not allow it **MUST** reject with `invalid_transition` rather than silently succeeding.
- **`reschedule` from `requires_action`** is `MAY`, because an outstanding payment action may be priced against the original slot. A business that allows it **MUST** apply the reschedule price-change rules.
- **`confirm` from `pending`** is `Yes` only when `confirmation_mode` is `manual`. For an `auto`-mode booking the operation does not apply: a business **MUST** return the current booking unchanged when it is already `confirmed` (idempotent), and **MUST** reject with `invalid_transition` otherwise.
- **`confirm` from `confirmed`** is idempotent: it **MUST** return the current booking, so a retried confirmation is safe.
- **`update` on terminal statuses** is `No`. The booking is a historical record; contact and address edits **MUST NOT** be accepted on `completed`, `no_show`, or `canceled` bookings.

Expand Down Expand Up @@ -362,7 +363,9 @@ Updates mutable fields on a booking. Only `buyer`, `recipient`, and `notes` are

### Confirm Booking -- `POST /bookings/{booking_id}/confirm`

Business-initiated confirmation for bookings with `confirmation_mode: manual`. Transitions from `pending` to `confirmed`. Calling this on an `auto`-mode booking that is already `confirmed` **MUST** return the current booking state (idempotent).
Business-initiated confirmation for bookings with `confirmation_mode: manual`. A platform or buyer agent **MUST NOT** call this operation, and a `booking_scoped_credential` **MUST NOT** authorize it. Authentication of the business caller is deployment-defined.

When `confirmation_mode` is `manual` and the booking is `pending`, the business **MUST** transition it to `confirmed`. When `confirmation_mode` is `auto` and the booking is already `confirmed`, the business **MUST** return the current booking unchanged (idempotent). When `confirmation_mode` is `auto` and the booking is not already `confirmed` (including a UCP-Native unpaid booking that remains `pending`), the business **MUST** reject with `invalid_transition` at HTTP `200 OK` in `messages[]`.

| Field | Type | Required | Description |
|-------|------|----------|-------------|
Expand Down
4 changes: 2 additions & 2 deletions site-docs/transport/a2a.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ These are reachable over REST and MCP only. Implementers **MUST NOT** invent tas
| Waitlist | List Entries, Get Entry, Leave, Accept Offer, Decline Offer |
| Registry | Register, Search Businesses, Search Services, Get, Update, Delete Registration |

In practice an A2A-only agent cannot complete a manual-confirmation booking (no Confirm Booking), cannot act on a waitlist offer it was notified about (no Accept or Decline), and cannot participate in registry-based discovery at all.
In practice an A2A-only agent cannot confirm a manual-mode booking, because Confirm Booking is business-only and has no A2A task type, cannot act on a waitlist offer it was notified about (no Accept or Decline), and cannot participate in registry-based discovery at all.

## End-to-End Booking Flow via A2A

Expand Down Expand Up @@ -95,7 +95,7 @@ Each task in the chain carries the A2A conversation context, enabling the busine

## Observability Join

Participating A2A agents carry the optional money-path join id via the `USP-Correlation-Id` HTTP header. See [Section 9.7](https://github.com/wix/universal-scheduling-protocol/blob/master/specification.md#97-observability-join-non-normative-recommendation). Omitting it is conformant USP.
Participating A2A agents carry the optional money-path join id via the `USP-Correlation-Id` HTTP header. See [Section 9.7](../../specification.md#97-observability-join-non-normative-recommendation). Omitting it is conformant USP.

## Agent Card

Expand Down
Loading
Loading