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: 56 additions & 1 deletion MIGRATION.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,58 @@
# Migration Guide

- [0.9.0 → 0.9.1](#migrating-from-090-to-091) — command authorisation requirements; schema selection relaxed
- [0.8.x → 0.9.0](#migrating-from-08x-to-090) — BSP → BEST rename; conformant CloudEvents 1.0 profile

---

# Migrating from 0.9.0 to 0.9.1

Spec 0.9.1 changes **no wire format** — hence a patch bump. It **relaxes** one requirement and **adds** normative requirements around authorising commands. Nothing a 0.9.0 client sends becomes invalid, and no client change is needed; some 0.9.0 *servers* will find they were relying on behaviour the spec now names as wrong.

## Relaxed — schema selection

0.9.0 required servers to select the validation schema "keyed by the command `type` field". 0.9.1 requires only that the schema come from the server's **own catalogue**, and permits the lookup to be keyed by `type` **or** by the schema name carried in `dataschema`.

This restores the intent of the original requirement. The security control was always *don't dereference a caller-supplied URI* — not *which server-owned identifier keys the lookup*. `dataschema` is best understood as a **selector, not a location**.

- A server already keying on `type` is **unchanged and still conformant**. No action needed.
- A server that wants to key on the catalogue schema name may now do so.
- Both MUST-NOT-fetch rules are unchanged and still apply.

## Added — one identifier, or a total mapping

| # | Requirement | Why it is new |
|---|---|---|
| 1 | Schema selection, authorisation, and dispatch **MUST** key on the same identifier | Authorising under one identifier while validating or dispatching under another is a confused-deputy vulnerability |
| 2 | A derivation between `type` and `schema` **MUST** be total and unambiguous over the catalogue, or the identifier **MUST** be resolved once and reused | PascalCase↔kebab-case transforms are commonly lossy; the failure is silent |
| 3 | Where `type` and `dataschema` disagree, a server **MUST NOT** satisfy one decision from each | Reject, or resolve from one field consistently and log the divergence |
| 4 | A command whose schema cannot be resolved **MUST** be rejected | Forwarding unvalidated is not an acceptable fallback |
| 5 | Servers **MUST** document their version policy, and not partially honour a declared version | A silently substituted version dead-ends downstream handlers |

**If you derive one identifier from the other, check the transform now.** The usual symptom is a policy error naming an operation the operator never configured — a correctly permitted command denied because the authorisation key and the schema key diverged.

## Added — command authorisation

`POST /commands` is one endpoint for every operation a service accepts, and those operations do not share a blast radius. Coarse Read/Write scopes are no longer sufficient on their own.

| # | Requirement | Level |
|---|---|---|
| 6 | Per-command policy per credential/service/role, evaluated on every submission | SHOULD |
| 7 | Policy is **deny-by-default**; an empty policy MUST NOT mean "allow everything" | MUST |
| 8 | Policy scoped to the deployment it governs (a shared policy grants prod access as a side effect of enabling a command in test) | MUST |
| 9 | `GET /commands` reflects what the authenticated caller may actually submit | SHOULD |
| 10 | Actor fields in `data` MUST NOT be treated as authenticated identity; acting-principal fields are rejected-on-mismatch or overwritten (overwriting preferred) | MUST |
| 11 | A field naming a subject other than the caller MUST have the caller's access to that subject authorised | MUST |
| 12 | Authorisation MUST NOT be inferred from the payload validating | MUST |
| 13 | Destructive/irreversible/compliance-bypassing commands SHOULD require a control beyond the credential, and that control MUST NOT be self-serviceable | SHOULD / MUST |
| 14 | A log-only approval mode MUST NOT be the default, and the active mode MUST be discoverable by operators | MUST |

## Clients and callers

No changes. `dataschema` remains required and remains the absolute catalogue URI.

---

# Migrating from 0.8.x to 0.9.0

Spec 0.9.0 makes two coordinated breaking changes: the protocol short name is renamed **BSP → BEST** (the full name *Behavioral State Protocol* is unchanged), and the message envelope becomes a **conformant CloudEvents 1.0 profile**. This page lists everything an implementation must change.
Expand All @@ -14,7 +69,7 @@ Spec 0.9.0 makes two coordinated breaking changes: the protocol short name is re
| 6 | Unknown envelope attributes | Rejected (`additionalProperties: false`) | **Ignored** — never reject a message for carrying unknown attributes |
| 7 | `source` field | Any string | URI-reference (RFC 3986) — bare names/routing keys remain valid; strings with spaces or invalid URI characters are not |

Unchanged: all capability endpoint paths (`/commands`, `/events`, `/queries`, `/subscriptions`), status codes, catalogue shapes, auth declaration, tenant routing rules, and the server-side validation model (validate by `type` against your own catalogue; never fetch the caller's `dataschema` URI).
Unchanged: all capability endpoint paths (`/commands`, `/events`, `/queries`, `/subscriptions`), status codes, catalogue shapes, auth declaration, tenant routing rules, and the server-side validation model (validate against your own catalogue; never fetch the caller's `dataschema` URI).

> **Transition alias (non-normative):** a host **may** continue serving the old `/.well-known/bsp` path and accepting relative `dataschema` values during a migration window. Only the new forms are conformant with 0.9.0; the alias is a courtesy to un-migrated clients and should be removed once callers have moved.

Expand Down
7 changes: 5 additions & 2 deletions ROADMAP.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
# BEST — Roadmap and Status

Last updated: 2026-07-22 (post spec/v0.9.0).
Last updated: 2026-07-30 (post spec/v0.9.1).

## Where the protocol stands

**v0.9.0** (current stable) was a deliberate identity-and-conformance release, executed as one breaking migration:
**v0.9.1** (current stable) is a security-surface release with no wire-format change. It fills the largest remaining gap — the spec described *how* to authenticate a caller but almost nothing about authorising an individual command, leaving the Read/Write scope table as the whole model. 0.9.1 adds deny-by-default per-command policy, actor binding (payload fields naming a principal carry no authority), and controls for destructive commands. It also **relaxes** the 0.9.0 rule that schema selection be keyed by `type`: the security control was always *don't dereference the caller's URI*, never *which server-owned identifier keys the lookup*, and the stricter wording had pushed implementations into lossy PascalCase↔kebab-case derivations that silently split authorisation from validation. See [MIGRATION.md](MIGRATION.md#migrating-from-090-to-091).

**v0.9.0** was a deliberate identity-and-conformance release, executed as one breaking migration:

- **Renamed BSP → BEST** (BEhavioral STate; the full name *Behavioral State Protocol* is unchanged). Rationale: "BSP" collides heavily inside computing (Binary Space Partitioning, Board Support Package, Bulk Synchronous Parallel); "BEST" is memorable, positions naturally against REST, and `/.well-known/best` plus the npm names were free.
- **Became a conformant CloudEvents 1.0 profile.** Every valid BEST message is a valid CloudEvents 1.0 message. The former deviations were resolved: `dataschema` is the absolute catalogue URI on the wire, `source` is a URI-reference, unknown envelope attributes are ignored rather than rejected. PascalCase `type` and JSON-only content remain as profile *restrictions*. See [design decisions](specs/design-decisions.md#cloudevents-conformance).
Expand Down Expand Up @@ -47,6 +49,7 @@ Identified during the 2026-07 review; none are scheduled yet. Ordered by expecte

| Version | Date | Highlights |
|---|---|---|
| spec/v0.9.1 | 2026-07-30 | Command authorisation requirements (deny-by-default, actor binding, high-impact controls); schema selection relaxed to any server-owned identifier; same-identifier rule for selection/authz/dispatch. No wire change |
| spec/v0.9.0 | 2026-07-22 | BSP → BEST rename; conformant CloudEvents 1.0 profile; MIGRATION.md; standards artifacts |
| spec/v0.8.1 | 2026-07-22 | Consolidated SPEC.md; example `dataschema` fixes; OpenAPI synced with spec surface |
| spec/v0.8.0 | 2026-07 | A2A transport binding removed |
6 changes: 4 additions & 2 deletions specs/agents/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,11 @@ Commands use the **CloudEvents 1.0 envelope** as wire format. The same envelope

> **BEST is a conformant CloudEvents 1.0 profile.** Every valid BEST message is a valid CloudEvents 1.0 message; BEST only *restricts* the envelope (PascalCase `type`, JSON-only content, `dataschema` required for commands). CloudEvents SDKs, brokers, and validators work with BEST traffic unchanged. See [Design Decisions — CloudEvents Conformance](/specs/design-decisions#cloudevents-conformance).

The `dataschema` field in an **incoming command** is informational metadata — it documents which schema the client used when constructing the payload. It is **not** an instruction to the server. The server selects the schema to validate against using the `type` field, by looking up that type in its own command catalogue. A well-formed client will have fetched the schema from `GET /commands` and its `dataschema` value will match what the server holds — but the server never needs to fetch it.
The `dataschema` field in an **incoming command** is a **selector, not a location** — it names an entry in the server's own command catalogue. The server validates against a schema it owns, selected either from the `type` or from the schema name `dataschema` carries; what it **MUST NOT** do is fetch the caller's URI. A well-formed client takes its `dataschema` value verbatim from `GET /commands`, so the two agree.

> **Note:** A server that fetches the caller-supplied `dataschema` URI to perform validation would be both architecturally wrong (the server owns its schema catalogue) and a security risk (caller-controlled URI fetch is an SSRF vector). See [Security Considerations](/specs/security#command-ingestion-dataschema-validation).
Servers **MUST** key schema selection, authorisation, and dispatch on the same identifier. `type` is PascalCase and the catalogue's `schema` is kebab-case; where a server derives one from the other, a transformation that is not total over its catalogue will drift these decisions apart.

> **Note:** A server that fetches the caller-supplied `dataschema` URI to perform validation would be both architecturally wrong (the server owns its schema catalogue) and a security risk (caller-controlled URI fetch is an SSRF vector). See [Security Considerations](/specs/security#command-ingestion-schema-selection).

| Field | Type | Required | Description |
|---|---|---|---|
Expand Down
6 changes: 3 additions & 3 deletions specs/design-decisions.md
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,7 @@ BEST uses the CloudEvents 1.0 envelope as its wire format for commands and event

### Why

CloudEvents 1.0 is a widely understood, well-structured envelope that LLM clients can read, reason about, and generate natively. Full conformance means BEST messages work unchanged with CloudEvents SDKs, brokers, and validators — no adaptation layer at the boundary — and BEST composes with the CNCF ecosystem instead of forking it. The earlier deviations turned out to be unnecessary: `source` as "any string" was already almost always a valid URI-reference, casing and JSON-only rules are legitimate profile restrictions, and the relative-`dataschema` portability argument was moot because servers validate against their own catalogue keyed by `type` and never dereference the field anyway.
CloudEvents 1.0 is a widely understood, well-structured envelope that LLM clients can read, reason about, and generate natively. Full conformance means BEST messages work unchanged with CloudEvents SDKs, brokers, and validators — no adaptation layer at the boundary — and BEST composes with the CNCF ecosystem instead of forking it. The earlier deviations turned out to be unnecessary: `source` as "any string" was already almost always a valid URI-reference, casing and JSON-only rules are legitimate profile restrictions, and the relative-`dataschema` portability argument was moot because servers validate against their own catalogue and never dereference the field anyway.

### Profile restrictions

Expand All @@ -274,13 +274,13 @@ CloudEvents 1.0 is a widely understood, well-structured envelope that LLM client
| `type` casing | No casing requirement | PascalCase mandated (e.g. `ProposeCounter`) | Consistency for LLM tooling; catalogue `schema` names are kebab-case, `type` is PascalCase — distinct fields with distinct purposes. |
| `datacontenttype` | Any media type | `"application/json"` only | BEST is JSON-only. Other content types would require out-of-band schema negotiation the protocol does not define. |
| `dataschema` presence | Optional | Required for commands; optional for events | Commands are validated before queuing; untyped events are a supported pattern. |
| `dataschema` value | Absolute URI | The catalogue entry's absolute URI (resolves to `GET /commands/{schema}/{version}`) | One canonical value; servers still validate from their own catalogue keyed by `type` and **never fetch** the caller-supplied URI (SSRF — see [Security](/specs/security#command-ingestion-dataschema-validation)). |
| `dataschema` value | Absolute URI | The catalogue entry's absolute URI (resolves to `GET /commands/{schema}/{version}`) | One canonical value; servers still validate from their **own** catalogue and **never fetch** the caller-supplied URI (SSRF — see [Security](/specs/security#command-ingestion-schema-selection)). |
| `source` | URI-reference, absolute recommended | Same, with an added rule: never treated as authenticated identity | Caller-declared origin; authorisation derives from credentials, not `source`. |
| Extension attributes | Producers may add them | Permitted; BEST defines none and messages must not rely on them | Consumers **must** ignore unknown attributes rather than reject — this also implements BEST's forward-compatibility rule (see [Versioning](/specs/versioning)). |

### Implication for implementers

- BEST messages may be validated with CloudEvents 1.0 validators and constructed with CloudEvents SDKs.
- Servers validate command payloads against their **own** catalogue schema selected by `type`; the wire `dataschema` is informational and is never fetched.
- Servers validate command payloads against their **own** catalogue schema, selected by `type` or by the schema name `dataschema` carries; the wire `dataschema` is a selector, never fetched.
- Consumers and servers must tolerate unknown envelope attributes (ignore, don't reject).
- CloudEvents broker integration requires no envelope adaptation.
Loading
Loading