@openwop/openwop-conformance 2.42.7 → 2.42.8

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,18 @@
1
1
  # `@openwop/openwop-conformance` Changelog
2
2
 
3
+ ## [2.42.8] — 2026-09-27 — RFC 0221, a closed audit schema again, and a dispatch race fixed
4
+
5
+ - **`v2-preferred-version-default`: a header-less request on an unversioned operation path is served major 2.** This follows a Class 3 correction to `versioning.md` §1.3: the `preferredVersion` default applies to `/.well-known/openwop` only. The new leg reads `OpenWOP-Version` on a header-less `GET /runs/{unknown}`. It passes on the v2 reference host and fails when the host applies `preferredVersion` to every path.
6
+ - **RFC 0221: a webhook secret the host generates is returned once.** New `v2-webhook-generated-secret.test.ts` (target major 2, gated on the `webhooks` family and `conformance-noop`). It registers without a secret, and the `201` must carry the generated `secret`. A real `run.completed` delivery must then verify under it (`OpenWOP-Signature`, scheme `v1`), and a supplied secret must not be echoed. Witnessed on the v2 reference host with openwop-examples #98; its previous code, which withheld the secret, fails the row.
7
+ - **The 2.42.8 cycle opens.** RFC 0189 (#1675) changed the packed `@openwop/spec-artifacts` tree (`spec/v2/core/i18n.md`, `spec/v2/core/portability.md`, `declaration.json`) after `v2.42.7` was tagged. The published-identity gate correctly refused to call that tree 2.42.7.
8
+ - **RFC 0219 `Active`: `OpenWOP-Client-Version` is declared, and the floor's refusal has an input.** New `v2-client-version-header` (major 2, four legs, requirement ids `openwop.requirement.0219.*`). `floor-comparison` (gated on `minClientVersion`): `<floor>`, `<floor>.0` and `<floor>.99` are served, never `426`. `malformed-not-refused` (gated on a host that refuses `0.0.1` with `426`): `not-a-version`, `1`, `01.0` and `1.0-rc.1` are served, never `426` or `400`; a host that serves `0.0.1` records `inapplicable`. `absent-not-refused`: a header-less request is never `426` on any host, and is served on a host that refuses `0.0.1`. `no-floor-no-refusal`: a host advertising no `minClientVersion` serves `0.0.1`. Sabotage-proved against a stub host implementing `versioning.md` §1.5 exactly: malformed-read-as-`0.0`, malformed-`400`, at-floor refused, a patch-bearing value refused `400`, header-less refused and no-floor-`426` each turn their leg `executed-fail`. **The v2 reference host and openwop-app read a malformed value as `0.0` and refuse it today; the malformed leg fails on both until their fixes land** (RFC 0219 gap G1).
9
+ - **`v2-min-client-version`: a non-`2xx`, non-`426` answer to `0.0.1` is a failure, not `blocked`** (RFC 0219 decision 4), now that the header is declared. When a refusal carries `details.minClientVersion`, it must equal the advertised floor (decision 5). Sabotaged: a `403` refusal and a `details.minClientVersion` of `0.9` against a `1.0` floor each fail the leg; a `details.minClientVersion` outside the axis-1 grammar fails the envelope check.
10
+ - **The v2 error envelope binds a registered `details` schema to its own code.** `schemas/v2/error-envelope.schema.json` (generated by `scripts/generate-error-envelope.mjs`) put every registered `details` schema and an open fallback in one `oneOf`. A conforming body matched two branches, so every envelope carrying registered `details` (`run_state_conflict` with `details.runStatus`, `unsupported_stream_mode` with `details.supported`) failed validation. The envelope now uses `allOf` of `if error == <code> then details: <schema>`. `client_version_unsupported` registers OPTIONAL `details.minClientVersion` (RFC 0219 decision 5).
11
+ - **`audit-log-integrity` validates the verify body against the CLOSED `audit-verify-result.schema.json` again.** The in-test relaxation that stripped every `additionalProperties: false` is gone. It existed because the postgres and sqlite reference hosts put a per-checkpoint `verified` bit on the wire, which the schema's `Checkpoint` does not name. That bit is host-internal (the aggregate verdict is the schema's OPTIONAL `checkpointsValid`), and the hosts now drop it (openwop-examples#96). Sabotaged: a host that emits `verified` fails with `must NOT have additional properties`.
12
+ - **RFC 0033 §C's budget rule is witnessable per attempt.** New OPTIONAL seam `GET …/test/mock-ai/dispatch-budgets` (`host-sample-test-seams.md` §28; `api/seams-v2.yaml` `getMockDispatchBudgets`) returns every mock-provider call's `maxTokens` for a node, in call order. `last-dispatch-budget` kept only the latest call. The `conformance-envelope-retry-attempted` fixture now sets `maxTokens: 256`, so attempt 1 carries a budget. The schema-violation leg of `envelope-completion-distinguishes-truncation` now requires that no retry's budget exceeds the attempt before it; before, it only range-checked the last budget (`< 20000`). Dispositions: seam served → real assertion (a host that sized the retry from the run's accumulated budget, 256 → 512, records `executed-fail`); served but contract broken → `blocked`; not served → partial witness, never `blocked`.
13
+ - **`v2-id-grammar`'s events leg waits for an event instead of racing the dispatch.** It made one 1-second poll straight after creating the run, and recorded `blocked`, which is bundle-fatal, when the host had not yet recorded an event. A host that queues runs, as MyndHyve does through Cloud Tasks, legitimately has none at that instant. MyndHyve's cut 3 on 2.42.7 was denied certification by this row alone. The leg now polls until an event exists, within a scaled 20-second deadline.
14
+ - **Suite `2.42.8`**. `@openwop/spec-artifacts` moves in lockstep at the same exact pin.
15
+
3
16
  ## [2.42.7] — 2026-09-27 — audit residuals, and extension families can graduate on evidence
4
17
 
5
18
  - **`v2-sse-last-event-id-cursor` joins the core-standard floor.** It was held off until the three bundle hosts were measured. All three record it `executed-pass`: the v2 reference host on 2.42.2, MyndHyve on 2.42.0 and openwop-app on 2.42.2. Two carry `partial-witness` only on the SHOULD leg (a malformed `Last-Event-ID` is accepted, which is recorded rather than failed). No existing claim moves.
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.42.7 @openwop/spec-artifacts@2.42.7
14
+ npm install --legacy-peer-deps @openwop/openwop-conformance@2.42.8 @openwop/spec-artifacts@2.42.8
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,7 @@ 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 565 scenario files under `src/scenarios/`.
138
+ The current suite has 567 scenario files under `src/scenarios/`.
139
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.
140
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.
141
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.
@@ -481,7 +481,7 @@ Server-required (added in 1.7.0):
481
481
  | ------------- | ----------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
482
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. |
483
483
 
484
- Current source tree: 565 scenario files. Use [`coverage.md`](./coverage.md) for current grade/gap tracking.
484
+ Current source tree: 567 scenario files. Use [`coverage.md`](./coverage.md) for current grade/gap tracking.
485
485
 
486
486
  ## Remaining Gaps
487
487
 
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "package": "@openwop/spec-artifacts",
3
- "version": "2.42.7",
4
- "stampSha256": "8f82b45d0b4004faf11a49d998e0bebefd3ad7a89d82e67322aabafcd18544a1"
3
+ "version": "2.42.8",
4
+ "stampSha256": "16b28d93516147ef6c72fd88e10bed168175639ab4c3feaa92859242e93ba00b"
5
5
  }
@@ -2,7 +2,7 @@
2
2
  "id": "conformance-envelope-retry-attempted",
3
3
  "name": "Conformance: envelope.retry.attempted (RFC 0032 §B.1)",
4
4
  "version": "1.0",
