@openwop/openwop-conformance 2.0.0-rc.6 → 2.0.0-rc.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/README.md CHANGED
@@ -112,7 +112,7 @@ Exit code is non-zero on any failed assertion. `--certify` distinguishes: `0`
112
112
 
113
113
  ## What's Covered
114
114
 
115
- The current suite has 497 scenario files under `src/scenarios/`.
115
+ The current suite has 498 scenario files under `src/scenarios/`.
116
116
  - 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.
117
117
  - 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.
118
118
  - 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.**
@@ -457,7 +457,7 @@ Server-required (added in 1.7.0):
457
457
  | ------------- | ----------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
458
458
  | **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. |
459
459
 
460
- Current source tree: 497 scenario files. Use [`coverage.md`](./coverage.md) for current grade/gap tracking.
460
+ Current source tree: 498 scenario files. Use [`coverage.md`](./coverage.md) for current grade/gap tracking.
461
461
 
462
462
  ## Remaining Gaps
463
463
 
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "package": "@openwop/spec-artifacts",
3
- "version": "2.0.0-rc.6",
4
- "stampSha256": "f9288182aafe29cd0a2aed7cf6ec0a48a8ad2744779db641a053351479148211"
3
+ "version": "2.0.0-rc.8",
4
+ "stampSha256": "496861ae55503d03945c4509b57c7eab4e9b3c508c784a919cdd7bbd896b4e95"
5
5
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openwop/openwop-conformance",
3
- "version": "2.0.0-rc.6",
3
+ "version": "2.0.0-rc.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.0.0-rc.6"
59
+ "@openwop/spec-artifacts": "2.0.0-rc.8"
60
60
  }
61
61
  }
package/requirements.json CHANGED
@@ -2,9 +2,9 @@
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": 542,
6
- "tests": 2113,
7
- "withStableId": 2113,
5
+ "files": 543,
6
+ "tests": 2115,
7
+ "withStableId": 2115,
8
8
  "interpolatedTitles": 0,
9
9
  "explicitIds": 2057
10
10
  },
@@ -28124,6 +28124,49 @@
28124
28124
  }
28125
28125
  ]
28126
28126
  },
28127
+ {
28128
+ "id": "openwop.it.v2-version-header-honored.openwop-version-is-honored-or-refused-never-ignored",
28129
+ "file": "v2-version-header-honored.test.ts",
28130
+ "line": 61,
28131
+ "title": "OpenWOP-Version is honored or refused — never ignored",
28132
+ "explicitId": null,
28133
+ "citations": [
28134
+ {
28135
+ "section": null,
28136
+ "requirement": "a host that does not serve major 2 MUST refuse with 406 protocol_version_unsupported, which it did",
28137
+ "interpolated": true
28138
+ },
28139
+ {
28140
+ "section": null,
28141
+ "requirement": "answering 200 to OpenWOP-Version: 2.0 with a body byte-identical to the header-less fetch means the header was IGNORED, not honored — the host served v1 and called it v2. Refusing with 406 is the correct answer for a host that does not implement major 2; silently returning v1 is the one answer that no presence-gated scenario can detect, because an absent v2 shape records `inapplicable` whether the host refused or ignored",
28142
+ "interpolated": true
28143
+ }
28144
+ ]
28145
+ },
28146
+ {
28147
+ "id": "openwop.it.v2-version-header-honored.a-host-advertising-two-majors-serves-two-different-representations",
28148
+ "file": "v2-version-header-honored.test.ts",
28149
+ "line": 92,
28150
+ "title": "a host advertising two majors serves two different representations",
28151
+ "explicitId": null,
28152
+ "citations": [
28153
+ {
28154
+ "section": null,
28155
+ "requirement": null,
28156
+ "interpolated": true
28157
+ },
28158
+ {
28159
+ "section": null,
28160
+ "requirement": "the v1 document and the closed v2 root differ by construction (capabilities.md §1), so a host advertising both majors returning identical bytes for both has advertised a major it does not actually serve",
28161
+ "interpolated": true
28162
+ },
28163
+ {
28164
+ "section": null,
28165
+ "requirement": null,
28166
+ "interpolated": true
28167
+ }
28168
+ ]
28169
+ },
28127
28170
  {
28128
28171
  "id": "openwop.it.v2-webhook-durable-delivery.a-failed-attempt-is-retried-and-the-event-is-delivered-at-least-once",
28129
28172
  "file": "v2-webhook-durable-delivery.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": 497,
4
+ "files": 498,
5
5
  "v1": 444,
6
- "v2": 54
6
+ "v2": 55
7
7
  },
