@openwop/openwop-conformance 2.36.1 → 2.37.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # `@openwop/openwop-conformance` Changelog
2
2
 
3
+ ## [2.37.0] — 2026-09-23 — rows that measured nothing now measure: a suite fake reachable through the public front, one effect identity per leg, the MRTR key that can tell
4
+
5
+ - **Every synthetic OIDC issuer publishes its own `kid`, so two scenarios at one issuer URL no longer collide in a host's JWKS cache.** `lib/oidc-issuer.ts` generated a fresh key per `createSyntheticOIDCIssuer` instance but named it `openwop-conformance-key-${rotationCounter}` with a per-instance counter — so EVERY instance published `openwop-conformance-key-1`. `v2-oidc-id-token-audience` and `v2-lane-exp-only-bound` both publish at `OPENWOP_TEST_OIDC_ISSUER_URL`; a host that caches JWKS by `kid` (RFC 7517 §4.5: a kid identifies one key) verified the second scenario's tokens against the first scenario's cached key and refused its VALID control as `invalid_signature`. Reported and measured by openwop-app-ce on openwop-app, suite 2.36.1: `v2-lane-exp-only-bound` 4/4 red in a full run (control included), 4/4 green filtered — an order-dependent red that reads as a host failure on RFC 0210's rows. The kid is now `openwop-conformance-key-<per-instance hex>-<rotation>`. New self-test: a kid-caching verifier (re-fetch only on an unknown kid) must accept both instances' valid tokens at one URL; reverting to the shared kid reds it.
6
+ - **RFC 0211 — the A2A error-details legs, and an isolation comparison that could not fail.** `v2-a2a-operation-map`'s unknown-vs-foreign-tenant leg compared `Object.keys(error.data)`, which is `["0"]` for any one-element array once `data` follows A2A §9.5 — so it would have passed whatever the entry disclosed. It now compares the per-element normalised ErrorInfo (`@type`, `reason`, `domain`, sorted metadata keys, minus an echo of the requested id) behind a positive control that an ErrorInfo exists (`0211.a2a-unreadable-not-found-details`). Three new legs in the same file: `0211.a2a-error-data-shape` (unknown `GetTask` ⇒ `TASK_NOT_FOUND`, terminal `CancelTask` ⇒ `TASK_NOT_CANCELABLE`, each an `Any[]` with exactly one ErrorInfo in domain `a2a-protocol.org`), `0211.a2a-no-openwop-envelope` (non-JSON body, no credential, unknown method — none answered `{ error, message }`), `0211.a2a-version-not-supported-shape` (`-32009` not `-32600`, reason `VERSION_NOT_SUPPORTED`, `metadata.supportedVersions` when present a comma-joined subset of the card), and `0211.a2a-httpjson-status` (gated on an HTTP+JSON interface — `inapplicable` on every committed host, not claimed). New major-2 `src/scenarios/v2-a2a-client-error-details.test.ts` (`0211.a2a-client-reads-either-shape`, gated on `a2a` + the seams profile): the host's client projects a peer's `-32009` to `interop_version_unsupported` with `data` as `Any[]` and as the legacy object — weak by construction and said so (code identification succeeds either way). `A2AFakePeer.setLegacyErrorData()` serves the pre-2.37.0 object shape for that leg. New `src/lib/a2a-error-info.ts`.
7
+ - **A scenario's own A2A peer or MCP server is reachable through the operator's public front, and one RFC 0175 row that passed without measuring anything now cannot.** An operator fronts ONE listener per fixture — the tunnel behind `OPENWOP_A2A_FAKE_PEER_URL` / `OPENWOP_MCP_FAKE_SERVER_URL` forwards to the pinned `_PORT`, where `setup.ts` starts the shared fake. Six legs also construct a fake of their own (version-pinned: a `0.3`-only peer, a `2025-06-18`-only server, a fresh `1.0` peer), start it on an ephemeral port, and hand the host `hostFacingEndpoint()` — which returned the front verbatim. On every tunnelled cut the host's request therefore went to the SHARED fake while the leg read its own, untouched one. Measured on the v2 reference host: `0207.a2a-traceparent-carried` recorded `blocked` in all three public cuts of 2026-09-23 (\"the suite peer received no SendMessage\") — the host's A2A client HAD reached the suite, just not the listener counting (measured, not inferred: on a loopback run of the same host, where each fake's own address is handed over, the §22 seam's `SendMessage` reached the leg's peer carrying the trace in both `metadata.openwop.traceparent` and the header — the host has no carrier gap); the §23 MCP seam leg of the same file recorded the same (masked as `partial-witness`, because the row's other leg witnessed it). Loopback cuts never showed it, because each fake's local address is distinct. New `src/lib/front-mux.ts`, the shape openwop#1513 used for the effect receiver: every fake carries a nonce; a fake that does not own the pinned port is handed `${front}/fx/<nonce>`, and whichever fake does own it strips the prefix and dispatches to the registered instance (a request for a stopped fake is answered `404`, never counted). A2A cards built from `hostFacingEndpoint()` carry the nonce, so the host's RPC after the card returns to the same fake. `src/lib/front-mux.test.ts` simulates the tunnel against the pinned port; removing the routing reds 3 of its 4 cases.
8
+ **What this says about committed evidence, stated rather than fixed forward.** The only committed bundle that executed these legs is `evidence/v2-host-bundles/openwop-host-v2-reference.json` (suite 2.35.0, a PUBLIC cut, relaxations none), on which RFC 0175 is Accepted. On that cut: `0175.negotiation-authenticated.mcp` recorded `executed-pass` having **measured nothing** — that leg has no seam knob forcing the lower offer, so the only thing making the exchange \"unauthenticated and lower\" was the scenario's `2025-06-18`-only server, which the host never contacted; it negotiated with the shared server at `preferredVersion` and the assertion held trivially. It now records `blocked` when the seam reports success and the lower-revision server saw no request (`v2-negotiation-authenticated`; the A2A leg gets the same guard, though there `peerOffersOnly` did force the lower offer, so its decision half was measured). `0175.minimum-version-refused` and `.mcp` measured the host's refusal genuinely — `peerOffersOnly` / `requestVersion` force it on the host's own decision path — but their wire half (\"no below-floor call reached the peer\") iterated a peer the host never called, so it could not have failed. No guard is added there: that half is a MUST NOT, and a host that refuses without contacting the peer satisfies it; the routing is what makes a zero mean zero. `myndhyve.json` and `openwop-workflow-engine.json` record all four rows `inapplicable` (neither advertises `a2a` or `mcp`), so they are untouched. The reference host's next public cut on 2.37.0 is the re-measurement.
9
+ - **`0158.duplicate-delivery` is deterministic, and the two legs that shared one effect identity no longer do.** The row gave different verdicts on repeated runs of one host cut. `v2-durability-recovery.test.ts` and `v2-terminal-event-once.test.ts` both drive `mode=duplicate-delivery` on the RFC 0158 seam and both passed it `resolveRegistrationUrl(...)` — on a tunnelled cut the operator's `OPENWOP_WEBHOOK_RECEIVER_URL` verbatim, the same string for both. Same fixture, same node, same URL ⇒ same Layer-2 business identity (`idempotency.md` §"Layer 2 Keying"), so a conformant host resolved the second exercise to the first's recorded outcome and performed no invocation; whichever leg ran second read zero arrivals. New `src/lib/effect-receiver.ts` mints one destination per exercise with a nonce in its path and counts only the arrivals bearing it (`arrivals()` vs `foreign()`); both legs now take their receiver from it. The 0158 leg reads the host's effect ledger before classifying a zero and distinguishes *resolved against an already-recorded outcome* / *attempted and transport-failed (`released`)* / *never attempted*, recording `blocked` with the cause named in each case instead of `executed-fail` — a missed fire is not an exactly-once violation, and `blocked` still denies certification (RFC 0168 §E.1). The blind post-terminal sleep becomes a bounded wait for the one legitimate arrival plus the same quiet window for a second; no window was widened. Measured on the v2 reference host: 1/20 pass before, 20/20 after on the same warm host and pinned receiver port, and the row still records `executed-fail` (2 arrivals) when the host's duplicate-delivery guard is sabotaged. `src/lib/effect-receiver.test.ts` pins it host-free.
10
+ - **`v2-mcp-mount-map` gains `0208.mcp-mrtr-input-request-key`: the MRTR input-request key is the open interrupt's `interruptId`, never the node it suspended on.** `interop-map.json` `mcp.mrtr` InputRequiredResult (host as server) spells the key `<interruptId>`, and `mcp.tasks.status` repeats it per that same row; `v2-mcp-tasks` already pins it on `tasks/get` (keys equal to the run's `node.suspended` ids), but the `tools/call` path was measured only by `Object.keys(inputRequests)[0]` — so a host keying it by **node id** passed. The new leg starts TWO runs of `conformance-approval`, which suspend at the SAME node: the keys MUST differ, MUST each match the tenant-bound `interruptId` grammar from `schemas/v2/ids.schema.json` (an author-chosen `nodeId` has no tenant segment and can never match it), MUST each be an id that run's own `node.suspended` carries when `runList` is advertised, and MUST be the key the retry's `inputResponses` is read under. Same gate as the rest of the file (`mcp.serverMount` + profile `mcp-2026-07-28`), `blocked` naming `conformance-approval` when the fixture is not advertised. Major 2 only — `mcp-integration.md` §C.2 leaves the key to the host at major 1 and the v1 leg stays key-agnostic. Sabotage: against the v2 reference host before its fix the leg fails with `got ["gate","gate"]` while every other leg in the file passes.
11
+
12
+ - **Version moved ahead of publication, not with it.** `@openwop/openwop-conformance` and its exact-pinned peer `@openwop/spec-artifacts` move to `2.37.0` now, before any 2.37.0 content lands, because `2.36.1` is published and the published-identity gate refuses a tree whose shipped files differ from the tarball at the same version. The scenarios this minor adds are recorded here as they land. Published on the `v2.37.0` corpus tag.
13
+
3
14
  ## [2.36.1] — 2026-09-23 — a leg that convicted every host of a rule the spec does not have
