@openwop/openwop-conformance 2.46.2 → 2.46.4

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,26 @@
1
1
  # `@openwop/openwop-conformance` Changelog
2
2
 
3
+ ## [2.46.4] — 2026-10-12 — RFC 0243: parallel fan-out has a v2 seat, and a link key needs both linking lanes
4
+
5
+ - **`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.
6
+ - **New `v2-agents-fan-out`:**
7
+ - `0243.fan-out-needs-dispatch` (claims-check, wave 5 of `family-advert-witness.ts`): `agents.fanOut` only beside `agents.dispatch: true`.
8
+ - `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.
9
+ - **`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.
10
+ - **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.
11
+
12
+ ## [2.46.3] — 2026-10-12 — discovery rules the schema cannot carry
13
+
14
+ 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).
15
+
16
+ - **New `v2-multi-agent-advertisement`** (claims-check, wave 4 of `family-advert-witness.ts`):
17
+ - `multiAgent.level3-cross-host-causation`: at `executionModel.version` >= 3 the host advertises `crossHostCausation` with a non-empty `hostId`. The levels are cumulative, so the description's "`version: 3`" now reads "3 or higher".
18
+ - `multiAgent.level4-replay-determinism`: at >= 4, `replayDeterminism` with `refusalDivergenceEmission: true`.
19
+ - `multiAgent.escalation-kind-canonical`: `confidenceEscalationInterruptKind` is `clarification` or `approval`. Level 2 fires one of the two, and the v2 interrupt `kind` is closed, so an `x-host-` value promises an interrupt the payload schema rejects. The description no longer points at `spec/v1/host-extensions.md` or an RFC 0044 §C kind-mapping document.
20
+ - **New `v2-auth-subject-link-key`:** a host advertising both the `saml` and `scim` lanes advertises `auth.subjectLinkKey` (`identity.md` §3 now says MUST, as RFC 0164 §A.3 did at v1). Forbidding the key with one lane is a new rule and needs an RFC.
21
+ - **Fan-out has no v2 seat.** The `dispatch-config` and `orchestrator-decision` descriptions named `capabilities.dispatch.*` and `POST /v1/workflows`. They now say plainly that a v2 host refuses `fanOutPolicy: "parallel"` and a non-empty `nextWorkerInputs` until a seat is added. That was already the rule, because the seat could not be advertised. `dispatch-config.schema.json` is hand-owned from here. Adding one needs an RFC.
22
+ - **What a host's next bundle shows:** MyndHyve advertises `x-host-myndhyve-low-confidence` and will record `escalation-kind-canonical` `executed-fail`. Its committed bundles stay valid measurements at their suite versions (§2.3). openwop-app (level 5) and the v2 reference host pass or are `inapplicable`.
23
+
3
24
  ## [2.46.2] — 2026-10-12 — the seams profile is derived from its advert
4
25
 
5
26
  - **`openwop-conformance-seams-v2` derives from `conformance.seamsProfile`, not from any `conformance` block.** The predicate was the bare `conformance` metadata key, which also carries `certificationBundleUrl` and `mockAgent`. A host that published only a bundle URL claimed the profile, so its claim flapped with whether the URL was set (openwop-app, 7 cuts on 2.45.30 with no host change), and four openwop-app bundles (`openwop-workflow-engine-2.35.1`, `-2.43.0`, `-2.44.0`, `-2.45.28-4683d9f89`) certified it with no seam mounted. A predicate `metadata` entry may now name one member, `key.member` (capabilities.md §7); `v2ProfileIds` and `v2-profiles-derived-only` share one `metadataPresent`.
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 653 scenario files under `src/scenarios/`. By target major ([`scenario-majors.json`](./scenario-majors.json)): 213 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: 653 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.2",
4
- "stampSha256": "073549c538635c3ba6989c6872c2c7586489e9d469315026a6d9d138e6dcfab9"
3
+ "version": "2.46.4",
4
+ "stampSha256": "81bd0894e7ad0d539784a6fb3ac038bddd46d7da22870e6da68295c1df0f2eee"
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.2",
3
+ "version": "2.46.4",
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.2"
59
+ "@openwop/spec-artifacts": "2.46.4"
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": 741,
6
- "tests": 2790,
7
- "withStableId": 2790,
5
+ "files": 744,
6
+ "tests": 2797,
7
+ "withStableId": 2797,
8
8
  "interpolatedTitles": 0,
9
- "explicitIds": 2646
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",
@@ -28670,6 +28707,34 @@
28670
28707
  }
28671
28708
  ]
28672
28709
  },
28710
+ {
28711
+ "id": "openwop.it.v2-auth-subject-link-key.a-host-advertising-both-the-saml-and-scim-lanes-advertises-subjectlinkkey",
28712
+ "file": "v2-auth-subject-link-key.test.ts",
28713
+ "line": 39,
28714
+ "title": "a host advertising both the saml and scim lanes advertises subjectLinkKey",
28715
+ "explicitId": "openwop.requirement.auth.subject-link-key-advertised",
28716
+ "citations": [
28717
+ {
28718
+ "section": null,
28719
+ "requirement": null,
28720
+ "interpolated": true
28721
+ }
28722
+ ]
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
+ },
28673
28738
  {
28674
28739
  "id": "openwop.it.v2-authorization-advertisement.the-authorization-record-validates-against-its-capabilities-schema-seat",
28675
28740
  "file": "v2-authorization-advertisement.test.ts",
@@ -33493,6 +33558,48 @@
33493
33558
  }
33494
33559
  ]
33495
33560
  },
33561
+ {
33562
+ "id": "openwop.it.v2-multi-agent-advertisement.executionmodel-version-3-advertises-crosshostcausation-with-a-hostid",
33563
+ "file": "v2-multi-agent-advertisement.test.ts",
33564
+ "line": 41,
33565
+ "title": "executionModel.version >= 3 advertises crossHostCausation with a hostId",
33566
+ "explicitId": "openwop.requirement.multiAgent.level3-cross-host-causation",
33567
+ "citations": [
33568
+ {
33569
+ "section": null,
33570
+ "requirement": null,
33571
+ "interpolated": true
33572
+ }
33573
+ ]
33574
+ },
33575
+ {
33576
+ "id": "openwop.it.v2-multi-agent-advertisement.executionmodel-version-4-advertises-replaydeterminism-with-refusaldivergenceemis",
33577
+ "file": "v2-multi-agent-advertisement.test.ts",
33578
+ "line": 50,
33579
+ "title": "executionModel.version >= 4 advertises replayDeterminism with refusalDivergenceEmission",
33580
+ "explicitId": "openwop.requirement.multiAgent.level4-replay-determinism",
33581
+ "citations": [
33582
+ {
33583
+ "section": null,
33584
+ "requirement": null,
33585
+ "interpolated": true
33586
+ }
33587
+ ]
33588
+ },
33589
+ {
33590
+ "id": "openwop.it.v2-multi-agent-advertisement.confidenceescalationinterruptkind-is-clarification-or-approval",
33591
+ "file": "v2-multi-agent-advertisement.test.ts",
33592
+ "line": 59,
33593
+ "title": "confidenceEscalationInterruptKind is clarification or approval",
33594
+ "explicitId": "openwop.requirement.multiAgent.escalation-kind-canonical",
33595
+ "citations": [
33596
+ {
33597
+ "section": null,
33598
+ "requirement": null,
33599
+ "interpolated": true
33600
+ }
33601
+ ]
33602
+ },
33496
33603
  {
33497
33604
  "id": "openwop.it.v2-multi-party-conversation-advertisement.the-multipartyconversation-record-validates-against-its-capabilities-schema-seat",
33498
33605
  "file": "v2-multi-party-conversation-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": 653,
4
+ "files": 656,
5
5
  "v1": 455,
6
- "v2": 213
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
  ],
@@ -1329,6 +1332,9 @@
1329
1332
  "v2-auth-challenge.test.ts": [
1330
1333
  2
1331
1334
  ],
1335
+ "v2-auth-subject-link-key.test.ts": [
1336
+ 2
1337
+ ],
1332
1338
  "v2-authorization-advertisement.test.ts": [
1333
1339
  2
1334
1340
  ],
@@ -1569,6 +1575,9 @@
1569
1575
  "v2-mrtr-rounds-ceiling.test.ts": [
1570
1576
  2
1571
1577
  ],
1578
+ "v2-multi-agent-advertisement.test.ts": [
1579
+ 2
1580
+ ],
1572
1581
  "v2-multi-party-conversation-advertisement.test.ts": [
1573
1582
  2
1574
1583
  ],
@@ -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.2",
5
- "corpusTag": "v2.46.2",
4
+ "version": "2.46.4",
5
+ "corpusTag": "v2.46.4",
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": "26eaead22194d03d4d8027eb6b9450ab66c8c15eb1bc77adba2118aa3384d32f",
13
- "api/v2/asyncapi.yaml": "e8e435d575d9ea7312d3dc5e0191f0a9e389e2edc3775975df67b455ff293c4a",
14
- "api/v2/openapi.yaml": "f77e9a4a0f61391341efbc426fa4b4eba215f731cbab0ddde7a229b640d4e6fc",
12
+ "api/seams-v2.yaml": "657ca41ba3259b0e9a1d466e025ec4aeb56cacda1f218bd0f7800f0c3584ec6b",
13
+ "api/v2/asyncapi.yaml": "438b60c9f0d42ddee5973ee135522de8de6dea6f344b929a9c33b5f9f8c18568",
14
+ "api/v2/openapi.yaml": "feecb1ff541e9fcb32731a63d97f0b3748ded7b290ed336b7fff92f28d6fc2ae",
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": "d859a413ff463ec39acc32a9cb76f2e8c7419e45683d6b7f886861765a9c36c7",
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": "5718c1ed7d95672e618bcb1f1da8e38180e144b12151307afc3aba1314dfcef0",
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": "4d4b4c5fe73c22a1f42242b403650206ad58fd981f8fd45795894cebe3a514ae",
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": "ca8ad2e3392b1cc1c1cd6ce6072f8a000c1a8a68ced7c319944297d90f114f87",
211
+ "spec/v1/core-standard-manifest.json": "a716a260a5226f365c4467486c06e0166d677f77210d9f75e9d8576cfcabae82",
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": "0a9d1b046391d3789a53dee8a3aba8dcc50037b87a5fad8fd5e8a051176c12ad",
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": "8a0c9db1bbfb97c008ab9328b7333c1cc3318196e78bfbefb49e781bdf4f9d44",
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": "02656ffa413b5546342b8580ae928a74e621460746df74d7db50ff4df41d2310",
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": "20e2ffa6bf1e1b10a4e0404a1f27fabdf9bc502ea705118b2ae0a9a0aaa60f40",
316
+ "spec/v2/release.json": "0b8a78d84b40c96afe63c4306283872ffe76e49afd79155e805dbd25985a9d25",
317
317
  "spec/v2/retention-floors.json": "eaf3722d95c79947af1d4269ef85117e126518c588cfcf1a2b21b97269f51624",
318
- "spec/v2/surface-baseline.json": "473b133e092e3107893e23f5d7e8d42d117ad894e4d3db5042a780431302ee84"
318
+ "spec/v2/surface-baseline.json": "40b2b2ee581c614c1d5adf5a11d6a76a8188d5ce4518aca59a2b7221b35b3e61"
319
319
  },
320
- "corpusCommit": "24368a14552d28c785adeea7b72647e8f6ae664e"
320
+ "corpusCommit": "c59d856a6bf3124d5e1fe97e51ba47ad54c5d502"
321
321
  }
@@ -9,6 +9,13 @@
9
9
  * facet legs; `conversationTurnModelProvenance`, `channelPresence`,
10
10
  * `nondeterminismPolicy`, `promptLibrary` and `envelopeContracts` carry no facet
11
11
  * a host-binding v2 rule gives meaning to, so they get the record leg only.
12
+ * Wave 4 (suite 2.46.3): the `multiAgent.executionModel` sub-blocks a version
13
+ * level requires and the escalation kind it may name, and `auth.subjectLinkKey`
14
+ * under the two linking lanes. These rules live in prose and schema
15
+ * descriptions, not in the schema itself: a conditional `required` or a narrowed
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`.
12
19
  *
