Status: Stable.
Why this exists
How a v2 host selects a major, what each version axis means, and what a release is.
1. Major negotiation
1.1 Advertisement
A v2 host MUST advertise two root fields, both REQUIRED in schemas/v2/capabilities.schema.json (capabilities.md):
protocolVersions[]— every<major>.<minor>it serves, each matching^(0|[1-9][0-9])\.(0|[1-9][0-9])$. Through the overlap that is["1.<n>", "2.<m>"]; after v1 end-of-support,["2.<m>"].preferredVersion— MUST be a member ofprotocolVersions[]. Through the overlap it MUST name a 1.x member (capabilities.md §1; §1.3). On a host serving a single major, it MUST equalprotocolVersion.
A host that drops v1 advertises a 2.x preferredVersion; its header-less representation becomes the closed v2 root.
A v2 consumer reads preferredVersion as the header-less default; absent on a v1 document, the default is max(protocolVersions[]), else protocolVersion.
1.2 Paths
- v1 operations keep their
/v1/…path keys unchanged through the overlap. - v2 operations are unversioned path keys on a bare origin (
servers[].url = https://{host}):/runs,/runs/{runId},/.well-known/openwop. There is no/v2/path space. - An unversioned path is the v2 surface; v1's rule that it answers
400does not apply.
Advertising a major is a claim about the whole path space, not about /.well-known/openwop alone (§1.3 selects that resource's representation):
- A host that advertises a major in
protocolVersions[]MUST reach, under that major, every operation named inspec/v2/path-manifest.jsonthat it serves under the other. - If
/v1/<op>answers and the unversioned/<op>returns404under the advertised major, the host MUST NOT advertise that major until the surface is reachable. - Seam and proprietary paths are not manifest operations and need no per-major twin.
spec/v2/path-manifest.json (generated) lists unversioned operations (method, path, operationId) and channels (name, address); each row's /v1 twin is derived by prefixing.
OpenAPI (api/v2/openapi.yaml), AsyncAPI (api/v2/asyncapi.yaml) and any kept proto MUST resolve to identical absolute paths for the shared event stream (scripts/check-path-parity.mjs). Seams are in conformance.md.
1.3 The request header
A request on an unversioned path MAY carry OpenWOP-Version: <major> or OpenWOP-Version: <major>.<minor>. 2 and 2.0 select the same major, and a host MUST accept both. Only the major selects: a minor in the header is informational. What pins a minor is minClientVersion plus the additive rules (COMPATIBILITY.md §2.4).
| Condition | Host behavior |
|---|---|
Header names a major in protocolVersions[] | MUST serve that major |
Header names a major not in protocolVersions[] | MUST answer 406 protocol_version_unsupported, with details.protocolVersions[] echoing the list |
Header absent on /.well-known/openwop | MUST serve preferredVersion's major |
| Header absent on any other unversioned path | MUST serve major 2: the path is the v2 surface (§1.2) |
/v1/… path with OpenWOP-Version other than 1 | MUST answer 400 protocol_version_mismatch |
A request on a /v1/… path key MUST NOT carry OpenWOP-Version with a value other than 1. protocol_version_unsupported, protocol_version_mismatch and client_version_unsupported (§1.5) are rows in spec/v2/errors.json (errors.md).
1.4 The response header
Every protocol response MUST carry OpenWOP-Version: <major>.<minor> naming the contract that produced it, /v1/ responses included (REQUIRED in v2). Reporting another version is a silent downgrade (scenario dual-stack-negotiation).
A protocol response is one produced by an operation in spec/v2/path-manifest.json (or its /v1/ twin through the overlap); a shell, hosting fallback, seam or proprietary route is not one.
On a manifest-named path:
- A non-protocol response MUST NOT carry
OpenWOP-Versionand MUST NOT beapplication/json. - A reader, a cache or the suite MUST NOT count a response without the header, or with a
text/htmlbody, as reaching the operation (reachedUnderMajor2).
A vendor path (§5) is not a shared name and is unconstrained.
Content negotiation on a shared name
A host MAY serve a protocol operation and a page under one unversioned name, selecting on Accept, iff:
- A request identifying as a protocol client —
OpenWOP-Versionpresent, or anAcceptadmittingapplication/jsonwithout preferringtext/html(absent and/included) — MUST get the protocol response for the applicable major (§1.3) withOpenWOP-Version. Only an explicittext/htmlpreference selects the page. - The page obeys the rules above.
- The response carries
Vary: Accept, OpenWOP-Version.
Otherwise the page MUST move off the shared name.
1.5 Client precedence and minClientVersion
When both majors are advertised, a v2 client MUST select the highest major it implements that the host lists. A v1 client (no header, /v1/ paths) is unaffected.
A client announces the corpus release it is built against (§4), not an SDK or product version, in the OpenWOP-Client-Version request header, as <major>.<minor> or <major>.<minor>.<patch> (non-negative integers, no leading zeros).
- A client SHOULD send it on every request under major 2.
- The header is optional on every operation and never selects a major (§1.3). A host MUST NOT choose a major or a representation from it.
- A host MUST NOT refuse a request because it omits the header or sends a malformed value.
- A value outside the grammar MUST be treated as absent, and MUST NOT produce a
400. - The value is the client's claim. A host MUST NOT use it as an authentication or authorization input.
minClientVersion (axis 15) is optional. When a host advertises it:
- It MUST use the axis-1 grammar.
- A client is below it when the client's major and minor, compared as integers, are less than the floor's. The patch never decides.
- A host MAY refuse a client below it. A refusal MUST be
426client_version_unsupported, and MAY carrydetails.minClientVersionnaming the floor (errors.md). - The discovery document is not exempt.
A host MUST NOT answer 426 client_version_unsupported to a request that does not carry a well-formed OpenWOP-Client-Version below its advertised minClientVersion.
This header rule binds requests served under major 2; major 1 follows its frozen text.
Open gap: RFC 9110 §15.5.22 requires an Upgrade header on every 426. This floor departs from it, and no Upgrade value is defined yet.
2. The 18 version axes
Dispositions:
unify— one type and grammar, with a codemod.first-class— own schema-enforced grammar and negotiation rule.retire— absorbed into the capability record's{status, since, until?}.delete— removed, with a register row.
| # | Axis | Disposition | v2 grammar | Owner |
|---|---|---|---|---|
| 1 | protocolVersion | first-class; preferredVersion's twin for v1 readers through the overlap, removed after | ^(0|[1-9][0-9])\.(0|[1-9][0-9])$ | this document |
| 2 | protocolVersions[] + preferredVersion | first-class, negotiation input | as #1 | this document |
| 3 | engineVersion | unify: integer everywhere; codemod openwop.codemod.engine-version-unify | integer, minimum 0 | this document |
| 4 | eventLogSchemaVersion | first-class, the era key | integer; v2 writes 3 | persistence.md |
| 5 | per-event schemaVersion | first-class; §0 growth rule | integer | events.md |
| 6 | schemaVersions map | delete | — | capabilities.md |
| 7 | version.pinned | first-class; the v1-pinned-run disposition | integer min/max | persistence.md |
| 8 | contractProvenance | delete | — | capabilities.md |
| 9 | minimumSuiteVersion | retire into spec/v2/declaration.json | semver | capabilities.md |
| 10 | bundleVersion | unify to one const family: certification v3 "3", export "2", debug "2" | string const | conformance.md |
| 11 | A2A versions[] / preferredVersion | first-class facet of a2a | ^[0-9]+\.[0-9]+$ | interop.md |
| 12 | MCP revisions[] / preferredVersion | first-class facet of mcp | date | interop.md |
| 13 | multiAgent.executionModel.version | first-class | integer with a schema maximum the suite reads | events.md |
| 14 | OpenAPI / AsyncAPI info.version | generated from the corpus tag | semver | this document |
| 15 | minClientVersion | first-class (§1.5) | as #1 | this document |
| 16 | channel schemaVersion / compatibleWith | first-class | integer / range | events.md |
| 17 | webhook signature scheme | retire into deprecations.json | — | webhooks.md |
| 18 | pack engines.openwop + registryVersion | first-class with the absent-ceiling rule | semver range / semver | packs.md |
- One grammar covers protocol and pack axes wherever a version is
<major>.<minor>(#1, #2, #11, #15). typeId@<semver>is a pack axis (packs.md); the2intypeId@2.0.0never meansOpenWOP-Version: 2.
2.1 engineVersion (axis 3)
engineVersionMUST be an integer (minimum 0) at the discovery root and on every per-event carrier.- A persisted v1 run document that carries the string form is legacy-stamped: the reader MUST normalise it to an integer and MUST NOT rewrite the stored document.
- The codemod
openwop.codemod.engine-version-unifyMUST refuse any value not matching^(0|[1-9][0-9]*)$.
2.2 eventLogSchemaVersion (axis 4)
eventLogSchemaVersion is the era key; the schema floor is minimum 2. Its stamping, absent-⇒-2, discovery and reader rules are in persistence.md §"The era key".
3. Where v2 lives
spec/v2/core/andspec/v2/ext/<key>/hold the prose.schemas/v2/holds every v2 schema, with$idunderhttps://openwop.dev/spec/v2/; the site publishes them at/spec/v2/.- The flat
schemas/tree is read-only; its v1$ids are immutable, so a domain move is answered by a redirect, never a rewrite. - AsyncAPI
servers.production.pathnameis empty; every channel address carries its own path, as OpenAPI path keys do.
4. One release identity
The corpus tag v2.<minor>.<patch> (release candidates v2.0.0-rc.<n>) is the only release event; suite, SDKs, registry and site derive from it. spec/v2/release.json carries the next tag as version, bumped only by the release PR.
- Every human-surface version (README banner,
docs/PROTOCOL-STATUS.md, OpenAPI and AsyncAPIinfo.version,conformance/package.json) MUST be generated from it and checked with--checkin the merge gate. - The published tarball digest MUST equal the tree's as a release precondition.
- The identity and advertised-versions checks keep their three-outcome discipline (conformance.md).
- A consumer that vendors any file from
schemas/,api/orspec/MUST pin to a published tag, record it, and refuse a sync from any other ref. - A v1.x consumer MUST NOT vendor
schemas/v2/.
5. The overlap
Through the overlap a host:
- MUST advertise both majors (§1.1);
- MUST emit
OpenWOP-Versionon every response (§1.4); - MUST serve
/.well-known/openwopas one resource whose representation the request header selects (capabilities.md).
A run minted under major 1 and read under major 2 MUST use the tenant-bound projection <tenantId>/<v1-id> (identity.md §5). A host MUST NOT return a bare v1 id in a major-2 response.
Retirement
The overlap ends at v1 end-of-support (overview.md). From then a host MAY drop the 1.<n> member from protocolVersions[]; when it does, every alias carrying the v1-end-of-support trigger goes with it.
- Retirement is atomic. Dropping v1 retires the whole
/v1path space at once (§1.1). - Retirement changes every header-less request's default contract, from major 1 to major 2. Before retirement, a host MUST check for collisions between manifest top-level path segments and non-protocol unversioned routes, and MUST move each colliding route or apply §1.4 content negotiation.
Host-proprietary paths: /host/<org>/…
Every vendor namespace — capability records (capabilities.md §3.2), error codes, event types, pack properties, paths — is keyed to an org registered in spec/v2/declaration.json.
- A host MAY serve operations the manifest does not name under
/host/<org>/…for its registered org. Such a path has no major, is served regardless ofOpenWOP-Version, is never a protocol operation, is never measured, and is outside §1.4. - An org MUST NOT be named after a manifest segment under
/host/(reservedOrgs). - A host SHOULD advertise the mount under
extensions.<org>.<name>. - A
/v1/host/<org>/…twin MAY ride the overlap; it retires atomically with/v1. - After retirement a host MUST NOT serve an operation at the twin. It MAY answer a
GETorHEADthere with308, no body, andLocationset to the same/host/<org>/…path.
6. Migration rows
Rows C5.1–C5.9 are spec/v1/migrations.json entries (C5.2 is owned by events.md). The persisted-data disposition for each is not-persisted, except C5.1 (legacy-stamped) and C5.7 (never-upgraded).
Sources: RFCs 0167, 0168, 0172, 0176, 0179, 0181, 0193, 0219, 0255.