4
15
 
5
16
  - **`mcp-2026-07-28-discover` asserted the opposite of `mcp-integration.md` §D, and a fixture that no longer existed.** 2.36.0 repaged the suite's own fake MCP server — "`tools/list` paged 3 + 3 over six tools with `cacheScope: \"private\"` (was one unpaged `public` page)", as that release's own RFC 0204 entry records — and both halves of the `tools/list` leg were left describing the old fixture. The first asserted `cacheScope === 'public'`; §D says `cacheScope` MUST be `private` whenever a result depends on the caller's tenant, workspace, principal or authorization, "which for `tools/list` on a multi-tenant host is always", and permits `public` ONLY when the result is byte-identical for every caller. So the leg required the one value the spec forbids here, and **every** certify run went red on it — the fixture is suite-owned, so the failure is deterministic and host-independent: it was a false `executed-fail` on the record of any host that ran it. The second asserted a three-name catalogue (`echo`, `needs_input`, `needs_input_loop`) against a six-tool paged one whose first page is `echo`, `structured-echo`, `always-error` — the MRTR tool the leg is *named for* is on page two, so the leg no longer reached the thing it exists to check. It now asserts the deterministic first page, follows `nextCursor`, and asserts page two carries the read-only-claim tool plus both MRTR tools and advertises no further cursor. Sabotage: setting the fixture back to `cacheScope: 'public'` turns the row `executed-fail` again. Found by the Conformance Soak on the 2.36.0 tree, not by a host — the corpus gate had quarantined the leg locally, which is exactly how an assertion drifts from its fixture for a full release. No spec text, schema or host obligation changed; only the suite's reading of one it already had.
