@openwop/openwop-conformance 2.46.3 → 2.46.5

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,19 @@
1
+ ## [2.46.5] — 2026-10-13 — RFC 0243 Accepted; a fan-out prediction corrected
2
+
3
+ - **No assertion changes.** This cycle opens because RFC 0243's gap G1 closes, which changes the packed `spec/v1/gaps.json`.
4
+ - **Correction to the [2.46.4] notes.** They said both fan-out rows are `inapplicable` everywhere. On a host advertising `agents.dispatch: true` that seeds and lists every fixture the package ships, `conformance-dispatch-fanout-race` is listed, so `0243.unadmitted-fan-out-refused` runs. It records `executed-fail` unless the host advertises `agents.fanOut` with the modes it runs or refuses the workflow at `POST /runs` with `400 validation_error` and `details.requiredCapability`. Leaving the fixture out of `fixtures` keeps the row `inapplicable`. The rule did not change; only the prediction was wrong.
5
+
1
6
  # `@openwop/openwop-conformance` Changelog
2
7
 
8
+ ## [2.46.4] — 2026-10-12 — RFC 0243: parallel fan-out has a v2 seat, and a link key needs both linking lanes
9
+
10
+ - **`agents.fanOut` (RFC 0243 §A).** An optional facet: presence means the host runs `fanOutPolicy: "parallel"`, with `joinModes`, `onChildFailureModes`, `maxFanOut` and `perItemInput` carrying the v1 `capabilities.dispatch.*` descriptor. The `dispatch-config` and `orchestrator-decision` descriptions now point at it.
11
+ - **New `v2-agents-fan-out`:**
12
+ - `0243.fan-out-needs-dispatch` (claims-check, wave 5 of `family-advert-witness.ts`): `agents.fanOut` only beside `agents.dispatch: true`.
13
+ - `0243.unadmitted-fan-out-refused`: on a host with `agents.dispatch: true` that does not admit a `race` join with `collect`, the new fixture `conformance-dispatch-fanout-race`, if advertised and readable, is refused at `POST /runs` with `400 validation_error` and `details.requiredCapability` naming the missing seat. A fixture `getWorkflow` cannot read is `inapplicable`: a refusal at load cannot be told apart from a fixture never seeded.
14
+ - **`v2-auth-subject-link-key` gains `0243.subject-link-key-needs-both-lanes` (RFC 0243 §B):** a host advertising `auth.subjectLinkKey` advertises both the `saml` and `scim` lanes.
15
+ - **What a host's next bundle shows:** no host advertises `agents.fanOut` or seeds the fixture, so both fan-out rows are `inapplicable` everywhere. openwop-app's live advertisement no longer carries `subjectLinkKey`, so the new auth row is `inapplicable` there; the v2 reference host advertises both lanes with the key and passes.
16
+
3
17
  ## [2.46.3] — 2026-10-12 — discovery rules the schema cannot carry
4
18
 
5
19
  Four rules from openwop-app's v2 discovery review (#4676, ADR 0858, ADR 0812). Each is stated in prose or a schema description, and none can go into the schema within 2.x: a conditional `required` or a narrowed pattern is a major change (COMPATIBILITY.md §2.4). The suite carries them, following RFC 0210 §C; the schema changes are booked for 3.0 (TODO.md).
