@openwop/openwop-conformance 2.0.0-rc.16 → 2.0.0-rc.18
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
|
|
115
|
+
The current suite has 499 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:
|
|
460
|
+
Current source tree: 499 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.
|
|
4
|
-
"stampSha256": "
|
|
3
|
+
"version": "2.0.0-rc.18",
|
|
4
|
+
"stampSha256": "42f5080dbb878855dc93dd06e69fc14c72147cbeba6fd2fb92038f6b997c49ca"
|
|
5
5
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openwop/openwop-conformance",
|
|
3
|
-
"version": "2.0.0-rc.
|
|
3
|
+
"version": "2.0.0-rc.18",
|
|
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.
|
|
59
|
+
"@openwop/spec-artifacts": "2.0.0-rc.18"
|
|
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":
|
|
6
|
-
"tests":
|
|
7
|
-
"withStableId":
|
|
5
|
+
"files": 545,
|
|
6
|
+
"tests": 2119,
|
|
7
|
+
"withStableId": 2119,
|
|
8
8
|
"interpolatedTitles": 0,
|
|
9
9
|
"explicitIds": 2057
|
|
10
10
|
},
|
|
@@ -25755,6 +25755,25 @@
|
|
|
25755
25755
|
}
|
|
25756
25756
|
]
|
|
25757
25757
|
},
|
|
25758
|
+
{
|
|
25759
|
+
"id": "openwop.it.v2-advertised-path-space-served.a-host-advertising-major-2-reaches-the-surfaces-it-already-serves-under-v1",
|
|
25760
|
+
"file": "v2-advertised-path-space-served.test.ts",
|
|
25761
|
+
"line": 87,
|
|
25762
|
+
"title": "a host advertising major 2 reaches the surfaces it already serves under /v1",
|
|
25763
|
+
"explicitId": null,
|
|
25764
|
+
"citations": [
|
|
25765
|
+
{
|
|
25766
|
+
"section": null,
|
|
25767
|
+
"requirement": null,
|
|
25768
|
+
"interpolated": true
|
|
25769
|
+
},
|
|
25770
|
+
{
|
|
25771
|
+
"section": null,
|
|
25772
|
+
"requirement": "at least one non-well-known surface MUST be reachable under major 2, or the advertisement rests entirely on the one resource the header selects",
|
|
25773
|
+
"interpolated": true
|
|
25774
|
+
}
|
|
25775
|
+
]
|
|
25776
|
+
},
|
|
25758
25777
|
{
|
|
25759
25778
|
"id": "openwop.it.v2-approver-enforced.a-resolver-outside-approverslist-is-refused-at-resolve-time",
|
|
25760
25779
|
"file": "v2-approver-enforced.test.ts",
|
package/scenario-majors.json
CHANGED
|
@@ -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":
|
|
4
|
+
"files": 499,
|
|
5
5
|
"v1": 444,
|
|
6
|
-
"v2":
|
|
6
|
+
"v2": 56
|
|
7
7
|
},
|
|
8
8
|
"majors": {
|
|
9
9
|
"a2a-1-0-agent-card.test.ts": [
|
|
@@ -1231,6 +1231,9 @@
|
|
|
1231
1231
|
"v2-advertised-fixtures-exist.test.ts": [
|
|
1232
1232
|
2
|
|
1233
1233
|
],
|
|
1234
|
+
"v2-advertised-path-space-served.test.ts": [
|
|
1235
|
+
2
|
|
1236
|
+
],
|
|
1234
1237
|
"v2-approver-enforced.test.ts": [
|
|
1235
1238
|
2
|
|
1236
1239
|
],
|
|
@@ -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.
|
|
4
|
+
"version": "2.0.0-rc.18",
|
|
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": "
|
|
13
|
-
"api/v2/asyncapi.yaml": "
|
|
14
|
-
"api/v2/openapi.yaml": "
|
|
12
|
+
"api/seams-v2.yaml": "f0ca594fef935552041375ae2d7ee4659804febc3369f2e7ae4041edb011d563",
|
|
13
|
+
"api/v2/asyncapi.yaml": "1352592381b127f42c8968b24de16fc985540bdc10eab439976a3df1c9e34fd0",
|
|
14
|
+
"api/v2/openapi.yaml": "e93f2d2f9577eb36a7cc3844311c4ac396d4cf484d7bfe0642003e5f6ca56e20",
|
|
15
15
|
"api/v2/redocly.yaml": "1e66b60e6118ad11a823bb620678be464d99dfe50a40e3e6f93ec9429b88b34c",
|
|
16
16
|
"schemas/README.md": "0c0b737ffcf8f30e7d2809cec8a498232de710f41443212922ad8337cdde0b51",
|
|
17
17
|
"schemas/a2a-task-state.schema.json": "c9365918f993f943b4b619d42551eb066a1ed33a08d895d51b395432a5b1f1bc",
|
|
@@ -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": "
|
|
201
|
+
"spec/v1/core-standard-manifest.json": "e416337a78a2205e522247dd6f4cd8a594c39af6ac2df4f1dba0915b4004f946",
|
|
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": "
|
|
231
|
+
"spec/v2/release.json": "dd64fd998377b02712cdd6dba07a92424339c99b94606d3044a9d245cb299341"
|
|
232
232
|
},
|
|
233
|
-
"corpusCommit": "
|
|
233
|
+
"corpusCommit": "d76647a930d2e2f77c968d232ef3741f2db55da0"
|
|
234
234
|
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RFC 0172 §A.1 / `spec/v2/core/versioning.md` §1.2 — a host that advertises
|
|
3
|
+
* major 2 serves the major-2 PATH SPACE, not just the major-2 discovery
|
|
4
|
+
* document (suite 2.0.0, target major 2; unaided).
|
|
5
|
+
*
|
|
6
|
+
* `v2-version-header-honored` checks that `OpenWOP-Version` is honored or
|
|
7
|
+
* refused rather than ignored. It probes `/.well-known/openwop`, because that
|
|
8
|
+
* is the one resource whose representation the header selects. **It therefore
|
|
9
|
+
* cannot see a host that negotiates correctly on the well-known resource and
|
|
10
|
+
* has mounted almost none of the rest of the v2 surface.**
|
|
11
|
+
*
|
|
12
|
+
* That is not hypothetical. A tier-1 host advertising
|
|
13
|
+
* `protocolVersions: ["1.1","2.0"]` was found serving **two of fifteen**
|
|
14
|
+
* top-level segments of the v2 path space: its unversioned mount was a
|
|
15
|
+
* deliberate allowlist (`['/runs','/interrupts']`, chosen over a blanket
|
|
16
|
+
* `/v1`-strip because the host serves a large non-`/v1` surface a blanket
|
|
17
|
+
* rewrite would shadow) and the list was simply incomplete. Every probe used to
|
|
18
|
+
* call the dual stack live — `protocolVersions`, `preferredVersion`, the
|
|
19
|
+
* response header, the two differing representations — hits `/.well-known`, so
|
|
20
|
+
* every one of them passed. `POST /webhooks` under major 2 returned `404` while
|
|
21
|
+
* `POST /v1/webhooks` returned `201`. The host found this itself by applying
|
|
22
|
+
* the artifact rule to a scenario it had first classified as a harness defect.
|
|
23
|
+
*
|
|
24
|
+
* The discriminator is a PAIR, not a single probe, because "this host does not
|
|
25
|
+
* implement webhooks at all" and "this host implements webhooks but did not
|
|
26
|
+
* mount them under major 2" are different facts that a lone `404` cannot
|
|
27
|
+
* separate:
|
|
28
|
+
*
|
|
29
|
+
* /v1<path> exists AND <path> is 404 under major 2 ⇒ the advertisement
|
|
30
|
+
* overstates: the surface exists and major 2 does not reach it.
|
|
31
|
+
*
|
|
32
|
+
* both 404 ⇒ the host does not serve that surface in either major. Not this
|
|
33
|
+
* scenario's business, and recorded as neither pass nor failure.
|
|
34
|
+
*
|
|
35
|
+
* Only parameterless GETs from `spec/v2/path-manifest.json` are probed: they
|
|
36
|
+
* need no fixture, mutate nothing, and a route that is not mounted answers 404
|
|
37
|
+
* regardless of auth, so the check is unaided and safe against a live host.
|
|
38
|
+
*
|
|
39
|
+
* @see spec/v2/core/versioning.md §1.2
|
|
40
|
+
* @see RFCS/0172-v2-versioning-and-release.md §A.1
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
import { describe, it, expect } from 'vitest';
|
|
44
|
+
import { loadEnv } from '../lib/env.js';
|
|
45
|
+
import { softSkip } from '../lib/soft-skip.js';
|
|
46
|
+
import { req } from '../lib/requirement-ids.js';
|
|
47
|
+
import { readFileSync } from 'node:fs';
|
|
48
|
+
import { join } from 'node:path';
|
|
49
|
+
import { SCHEMAS_DIR } from '../lib/paths.js';
|
|
50
|
+
|
|
51
|
+
const ID = 'openwop.requirement.0172.advertised-path-space-served';
|
|
52
|
+
const DOC = 'spec/v2/core/versioning.md §1.2';
|
|
53
|
+
|
|
54
|
+
/** The well-known resource is the one the HEADER selects; it has no /v1 twin to pair against. */
|
|
55
|
+
const NOT_PAIRABLE = new Set(['/.well-known/openwop']);
|
|
56
|
+
|
|
57
|
+
interface Probe { readonly status: number | null }
|
|
58
|
+
|
|
59
|
+
async function get(path: string, major2: boolean): Promise<Probe> {
|
|
60
|
+
const { baseUrl, apiKey } = loadEnv();
|
|
61
|
+
const headers: Record<string, string> = { Accept: 'application/json' };
|
|
62
|
+
if (major2) headers['OpenWOP-Version'] = '2.0';
|
|
63
|
+
if (apiKey) headers['authorization'] = `Bearer ${apiKey}`;
|
|
64
|
+
try {
|
|
65
|
+
const res = await fetch(`${baseUrl.replace(/\/$/, '')}${path}`, { headers });
|
|
66
|
+
return { status: res.status };
|
|
67
|
+
} catch {
|
|
68
|
+
return { status: null };
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function parameterlessGets(): string[] {
|
|
73
|
+
try {
|
|
74
|
+
const manifest = JSON.parse(
|
|
75
|
+
readFileSync(join(SCHEMAS_DIR, '..', 'spec', 'v2', 'path-manifest.json'), 'utf8'),
|
|
76
|
+
) as { operations?: ReadonlyArray<{ method: string; path: string }> };
|
|
77
|
+
return (manifest.operations ?? [])
|
|
78
|
+
.filter((o) => o.method === 'GET' && !o.path.includes('{') && !NOT_PAIRABLE.has(o.path))
|
|
79
|
+
.map((o) => o.path)
|
|
80
|
+
.sort();
|
|
81
|
+
} catch {
|
|
82
|
+
return [];
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
describe('v2-advertised-path-space-served (RFC 0172 §A.1)', () => {
|
|
87
|
+
it('a host advertising major 2 reaches the surfaces it already serves under /v1', async () => {
|
|
88
|
+
const { baseUrl } = loadEnv();
|
|
89
|
+
const bare = await (async () => {
|
|
90
|
+
try {
|
|
91
|
+
const res = await fetch(`${baseUrl.replace(/\/$/, '')}/.well-known/openwop`, { headers: { Accept: 'application/json' } });
|
|
92
|
+
return res.status === 200 ? ((await res.json()) as Record<string, unknown>) : null;
|
|
93
|
+
} catch {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
})();
|
|
97
|
+
if (!bare) return softSkip('blocked', 'the discovery document is unreadable, so the advertised majors are unknown');
|
|
98
|
+
|
|
99
|
+
const versions = Array.isArray(bare['protocolVersions']) ? (bare['protocolVersions'] as unknown[]).map(String) : [];
|
|
100
|
+
if (!new Set(versions.map((v) => v.split('.')[0])).has('2')) {
|
|
101
|
+
return softSkip('inapplicable', `the host advertises [${versions.join(', ') || 'no protocolVersions'}] — it does not claim major 2, so there is no path space to hold it to`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const paths = parameterlessGets();
|
|
105
|
+
if (paths.length === 0) return softSkip('blocked', 'spec/v2/path-manifest.json is unreadable from this layout');
|
|
106
|
+
|
|
107
|
+
const overstated: string[] = [];
|
|
108
|
+
const served: string[] = [];
|
|
109
|
+
let pairable = 0;
|
|
110
|
+
for (const path of paths) {
|
|
111
|
+
const v1 = await get(`/v1${path}`, false);
|
|
112
|
+
if (v1.status === null) return softSkip('blocked', `the host became unreachable while probing /v1${path}`);
|
|
113
|
+
// The host does not serve this surface in EITHER major. Legitimate, and a
|
|
114
|
+
// different question from the one asked here.
|
|
115
|
+
if (v1.status === 404) continue;
|
|
116
|
+
pairable += 1;
|
|
117
|
+
|
|
118
|
+
const v2 = await get(path, true);
|
|
119
|
+
if (v2.status === null) return softSkip('blocked', `the host became unreachable while probing ${path}`);
|
|
120
|
+
// Any status but 404 means the route is MOUNTED — 401/403/422 all answer
|
|
121
|
+
// "this path exists". Only 404 says major 2 cannot reach it.
|
|
122
|
+
if (v2.status === 404) overstated.push(`${path} (/v1 → ${v1.status}, major 2 → 404)`);
|
|
123
|
+
else served.push(path);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (pairable === 0) {
|
|
127
|
+
return softSkip('inapplicable', 'no parameterless GET in the v2 manifest is served under /v1 either, so there is no pair to compare and the advertisement cannot be checked this way');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
expect(
|
|
131
|
+
overstated,
|
|
132
|
+
req(ID, DOC, `a host advertising major 2 MUST reach, under major 2, the surfaces it already serves under /v1 — advertising the major is a claim about the PATH SPACE and not only about the well-known resource, whose representation the header selects and which therefore passes even when almost nothing else is mounted (${overstated.length} of ${pairable} pairable surface(s) unreachable: ${overstated.slice(0, 6).join('; ')})`),
|
|
133
|
+
).toEqual([]);
|
|
134
|
+
|
|
135
|
+
expect(
|
|
136
|
+
served.length,
|
|
137
|
+
req(ID, DOC, 'at least one non-well-known surface MUST be reachable under major 2, or the advertisement rests entirely on the one resource the header selects'),
|
|
138
|
+
).toBeGreaterThan(0);
|
|
139
|
+
});
|
|
140
|
+
});
|