package/README.md CHANGED
@@ -11,7 +11,7 @@
11
11
  # --legacy-peer-deps is REQUIRED, not optional: the exact peer pin is what npm's
12
12
  # default resolver refuses. npm 10.9 fails outright with
13
13
  # "Cannot read properties of null (reading 'edgesOut')" — use npm >= 11.
14
- npm install --legacy-peer-deps @openwop/openwop-conformance@2.36.1 @openwop/spec-artifacts@2.36.1
14
+ npm install --legacy-peer-deps @openwop/openwop-conformance@2.37.0 @openwop/spec-artifacts@2.37.0
15
15
  # or run without install:
16
16
  npx @openwop/openwop-conformance --base-url https://api.example.com --api-key hk_test_...
17
17
  ```
@@ -135,7 +135,8 @@ Exit code is non-zero on any failed assertion. `--certify` distinguishes: `0`
135
135
 
136
136
  ## What's Covered
137
137
 
138
- The current suite has 553 scenario files under `src/scenarios/`.
138
+ The current suite has 557 scenario files under `src/scenarios/`.
139
+ - 2026-09-23 (suite 2.37.0 cycle, RFC 0213): NEW `v2-sse-last-event-id-cursor.test.ts` (a `Last-Event-ID` past the log is an exclusive cursor; a malformed id, when refused, is `400 validation_error`; the cursor never changes the answer for an unknown or foreign-tenant run — public test of `event-cursor-after-authorization`), `v2-idempotency-in-flight.test.ts` (five concurrent same-key creates yield one run; each loser is a marked replay or `409 idempotency_in_flight` with no retry timing in `details`; `partial-witness` when no loser was refused in flight) and `v2-interrupt-resolve-terminal.test.ts` (a run-scoped resolve after cancel or completion is `409 interrupt_already_resolved`, never `interrupt_cancelled`). All three sit off the core-standard floor until measured on the three bundle hosts.
139
140
  - 2026-09-03 (suite `1.157.0 -> 1.158.0`, gap G17): NEW `idempotency-concurrent-claim.test.ts` — drives the new `host-sample-test-seams.md` §25 concurrent duplicate-delivery seam for the RFC 0150 §B / `idempotency.md` §"Concurrent duplicates (Layer 2)" atomic-claim MUST, which is unconditional and had no witness of any kind. Asserts every executor mints the SAME `logicalInvocationId` **before** asserting `delivered === 1` — without the identity check a host passes by minting different ids and never colliding, one effect because nothing raced. Not profile-gated and so not opt-out-able (the obligation is unconditional); an unmounted seam records `blocked`, which is not certifiable. Graduates `layer2-invocation-claim-atomic` reference-impl -> protocol.
140
141
  - 2026-08-19 (suite `1.137.0 → 1.138.0`): NEW `durability-poison-exhaustion.test.ts` — RFC 0158 §C.8, the FIRST row of that RFC's conformance table to land. Asserts what `failure-path.test.ts` cannot: 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.
141
142
  - 2026-08-19 (suite `1.136.15 → 1.137.0`): NEW `replay-fanout-suppression.test.ts` — capability-gated on `webhooks.supported`, **outside every profile floor**; witnesses `replay.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 ONE `it` against ONE receiver and ONE subscription — a positive control, the MUST NOT, and a `branch` boundary 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 records `blocked`: **unobservable, not unmet.**
