@openwop/openwop-conformance 2.3.4 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +34 -0
- package/README.md +2 -2
- package/dist/spec-artifacts.lock.json +2 -2
- package/package.json +2 -2
- package/requirements.json +63 -7
- package/scenario-majors.json +5 -2
- package/schemas/CORPUS-STAMP.json +46 -46
- package/src/lib/mcp-fake-server.ts +32 -1
- package/src/scenarios/v2-bound-id-kinds.test.ts +128 -0
- package/src/scenarios/v2-negotiation-authenticated.test.ts +6 -1
- package/src/scenarios/v2-webhook-durable-delivery.test.ts +18 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,39 @@
|
|
|
1
1
|
# `@openwop/openwop-conformance` Changelog
|
|
2
2
|
|
|
3
|
+
## [2.4.0] — 2026-09-18 — every bound kind, not just runId
|
|
4
|
+
|
|
5
|
+
**Why a minor.** One scenario file is added (`PROTOCOL-STATUS.md` §suite version).
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- **`v2-bound-id-kinds.test.ts`** — one leg per tenant-bound kind with a wire
|
|
10
|
+
surface (`subscriptionId` via `webhookId`, `interruptId`, `effectId`), each
|
|
11
|
+
gated on the family that mints it: the grammar, the `~`-projected accept side,
|
|
12
|
+
and the `403 id_tenant_mismatch` a foreign tenant segment MUST draw whether or
|
|
13
|
+
not the id exists. Until now every v2 scenario that read a bound id read
|
|
14
|
+
`runId`, so a host binding one kind of five was green — which is what a tier-1
|
|
15
|
+
host's own audit found on its wire. The `deliveryId` leg records
|
|
16
|
+
`inapplicable`: the corpus serves no dead-letter read, so that kind has
|
|
17
|
+
nowhere to appear (RFC 0187 §Unresolved).
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- **`v2-webhook-durable-delivery` counted another scenario's deliveries against
|
|
22
|
+
its retry budget.** Both legs filtered attempts by `runId` alone; a host that
|
|
23
|
+
cannot reach the suite's loopback registers every scenario against one
|
|
24
|
+
tunnelled receiver URL, so a concurrent scenario's subscription matched the
|
|
25
|
+
same run. A tier-2 host measured 6 attempts against a `maxAttempts` of 5 at
|
|
26
|
+
`--max-workers 2` while its own logs showed five. Both legs now also filter on
|
|
27
|
+
the delivery's own `webhookId` — exact, and holds at any worker count.
|
|
28
|
+
- **`McpFakeServer.needs_input_loop`** re-issues `input_required` for
|
|
29
|
+
`arguments.rounds` retries, so a host's MRTR loop can be driven past its
|
|
30
|
+
ceiling; `v2-mrtr-rounds-ceiling` recorded `blocked` without it.
|
|
31
|
+
- **`v2-negotiation-authenticated`** reads `details.runId` off the closed error
|
|
32
|
+
envelope like `v2-minimum-version-refused` does.
|
|
33
|
+
## [2.3.5] — 2026-09-17 — spec prose editorial pass
|
|
34
|
+
|
|
35
|
+
**Why a patch.** No scenario changed. The v2 prose shipped in spec-artifacts was edited (openwop CHANGELOG [2.3.5]), so 2.3.4 no longer identifies its published contents and the pins move together.
|
|
36
|
+
|
|
3
37
|
## [2.3.4] — 2026-09-17 — registers regenerated
|
|
4
38
|
|
|
5
39
|
**Why a patch.** No scenario changed; `spec/v1/gaps.json` (shipped in spec-artifacts) regenerated from the dispositioned registers (openwop CHANGELOG [2.3.4]).
|
package/README.md
CHANGED
|
@@ -117,7 +117,7 @@ Exit code is non-zero on any failed assertion. `--certify` distinguishes: `0`
|
|
|
117
117
|
|
|
118
118
|
## What's Covered
|
|
119
119
|
|
|
120
|
-
The current suite has
|
|
120
|
+
The current suite has 522 scenario files under `src/scenarios/`.
|
|
121
121
|
- 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.
|
|
122
122
|
- 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.
|
|
123
123
|
- 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.**
|
|
@@ -462,7 +462,7 @@ Server-required (added in 1.7.0):
|
|
|
462
462
|
| ------------- | ----------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
463
463
|
| **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. |
|
|
464
464
|
|
|
465
|
-
Current source tree:
|
|
465
|
+
Current source tree: 522 scenario files. Use [`coverage.md`](./coverage.md) for current grade/gap tracking.
|
|
466
466
|
|
|
467
467
|
## Remaining Gaps
|
|
468
468
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openwop/openwop-conformance",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0",
|
|
4
4
|
"description": "Production-ready black-box conformance suite for OpenWOP v1.0 compliant servers.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -56,6 +56,6 @@
|
|
|
56
56
|
"@openwop/spec-artifacts": "file:../spec-artifacts"
|
|
57
57
|
},
|
|
58
58
|
"peerDependencies": {
|
|
59
|
-
"@openwop/spec-artifacts": "2.
|
|
59
|
+
"@openwop/spec-artifacts": "2.4.0"
|
|
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":
|
|
6
|
-
"tests":
|
|
7
|
-
"withStableId":
|
|
5
|
+
"files": 571,
|
|
6
|
+
"tests": 2172,
|
|
7
|
+
"withStableId": 2172,
|
|
8
8
|
"interpolatedTitles": 0,
|
|
9
|
-
"explicitIds":
|
|
9
|
+
"explicitIds": 2125
|
|
10
10
|
},
|
|
11
11
|
"records": [
|
|
12
12
|
{
|
|
@@ -26006,6 +26006,62 @@
|
|
|
26006
26006
|
}
|
|
26007
26007
|
]
|
|
26008
26008
|
},
|
|
26009
|
+
{
|
|
26010
|
+
"id": "openwop.it.v2-bound-id-kinds.subscriptionid-post-webhooks-mints-a-bound-webhookid-the-projected-segment-is-ac",
|
|
26011
|
+
"file": "v2-bound-id-kinds.test.ts",
|
|
26012
|
+
"line": 60,
|
|
26013
|
+
"title": "subscriptionId: POST /webhooks mints a bound webhookId, the projected segment is accepted, and a foreign tenant segment is refused 403",
|
|
26014
|
+
"explicitId": "openwop.requirement.0187.bound-id-kinds.webhook",
|
|
26015
|
+
"citations": [
|
|
26016
|
+
{
|
|
26017
|
+
"section": "webhooks.md §Surfaces",
|
|
26018
|
+
"requirement": "POST /webhooks MUST answer 201 { webhookId }"
|
|
26019
|
+
},
|
|
26020
|
+
{
|
|
26021
|
+
"section": "spec/v2/core/identity.md §5 (RFC 0187 §A)",
|
|
26022
|
+
"requirement": null,
|
|
26023
|
+
"interpolated": true
|
|
26024
|
+
},
|
|
26025
|
+
{
|
|
26026
|
+
"section": "spec/v2/core/identity.md §5 (RFC 0187 §A)",
|
|
26027
|
+
"requirement": "the refusal MUST name id_tenant_mismatch, not not_found — the tenant check is the point"
|
|
26028
|
+
},
|
|
26029
|
+
{
|
|
26030
|
+
"section": "spec/v2/core/identity.md §5 (RFC 0187 §A)",
|
|
26031
|
+
"requirement": null,
|
|
26032
|
+
"interpolated": true
|
|
26033
|
+
}
|
|
26034
|
+
]
|
|
26035
|
+
},
|
|
26036
|
+
{
|
|
26037
|
+
"id": "openwop.it.v2-bound-id-kinds.interruptid-the-id-a-suspension-puts-on-the-wire-is-bound",
|
|
26038
|
+
"file": "v2-bound-id-kinds.test.ts",
|
|
26039
|
+
"line": 87,
|
|
26040
|
+
"title": "interruptId: the id a suspension puts on the wire is bound",
|
|
26041
|
+
"explicitId": null,
|
|
26042
|
+
"citations": []
|
|
26043
|
+
},
|
|
26044
|
+
{
|
|
26045
|
+
"id": "openwop.it.v2-bound-id-kinds.effectid-every-row-get-runs-runid-effects-serves-is-bound",
|
|
26046
|
+
"file": "v2-bound-id-kinds.test.ts",
|
|
26047
|
+
"line": 108,
|
|
26048
|
+
"title": "effectId: every row GET /runs/{runId}/effects serves is bound",
|
|
26049
|
+
"explicitId": "openwop.requirement.0187.bound-id-kinds.per-kind",
|
|
26050
|
+
"citations": [
|
|
26051
|
+
{
|
|
26052
|
+
"section": "security-defaults.md §Layer-2 effect identity",
|
|
26053
|
+
"requirement": "GET /runs/{runId}/effects MUST answer 200"
|
|
26054
|
+
}
|
|
26055
|
+
]
|
|
26056
|
+
},
|
|
26057
|
+
{
|
|
26058
|
+
"id": "openwop.it.v2-bound-id-kinds.deliveryid-the-kind-has-no-wire-surface-to-be-bound-on-a-corpus-gap-recorded",
|
|
26059
|
+
"file": "v2-bound-id-kinds.test.ts",
|
|
26060
|
+
"line": 123,
|
|
26061
|
+
"title": "deliveryId: the kind has no wire surface to be bound on — a corpus gap, recorded",
|
|
26062
|
+
"explicitId": null,
|
|
26063
|
+
"citations": []
|
|
26064
|
+
},
|
|
26009
26065
|
{
|
|
26010
26066
|
"id": "openwop.it.v2-bound-id-path-projection.a-tenant-bound-id-is-readable-at-its-escaped-path-segment-links-carry-that-form",
|
|
26011
26067
|
"file": "v2-bound-id-path-projection.test.ts",
|
|
@@ -27505,7 +27561,7 @@
|
|
|
27505
27561
|
{
|
|
27506
27562
|
"id": "openwop.it.v2-negotiation-authenticated.an-unauthenticated-a2a-exchange-cannot-lower-the-version-below-preferredversion",
|
|
27507
27563
|
"file": "v2-negotiation-authenticated.test.ts",
|
|
27508
|
-
"line":
|
|
27564
|
+
"line": 91,
|
|
27509
27565
|
"title": "an unauthenticated A2A exchange cannot lower the version below preferredVersion",
|
|
27510
27566
|
"explicitId": null,
|
|
27511
27567
|
"citations": []
|
|
@@ -27513,7 +27569,7 @@
|
|
|
27513
27569
|
{
|
|
27514
27570
|
"id": "openwop.it.v2-negotiation-authenticated.an-unauthenticated-mcp-exchange-cannot-lower-the-revision-below-preferredversion",
|
|
27515
27571
|
"file": "v2-negotiation-authenticated.test.ts",
|
|
27516
|
-
"line":
|
|
27572
|
+
"line": 111,
|
|
27517
27573
|
"title": "an unauthenticated MCP exchange cannot lower the revision below preferredVersion",
|
|
27518
27574
|
"explicitId": null,
|
|
27519
27575
|
"citations": []
|
|
@@ -29331,7 +29387,7 @@
|
|
|
29331
29387
|
{
|
|
29332
29388
|
"id": "openwop.it.v2-webhook-durable-delivery.an-exhausted-delivery-is-dead-lettered-never-dropped",
|
|
29333
29389
|
"file": "v2-webhook-durable-delivery.test.ts",
|
|
29334
|
-
"line":
|
|
29390
|
+
"line": 321,
|
|
29335
29391
|
"title": "an exhausted delivery is dead-lettered, never dropped",
|
|
29336
29392
|
"explicitId": "openwop.requirement.0173.webhook-durable-delivery.dead-letter",
|
|
29337
29393
|
"citations": [
|
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": 522,
|
|
5
5
|
"v1": 445,
|
|
6
|
-
"v2":
|
|
6
|
+
"v2": 79
|
|
7
7
|
},
|
|
8
8
|
"majors": {
|
|
9
9
|
"a2a-1-0-agent-card.test.ts": [
|
|
@@ -1244,6 +1244,9 @@
|
|
|
1244
1244
|
"v2-assurance-downgrade-audited.test.ts": [
|
|
1245
1245
|
2
|
|
1246
1246
|
],
|
|
1247
|
+
"v2-bound-id-kinds.test.ts": [
|
|
1248
|
+
2
|
|
1249
|
+
],
|
|
1247
1250
|
"v2-bound-id-path-projection.test.ts": [
|
|
1248
1251
|
2
|
|
1249
1252
|
],
|
|
@@ -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.
|
|
5
|
-
"corpusTag": "v2.
|
|
4
|
+
"version": "2.4.0",
|
|
5
|
+
"corpusTag": "v2.4.0",
|
|
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": "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": "d7297c933f4987c085f1427b1cb245c0e7f88d0ab91a1d1534c7959bb659940e",
|
|
13
|
+
"api/v2/asyncapi.yaml": "5730b64974958076b2da1e77990011228465c088334a8a5c6005e136cf86e20d",
|
|
14
|
+
"api/v2/openapi.yaml": "cc94e454854f28d7aae5f0cab264a21dfdc8ce43259dfd30ea4e2340ede756fc",
|
|
15
15
|
"api/v2/redocly.yaml": "1e66b60e6118ad11a823bb620678be464d99dfe50a40e3e6f93ec9429b88b34c",
|
|
16
16
|
"schemas/README.md": "0c0b737ffcf8f30e7d2809cec8a498232de710f41443212922ad8337cdde0b51",
|
|
17
17
|
"schemas/a2a-task-state.schema.json": "c9365918f993f943b4b619d42551eb066a1ed33a08d895d51b395432a5b1f1bc",
|
|
@@ -144,12 +144,12 @@
|
|
|
144
144
|
"schemas/v2/error-envelope.schema.json": "9c8f747e5bea81943d70e0930bc7d26586a4c3a3d90be77f46359b19b7e56cbe",
|
|
145
145
|
"schemas/v2/eval-summary.schema.json": "9d31025752d0f67ad3b8a32e82a5314972c530ef9f780dd82e484ae8c5131198",
|
|
146
146
|
"schemas/v2/export-bundle.schema.json": "bd28905a7729437ab7f2ad8bb33e13a371b2bf5605a7eb2962a0d246de312d69",
|
|
147
|
-
"schemas/v2/form-content-pack-manifest.schema.json": "
|
|
147
|
+
"schemas/v2/form-content-pack-manifest.schema.json": "461529e4287ec3b15a9fbcde57226a1c6f17ea3ae71b33b418e6f595ff0619fe",
|
|
148
148
|
"schemas/v2/frontend-plugin-manifest.schema.json": "fed6b8ddb3056c53b1bfc91525410a4445a3c37a8bf9eb9f6f9964ffea26d993",
|
|
149
149
|
"schemas/v2/goal.schema.json": "146c4182e36d3538778f947e668a92cdb69ec42ee0820104b00063b035e342d7",
|
|
150
150
|
"schemas/v2/heartbeat-evaluated.schema.json": "4086b1ba1d0547c08b038dfb263ac89416a3e406430db1250156fb2f9b74f8d6",
|
|
151
151
|
"schemas/v2/heartbeat-state-changed.schema.json": "eb68d0082aa996650e3a6b8ccf32457f959cebc7e6728cc7c331b9d171a4bef6",
|
|
152
|
-
"schemas/v2/ids.schema.json": "
|
|
152
|
+
"schemas/v2/ids.schema.json": "65cfaa193be0ca9cf48af39c18c553979e3ec1f7fc04509aaa513b322d7b7e4a",
|
|
153
153
|
"schemas/v2/localized-content-language-settings.schema.json": "92240f19a2b4b2f05ab41a797f703485a28813848035478e72f395edd04c1d35",
|
|
154
154
|
"schemas/v2/localized-content-page-response.schema.json": "ef48329749758afb21e4d3cbc3f017abb911f75726819457f35b9f1390332c75",
|
|
155
155
|
"schemas/v2/localized-content-page.schema.json": "e0f97660121955d6705d935f34e748adb739ac70f8536a009cd8d6f303735151",
|
|
@@ -170,7 +170,7 @@
|
|
|
170
170
|
"schemas/v2/run-ancestry-response.schema.json": "334c17f2062dea62726ca1d07b8be3f169cdc422af843c1def5fb317039814c9",
|
|
171
171
|
"schemas/v2/run-diff-response.schema.json": "8bdb2045c274580038bd74781f7d062f5049839797c164a89de4ed89edc53a74",
|
|
172
172
|
"schemas/v2/run-event-payloads.schema.json": "29dec16f0d40c9022c169d4d413742ddd775a3d8c895d3a60ccc093e2a872439",
|
|
173
|
-
"schemas/v2/run-event.schema.json": "
|
|
173
|
+
"schemas/v2/run-event.schema.json": "2f0d7416bd376c56affe97b6b2b49981532e8e9ff45c0a162cf806c78b3e7fb7",
|
|
174
174
|
"schemas/v2/run-list-response.schema.json": "c2dbc34768dcf64528914a213aa3cb55952e8e3e90a64b04a9423b2a19749d38",
|
|
175
175
|
"schemas/v2/run-options.schema.json": "f0e9f494d80086c2ed2cf9fe87df54e5b4e66601699c4a1089f4edbc7ba58867",
|
|
176
176
|
"schemas/v2/run-orchestrator-decided-event.schema.json": "bb76e15ff72e669f12938684033b55dc567470e1a378e19ffc3fa0e4034ab5a7",
|
|
@@ -185,10 +185,10 @@
|
|
|
185
185
|
"schemas/v2/tool-descriptor.schema.json": "658b18ef173e0f12d732f92d4866a9b3daeb8f88b493db8b025a577e09dd681c",
|
|
186
186
|
"schemas/v2/trigger-event.schema.json": "7227fb9a130c33374fa8eab35e4448c4e8f22c962b8f271781cf4dbb5e4288b7",
|
|
187
187
|
"schemas/v2/trigger-subscription-registration.schema.json": "04bc314a07c7e8b565577cb8df5821d968ce2af915900dfdccd4a24e928d20db",
|
|
188
|
-
"schemas/v2/trigger-subscription.schema.json": "
|
|
188
|
+
"schemas/v2/trigger-subscription.schema.json": "c887d4b9a12be0133f8c9dfd3a8bafcb518e27ecc2e800447ebc4d1fef111503",
|
|
189
189
|
"schemas/v2/ui-plugin-message.schema.json": "879d65867c01656b3962d64283e66364dcb8f626ad26c5fcf2d2d55a79ae0ce9",
|
|
190
|
-
"schemas/v2/webhook-delivery.schema.json": "
|
|
191
|
-
"schemas/v2/workflow-chain-pack-manifest.schema.json": "
|
|
190
|
+
"schemas/v2/webhook-delivery.schema.json": "4e8ab3b722ad1731adf0548c1217f0de62da40de14e8abf77533aee4ace4c079",
|
|
191
|
+
"schemas/v2/workflow-chain-pack-manifest.schema.json": "b61cc945b4df1c7093a1058fa71ee59ef10bacb3433ae4d7ae7536136e616cc2",
|
|
192
192
|
"schemas/v2/workflow-definition.schema.json": "9aafddec32b84eb6aec3185be024850f1b9444c6193568771b0b636595fef136",
|
|
193
193
|
"schemas/v2/workload-identity.schema.json": "1f589debfac6d70e0908da511550ec69a27b4bcc8314551cd843b6d923605efc",
|
|
194
194
|
"schemas/v2/workspace-file-create.schema.json": "b6b5260857318c3b67de0231ed15e3a746f525880a763e7b36a7854178dd10d0",
|
|
@@ -198,65 +198,65 @@
|
|
|
198
198
|
"schemas/workload-identity.schema.json": "64ed93e12cc9f0657e56ff9b8f045e5d68d397df5276d103c07f42c7f34f0579",
|
|
199
199
|
"schemas/workspace-file-create.schema.json": "70a23b92c2958c0165d7d29aea9020a045dc7816af82c80c5f50d0aac19c4856",
|
|
200
200
|
"schemas/workspace-file.schema.json": "464de85c2a068243084ee9c1d969bc7cd5d8f7948574e58450d6493c38a0e1e4",
|
|
201
|
-
"spec/v1/alias-detectors.json": "
|
|
201
|
+
"spec/v1/alias-detectors.json": "e90916db12d2297e2e560b2d54d2c4702d6cb3bef7179a575844687a7ec17262",
|
|
202
202
|
"spec/v1/capability-declaration-classes.json": "e7729aed5c4b4e1dd02abab0530f14cc95f5d4070fe51fb139e7f5cccefa00c6",
|
|
203
|
-
"spec/v1/core-standard-manifest.json": "
|
|
204
|
-
"spec/v1/deprecations.json": "
|
|
203
|
+
"spec/v1/core-standard-manifest.json": "41af65d02636d651a99f0f48caa5dd5ed45be576f745166bd1510908674997e0",
|
|
204
|
+
"spec/v1/deprecations.json": "c81c8f71460936c8d489d8270477c7661cf4a5d7b7bad86c98899750b5656027",
|
|
205
205
|
"spec/v1/deprecations.schema.json": "18c87e78bedc210431f795ae44c5b5d202f2f3317850d5cf86867d4f1fa1cdfb",
|
|
206
206
|
"spec/v1/event-codemap.json": "3da60d884157793a360da532a9fcbbfb5285636db325a74cec94b34622186d97",
|
|
207
207
|
"spec/v1/event-codemap.schema.json": "d05933b2e88103aff51a2f774df97b9bda065e0fc88dbd9b114cc47a32189174",
|
|
208
208
|
"spec/v1/extensions.json": "79a60754aa16cbdbdb604f8e5c00038af535a1690a4d07bd5ff7c38f0256a7bc",
|
|
209
|
-
"spec/v1/gaps.json": "
|
|
209
|
+
"spec/v1/gaps.json": "6f595da01299deecf928d358449eb7f97ebf9bbfe82e40be82f629f0e256cdfd",
|
|
210
210
|
"spec/v1/gaps.schema.json": "8fd83259f556553c9df0f53e7a82ca8c2a4771d471197f69b4e2ae0ceeacfd66",
|
|
211
211
|
"spec/v1/migrations.json": "8839497d6a4830b53fa7867014a8336f0d725ef2346352e54fb580b7947897af",
|
|
212
212
|
"spec/v1/migrations.schema.json": "886779aa6c22e646db097f5df210adb018a4dd14a7b815465a18c8a7056c8f72",
|
|
213
213
|
"spec/v1/operation-path-manifest.json": "5f5f4e3842669371730ebbd1aace3fb794192615f0018dc144f484ba5db82ac3",
|
|
214
|
-
"spec/v1/spec-gaps.json": "
|
|
215
|
-
"spec/v2/README.md": "
|
|
214
|
+
"spec/v1/spec-gaps.json": "eb3bfcb9c7d05c9a6845d4a40ec9493cfaa5a671932daf1af561310136fdabe8",
|
|
215
|
+
"spec/v2/README.md": "5d49f56f41e06b44312c61ad8144b786c9a90ec42fd0660a40d89520ef206137",
|
|
216
216
|
"spec/v2/core/capabilities.md": "c5107a4964e487c20bbb5254cfe1f706e0d9ba7e34da9070bc43b50b385e7948",
|
|
217
|
-
"spec/v2/core/conformance.md": "
|
|
217
|
+
"spec/v2/core/conformance.md": "b8d7dae142e3708a7597533cb77827363b6cfc91b89538081a9afe5e23c975f4",
|
|
218
218
|
"spec/v2/core/connection-packs.md": "1d759681e2962f5103228c52c45f24607ed4bb9ae73e30605f690c1d5a59e9ee",
|
|
219
219
|
"spec/v2/core/errors.md": "ad85fc830139e587f20deabf88297b91b1fb6ce31e80d5442624a650246ab16e",
|
|
220
|
-
"spec/v2/core/events.md": "
|
|
220
|
+
"spec/v2/core/events.md": "f1bac16419740f3b722febb6f5caaabed8838e092cba3d080fe9184024049dcf",
|
|
221
221
|
"spec/v2/core/form-content-packs.md": "6ffaae85d4550be48dd53fb6e2959af8aceb35bd87c5343b722885084f197713",
|
|
222
|
-
"spec/v2/core/headers.md": "
|
|
222
|
+
"spec/v2/core/headers.md": "6f350e4a6893e82fcab40aec6cb515b989e8b56a20a74f6d10d24a0d3fdf5cf0",
|
|
223
223
|
"spec/v2/core/idempotency.md": "b86c352d964aebad532727f314e26ed2177d49de382076fa64d3162e2f56118d",
|
|
224
224
|
"spec/v2/core/identity.md": "9d2b823eb899463807452fe5721dcf09881837c161be024175957db27870b243",
|
|
225
225
|
"spec/v2/core/interop.md": "d4c404e469e1d9d9be88bc2c939d777e7d7c1d0419b1251c18770f039838a401",
|
|
226
|
-
"spec/v2/core/interrupt.md": "
|
|
226
|
+
"spec/v2/core/interrupt.md": "548c7a7bdfcab56db33dfad3d397cae2ed5f9828a63418021936eb392395e3e4",
|
|
227
227
|
"spec/v2/core/overview.md": "69377c90bbb87b6bfe4b8be4f1b65810094b0759c8d4f93ca6df92cc530ea023",
|
|
228
228
|
"spec/v2/core/packs.md": "8b3e1587270bb1aec814befbd287181a05b7f76aeb0802787ee7ebdc85901205",
|
|
229
|
-
"spec/v2/core/persistence.md": "
|
|
229
|
+
"spec/v2/core/persistence.md": "898dc929d021a03d245691148fe4fdec4582b4faa9237aa32037bbd3a2993b7d",
|
|
230
230
|
"spec/v2/core/replay.md": "bd191a2565aa7ad0df9b35a0716f3de53be34b8722f1807c5e5b02859404a9b9",
|
|
231
231
|
"spec/v2/core/runs.md": "f7a95fc82fda5b7c0c613de6e5edd376bbd6d8ba4c665a500c94eacbe42b86f2",
|
|
232
|
-
"spec/v2/core/security-defaults.md": "
|
|
233
|
-
"spec/v2/core/versioning.md": "
|
|
234
|
-
"spec/v2/core/webhooks.md": "
|
|
232
|
+
"spec/v2/core/security-defaults.md": "d69a8dfc9057e189caa61f14810e3090b3b053e8fde419d09db84729a42b84c7",
|
|
233
|
+
"spec/v2/core/versioning.md": "fd6564879cf043b68338db20b20de93755a1e7147000afb4738c07770deeddaf",
|
|
234
|
+
"spec/v2/core/webhooks.md": "969c367650e5e93beb090242bcbec59f43b95bdf646fa6c512f6c7fa1f05912e",
|
|
235
235
|
"spec/v2/core/workflow-chain-packs.md": "ef2b07d2d99334eaec0cc370314e2ad2f84ea58deb699e5d92e6e52d6056613f",
|
|
236
236
|
"spec/v2/declaration.json": "a09bac61c1b825e4a77b5abc0f86f1f8c20c381e0650eb08c2f6d0ef0aaec1dc",
|
|
237
237
|
"spec/v2/declaration.schema.json": "c86e3083fb9789c1958ecb4576cc6f71aa90e6f339521139238543ed08211dd4",
|
|
238
238
|
"spec/v2/errors.json": "4ff032033f02d7e3fb4204581456408ba5e7642b438c1f349bbd1c264e63e419",
|
|
239
239
|
"spec/v2/event-codemap.json": "37869a03cdfd8758b57e6f350641a6ebbd383231cebda51f5473ad85f94ab74c",
|
|
240
240
|
"spec/v2/event-codemap.schema.json": "b173f2a9bcc0bf9b62a474ce3b91e1431d597e64fd1560fc58662cd8606eaf9c",
|
|
241
|
-
"spec/v2/ext/README.md": "
|
|
242
|
-
"spec/v2/ext/a2uiSurface/README.md": "
|
|
243
|
-
"spec/v2/ext/brand/README.md": "
|
|
244
|
-
"spec/v2/ext/canvas/README.md": "
|
|
245
|
-
"spec/v2/ext/chat/README.md": "
|
|
246
|
-
"spec/v2/ext/coordination/README.md": "
|
|
247
|
-
"spec/v2/ext/dataIntegration/README.md": "
|
|
248
|
-
"spec/v2/ext/entities/README.md": "
|
|
249
|
-
"spec/v2/ext/grpc-transport/README.md": "
|
|
250
|
-
"spec/v2/ext/kanban/README.md": "
|
|
251
|
-
"spec/v2/ext/knowledge/README.md": "
|
|
252
|
-
"spec/v2/ext/launchStudio/README.md": "
|
|
253
|
-
"spec/v2/ext/messaging/README.md": "
|
|
254
|
-
"spec/v2/ext/portability/README.md": "
|
|
255
|
-
"spec/v2/ext/provider-idempotency/README.md": "
|
|
241
|
+
"spec/v2/ext/README.md": "68b4203c1ffcb1434af79b5e04ae3d23501977e7d48b2256c18a6c92559f6270",
|
|
242
|
+
"spec/v2/ext/a2uiSurface/README.md": "7dfdd0db93b8bd2d8cfb0ebcc7c474f21f2cb0d3108cb66becf311288d21a9ef",
|
|
243
|
+
"spec/v2/ext/brand/README.md": "f5d3c6232e0a63fe445e9707dd6b0e117f8ececcec7194049fafa5562b63aa10",
|
|
244
|
+
"spec/v2/ext/canvas/README.md": "601537975b78d6f762a4a42c7cae583c54c61dfb73817cef99ac03177e671c11",
|
|
245
|
+
"spec/v2/ext/chat/README.md": "f4023b16b3749fbe3786439e370ee73c459c02f8d1c439b5df0287e1f24e1e56",
|
|
246
|
+
"spec/v2/ext/coordination/README.md": "70b00ede75f22c5fbdf4d8e0840d82d2dfeac61bda1459f94d6ebc2adb6d2865",
|
|
247
|
+
"spec/v2/ext/dataIntegration/README.md": "0a040215adbfc1cc176c2fbe052598701c9c36461367f0cd6b9e8d7bb1ee20da",
|
|
248
|
+
"spec/v2/ext/entities/README.md": "24cbf19103cb4711f41088a79a49077eaf4750b0c746764a8302e33b74ea54c8",
|
|
249
|
+
"spec/v2/ext/grpc-transport/README.md": "0d01d939fc7a4eab050f141b42e4fe145f312bcdd89ef78359a47e33c4e94751",
|
|
250
|
+
"spec/v2/ext/kanban/README.md": "c6b3b3bbf6c9cecae674fc7874e145dc3e4f723a864409ad6e96b0173868f801",
|
|
251
|
+
"spec/v2/ext/knowledge/README.md": "067280b15e14b3c46550d7b0ee68d49e7d1455ce86218d05bbc44535eb668bf1",
|
|
252
|
+
"spec/v2/ext/launchStudio/README.md": "3c4258e781d069dced34ab45552ce5b2b4af6ec7e08f0e2e00584d87a7981b15",
|
|
253
|
+
"spec/v2/ext/messaging/README.md": "0ce7535498458ad63d9b7d5b143da618a2da2ce4033ca53ea3fc4cb0c65419db",
|
|
254
|
+
"spec/v2/ext/portability/README.md": "15b8a881df861ff289489ca13a3a4871cf6e14aed90ca018afadff2388a05476",
|
|
255
|
+
"spec/v2/ext/provider-idempotency/README.md": "e3f159b2d1b53c4c279b911de91d80b5410a1e6a05ba940a7f4d2c0d547cf000",
|
|
256
256
|
"spec/v2/ext/provider-idempotency/registry.json": "5b5fecad604fb4ab38da39f5abe697a4e57b05406c398f8afeb377331b2bd965",
|
|
257
|
-
"spec/v2/ext/restTransport/README.md": "
|
|
258
|
-
"spec/v2/ext/sandbox-runtime-notes/README.md": "
|
|
259
|
-
"spec/v2/ext/webResearch/README.md": "
|
|
257
|
+
"spec/v2/ext/restTransport/README.md": "8cfc6471ca1ca6e43bb53b21d47b33e73c6a5821e631eb4f565b971c01e2e5de",
|
|
258
|
+
"spec/v2/ext/sandbox-runtime-notes/README.md": "94a25409596e5122c217c566cb02bd44dde33c8130f52507988d498def598f29",
|
|
259
|
+
"spec/v2/ext/webResearch/README.md": "76409f9d6a4aa512ba2b8bdffe706b2b509503266037c3ef890950f7b8c9bd2b",
|
|
260
260
|
"spec/v2/facets/a2a.schema.json": "6b0bceddec6d18d5115aa7d04720a0cc1ad7a9a0a78f53c208b34becaba5d16e",
|
|
261
261
|
"spec/v2/facets/aiProviders.schema.json": "0c76e48e5d487b5be3b3ab1f6512a4e206c840ee5087d1336c7d7e2853b58d03",
|
|
262
262
|
"spec/v2/facets/auth.schema.json": "73440c944ad12c41aab0618aeb799a90a0b59e97fc685527f248afb7d015381a",
|
|
@@ -267,12 +267,12 @@
|
|
|
267
267
|
"spec/v2/facets/runList.schema.json": "6223e1baaebbb9f4faa71bfe968f8d22b4aa88046d0e43e8d5f56409e7f86e90",
|
|
268
268
|
"spec/v2/facets/sandbox.schema.json": "e75f54486d387b2d089791aa93b5093cd1450f1920753a0f97fc23acdfe978ed",
|
|
269
269
|
"spec/v2/facets/webhooks.schema.json": "0b766033ef4b4c6c2ac6f9979a19843ba024d3a1d1576972dcb55aaba52f1b9a",
|
|
270
|
-
"spec/v2/id-field-bindings.json": "
|
|
270
|
+
"spec/v2/id-field-bindings.json": "e73c06f3055ad30173507ac817a458b96362c8f26209ba105c53653292cf63d3",
|
|
271
271
|
"spec/v2/path-manifest.json": "a6f4e652e866889e8bde815874062f62929aee95dc8e58e7152c56adee262163",
|
|
272
272
|
"spec/v2/peer-dependency-aliases.json": "d10299280abee08258502925bc327293ee413e0108cd6e6ec75ff6110653308d",
|
|
273
273
|
"spec/v2/profiles.json": "0636f19fceae625390003a347e70ef4797d84766b5c24ce8a02cea52aadebca4",
|
|
274
|
-
"spec/v2/release.json": "
|
|
274
|
+
"spec/v2/release.json": "975e8d6100d4c0697384dd093eb586eed3f41481a26b954e052fa321525a0273",
|
|
275
275
|
"spec/v2/retention-floors.json": "eaf3722d95c79947af1d4269ef85117e126518c588cfcf1a2b21b97269f51624"
|
|
276
276
|
},
|
|
277
|
-
"corpusCommit": "
|
|
277
|
+
"corpusCommit": "5f410d86a87e25473f8e1d63ce08e9b0947c7e43"
|
|
278
278
|
}
|
|
@@ -88,6 +88,13 @@ const NEEDS_INPUT_TOOL = {
|
|
|
88
88
|
inputSchema: { type: 'object', properties: {}, additionalProperties: false },
|
|
89
89
|
};
|
|
90
90
|
|
|
91
|
+
/** The MRTR ceiling fixture: keeps asking for `arguments.rounds` retries (RFC 0175 §E.1). */
|
|
92
|
+
const NEEDS_INPUT_LOOP_TOOL = {
|
|
93
|
+
name: 'needs_input_loop',
|
|
94
|
+
description: 'Re-issues input_required for `rounds` retries before completing — drives a host past its mcp.mrtr.maxRounds ceiling.',
|
|
95
|
+
inputSchema: { type: 'object', properties: { rounds: { type: 'integer', minimum: 1 } }, required: ['rounds'] },
|
|
96
|
+
};
|
|
97
|
+
|
|
91
98
|
export class McpFakeServer {
|
|
92
99
|
private _server: Server | null = null;
|
|
93
100
|
private _boundPort = 0;
|
|
@@ -263,7 +270,7 @@ export class McpFakeServer {
|
|
|
263
270
|
});
|
|
264
271
|
|
|
265
272
|
case 'tools/list':
|
|
266
|
-
return ok({ resultType: 'complete', tools: [ECHO_TOOL, NEEDS_INPUT_TOOL], ttlMs: 60_000, cacheScope: 'public' });
|
|
273
|
+
return ok({ resultType: 'complete', tools: [ECHO_TOOL, NEEDS_INPUT_TOOL, NEEDS_INPUT_LOOP_TOOL], ttlMs: 60_000, cacheScope: 'public' });
|
|
267
274
|
|
|
268
275
|
case 'tools/call': {
|
|
269
276
|
const name = params['name'];
|
|
@@ -278,6 +285,30 @@ export class McpFakeServer {
|
|
|
278
285
|
this._nextResultAssertsAuthority = false;
|
|
279
286
|
return ok({ resultType: 'complete', content: [{ type: 'text', text }], isError: false, ...extra });
|
|
280
287
|
}
|
|
288
|
+
if (name === 'needs_input_loop') {
|
|
289
|
+
// The ceiling fixture (RFC 0175 §E.1): re-issues `input_required` for
|
|
290
|
+
// `arguments.rounds` retries before completing, so a host's MRTR loop
|
|
291
|
+
// can be driven PAST `mcp.mrtr.maxRounds`. `needs_input` completes on
|
|
292
|
+
// the first retry and therefore cannot reach any ceiling >= 1, which is
|
|
293
|
+
// why `v2-mrtr-rounds-ceiling` recorded `blocked` until this existed.
|
|
294
|
+
if (clientCaps['elicitation'] === undefined) {
|
|
295
|
+
return err(400, MCP_ERR.MISSING_REQUIRED_CLIENT_CAPABILITY, 'needs_input_loop requires the elicitation client capability', { requiredCapabilities: ['elicitation'] });
|
|
296
|
+
}
|
|
297
|
+
const wanted = Number(((params['arguments'] ?? {}) as { rounds?: unknown }).rounds ?? 1);
|
|
298
|
+
const state = params['requestState'];
|
|
299
|
+
let served = 0;
|
|
300
|
+
if (typeof state === 'string') {
|
|
301
|
+
const m = /^mrtr:needs_input_loop:(\d+)$/.exec(state);
|
|
302
|
+
if (m === null) return err(400, -32602, 'requestState missing or not the value this server issued (clients MUST echo it exactly)');
|
|
303
|
+
served = Number(m[1]);
|
|
304
|
+
}
|
|
305
|
+
if (served >= wanted) return ok({ resultType: 'complete', content: [{ type: 'text', text: `looped ${served}` }], isError: false });
|
|
306
|
+
return ok({
|
|
307
|
+
resultType: 'input_required',
|
|
308
|
+
inputRequests: { who: { method: 'elicitation/create', params: { mode: 'form', message: `round ${served + 1} of ${wanted}`, requestedSchema: { type: 'object', properties: { name: { type: 'string' } }, required: ['name'] } } } },
|
|
309
|
+
requestState: `mrtr:needs_input_loop:${served + 1}`,
|
|
310
|
+
});
|
|
311
|
+
}
|
|
281
312
|
if (name === 'needs_input') {
|
|
282
313
|
const responses = params['inputResponses'] as Record<string, { action?: string; content?: { name?: string } }> | undefined;
|
|
283
314
|
const state = params['requestState'];
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `spec/v2/core/identity.md` §5 + RFC 0187 §A — every tenant-bound KIND that
|
|
3
|
+
* has a wire surface, not just `runId` (suite 2.4.0, target major 2).
|
|
4
|
+
*
|
|
5
|
+
* §5 names five tenant-bound kinds: `runId`, `interruptId`, `subscriptionId`,
|
|
6
|
+
* `deliveryId`, `effectId`. Until this file, every v2 scenario that read a
|
|
7
|
+
* bound id on the wire read `runId` — so a host that bound one kind of five was
|
|
8
|
+
* green. A tier-1 host's own audit found exactly that: its id derivation walked
|
|
9
|
+
* the `$ref`s but matched only `runId`, and `interruptId` / `subscriptionId` /
|
|
10
|
+
* `deliveryId` / `effectId` went out BARE on every major-2 channel, with the
|
|
11
|
+
* accept path unbound on two prefixes. The suite could not have caught it.
|
|
12
|
+
*
|
|
13
|
+
* `webhookId` is the `subscriptionId` kind as of RFC 0187 §A.1: the mint
|
|
14
|
+
* surface (`POST /webhooks → { webhookId }`) had inherited v1's bare
|
|
15
|
+
* `type: string`, so the kind had no HTTP surface and the HTTP surface had no
|
|
16
|
+
* kind — and `identity.md` §5's `403 id_tenant_mismatch` check had nothing to
|
|
17
|
+
* read. Both production hosts mint it bare today and say so; this file is why
|
|
18
|
+
* that is now visible rather than assumed.
|
|
19
|
+
*
|
|
20
|
+
* One leg per kind, each gated on the family that mints it, so a host without
|
|
21
|
+
* the family records `inapplicable` rather than a failure. The `deliveryId`
|
|
22
|
+
* leg records `inapplicable` for a different reason and it is a corpus finding,
|
|
23
|
+
* not a host one: `api/v2/openapi.yaml` serves no dead-letter read, so a bound
|
|
24
|
+
* `deliveryId` has nowhere to appear (RFC 0187 §Unresolved).
|
|
25
|
+
*
|
|
26
|
+
* @see spec/v2/core/identity.md §5
|
|
27
|
+
* @see RFCS/0187-host-found-bindings.md §A
|
|
28
|
+
*/
|
|
29
|
+
import { describe, it, expect } from 'vitest';
|
|
30
|
+
import { driver, type OpenWOPResponse } from '../lib/driver.js';
|
|
31
|
+
import { v2Discovery, gateFamily } from '../lib/v2.js';
|
|
32
|
+
import { readErrorCode } from '../lib/error-envelope.js';
|
|
33
|
+
import { softSkip } from '../lib/soft-skip.js';
|
|
34
|
+
import { req } from '../lib/requirement-ids.js';
|
|
35
|
+
import { projectBoundId, BOUND_ID } from '../lib/bound-id.js';
|
|
36
|
+
|
|
37
|
+
const DOC = 'spec/v2/core/identity.md §5 (RFC 0187 §A)';
|
|
38
|
+
const WEBHOOK_ID = 'openwop.requirement.0187.bound-id-kinds.webhook';
|
|
39
|
+
const PER_KIND = 'openwop.requirement.0187.bound-id-kinds.per-kind';
|
|
40
|
+
|
|
41
|
+
async function http(fn: () => Promise<OpenWOPResponse>): Promise<OpenWOPResponse | null> {
|
|
42
|
+
try { return await fn(); } catch { return null; }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** A bound id whose tenant segment is NOT the caller's — the one input §5's 403 check exists for. */
|
|
46
|
+
function foreignTenant(id: string): string {
|
|
47
|
+
const opaque = id.includes('/') ? id.slice(id.indexOf('/') + 1) : id;
|
|
48
|
+
return `not-the-callers-tenant/${opaque}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Assert one id is the tenant-bound grammar. */
|
|
52
|
+
function expectBound(id: unknown, kind: string, where: string, requirement: string): void {
|
|
53
|
+
expect(
|
|
54
|
+
typeof id === 'string' && BOUND_ID.test(id),
|
|
55
|
+
req(requirement, DOC, `${kind} on the wire MUST be tenant-bound \`<tenantId>/<opaque>\` (identity.md §5 kind table) — ${where} carried ${JSON.stringify(id)}`),
|
|
56
|
+
).toBe(true);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
describe('v2 bound-id kinds (identity.md §5, RFC 0187 §A)', () => {
|
|
60
|
+
it('subscriptionId: POST /webhooks mints a bound webhookId, the projected segment is accepted, and a foreign tenant segment is refused 403', async () => {
|
|
61
|
+
if (!(await v2Discovery())) return softSkip('blocked', 'v2 discovery unreachable');
|
|
62
|
+
if (!(await gateFamily('webhooks'))) return softSkip('inapplicable', 'webhooks family not advertised — no subscription to mint (gate recorded under openwop.family.webhooks)');
|
|
63
|
+
const reg = await http(() => driver.post('/webhooks', { url: 'https://subscriber.invalid/hook', events: ['run.completed'] }));
|
|
64
|
+
if (reg === null) return softSkip('blocked', 'POST /webhooks unreachable (fetch failed)');
|
|
65
|
+
if (reg.status === 400 && readErrorCode(reg.json) === 'webhook_url_rejected') return softSkip('blocked', 'host SSRF guard rejected the fixture receiver URL — set OPENWOP_WEBHOOK_RECEIVER_URL or allow the reserved .invalid host to witness');
|
|
66
|
+
expect(reg.status, req(WEBHOOK_ID, 'webhooks.md §Surfaces', 'POST /webhooks MUST answer 201 { webhookId }')).toBe(201);
|
|
67
|
+
const webhookId = (reg.json as { webhookId?: unknown } | null)?.webhookId;
|
|
68
|
+
expectBound(webhookId, 'webhookId', 'POST /webhooks 201', WEBHOOK_ID);
|
|
69
|
+
if (typeof webhookId !== 'string' || !BOUND_ID.test(webhookId)) return softSkip('blocked', `the mint answered ${JSON.stringify(webhookId)}, which is not a tenant-bound id — the projection and 403 legs have no bound id to drive (the grammar failure is recorded by the assertion above)`);
|
|
70
|
+
|
|
71
|
+
// The 403 check §5 exists for: a bound id whose tenant is not the caller's.
|
|
72
|
+
const foreign = await http(() => driver.delete(`/webhooks/${projectBoundId(foreignTenant(webhookId))}`));
|
|
73
|
+
expect(
|
|
74
|
+
foreign?.status ?? null,
|
|
75
|
+
req(WEBHOOK_ID, DOC, `a tenant-bound webhookId whose tenant segment is not the caller's MUST be refused 403 id_tenant_mismatch — got ${foreign?.status ?? 'no response'} ${readErrorCode(foreign?.json) ?? ''}`.trim()),
|
|
76
|
+
).toBe(403);
|
|
77
|
+
expect(readErrorCode(foreign?.json), req(WEBHOOK_ID, DOC, 'the refusal MUST name id_tenant_mismatch, not not_found — the tenant check is the point')).toBe('id_tenant_mismatch');
|
|
78
|
+
|
|
79
|
+
// The accept side: the RFC 0184 projection on a tenant-bound parameter that is not runId.
|
|
80
|
+
const del = await http(() => driver.delete(`/webhooks/${projectBoundId(webhookId)}`));
|
|
81
|
+
expect(
|
|
82
|
+
del?.status ?? null,
|
|
83
|
+
req(WEBHOOK_ID, DOC, `DELETE /webhooks/{webhookId} MUST accept the ~-projected spelling of the id it minted (RFC 0184 §A: every tenant-bound path parameter) — got ${del?.status ?? 'no response'} ${readErrorCode(del?.json) ?? ''}`.trim()),
|
|
84
|
+
).toBe(204);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('interruptId: the id a suspension puts on the wire is bound', async () => {
|
|
88
|
+
if (!(await v2Discovery())) return softSkip('blocked', 'v2 discovery unreachable');
|
|
89
|
+
if (!(await gateFamily('interrupt'))) return softSkip('inapplicable', 'interrupt family not advertised — no suspension to read (gate recorded under openwop.family.interrupt)');
|
|
90
|
+
const create = await http(() => driver.post('/runs', { workflowId: 'conformance-approval' }));
|
|
91
|
+
if (create === null) return softSkip('blocked', 'POST /runs unreachable (fetch failed)');
|
|
92
|
+
if (create.status !== 201) return softSkip('blocked', `POST /runs {workflowId: conformance-approval} answered ${create.status} ${readErrorCode(create.json) ?? ''} — the suspending fixture is not seeded on this host`.trim());
|
|
93
|
+
const runId = (create.json as { runId?: unknown }).runId;
|
|
94
|
+
if (typeof runId !== 'string') return softSkip('blocked', 'the create answered no runId');
|
|
95
|
+
const until = Date.now() + 15_000;
|
|
96
|
+
let suspended: Record<string, unknown> | undefined;
|
|
97
|
+
while (Date.now() < until && !suspended) {
|
|
98
|
+
const poll = await http(() => driver.get(`/runs/${projectBoundId(runId)}/events/poll?timeout=1`));
|
|
99
|
+
const events = ((poll?.json as { events?: Array<Record<string, unknown>> } | null)?.events ?? []);
|
|
100
|
+
suspended = events.find((e) => e['type'] === 'node.suspended');
|
|
101
|
+
if (!suspended) await new Promise((r) => setTimeout(r, 300));
|
|
102
|
+
}
|
|
103
|
+
if (!suspended) return softSkip('blocked', 'the approval fixture did not reach node.suspended inside 15s — no interruptId on the wire to read');
|
|
104
|
+
const payload = (suspended['payload'] ?? {}) as Record<string, unknown>;
|
|
105
|
+
expectBound(payload['interruptId'], 'interruptId', 'node.suspended.payload', PER_KIND);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('effectId: every row GET /runs/{runId}/effects serves is bound', async () => {
|
|
109
|
+
if (!(await v2Discovery())) return softSkip('blocked', 'v2 discovery unreachable');
|
|
110
|
+
if (!(await gateFamily('idempotency'))) return softSkip('inapplicable', 'idempotency family not advertised — the Layer-2 effect ledger is its obligation (gate recorded under openwop.family.idempotency)');
|
|
111
|
+
const create = await http(() => driver.post('/runs', { workflowId: 'conformance-noop' }));
|
|
112
|
+
if (create === null || create.status !== 201) return softSkip('blocked', `POST /runs answered ${create?.status ?? 'no response'} — no run to read effects on`);
|
|
113
|
+
const runId = (create.json as { runId: string }).runId;
|
|
114
|
+
const res = await http(() => driver.get(`/runs/${projectBoundId(runId)}/effects`));
|
|
115
|
+
if (res === null) return softSkip('blocked', 'GET /runs/{runId}/effects unreachable (fetch failed)');
|
|
116
|
+
if (res.status === 404 || res.status === 405) return softSkip('inapplicable', `the host advertises idempotency but serves no effect ledger at /runs/{runId}/effects (${res.status})`);
|
|
117
|
+
expect(res.status, req(PER_KIND, 'security-defaults.md §Layer-2 effect identity', 'GET /runs/{runId}/effects MUST answer 200')).toBe(200);
|
|
118
|
+
const effects = ((res.json as { effects?: Array<Record<string, unknown>> } | null)?.effects ?? []);
|
|
119
|
+
if (effects.length === 0) return softSkip('inapplicable', 'the noop fixture records no effects — no effectId on the wire to read (a host with an effect-producing fixture witnesses this leg)');
|
|
120
|
+
for (const e of effects) expectBound(e['effectId'], 'effectId', 'GET /runs/{runId}/effects', PER_KIND);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('deliveryId: the kind has no wire surface to be bound on — a corpus gap, recorded', async () => {
|
|
124
|
+
if (!(await v2Discovery())) return softSkip('blocked', 'v2 discovery unreachable');
|
|
125
|
+
if (!(await gateFamily('webhooks'))) return softSkip('inapplicable', 'webhooks family not advertised (gate recorded under openwop.family.webhooks)');
|
|
126
|
+
return softSkip('inapplicable', 'identity.md §5 binds deliveryId, but api/v2/openapi.yaml serves no dead-letter read (a GET /webhooks/{webhookId}/dead-letters projection is needed) — the kind has nowhere to appear on the wire, so no host can be held to it. RFC 0187 §Unresolved; v2-webhook-durable-delivery records the same absence for exhaustion.');
|
|
127
|
+
});
|
|
128
|
+
});
|
|
@@ -66,7 +66,12 @@ async function assertNotLowered(id: string, preferred: string, lower: string, re
|
|
|
66
66
|
req(id, 'interop.md §Authentication', `the wire toward the peer MUST NOT carry the lower version (${lower}) under an unauthenticated exchange — captured ${v}`),
|
|
67
67
|
).not.toBe(lower);
|
|
68
68
|
}
|
|
69
|
-
|
|
69
|
+
// A refusal is the closed error envelope (errors.md): the run that carries the
|
|
70
|
+
// decision is named in `details.runId`, exactly where v2-minimum-version-refused
|
|
71
|
+
// reads it. Reading only the top-level key recorded `blocked` on a host that had
|
|
72
|
+
// emitted the event correctly.
|
|
73
|
+
const body = (res.json ?? {}) as { runId?: unknown; details?: Record<string, unknown> };
|
|
74
|
+
const runId = body.runId ?? body.details?.['runId'];
|
|
70
75
|
if (typeof runId !== 'string') {
|
|
71
76
|
softSkip('blocked', 'the seam answered without a runId — the negotiation.decided event for the unauthenticated exchange is not addressable from the suite');
|
|
72
77
|
return;
|
|
@@ -39,7 +39,7 @@ import { req } from '../lib/requirement-ids.js';
|
|
|
39
39
|
const FIXTURE = 'conformance-noop';
|
|
40
40
|
const TERMINAL = new Set(['completed', 'failed', 'cancelled']);
|
|
41
41
|
|
|
42
|
-
interface Attempt { readonly key: string; readonly runId: string | null; readonly status: number; readonly at: number }
|
|
42
|
+
interface Attempt { readonly key: string; readonly runId: string | null; readonly webhookId: string; readonly status: number; readonly at: number }
|
|
43
43
|
|
|
44
44
|
/**
|
|
45
45
|
* A receiver that fails the first `failFirst` attempts for each delivery key
|
|
@@ -67,7 +67,7 @@ async function startReceiver(failFirst: number): Promise<{ server: Server; url:
|
|
|
67
67
|
const n = (seen.get(key) ?? 0) + 1;
|
|
68
68
|
seen.set(key, n);
|
|
69
69
|
const status = n <= failFirst ? 500 : 204;
|
|
70
|
-
attempts.push({ key, runId, status, at: Date.now() });
|
|
70
|
+
attempts.push({ key, runId, webhookId, status, at: Date.now() });
|
|
71
71
|
res.writeHead(status);
|
|
72
72
|
res.end();
|
|
73
73
|
});
|
|
@@ -246,7 +246,14 @@ describe('RFC 0173 §B — webhook-durable-delivery (gated on webhooks)', () =>
|
|
|
246
246
|
const runId = (create.json as { runId: string }).runId;
|
|
247
247
|
await waitTerminal(runId, 10_000);
|
|
248
248
|
|
|
249
|
-
|
|
249
|
+
// Filter by the delivery's own SUBSCRIPTION, not just its run. A host that
|
|
250
|
+
// cannot reach the suite's loopback registers every scenario against one
|
|
251
|
+
// tunnelled receiver URL (`resolveRegistrationUrl`), so a concurrently
|
|
252
|
+
// running scenario's subscription matches this run too and its attempts
|
|
253
|
+
// landed in this budget: a tier-2 host measured 6 attempts against a
|
|
254
|
+
// maxAttempts of 5 at `--max-workers 2`, with its own logs showing five.
|
|
255
|
+
// The webhook id is on every delivery (webhooks.md §Headers).
|
|
256
|
+
const ours = () => receiver.attempts.filter((a) => a.runId === runId && a.webhookId === sub.webhookId);
|
|
250
257
|
const retried = await waitFor(() => ours().some((a) => a.status === 204), retryWaitMs(doc));
|
|
251
258
|
const attempts = ours();
|
|
252
259
|
expect(
|
|
@@ -326,7 +333,14 @@ describe('RFC 0173 §B — webhook-durable-delivery (gated on webhooks)', () =>
|
|
|
326
333
|
expect(create.status, req('openwop.requirement.0173.webhook-durable-delivery.dead-letter', 'runs.md §Create', 'POST /runs MUST answer 201 for the noop fixture')).toBe(201);
|
|
327
334
|
const runId = (create.json as { runId: string }).runId;
|
|
328
335
|
await waitTerminal(runId, 10_000);
|
|
329
|
-
|
|
336
|
+
// Filter by the delivery's own SUBSCRIPTION, not just its run. A host that
|
|
337
|
+
// cannot reach the suite's loopback registers every scenario against one
|
|
338
|
+
// tunnelled receiver URL (`resolveRegistrationUrl`), so a concurrently
|
|
339
|
+
// running scenario's subscription matches this run too and its attempts
|
|
340
|
+
// landed in this budget: a tier-2 host measured 6 attempts against a
|
|
341
|
+
// maxAttempts of 5 at `--max-workers 2`, with its own logs showing five.
|
|
342
|
+
// The webhook id is on every delivery (webhooks.md §Headers).
|
|
343
|
+
const ours = () => receiver.attempts.filter((a) => a.runId === runId && a.webhookId === sub.webhookId);
|
|
330
344
|
await waitFor(() => ours().length > 1, retryWaitMs(doc));
|
|
331
345
|
const attempts = ours();
|
|
332
346
|
expect(
|