| Field | Value |
|---|---|
| RFC | 0149 |
| Title | Machine-Contract and Version Reconciliation |
| Status | Accepted |
| Author(s) | David Tufts (@davidscotttufts) |
| Created | 2026-08-11 |
| Updated | 2026-08-16 (UQ3 resolved by measurement — one deployed 1.0.0 exists (MyndHyve tier-2), no corpus normalization; INTEROP row caveated.) 2026-08-16 (§B canonical-typo lint landed — UQ2 measured then decided: distance one on discovery-shaped root keys, spec/v1 + RFCs ≥ 0149; 0 findings; acceptance item "authoring lint" checked.) 2026-08-16 (§D example extraction landed — See Amendment record. |
| Affects | api/openapi.yaml, api/asyncapi.yaml, schemas/capabilities.schema.json, spec/v1/{capabilities,version-negotiation,profiles}.md, normative examples, corpus generators and SDK parity gates |
| Compatibility | safety-fix for machine-contract defects; editorial/additive for examples and authoring lint |
| Supersedes | Contradictory base-path, discovery-wrapper, and protocol-version examples |
| Superseded by | — |
Summary
This RFC reconciles OpenWOP's machine contracts so OpenAPI, AsyncAPI, SDKs, prose examples, discovery layout, and protocol-version grammar resolve to one canonical interpretation. It removes the duplicated OpenAPI /v1 base, validates every normative example, standardizes protocolVersion as major.minor, and adds authoring-time typo detection without closing forward-compatible server-emitted documents at runtime.
Motivation
The canonical OpenAPI server URL ends in /v1 while versioned paths also begin /v1, so generic resolution yields /v1/v1/runs. Normative capability examples still use the prohibited top-level capabilities wrapper even though RFC 0073 requires families at the document root. protocolVersion is described as semver, accepted with startsWith('1.'), and illustrated as 1.0. Validators pass because they check shape, not cross-artifact intent.
Proposal
§A — Canonical URL resolution
servers:
- - url: https://{host}/v1
+ - url: https://{host}
All canonical versioned paths remain /v1/*; /.well-known/openwop remains unversioned. A corpus gate MUST resolve every server/path pair and prove exactly one /v1 segment for versioned operations. SDK operation URLs and AsyncAPI bindings MUST match the resolved OpenAPI path.
Landed 2026-08-11 as openapi-resolved-paths.test.ts (suite 1.74.0) plus the one-line servers[].url correction. The gate was written first and observed red against the uncorrected document, then green after it — 44 versioned path keys resolved to /v1/v1/* before the fix.
The red run surfaced a second consequence this section had not anticipated: /.well-known/openwop is a path key in the same document, so the duplicated base resolved it to /v1/.well-known/openwop. A client generated from the canonical contract could not perform discovery at all, which is a worse failure than a mis-resolved operation because discovery is the bootstrap — every capability decision downstream depends on it. The gate therefore asserts unversioned resolution for /.well-known/* as a distinct leg, since a base path that silently versions the discovery route is the same defect pointing the other way.
Classified editorial rather than safety-fix on the evidence that nothing which ever worked can break: the doubled prefix names routes no host serves, and the reference SDKs already issue /v1/runs against a bare base URL, so the SDKs and the OpenAPI document disagreed and the SDKs were correct. Resolves UQ1.
§B — Discovery examples and authoring validation
Every OpenWOP discovery example MUST place canonical capability families at document root. A top-level property named capabilities in an OpenWOP discovery example MUST fail authoring CI. Server-emitted discovery remains open at runtime for additive v1 fields. Authoring lint MUST reject:
- a legacy
capabilitieswrapper; - a property within edit distance one of a canonical family unless it is namespaced
x-host-orvendor.; and - a vendor field occupying a canonical reserved name.
The runtime schema MUST NOT reject an otherwise legal unknown server-emitted property solely because an older client does not recognize it.
Wrapper lint landed 2026-08-12 as capability-example-root-layout.test.ts (suite 1.76.0), with all eight offending spec/v1 examples unwrapped. The defect was worse than "some examples are stale": several sat under headings such as "Capability advertisement (normative)", introduced by prose saying hosts "advertise it under /.well-known/openwop" — so the corpus taught, in normative voice, a document shape RFC 0073 Phase 4 grades as non-conformant. No gate caught it because a fenced example is prose to every validator in the corpus, which is the same blind spot RFC 0149 §D addresses for examples generally.
Classified editorial: RFC 0073 already made root layout the normative MUST, so this corrects examples to match a rule that has been in force since Phase 1 rather than changing one. Runtime tolerance is untouched — the lint reads no host, and the wrapper remains a legal server-emitted shape until it retires with the schema's additionalProperties tolerance at v2.0.
The historical carve-out is asserted, not assumed. Every RFCS/ example still showing the wrapper is in an RFC numbered below 0073, so those are left as the dated record of what was proposed — rewriting them would make the record lie. The lint tests that boundary, so a new post-0073 RFC introducing a wrapper fails and the exemption cannot widen into a licence.
Two further defects surfaced in the same snippets and are not fixed here, because both belong to sections still inside their own safety-fix window: idempotency.md's advertisement example is the partitionRecoveryStrategy: "last-writer-wins" value RFC 0150 §D retires, and it is fenced as ``json while containing "crossRegion": "single-region" | "best-effort" | "strict"`, which no JSON parser accepts — an example that cannot parse, which is exactly what §D's extraction requirement is for.
Positive:
{ "protocolVersion": "1.0", "idempotency": { "supported": true } }
Negative authoring example:
{ "protocolVersion": "1.0", "capabilities": { "idempotency": { "supported": true } } }
§C — Protocol version grammar
protocolVersion MUST use ASCII <major>.<minor> with no leading zero except zero itself: ^(0|[1-9][0-9])\.(0|[1-9][0-9])$. 1.0 and 1.12 are valid; 1, 1.0.0, v1.0, and 01.0 are invalid. Compatibility comparison uses the integer major as the hard boundary and minor as the additive contract level. Patch belongs to suite/SDK versions, not the spec version.
Hosts MUST advertise the highest protocol minor they implement. Consumers MUST reject a different unsupported major and MUST tolerate a higher minor under v1 additive rules while capability-gating optional behavior.
§D — Lifecycle coherence
The corpus generator MUST fail when an Accepted RFC retains an unresolved acceptance blocker not explicitly carried to a register/known-limit, or when a Stable/FINAL spec describes its owning RFC as pending acceptance. Normative examples MUST be extracted into fixtures and validated against the same schemas used by conformance.
Example extraction landed 2026-08-16 (suite 1.116.0) as normative-example-extraction.test.ts with an explicit declaration rather than a heuristic: <!-- normative-example: <name>.schema.json --> on the line above a fence marks a whole-instance example; the suite extracts and validates every declared example, and fails on any undeclared whole-instance example. Measured before landing: 183 fenced JSON blocks in spec/v1, 111 strict-JSON, 26 whole instances of a schema — those 26 are declared; the rest are fragments and stay prose. "Into fixtures" is satisfied in-process (extraction at test time against the same schema files) rather than by committing copies, so the prose stays the single source.
§E — Security
Add invariant discovery-canonical-family-no-shadow: a vendor extension MUST NOT shadow or wrap a canonical capability family in a way that changes negotiation. This is structural and server-free. No discovery example may contain credentials or tenant data.
Compatibility
The OpenAPI base correction describes the already normative endpoints and is a safety defect repair for generators; it does not move a real endpoint. Hosts that accidentally served /v1/v1/* MAY retain a non-normative redirect during migration. Example corrections and lifecycle lint are non-wire changes. The major.minor grammar codifies the dominant wire shape; the suite first warns on legacy 1.0.0, then enforces after the 90-day safety window. Runtime discovery stays open, preserving v1 forward compatibility.
Conformance
New scenarios:
openapi-resolved-paths.test.ts;openapi-asyncapi-sdk-parity.test.ts;capability-example-root-layout.test.ts;discovery-canonical-family-no-shadow.test.ts;protocol-version-grammar.test.ts; andrfc-lifecycle-coherence.test.ts.
All are server-free and always-on. Fixtures cover valid root discovery, wrapper, typo, vendor extension, version grammar, and every extracted normative example. SDK repositories consume a generated canonical operation-path manifest.
Alternatives considered
1. Remove /v1 from every OpenAPI path. Rejected: it would make path keys disagree with actual endpoints and AsyncAPI organization. 2. Close discovery with additionalProperties:false. Rejected for v1 server-emitted forward compatibility. 3. Keep semver patch in protocolVersion. Rejected: the corpus version axis is major.minor and examples/behavior use it. 4. Do nothing. Rejected: green validators produce incorrect clients and contradictory examples.
Unresolved questions
1. ~~Which generators or clients have implemented a workaround for /v1/v1?~~ Resolved 2026-08-11 — none. The reference SDKs issue /v1/ against a bare base URL, so they never consumed the OpenAPI server value; no host serves the doubled route; and no workaround was found in the corpus or the SDK repositories. This is what made the correction editorial. 2. ~~What edit-distance rule avoids false positives for legitimate extension names?~~ Resolved 2026-08-16 — measured, then decided. Plain distance-one over every fenced root object in spec/v1 + RFCS/ (218) produced six near-misses (ts/fs, agent/agents, secret/secrets, context/content, prompt/prompts, schemaVersion/schemaVersions), all keys of event or run objects. So the rule is distance one applied to discovery-shaped objects only — every root key canonical, vendor-namespaced, the legacy wrapper, or within one edit of a canonical family, and at least one canonical-or-near — in spec/v1 and RFCs numbered ≥ 0149 (older RFCs are the dated record, the wrapper leg's 0073 logic). 53 discovery-shaped objects, 0 findings; a typo-only snippet ({ "compensaton": … }) is caught. Landed as the third leg of capability-example-root-layout.test.ts with the predicate pinned. 3. ~~Is a temporary 1.0.0 normalization warning required for any deployed host?~~ Resolved 2026-08-16 by measurement: no normalization, and yes there is a deployed 1.0.0. The four reference hosts advertise 1.0 and openwop-app 1.1 (app.openwop.dev/api/.well-known/openwop, deploy 8b2cc5ab), but the tier-2 sibling api.myndhyve.ai served "1.0.0" on 2026-08-16. A consumer-side normalization (1.0.0 → 1.0) would re-open exactly the ambiguity §C closed — a consumer could no longer tell a patch convention from a typo from a different protocol — so the corpus does not add one; under the shipped predicate that host does not derive openwop-discovery-core and fails protocol-version-grammar until it advertises 1.0, and INTEROP-MATRIX.md says so on its row. The fix is one string on the host. 4. Which stale lifecycle statements are intentional historical notes rather than defects? Measured 2026-08-12 (docs/RFC-LIFECYCLE-COHERENCE.md): the acceptance-checkbox signal is inconsistent, not absent — of 141 Accepted RFCs, 42% ticked every box, 25% ticked none, 24% ticked some. A blanket gate would fail 69 RFCs, mostly for an authoring convention, and a gate that fires 69 times on its first run gets disabled rather than fixed. The 34 partial RFCs are the real triage set: someone was ticking and stopped. Triaged 2026-08-12, and the first hypothesis was wrong. The 34 partial RFCs are not a blocker backlog: reading the eight trailing items across 0027/0028/0029/0040/0041 shows every one is deliberately unticked and annotated with why — "(Will land alongside the first non-steward advertisement.)", "(Path-to-Accepted.)", "(Follow-up — … not normative gate-blockers.)". That inline annotation is §D's "explicitly carried", just carried in a parenthetical rather than a register row. Re-measured on that basis: 150 unticked items are annotated, 200 are bare — and the bare ones are dominated by items self-evidently done (0003: "Spec text merged (this file)"), clustered in the RFCs where nobody ticked anything. So the signal §D wants is annotated vs bare, not ticked vs unticked: an unticked acceptance item MUST carry its reason — an external gate, a follow-up note, or a register/known-limit pointer.* Enforceable immediately for new RFCs; 200 existing items would need annotating before it could apply retroactively. Still open: the "Stable spec describes its RFC as pending" half, whose phrasing search returned no hits (recorded as unproven, not clean).
Implementation notes (non-normative)
Land the URL-resolution test before the YAML correction to prove it red. Example extraction should preserve source file/line in failures. This RFC is SR-2 under RFC 0147.
Acceptance criteria
- [x] OpenAPI paths resolve correctly and match AsyncAPI/SDKs. (Resolution landed — §A,
openapi-resolved-paths.test.ts. 2026-08-16, suite1.114.0 -> 1.115.0: the AsyncAPI/SDK parity half landed —openapi-asyncapi-sdk-parity.test.ts: (1)spec/v1/operation-path-manifest.json, the generated canonical operation-path manifest (scripts/generate-operation-path-manifest.mjs,--checkinopenwop:check), re-derived in-test fromapi/openapi.yamland compared byte-for-byte, every row exactly one/v1; (2) every AsyncAPI channeladdressresolves under the AsyncAPI serverpathnameto a GET path key of the OpenAPI document with exactly one/v1; (3) the SDK half reaches intoopenwop-sdkswhen reachable (OPENWOP_SDKS_DIRor the sibling checkout):sdk/parity-expectations.jsoncovers every canonical operation with the same method + resolved path, declares no orphan, and the TypeScript host client (client.ts+sse.ts) issues every ts-typed path and no/v1path outside the contract or/v1/host/*. When the SDK tree is not reachable the requirementopenwop.requirement.rfc0149.sdk-path-parityis recordedblockedwith the reason (RFC 0148 §A) rather than passing vacuously — that is the honest shape of a cross-repository requirement in a single-repository runner. Witnessed green against the localopenwop-sdkscheckout: 47 canonical + 5 host-extension + 4 test-catalog operations, zero drift.) - [x] All normative discovery examples use root layout and validate. (Root layout landed — §B, all eight
spec/v1examples unwrapped. "And validate" landed 2026-08-16, suite1.115.0 -> 1.116.0—normative-example-extraction.test.ts: a fenced example becomes a validated instance by declaring its schema with<!-- normative-example: <name>.schema.json -->on the line above the fence; every declared example is extracted at test time and validated againstschemas/<name>with the same Ajv registration the rest of the suite uses, and every fenced block that validates as a whole instance of some non-permissive schema MUST be declared (the inverse guard — sabotage-verified both ways). 26 whole-document examples across 19spec/v1docs declared in the same change (capabilities,error-envelope×14, the six pack-manifest kinds,run-snapshot,prompt-ref,localized-content-section,audit-verify-result); the one whole discovery example (capabilities.md) is declared againstcapabilities.schema.json, validates, and carries no credential/tenant keys (§E leg). Extraction is in-process rather than committed underfixtures/— a committed copy of a prose example is a second source of truth that drifts, which is what §D exists to remove;CONTRIBUTING.md§Prose specs states the authoring rule.) - [x] Authoring lint catches wrappers, canonical typos, and shadows without closing runtime discovery. (Wrappers landed §B; shadows landed §E; canonical typos landed 2026-08-16 — UQ2 resolved by measurement (see above), third leg of
capability-example-root-layout.test.ts, discovery-shaped predicate pinned, 0 findings in scope. Runtime discovery untouched: the lint reads the corpus, not a host.) - [x] Version grammar landed — §C, enforced in the schema
pattern,profiles.ts, andversion-negotiation.md. No migration warning phase: with no deployed hosts, the steward's call was to enforce directly rather than warn-then-enforce, so the 90-day step this item anticipated does not apply. - [ ] Lifecycle coherence gate passes across the corpus. (Landed for RFCs >= 0147 —
rfc-lifecycle-coherence.test.ts. Deliberately not retroactive: 200 bare items across earlier RFCs would need annotating first, and a gate that fires 200 times on its first run gets disabled rather than fixed. Seedocs/RFC-LIFECYCLE-COHERENCE.md.) - [x] CHANGELOG, SDK path manifest, and version-negotiation runbook updated. (CHANGELOG and runbook landed. 2026-08-16: the SDK path manifest landed on the corpus side as
spec/v1/operation-path-manifest.json— published FROM the corpus for SDK repositories to consume, rather than generated INTOopenwop-sdks; the consuming half isopenwop-sdkssdk/parity-expectations.json, whichopenapi-asyncapi-sdk-parity.test.tschecks against the manifest when the repository is reachable.rest-endpoints.md§Versioning documents the manifest.)
Amendment record
Change history relocated from the Updated metadata cell (newest first).
normative-example-extraction.test.ts, 26 examples declared, suite1.116.0; AsyncAPI/SDK parity half landed —openapi-asyncapi-sdk-parity.test.ts+spec/v1/operation-path-manifest.json, suite1.114.0 -> 1.115.0; see the acceptance items).- 2026-08-12 (
Active->Accepted; 7-day comment window waived by the steward perMAINTAINERS.md§"Bootstrap-phase RFC waivers". - Landed: §A canonical URL resolution — 44 operations resolved
/v1/v1/*, plus discovery itself; fixed withopenapi-resolved-paths.test.ts(#942). §B wrapper lint with all eight normative examples unwrapped,capability-example-root-layout.test.ts(#945). §D feasibility measured and the checkbox hypothesis corrected (#946, #948). - Suite
1.76.0. §CprotocolVersiongrammar — the field was specified three incompatible ways at once (schemaminLength: 1, predicatestartsWith('1.'), prose calling it semver), so"1.0.0"both validated AND derivedopenwop-core; now^(0\|[1-9][0-9])\.(0\|[1-9][0-9])$in the schema,profiles.ts, andversion-negotiation.md§"Protocol version grammar", with a leg asserting the schema pattern and the predicate are the same string. - Closes gap V2, open since v1.0. Suite
1.79.0. §Ediscovery-canonical-family-no-shadow— a canonical family name nested insidex-host-/vendor./private.*shadows what a consumer negotiates on;host-extensions.md's opacity rule binds the consumer, not the host publishing the collision. - Corpus was already clean, so it landed sabotage-verified as a guardrail. Suite
1.80.0. §D's lifecycle gate —rfc-lifecycle-coherence.test.ts, enforcing annotated-vs-bare onAcceptedRFCs >= 0147, with all 67 bare items across 0147-0156 annotated to land it and a leg asserting the rule binds RFC 0149 itself. - Suite
1.81.0. Carried forward, not closed: §D's example-extraction clause (measured; the annotated-vs-bare rule is recommended, not enforced), and §E'sdiscovery-canonical-family-no-shadowinvariant.).
References
- RFC 0073, RFC 0094, RFC 0144, RFC 0146
spec/v1/capabilities.md§Document-root layoutspec/v1/version-negotiation.mdCOMPATIBILITY.md§§2–3- RFC 0147 Workstream 2