@@ -480,7 +481,7 @@ Server-required (added in 1.7.0):
480
481
  | ------------- | ----------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
481
482
  | **Redaction** | [`capabilities.md`](../spec/v1/capabilities.md) §"Secrets" + NFR-7 + §"aiProviders" | Vendor-neutral assertions that the server doesn't leak secret material. Three scenario groups: (a) discovery shape contract — `secrets` + `aiProviders` advertisements are well-formed regardless of `secrets.supported`; when `supported === true`, scopes MUST be non-empty + `resolution === 'host-managed'`; `byok ⊆ supported`. (b) bearer-token redaction — invalid Bearer canary in `Authorization` header is not echoed in the 401 response body. (c) credentialRef echo control — gated on `secrets.supported === true`; canary planted in `configurable.ai.credentialRef` MUST NOT appear in any RunEvent payload (poll-based capture; transport-agnostic). Uses runtime-built canary fixtures (`lib/canaries.ts`) that defeat static secret scanners. 6 scenarios. |
482
483
 
483
- Current source tree: 553 scenario files. Use [`coverage.md`](./coverage.md) for current grade/gap tracking.
484
+ Current source tree: 557 scenario files. Use [`coverage.md`](./coverage.md) for current grade/gap tracking.
484
485
 
485
486
  ## Remaining Gaps
