@openwop/openwop-conformance
v1.141.0
Published
Production-ready black-box conformance suite for OpenWOP v1.0 compliant servers.
Readme
@openwop/openwop-conformance — Conformance Suite for the Multi-Agent Workflow Orchestration Protocol
openwop is an open, wire-level protocol for multi-agent workflow orchestration — a single contract for runs in which LLM agents, deterministic tools, sub-workflows, and human reviewers collaborate, with durable suspend / resume, replay, version negotiation, and observability owned by the protocol itself. This package is the black-box conformance suite: point it at any OpenWOP-compliant server (your own or a third party's) and it issues real HTTP requests against the spec'd endpoints and asserts that responses match.
npm install @openwop/openwop-conformance
# or run without install:
npx @openwop/openwop-conformance --base-url https://api.example.com --api-key hk_test_...Spec: github.com/openwop/openwop · See
CHANGELOG.mdbelow for release history.
The suite is intentionally self-contained — it does NOT depend on the reference implementation. A spec-compliant server written in any language can run this suite against itself by spinning up its server, exporting the env vars, and running npx vitest run.
Status: Tracks the FINAL v1 protocol contract. The suite version evolves independently as new scenarios ship (vendor-neutral redaction, cost attribution, post-v1 ecosystem triggers); see
CHANGELOG.mdfor the current release.
Quickstart
Two ways to run: the friendly openwop-conformance CLI (recommended for
operators) or vitest directly (recommended for CI).
CLI
cd conformance
npm install
# Build the CLI binary
npm run build:cli
# Server-free subset (no deployment target needed)
./dist/cli.js --offline
# Full suite against a deployed server
./dist/cli.js \
--base-url https://api.example.com \
--api-key hk_test_abc123 \
--impl acme-openwop-server --impl-version 1.0
# Filter by test-name pattern
./dist/cli.js --base-url ... --api-key ... --filter "discovery|errors"./dist/cli.js --help for the full flag reference. Env vars
(OPENWOP_BASE_URL, OPENWOP_API_KEY, OPENWOP_IMPLEMENTATION_*) override CLI
flags only when the flag is unset.
Direct vitest
cd conformance
npm install
export OPENWOP_BASE_URL="https://api.example.com"
export OPENWOP_API_KEY="hk_test_..."
npx vitest run # full suite (parallel files, ~95s)
npx vitest run src/scenarios/discovery.test.ts # single file
npm run test:strict # full suite, no-file-parallelismtest:strict vs test. The default test script runs files in parallel (vitest default, ~3-5× faster). Most scenarios are isolation-safe at that level. Two exceptions document --no-file-parallelism as their canonical execution mode:
production-backpressure.test.ts— saturates the host'sinflightCap; under parallel execution, neighbor tests posting/v1/runsduring the saturation window see a 503 from the cap (cap-collateral). The scenario soft-skips its envelope assertions when this happens (logs a warning);test:strictexercises the full envelope contract.- OTel scenarios (
otel-emission.test.ts,otel-trace-propagation.test.ts,metric-emission.test.ts) — each vitest worker spawns its own collector and only one can bind the configured OTLP port; concurrent file execution causes ephemeral-port fallbacks that don't receive the host's traffic.
Run npm run test for normal CI cadence; npm run test:strict when claiming full envelope coverage for production-profile + OTel claims.
Optional environment flags
| Variable | Effect |
| ----------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| OPENWOP_REQUIRE_BEHAVIOR=true | Capability-gated scenarios (audit-log integrity, rate-limit envelope, multi-region idempotency, configurableSchema, webhook sig versioning, etc.) FAIL instead of skipping when the host doesn't advertise the profile. Lets a host claim "full coverage" mechanically. See coverage.md §"Capability-gated scenarios". |
| OPENWOP_TEST_PUBLIC_REGISTRY=true | Runs registry-public.test.ts against the hosted registry at packs.openwop.dev. Skipped by default so the suite doesn't depend on outbound connectivity. |
| OPENWOP_OTEL_COLLECTOR=true | Boots the in-suite OTLP collector for otel-emission.test.ts, otel-trace-propagation.test.ts, metric-emission.test.ts, and otel-emission-grpc.test.ts. The collector accepts OTLP/HTTP-JSON (application/json), OTLP/HTTP-protobuf (application/x-protobuf — hand-rolled decoder at src/lib/otlp-protobuf.ts), and OTLP/gRPC (when OPENWOP_OTEL_COLLECTOR_GRPC=true — h2c HTTP/2 + hand-rolled framing at src/lib/grpc-framing.ts). Zero new npm deps. Hosts may emit via OTEL_EXPORTER_OTLP_PROTOCOL=http/json, http/protobuf, or grpc. Skipped by default. Run OTel scenarios with --no-file-parallelism — each vitest worker spawns its own collector and only one can bind the same port, so concurrent file execution causes ephemeral-port fallbacks that don't receive the host's OTLP traffic. |
| OPENWOP_OTEL_COLLECTOR_GRPC=true | Boots the parallel OTLP/gRPC collector alongside the HTTP one (h2c HTTP/2 on a separate port). Shares the same spans() + metrics() store; spans captured over either transport surface in getCollector().spans(). Requires OPENWOP_OTEL_COLLECTOR=true. Same --no-file-parallelism requirement applies. |
| OPENWOP_OTEL_COLLECTOR_GRPC_PORT=4317 | Bind the OTLP/gRPC collector on a specific port (default 4317, OTLP/gRPC convention). The host MUST be configured with OTEL_EXPORTER_OTLP_ENDPOINT=http://127.0.0.1:<port> AND OTEL_EXPORTER_OTLP_PROTOCOL=grpc. |
| OPENWOP_OTEL_COLLECTOR_PORT=14318 | Bind the OTel collector on a specific port (default 4318). The host MUST be configured with OTEL_EXPORTER_OTLP_ENDPOINT=http://127.0.0.1:<port>. |
| OPENWOP_WEBHOOK_ALLOW_PRIVATE=true | Relaxes the webhook egress guard for the loopback test receiver used by webhook-signed-delivery.test.ts, webhook-negative.test.ts, and replay-fanout-suppression.test.ts. The receiver is http://127.0.0.1:{port}/, which webhooks.md forbids three separate times, and the opt-in MUST relax all three to be witnessable: (1) the scheme check — §"SSRF protection" bullet 1 rejects non-https://, and this gate fires FIRST on a host that validates scheme before address; (2) the registration-time address check — §"SSRF protection"; and (3) delivery-time re-resolution — §"Delivery-time egress validation (RFC 0093)". Gates 2 and 3 are independent MUSTs at different layers, so an opt-in reaching only one layer cannot produce a witness: delivery-only leaves registration returning 400 webhook_url_rejected, registration-only leaves the dispatcher refusing to connect. A host whose opt-in reaches one layer is not non-conformant — it cannot witness these scenarios, which is a property of the test posture and not of its webhook signing. The scheme gate went unstated here until 2026-08-25; a tier-2 host validating scheme-then-address was blocked by a gate the documented contract never named. Relaxing gates 2 and 3 is test-only posture — see SECURITY/threat-model-secret-leakage.md §4.9 for why a registration-time relaxation is the more dangerous of the two. When the host rejects, the scenario records blocked (RFC 0148 §A), not a pass. |
| OPENWOP_WEBHOOK_RECEIVER_URL=<https-url> | The route to a webhook witness that relaxes nothing. A host refusing the loopback receiver on two independent grounds — private address and non-https: — cannot be unblocked by OPENWOP_WEBHOOK_ALLOW_PRIVATE, because the scheme arm still stands. This supplies the public https: front for THIS SUITE'S OWN receiver (tunnel / TLS-terminating proxy), never an arbitrary endpoint: the scenario asserts on what this process received, so pointing registration elsewhere makes every header assertion vacuous while the row turns green. Zero deliveries with it set is a hard failure, never a skip. Preferred over the flag — it waives nothing and writes no durable subscription row aimed at a private address (SECURITY/threat-model-secret-leakage.md §4.9). Pair with OPENWOP_WEBHOOK_RECEIVER_PORT. |
| OPENWOP_WEBHOOK_RECEIVER_PORT=<port> | Pins the in-process webhook receiver to a known port instead of an ephemeral one. Required in practice by OPENWOP_WEBHOOK_RECEIVER_URL: a tunnel must be aimed at a port known in advance, and the receiver binds 0 by default. Unset ⇒ ephemeral, as before. |
| OPENWOP_MCP_FAKE_SERVER=true | Boots the synthetic MCP peer for mcp-tool-roundtrip.test.ts. |
| OPENWOP_MCP_REAL_SERVER_URL=<base-url> | Points the MCP wire-shape probe at a real MCP server. The probe POSTs JSON-RPC and reads a single-JSON response — matches MCP's streamable-http transport in single-response mode. Does NOT support stdio transport (which is what most modelcontextprotocol/servers references default to) or SSE-streamed responses; an operator collecting interop evidence today runs a custom StreamableHTTPServerTransport-style server that returns a single JSON body per request. Adding SSE-frame parsing is tracked in docs/PROTOCOL-GAP-CLOSURE-PLAN.md Track 6. Assertions relax to shape-only. When both this and OPENWOP_MCP_FAKE_SERVER are set, the real URL wins. Phase 3 T3.4 interop-evidence path. |
| OPENWOP_A2A_FAKE_PEER=true | Boots the synthetic A2A peer for a2a-task-roundtrip.test.ts. |
| OPENWOP_A2A_REAL_PEER_URL=<base-url> | Points the A2A AgentCard + task-lifecycle probe at a real reference A2A peer. Drift-point subtests (AUTH_REQUIRED / REJECTED) stay fake-peer-only — real peers don't expose a state-forcing API. Phase 3 T3.4 interop-evidence path. |
| OPENWOP_FORCE_RATE_LIMIT=true | Signals the host (test-only key) to fabricate a 429 so rate-limit-envelope.test.ts can exercise envelope shape deterministically. |
| OPENWOP_TEST_PROMPT_PACK_INSTALLED=true | Promotes the prompt-pack-install.test.ts existence claim from soft-skip to hard assertion (RFC 0028 §B). Set when the target host is known to have at least one prompt pack installed at boot — RFC 0028 §B does NOT require any host with endpointsSupported: true to have packs installed, so the suite stays conformant against a fresh production host with no pack subscriptions when the flag is unset. The in-tree workflow-engine sample auto-installs vendor.openwop.prompt-sample via promptPackLoader.ts, so operators running against it should set the flag. |
Exit code is non-zero on any failed assertion. --certify distinguishes: 0 — every claimed profile certifiable; 1 — assertion failures; 3 — rejected (RFC 0148 §A): a claimed profile has an unclassified return — a floor/prefix requirement with no ledger row while the ledger is present, or an executed-pass with assertionCount: 0. The bundle is still written on 3 so the rejection is auditable; the reasons are printed per profile. Every scenario file records its file-level disposition (with assertion count) into <report-dir>/requirement-ledger.jsonl via OPENWOP_LEDGER_PATH, which the runner sets for you. A zero-assertion file that noted no reason (softSkip / seamAbsent / behaviorGate) is recorded blocked with a fixed marker rather than as a pass — RFC 0148 §A — and stays unclassified for a claimed floor. A runtime-derived profile (PROFILE_FLOOR_SCENARIOS[p].runtimeDerived; today openwop-node-packs, whose discovery predicate is openwop-core) is listed in claimedProfiles only when the host holds it — every floor row a witnessed pass; otherwise the summary prints not held and the claim is dropped rather than rejected.
What's Covered
The current suite has 465 scenario files under src/scenarios/.
- 2026-08-19 (suite
1.137.0 → 1.138.0): NEWdurability-poison-exhaustion.test.ts— RFC 0158 §C.8, the FIRST row of that RFC's conformance table to land. Asserts whatfailure-path.test.tscannot: not just that deterministically failing work reaches terminal, but that attempts STOP — counted on the log, re-counted after a scaled quiet window, asserted unchanged. A host still redelivering records more. Seam-gated on the existing event-log seam (blocked= unobservable, not unmet) and outside every profile floor. - 2026-08-19 (suite
1.136.15 → 1.137.0): NEWreplay-fanout-suppression.test.ts— capability-gated onwebhooks.supported, outside every profile floor; witnessesreplay.md§"Host-initiated fan-out is an external effect", which was the largest normative MUST NOT on the replay surface with no scenario and no SECURITY invariant. Three legs in ONEitagainst ONE receiver and ONE subscription — a positive control, the MUST NOT, and abranchboundary leg — because "no delivery arrived" passes identically when delivery never worked, so absence is asserted only after presence is proven on that exact wiring. A host with an SSRF guard correctly refuses the loopback receiver and recordsblocked: unobservable, not unmet. - 2026-08-18 (suite
1.136.7 → 1.136.8, SP-04): NEWspec-section-citations.test.ts— server-free; a<doc>.md §"<Section>"citation of a checked doc MUST resolve to a heading that exists.storage-adapters.md §"Claim acquisition"was cited by four artifacts, including a normative MUST inproduction-profile.md§Durability, and the section did not exist for the life of RFC 0009. Scoped to the docs whose section citations carry the durability contract; a corpus-wide sweep finds ~260 unresolved citations that need triage before they can gate. - 2026-08-17 (suite
1.135.4 → 1.136.0, S34): NEWnode-failed-payload-shape.test.ts— fixture-gated onconformance-failure; everynode.failedon the run's log MUST validate againstrun-event-payloads.schema.json§nodeFailed (erroris a REQUIRED object{ code, message }, not a string) — the second sibling host measured that reference hosts emitteddata.code/ a bare string for years and nothing on the wire checked. - 2026-08-17 (suite
1.134.0 → 1.135.0, S26): NEWdiscovery-families-view.test.ts— server-free pin fordiscoveryFamilies(root-first, deprecatedcapabilitieswrapper as fallback), the view 38 wrapper-only readers now go through. - 2026-08-16 (suite
1.128.0 → 1.129.0): NEWcompensation-recovery.test.ts— RFC 0151 §C retry-stable identity, §E tenant-bound operator authority, §B/§F recorded-facts replay, through the §21 recovery extension (unwindfailFirstInverseAttempts/hold+inverseActions[],replaysource[]/replayed[], NEWoperatorseam); gated oncompensation.supported,blockedper sub-feature when the extension is absent. - 2026-08-16 (suite
1.127.0 → 1.128.0): NEWworkload-identity-chain-bounds.test.ts— RFC 0154 §B chain bounds through the §20 seam (too-long / cyclic / scope-amplified refusals + a narrowing chain resolves), gated onauth.workloadIdentity.delegation.supported. - 2026-08-16 (suite
1.123.0 → 1.124.0, S22): NEWerror-envelope-canonical-shape.test.ts(server-free) pins the decision that the HTTP error envelope is flat —{ error: <code>, message, details? },details.retriable— against the schema, the newlib/error-envelope.tshelper (readErrorCode/readRetriable, tolerating the legacy nested{ error: { code } }seam shape through the first minor after 2026-11-10) and the prose; 15 HTTP-envelope legs read through the helper. - 2026-08-16 (suite
1.122.0 → 1.123.0) added two, both server-free and always-on:conformance-execution-witness.test.ts(an early return can never become a pass — the runner'sresolveFileRecord, the emitter derivation and the consumer verifier each pinned with sabotage vectors) andconformance-advertised-seam-required.test.ts(underOPENWOP_REQUIRE_BEHAVIOR=truean advertised profile with a missing seam FAILS —behaviorGate,behaviorGatePresent,seamAbsent— and the only escape is an explicit opt-out that recordsskipped);mcp-stateless-request.test.tsgained the Mcp-Method/Mcp-Name half ofmcp-header-body-consistent. Every scenario RFC 0148 §Conformance names now exists. - 2026-08-16 (suite
1.119.0 → 1.120.0, no new files): RFC 0148 §C gap G7 closed —PROFILE_FLOOR_SCENARIOSgained a discovery-conditional model (conditional: [{ path, includes, required }], evaluated against the captured discovery document) foropenwop-replay-fork, andopenwop-interrupts/openwop-secrets/openwop-provider-policy/openwop-memory/openwop-trigger-bridgereceived floors transcribed intoprofiles.md; every catalog profile now has a floor and unprovable is reserved for names outside the catalog. Against the tier-1 reference host: interrupts, secrets and provider-policy now certify on witnessed floors; replay-fork does not (itsreplaybranch carries the unconditional no-re-fire obligation,blockedwhile the host advertisessideEffectSuppression: none); trigger-bridge does not (delivery seam unwired). - 2026-08-16 (suite
1.118.0 → 1.119.0, no new files): 84 scenarios that returned early in silence now say why — 256 early-return sites converted tosoftSkip(kind, reason)(inapplicablefor a capability/profile/fixture the host does not advertise,blockedfor a seam, prior step, or precondition that was unavailable); against the tier-1 reference host the RFC 0148 §A marker rows fell from 84 to 8 (alldescribe.skipIf/it.skipfiles) andinapplicablerose from 47 to 97. - 2026-08-16 (suite
1.117.0 → 1.118.0, no new files): the runner resolves a silent zero-assertion file toblocked(RFC 0148 §A) instead of a vacuous pass; NEWlib/soft-skip.ts(softSkip,seamAbsent) lets a scenario say why it returned early, and ten scenarios were converted as exemplars. - 2026-08-16 (suite
1.116.0 → 1.117.0, no new files):openwop-node-packsbecame runtime-derived in--certify(claimed only when held) and its two floor scenarios recordinapplicable/blockedwith reasons instead of returning early into a vacuous pass. - 2026-08-16 (S7 — RFC 0149 §D, suite
1.115.0 → 1.116.0) added one, server-free:normative-example-extraction.test.ts(a<!-- normative-example: <name>.schema.json -->marker above a fenced example declares it a whole instance; every declared example is extracted at test time and validated againstschemas/<name>, and every undeclared whole-instance example fails — 26 declared acrossspec/v1). - 2026-08-16 (S12 — RFC 0148 §Conformance + RFC 0149 §A coverage residue, suite
1.114.0 → 1.115.0) added three, all server-free and always-on:certification-bundle-non-vacuous.test.ts(the consumer re-derivation of a v2 bundle —verifyBundleV2: missing witness, vacuous pass, duplicate requirement, tampered totals, invented disposition, missing reason all REJECT the evidence; an honestblocked/executed-failon a required row is valid evidence that does not certify),certification-bundle-redaction.test.ts(secret canaries never enter evidence — the emitter scrubs the finished document with the handed credential +OPENWOP_*secrets + the conformance canary, and the verifier rejects the canary anywhere), andopenapi-asyncapi-sdk-parity.test.ts(the generatedspec/v1/operation-path-manifest.jsonre-derives fromapi/openapi.yaml; AsyncAPI addresses resolve to OpenAPI GET paths with exactly one/v1; the SDK half witnessesopenwop-sdkswhen reachable and recordsblockedwith a reason when not). - 2026-08-16 (RFC 0148 acceptance item 2 — S6, suite
1.113.0 → 1.114.0) added one:runner-ledger.test.ts(server-free — the §A ledger file sink, the reader's conflict rank, file-level disposition derivation, and--certify's rejection of silent and vacuous returns). Every scenario file now records its own disposition (withassertionCount) into the ledger fromsetup.ts, and--certifyexits 3 when a claimed profile has an unclassified return. The first end-to-end run found that theopenwop-core-standardfloor had namedaudit-log-verification.test.ts— a file that never existed — since RFC 0088; the scenario it meant (audit-log-integrity.test.ts) gates on the optional annex profileopenwop-audit-log-integrity, so by the floor's own no-soft-skip rule the row was never a floor scenario and is removed (core-standard-profile.md§C). - 2026-08-16 (RFC 0153 §B/§C/§D/§E — S16, suite
1.112.0 → 1.113.0) added six:mcp-2026-07-28-discover.test.ts(peer pin of the now dual-eraMcpFakeServer— stateless_meta, header/body agreement-32020,-32022+data.supported[],server/discover,resultType,CacheableResult, MRTRinput_required→ retry with echoedrequestState,-32021without theelicitationcapability — plus the hostserver/discover⇔protocolVersionsleg),mcp-stateless-request,mcp-mrtr-roundtrip(client half cross-checked against the wire; server half against an exposed suspending tool),mcp-cache-tenant-scope,mcp-extension-opacity,mcp-current-auth-boundary— all gated onmcp.profiles ∋ mcp-2026-07-28/ the §B advert + additive seam blocks (mrtr,extensionAuthority),blockedon today's 2025-06-18 hosts. Header-less requests to the fake server still get 2025-06-18 semantics, somcp-tool-roundtripand the legacymcp-server-*legs are unchanged. - 2026-08-16 (RFC 0152 §C/§D/§E — S15, suite
1.111.0 → 1.112.0) added four:a2a-1-0-agent-card.test.ts(server-free — the suite's ownA2AFakePeer, now dual-era, pinned at 1.0 from the wire:supportedInterfaces[]card,SendMessage/GetTask/CancelTask/ListTasks,TASK_STATE_*,Partoneof,-32009on an unsupportedA2A-Version, header-less = 0.3),a2a-card-runtime-consistency.test.ts(black-box, gated ona2a.profiles ∋ a2a-1.0),a2a-1-0-task-roundtrip.test.ts(host as 1.0 server, same gate),a2a-peer-authority.test.ts(host as client; additive seamscenario,blockeduntil the host reports). Also (S14)fixtures-valid.test.tsandworkflow-primary-output-annotation.test.tsnow register schemas by enumerating the directory instead of a fixed list — the fourth cross-file$ref(#1009) had broken the list at describe time, here and in every downstream runner resolving the sibling corpus with a pinned suite. Correction recorded the same day: openwop-app's live origin passes the RFC 0152/0153 §A legs and has NOT wired theinvokeseams, so its §B legs areblocked(RFC 0148 §A) — an earlier note here said they pass. - 2026-08-16 (RFC 0157 — chain fragments carry compensation, suite
1.110.0 → 1.111.0) added one:chain-compensation-expansion.test.ts— theFragmentNode.compensation/WorkflowChain.compensationmirrors are byte-equal to their sources, the manifest stays self-contained (no cross-file$ref— the S14 lesson), andcarryCompensation(composed after the byte-mirrored expansion core, not inside it) carries the declaration verbatim with params substituted and node-id refs rewritten, refuses an unresolvable compensator before any node is emitted, and copies / accepts / conflicts the chain policy intosettings.compensation. - 2026-08-16 (RFC 0155 §A —
openwop-discovery-corecanonical /openwop-coredeprecated alias, suite1.109.0 → 1.110.0) added one:profile-discovery-core-alias.test.ts— the alias MUST derive exactly when the canonical name derives, checked over payloads on both sides of the predicate (an equivalence over payloads that all derive proves nothing about the other side), the doc's rename and claim vocabulary (an unqualified claim meansopenwop-core-standard), and a gated live-host leg;PROFILE_NAMESgained the canonical name andderiveProfilesemits both or neither. Same release:spec/v1/extensions.jsongained a DERIVEDcoverageblock — 5 covered / 81 uncovered / 4 core of 90 capability families — gated bygenerate-extension-registry-coverage.mjs --checkinopenwop:check, so "unlisted means uncovered" is a checked list rather than a sentence. - 2026-08-13 (host-callback declaration — every scenario needing the HOST to reach the harness declares it, so an off-process consumer can enumerate the unwitnessable set before running).
- 2026-08-13 (RFC 0153 §A/§B — MCP revision negotiation witness, suite
1.96.0 → 1.97.0) added one:mcp-version-negotiation.test.ts(four capability-gated legs) plus header capture onMcpFakeServer. Both fake peers had the identical gap — recording the call body but not its headers — which is worth naming as a shared assumption rather than two oversights: the interesting part of a call is its body. Version negotiation is precisely the case where that is wrong. MCP revisions are dates, so the wire header is checked for date form and not merely for presence: a host sendinglatestor2026-7-28has sent something unmatchable against a pinned peer, and the failure then surfaces at the peer rather than at the handshake meant to prevent it. A further leg requires the negotiated revision to appear inprotocolVersions, because a host that negotiates a revision it never advertised has made its own discovery document unreliable — worse than advertising nothing, since a consumer that read it decided on a fact that was not true.Pinned real MCP current peer passes in CIstays separate and unmet. - 2026-08-13 (RFC 0152 §B — A2A version negotiation witness, suite
1.95.1 → 1.96.0) added one:a2a-version-negotiation.test.ts(four capability-gated legs) plus header capture onA2AFakePeer. RFC 0152 was twice described here as needing a live upstream peer, and that was half true: interop needs a real peer, negotiation does not — negotiation is the host's behavior and the host is right here. What was actually missing is thatA2AFakePeerrecorded method, path, rpcMethod, and body but not headers, and §B lives entirely in the headers: the peer could see that a call happened but not which version was negotiated, which is the only part §B is about. The fix was to record what the fake peer already received. The load-bearing leg is negative — "a host MUST NOT silently downgrade an authenticated request" — and a silent downgrade is dangerous precisely because it SUCCEEDS: the caller believes it negotiated 1.0, the peer answered 0.3, and nothing in the response says otherwise. A failure would at least be visible. So a host that proceeds after a downgrade MUST report the version it actually negotiated, and the wire header MUST match.Real upstream A2A 1.0 peer passes in CIremains separate and unmet — that item tests interoperation, which no fake can stand in for, and the RFC's acceptance criteria still say so. - 2026-08-13 (RFC 0154 §A/§B — the workload-identity BEHAVIORAL witness, suite
1.94.0 → 1.95.0) added one:workload-identity-behavior.test.ts(six capability-gated legs) plushost-sample-test-seams.md§20. §A's requirements are invisible from the wire: verify, bind to request, resolve to a principal before authorization, fail closed — and a normal request either succeeds or 401s, both of which look identical whether the host verified anything or simply trusted a header. That invisibility is why the seam exists, and why a missing seam is reported asblockedrather than skipped: RFC 0148 §A resolves an unobservable requirement to blocked, so without it RFC 0154 cannot be certified at all. The negatives carry the weight — a host that echoes its input satisfies every positive assertion, so only the rejections distinguish a verifier from a passthrough:audience_mismatch(an identity minted for another host, accepted here, is how a credential valid elsewhere becomes valid here — RFC 0147 R12),delegation_expired(a delegation without a live expiry is a standing grant, which is not what delegation means), andsender_constraint_missing(without proof-of-possession a bearer credential is replayable by anyone who observed it). Failures MUST be non-retriable — an identity that does not resolve will not resolve on retry, and marking it retriable invites hammering a failing authorization path. The seam MUST NOT be a mock: it drives the host's real resolver, or the witness proves nothing about production. - 2026-08-13 (RFC 0151 §C–§G — the compensation BEHAVIORAL witness, suite
1.93.0 → 1.94.0) added one:compensation-behavior.test.ts(five capability-gated legs). This closes a gap in the SUITE, not in any host. RFC 0147 §A.5 requires a host to execute every normative behavioral path in strict mode — and RFC 0151 had no behavioral scenario at all, only a server-free schema check. A willing host had nothing to run. For several turns the blocker was described as "no host implements this" when part of it was that the evidence-collecting apparatus did not exist either. Each leg is gated oncompensation.supportedviabehaviorGate, so it soft-skips against a non-advertising host and hard-fails underOPENWOP_REQUIRE_BEHAVIOR=true, recording an RFC 0148 §A ledger disposition either way. The legs pin the §C rules that are otherwise unverifiable: plan persisted before the first inverse action (requestedstrictly precedesstarted— a host that unwinds before persisting cannot resume, and the crash is exactly when that matters); descending forward-completion order (compensating forward can release a resource a later inverse action still needs); replay does not re-fire (§F — a replay that re-executes inverse effects turns a recovery into a second outage); and no provider bodies or credentials in events (§D/§G — these land in the durable log, the least revocable place a credential can reach). A missing sample seam fails with a message saying the requirement is unobservable and thereforeblocked, not passed. - 2026-08-13 (RFC 0148 §C — certification bundle v2, suite
1.91.0 → 1.92.0) added one:certification-bundle-v2.test.ts(eight legs, server-free) plusschemas/certification-bundle-v2.schema.json. v1 recorded{passed, failed, skipped}as scenario-FILE lists, and those three words cannot express the distinction this program turns on: a file counted aspassedwhether its assertions ran or its runner returned early, andskippedflattened three different claims — "the operator excluded this", "the requirement does not apply", and "we could not check" — into one word, when only the first two are certifiable. v2 carries per-requirement dispositions from §A plus the counts §A.4 requires. Two properties are load-bearing:blockedis a REQUIRED total, becauseblocked: 0asserted is a different claim fromblockedunstated and an omitted total reads as zero; andassertionCountmakes a vacuous pass visible — the schema deliberately still ADMITSexecuted-passwithassertionCount: 0, because forbidding it would only move the lie one field over, while putting the number in the artifact lets a reader see a pass that executed nothing without re-running the suite. Sabotage-verified: droppingblockedfromrequiredand removingminItemson the requirement list red two legs. - 2026-08-13 (RFC 0154 §A/§B — workload identity and delegation, SHAPE ONLY, suite
1.90.0 → 1.91.0) added one:workload-identity-profile.test.ts(eleven legs, server-free) plusschemas/workload-identity.schema.jsonand anauth.workloadIdentitycapability. §A's real requirements are behavioral — cryptographically verify the presented identity, bind it to the request, resolve it to a principal before authorization, fail closed when unresolvable — and a schema demonstrates none of them. What a schema can do is make the dangerous shape unrepresentable, which is what this checks: raw certificates, tokens, proofs, and credentials MUST NOT enter these objects.subjectis opaque;proofRefandthumbprintRefare pattern-constrained digest references, so a JWT or a raw key cannot be pasted in and still validate. That matters because these objects are projected into events, spans, and audit records — the three places credential material must never reach.audienceis required on a delegation because an identity minted for somewhere else, accepted here, is the confused-deputy path (RFC 0147 R12), and identity is not authorization is the one rule a schema cannot enforce at all. Sabotage-verified: openingadditionalPropertiesand dropping the digest pattern red two legs. - 2026-08-12 (RFC 0152 + RFC 0153 §A — versioned composition discovery, SHAPE ONLY, suite
1.89.0 → 1.90.0) added one:versioned-composition-profiles.test.ts(eleven legs, server-free) plusprotocolVersions/preferredVersion/profilesona2aandmcp, and a closedfeatureslist onmcp. Both RFCs address the same defect, which is why they land together:supported: truewith no version is a claim a peer cannot negotiate against. Two hosts can both advertise it, share no revision, and discover that only when a call fails — at the peer, not at the handshake meant to prevent it. MCP versions are date-patterned rather than free strings because MCP revisions are dates: acceptinglatestor2026-7-28would let two hosts disagree about which revision they share while both validate. The MCP feature list is closed because an unrecognized name is indistinguishable from a typo. Shape only, and the file says so — nothing contacts a peer, and a final leg asserts both RFCs keep their upstream-peer acceptance items unticked, since interop is the one thing a schema cannot demonstrate. A first draft had the negatives passing for the wrong reason: thea2afamily independently requiresagentCardUrl, so fixtures omitting it failed on the missing card URL rather than the version defect under test — the vacuity pattern, inside a test written to prevent it. - 2026-08-12 (RFC 0151 §A/§B — compensation profile SHAPE ONLY, suite
1.88.0 → 1.89.0) added one:compensation-profile.test.ts(nine legs, server-free) plus thecompensationcapability family and the node-levelcompensationdeclaration. What it proves is stated in the file itself: the schemas admit the shapes §A/§B describe and reject the ones they forbid. That is shape-only evidence — not evidence that any host orders an unwind, persists a plan before the first inverse action, or resumes after a crash. RFC 0147 §A.5 forbidsAcceptedon shape-only evidence for a behavioral requirement, and a final leg asserts 0151's behavioral acceptance item stays unticked and annotated, so reading the suite alone cannot leave a different impression. Design constraints the schema encodes: compensation is a second effect, not an undo — it can fail, be partially applied, and be harmful (R9), hencerequiresApprovaland security-high; inputs come from recorded facts, since an inverse built from a re-inferred value is not the inverse of what was done; andnodeTypeIdresolves at registration, so an unwind cannot fail on a typo first discovered during a failure. Sabotage-verified: openingadditionalPropertieson the declaration reds the closed-shape leg. - 2026-08-12 (RFC 0147 §A.10 — program self-audit, suite
1.87.0 → 1.88.0) added one:rfc-0147-self-audit.test.ts(four legs, server-free) plusdocs/RFC-0147-SELF-AUDIT.md. §A states ten invariants that "apply to every workstream", and §A.10 forbids citing the RFC's partial implementation as evidence its gaps are closed — so a program that audits everything except itself has the same defect it was written to fix, one level up. The audit found two violations: §A.5 (0151–0154 reachedAcceptedwith no evidence, not merely shape-only) and §A.6 (0148/0150/0152/0153/0154 had comment windows waived underMAINTAINERS.mdbootstrap language — the exact mechanism §A.6 says must not shorten a high-risk window). The available technicality — 0147 wasDraftat the instant of the flip — is recorded and not relied upon, because reading the program's partial state in whichever direction is convenient is the shape §A.10 forbids. The gate does not assert compliance; it asserts every invariant carries an explicit disposition, including the adverse ones. Making it fail on a violation would create pressure to delete the row rather than fix the program. Sabotage-verified both ways: replacingVIOLATEDwithsatisfiedreds, and deleting the §A.6 row reds. - 2026-08-12 (RFC 0155 §B/§C — core-standard manifest + extension registry, suite
1.86.0 → 1.87.0) added one:core-manifest-and-extension-registry.test.ts(eight legs, server-free), plusspec/v1/core-standard-manifest.json,spec/v1/extensions.json, and a generator wired intoopenwop:check. §B's value is the sentence after the inventory — "prose and code profile definitions MUST be generated from or checked against this manifest". Three places described the core-standard floor independently and could only be assumed to agree; they did not, andPROFILE_FLOOR_SCENARIOSbeing an incomplete transcription is what let five profiles verify as floor-proven against nothing (RFC 0148 §C). The manifest is derived, never hand-listed, because a hand-listed manifest drifts the moment the corpus moves and then asserts the drift with a digest attached. §C's registry carries six records backfilled from each RFC's own status. Every one isdraft, and none can currently bestable: §C gates stable on a Tier-3 implementation and none exists, so that ceiling is recorded rather than worked around — a fact about adoption, not about the work. Sabotage-verified on both halves: dropping a floor scenario from the manifest reds the parity leg, and marking any extensionstablewithout anevidenceTierreds the overclaim leg. - 2026-08-12 (RFC 0150 §C — golden vectors, suite
1.85.0 → 1.86.0) added one:semantic-digest-vectors.test.ts(sixteen legs, server-free) plusconformance/vectors/semantic-request-digest-v2.json. §C's acceptance criterion is that TypeScript, Python, and Go compute the same digest, and prose cannot deliver that — three independent readings of "canonicalize via JCS and hash" is exactly how three implementations diverge, invisibly, until two hosts replay the same run and get different cache keys. The vectors are the contract; an SDK reproduces the file. Several are pairs whose relationship IS the requirement: tools sorted vs reversed MUST agree, message order reversed MUST NOT, and the two Unicode forms oféMUST NOT — because JCS does not apply NFC. That last pair catches a well-meaning "add NFC to be safe" change that every other vector passes. The first draft of this gate had the bug it was written to catch: the relationship legs compared two stored digests from the same file — two constants — so they validated the vector set and would have passed unchanged against a broken implementation. They now recompute frominput. Sabotage before the fix reddened one leg; after, two. - 2026-08-12 (RFC 0150 §C — semantic request digest v2, safety-fix, suite
1.84.0 → 1.85.0) added one:semantic-digest-v2.test.ts(six legs, server-free). Three defects inreplay.md. (1) The exclusion list forbade exactly what §C requires: it saidmax_tokens,stop, andseedMUST NOT influence the cache key — but all three change the completion, so keying them identically causes a wrong hit, not a miss, deterministically returning a response the caller never asked for. (2) It prescribed a normalization JCS does not perform: canonicalize "via RFC 8785 JCS", then, for hosts without JCS, "UTF-8 NFC for all strings" — so the two routes the same sentence offered produced different bytes for the same input, defeating the cross-host portability §D asserts as a normative invariant. (3) It quoted a formula that no longer exists: theattempt-bearing Layer-2 composition RFC 0150 §B retired, left behind whenidempotency.mdwent to v1.4 — a cross-document staleness §B itself introduced and did not catch. v2 stamps the canonical objectopenwop-semantic-request-v2so retired digests are distinguishable, and carries unknown provider options in a namespacedproviderOptionsrather than dropping them. The gate reads normative text only, excluding blockquotes, because the document deliberately quotes the rules it retired — with a guard that the filter retains >70% of the file, and sabotage-verified that reintroducing a retired rule in normative voice still reds. - 2026-08-12 (RFC 0148 §B — strict behavior, suite
1.83.0 → 1.84.0) added one:strict-behavior-gate.test.ts(seven legs, server-free). §B says "a host MUST NOT both advertise and opt out of the same profile" — andbehaviorGate()detected exactly that contradiction, emitted aconsole.warn, and proceeded as if advertised. A MUST NOT enforced by a warning is not enforced: nothing consumes the warning, nothing fails on it, and the certification bundle produced from that run records a pass. It matters because the two claims are opposite in kind — advertising says the host implements the profile, opting out says the operator declares it does not — so a run where both hold has no defensible reading, and the resolution the gate chose (advertisement wins) extracted more certification claim from a more contradictory input. Now throws. The second half wires §B to §A: a gate decision records a ledger disposition, so an honest opt-out isskippedand an unadvertised profile isinapplicable— both certifiable, and both distinct from theblockedthat §A assigns to silence. Before the ledger existed there was nothing to record into, which is why §B could not be implemented before §A. - 2026-08-12 (RFC 0148 §A — requirement execution ledger, suite
1.82.1 → 1.83.0) added one:requirement-ledger.test.ts(thirteen legs, server-free) plussrc/lib/requirement-ledger.tsandsrc/lib/requirement-registry.ts. §A's operative sentence is negative — "a plain test return, caught exception converted to a return, or empty assertion body MUST NOT produceexecuted-pass" — and a ledger that merely offers five dispositions does not deliver it. Every vacuity found in this corpus reachedpassby not running, not by running wrong:[].every(...)over an undefined floor, a gated subtest that 404'd and soft-skipped, a scenario whose assertions never executed while its file counted green. So the ledger inverts the default: a requirement with no recorded disposition resolves toblocked, never to a pass. Silence is evidence of nothing, and the data structure says so instead of relying on each author to remember it.blockedis not certifiable (skippedandinapplicableare); an empty requirement set does not certify, because that is the[].every(...)shape itself;requirementsFor()returns null rather than[]for an unwritten floor, forcing the caller to distinguish "empty by design" from "not transcribed yet"; a non-pass disposition without a reason throws; and contradictory dispositions for one requirement throw rather than last-write-wins, since silent overwrite would let a later soft-skip bury an earlier real failure. Scoped to the certification floor, not all 423 files — tagging every assertion in one pass would produce a registry nobody could review, and an unreviewed requirement ID is worth less than none because it looks like coverage. Sabotage-verified on the load-bearing property: defaulting absence toexecuted-passreds the gate. - 2026-08-12 (RFC 0150 §B — cross-scope effect identity, additive, suite
1.81.0 → 1.82.0) added one:effect-identity-cross-scope.test.ts(four legs, server-free).runIdis in the §B preimage, so an effect issued outside any run cannot produce a Layer-2 identity at all and can never collide with one. Layer 2 dedupes a node effect against its own retries; it does not dedupe it against the same logical effect issued via an operator route, admin action, or scheduled job. Since §"Why this exists" requires Layer 2 "for any node executor that performs an external side effect", a host reading that literally would use the ordinal form and stop — and for a cross-entry-point effect that reintroduces the duplicate-effect class §B exists to kill, on the highest-stakes path it touches. v1.4 requires such effects to be additionally keyed on a business identity, stable across entry points and containing norunId/nodeId/ordinal. Reported by a tier-1 host from a shipped node pack (refund-orderplus three non-run entry points reaching one implementation), not proposed in the abstract. The fork note and this are one limitation seen twice, and a leg asserts the linkage so a reader cannot read the fork case as a special exception. - 2026-08-12 (RFC 0149 §D — lifecycle coherence, governance, suite
1.80.0 → 1.81.0) added one:rfc-lifecycle-coherence.test.ts(three legs, server-free). §D asks the generator to fail when anAcceptedRFC "retains an unresolved acceptance blocker not explicitly carried". The obvious gate — every box ticked beforeAccepted— was measured and rejected: of 141AcceptedRFCs, 42% ticked all, 25% ticked none, 24% ticked some, so a blanket rule fails 69 RFCs on its first run, and a gate that fires 69 times gets disabled rather than fixed. The first triage hypothesis was also wrong, and correcting it produced the rule that shipped: the partially-ticked RFCs are not a blocker backlog — every trailing item in0027/0040/0041is deliberately unticked and annotated with why, which is §D's "explicitly carried", just in a parenthetical rather than a register row. So the signal is annotated vs bare, not ticked vs unticked: an unticked item with no explanation is indistinguishable from one nobody checked. Scoped to RFCs ≥ 0147 — the program's own cohort — so the rule binds the RFC that proposed it, with a leg asserting exactly that; earlier RFCs are the dated record of a period when the convention did not exist, and the boundary is asserted so a new RFC cannot inherit the exemption. All 67 bare items across 0147–0156 were annotated to land it, which is the real work: each now names its gate. Sabotage-verified — stripping one annotation reds the leg with its file:line. - 2026-08-12 (RFC 0149 §E — canonical-family shadowing, security, suite
1.79.0 → 1.80.0) added one:discovery-canonical-family-no-shadow.test.ts(three legs, server-free). RFC 0073 puts canonical families at the document root andhost-extensions.mdputs vendor surface underx-host-*/vendor.*/private.*; together that is what makes discovery negotiable. Nothing forbade a canonical family name appearing INSIDE the namespaced region —vendor.acme.auth, or anx-host-acme-*object carrying its owninterrupts. A consumer that merges vendor surface over the root before negotiating then reads a vendor'sauthblock as the auth contract.host-extensions.mdalready tells clients to treat extension surface as opaque, but that binds the consumer — it does not stop a host publishing the collision, and the consumer that gets the merge order wrong is exactly the one the rule exists to protect. Second leg, unconditional: no discovery example may carry credential material, because discovery is served credential-free to anonymous callers, so a real-shaped secret in an example sits in the most-copied, least-guarded artifact in the corpus. The family list is read fromcapabilities.schema.jsonrather than hand-listed, so a new family is covered the day it lands. The corpus was already clean, making this a guardrail rather than a repair — so it was sabotage-verified rather than red-before-green: injecting"vendor.acme.suite": { "auth": … }reds the shadow leg with its file:line and the colliding key, and ansk--prefixed value reds the credential leg. - 2026-08-12 (RFC 0149 §C —
protocolVersiongrammar, safety-fix, suite1.78.0 → 1.79.0) added one:protocol-version-grammar.test.ts(twenty legs, server-free). The field was specified three incompatible ways at once:capabilities.schema.jsonconstrained it tominLength: 1, the suite's own core predicate testedstartsWith('1.'), and prose called it semver while every example showed two components. So"v1.0","1.0.0", and"banana"all validated — and"1.0.0"additionally derivedopenwop-core, because the predicate deciding whether a host is openwop-compatible at all was looser than the schema every host validates against. Comparison needs an integer major (the hard boundary) and an integer minor (the additive contract level); neither is extractable from a string nothing constrains, so two hosts advertising"1.0"and"1.0.0"gave a consumer no way to tell a patch convention from a typo from a different protocol. Now^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$in the schema, inprofiles.ts, and inversion-negotiation.md§"Protocol version grammar" — with a leg asserting the schema pattern and the predicate agree, so they cannot drift apart again. Closes gap V2, open since v1.0 with ownerfuture. - 2026-08-12 (RFC 0150 §D — multi-region effect vocabulary, safety-fix, suite
1.77.0 → 1.78.0) added one:multi-region-effect-vocabulary.test.ts(eight legs, server-free). Two capability values promised things they did not deliver.crossRegion: "strict"was documented as "cross-region read-visibility is bounded bymultiRegion.replicationLagBoundMs" — a latency claim occupying the top slot of a ladder implementers read as effect safety. A host replicating synchronously at 0 ms can still issue duplicate effects from two regions, because knowing what the other region wrote is not the same as being authorized to act. It was removed rather than renamed tofenced-effects: a rename would have promoted every existingstrictadvertisement into a claim no host substantiated, and its latency content is not lost becausereplicationLagBoundMsalready carries it.partitionRecoveryStrategyofferedlast-writer-winsandfirst-writer-wins, both time-ordered — but under a partition there is no shared clock, so each region believes it wrote last, which cannot satisfy the annex MUST that "re-running the same conflict input MUST produce the same survivor", and both select a different survivor than the lex-min(runId) rule the same document requires. The schema was advertising strategies that violate two MUSTs already in force. RFC 0150 §D names onlylast-writer-wins; removing that alone would have left the identical defect under a different label, so both went andlexicographic-min-run-idwas added to name the rule the annex actually requires. The gate also catches the unparseable ```json example RFC 0149 §B found and deliberately left. Corpus-structural tier: it reads the schema and the prose, not a host — whether an engine fences before issuing an effect needs a partition simulator plus an observable effect sink, which the existing seam does not provide. - 2026-08-12 (RFC 0150 §B — Layer-2 effect identity v2, safety-fix, suite
1.76.0 → 1.77.0) added one:effect-identity-composition.test.ts(six always-on server-free legs, skipped under the published layout).spec/v1/idempotency.mdspecified the Layer-2 key assha256(runId ':' nodeId ':' attempt ':' providerKey)while the same document promised that a retried call's identity "is identical" — both cannot hold, and the formula was the wrong half. A retry counter in the preimage gives every retry a fresh key, so the invocation log never hits, and because the engine injects that value as the provider'sIdempotency-Key, it also sends Stripe or OpenAI a different key each attempt — defeating the provider-side dedup the same paragraph invokes as its safety net. v1.2 specifies the domain-separated, tenant-bound composition over alogicalInvocationOrdinalthat MUST NOT change across retries. The gate's tier is labelled corpus-structural on purpose: it reads the normative composition, not a host, because whether an engine reuses the identity across a retry is observed by the provider, not the caller, and a wire probe claiming otherwise would be the vacuous-witness pattern RFC 0148 exists to close (gap G9). Sabotage: weakening the ordinal rule toSHOULD NOT, or deleting it, reds the leg. - 2026-08-12 (RFC 0149 §B — discovery-example root layout, editorial, suite
1.75.0 → 1.76.0) added one:capability-example-root-layout.test.ts(three legs, server-free, skipped under the published layout becausespec/v1/andRFCS/are not in the tarball). RFC 0073 made the document-root layout "the normative MUST since Phase 1" and, at Phase 4, made the suite grade a wrapper-only host non-conformant — thecapabilitieswrapper survives only as a deprecated shape runtime discovery tolerates until v2.0. Tolerating a shape at runtime and teaching it in a normative example are different things: eightspec/v1examples, several under headings like "Capability advertisement (normative)" introduced by prose saying hosts "advertise it under/.well-known/openwop", showed the deprecated wrapper. An implementer copying one produced a document RFC 0073 grades as non-conformant, and no gate noticed, because a fenced example is prose to every validator in the corpus. All eight unwrapped. Authoring-time only — nothing here reads a host, and RFC 0149 §B keeps runtime discovery open to unknown server-emitted properties. The RFC leg asserts the historical carve-out rather than assuming it: everyRFCS/example still showing the wrapper is in an RFC numbered below 0073, so a new post-0073 RFC introducing one fails and the exemption cannot widen into a licence. Sabotage: restoring one wrapped example reds the lint with its file:line. - 2026-08-12 (RFC 0148 §C / gap G6 — floor enforcement, implementation of existing normative text, suite
1.74.0 → 1.75.0) added one:certification-floor-enforcement.test.ts(six always-on server-free legs).PROFILE_FLOOR_SCENARIOSdefined a floor foropenwop-core-standardalone, andverifyBundleProfile()computedfloorProvenfrommissingFloor.length === 0 && prefixOk— both vacuously true over an absent floor, so a claimed profile with nothing behind it verified as proven. The floors were transcribed, not invented:profiles.md§"Claiming vs passing" already requires "predicate AND passing the conformance scenarios labelled with the profile tag", and its per-profile sections already name them — so this enforces normative prose rather than adding to it, and needs no safety-fix window. An empty floor and an unwritten floor are now different values:openwop-coreandopenwop-fixturesare discovery-payload-only by prose and carry an explicitdiscoveryOnlymarker; an absent key means unprovable, surfaced asfloorUnspecifiedso "the corpus has no floor" stays distinct from "the host failed its floor".openwop-replay-forkis deliberately left unspecified — its floor is conditional on the advertised mode, which a flat required-list cannot express, and forcing it would either fail an honest single-mode host or restore the vacuity. Sabotage: restoring the old undefined-floor branch reds two legs. - 2026-08-11 (RFC 0149 §A — canonical URL resolution, editorial, suite
1.73.1 → 1.74.0) added one:openapi-resolved-paths.test.ts(three always-on server-free legs). It resolves eachservers[].urlagainst each path key and asserts exactly one/v1segment for versioned operations. Why every existing gate was blind to this:redocly lintvalidates the server, validates the paths, and never composes the two — the defect existed only in the join, where all 44 versioned path keys resolved to/v1/v1/*. The reference SDKs were the control:OpenwopClientissues/v1/runsagainst a bare base URL, so the SDKs and the canonical document disagreed about where the version segment lives and the SDKs were correct. A third leg asserts/.well-known/*resolves unversioned — the red run showed the duplicated base also resolved discovery to/v1/.well-known/openwop, so a generated client could not bootstrap at all. A guard leg fails if the extraction finds no servers or no paths, because a scan that silently matched nothing would make the other legs vacuously true. Written red first, observed failing on both counts, then green after the one-line correction. - 2026-08-11 (RFC 0146 —
contractProvenance, additive, suite1.72.2 → 1.73.0) added one:contract-provenance.test.ts(four always-on corpus legs + one behavioral). Why validation alone cannot catch what this closes: v1.x changes are additive, so a discovery document written against an older contract still validates against the newer schema — which is exactly why a host validating against a vendored copy 7 properties behind was green. Only a claim on the wire surfaces it. The legs assert the SHAPE of that claim and never that a host is current, because a host on an older corpus revision is CONFORMANT; a leg failing a host for being behind would convert an optional disclosure into a de-facto upgrade mandate inside a version line where be