13
20
  * A claims-check family is witnessed by what the host says about itself, so
14
21
  * every leg here reads the discovery record alone. Each leg checks one facet
@@ -69,7 +76,8 @@ export type AdvertFamily =
69
76
  | 'scheduling' | 'artifactTypes' | 'aiEnvelope' | 'deadLetter' | 'portability'
70
77
  | 'dataResidency' | 'conversationTurnModelProvenance' | 'multiPartyConversation' | 'channelPresence'
71
78
  | 'nodePackRuntimes' | 'uiPlugins' | 'purposePropagation' | 'nondeterminismPolicy'
72
- | 'agentRuntime' | 'promptLibrary' | 'envelopeContracts';
79
+ | 'agentRuntime' | 'promptLibrary' | 'envelopeContracts'
80
+ | 'multiAgent' | 'auth' | 'agents';
73
81
 
74
82
  export function recordSchemaLeg(profile: MajorProfile, discovery: unknown, family: AdvertFamily, doc: string): AdvertOutcome {
75
83
  return onFamily(profile, discovery, family, (rec) => observed([bySchema(`/properties/${family}`, rec, doc, `the ${family} record`)]));
@@ -409,3 +417,122 @@ export const agentRuntimeImpliesManifestRuntimeLeg = (p: MajorProfile, d: unknow
409
417
  const ok = agents !== null && isRecord(agents['manifestRuntime']);
410
418
  return observed([{ ok, doc: 'host-services.md §agentRuntime', message: `a host advertising agentRuntime MUST also advertise agents.manifestRuntime${ok ? '' : agents === null ? ' (agents is absent)' : ' (agents.manifestRuntime is absent)'}` }]);
411
419
  });