5
- "description": "Drives `core.ai.structuredOutput` against the conformance-only `mock` provider. The conformance scenario POSTs a 2-entry program to `/v1/host/sample/test/mock-ai/program` (keyed by `nodeId: 'retry-attempted-structured-call'`) BEFORE starting the run: attempt 1 returns invalid JSON, attempt 2 returns a valid envelope. The host's `dispatchStructured` retry loop MUST emit exactly one `envelope.retry.attempted` event with `attempt: 2` between the two provider calls (RFC 0032 §B.1). The run terminates `completed` after the second attempt succeeds.",
5
+ "description": "Drives `core.ai.structuredOutput` against the conformance-only `mock` provider. The conformance scenario POSTs a 2-entry program to `/v1/host/sample/test/mock-ai/program` (keyed by `nodeId: 'retry-attempted-structured-call'`) BEFORE starting the run: attempt 1 returns invalid JSON, attempt 2 returns a valid envelope. The host's `dispatchStructured` retry loop MUST emit exactly one `envelope.retry.attempted` event with `attempt: 2` between the two provider calls (RFC 0032 §B.1). The run terminates `completed` after the second attempt succeeds. `maxTokens: 256` gives attempt 1 a concrete output budget, so `envelope-completion-distinguishes-truncation` can compare each attempt's budget through the `mock-ai/dispatch-budgets` seam (host-sample-test-seams.md §28): a schema-violation retry MUST NOT carry a larger budget than the attempt before it (RFC 0033 §C).",
6
6
  "nodes": [
7
7
  {
8
8
  "id": "retry-attempted-structured-call",
@@ -12,6 +12,7 @@
12
12
  "config": {
13
13
  "provider": "mock",
14
14
  "model": "mock-mini",
15
+ "maxTokens": 256,
15
16
  "outputSchema": {
16
17
  "type": "object",
17
18
  "required": ["valid"],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openwop/openwop-conformance",
3
- "version": "2.42.7",
3
+ "version": "2.42.8",
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.42.7"
59
+ "@openwop/spec-artifacts": "2.42.8"
60
60
  }
61
61
  }
package/requirements.json CHANGED
@@ -2,11 +2,11 @@
2
2
  "$comment": "GENERATED by conformance/scripts/generate-requirement-registry.mjs — do not edit. One record per it()/test() in src/scenarios. Ids: openwop.it.<file-stem>.<title-slug>[~n] (src/lib/requirement-ids.ts). A record with id null has an interpolated title; its run-time row is keyed by the rendered title and maps here by file+line only. Renamed ids need a row in requirement-aliases.json.",
3
3
  "generatedFrom": "src/scenarios/*.test.ts",
4
4
  "counts": {
5
- "files": 640,
6
- "tests": 2460,
7
- "withStableId": 2460,
5
+ "files": 642,
6
+ "tests": 2466,
7
+ "withStableId": 2466,
8
8
  "interpolatedTitles": 0,
9
- "explicitIds": 2350
9
+ "explicitIds": 2356
10
10
  },
11
11
  "records": [
12
12
  {
@@ -5534,7 +5534,7 @@
5534
5534
  {
5535
5535
  "id": "openwop.it.audit-log-integrity.host-that-claims-the-profile-advertises-required-capability-fields",
5536
5536
  "file": "audit-log-integrity.test.ts",
5537
- "line": 69,
5537
+ "line": 60,
5538
5538
  "title": "host that claims the profile advertises required capability fields",
5539
5539
  "explicitId": "openwop.it.audit-log-integrity.host-that-claims-the-profile-advertises-required-capability-fields",
5540
5540
  "citations": [
@@ -5551,7 +5551,7 @@
5551
5551
  {
5552
5552
  "id": "openwop.it.audit-log-integrity.get-v1-audit-verify-on-a-recent-range-reports-chainvalid-true",
5553
5553
  "file": "audit-log-integrity.test.ts",
5554
- "line": 92,
5554
+ "line": 83,
5555
5555
  "title": "GET /v1/audit/verify on a recent range reports chainValid: true",
5556
5556
  "explicitId": "openwop.it.audit-log-integrity.get-v1-audit-verify-on-a-recent-range-reports-chainvalid-true",
5557
5557
  "citations": [
@@ -11541,7 +11541,7 @@
11541
11541
  {
11542
11542
  "id": "openwop.it.envelope-completion-distinguishes-truncation.capabilities-envelopes-reliability-completion-when-present-conforms-to-rfc-0033",
11543
11543
  "file": "envelope-completion-distinguishes-truncation.test.ts",
11544
- "line": 103,
11544
+ "line": 104,
11545
11545
  "title": "capabilities.envelopes.reliability.completion (when present) conforms to RFC 0033 §E",
11546
11546
  "explicitId": "openwop.it.envelope-completion-distinguishes-truncation.capabilities-envelopes-reliability-completion-when-present-conforms-to-rfc-0033",
11547
11547
  "citations": [
@@ -11558,7 +11558,7 @@
11558
11558
  {
11559
11559
  "id": "openwop.it.envelope-completion-distinguishes-truncation.truncation-emits-envelope-truncated-envelope-retry-attempted-with-reason-truncat",
11560
11560
  "file": "envelope-completion-distinguishes-truncation.test.ts",
11561
- "line": 126,
11561
+ "line": 127,
11562
11562
  "title": "truncation: emits envelope.truncated + envelope.retry.attempted with reason: \"truncation\"",
11563
11563
  "explicitId": "openwop.it.envelope-completion-distinguishes-truncation.truncation-emits-envelope-truncated-envelope-retry-attempted-with-reason-truncat",
11564
11564
  "citations": [
@@ -11579,7 +11579,7 @@
11579
11579
  {
11580
11580
  "id": "openwop.it.envelope-completion-distinguishes-truncation.truncation-retry-budget-strictly-greater-than-initial-rfc-0033-b-truncationbudge",
11581
11581
  "file": "envelope-completion-distinguishes-truncation.test.ts",
11582
- "line": 151,
11582
+ "line": 152,
11583
11583
  "title": "truncation: retry budget strictly greater than initial (RFC 0033 §B truncationBudgetMultiplier)",
11584
11584
  "explicitId": "openwop.it.envelope-completion-distinguishes-truncation.truncation-retry-budget-strictly-greater-than-initial-rfc-0033-b-truncationbudge",
11585
11585
  "citations": [
@@ -11592,7 +11592,7 @@
11592
11592
  {
11593
11593
  "id": "openwop.it.envelope-completion-distinguishes-truncation.schema-violation-no-envelope-truncated-envelope-retry-attempted-reason-schema-vi",
11594
11594
  "file": "envelope-completion-distinguishes-truncation.test.ts",
11595
- "line": 175,
11595
+ "line": 176,
11596
11596
  "title": "schema-violation: NO envelope.truncated; envelope.retry.attempted reason ∈ {schema-violation, parse-error}",
11597
11597
  "explicitId": "openwop.it.envelope-completion-distinguishes-truncation.schema-violation-no-envelope-truncated-envelope-retry-attempted-reason-schema-vi",
11598
11598
  "citations": [
@@ -11609,13 +11609,18 @@
11609
11609
  {
11610
11610
  "id": "openwop.it.envelope-completion-distinguishes-truncation.schema-violation-retry-budget-unchanged-from-initial-no-budget-multiplication-on",
11611
11611
  "file": "envelope-completion-distinguishes-truncation.test.ts",
11612
- "line": 205,
11612
+ "line": 206,
11613
11613
  "title": "schema-violation: retry budget UNCHANGED from initial (no budget multiplication on this path)",
11614
11614
  "explicitId": "openwop.it.envelope-completion-distinguishes-truncation.schema-violation-retry-budget-unchanged-from-initial-no-budget-multiplication-on",
11615
11615
  "citations": [
11616
11616
  {
11617
11617
  "section": "RFCS/0033-envelope-completion-contract.md §C",
11618
11618
  "requirement": "schema-violation retry MUST NOT multiply maxTokens — budget stays at the original value (host default)"
11619
+ },
11620
+ {
11621
+ "section": "RFCS/0033-envelope-completion-contract.md §C",
11622
+ "requirement": null,
11623
+ "interpolated": true
11619
11624
  }
11620
11625
  ]
11621
11626
  },
@@ -28349,6 +28354,86 @@
28349
28354
  }
28350
28355
  ]
28351
28356
  },
28357
+ {
28358
+ "id": "openwop.it.v2-client-version-header.a-client-at-the-floor-is-not-below-it-equal-on-major-minor-the-patch-never-decid",
28359
+ "file": "v2-client-version-header.test.ts",
28360
+ "line": 79,
28361
+ "title": "a client at the floor is not below it: equal on major.minor, the patch never decides",
28362
+ "explicitId": "openwop.requirement.0219.floor-comparison",
28363
+ "citations": [
28364
+ {
28365
+ "section": "spec/v2/core/versioning.md §1.5",
28366
+ "requirement": null,
28367
+ "interpolated": true
28368
+ },
28369
+ {
28370
+ "section": "spec/v2/core/versioning.md §1.5",
28371
+ "requirement": null,
28372
+ "interpolated": true
28373
+ }
28374
+ ]
28375
+ },
28376
+ {
28377
+ "id": "openwop.it.v2-client-version-header.a-malformed-openwop-client-version-is-treated-as-absent-never-426-never-400",
28378
+ "file": "v2-client-version-header.test.ts",
28379
+ "line": 90,
28380
+ "title": "a malformed OpenWOP-Client-Version is treated as absent: never 426, never 400",
28381
+ "explicitId": "openwop.requirement.0219.malformed-not-refused",
28382
+ "citations": [
28383
+ {
28384
+ "section": "spec/v2/core/versioning.md §1.5",
28385
+ "requirement": null,
28386
+ "interpolated": true
28387
+ },
28388
+ {
28389
+ "section": "spec/v2/core/versioning.md §1.5",
28390
+ "requirement": null,
28391
+ "interpolated": true
28392
+ },
28393
+ {
28394
+ "section": "spec/v2/core/versioning.md §1.5",
28395
+ "requirement": null,
28396
+ "interpolated": true
28397
+ }
28398
+ ]
28399
+ },
28400
+ {
28401
+ "id": "openwop.it.v2-client-version-header.a-request-without-openwop-client-version-is-served-where-a-below-floor-one-is-re",
28402
+ "file": "v2-client-version-header.test.ts",
28403
+ "line": 102,
28404
+ "title": "a request without OpenWOP-Client-Version is served where a below-floor one is refused",
28405
+ "explicitId": "openwop.requirement.0219.absent-not-refused",
28406
+ "citations": [
28407
+ {
28408
+ "section": "spec/v2/core/versioning.md §1.5",
28409
+ "requirement": "a host MUST NOT answer 426 client_version_unsupported to a request that carries no OpenWOP-Client-Version"
28410
+ },
28411
+ {
28412
+ "section": "spec/v2/core/versioning.md §1.5",
28413
+ "requirement": null,
28414
+ "interpolated": true
28415
+ }
28416
+ ]
28417
+ },
28418
+ {
28419
+ "id": "openwop.it.v2-client-version-header.a-host-that-advertises-no-minclientversion-refuses-no-client-on-version-grounds",
28420
+ "file": "v2-client-version-header.test.ts",
28421
+ "line": 115,
28422
+ "title": "a host that advertises no minClientVersion refuses no client on version grounds",
28423
+ "explicitId": "openwop.requirement.0219.no-floor-no-refusal",
28424
+ "citations": [
28425
+ {
28426
+ "section": "spec/v2/core/versioning.md §1.5",
28427
+ "requirement": null,
28428
+ "interpolated": true
28429
+ },
28430
+ {
28431
+ "section": "spec/v2/core/versioning.md §1.5",
28432
+ "requirement": null,
28433
+ "interpolated": true
28434
+ }
28435
+ ]
28436
+ },
28352
28437
  {
28353
28438
  "id": "openwop.it.v2-coherence-not-in-bundle.every-corpus-ledger-id-has-a-scenario-in-src-coherence-and-none-in-src-scenarios",
28354
28439
  "file": "v2-coherence-not-in-bundle.test.ts",
@@ -29690,7 +29775,7 @@
29690
29775
  {
29691
29776
  "id": "openwop.it.v2-id-grammar.every-id-on-the-run-snapshot-matches-its-ids-schema-json-kind",
29692
29777
  "file": "v2-id-grammar.test.ts",
29693
- "line": 53,
29778
+ "line": 55,
29694
29779
  "title": "every id on the run snapshot matches its ids.schema.json kind",
29695
29780
  "explicitId": "openwop.requirement.0170.id-grammar.snapshot",
29696
29781
  "citations": [
@@ -29709,7 +29794,7 @@
29709
29794
  {
29710
29795
  "id": "openwop.it.v2-id-grammar.every-id-on-the-run-events-matches-its-ids-schema-json-kind",
29711
29796
  "file": "v2-id-grammar.test.ts",
29712
- "line": 63,
29797
+ "line": 65,
29713
29798
  "title": "every id on the run events matches its ids.schema.json kind",
29714
29799
  "explicitId": "openwop.requirement.0170.id-grammar.events",
29715
29800
  "citations": [
@@ -29727,7 +29812,7 @@
29727
29812
  {
29728
29813
  "id": "openwop.it.v2-id-grammar.a-run-id-whose-tenant-segment-is-not-the-caller-s-is-refused",
29729
29814
  "file": "v2-id-grammar.test.ts",
29730
- "line": 78,
29815
+ "line": 90,
29731
29816
  "title": "a run id whose tenant segment is not the caller's is refused",
29732
29817
  "explicitId": "openwop.requirement.0170.id-grammar.tenant-binding",
29733
29818
  "citations": [
@@ -29750,7 +29835,7 @@
29750
29835
  {
29751
29836
  "id": "openwop.it.v2-id-grammar.a-bare-unbound-run-id-in-a-major-2-path-admitted-under-the-caller-s-tenant-throu",
29752
29837
  "file": "v2-id-grammar.test.ts",
29753
- "line": 88,
29838
+ "line": 100,
29754
29839
  "title": "a bare (unbound) run id in a major-2 path: admitted under the caller's tenant through the overlap, refused 400 validation_error after it",
29755
29840
  "explicitId": "openwop.requirement.0170.id-grammar.bare-id",
29756
29841
  "citations": [
@@ -31289,7 +31374,7 @@
31289
31374
  {
31290
31375
  "id": "openwop.it.v2-min-client-version.a-client-below-minclientversion-is-refused-with-426-client-version-unsupported",
31291
31376
  "file": "v2-min-client-version.test.ts",
31292
- "line": 35,
31377
+ "line": 38,
31293
31378
  "title": "a client below minClientVersion is refused with 426 client_version_unsupported",
31294
31379
  "explicitId": "openwop.requirement.0172.min-client-version",
31295
31380
  "citations": [
@@ -31297,6 +31382,11 @@
31297
31382
  "section": "spec/v2/core/versioning.md §1.5",
31298
31383
  "requirement": "minClientVersion MUST use the <major>.<minor> grammar (axis 15, as axis 1)"
31299
31384
  },
31385
+ {
31386
+ "section": "spec/v2/core/versioning.md §1.5",
31387
+ "requirement": null,
31388
+ "interpolated": true
31389
+ },
31300
31390
  {
31301
31391
  "section": "spec/v2/core/versioning.md §1.5",
31302
31392
  "requirement": "a 426 refusal MUST carry client_version_unsupported"
@@ -31305,6 +31395,11 @@
31305
31395
  "section": "spec/v2/core/errors.md §The envelope",
31306
31396
  "requirement": null,
31307
31397
  "interpolated": true
31398
+ },
31399
+ {
31400
+ "section": "spec/v2/core/versioning.md §1.5",
31401
+ "requirement": null,
31402
+ "interpolated": true
31308
31403
  }
31309
31404
  ]
31310
31405
  },
@@ -32238,7 +32333,7 @@
32238
32333
  {
32239
32334
  "id": "openwop.it.v2-preferred-version-default.preferredversion-is-a-member-of-protocolversions",
32240
32335
  "file": "v2-preferred-version-default.test.ts",
32241
- "line": 40,
32336
+ "line": 41,
32242
32337
  "title": "preferredVersion is a member of protocolVersions[]",
32243
32338
  "explicitId": "openwop.requirement.0172.preferred-version-default.member",
32244
32339
  "citations": [
@@ -32260,7 +32355,7 @@
32260
32355
  {
32261
32356
  "id": "openwop.it.v2-preferred-version-default.a-header-less-get-well-known-openwop-is-the-preferredversion-representation",
32262
32357
  "file": "v2-preferred-version-default.test.ts",
32263
- "line": 52,
32358
+ "line": 53,
32264
32359
  "title": "a header-less GET /.well-known/openwop is the preferredVersion representation",
32265
32360
  "explicitId": "openwop.requirement.0172.preferred-version-default.headerless",
32266
32361
  "citations": [
@@ -32288,10 +32383,24 @@
32288
32383
  }
32289
32384
  ]
32290
32385
  },
32386
+ {
32387
+ "id": "openwop.it.v2-preferred-version-default.a-header-less-request-on-any-other-unversioned-path-is-served-major-2",
32388
+ "file": "v2-preferred-version-default.test.ts",
32389
+ "line": 71,
32390
+ "title": "a header-less request on any other unversioned path is served major 2",
32391
+ "explicitId": "openwop.requirement.0172.preferred-version-default.unversioned-is-v2",
32392
+ "citations": [
32393
+ {
32394
+ "section": "spec/v2/core/versioning.md §1.2, §1.3",
32395
+ "requirement": null,
32396
+ "interpolated": true
32397
+ }
32398
+ ]
32399
+ },
32291
32400
  {
32292
32401
  "id": "openwop.it.v2-preferred-version-default.through-the-overlap-preferredversion-names-a-1-x-member",
32293
32402
  "file": "v2-preferred-version-default.test.ts",
32294
- "line": 70,
32403
+ "line": 88,
32295
32404
  "title": "through the overlap preferredVersion names a 1.x member",
32296
32405
  "explicitId": "openwop.requirement.0172.preferred-version-default.overlap",
32297
32406
  "citations": [
@@ -34020,6 +34129,40 @@
34020
34129
  }
34021
34130
  ]
34022
34131
  },
34132
+ {
34133
+ "id": "openwop.it.v2-webhook-generated-secret.a-secret-less-registration-returns-the-generated-secret-and-deliveries-verify-un",
34134
+ "file": "v2-webhook-generated-secret.test.ts",
34135
+ "line": 44,
34136
+ "title": "a secret-less registration returns the generated secret, and deliveries verify under it; a supplied secret is never echoed",
34137
+ "explicitId": "openwop.requirement.0221.generated-secret-returned",
34138
+ "citations": [
34139
+ {
34140
+ "section": "RFC 0221 · webhooks.md §Surfaces",
34141
+ "requirement": "a registration that omits secret MUST get the host-generated secret back in the 201 as a non-empty string"
34142
+ },
34143
+ {
34144
+ "section": "runs.md §Create",
34145
+ "requirement": "POST /runs MUST answer 201 for the noop fixture"
34146
+ },
34147
+ {
34148
+ "section": "webhooks.md §Durability",
34149
+ "requirement": "the subscription MUST receive the run.completed delivery"
34150
+ },
34151
+ {
34152
+ "section": "RFC 0221 · webhooks.md §Surfaces",
34153
+ "requirement": "the delivery MUST verify (OpenWOP-Signature, scheme v1) under the secret the 201 returned"
34154
+ },
34155
+ {
34156
+ "section": "RFC 0221 · webhooks.md §Surfaces",
34157
+ "requirement": null,
34158
+ "interpolated": true
34159
+ },
34160
+ {
34161
+ "section": "RFC 0221 · RFC 0201 §B.6",
34162
+ "requirement": "the 201 MUST NOT echo a supplied secret"
34163
+ }
34164
+ ]
34165
+ },
34023
34166
  {
34024
34167
  "id": "openwop.it.v2-webhook-message-id-stable.every-attempt-of-one-delivery-carries-one-webhook-id-and-distinct-deliveries-car",
34025
34168
  "file": "v2-webhook-message-id-stable.test.ts",
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "$comment": "GENERATED by conformance/scripts/generate-scenario-majors.mjs (RFC 0168 §D.3). Do not edit; add a file to BOTH_MAJORS in the generator to target both majors.",
3
3
  "counts": {
4
- "files": 565,
4
+ "files": 567,
5
5
  "v1": 451,
6
- "v2": 126
6
+ "v2": 128
7
7
  },
8
8
  "majors": {
9
9
  "a2a-1-0-agent-card.test.ts": [
@@ -1317,6 +1317,9 @@
1317
1317
  "v2-chain-pin-exact.test.ts": [
1318
1318
  2
1319
1319
  ],
1320
+ "v2-client-version-header.test.ts": [
1321
+ 2
1322
+ ],
1320
1323
  "v2-coherence-not-in-bundle.test.ts": [
1321
1324
  2
1322
1325
  ],
@@ -1587,6 +1590,9 @@
1587
1590
  "v2-webhook-endpoint-verification.test.ts": [
1588
1591
  2
1589
1592
  ],
1593
+ "v2-webhook-generated-secret.test.ts": [
1594
+ 2
1595
+ ],
1590
1596
  "v2-webhook-message-id-stable.test.ts": [
1591
1597
  2
1592
1598
  ],
@@ -1,17 +1,17 @@
1
1
  {
2
2
  "_comment": "Provenance of @openwop/spec-artifacts (RFC 0168 §D.2). files: SHA-256 per file; the conformance suite compares the installed peer against dist/spec-artifacts.lock.json at start.",
3
3
  "package": "@openwop/spec-artifacts",
4
- "version": "2.42.7",
5
- "corpusTag": "v2.42.7",
4
+ "version": "2.42.8",
5
+ "corpusTag": "v2.42.8",
6
6
  "files": {
7
7
  "api/.redocly.lint-ignore.yaml": "bf5a8350b88a72fa43f59605ed8d903ed24b6cfccda5e45509c9f6ed9ee4e712",
8
8
  "api/asyncapi.yaml": "d5ecb9ee6114582be3b1f662c84bfac9ae96dae7bacb853e461168f70a8e1c7d",
9
9
  "api/grpc/openwop.proto": "c3e72bb17cba514ee98feb6434e6c9b6ea6795bfd086489ec69fd882dd1ad977",
10
10
  "api/openapi.yaml": "69464bc0e1e71ef3b543a73d4fa4f67202343d2603c80af8033f6c3619ef1b30",
11
11
  "api/redocly.yaml": "b0604c89b2ca6d5076ec25725c539dad44a741a811fe524439ee6daef8baa09f",
12
- "api/seams-v2.yaml": "32ff503cc953a0d824789241295f08504db7d8a167bd2a2d9f489bcb7cb015e8",
13
- "api/v2/asyncapi.yaml": "a7a442826ad9641640b2cc6b9ce1de9beef93b9290f19cd17cf2254ea94c932b",
14
- "api/v2/openapi.yaml": "4d631c452aeec8eea96c03ea85f14fc56ea73ca476433bf98cc9c4bf0984e820",
12
+ "api/seams-v2.yaml": "deda53b9f8917a1dc4b283e50cb47985c0fd06525c96fdf853c2fdb1a2de5cb8",
13
+ "api/v2/asyncapi.yaml": "a35292b35d003361a9a0a7997557e022641cd869a6aefcd31ab266d0c3fea215",
14
+ "api/v2/openapi.yaml": "8381ff1d3cca7c0610a5f5a885763a625630c8b36bb5c5f6cd1d3f3cceb176a7",
15
15
  "api/v2/redocly.yaml": "1e66b60e6118ad11a823bb620678be464d99dfe50a40e3e6f93ec9429b88b34c",
16
16
  "schemas/README.md": "0c0b737ffcf8f30e7d2809cec8a498232de710f41443212922ad8337cdde0b51",
17
17
  "schemas/a2a-task-state.schema.json": "75d5049dea8bd873ff0e7546f1c60c8d36c219bec7084264360be54a8be30ae1",
@@ -116,7 +116,7 @@
116
116
  "schemas/v2/artifact.schema.json": "85b9dc284d39b99d2a66fa8cbf47f2dd3d8526d3bd6cc411ad315b372de38975",
117
117
  "schemas/v2/audit-verify-result.schema.json": "2a4bfc42976717f78c10468fdba756ca6b19e49762699e6dffd418ffff08e55d",
118
118
  "schemas/v2/budget-policy.schema.json": "c7449daeb6e1d95e7a047b8c2814d3d54748697c27046972834874867d9b5f4c",
119
- "schemas/v2/capabilities.schema.json": "65bd0b18b4c00708bc582af9fb4399fd3009c4d452401c6c8ad26695b00b7235",
119
+ "schemas/v2/capabilities.schema.json": "959b7a1b8e40aeafb8b9702bd0e4b220c0beb02a6afa3ca64fe9b04b95ae3df5",
120
120
  "schemas/v2/certification-bundle.schema.json": "82ce31a1ed6c7e21b0ba88ff62a7b8b768fc3b176b1d06fce023203830b94725",
121
121
  "schemas/v2/channel-presence-payload.schema.json": "4eeb224910e6f7eee3ab5e7c3899559c25c83a26d0022e0ad195c4672611977a",
122
122
  "schemas/v2/channel-written-payload.schema.json": "ccecff3c71a3275ad8db035ac0e04db25e6f8d23cf091ddbcd5a60d1be1cbb6b",
@@ -142,7 +142,7 @@
142
142
  "schemas/v2/envelopes/schema.request.schema.json": "be2c5f7aa881cdd252ccff47ac02193659e1393633a1541f4297383205d384a2",
143
143
  "schemas/v2/envelopes/schema.response.schema.json": "d049a081267c06c28879c331b147f3ed821b14ccfd93005c67aff64db0a8c68e",
144
144
  "schemas/v2/envelopes/ui.a2ui-surface.schema.json": "e92a2548ba65454c9e0a34f72ea6768d18f59e61fa5da7668d0b2d5baddaddc8",
145
- "schemas/v2/error-envelope.schema.json": "b812a33626efd68d492269ff4a713e5a91a59d7071eb2470db99757f260e5d47",
145
+ "schemas/v2/error-envelope.schema.json": "467d7109c54bff71a6cfd457d3bd65a23ca34b8e5577e742d080692771a78ebf",
146
146
  "schemas/v2/eval-summary.schema.json": "2f384daf05b8f3b4f604688cae504d6d9971ba41b8c771e55fe9a17e09e86b7a",
147
147
  "schemas/v2/export-bundle.schema.json": "bd28905a7729437ab7f2ad8bb33e13a371b2bf5605a7eb2962a0d246de312d69",
148
148
  "schemas/v2/form-content-pack-manifest.schema.json": "a1942a124e250a6e9fbdb6b811c5a125e78fbcbcaa46dfc71d0636f24710a339",
@@ -202,51 +202,53 @@
202
202
  "schemas/workload-identity.schema.json": "64ed93e12cc9f0657e56ff9b8f045e5d68d397df5276d103c07f42c7f34f0579",
203
203
  "schemas/workspace-file-create.schema.json": "70a23b92c2958c0165d7d29aea9020a045dc7816af82c80c5f50d0aac19c4856",
204
204
  "schemas/workspace-file.schema.json": "464de85c2a068243084ee9c1d969bc7cd5d8f7948574e58450d6493c38a0e1e4",
205
- "spec/v1/alias-detectors.json": "2401fcb1c18cdd688c018b3d716ae6bca85c5e356220ee2b793bd9d81872412d",
205
+ "spec/v1/alias-detectors.json": "29f21e0fb2c7a232a92a5c19b4cba3a86bc3b5e16f06780452f0421c66c6ec38",
206
206
  "spec/v1/capability-declaration-classes.json": "e7729aed5c4b4e1dd02abab0530f14cc95f5d4070fe51fb139e7f5cccefa00c6",
207
- "spec/v1/core-standard-manifest.json": "5c8757f4745d701dd79b34c63e4ba5ef9623292cc8ae7c661f2976f4a5b142a1",
208
- "spec/v1/deprecations.json": "2d03f4729810280147ea08c630ea65434f2dc5370567d0aac41c3595f8337d40",
207
+ "spec/v1/core-standard-manifest.json": "1359a69ad091b8bd3fb8fb7b641b32496d0d7a70d40cb0d1b93c6f171566276e",
208
+ "spec/v1/deprecations.json": "46df807fb4adf0aae24671c20f69975cf3496e3bc3a521cb7e6ea556bdce563c",
209
209
  "spec/v1/deprecations.schema.json": "3e393c405d2a41b467d8c5e3c468549078df1ce6a6d2588fc488097b95d9b55b",
210
210
  "spec/v1/event-codemap.json": "3da60d884157793a360da532a9fcbbfb5285636db325a74cec94b34622186d97",
211
211
  "spec/v1/event-codemap.schema.json": "d05933b2e88103aff51a2f774df97b9bda065e0fc88dbd9b114cc47a32189174",
212
212
  "spec/v1/extensions.json": "79a60754aa16cbdbdb604f8e5c00038af535a1690a4d07bd5ff7c38f0256a7bc",
213
- "spec/v1/gaps.json": "456f3453ce453f0149f3d95c1ac26e669ae7d2bf2541a26320860105c4d55566",
213
+ "spec/v1/gaps.json": "2b032d91d614a758db347be4f5e617b8ac3c4622b6f95ffcfa928276c0fb7377",
214
214
  "spec/v1/gaps.schema.json": "8fd83259f556553c9df0f53e7a82ca8c2a4771d471197f69b4e2ae0ceeacfd66",
215
- "spec/v1/migrations.json": "8839497d6a4830b53fa7867014a8336f0d725ef2346352e54fb580b7947897af",
215
+ "spec/v1/migrations.json": "2efcd9064b2bc65a84a3b3d281aa86a91efd002690c1f4f14e8335c4a18194d1",
216
216
  "spec/v1/migrations.schema.json": "886779aa6c22e646db097f5df210adb018a4dd14a7b815465a18c8a7056c8f72",
217
217
  "spec/v1/operation-path-manifest.json": "fada0342815fb129c1ae61f1829bd6e34417717408ce86fcc54280f373c19e74",
218
218
  "spec/v1/spec-gaps.json": "ea9612af55bea9dae3e82223d4f714243f8f590b0193f1188257b13491a38afc",
219
- "spec/v2/README.md": "4cb63710360e9322a1c7c04aacbad5ce890f79c2da24e7da5ac3bf1fc90bebd7",
219
+ "spec/v2/README.md": "4fa067d377580495e60baf08a4eafc3214b8ca48d8fb270bf91fc5066fad7186",
220
220
  "spec/v2/core/capabilities.md": "225269a257421ebe677070dd13b05f6f297a2a343916e440b7428b839e2da9a4",
221
221
  "spec/v2/core/conformance.md": "e7bd7d54062f28801071165a1b92edbeee3774bb3710f31eed75817139b66d95",
222
222
  "spec/v2/core/connection-packs.md": "3168d2c8f1a59417a7256a678f4203570be434f64dead6b11fdec86b352686df",
223
223
  "spec/v2/core/conversation.md": "6db43d8160bc45d3944aa1151816a6f0fd1669d59d7f822267a2d6cc9db22990",
224
- "spec/v2/core/errors.md": "7ba57f0922673038a5f8479b47da88810bf15145ed14a0cd1ef43214e8eb27b5",
224
+ "spec/v2/core/errors.md": "d14649ee60f8260ec163464163409ad7f50cb86fee7b846e0190f66070b3774b",
225
225
  "spec/v2/core/events.md": "234338deb97be648731e8ab2126a90b7b8ad0a7070d98ffc85ee4689ff059791",
226
- "spec/v2/core/form-content-packs.md": "347b1c649864360bf2731e6e047a3e26b0783530201185827e85359e90f0b233",
227
- "spec/v2/core/headers.md": "89839a4e1ad793e565afe672c8e003ab3852ab7be89145a2510860ac5ca4de5c",
226
+ "spec/v2/core/form-content-packs.md": "41d40afe0a3760bcd792a4c406e16f28b0ab0aa7b0925323110d3fa2730d5400",
227
+ "spec/v2/core/headers.md": "5cb1624fa63acc81992b6a6d842ec392cf7271aaf2f65d1f83c8c4fa286e6730",
228
228
  "spec/v2/core/host-services.md": "15923cceb95cba148a32e0944f6a2d1d25e9cc36b0c169b67ee42d483bcac800",
229
+ "spec/v2/core/i18n.md": "846c6d2878fc8454c2ae0a1576f6b6e6c8958d47187a6c96cc5826ca3c3e2e91",
229
230
  "spec/v2/core/idempotency.md": "21830b7d23dd03e9e8ba4b0de253dd001fc960f166310d6b6b60b1c33cf60b30",
230
- "spec/v2/core/identity.md": "fb6606a322a9167b8ab91402cf7214b6ced3e439eb4b6f9240a0739d9a3e58f1",
231
+ "spec/v2/core/identity.md": "acb7f739729c69a1f40dcaa5b1d873ba9f32993c17bfd297f5fa56351cf47c69",
231
232
  "spec/v2/core/interop.md": "3cfa2af70ae6ba1f4b4268447d46d1d2f5fb65f7917f15bad159d9779d25b1a6",
232
233
  "spec/v2/core/interrupt.md": "b31b3f3be7888043ef38c82367a8d743a775c2b6bd499f7da44355ff00452435",
233
234
  "spec/v2/core/node-pack-runtimes.md": "d52966319c53aaa03575025915b73b470a42832c039c41d73aff6931758630cb",
234
235
  "spec/v2/core/oauth.md": "57f167d899993b6959c243e4102819f4014b365299b9b2b253decd2781e6b8b7",
235
- "spec/v2/core/overview.md": "bb6b088d5ffd3ea3f336bba37ef2ca45e7b0bee2c7f25ecb2d78d3f25a3fcee1",
236
+ "spec/v2/core/overview.md": "3dbc8c0cf7474b31816b1a12412c1edf5597066c38edcdbca4dd2e465880f65b",
236
237
  "spec/v2/core/packs.md": "26b0fc3b81f98adbdf70796482db89fa81a6ce3da6f830cd5e8b3d71541c497f",
237
238
  "spec/v2/core/persistence.md": "bba672ed3bf6d83af6e4e65c0f4f480fc21fe12207811e321306af2108d9010d",
239
+ "spec/v2/core/portability.md": "a35fbe66b8b5ccf63cf0216157d4b61d4c47f6dc8e757c830569ddf244e21e8a",
238
240
  "spec/v2/core/replay.md": "a5be4e05242f25f8b5a8a4fb31bd359d5abb4bf34fa8e536403a8ee5779ab876",
239
- "spec/v2/core/runs.md": "26b459d522901ccf752470c6c9c902af435d7e9690755438ddb24c150424341d",
241
+ "spec/v2/core/runs.md": "72a94c27d884036d84290e9b108702e98b0c6b2fb598213bbda44fba9c9f860d",
240
242
  "spec/v2/core/security-defaults.md": "179fdd2efcc1454453243f8ab6473335dfd4389e70cbbc2669f5b83714092973",
241
243
  "spec/v2/core/tool-catalog.md": "6b54b8ce221107de19c718f374a720123c6a5f55db4054e6625f73d06e0a442f",
242
- "spec/v2/core/versioning.md": "000fa2db433039fc4f2b0ad7dc2cbf7aadc10eae60322018727da6f99eda4dc3",
243
- "spec/v2/core/webhooks.md": "0398db27b2ddd97bc2a9a9b1236b5139451765462789dee9c3a8cc6b7b81e666",
244
+ "spec/v2/core/versioning.md": "2bfe16a72a909c46b4585fab39dc9e94c97506ed95df6b1b527f97b4cd3684ef",
245
+ "spec/v2/core/webhooks.md": "78fe97ab5bdee20218183efe32abf5ba1996705d757cb37c8d6a4d808c44199f",
244
246
  "spec/v2/core/workflow-chain-packs.md": "35a957f1dd22016febd76bf2c4dfb79cda58b95d8efd8893b5e5babda9f79cf3",
245
247
  "spec/v2/corrections.json": "b19f8a80fce7e6473012a20a216e358dd38ae7411e6a348f8031e325ed1a0a01",
246
248
  "spec/v2/corrections.schema.json": "46bbffb954d13bd9995aebc912ceeb2544f7064331abf25fd6ab85c81a5cec94",
247
- "spec/v2/declaration.json": "6ed50f204eaac5885188237378feb0f555c4290455861cad133141b8d1296eff",
249
+ "spec/v2/declaration.json": "1253a5590ec2d9ba511d15e8a9d9da5e6926c4daa981a26b73b29bc915477013",
248
250
  "spec/v2/declaration.schema.json": "ef951633e4070899f64828f5cf1b5f4870270a0360f81dbb49b5b4d6cd407550",
249
- "spec/v2/errors.json": "034d5c77088c594e104de783ce7ca55c8174923b4fd177593da231f3bfcacaaa",
251
+ "spec/v2/errors.json": "b89664e9e0e7603253712263ce0843ce607e276c67d0acaf4a680c39bb2bd682",
250
252
  "spec/v2/errors.schema.json": "47404ad53e6147cf9017b813483712466573e419fb33e6c4b6c12d3e7deba1bc",
251
253
  "spec/v2/event-codemap.json": "37869a03cdfd8758b57e6f350641a6ebbd383231cebda51f5473ad85f94ab74c",
252
254
  "spec/v2/event-codemap.schema.json": "b173f2a9bcc0bf9b62a474ce3b91e1431d597e64fd1560fc58662cd8606eaf9c",
@@ -263,7 +265,7 @@
263
265
  "spec/v2/ext/knowledge/README.md": "67be1dc97e5cdc9527d56f4836dc96841548a9c2f8f964777825d56fdf9f0e53",
264
266
  "spec/v2/ext/launchStudio/README.md": "c732bb855252c31ee1e971cbc2936f2ca29b234c111d548c648095634507babb",
265
267
  "spec/v2/ext/messaging/README.md": "32c222c0bce515e75b62a5a44b9f09d0e23c1b40f424e1ab8309deefd296c4b6",
266
- "spec/v2/ext/portability/README.md": "0df93357a90ca6317f16a2bdd45b5073965a3d52a020daf900a74f5fcd709bf8",
268
+ "spec/v2/ext/portability/README.md": "c59423ee446638b748a911b9e97e803c4786157be6120f656f0a060586bc7b82",
267
269
  "spec/v2/ext/provider-idempotency/README.md": "9d1bccef0ec16a19a3370ae79c2f5a1726ce0d1d25a51352907f288a2ccb15d7",
268
270
  "spec/v2/ext/provider-idempotency/registry.json": "5b5fecad604fb4ab38da39f5abe697a4e57b05406c398f8afeb377331b2bd965",
269
271
  "spec/v2/ext/restTransport/README.md": "d485417310616b2f1da97106d970293f3f5712f6cadb6b840f5ac84ee28bdda6",
@@ -295,9 +297,9 @@
295
297
  "spec/v2/path-manifest.json": "17298a245c5da9a70aa7ab0b7f2db4277e06209d6fcdd7187a8b4cea179c08f3",
296
298
  "spec/v2/peer-dependency-aliases.json": "d10299280abee08258502925bc327293ee413e0108cd6e6ec75ff6110653308d",
297
299
  "spec/v2/profiles.json": "180beb9ef2d161766fa23b2987e7e829851e3d316273147cf6a4a3f324befd6b",
298
- "spec/v2/release.json": "0b9bc03ec7efd4cfacfe6ca72b57655787ee9cc5473e9a9b7af21bb1f25350a7",
300
+ "spec/v2/release.json": "f2f87ba5a5476f2213b3486a2618ef3c7381b47c96dd3f082bee0e07ac4a35bc",
299
301
  "spec/v2/retention-floors.json": "eaf3722d95c79947af1d4269ef85117e126518c588cfcf1a2b21b97269f51624",
300
- "spec/v2/surface-baseline.json": "549832c2de97b7c7aed07934c1c73c2709ac226dc0cb71fc8c141010bc4928f1"
302
+ "spec/v2/surface-baseline.json": "fce9d8eeea653476e09b19e525a0b104caab6c51a0b7b427d7d941e2b7118573"
301
303
  },
302
- "corpusCommit": "f6209c3b2095005b844b44d5046873e4170d9736"
304
+ "corpusCommit": "0ed567fece2f9c5c5ad337fed92f6926ad7547af"
303
305
  }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Per-attempt dispatch budgets — `GET /v1/host/sample/test/mock-ai/dispatch-budgets`
3
+ * (host-sample-test-seams.md §28; v2 `api/seams-v2.yaml` `getMockDispatchBudgets`).
4
+ *
5
+ * The companion `last-dispatch-budget` seam reports only the most recent call's
6
+ * `maxTokens`, so a retry's budget cannot be compared with the attempt before
7
+ * it. This seam returns every call's budget in call order. OPTIONAL: an
8
+ * unserved seam (404/405/501) is `unserved`, so the caller falls back to its
9
+ * pre-seam partial witness; a served seam that answers anything else is
10
+ * `broken`, which the caller records as `blocked`.
11
+ */
12
+
13
+ import { driver } from './driver.js';
14
+
15
+ export type DispatchBudgets =
16
+ | { ok: true; attempts: Array<number | null> }
17
+ | { ok: false; unserved: true; reason: string }
18
+ | { ok: false; unserved: false; reason: string };
19
+
20
+ const UNSERVED = new Set([404, 405, 501]);
21
+
22
+ export async function readDispatchBudgets(nodeId: string): Promise<DispatchBudgets> {
23
+ const res = await driver.get(`/v1/host/sample/test/mock-ai/dispatch-budgets?nodeId=${encodeURIComponent(nodeId)}`);
24
+ if (UNSERVED.has(res.status)) {
25
+ return { ok: false, unserved: true, reason: `the dispatch-budgets seam is not served (${res.status})` };
26
+ }
27
+ const attempts = (res.json as { attempts?: unknown } | undefined)?.attempts;
28
+ if (res.status !== 200 || !Array.isArray(attempts)) {
29
+ return { ok: false, unserved: false, reason: `the dispatch-budgets seam answered ${res.status} without an attempts[] array (host-sample-test-seams.md §28)` };
30
+ }
31
+ const out: Array<number | null> = [];
32
+ for (const a of attempts) {
33
+ const m = (a as { maxTokens?: unknown } | null)?.maxTokens;
34
+ if (m !== null && !(typeof m === 'number' && Number.isInteger(m))) {
35
+ return { ok: false, unserved: false, reason: `the dispatch-budgets seam returned an entry whose maxTokens is ${JSON.stringify(m)}, not an integer or null (host-sample-test-seams.md §28)` };
36
+ }
37
+ out.push(m);
38
+ }
39
+ return { ok: true, attempts: out };
40
+ }
@@ -27,23 +27,14 @@ import { join } from 'node:path';
27
27
  import { SCHEMAS_DIR } from '../lib/paths.js';
28
28
 
29
29
  /**
30
- * audit-verify-result.schema.json with every `additionalProperties: false`
31
- * relaxed: the REQUIRED members and their types are enforced, extra members
32
- * are not (the reference postgres host emits `checkpointsValid` and a
33
- * per-checkpoint `verified` bit; convicting extras is a separate decision).
30
+ * audit-verify-result.schema.json, CLOSED: every `additionalProperties: false`
31
+ * is enforced. (Until 2026-09-27 the suite stripped them because the reference
32
+ * postgres/sqlite hosts emitted a per-checkpoint `verified` bit the schema does
33
+ * not name; the hosts now keep that bit host-internal. The aggregate verdict is
34
+ * the schema's OPTIONAL `checkpointsValid`.)
34
35
  */
35
- function relaxAdditional(node: unknown): unknown {
36
- if (Array.isArray(node)) return node.map(relaxAdditional);
37
- if (node === null || typeof node !== 'object') return node;
38
- const out: Record<string, unknown> = {};
39
- for (const [k, v] of Object.entries(node as Record<string, unknown>)) {
40
- if (k === 'additionalProperties' && v === false) continue;
41
- out[k] = relaxAdditional(v);
42
- }
43
- return out;
44
- }
45
- const VERIFY_RESULT_SCHEMA = relaxAdditional(
46
- JSON.parse(readFileSync(join(SCHEMAS_DIR, 'audit-verify-result.schema.json'), 'utf8')),
36
+ const VERIFY_RESULT_SCHEMA = JSON.parse(
37
+ readFileSync(join(SCHEMAS_DIR, 'audit-verify-result.schema.json'), 'utf8'),
47
38
  ) as Record<string, unknown>;
48
39
 
49
40
  interface AuditIntegrityCaps {
@@ -33,7 +33,8 @@ import { pollUntilTerminal } from '../lib/polling.js';
33
33
  import { isFixtureAdvertised } from '../lib/fixtures.js';
34
34
  import { capabilityFamily } from '../lib/discovery-capabilities.js';
35
35
  import { req } from '../lib/requirement-ids.js';
36
- import { softSkip } from '../lib/soft-skip.js';
36
+ import { softSkip, blockedDespiteAssertions } from '../lib/soft-skip.js';
37
+ import { readDispatchBudgets } from '../lib/mock-ai-budgets.js';
37
38
 
38
39
  const HTTP_SKIP = !process.env.OPENWOP_BASE_URL;
39
40
  /**
@@ -210,26 +211,61 @@ describe.skipIf(HTTP_SKIP)('envelope-completion-distinguishes-truncation: schema
210
211
  ]);
211
212
  if (seed.status === 404) return softSkip('blocked', 'precondition not met — `seed.status === 404` returned early (seam, prior step, or fixture unavailable)');
212
213
 
213
- await startRunAndRead(SCHEMA_VIOLATION_FIXTURE);
214
- const budget = await lastBudget(SCHEMA_VIOLATION_FIXTURE);
215
- if (budget === null) return softSkip('blocked', 'precondition not met — `budget === null` returned early (seam, prior step, or fixture unavailable)');
216
- // The schema-violation fixture doesn't set maxTokens explicitly →
217
- // budget snapshots whatever the host's default is on each call.
218
- // The KEY invariant: the retry call's budget MUST NOT be multiplied
219
- // (the truncation path doubles; this path keeps the same). The
220
- // budget on the last call equals the budget on the first call.
221
- // Without a per-call history hook, we can't strictly compare; we
222
- // assert the budget didn't grow into the truncation-path range
223
- // (which would be ≥2× the default — typically 8000 for the
224
- // sample's structuredOutput dispatch path).
225
- if (budget !== null) {
214
+ const result = await startRunAndRead(SCHEMA_VIOLATION_FIXTURE);
215
+ const REQ_ID = 'openwop.it.envelope-completion-distinguishes-truncation.schema-violation-retry-budget-unchanged-from-initial-no-budget-multiplication-on';
216
+
217
+ // Suite 2.42.7: the per-attempt witness. `last-dispatch-budget` reports only
218
+ // the most recent call, so the retry's budget could not be compared with the
219
+ // attempt before it — a host that doubled 256 → 512 on a schema-violation
220
+ // retry passed the `< 20_000` range check below. `dispatch-budgets`
221
+ // (host-sample-test-seams.md §28) returns every call's budget in call order,
222
+ // and the fixture now sets `maxTokens: 256` so attempt 1 carries one. A served
223
+ // seam that breaks its contract is `blocked`; an UNSERVED one falls through to
224
+ // the pre-seam partial witness below (the seam is newer than this leg).
225
+ const budgets = await readDispatchBudgets(NODE_OF[SCHEMA_VIOLATION_FIXTURE] as string);
226
+ if (!budgets.ok && !budgets.unserved) return blockedDespiteAssertions(budgets.reason);
227
+ if (!budgets.ok) {
228
+ const budget = await lastBudget(SCHEMA_VIOLATION_FIXTURE);
229
+ if (budget === null) return softSkip('blocked', 'precondition not met — `budget === null` returned early (seam, prior step, or fixture unavailable)');
230
+ // Pre-seam path: only the most recent call's budget is visible, so it is
231
+ // range-checked against the truncation path (≥2× the default — typically
232
+ // 8000 for the sample's structuredOutput dispatch path), not compared.
226
233
  expect(
227
234
  budget,
228
- req('openwop.it.envelope-completion-distinguishes-truncation.schema-violation-retry-budget-unchanged-from-initial-no-budget-multiplication-on',
235
+ req(REQ_ID,
229
236
  'RFCS/0033-envelope-completion-contract.md §C',
230
237
  'schema-violation retry MUST NOT multiply maxTokens — budget stays at the original value (host default)',
231
238
  ),
232
239
  ).toBeLessThan(20_000);
240
+ // partial-witness-ok: the host does not serve the optional dispatch-budgets
241
+ // seam (host-sample-test-seams.md §28), so the retry's budget is range-checked,
242
+ // not compared with the attempt before it. The acceptance predicate refuses a
243
+ // partial-witness row.
244
+ return softSkip('inapplicable', `partial witness — the §C budget rule is only range-checked: ${budgets.reason}; the most recent call's maxTokens (${budget}) is below the truncation-path range, but a retry that grew the budget within that range is not detected`);
245
+ }
246
+ if (result === null) return blockedDespiteAssertions('the run create or event-log read failed — the retry count the budgets are checked against was never observed');
247
+ const retries = result.events.filter((e) => e.type === 'envelope.retry.attempted').length;
248
+ // RFC 0033 §C: a host MAY retry. No retry means no retry budget to compare;
249
+ // the sibling leg above convicts a host that should have retried and did not.
250
+ // partial-witness-ok: no retry happened, so there is no retry budget to observe.
251
+ if (retries === 0) return softSkip('inapplicable', 'the host made no schema-violation retry (RFC 0033 §C: MAY retry) — there is no retry budget to compare');
252
+ if (budgets.attempts.length < retries + 1) {
253
+ return blockedDespiteAssertions(`the dispatch-budgets seam recorded ${budgets.attempts.length} call(s) but the run emitted ${retries} envelope.retry.attempted event(s), i.e. ${retries + 1} calls — the seam does not report every attempt (host-sample-test-seams.md §28)`);
254
+ }
255
+ const attempts = budgets.attempts;
256
+ if (!attempts.every((m): m is number => m !== null)) {
257
+ // partial-witness-ok: the seam is served but at least one call carried no
258
+ // budget (maxTokens null), so no two attempts' budgets can be compared.
259
+ return softSkip('inapplicable', `partial witness — the §C budget rule is UNWITNESSED: the dispatch-budgets seam shows a call with no output budget (per-attempt budgets: ${JSON.stringify(attempts)}), so no retry's budget can be compared with the attempt before it`);
260
+ }
261
+ for (let i = 1; i < attempts.length; i++) {
262
+ expect(
263
+ attempts[i]! <= attempts[i - 1]!,
264
+ req(REQ_ID,
265
+ 'RFCS/0033-envelope-completion-contract.md §C',
266
+ `schema-violation retries SHALL NOT include an increased output budget — attempt ${i + 1} carried maxTokens ${attempts[i]} after attempt ${i} carried ${attempts[i - 1]} (per-attempt budgets: ${JSON.stringify(attempts)})`,
267
+ ),
268
+ ).toBe(true);
233
269
  }
234
270
  });
235
271
  });
@@ -0,0 +1,124 @@
1
+ /**
2
+ * v2 — `OpenWOP-Client-Version` (suite 2.42.8; RFC 0219;
3
+ * `spec/v2/core/versioning.md` §1.5 "Client precedence and minClientVersion").
4
+ *
5
+ * A client announces the protocol version it implements in
6
+ * `OpenWOP-Client-Version: <major>.<minor>[.<patch>]`. A host that advertises
7
+ * `minClientVersion` MAY refuse a client below it with `426
8
+ * client_version_unsupported`, and MUST NOT refuse anything else on version
9
+ * grounds. Every leg probes `GET /.well-known/openwop` under
10
+ * `OpenWOP-Version: 2.0` (the discovery document is not exempt from refusal).
11
+ *
12
+ * floor-comparison (gated on minClientVersion) `<floor>`, `<floor>.0` and
13
+ * `<floor>.99` are served: equal on major.minor is not
14
+ * below, and the patch never decides.
15
+ * malformed-not-refused (gated on a host that refuses `0.0.1` with 426) the
16
+ * malformed values `not-a-version`, `1`, `01.0` and
17
+ * `1.0-rc.1` are treated as absent and served — never
18
+ * 426, never 400. A host that serves `0.0.1` does not
19
+ * exercise the refusal, so a served malformed value
20
+ * proves nothing there: `inapplicable`.
21
+ * absent-not-refused a header-less request is never 426 (every host); on a
22
+ * host that refuses `0.0.1` with 426, it is also served.
23
+ * no-floor-no-refusal (hosts advertising NO minClientVersion) `0.0.1` is
24
+ * served, not 426. None of today's certified hosts is in
25
+ * this state, so it records `inapplicable` on each.
26
+ *
27
+ * Unwitnessed and recorded in RFC 0219's falsifiability table: a host choosing
28
+ * a major or a representation from the header, and the header used as an
29
+ * authentication or authorization input.
30
+ */
31
+
32
+ import { describe, it, expect } from 'vitest';
33
+ import { driver, type OpenWOPResponse } from '../lib/driver.js';
34
+ import { v2Discovery } from '../lib/v2.js';
35
+ import { softSkip } from '../lib/soft-skip.js';
36
+ import { req } from '../lib/requirement-ids.js';
37
+
38
+ const DOC = 'spec/v2/core/versioning.md §1.5';
39
+ const PATH = '/.well-known/openwop';
40
+ const FLOOR = /^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$/;
41
+ /** Below any floor a host can advertise except `0.0`, which nothing is below. */
42
+ const BELOW = '0.0.1';
43
+ const MALFORMED = ['not-a-version', '1', '01.0', '1.0-rc.1'] as const;
44
+
45
+ async function discovery(): Promise<Record<string, unknown> | null> {
46
+ try { return await v2Discovery(); } catch { return null; }
47
+ }
48
+ async function probe(clientVersion: string | null): Promise<OpenWOPResponse | null> {
49
+ const headers: Record<string, string> = { 'OpenWOP-Version': '2.0' };
50
+ if (clientVersion !== null) headers['OpenWOP-Client-Version'] = clientVersion;
51
+ try { return await driver.get(PATH, { authenticated: false, headers }); } catch { return null; }
52
+ }
53
+ const served = (r: OpenWOPResponse): boolean => r.status >= 200 && r.status < 300;
54
+
55
+ /** The advertised floor, or a skip reason. */
56
+ async function advertisedFloor(): Promise<{ floor: string } | { skip: [kind: 'blocked' | 'inapplicable', reason: string] }> {
57
+ const doc = await discovery();
58
+ if (!doc) return { skip: ['blocked', 'v2 discovery unreachable — /.well-known/openwop did not answer 200 with a JSON body under OpenWOP-Version: 2.0'] };
59
+ const floor = doc['minClientVersion'];
60
+ if (floor === undefined) return { skip: ['inapplicable', 'the host does not advertise minClientVersion — the floor legs are gated on a host that sets it'] };
61
+ // The floor's own grammar is v2-min-client-version's row; a floor this leg cannot read compares nothing.
62
+ if (typeof floor !== 'string' || !FLOOR.test(floor)) return { skip: ['inapplicable', `minClientVersion ${JSON.stringify(floor)} is not <major>.<minor> — judged by v2-min-client-version, nothing to compare here`] };
63
+ if (floor === '0.0') return { skip: ['inapplicable', 'minClientVersion is 0.0 — no well-formed client version is below it, so no refusal can be provoked'] };
64
+ return { floor };
65
+ }
66
+
67
+ /** Gate for the legs that need a host which actually refuses a below-floor client. */
68
+ async function refusingHost(): Promise<{ floor: string } | { skip: [kind: 'blocked' | 'inapplicable', reason: string] }> {
69
+ const f = await advertisedFloor();
70
+ if ('skip' in f) return f;
71
+ const control = await probe(BELOW);
72
+ if (control === null) return { skip: ['blocked', `GET ${PATH} with OpenWOP-Client-Version: ${BELOW} unreachable (fetch failed)`] };
73
+ if (served(control)) return { skip: ['inapplicable', `the host advertises minClientVersion ${f.floor} but served a client announcing ${BELOW} (${control.status}) — refusal is a MAY; a served header-less or malformed request distinguishes nothing on a host that refuses nobody`] };
74
+ if (control.status !== 426) return { skip: ['blocked', `the control (${BELOW}, below minClientVersion ${f.floor}) answered ${control.status} — neither served nor 426; v2-min-client-version records that as a failure, and this leg has no refusal to compare against`] };
75
+ return f;
76
+ }
77
+
78
+ describe('v2 OpenWOP-Client-Version (RFC 0219 — versioning.md §1.5)', () => {
79
+ it('a client at the floor is not below it: equal on major.minor, the patch never decides', async () => {
80
+ const f = await advertisedFloor();
81
+ if ('skip' in f) return softSkip(...f.skip);
82
+ for (const v of [f.floor, `${f.floor}.0`, `${f.floor}.99`]) {
83
+ const res = await probe(v);
84
+ if (res === null) return softSkip('blocked', `GET ${PATH} with OpenWOP-Client-Version: ${v} unreachable (fetch failed)`);
85
+ expect(res.status, req('openwop.requirement.0219.floor-comparison', DOC, `a client announcing ${v} is not below minClientVersion ${f.floor} (major and minor compared as integers; the patch never decides) and MUST be served, not refused (got ${res.status})`)).not.toBe(426);
86
+ expect(served(res), req('openwop.requirement.0219.floor-comparison', DOC, `a well-formed OpenWOP-Client-Version at the floor MUST NOT make the request fail — ${v} got ${res.status}`)).toBe(true);
87
+ }
88
+ });
89
+
90
+ it('a malformed OpenWOP-Client-Version is treated as absent: never 426, never 400', async () => {
91
+ const f = await refusingHost();
92
+ if ('skip' in f) return softSkip(...f.skip);
93
+ for (const v of MALFORMED) {
94
+ const res = await probe(v);
95
+ if (res === null) return softSkip('blocked', `GET ${PATH} with OpenWOP-Client-Version: ${v} unreachable (fetch failed)`);
96
+ expect(res.status, req('openwop.requirement.0219.malformed-not-refused', DOC, `a value outside the grammar (${JSON.stringify(v)}) MUST be treated as absent, so it MUST NOT be refused 426 client_version_unsupported (floor ${f.floor})`)).not.toBe(426);
97
+ expect(res.status, req('openwop.requirement.0219.malformed-not-refused', DOC, `a value outside the grammar (${JSON.stringify(v)}) MUST NOT produce a 400`)).not.toBe(400);
98
+ expect(served(res), req('openwop.requirement.0219.malformed-not-refused', DOC, `a malformed value is treated as absent and a header-less request is served — ${JSON.stringify(v)} got ${res.status}`)).toBe(true);
99
+ }
100
+ });
101
+
102
+ it('a request without OpenWOP-Client-Version is served where a below-floor one is refused', async () => {
103
+ // A 426 to a header-less request is a violation on ANY host, floor or not — probe it before the gate,
104
+ // because a host that refuses it also refuses the header-less discovery read the gate depends on.
105
+ const res = await probe(null);
106
+ if (res === null) return softSkip('blocked', `GET ${PATH} without OpenWOP-Client-Version unreachable (fetch failed)`);
107
+ expect(res.status, req('openwop.requirement.0219.absent-not-refused', DOC, 'a host MUST NOT answer 426 client_version_unsupported to a request that carries no OpenWOP-Client-Version')).not.toBe(426);
108
+ const f = await refusingHost();
109
+ // partial-witness-ok: the header-less request was not refused 426 (asserted above, binding on every host);
110
+ // that it is SERVED distinguishes something only on a host that refuses a below-floor client.
111
+ if ('skip' in f) return softSkip(...f.skip);
112
+ expect(served(res), req('openwop.requirement.0219.absent-not-refused', DOC, `a host MUST NOT refuse a request because it omits OpenWOP-Client-Version (got ${res.status})`)).toBe(true);
113
+ });
114
+
115
+ it('a host that advertises no minClientVersion refuses no client on version grounds', async () => {
116
+ const doc = await discovery();
117
+ if (!doc) return softSkip('blocked', 'v2 discovery unreachable — /.well-known/openwop did not answer 200 with a JSON body under OpenWOP-Version: 2.0');
118
+ if (doc['minClientVersion'] !== undefined) return softSkip('inapplicable', `the host advertises minClientVersion ${JSON.stringify(doc['minClientVersion'])} — this leg is for a host with no floor`);
119
+ const res = await probe(BELOW);
120
+ if (res === null) return softSkip('blocked', `GET ${PATH} with OpenWOP-Client-Version: ${BELOW} unreachable (fetch failed)`);
121
+ expect(res.status, req('openwop.requirement.0219.no-floor-no-refusal', DOC, `a host that advertises no minClientVersion MUST NOT answer 426 client_version_unsupported (a client announcing ${BELOW})`)).not.toBe(426);
122
+ expect(served(res), req('openwop.requirement.0219.no-floor-no-refusal', DOC, `a well-formed OpenWOP-Client-Version MUST NOT make the request fail on a host with no floor (got ${res.status})`)).toBe(true);
123
+ });
124
+ });
@@ -28,9 +28,11 @@ import { readErrorCode } from '../lib/error-envelope.js';
28
28
  import { softSkip } from '../lib/soft-skip.js';
29
29
  import { req } from '../lib/requirement-ids.js';
30
30
  import { BOUND_ID as RUN_ID } from '../lib/bound-id.js';
31
+ import { scaledTimeoutMs } from '../lib/polling.js';
31
32
 
32
33
  const DOC = 'spec/v2/core/identity.md §5';
33
34
  const NOOP_WORKFLOW_ID = 'conformance-noop';
35
+ const EVENTS_DEADLINE_MS = 20_000;
34
36
  const FOREIGN_RUN_ID = 'openwop-conformance-foreign-tenant/foreignopaque0123456789abcdef';
35
37
 
36
38
  async function discovery(): Promise<Record<string, unknown> | null> {
@@ -63,17 +65,27 @@ describe('v2 id-grammar (RFC 0170 §D.1)', () => {
63
65
  it('every id on the run events matches its ids.schema.json kind', async () => {
64
66
  const c = await createRun();
65
67
  if ('reason' in c) return softSkip('blocked', c.reason);
66
- const res = await http(() => driver.get(`/runs/${encodeURIComponent(c.runId)}/events/poll?timeout=1`));
67
- if (res === null || res.status !== 200) return softSkip('blocked', `GET /runs/{runId}/events/poll answered ${res?.status ?? 'no response'}`);
68
- const events = (res.json as { events?: unknown } | undefined)?.events;
69
- if (!Array.isArray(events) || events.length === 0) return softSkip('blocked', 'the poll returned no events for the run just created — nothing to check the eventId / runId / nodeId grammars against');
68
+ // Poll until the run has recorded an event. A single 1 s poll straight after
69
+ // the create raced the host's dispatch: a host that queues the run (Cloud
70
+ // Tasks on MyndHyve) can legitimately have no event yet, and `blocked` is
71
+ // bundle-fatal (2.42.8; MyndHyve cut 3 on 2.42.7). A host that records
72
+ // nothing for a noop run inside the scaled deadline is still `blocked`.
73
+ const deadline = Date.now() + scaledTimeoutMs(EVENTS_DEADLINE_MS);
74
+ let res: OpenWOPResponse | null = null;
75
+ let events: unknown;
76
+ do {
77
+ res = await http(() => driver.get(`/runs/${encodeURIComponent(c.runId)}/events/poll?timeout=5`));
78
+ if (res === null || res.status !== 200) return softSkip('blocked', `GET /runs/{runId}/events/poll answered ${res?.status ?? 'no response'}`);
79
+ events = (res.json as { events?: unknown } | undefined)?.events;
80
+ } while ((!Array.isArray(events) || events.length === 0) && Date.now() < deadline);
81
+ if (!Array.isArray(events) || events.length === 0) return softSkip('blocked', `the poll returned no events for the run just created within ${scaledTimeoutMs(EVENTS_DEADLINE_MS)} ms — nothing to check the eventId / runId / nodeId grammars against`);
70
82
  const validate = v2Validator('run-event');
71
83
  for (const ev of events) {
72
84
  const r = validate(ev);
73
85
  expect(r.ok, req('openwop.requirement.0170.id-grammar.events', DOC, `every RunEventDoc id (eventId, runId, nodeId, causationId) MUST match its kind — event ${String((ev as { eventId?: unknown }).eventId)} (${r.errors})`)).toBe(true);
74
86
  expect((ev as { runId?: unknown }).runId, req('openwop.requirement.0170.id-grammar.events', DOC, 'every event MUST carry the run\'s own runId')).toBe(c.runId);
75
87
  }
76
- });
88
+ }, scaledTimeoutMs(EVENTS_DEADLINE_MS) + 15_000);
77
89
 
78
90
  it('a run id whose tenant segment is not the caller\'s is refused', async () => {
79
91
  if (!(await discovery())) return softSkip('blocked', 'v2 discovery unreachable — /.well-known/openwop did not answer 200 with a JSON body under OpenWOP-Version: 2.0');
@@ -9,16 +9,19 @@
9
9
  * any advertised floor) is sent; when the host refuses, the refusal MUST be the
10
10
  * registered code at its registered status in the closed envelope; a host that
11
11
  * exercises the MAY by serving the request records `inapplicable` with that
12
- * reason. `OpenWOP-Client-Version` is the `OpenWOP-<Name>` header this
13
- * scenario uses to announce the client; it is not yet declared in
14
- * `api/v2/openapi.yaml` / `headers.md`.
12
+ * reason. `OpenWOP-Client-Version` is declared in `api/v2/openapi.yaml` /
13
+ * `headers.md` and specified in versioning.md §1.5 (RFC 0219), so a non-`2xx`,
14
+ * non-`426` answer to a well-formed below-floor value is a refusal in an
15
+ * unregistered form: a failure, not `blocked` (RFC 0219, Unresolved question 4,
16
+ * decided 2026-09-27). The header's other legs (at-floor, absent, malformed, no
17
+ * floor) are `v2-client-version-header`.
15
18
  */
16
19
 
17
20
  import { describe, it, expect } from 'vitest';
18
21
  import { driver, type OpenWOPResponse } from '../lib/driver.js';
19
22
  import { v2Discovery, v2Validator } from '../lib/v2.js';
20
23
  import { readErrorCode } from '../lib/error-envelope.js';
21
- import { blockedDespiteAssertions, softSkip } from '../lib/soft-skip.js';
24
+ import { softSkip } from '../lib/soft-skip.js';
22
25
  import { req } from '../lib/requirement-ids.js';
23
26
 
24
27
  const DOC = 'spec/v2/core/versioning.md §1.5';
@@ -40,21 +43,22 @@ describe('v2 min-client-version (RFC 0172 §A.5 — gated on minClientVersion)',
40
43
  expect(typeof floor === 'string' && VERSION.test(floor), req('openwop.requirement.0172.min-client-version', DOC, 'minClientVersion MUST use the <major>.<minor> grammar (axis 15, as axis 1)')).toBe(true);
41
44
  const res = await http(() => driver.get('/.well-known/openwop', { authenticated: false, headers: { 'OpenWOP-Client-Version': '0.0.1' } }));
42
45
  if (res === null) return softSkip('blocked', 'GET /.well-known/openwop with OpenWOP-Client-Version unreachable (fetch failed)');
43
- // unfailable-leg audit wave 2, 2026-09-27: every non-426 answer — including
44
- // a 4xx/5xx that refused the client in an unregistered form — recorded
45
- // `inapplicable` AFTER the grammar assert, i.e. a partial-witness PASS of
46
- // the 426 refusal row. Only a SERVED request (2xx) is the MAY exercised; any
47
- // other non-426 answer is `blocked`. Not asserted as a failure:
48
- // `OpenWOP-Client-Version` is not yet declared in api/v2/openapi.yaml /
49
- // headers.md, so a non-2xx here is not provably a version refusal.
50
- if (res.status !== 426 && (res.status < 200 || res.status >= 300)) {
51
- return blockedDespiteAssertions(`the host advertises minClientVersion ${String(floor)} and answered ${res.status} to a client announcing 0.0.1 — neither served (the MAY) nor refused with the registered 426 client_version_unsupported; the refusal form was not observed`);
52
- }
46
+ // RFC 0219 Q4 (2026-09-27): the header is declared, so a host that answers a
47
+ // well-formed below-floor value with anything but 2xx (the MAY: served) or
48
+ // 426 client_version_unsupported refuses a declared header in an
49
+ // unregistered form. That is a failure. Before RFC 0219 it recorded `blocked`.
50
+ expect(res.status === 426 || (res.status >= 200 && res.status < 300), req('openwop.requirement.0172.min-client-version', DOC, `a client announcing 0.0.1 below minClientVersion ${String(floor)} MUST be served or refused with 426 client_version_unsupported — the host answered ${res.status}`)).toBe(true);
53
51
  // partial-witness-ok: the minClientVersion grammar MUST was asserted above; refusing a
54
52
  // below-floor client is a MAY and this host served it, so the 426 shape binds nothing.
55
53
  if (res.status !== 426) return softSkip('inapplicable', `the host advertises minClientVersion ${String(floor)} but served a client announcing 0.0.1 (${res.status}) — refusal is a MAY; nothing further is observable`);
56
54
  expect(readErrorCode(res.json), req('openwop.requirement.0172.min-client-version', DOC, 'a 426 refusal MUST carry client_version_unsupported')).toBe('client_version_unsupported');
57
55
  const r = v2Validator('error-envelope')(res.json);
58
56
  expect(r.ok, req('openwop.requirement.0172.min-client-version', 'spec/v2/core/errors.md §The envelope', `the refusal MUST be the closed error envelope (${r.errors})`)).toBe(true);
57
+ // RFC 0219 Q5: details.minClientVersion is OPTIONAL; when a host sends it, it names the floor
58
+ // (errors.json registers its grammar, which the envelope validator above already checked).
59
+ const details = (res.json as { details?: { minClientVersion?: unknown } } | null)?.details;
60
+ if (details && details.minClientVersion !== undefined) {
61
+ expect(details.minClientVersion, req('openwop.requirement.0172.min-client-version', DOC, `a refusal's details.minClientVersion MUST name the advertised floor ${String(floor)}`)).toBe(floor);
62
+ }
59
63
  });
60
64
  });
@@ -8,7 +8,8 @@
8
8
  * served `preferredVersion`'s major, so a header-less `GET /.well-known/openwop`
9
9
  * is the `preferredVersion` representation and its `OpenWOP-Version` response
10
10
  * header names that major. The header-less fetch bypasses the driver (which
11
- * stamps `OpenWOP-Version: 2.0` under target major 2).
11
+ * stamps `OpenWOP-Version: 2.0` under target major 2). Any OTHER unversioned path
12
+ * is the v2 surface (§1.2), so a header-less request there is served major 2.
12
13
  */
13
14
 
14
15
  import { describe, it, expect } from 'vitest';
@@ -67,6 +68,23 @@ describe('v2 preferred-version-default (RFC 0172 §A.1, §A.3)', () => {
67
68
  expect(Array.isArray(body?.['protocolVersions']) && typeof body?.['preferredVersion'] === 'string', req('openwop.requirement.0172.preferred-version-default.headerless', 'spec/v2/core/capabilities.md §1', 'the v1 representation MUST carry protocolVersions[] and preferredVersion additively so a single fetch names the other major')).toBe(true);
68
69
  }
69
70
  });
71
+ it('a header-less request on any other unversioned path is served major 2', async () => {
72
+ const doc = await discovery();
73
+ if (!doc) return softSkip('blocked', 'v2 discovery unreachable — /.well-known/openwop did not answer 200 with a JSON body under OpenWOP-Version: 2.0');
74
+ // versioning.md §1.2/§1.3: an unversioned operation path is the v2 surface, so the
75
+ // header-less default (preferredVersion) applies to /.well-known/openwop only. Any
76
+ // answer carries OpenWOP-Version (§1.4), so an unknown run id is enough to read it.
77
+ const path = `/runs/openwop-conformance~2F${'h'.repeat(22)}`;
78
+ let version: string | null = null;
79
+ try {
80
+ const res = await fetch(`${loadEnv().baseUrl}${path}`, { headers: { Accept: 'application/json', Authorization: `Bearer ${loadEnv().apiKey}` } });
81
+ await res.text();
82
+ version = res.headers.get('openwop-version');
83
+ } catch {
84
+ return softSkip('blocked', `header-less GET ${path} unreachable (fetch failed)`);
85
+ }
86
+ expect(version?.trim().split('.')[0], req('openwop.requirement.0172.preferred-version-default.unversioned-is-v2', 'spec/v2/core/versioning.md §1.2, §1.3', `a header-less request on an unversioned operation path MUST be served major 2 and say so in OpenWOP-Version — the preferredVersion default is for /.well-known/openwop only (got ${JSON.stringify(version)})`)).toBe('2');
87
+ });
70
88
  it('through the overlap preferredVersion names a 1.x member', async () => {
71
89
  const doc = await discovery();
72
90
  if (!doc) return softSkip('blocked', 'v2 discovery unreachable — /.well-known/openwop did not answer 200 with a JSON body under OpenWOP-Version: 2.0');
@@ -0,0 +1,76 @@
1
+ /**
2
+ * RFC 0221 — a secret the host generates is returned once (`spec/v2/core/webhooks.md`
3
+ * §Surfaces; suite 2.42.8, target major 2; gated on the `webhooks` family and the
4
+ * `conformance-noop` fixture).
5
+ *
6
+ * `registerWebhook` makes `secret` optional. When the request omits it, the host
7
+ * MUST generate one and return it in the `201` as `secret`, the only time it
8
+ * appears on the wire, and every delivery to that subscription MUST verify under
9
+ * it (`OpenWOP-Signature`, scheme `v1`). When the request supplies a secret, the
10
+ * `201` MUST NOT echo it (RFC 0201 §B.6, now for every subscription).
11
+ *
12
+ * How it FAILS: a `201` without `secret` for a secret-less registration (the
13
+ * subscriber can never verify a delivery); a returned secret the deliveries are
14
+ * not signed with; a `201` that echoes a supplied secret.
15
+ *
16
+ * @see spec/v2/core/webhooks.md §Surfaces
17
+ * @see RFCS/0221-generated-webhook-secret-returned-once.md
18
+ */
19
+
20
+ import { afterEach, describe, it, expect } from 'vitest';
21
+ import { createHmac } from 'node:crypto';
22
+ import { req } from '../lib/requirement-ids.js';
23
+ import { readErrorCode } from '../lib/error-envelope.js';
24
+ import { softSkip } from '../lib/soft-skip.js';
25
+ import { v2Discovery, gateFamily } from '../lib/v2.js';
26
+ import { hitHeader, startModalReceiver, type ModalHit } from '../lib/webhook-receiver.js';
27
+ import { SW_FIXTURE, deliveriesFor, driveRun, loopbackRefusal, registerSw, unregisterAllSw, waitFor } from '../lib/standard-webhooks.js';
28
+
29
+ const ID = 'openwop.requirement.0221.generated-secret-returned';
30
+ const SPEC = 'RFC 0221 · webhooks.md §Surfaces';
31
+
32
+ let closeReceiver: (() => Promise<void>) | null = null;
33
+ afterEach(async () => {
34
+ await unregisterAllSw();
35
+ if (closeReceiver) { const c = closeReceiver; closeReceiver = null; await c(); }
36
+ });
37
+
38
+ function v1SignedBy(secret: string, h: ModalHit): boolean {
39
+ const ts = hitHeader(h, 'openwop-timestamp');
40
+ return ts !== undefined && hitHeader(h, 'openwop-signature') === `sha256=${createHmac('sha256', secret).update(`${ts}.${h.body}`, 'utf8').digest('hex')}`;
41
+ }
42
+
43
+ describe('RFC 0221 — a host-generated webhook secret is returned once', () => {
44
+ it('a secret-less registration returns the generated secret, and deliveries verify under it; a supplied secret is never echoed', async () => {
45
+ let doc: Record<string, unknown> | null = null;
46
+ try { doc = await v2Discovery(); } catch { doc = null; }
47
+ if (!doc) return softSkip('blocked', 'v2 discovery unreachable');
48
+ if (!(await gateFamily('webhooks'))) return softSkip('inapplicable', 'webhooks family not advertised (gate recorded under openwop.family.webhooks)');
49
+ const fixtures = Array.isArray(doc['fixtures']) ? (doc['fixtures'] as unknown[]) : [];
50
+ if (!fixtures.includes(SW_FIXTURE)) return softSkip('inapplicable', `${SW_FIXTURE} fixture not advertised — no run to deliver`);
51
+
52
+ const rx = await startModalReceiver();
53
+ closeReceiver = rx.close;
54
+ const target = rx.urlFor('no-echo');
55
+ const reg = await registerSw({ url: target.url, events: ['run.completed'] });
56
+ const refused = loopbackRefusal(reg, target.tunnelled);
57
+ if (refused) return softSkip('blocked', refused);
58
+ if (reg.status !== 201) return softSkip('blocked', `registerWebhook answered ${reg.status} ${readErrorCode(reg.json) ?? ''} — webhooks.md §Surfaces owns that contract`);
59
+
60
+ const body = reg.json as { webhookId: string; secret?: unknown };
61
+ expect(typeof body.secret === 'string' && body.secret.length > 0, req(ID, SPEC, 'a registration that omits secret MUST get the host-generated secret back in the 201 as a non-empty string')).toBe(true);
62
+ const secret = body.secret as string;
63
+
64
+ const run = await driveRun();
65
+ expect(run.status, req(ID, 'runs.md §Create', 'POST /runs MUST answer 201 for the noop fixture')).toBe(201);
66
+ await waitFor(() => deliveriesFor(rx.hits, body.webhookId, run.runId).length > 0, 15_000);
67
+ const delivery = deliveriesFor(rx.hits, body.webhookId, run.runId)[0];
68
+ expect(delivery, req(ID, 'webhooks.md §Durability', 'the subscription MUST receive the run.completed delivery')).toBeDefined();
69
+ expect(v1SignedBy(secret, delivery!), req(ID, SPEC, 'the delivery MUST verify (OpenWOP-Signature, scheme v1) under the secret the 201 returned')).toBe(true);
70
+
71
+ const supplied = `conformance-${'s'.repeat(24)}`;
72
+ const own = await registerSw({ url: rx.urlFor('no-echo').url, events: ['run.completed'], secret: supplied });
73
+ expect(own.status, req(ID, SPEC, `a registration with a supplied secret MUST answer 201 (got ${own.status} ${readErrorCode(own.json) ?? ''})`)).toBe(201);
74
+ expect(own.text.includes(supplied) || (own.json as { secret?: unknown }).secret !== undefined, req(ID, 'RFC 0221 · RFC 0201 §B.6', 'the 201 MUST NOT echo a supplied secret')).toBe(false);
75
+ });
76
+ });