package/README.md CHANGED
@@ -137,7 +137,7 @@ Each scenario file records its disposition and assertion count in `<report-dir>/
137
137
 
138
138
  ## What's Covered
139
139
 
140
- The current suite has 655 scenario files under `src/scenarios/`. By target major ([`scenario-majors.json`](./scenario-majors.json)): 215 files run against a v2 host and 455 against a v1 host; 15 of them run at both.
140
+ The current suite has 656 scenario files under `src/scenarios/`. By target major ([`scenario-majors.json`](./scenario-majors.json)): 216 files run against a v2 host and 455 against a v1 host; 15 of them run at both.
141
141
 
142
142
  The v2 scenarios (`src/scenarios/v2-*.test.ts`) cover, by area:
143
143
 
@@ -166,7 +166,7 @@ Three kinds of scenario share the tree:
166
166
  - **Host** checks drive a live host over HTTP.
167
167
  - **Gated** checks run only when the host advertises the family, profile or fixture they need, and otherwise record `inapplicable` or `blocked` with a reason. `--require-behavior` turns an advertised-but-unwitnessable behaviour into a failure.
168
168
 
169
- Current source tree: 655 scenario files. [`coverage.md`](./coverage.md) maps each spec document and every OpenAPI operation to its scenarios. [`CHANGELOG.md`](./CHANGELOG.md) records when each scenario landed.
169
+ Current source tree: 656 scenario files. [`coverage.md`](./coverage.md) maps each spec document and every OpenAPI operation to its scenarios. [`CHANGELOG.md`](./CHANGELOG.md) records when each scenario landed.
170
170
 
171
171
  ## How the 2.x suite is built
172
172
 
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "package": "@openwop/spec-artifacts",
3
- "version": "2.46.3",
4
- "stampSha256": "982ec02aa2b5d1d3ec6a64da49429f3361125a0a2bc02aa669c3efff64da1b01"
3
+ "version": "2.46.5",
4
+ "stampSha256": "0f3441572726c05b4ce03f9892f3b90ef77a8e37fa913c5f069fef3553cedf94"
5
5
  }
@@ -0,0 +1,37 @@
1
+ {
2
+ "id": "conformance-dispatch-fanout-race",
3
+ "name": "Conformance: Dispatch Fan-Out Race",
4
+ "version": "1.0",
5
+ "description": "A core.dispatch node pinning fanOutPolicy parallel with a race join. A host that does not admit it must refuse it (RFC 0243).",
6
+ "nodes": [
7
+ {
8
+ "id": "orchestrator",
9
+ "typeId": "core.orchestrator.supervisor",
10
+ "name": "Supervisor",
11
+ "position": { "x": 0, "y": 0 },
12
+ "config": {},
13
+ "inputs": {}
14
+ },
15
+ {
16
+ "id": "dispatch",
17
+ "typeId": "core.dispatch",
18
+ "name": "Dispatch",
19
+ "position": { "x": 200, "y": 0 },
20
+ "config": {
21
+ "workerDispatchModel": "child-run",
22
+ "fanOutPolicy": "parallel",
23
+ "joinPolicy": { "mode": "race", "onChildFailure": "collect" }
24
+ },
25
+ "inputs": {}
26
+ }
27
+ ],
28
+ "edges": [
29
+ { "id": "e1", "sourceNodeId": "orchestrator", "targetNodeId": "dispatch" }
30
+ ],
31
+ "triggers": [
32
+ { "id": "manual", "type": "manual", "enabled": true }
33
+ ],
34
+ "variables": [],
35
+ "metadata": { "tags": ["conformance", "dispatch", "fan-out"] },
36
+ "settings": { "timeout": 60000, "maxRetries": 0 }
37
+ }
package/fixtures.md CHANGED
@@ -68,6 +68,7 @@ All fixtures MUST advertise:
68
68
  | Prompt End-to-End | `conformance-prompt-end-to-end` | RFC 0027 + RFC 0029 end-to-end. Single `mock-ai` node with `config.systemPromptRef` set; host MUST emit `agent.promptResolved` + `prompt.composed` events during dispatch, then complete. Capability-gated on `capabilities.prompts.supported`. | `completed` | ≤ 10s |
69
69
  | Prompt All Four Kinds | `conformance-prompt-all-four-kinds` | RFC 0027 §A four-kind dispatch coverage with a MULTI-ENTRY `fewShotPromptRefs[]` array. Single `mock-ai` node with one ref per singular-kind slot (`systemPromptRef`, `userPromptRef`, `schemaHintPromptRef`) + two distinct templateIds in `fewShotPromptRefs[]`; host MUST emit 5 `agent.promptResolved` events (one per slot) AND 5 `prompt.composed` events. Multi-entry few-shot is the regression pin for `fewShotPromptRefs[slotIndex]` per-index resolution — a host that hard-codes `[0]` would emit the same template twice in the few-shot events and fail the per-templateId assertion. Capability-gated on `capabilities.prompts.supported`. | `completed` | ≤ 10s |
70
70
  | Dispatch Loop | `conformance-dispatch-loop` | Verifies `core.dispatch` loop mechanism | `completed` | ≤ 30s |
71
+ | Dispatch Fan-Out Race | `conformance-dispatch-fanout-race` | RFC 0243 §A.2 — a `core.dispatch` node pinning `fanOutPolicy: 'parallel'` with a `race` join. A host that does not admit it refuses it at load, or at `createRun` with `400 validation_error` and `details.requiredCapability` (`v2-agents-fan-out`) | refused; no run | ≤ 5s |
71
72
  | Interrupt — Quorum | `conformance-interrupt-quorum` | Verifies `openwop-interrupt-quorum` profile (multi-approver, majority rejection) | `completed` after 3 accepts, `failed` after quorum reject (major 2: `approval_rejected`) | unbounded (suspends) |
72
73
  | Interrupt — External Event | `conformance-interrupt-external-event` | Verifies `openwop-interrupt-external-event` profile (correlation-matched callback) | `completed` after matching POST, `failed` on timeout | ≤ 60s (timeoutMs configured) |
73
74
  | Interrupt — Auth Required | `conformance-interrupt-auth-required` | Verifies `openwop-interrupt-auth-required` profile (bearer-token resume only) | `completed` after bearer resolve | unbounded (suspends) |
@@ -395,6 +396,22 @@ The `messages`-mode stream fixture (AI token streaming) is covered by the determ
395
396
  ---
396
397
 
397
398
 
399
+ ### `conformance-dispatch-fanout-race`
400
+
401
+ - **Purpose**: observe the RFC 0243 §A.2 refusal of a parallel fan-out the host does not admit (`spec/v2/facets/agents.schema.json` §fanOut).
402
+ - **Topology**: two nodes, `core.orchestrator.supervisor` then `core.dispatch` with `{ workerDispatchModel: "child-run", fanOutPolicy: "parallel", joinPolicy: { mode: "race", onChildFailure: "collect" } }`.
403
+ - **Inputs**: none.
404
+ - **Conformance test driver**:
405
+ 1. Read discovery. Skip unless `agents.dispatch` is `true` and the host does not admit a `race` join with `collect`.
406
+ 2. Skip unless the host lists the fixture in discovery `fixtures`. `GET /workflows/conformance-dispatch-fanout-race`; a `404` is `inapplicable` (the host refused it at load).
407
+ 3. POST `/runs` with `{workflowId: "conformance-dispatch-fanout-race"}`.
408
+ 4. **Assert** `400`, `error: "validation_error"`, and `details.requiredCapability` naming the missing seat (`agents.fanOut`, `agents.fanOut.joinModes` or `agents.fanOut.onChildFailureModes`).
409
+ - **Terminal status**: none; no run is created.
410
+ - **Server prerequisites**: a host advertising `agents.dispatch: true`. A host that admits the dispatch need not seed it.
411
+
412
+ ---
413
+
414
+
398
415
  ### `conformance-budget-tool-calls`
399
416
 
400
417
  - **Purpose**: spend a run budget unaided, so `budget` can be witnessed without a model or a seam (`v2-budget-enforcement`).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openwop/openwop-conformance",
3
- "version": "2.46.3",
3
+ "version": "2.46.5",
4
4
  "description": "Black-box conformance suite for OpenWOP hosts (v2 current; v1 scenarios retained for the frozen v1 tree).",
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.46.3"
59
+ "@openwop/spec-artifacts": "2.46.5"
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": 743,
6
- "tests": 2794,
7
- "withStableId": 2794,
5
+ "files": 744,
6
+ "tests": 2797,
7
+ "withStableId": 2797,
8
8
  "interpolatedTitles": 0,
9
- "explicitIds": 2650
9
+ "explicitIds": 2653
10
10
  },
11
11
  "records": [
12
12
  {
@@ -28106,6 +28106,43 @@
28106
28106
  }
28107
28107
  ]
28108
28108
  },
28109
+ {
28110
+ "id": "openwop.it.v2-agents-fan-out.a-host-advertising-agents-fanout-advertises-agents-dispatch-true",
28111
+ "file": "v2-agents-fan-out.test.ts",
28112
+ "line": 52,
28113
+ "title": "a host advertising agents.fanOut advertises agents.dispatch: true",
28114
+ "explicitId": "openwop.requirement.0243.fan-out-needs-dispatch",
28115
+ "citations": [
28116
+ {
28117
+ "section": null,
28118
+ "requirement": null,
28119
+ "interpolated": true
28120
+ }
28121
+ ]
28122
+ },
28123
+ {
28124
+ "id": "openwop.it.v2-agents-fan-out.a-readable-workflow-pinning-an-unadmitted-fan-out-is-refused-at-createrun-with-v",
28125
+ "file": "v2-agents-fan-out.test.ts",
28126
+ "line": 61,
28127
+ "title": "a readable workflow pinning an unadmitted fan-out is refused at createRun with validation_error naming the seat",
28128
+ "explicitId": "openwop.requirement.0243.unadmitted-fan-out-refused",
28129
+ "citations": [
28130
+ {
28131
+ "section": "agents.schema.json §fanOut",
28132
+ "requirement": null,
28133
+ "interpolated": true
28134
+ },
28135
+ {
28136
+ "section": "agents.schema.json §fanOut",
28137
+ "requirement": "the refusal MUST carry validation_error"
28138
+ },
28139
+ {
28140
+ "section": "agents.schema.json §fanOut",
28141
+ "requirement": null,
28142
+ "interpolated": true
28143
+ }
28144
+ ]
28145
+ },
28109
28146
  {
28110
28147
  "id": "openwop.it.v2-ai-envelope-advertisement.the-aienvelope-record-validates-against-its-capabilities-schema-seat",
28111
28148
  "file": "v2-ai-envelope-advertisement.test.ts",
@@ -28673,7 +28710,7 @@
28673
28710
  {
28674
28711
  "id": "openwop.it.v2-auth-subject-link-key.a-host-advertising-both-the-saml-and-scim-lanes-advertises-subjectlinkkey",
28675
28712
  "file": "v2-auth-subject-link-key.test.ts",
28676
- "line": 34,
28713
+ "line": 39,
28677
28714
  "title": "a host advertising both the saml and scim lanes advertises subjectLinkKey",
28678
28715
  "explicitId": "openwop.requirement.auth.subject-link-key-advertised",
28679
28716
  "citations": [
@@ -28684,6 +28721,20 @@
28684
28721
  }
28685
28722
  ]
28686
28723
  },
28724
+ {
28725
+ "id": "openwop.it.v2-auth-subject-link-key.a-host-advertising-subjectlinkkey-advertises-both-the-saml-and-scim-lanes",
28726
+ "file": "v2-auth-subject-link-key.test.ts",
28727
+ "line": 48,
28728
+ "title": "a host advertising subjectLinkKey advertises both the saml and scim lanes",
28729
+ "explicitId": "openwop.requirement.0243.subject-link-key-needs-both-lanes",
28730
+ "citations": [
28731
+ {
28732
+ "section": null,
28733
+ "requirement": null,
28734
+ "interpolated": true
28735
+ }
28736
+ ]
28737
+ },
28687
28738
  {
28688
28739
  "id": "openwop.it.v2-authorization-advertisement.the-authorization-record-validates-against-its-capabilities-schema-seat",
28689
28740
  "file": "v2-authorization-advertisement.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": 655,
4
+ "files": 656,
5
5
  "v1": 455,
6
- "v2": 215
6
+ "v2": 216
7
7
  },
8
8
  "majors": {
9
9
  "a2a-1-0-agent-card.test.ts": [
@@ -1302,6 +1302,9 @@
1302
1302
  "v2-agent-runtime-advertisement.test.ts": [
1303
1303
  2
1304
1304
  ],
1305
+ "v2-agents-fan-out.test.ts": [
1306
+ 2
1307
+ ],
1305
1308
  "v2-ai-envelope-advertisement.test.ts": [
1306
1309
  2
1307
1310
  ],
@@ -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.46.3",
5
- "corpusTag": "v2.46.3",
4
+ "version": "2.46.5",
5
+ "corpusTag": "v2.46.5",
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": "ac6de5303872664b78ff419c035915593e01599e02835e777a0123de1baeb398",
11
11
  "api/redocly.yaml": "b0604c89b2ca6d5076ec25725c539dad44a741a811fe524439ee6daef8baa09f",
12
- "api/seams-v2.yaml": "cb175971637256b36144514f7dbda94281892a1de32afb4eb2bb2e515e8029a2",
13
- "api/v2/asyncapi.yaml": "cb8c4a0ab6b398fd32f35d5ad496e6fbf115f4af1546533440f1827cc660aaba",
14
- "api/v2/openapi.yaml": "f4fef925c3013db82449498de884da1ae38a19f802968a0adf2d7c49aa927a0b",
12
+ "api/seams-v2.yaml": "d24c33f8922c3013801ec951c81ea66f77060769b8cd9daad3b8d1f75b7c5eb7",
13
+ "api/v2/asyncapi.yaml": "13dcfe2684e3f12f47222c10b582731f0a72ce9d70758c208bd37e454041fc66",
14
+ "api/v2/openapi.yaml": "b7b485f7bab904669f25b8307b4b17fe88e971eb156d258f3cae73492ce5e6ff",
15
15
  "api/v2/redocly.yaml": "1e66b60e6118ad11a823bb620678be464d99dfe50a40e3e6f93ec9429b88b34c",
16
16
  "schemas/README.md": "276fc1d51eb08e14e9127b9fd27761d7046ee33c12000f3a64a801322db8bf58",
17
17
  "schemas/a2a-task-state.schema.json": "75d5049dea8bd873ff0e7546f1c60c8d36c219bec7084264360be54a8be30ae1",
@@ -117,7 +117,7 @@
117
117
  "schemas/v2/artifact.schema.json": "85b9dc284d39b99d2a66fa8cbf47f2dd3d8526d3bd6cc411ad315b372de38975",
118
118
  "schemas/v2/audit-verify-result.schema.json": "14af8e9283510f0a40e48a2059c929ee44e0fc9fa3cc66a95bfbe90b17c48297",
119
119
  "schemas/v2/budget-policy.schema.json": "1de68095db0beb5c7a4be629de26e79f62ebbe748a88d46f36e768edfd1f0d68",
120
- "schemas/v2/capabilities.schema.json": "6dc5d67585d6e9a18ab6a0b9b4c9b70b821517b401218fa855539636fb0b5974",
120
+ "schemas/v2/capabilities.schema.json": "ac237b887fa6e3117eddf5568bb7c2a2664f491c140968b060a173e12e79d643",
121
121
  "schemas/v2/certification-bundle.schema.json": "5cfa7fb5247160333e33a58b60d42bcd4e5269d8391d6a112d301e14828892ed",
122
122
  "schemas/v2/channel-presence-payload.schema.json": "fc03df0c20cc12a25f7ee6a47503a4c5931d79fe3d5ff24815e8fd7c1bcb9a87",
123
123
  "schemas/v2/channel-written-payload.schema.json": "ccecff3c71a3275ad8db035ac0e04db25e6f8d23cf091ddbcd5a60d1be1cbb6b",
@@ -133,7 +133,7 @@
133
133
  "schemas/v2/credential-provenance.schema.json": "f86b09e94b1c524dec7d1c6353965665ae8891e9517faef6e742d639c5578c99",
134
134
  "schemas/v2/credential-reference.schema.json": "ea699ce30f58fb2c32d4279158b3f730f70be7b2dc8c3037693a76e66eb3e001",
135
135
  "schemas/v2/debug-bundle.schema.json": "0ec1e0bca9c0a359cc6cf3448e5200a1c3d3d5b1e5efaa6605231477daaf3fee",
136
- "schemas/v2/dispatch-config.schema.json": "c2da8c7075e120c6204499a48dbf1a95a54e30ab897331425ac37660a53ad216",
136
+ "schemas/v2/dispatch-config.schema.json": "27cb203d2a5cec90d8eb398569acbd8314ad9b8e231d1acfb8db4aa76f8d8f8b",
137
137
  "schemas/v2/effect-ledger-projection.schema.json": "0d118d385a43921118a76888449c5c3c5618eab1b80914b84a0a1b48d18822c1",
138
138
  "schemas/v2/effect-seam-manifest.schema.json": "197ef9944ab027a50d1b9352f317cbbc12c8ad955fdb14650fb90477052ffef0",
139
139
  "schemas/v2/envelopes/clarification.request.schema.json": "9e39d7b09f0b3ce7902d6aa19ca0eb52867b634efc78a881469c4d44fb3333e7",
@@ -161,7 +161,7 @@
161
161
  "schemas/v2/memory-entry.schema.json": "290576ae84f4fef4cdf975932b87963283b11c50ea3d2654b32cd96aa6537c3d",
162
162
  "schemas/v2/memory-list-options.schema.json": "01ceb7370c4eacd227d2c9b7e1951ee16650abe26a32dc9d6a8e46a07a39093f",
163
163
  "schemas/v2/node-pack-manifest.schema.json": "3eb2db1d84f0b8dbde917686c72c121788a4ce6e42cf13d497a3b68564546d0e",
164
- "schemas/v2/orchestrator-decision.schema.json": "1b8146b1d050dc48e3900928c5a0b89874433b3b7fa9ee59c037557b80bd0910",
164
+ "schemas/v2/orchestrator-decision.schema.json": "503b9b28109d875ec7a1acccb45447312a6b1c7d5f4175a9f43c8338d37bd1b1",
165
165
  "schemas/v2/org-chart-responsibility-view.schema.json": "8a2565e4cdc6737a3946a70799f93642808b92f9c8d56dcdff3a0782053b89a1",
166
166
  "schemas/v2/pack-lockfile.schema.json": "2685417702c105146b780cd99154296a46578fbb10d81c8c713348ee26fea898",
167
167
  "schemas/v2/part.schema.json": "08b75eaec9a0b4a8cedca7bab732dd906b52b0749eaa444ec1ac015aa43519a5",
@@ -208,14 +208,14 @@
208
208
  "schemas/workspace-file.schema.json": "464de85c2a068243084ee9c1d969bc7cd5d8f7948574e58450d6493c38a0e1e4",
209
209
  "spec/v1/alias-detectors.json": "22444c22bb88c568339ef512553f9fc38146e22b566d11ab8608f655d688d19c",
210
210
  "spec/v1/capability-declaration-classes.json": "e7729aed5c4b4e1dd02abab0530f14cc95f5d4070fe51fb139e7f5cccefa00c6",
211
- "spec/v1/core-standard-manifest.json": "c65192dc000e6ad0f7b6eb641ddbf59a905fb09a702cb23d31b8cc807a85b81c",
211
+ "spec/v1/core-standard-manifest.json": "7a626658a36a5ab612b7ed0be33773c9b0f2e2e5f80cc7244dcb962d4a4bc62d",
212
212
  "spec/v1/deprecations.json": "e723bc11d7272c71789ad1303bb7f21809a8a024abf57594f9d5c35dc199ece9",
213
213
  "spec/v1/deprecations.schema.json": "3e393c405d2a41b467d8c5e3c468549078df1ce6a6d2588fc488097b95d9b55b",
214
214
  "spec/v1/end-of-support.json": "35f0bf3b70fe78b16e2a2fd81a153fc27e703f6c16376c6fe7cdf1f4f2c4e6d4",
215
215
  "spec/v1/event-codemap.json": "3da60d884157793a360da532a9fcbbfb5285636db325a74cec94b34622186d97",
216
216
  "spec/v1/event-codemap.schema.json": "d05933b2e88103aff51a2f774df97b9bda065e0fc88dbd9b114cc47a32189174",
217
217
  "spec/v1/extensions.json": "79a60754aa16cbdbdb604f8e5c00038af535a1690a4d07bd5ff7c38f0256a7bc",
218
- "spec/v1/gaps.json": "2415b9a2152cb526fa7296b82a433d53ce19194a52389d44f43a81539ac26a1c",
218
+ "spec/v1/gaps.json": "e99666a5b15814ee1e5a109924c992f3b15a4fdae70e3c1f0f790f2099137686",
219
219
  "spec/v1/gaps.schema.json": "8fd83259f556553c9df0f53e7a82ca8c2a4771d471197f69b4e2ae0ceeacfd66",
220
220
  "spec/v1/migrations.json": "4c50760be70cb644d02427bcaf5749606700edf4513f423903cf3b1d12f5c300",
221
221
  "spec/v1/migrations.schema.json": "886779aa6c22e646db097f5df210adb018a4dd14a7b815465a18c8a7056c8f72",
@@ -229,13 +229,13 @@
229
229
  "spec/v2/core/conversation.md": "8d6abedf6af408a74577753bfca24719d318e2632e10b45ff5d63bdcf31a8aaa",
230
230
  "spec/v2/core/errors.md": "e48838701b86bbfe0ffee77b18a8ffc4e5f04724b4318507d9771da653f8da0f",
231
231
  "spec/v2/core/events.md": "370621629c2439747f2e4ddab4780eea4345354f79e5098739d66823873f3105",
232
- "spec/v2/core/execution.md": "2cabb386aea55f70b6e4d10b2b637819589b6cf980325853024ee4b5a3f346d5",
232
+ "spec/v2/core/execution.md": "4218dbb397534940f83ca5d46561f02bf8f3fdf68d2ae7fefe7f2eb2ed771103",
233
233
  "spec/v2/core/form-content-packs.md": "02931227899676fd4581a27017be2a6c4953904717be9b81c29d12f87ab3917d",
234
234
  "spec/v2/core/headers.md": "f69fe81e4a3ad21879bb85948660c16860eb05bf152928c31aa4898f6d7d1887",
235
235
  "spec/v2/core/host-services.md": "168a40935a03867ee82461b5d83d05578be25ee3e5af4eb080587b891231e242",
236
236
  "spec/v2/core/i18n.md": "882904b7d59f992ab1e5f7346167161a54cf665b8fee47e64a3862cf9133a580",
237
237
  "spec/v2/core/idempotency.md": "adbd317a6ebb57c2759c076297b31f67def9e391576bccd0bbea787b13e85872",
238
- "spec/v2/core/identity.md": "3110138944189c378b0ca959ce1bfe5e808e6fdfc9481afeee128c899db05547",
238
+ "spec/v2/core/identity.md": "d0e3d388484507dd6260601ebf8dd1447b0b0f26ce2fe76c8a5d49e61387cffd",
239
239
  "spec/v2/core/interop.md": "6ec8259117bdb387b9323ca22c707b3b8571c00034e461c4ce79f4070c4d6b4a",
240
240
  "spec/v2/core/interrupt.md": "10f15dfe30baa0fc748aea0a067f9b271bea316fde6b598d78f77edc296f698a",
241
241
  "spec/v2/core/node-pack-runtimes.md": "e1088cb69b8c3c67256c494063328916c2980ab1aa97a7063de25bab82fdb1e5",
@@ -254,7 +254,7 @@
254
254
  "spec/v2/core/workflow-chain-packs.md": "fdce7a8477e269950a2bac05ee6eddbd66eb0b670b464d00054f7196ec05d29c",
255
255
  "spec/v2/corrections.json": "3a8d93e33865b692d26a3b6bfd3819766e206b2e0c2a69669baa4bf833d05a49",
256
256
  "spec/v2/corrections.schema.json": "46bbffb954d13bd9995aebc912ceeb2544f7064331abf25fd6ab85c81a5cec94",
257
- "spec/v2/declaration.json": "6fa19c3a68df5a951f9dabea13afbd4ee3b288a110002461f373b191b3eca99b",
257
+ "spec/v2/declaration.json": "773af65df59ce7bc2b3ab17093e23ac8083a8faaeff643302fe30198deae933b",
258
258
  "spec/v2/declaration.schema.json": "ef951633e4070899f64828f5cf1b5f4870270a0360f81dbb49b5b4d6cd407550",
259
259
  "spec/v2/eos-override.json": "4c72d2f6a658943b01c1de1643cf294642091933af7832f38ee5495df684cb2d",
260
260
  "spec/v2/errors.json": "1f0e29455edbd22e28f67f221766209c3c55ac4987beb77993b5702dd7cae131",
@@ -281,7 +281,7 @@
281
281
  "spec/v2/ext/sandbox-runtime-notes/README.md": "0d1290fb1e20b2e35c0b15051541ab6546e4e2a6f693b4f912cb3f7d6a65e92c",
282
282
  "spec/v2/ext/webResearch/README.md": "b675e6f6646128d781b661f754c2393eed86e5935d16c3a22df582a5ce025b9b",
283
283
  "spec/v2/facets/a2a.schema.json": "b68bf5e29e455e46c2ad7cfcccf5963730ec2310ff9db81884550d8df30fd6ef",
284
- "spec/v2/facets/agents.schema.json": "5e4d76d298fb1371b0e47a3b6fcf99ef65ae139d619d25d021d334c42583c4f1",
284
+ "spec/v2/facets/agents.schema.json": "3be9326f7db9c7b9b4268aec731c0c2b087eba33c9bb31669ec75a18961bc40e",
285
285
  "spec/v2/facets/aiProviders.schema.json": "4eb911c2403b8f618325194855a1326680fbda2549dfbfe86a70dd2743f8fb12",
286
286
  "spec/v2/facets/auditLogIntegrity.schema.json": "9a7f81320c8f940c6f4129bc7dd895a4063231c5458a717a08c2cbf9bb886c46",
287
287
  "spec/v2/facets/auth.schema.json": "7d98bc81a0f7d307460d039eebfebd21388091bfe1d538762583940f94c0bd2a",
@@ -313,9 +313,9 @@
313
313
  "spec/v2/path-manifest.json": "7b5679a4dad98c80d855361d88a98c8a4e045af15e310862a05b285b815dc9fc",
314
314
  "spec/v2/peer-dependency-aliases.json": "d10299280abee08258502925bc327293ee413e0108cd6e6ec75ff6110653308d",
315
315
  "spec/v2/profiles.json": "b382b261c7844798575f7c1b37c934f0da19846016a2337d883d4f5ef6e05479",
316
- "spec/v2/release.json": "86eefef2a95ae211e813e113b435bdefe8319f263ad43d005362a6ded3db4aaa",
316
+ "spec/v2/release.json": "5a677aeedc38b41d42e2e8bd4dbe56f98cefc23673fd9f718787d22c176ef0fc",
317
317
  "spec/v2/retention-floors.json": "eaf3722d95c79947af1d4269ef85117e126518c588cfcf1a2b21b97269f51624",
318
- "spec/v2/surface-baseline.json": "046cfc5a89c711a4b06244389ba2131d89d933cc9c888c78b68f36779914228c"
318
+ "spec/v2/surface-baseline.json": "4e0350c79c6415b758386f19b7d52971d4bc27dd79984b2cf11d5906ca45df86"
319
319
  },
320
- "corpusCommit": "4b2a2a43ddd40943ebf4c332592171c3587c6e15"
320
+ "corpusCommit": "89ee5f47a9c41ffa3a1e12531492125e97bc7df9"
321
321
  }
@@ -14,6 +14,8 @@
14
14
  * under the two linking lanes. These rules live in prose and schema
15
15
  * descriptions, not in the schema itself: a conditional `required` or a narrowed
16
16
  * enum is a major change (COMPATIBILITY.md §2.4), so the suite carries them.
17
+ * Wave 5 (suite 2.46.4, RFC 0243): `auth.subjectLinkKey` only beside both
18
+ * linking lanes, and `agents.fanOut` only beside `agents.dispatch: true`.
17
19
  *
18
20
  * A claims-check family is witnessed by what the host says about itself, so
19
21
  * every leg here reads the discovery record alone. Each leg checks one facet
@@ -75,7 +77,7 @@ export type AdvertFamily =
75
77
  | 'dataResidency' | 'conversationTurnModelProvenance' | 'multiPartyConversation' | 'channelPresence'
76
78
  | 'nodePackRuntimes' | 'uiPlugins' | 'purposePropagation' | 'nondeterminismPolicy'
77
79
  | 'agentRuntime' | 'promptLibrary' | 'envelopeContracts'
78
- | 'multiAgent' | 'auth';
80
+ | 'multiAgent' | 'auth' | 'agents';
79
81
 
80
82
  export function recordSchemaLeg(profile: MajorProfile, discovery: unknown, family: AdvertFamily, doc: string): AdvertOutcome {
81
83
  return onFamily(profile, discovery, family, (rec) => observed([bySchema(`/properties/${family}`, rec, doc, `the ${family} record`)]));
@@ -494,3 +496,43 @@ export const authSubjectLinkKeyLeg = (p: MajorProfile, d: unknown): AdvertOutcom
494
496
  const ok = (SUBJECT_LINK_KEY_CLASSES as readonly unknown[]).includes(k);
495
497
  return observed([{ ok, doc: 'identity.md §3', message: `a host advertising both the saml and scim lanes MUST advertise auth.subjectLinkKey (opaque-idp or configured-immutable)${ok ? '' : ` (got ${JSON.stringify(k)})`}` }]);
496
498
  });
499
+
500
+ /**
501
+ * RFC 0243 §B, identity.md §3: the link-key class names the link contract, and
502
+ * only both linking lanes imply one. A key with either lane missing promises a
503
+ * link the host cannot form.
504
+ */
505
+ export const authSubjectLinkKeyLanesLeg = (p: MajorProfile, d: unknown): AdvertOutcome => onFamily(p, d, 'auth', (rec) => {
506
+ if (rec['subjectLinkKey'] === undefined) return inapplicable('the host does not advertise auth.subjectLinkKey');
507
+ const lanes = Array.isArray(rec['lanes']) ? (rec['lanes'] as unknown[]).map((l) => (isRecord(l) ? l['lane'] : undefined)) : [];
508
+ const missing = ['saml', 'scim'].filter((l) => !lanes.includes(l));
509
+ const ok = missing.length === 0;
510
+ return observed([{ ok, doc: 'identity.md §3', message: `a host MUST NOT advertise auth.subjectLinkKey without both the saml and scim lanes${ok ? '' : ` (missing ${missing.join(', ')})`}` }]);
511
+ });
512
+
513
+ /**
514
+ * RFC 0243 §A.1: `agents.fanOut` is a mode of `core.dispatch`, so it is
515
+ * advertised only beside `agents.dispatch: true`.
516
+ */
517
+ export const agentsFanOutDispatchLeg = (p: MajorProfile, d: unknown): AdvertOutcome => onFamily(p, d, 'agents', (rec) => {
518
+ if (!isRecord(rec['fanOut'])) return inapplicable('the host does not advertise agents.fanOut');
519
+ const ok = rec['dispatch'] === true;
520
+ return observed([{ ok, doc: 'agents.schema.json §fanOut', message: `a host MUST NOT advertise agents.fanOut without agents.dispatch: true${ok ? '' : ` (got dispatch ${JSON.stringify(rec['dispatch'])})`}` }]);
521
+ });
522
+
523
+ /**
524
+ * RFC 0243 §A.2: the seats a refusal of a `parallel` dispatch pinning `mode`
525
+ * and `onChildFailure` may name in `details.requiredCapability`, given the
526
+ * host's `agents` record; `null` when the host admits that dispatch. A host
527
+ * lacking both modes may name either.
528
+ */
529
+ export function fanOutRefusalSeats(agents: Record<string, unknown>, mode: string, onChildFailure: string): string[] | null {
530
+ const fanOut = agents['fanOut'];
531
+ if (!isRecord(fanOut)) return ['agents.fanOut'];
532
+ const lacks = (k: string, v: string): boolean => !(Array.isArray(fanOut[k]) && (fanOut[k] as unknown[]).includes(v));
533
+ const seats = [
534
+ ...(lacks('joinModes', mode) ? ['agents.fanOut.joinModes'] : []),
535
+ ...(lacks('onChildFailureModes', onChildFailure) ? ['agents.fanOut.onChildFailureModes'] : []),
536
+ ];
537
+ return seats.length > 0 ? seats : null;
538
+ }
@@ -0,0 +1,87 @@
1
+ /**
2
+ * v2 — the `agents.fanOut` seat (RFC 0243; its rules are the facet's
3
+ * description in `spec/v2/facets/agents.schema.json`, `execution.md` §agents).
4
+ *
5
+ * Two legs:
6
+ *
7
+ * 1. Claims-check: a host advertising `agents.fanOut` advertises
8
+ * `agents.dispatch: true`. The leg is `agentsFanOutDispatchLeg` in
9
+ * `lib/family-advert-witness.ts` (wave 5).
10
+ * 2. The refusal: a `core.dispatch` node pinning `fanOutPolicy: 'parallel'`
11
+ * with a `race` join is refused where the host does not admit it, and
12
+ * `details.requiredCapability` names the missing seat. v2 has no
13
+ * registration endpoint, so the refusal at load is not observable from
14
+ * outside. The suite observes the fallback: on a host advertising the
15
+ * fixture `conformance-dispatch-fanout-race`, if `getWorkflow` reads it,
16
+ * `createRun` naming it answers `400 validation_error` and creates no
17
+ * run. An unadvertised fixture, or one `getWorkflow` answers 404 for
18
+ * (refused at load), is `inapplicable`.
19
+ *
20
+ * @see spec/v2/facets/agents.schema.json §fanOut
21
+ * @see RFCS/0243-dispatch-fan-out-seat.md
22
+ */
23
+
24
+ import { describe, it, expect } from 'vitest';
25
+ import { driver, type OpenWOPResponse } from '../lib/driver.js';
26
+ import { v2Discovery, familyAdvertised } from '../lib/v2.js';
27
+ import { softSkip } from '../lib/soft-skip.js';
28
+ import { req } from '../lib/requirement-ids.js';
29
+ import { readErrorCode } from '../lib/error-envelope.js';
30
+ import { majorProfile } from '../lib/major-profile.js';
31
+ import { agentsFanOutDispatchLeg, fanOutRefusalSeats } from '../lib/family-advert-witness.js';
32
+ import { isFixtureAdvertised } from '../lib/fixtures.js';
33
+
34
+ const PROFILE = majorProfile(2);
35
+ const UNREADABLE = 'v2 discovery unreachable — /.well-known/openwop did not answer 200 JSON under OpenWOP-Version: 2.0';
36
+ const DOC = 'agents.schema.json §fanOut';
37
+ const ID_DISPATCH = 'openwop.requirement.0243.fan-out-needs-dispatch';
38
+ const ID_REFUSED = 'openwop.requirement.0243.unadmitted-fan-out-refused';
39
+ const FIXTURE = 'conformance-dispatch-fanout-race';
40
+
41
+ async function discovery(): Promise<Record<string, unknown> | null> {
42
+ try { return await v2Discovery(); } catch { return null; }
43
+ }
44
+
45
+ async function http(fn: () => Promise<OpenWOPResponse>): Promise<OpenWOPResponse | null> {
46
+ try { return await fn(); } catch { return null; }
47
+ }
48
+
49
+ const isRecord = (v: unknown): v is Record<string, unknown> => v !== null && typeof v === 'object' && !Array.isArray(v);
50
+
51
+ describe('v2 agents.fanOut (RFC 0243)', () => {
52
+ it('a host advertising agents.fanOut advertises agents.dispatch: true', async () => {
53
+ const doc = await discovery();
54
+ if (!doc) return softSkip('blocked', UNREADABLE);
55
+ if (!(await familyAdvertised('agents'))) return softSkip('inapplicable', 'the host does not advertise agents');
56
+ const out = agentsFanOutDispatchLeg(PROFILE, doc);
57
+ if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
58
+ for (const x of out.findings) expect(x.ok, req(ID_DISPATCH, x.doc, x.message)).toBe(true);
59
+ });
60
+
61
+ it('a readable workflow pinning an unadmitted fan-out is refused at createRun with validation_error naming the seat', async () => {
62
+ const doc = await discovery();
63
+ if (!doc) return softSkip('blocked', UNREADABLE);
64
+ const agents = doc['agents'];
65
+ if (!isRecord(agents) || agents['dispatch'] !== true) return softSkip('inapplicable', 'the host does not advertise agents.dispatch: true, so it runs no core.dispatch node');
66
+ const seats = fanOutRefusalSeats(agents, 'race', 'collect');
67
+ if (seats === null) return softSkip('inapplicable', 'the host admits a parallel race join with collect, so the fixture is not refused');
68
+
69
+ if (!isFixtureAdvertised(FIXTURE)) return softSkip('inapplicable', `the host does not advertise fixture ${FIXTURE}`);
70
+
71
+ const wf = await http(() => driver.get(`/workflows/${FIXTURE}`));
72
+ if (wf === null) return softSkip('blocked', 'GET /workflows unreachable (fetch failed)');
73
+ if (wf.status === 404) return softSkip('inapplicable', `${FIXTURE} is not readable: refused at load, or not seeded`);
74
+ if (wf.status !== 200) return softSkip('blocked', `GET /workflows/${FIXTURE} answered ${wf.status}`);
75
+
76
+ const res = await http(() => driver.post('/runs', { workflowId: FIXTURE }));
77
+ if (res === null) return softSkip('blocked', 'POST /runs unreachable (fetch failed)');
78
+ if (res.status === 429) return softSkip('blocked', 'POST /runs answered 429 — the run budget, not the wire');
79
+ const runId = (res.json as { runId?: unknown } | undefined)?.runId;
80
+ if (res.status === 201 && typeof runId === 'string') await http(() => driver.post(`/runs/${encodeURIComponent(runId)}/cancel`, {}));
81
+
82
+ expect(res.status, req(ID_REFUSED, DOC, `createRun naming a readable workflow the host does not admit MUST return 400 (got ${res.status} ${readErrorCode(res.json) ?? ''})`.trim())).toBe(400);
83
+ expect(readErrorCode(res.json), req(ID_REFUSED, DOC, 'the refusal MUST carry validation_error')).toBe('validation_error');
84
+ const details = isRecord(res.json) && isRecord(res.json['details']) ? res.json['details'] : {};
85
+ expect(seats, req(ID_REFUSED, DOC, `details.requiredCapability MUST name the missing seat, one of ${seats.join(', ')} (got ${JSON.stringify(details['requiredCapability'])})`)).toContain(details['requiredCapability']);
86
+ });
87
+ });
@@ -9,8 +9,12 @@
9
9
  * and would also re-judge committed bundles. The schema change waits for 3.0.
10
10
  * The behavioural link legs are `v2-subject-link-record`.
11
11
  *
12
+ * The converse is RFC 0243 §B: a key without both lanes names a link contract
13
+ * the host cannot form, so the host MUST NOT advertise it.
14
+ *
12
15
  * Dispositions: discovery unreadable ⇒ `blocked`; `auth` absent, or the two
13
- * lanes not both advertised ⇒ `inapplicable`.
16
+ * lanes not both advertised (first leg) or the key absent (second leg) ⇒
17
+ * `inapplicable`.
14
18
  *
15
19
  * @see spec/v2/core/identity.md §3
16
20
  */
@@ -20,11 +24,12 @@ import { v2Discovery, familyAdvertised } from '../lib/v2.js';
20
24
  import { softSkip } from '../lib/soft-skip.js';
21
25
  import { req } from '../lib/requirement-ids.js';
22
26
  import { majorProfile } from '../lib/major-profile.js';
23
- import { authSubjectLinkKeyLeg } from '../lib/family-advert-witness.js';
27
+ import { authSubjectLinkKeyLanesLeg, authSubjectLinkKeyLeg } from '../lib/family-advert-witness.js';
24
28
 
25
29
  const PROFILE = majorProfile(2);
26
30
  const UNREADABLE = 'v2 discovery unreachable — /.well-known/openwop did not answer 200 JSON under OpenWOP-Version: 2.0';
27
31
  const ID_KEY = 'openwop.requirement.auth.subject-link-key-advertised';
32
+ const ID_LANES = 'openwop.requirement.0243.subject-link-key-needs-both-lanes';
28
33
 
29
34
  async function discovery(): Promise<Record<string, unknown> | null> {
30
35
  try { return await v2Discovery(); } catch { return null; }
@@ -39,4 +44,13 @@ describe('v2 auth.subjectLinkKey (identity.md §3)', () => {
39
44
  if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
40
45
  for (const x of out.findings) expect(x.ok, req(ID_KEY, x.doc, x.message)).toBe(true);
41
46
  });
47
+
48
+ it('a host advertising subjectLinkKey advertises both the saml and scim lanes', async () => {
49
+ const doc = await discovery();
50
+ if (!doc) return softSkip('blocked', UNREADABLE);
51
+ if (!(await familyAdvertised('auth'))) return softSkip('inapplicable', 'the host does not advertise auth');
52
+ const out = authSubjectLinkKeyLanesLeg(PROFILE, doc);
53
+ if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
54
+ for (const x of out.findings) expect(x.ok, req(ID_LANES, x.doc, x.message)).toBe(true);
55
+ });
42
56
  });