486
487
 
package/coverage.md CHANGED
@@ -258,6 +258,7 @@ Every OpenAPI operation should have:
258
258
  | `resolveInterruptByToken` | `interrupt-token-matrix.test.ts` covers replay (already-resolved) + unknown token; `interrupt-external-event-correlation.test.ts` covers positive path | Replay path + unknown-token path covered with explicit assertions | Add wrong-action case once the host advertises a typed allowed-actions vocabulary in the interrupt manifest. |
259
259
  | `getArtifact` | Indirect through approval payload fixtures | `route-coverage.test.ts` covers unknown artifact `404`/`403` envelope; `artifact-auth.test.ts` (CF-4 close-out 2026-05-15; SQLite host 401-before-404 stub landed 2026-05-19, closes the info-leak surface for every HTTP method) covers `401` unauthenticated path | Negative paths covered (401 + 405 non-GET + 404/403) | Add positive artifact-read scenario once a reference host implements `getArtifact` end-to-end. |
260
260
  | `registerWebhook` | Webhook spec exists | `route-coverage.test.ts` covers invalid URL validation envelope | Add positive registration with a test receiver when harness support exists. |
261
+ | `rotateWebhookSecret` | None on v1 — no v1 host advertises `webhooks.secretRotation` yet; the v2 twin is covered behaviourally by `v2-webhook-secret-rotation.test.ts` (RFC 0201 §E.18) | `v1-webhook-rotation-contract.test.ts` (corpus coherence) — the v1 contract carries the route `spec/v1/webhooks.md` §Rotation mandates, with `webhooks:manage` on all three lanes, the 400/403/404 refusals the prose enumerates, and a 200 that returns no secret | A behavioural v1 leg needs a v1 host advertising `webhooks.secretRotation`; none does. |
261
262
  | `unregisterWebhook` | Webhook spec exists | `route-coverage.test.ts` covers unknown subscription behavior | Add full register-then-unregister roundtrip with a test receiver. |
262
263
  | `listPromptTemplates` | `prompt-template-shape.test.ts` + `prompt-list-and-fetch.test.ts` cover schema shape + advertisement contract + list/get contract for `capabilities.prompts.*` against the reference workflow-engine (RFC 0028 `Active` — endpoints live under `openwop-app:backend/typescript/src/routes/prompts.ts`) | Behavioral list + advertisement-shape covered | Add cross-host list-with-filter parity scenario when a second host advertises `endpointsSupported: true`. |
263
264
  | `createPromptTemplate` | `prompt-mutable-lifecycle.test.ts` covers CRUD lifecycle against the reference workflow-engine (gated on `mutableLibrary: true`); user-source POST succeeds, pack + host-built-in templates return 403 | Positive create + readonly-source 403 path covered | Add explicit `409` duplicate-id scenario + auth/scope matrix scenarios. |
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "package": "@openwop/spec-artifacts",
3
- "version": "2.36.1",
4
- "stampSha256": "8cccf11a7baf5e5c0b8c1edcc72b473c2107786fb460b6e014fd0d5bce37f262"
3
+ "version": "2.37.0",
4
+ "stampSha256": "7122d36a173c88dfe75b1fd136824e9a3601583b9035916a666b49e11010b77e"
5
5
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openwop/openwop-conformance",
3
- "version": "2.36.1",
3
+ "version": "2.37.0",
4
4
  "description": "Production-ready black-box conformance suite for OpenWOP v1.0 compliant servers.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -56,6 +56,6 @@
56
56
  "@openwop/spec-artifacts": "file:../spec-artifacts"
57
57
  },
58
58
  "peerDependencies": {
59
- "@openwop/spec-artifacts": "2.36.1"
59
+ "@openwop/spec-artifacts": "2.37.0"
60
60
  }
61
61
  }