OpenWOP openwop.dev

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 of protocolVersions[]. Through the overlap it MUST name a 1.x member (capabilities.md §1; §1.3). On a host serving a single major, it MUST equal protocolVersion.

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 400 does 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 in spec/v2/path-manifest.json that it serves under the other.
  • If /v1/<op> answers and the unversioned /<op> returns 404 under 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).

ConditionHost 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/openwopMUST serve preferredVersion's major
Header absent on any other unversioned pathMUST serve major 2: the path is the v2 surface (§1.2)
/v1/… path with OpenWOP-Version other than 1MUST 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-Version and MUST NOT be application/json.
  • A reader, a cache or the suite MUST NOT count a response without the header, or with a text/html body, 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:

  1. A request identifying as a protocol client — OpenWOP-Version present, or an Accept admitting application/json without preferring text/html (absent and / included) — MUST get the protocol response for the applicable major (§1.3) with OpenWOP-Version. Only an explicit text/html preference selects the page.
  2. The page obeys the rules above.
  3. 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 426 client_version_unsupported, and MAY carry details.minClientVersion naming 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.
#AxisDispositionv2 grammarOwner
1protocolVersionfirst-class; preferredVersion's twin for v1 readers through the overlap, removed after^(0|[1-9][0-9])\.(0|[1-9][0-9])$this document
2protocolVersions[] + preferredVersionfirst-class, negotiation inputas #1this document
3engineVersionunify: integer everywhere; codemod openwop.codemod.engine-version-unifyinteger, minimum 0this document
4eventLogSchemaVersionfirst-class, the era keyinteger; v2 writes 3persistence.md
5per-event schemaVersionfirst-class; §0 growth ruleintegerevents.md
6schemaVersions mapdelete—capabilities.md
7version.pinnedfirst-class; the v1-pinned-run dispositioninteger min/maxpersistence.md
8contractProvenancedelete—capabilities.md
9minimumSuiteVersionretire into spec/v2/declaration.jsonsemvercapabilities.md
10bundleVersionunify to one const family: certification v3 "3", export "2", debug "2"string constconformance.md
11A2A versions[] / preferredVersionfirst-class facet of a2a^[0-9]+\.[0-9]+$interop.md
12MCP revisions[] / preferredVersionfirst-class facet of mcpdateinterop.md
13multiAgent.executionModel.versionfirst-classinteger with a schema maximum the suite readsevents.md
14OpenAPI / AsyncAPI info.versiongenerated from the corpus tagsemverthis document
15minClientVersionfirst-class (§1.5)as #1this document
16channel schemaVersion / compatibleWithfirst-classinteger / rangeevents.md
17webhook signature schemeretire into deprecations.json—webhooks.md
18pack engines.openwop + registryVersionfirst-class with the absent-ceiling rulesemver range / semverpacks.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); the 2 in typeId@2.0.0 never means OpenWOP-Version: 2.

2.1 engineVersion (axis 3)

  • engineVersion MUST 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-unify MUST 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/ and spec/v2/ext/<key>/ hold the prose.
  • schemas/v2/ holds every v2 schema, with $id under https://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.pathname is 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 AsyncAPI info.version, conformance/package.json) MUST be generated from it and checked with --check in 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/ or spec/ 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-Version on every response (§1.4);
  • MUST serve /.well-known/openwop as 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 /v1 path 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 of OpenWOP-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 GET or HEAD there with 308, no body, and Location set 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.

Next in Core.

Edit this page on GitHub ↗

  • Discovery and CapabilitiesA host advertises what it supports in one discovery document: one record type per family…
  • Persistence and CoexistenceHow a v2 host reads what a v1 host wrote, handles a run in flight at the cut, and maps…
  • IdentityWho owns a run, how each lane binds and revokes, and the grammar of links, ids and…
  • OpenWOP v2 Core — Overviewspec/v2/core/ is what a host implements to pass the 2.0.0 floor: reading order, the six…