8
8
  "majors": {
9
9
  "a2a-1-0-agent-card.test.ts": [
@@ -1381,6 +1381,9 @@
1381
1381
  "v2-v1-signed-webhook-accepted.test.ts": [
1382
1382
  2
1383
1383
  ],
1384
+ "v2-version-header-honored.test.ts": [
1385
+ 2
1386
+ ],
1384
1387
  "v2-webhook-durable-delivery.test.ts": [
1385
1388
  2
1386
1389
  ],
@@ -1,7 +1,7 @@
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.0.0-rc.6",
4
+ "version": "2.0.0-rc.8",
5
5
  "corpusTag": null,
6
6
  "files": {
7
7
  "api/.redocly.lint-ignore.yaml": "bf5a8350b88a72fa43f59605ed8d903ed24b6cfccda5e45509c9f6ed9ee4e712",
@@ -9,9 +9,9 @@
9
9
  "api/grpc/openwop.proto": "c3e72bb17cba514ee98feb6434e6c9b6ea6795bfd086489ec69fd882dd1ad977",
10
10
  "api/openapi.yaml": "39081c59fb696159806b0f2f9a42e7e9ff830d622fcf2ad4159b21357580a955",
11
11
  "api/redocly.yaml": "b0604c89b2ca6d5076ec25725c539dad44a741a811fe524439ee6daef8baa09f",
12
- "api/seams-v2.yaml": "ac5a9173b5d35869d6a5d2c0e7efd37b377414e5cded22fc261e088eb614a286",
13
- "api/v2/asyncapi.yaml": "958537c1279125d6d14416889e9aa48a89dc1941f892348fa46496faedfd8a78",
14
- "api/v2/openapi.yaml": "4bed5a9c899b77092a2da2a4337910c80c0fc39612b47b46a38d9a05a2868ade",
12
+ "api/seams-v2.yaml": "135f40f745b11c557f69b9b1dc0004f4400df48135964cfa75f3c26036f75a7d",
13
+ "api/v2/asyncapi.yaml": "1470d4ee12b87b6bd6569f3b6b7f84f5cded66bae920734778cd0bec45144658",
14
+ "api/v2/openapi.yaml": "352105be576e2c93d18c55272c8604519213e7df9d6582df0fb8382c0a2c66cd",
15
15
  "api/v2/redocly.yaml": "1e66b60e6118ad11a823bb620678be464d99dfe50a40e3e6f93ec9429b88b34c",
16
16
  "schemas/README.md": "0c0b737ffcf8f30e7d2809cec8a498232de710f41443212922ad8337cdde0b51",
17
17
  "schemas/a2a-task-state.schema.json": "c9365918f993f943b4b619d42551eb066a1ed33a08d895d51b395432a5b1f1bc",
@@ -32,7 +32,7 @@
32
32
  "schemas/audit-verify-result.schema.json": "e8fd013fb610ba5db27ab0113ec2eb18e7da5f9310dd3de2dd3dc87a259c21fe",
33
33
  "schemas/budget-policy.schema.json": "3d4fe30ae129fe136ab9ece395f2a2a42a76360786b1b9a1d5f1cbc753eb5b39",
34
34
  "schemas/capabilities.schema.json": "e672c35d503532d8039b5552115f5ec150d60317f537f13180d830b0fe315947",
35
- "schemas/certification-bundle-v2.schema.json": "3540232676086869ae78d24f3b9c2154f9c4a8a5eeb976512d6a7409f8cf19bb",
35
+ "schemas/certification-bundle-v2.schema.json": "2f85530dd04824a15810c94e96869362dee1e78396eca21fdd18e5501416bb7d",
36
36
  "schemas/channel-presence-payload.schema.json": "f9d0ef4219814c2b765baf3e68cbe5eea2cb42379f516a93a0459dcd5f7db47e",
37
37
  "schemas/channel-written-payload.schema.json": "54723a56bc5a35232f64544ca686ac1ac959a5fb05b32b2401facca6b3ef51a3",
38
38
  "schemas/chat-card-pack-manifest.schema.json": "402f7e9081cc39857a52e772061f4e034a957dcbc17501fe6e8e0bcb3d337e42",
@@ -198,7 +198,7 @@
198
198
  "schemas/workspace-file.schema.json": "464de85c2a068243084ee9c1d969bc7cd5d8f7948574e58450d6493c38a0e1e4",
199
199
  "spec/v1/alias-detectors.json": "fee4594ef49953953ffcd0b3813300067d16b3e65ebff2aac722034ac9b3f545",
200
200
  "spec/v1/capability-declaration-classes.json": "e7729aed5c4b4e1dd02abab0530f14cc95f5d4070fe51fb139e7f5cccefa00c6",
201
- "spec/v1/core-standard-manifest.json": "5f61d1d51233de923032cfb7fe1459ea0d8c60e4ff70fc9e610c001686da9864",
201
+ "spec/v1/core-standard-manifest.json": "3bc6be4271442cd38a445c20643be92ec1432d188807d6779cae40bf611adb03",
202
202
  "spec/v1/deprecations.json": "520d79927a8ed43ccf62c207a7cf6cf7b19b16e00bbbbdf0410667e939c19d85",
203
203
  "spec/v1/deprecations.schema.json": "4cdbbb8eb9c0092913a5704c943880f0c8581bcf0f640a7df5a84105d53c8aee",
204
204
  "spec/v1/event-codemap.json": "3da60d884157793a360da532a9fcbbfb5285636db325a74cec94b34622186d97",
@@ -228,7 +228,7 @@
228
228
  "spec/v2/path-manifest.json": "034152e09b1458c66810d4050e20a273b2b9b8fe2d92b66e8b819477de58a1be",
229
229
  "spec/v2/peer-dependency-aliases.json": "d10299280abee08258502925bc327293ee413e0108cd6e6ec75ff6110653308d",
230
230
  "spec/v2/profiles.json": "1f0ee40491131cf561ef19a0394c47ba476b4567514fe16befb6cada4a10a5a4",
231
- "spec/v2/release.json": "64dedadf70a0cd6928ee4bbe7012631cf0612c03e2e2df20d5e48f710998a2e9"
231
+ "spec/v2/release.json": "f2ef4468ef76f5da0580bd7ca2b8f2237fb4e4ce82f81afba34b1819bc238e68"
232
232
  },
233
- "corpusCommit": "12aac8ee261f99a5593551dd1c0f317195d5cb28"
233
+ "corpusCommit": "fa9baad71198fd4994360a8331dbbffd41165bd4"
234
234
  }
@@ -83,7 +83,7 @@ describe('v2-era-stamp-universal (RFC 0176 §A.7)', () => {
83
83
  const advertised = doc['eventLogSchemaVersion'];
84
84
  if (advertised === undefined) return softSkip('inapplicable', 'the host advertises no eventLogSchemaVersion');
85
85
  if (advertised !== 3) {
86
- return softSkip('inapplicable', `the host advertises era ${JSON.stringify(advertised)}, not 3 — it has not moved its writers to the v2 era, which is legitimate through the overlap`);
86
+ return softSkip('inapplicable', `the host advertises era ${JSON.stringify(advertised)}, not 3 — this scenario checks that the advertised value AGREES with what the creation paths write, whatever that value is. Whether it MUST be 3 is v2-era-key's assertion, not this one; a host reached under target major 2 is a v2 host and persistence.md requires 3 there`);
87
87
  }
88
88
 
89
89
  const created = await http(() => driver.post('/runs', { workflowId: 'conformance-noop', inputs: {} }));
@@ -0,0 +1,124 @@
1
+ /**
2
+ * RFC 0172 §A.3 — the `OpenWOP-Version` header is HONORED or REFUSED, never
3
+ * ignored (suite 2.0.0, target major 2; unaided).
4
+ *
5
+ * A host that does not implement v2 has two correct answers to
6
+ * `OpenWOP-Version: 2`: serve the v2 representation, or refuse with `406
7
+ * protocol_version_unsupported`. There is a third thing it can do, and it is
8
+ * the dangerous one — return `200` with the v1 document, ignoring the header
9
+ * entirely.
10
+ *
11
+ * That third case is invisible to every presence-gated scenario in the suite.
12
+ * A scenario that skips when a v2 shape is absent records `inapplicable`
13
+ * whether the host REFUSED major 2 or silently handed back v1; both are
14
+ * non-failures, so a bundle looks clean while witnessing nothing. This is not
15
+ * hypothetical: it is the shape that let RFC 0165 sit `Accepted` on a host
16
+ * that served none of it — the acceptance cited merged PRs, the gated
17
+ * scenarios recorded `inapplicable`, and nothing in the chain ever compared
18
+ * the two representations. Measured on a live tier-1 host 2026-09-04:
19
+ * `OpenWOP-Version: 2` returned `200` with a body byte-identical to the
20
+ * header-less fetch. Credit to the openwop-app session for the probe.
21
+ *
22
+ * The check is cheap and needs no v2 support: fetch the resource twice, once
23
+ * with the header and once without. A host advertising two majors MUST NOT
24
+ * return the same bytes for both, because the two representations differ by
25
+ * construction (`capabilities.md` §1). A host advertising one major MUST
26
+ * refuse rather than ignore.
27
+ *
28
+ * @see spec/v2/core/versioning.md §1.3
29
+ * @see RFCS/0172-v2-versioning-and-release.md §A.3
30
+ */
31
+
32
+ import { describe, it, expect } from 'vitest';
33
+ import { loadEnv } from '../lib/env.js';
34
+ import { softSkip } from '../lib/soft-skip.js';
35
+ import { req } from '../lib/requirement-ids.js';
36
+
37
+ const ID = 'openwop.requirement.0172.version-header-honored';
38
+ const DOC = 'spec/v2/core/versioning.md §1.3';
39
+ const PATH = '/.well-known/openwop';
40
+
41
+ interface Fetched {
42
+ readonly status: number;
43
+ readonly body: string;
44
+ readonly version: string | null;
45
+ }
46
+
47
+ async function fetchRepresentation(header: string | null): Promise<Fetched | null> {
48
+ const { baseUrl, apiKey } = loadEnv();
49
+ const headers: Record<string, string> = { Accept: 'application/json' };
50
+ if (header !== null) headers['OpenWOP-Version'] = header;
51
+ if (apiKey) headers['authorization'] = `Bearer ${apiKey}`;
52
+ try {
53
+ const res = await fetch(`${baseUrl.replace(/\/$/, '')}${PATH}`, { headers });
54
+ return { status: res.status, body: await res.text(), version: res.headers.get('openwop-version') };
55
+ } catch {
56
+ return null;
57
+ }
58
+ }
59
+
60
+ describe('v2-version-header-honored (RFC 0172 §A.3)', () => {
61
+ it('OpenWOP-Version is honored or refused — never ignored', async () => {
62
+ const bare = await fetchRepresentation(null);
63
+ const asked = await fetchRepresentation('2.0');
64
+ if (bare === null || asked === null) {
65
+ return softSkip('blocked', `${PATH} unreachable for one of the two probes — the comparison needs both`);
66
+ }
67
+ if (bare.status !== 200) {
68
+ return softSkip('blocked', `the header-less GET ${PATH} answered ${bare.status}; there is no baseline representation to compare against`);
69
+ }
70
+
71
+ // A refusal is a correct answer and ends the check: the host has told the
72
+ // truth about not serving major 2.
73
+ if (asked.status === 406) {
74
+ expect(
75
+ asked.status,
76
+ req(ID, DOC, 'a host that does not serve major 2 MUST refuse with 406 protocol_version_unsupported, which it did'),
77
+ ).toBe(406);
78
+ return softSkip('inapplicable', 'the host refused major 2 with the specified 406 — a correct answer, and there is no second representation to compare bytes against');
79
+ }
80
+ if (asked.status !== 200) {
81
+ return softSkip('blocked', `GET ${PATH} with OpenWOP-Version: 2.0 answered ${asked.status} — neither the v2 representation (200) nor the specified refusal (406), so this scenario cannot rule on it`);
82
+ }
83
+
84
+ // 200 means the host claims to have served major 2. The bytes decide
85
+ // whether it actually did.
86
+ expect(
87
+ asked.body === bare.body,
88
+ req(ID, DOC, `answering 200 to OpenWOP-Version: 2.0 with a body byte-identical to the header-less fetch means the header was IGNORED, not honored — the host served v1 and called it v2. Refusing with 406 is the correct answer for a host that does not implement major 2; silently returning v1 is the one answer that no presence-gated scenario can detect, because an absent v2 shape records \`inapplicable\` whether the host refused or ignored`),
89
+ ).toBe(false);
90
+ });
91
+
92
+ it('a host advertising two majors serves two different representations', async () => {
93
+ const bare = await fetchRepresentation(null);
94
+ if (bare === null || bare.status !== 200) {
95
+ return softSkip('blocked', `the header-less GET ${PATH} answered ${bare?.status ?? 'no response'}`);
96
+ }
97
+ let doc: Record<string, unknown>;
98
+ try {
99
+ doc = JSON.parse(bare.body) as Record<string, unknown>;
100
+ } catch {
101
+ return softSkip('blocked', `${PATH} did not return a JSON object`);
102
+ }
103
+ const versions = Array.isArray(doc['protocolVersions']) ? (doc['protocolVersions'] as unknown[]).map(String) : [];
104
+ const majors = new Set(versions.map((v) => v.split('.')[0]));
105
+ if (!(majors.has('1') && majors.has('2'))) {
106
+ return softSkip('inapplicable', `the host advertises [${versions.join(', ') || 'no protocolVersions'}] — one major or none, so there is no second representation to differ from`);
107
+ }
108
+
109
+ const asked = await fetchRepresentation('2.0');
110
+ if (asked === null) return softSkip('blocked', `${PATH} unreachable under OpenWOP-Version: 2.0`);
111
+ expect(
112
+ asked.status,
113
+ req(ID, DOC, `a host advertising both majors MUST serve major 2 when asked for it, not refuse (got ${asked.status})`),
114
+ ).toBe(200);
115
+ expect(
116
+ asked.body === bare.body,
117
+ req(ID, DOC, 'the v1 document and the closed v2 root differ by construction (capabilities.md §1), so a host advertising both majors returning identical bytes for both has advertised a major it does not actually serve'),
118
+ ).toBe(false);
119
+ expect(
120
+ asked.version?.trim().split('.')[0] ?? null,
121
+ req(ID, DOC, `the response to OpenWOP-Version: 2.0 MUST carry OpenWOP-Version naming the major it served (got ${String(asked.version)})`),
122
+ ).toBe('2');
123
+ });
124
+ });