420
+
421
+ // ---------------------------------------------------------------------------
422
+ // Wave 4 — multiAgent.executionModel (execution.md §multiAgent) and auth (identity.md §3)
423
+ // ---------------------------------------------------------------------------
424
+
425
+ const MA_DOC = 'execution.md §multiAgent';
426
+
427
+ /** The executionModel facet and its integer level, or a skip when either is absent. */
428
+ function executionModel(rec: Record<string, unknown>, atLeast: number): { em: Record<string, unknown> } | AdvertOutcome {
429
+ const em = rec['executionModel'];
430
+ if (!isRecord(em)) return inapplicable('the host does not advertise multiAgent.executionModel');
431
+ const v = em['version'];
432
+ if (typeof v !== 'number' || !Number.isInteger(v)) return observed([{ ok: false, doc: MA_DOC, message: 'multiAgent.executionModel.version MUST be an integer' }]);
433
+ if (v < atLeast) return inapplicable(`multiAgent.executionModel.version is ${v} (< ${atLeast})`);
434
+ return { em };
435
+ }
436
+ const isOutcome = (x: unknown): x is AdvertOutcome => isRecord(x) && typeof x['kind'] === 'string';
437
+
438
+ /**
439
+ * Level 3 (cumulative): "Hosts advertising `version: 3` MUST advertise this
440
+ * sub-block with a stable `hostId`" — the `causationHostId` rule reads that id.
441
+ */
442
+ export const multiAgentCrossHostCausationLeg = (p: MajorProfile, d: unknown): AdvertOutcome => onFamily(p, d, 'multiAgent', (rec) => {
443
+ const got = executionModel(rec, 3);
444
+ if (isOutcome(got)) return got;
445
+ const chc = got.em['crossHostCausation'];
446
+ const hostId = isRecord(chc) ? chc['hostId'] : undefined;
447
+ return observed([
448
+ { ok: isRecord(chc), doc: MA_DOC, message: 'a host at executionModel.version >= 3 MUST advertise executionModel.crossHostCausation' },
449
+ { ok: typeof hostId === 'string' && hostId.length > 0, doc: MA_DOC, message: 'executionModel.crossHostCausation MUST carry a non-empty hostId (the causationHostId value)' },
450
+ ]);
451
+ });
452
+
453
+ /**
454
+ * Level 4 (cumulative): "Hosts advertising `version: 4` MUST advertise this
455
+ * sub-block" and "MUST set [`refusalDivergenceEmission`] to `true`".
456
+ */
457
+ export const multiAgentReplayDeterminismLeg = (p: MajorProfile, d: unknown): AdvertOutcome => onFamily(p, d, 'multiAgent', (rec) => {
458
+ const got = executionModel(rec, 4);
459
+ if (isOutcome(got)) return got;
460
+ const rd = got.em['replayDeterminism'];
461
+ return observed([
462
+ { ok: isRecord(rd), doc: MA_DOC, message: 'a host at executionModel.version >= 4 MUST advertise executionModel.replayDeterminism' },
463
+ { ok: isRecord(rd) && rd['refusalDivergenceEmission'] === true, doc: MA_DOC, message: 'executionModel.replayDeterminism.refusalDivergenceEmission MUST be true at version >= 4' },
464
+ ]);
465
+ });
466
+
467
+ export const ESCALATION_KINDS = ['clarification', 'approval'] as const;
468
+
469
+ /**
470
+ * `confidenceEscalationInterruptKind`: level 2 fires "a `clarification`
471
+ * interrupt (preferred) or an `approval` interrupt", and the host MUST emit the
472
+ * kind it advertises. The schema still admits `x-host-…` (removing it narrows a
473
+ * type); the v2 interrupt `kind` is closed, so such a value promises an interrupt
474
+ * the payload schema rejects. This leg enforces the prose.
475
+ */
476
+ export const multiAgentEscalationKindLeg = (p: MajorProfile, d: unknown): AdvertOutcome => onFamily(p, d, 'multiAgent', (rec) => {
477
+ const em = rec['executionModel'];
478
+ if (!isRecord(em)) return inapplicable('the host does not advertise multiAgent.executionModel');
479
+ const k = em['confidenceEscalationInterruptKind'];
480
+ if (k === undefined) return inapplicable('the host does not advertise executionModel.confidenceEscalationInterruptKind (optional)');
481
+ const ok = (ESCALATION_KINDS as readonly unknown[]).includes(k);
482
+ return observed([{ ok, doc: MA_DOC, message: `executionModel.confidenceEscalationInterruptKind MUST be clarification or approval at v2${ok ? '' : ` (got ${JSON.stringify(k)})`}` }]);
483
+ });
484
+
485
+ export const SUBJECT_LINK_KEY_CLASSES = ['opaque-idp', 'configured-immutable'] as const;
486
+
487
+ /**
488
+ * identity.md §3: "Advertising both `saml` and `scim` lanes implies the link
489
+ * contract. The `auth.subjectLinkKey` facet … names the key class the host forms
490
+ * links under." A host that forms links names their class.
491
+ */
492
+ export const authSubjectLinkKeyLeg = (p: MajorProfile, d: unknown): AdvertOutcome => onFamily(p, d, 'auth', (rec) => {
493
+ const lanes = Array.isArray(rec['lanes']) ? (rec['lanes'] as unknown[]).map((l) => (isRecord(l) ? l['lane'] : undefined)) : [];
494
+ if (!(lanes.includes('saml') && lanes.includes('scim'))) return inapplicable('the host does not advertise both the saml and scim lanes');
495
+ const k = rec['subjectLinkKey'];
496
+ const ok = (SUBJECT_LINK_KEY_CLASSES as readonly unknown[]).includes(k);
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)})`}` }]);
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
+ });
@@ -0,0 +1,56 @@
1
+ /**
2
+ * v2 — `auth.subjectLinkKey` under the two linking lanes (`spec/v2/core/identity.md`
3
+ * §3; RFC 0163 §A, RFC 0164 §A.3). The leg lives in
4
+ * `lib/family-advert-witness.ts` (wave 4).
5
+ *
6
+ * Advertising both the `saml` and `scim` lanes implies the link contract, and
7
+ * the host MUST name the key class it forms links under. The schema cannot
8
+ * say so: a conditional `required` is a major change (COMPATIBILITY.md §2.4),
9
+ * and would also re-judge committed bundles. The schema change waits for 3.0.
10
+ * The behavioural link legs are `v2-subject-link-record`.
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
+ *
15
+ * Dispositions: discovery unreadable ⇒ `blocked`; `auth` absent, or the two
16
+ * lanes not both advertised (first leg) or the key absent (second leg) ⇒
17
+ * `inapplicable`.
18
+ *
19
+ * @see spec/v2/core/identity.md §3
20
+ */
21
+
22
+ import { describe, it, expect } from 'vitest';
23
+ import { v2Discovery, familyAdvertised } from '../lib/v2.js';
24
+ import { softSkip } from '../lib/soft-skip.js';
25
+ import { req } from '../lib/requirement-ids.js';
26
+ import { majorProfile } from '../lib/major-profile.js';
27
+ import { authSubjectLinkKeyLanesLeg, authSubjectLinkKeyLeg } from '../lib/family-advert-witness.js';
28
+
29
+ const PROFILE = majorProfile(2);
30
+ const UNREADABLE = 'v2 discovery unreachable — /.well-known/openwop did not answer 200 JSON under OpenWOP-Version: 2.0';
31
+ const ID_KEY = 'openwop.requirement.auth.subject-link-key-advertised';
32
+ const ID_LANES = 'openwop.requirement.0243.subject-link-key-needs-both-lanes';
33
+
34
+ async function discovery(): Promise<Record<string, unknown> | null> {
35
+ try { return await v2Discovery(); } catch { return null; }
36
+ }
37
+
38
+ describe('v2 auth.subjectLinkKey (identity.md §3)', () => {
39
+ it('a host advertising both the saml and scim lanes advertises subjectLinkKey', async () => {
40
+ const doc = await discovery();
41
+ if (!doc) return softSkip('blocked', UNREADABLE);
42
+ if (!(await familyAdvertised('auth'))) return softSkip('inapplicable', 'the host does not advertise auth');
43
+ const out = authSubjectLinkKeyLeg(PROFILE, doc);
44
+ if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
45
+ for (const x of out.findings) expect(x.ok, req(ID_KEY, x.doc, x.message)).toBe(true);
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
+ });
56
+ });
@@ -0,0 +1,67 @@
1
+ /**
2
+ * v2 — the `multiAgent.executionModel` advertisement (`spec/v2/core/execution.md`
3
+ * §multiAgent; RFCs 0039, 0040, 0041, 0044). The legs live in
4
+ * `lib/family-advert-witness.ts` (wave 4).
5
+ *
6
+ * crossHostCausation at `version` >= 3 the host advertises
7
+ * `crossHostCausation` with a non-empty `hostId`;
8
+ * replayDeterminism at `version` >= 4 the host advertises
9
+ * `replayDeterminism` with `refusalDivergenceEmission: true`;
10
+ * escalationKind `confidenceEscalationInterruptKind`, when present, is
11
+ * `clarification` or `approval`.
12
+ *
13
+ * The schema admits all three violations: a conditional `required` and a
14
+ * narrowed `x-host-…` pattern are major changes (COMPATIBILITY.md §2.4), so the
15
+ * rules are carried here and the schema change waits for 3.0 (TODO.md).
16
+ *
17
+ * Dispositions: discovery unreadable ⇒ `blocked`; `multiAgent`, its
18
+ * `executionModel`, or the level a leg needs absent ⇒ `inapplicable`.
19
+ *
20
+ * @see spec/v2/core/execution.md §multiAgent
21
+ */
22
+
23
+ import { describe, it, expect } from 'vitest';
24
+ import { v2Discovery, familyAdvertised } from '../lib/v2.js';
25
+ import { softSkip } from '../lib/soft-skip.js';
26
+ import { req } from '../lib/requirement-ids.js';
27
+ import { majorProfile } from '../lib/major-profile.js';
28
+ import { multiAgentCrossHostCausationLeg, multiAgentEscalationKindLeg, multiAgentReplayDeterminismLeg } from '../lib/family-advert-witness.js';
29
+
30
+ const PROFILE = majorProfile(2);
31
+ const UNREADABLE = 'v2 discovery unreachable — /.well-known/openwop did not answer 200 JSON under OpenWOP-Version: 2.0';
32
+ const ID_CROSS_HOST = 'openwop.requirement.multiAgent.level3-cross-host-causation';
33
+ const ID_REPLAY = 'openwop.requirement.multiAgent.level4-replay-determinism';
34
+ const ID_KIND = 'openwop.requirement.multiAgent.escalation-kind-canonical';
35
+
36
+ async function discovery(): Promise<Record<string, unknown> | null> {
37
+ try { return await v2Discovery(); } catch { return null; }
38
+ }
39
+
40
+ describe('v2 multiAgent advertisement (execution.md §multiAgent)', () => {
41
+ it('executionModel.version >= 3 advertises crossHostCausation with a hostId', async () => {
42
+ const doc = await discovery();
43
+ if (!doc) return softSkip('blocked', UNREADABLE);
44
+ if (!(await familyAdvertised('multiAgent'))) return softSkip('inapplicable', 'the host does not advertise multiAgent');
45
+ const out = multiAgentCrossHostCausationLeg(PROFILE, doc);
46
+ if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
47
+ for (const x of out.findings) expect(x.ok, req(ID_CROSS_HOST, x.doc, x.message)).toBe(true);
48
+ });
49
+
50
+ it('executionModel.version >= 4 advertises replayDeterminism with refusalDivergenceEmission', async () => {
51
+ const doc = await discovery();
52
+ if (!doc) return softSkip('blocked', UNREADABLE);
53
+ if (!(await familyAdvertised('multiAgent'))) return softSkip('inapplicable', 'the host does not advertise multiAgent');
54
+ const out = multiAgentReplayDeterminismLeg(PROFILE, doc);
55
+ if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
56
+ for (const x of out.findings) expect(x.ok, req(ID_REPLAY, x.doc, x.message)).toBe(true);
57
+ });
58
+
59
+ it('confidenceEscalationInterruptKind is clarification or approval', async () => {
60
+ const doc = await discovery();
61
+ if (!doc) return softSkip('blocked', UNREADABLE);
62
+ if (!(await familyAdvertised('multiAgent'))) return softSkip('inapplicable', 'the host does not advertise multiAgent');
63
+ const out = multiAgentEscalationKindLeg(PROFILE, doc);
64
+ if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
65
+ for (const x of out.findings) expect(x.ok, req(ID_KIND, x.doc, x.message)).toBe(true);
66
+ });
67
+ });