@openwop/openwop-conformance 2.35.0 → 2.36.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.
Files changed (123) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +3 -3
  3. package/coverage.md +13 -1
  4. package/dist/lib/certification-bundle-v3.js +21 -4
  5. package/dist/spec-artifacts.lock.json +2 -2
  6. package/fixtures/a2ui-v09/negative-event-name-deleteall.json +139 -0
  7. package/fixtures/a2ui-v09/negative-extra-property.json +140 -0
  8. package/fixtures/a2ui-v09/negative-foreign-catalog.json +139 -0
  9. package/fixtures/a2ui-v09/negative-formatstring-label.json +145 -0
  10. package/fixtures/a2ui-v09/negative-functioncall-openurl.json +129 -0
  11. package/fixtures/a2ui-v09/negative-image-component.json +145 -0
  12. package/fixtures/a2ui-v09/negative-textfield-obscured.json +140 -0
  13. package/fixtures/a2ui-v09/negative-theme-iconurl.json +142 -0
  14. package/fixtures/a2ui-v09/positive-approve-brief.json +139 -0
  15. package/fixtures/conformance-artifact-emit.json +28 -0
  16. package/fixtures/conformance-clarification-nested.json +53 -0
  17. package/fixtures/conformance-clarification-sensitive.json +49 -0
  18. package/fixtures/conformance-credential.json +25 -0
  19. package/fixtures/conformance-mcp-client.json +28 -0
  20. package/fixtures/interrupt-payloads/interrupt-payload-approval.json +1 -0
  21. package/fixtures/interrupt-payloads/interrupt-payload-clarification.json +1 -0
  22. package/fixtures/interrupt-payloads/interrupt-payload-conversation-close.json +1 -0
  23. package/fixtures/interrupt-payloads/interrupt-payload-conversation-exchange.json +1 -0
  24. package/fixtures/interrupt-payloads/interrupt-payload-conversation-start.json +1 -0
  25. package/fixtures/interrupt-payloads/interrupt-payload-custom.json +1 -0
  26. package/fixtures/interrupt-payloads/interrupt-payload-external-event.json +1 -0
  27. package/fixtures/interrupt-payloads/interrupt-payload-low-confidence.json +1 -0
  28. package/fixtures/interrupt-payloads/negative-approval-with-clarification-data.json +1 -0
  29. package/fixtures/interrupt-payloads/negative-conversation-close-with-conversation-start-data.json +1 -0
  30. package/fixtures/interrupt-payloads/negative-conversation-start-with-conversation-exchange-data.json +1 -0
  31. package/fixtures/interrupt-payloads/negative-custom-with-conversation-start-data.json +1 -0
  32. package/fixtures/interrupt-payloads/negative-low-confidence-with-custom-data.json +1 -0
  33. package/fixtures/node-pack-runtime/negative-entry-mismatch.json +31 -0
  34. package/fixtures/node-pack-runtime/negative-headers.json +37 -0
  35. package/fixtures/node-pack-runtime/negative-http.json +31 -0
  36. package/fixtures/node-pack-runtime/negative-icons.json +37 -0
  37. package/fixtures/node-pack-runtime/negative-meta.json +36 -0
  38. package/fixtures/node-pack-runtime/negative-non-remote-language.json +31 -0
  39. package/fixtures/node-pack-runtime/negative-packages.json +41 -0
  40. package/fixtures/node-pack-runtime/negative-sse.json +31 -0
  41. package/fixtures/node-pack-runtime/negative-templated-url.json +31 -0
  42. package/fixtures/node-pack-runtime/negative-two-remotes.json +35 -0
  43. package/fixtures/node-pack-runtime/negative-variables.json +38 -0
  44. package/fixtures/node-pack-runtime/negative-version-range.json +31 -0
  45. package/fixtures/node-pack-runtime/positive-every-optional-member.json +40 -0
  46. package/fixtures/node-pack-runtime/positive-rfc-example.json +31 -0
  47. package/fixtures/node-pack-runtime/upstream/mcp-registry-server-2025-12-11.schema.json +574 -0
  48. package/fixtures/oauth-providers/synthetic.json +36 -4
  49. package/fixtures/upstream/a2a-v1.0.1/README.md +24 -0
  50. package/fixtures/upstream/a2a-v1.0.1/a2a.proto +811 -0
  51. package/fixtures/upstream/a2ui-v0.9/README.md +33 -0
  52. package/fixtures/upstream/a2ui-v0.9/catalog.json +1383 -0
  53. package/fixtures/upstream/a2ui-v0.9/common_types.json +305 -0
  54. package/fixtures/upstream/a2ui-v0.9/server_to_client.json +132 -0
  55. package/fixtures.md +139 -0
  56. package/package.json +2 -2
  57. package/requirements.json +4480 -97
  58. package/scenario-majors.json +93 -6
  59. package/schemas/CORPUS-STAMP.json +103 -89
  60. package/src/lib/behavior-gate.ts +7 -1
  61. package/src/lib/certification-bundle-v3.ts +18 -4
  62. package/src/lib/localized-content.ts +53 -0
  63. package/src/lib/mcp-fake-server.ts +105 -4
  64. package/src/lib/node-pack-runtime.ts +41 -0
  65. package/src/lib/oauth-as-double.ts +316 -0
  66. package/src/lib/protected-resource.ts +88 -0
  67. package/src/lib/standard-webhooks.ts +120 -0
  68. package/src/lib/toolCatalog.ts +54 -4
  69. package/src/lib/trace-context.ts +98 -0
  70. package/src/lib/v2.ts +6 -0
  71. package/src/lib/webhook-receiver.ts +201 -1
  72. package/src/scenarios/artifact-type-pack-manifest-validation.test.ts +10 -1
  73. package/src/scenarios/auth-challenge-no-oracle.test.ts +122 -0
  74. package/src/scenarios/auth-oauth2-client-credentials.test.ts +46 -5
  75. package/src/scenarios/cross-host-traceparent-propagation.test.ts +92 -58
  76. package/src/scenarios/debugBundle.test.ts +41 -0
  77. package/src/scenarios/fixtures-valid.test.ts +72 -0
  78. package/src/scenarios/inbound-credential-no-passthrough.test.ts +115 -0
  79. package/src/scenarios/interrupt-approver-routing.test.ts +3 -0
  80. package/src/scenarios/localized-content-delivery.test.ts +26 -15
  81. package/src/scenarios/mcp-current-auth-boundary.test.ts +38 -0
  82. package/src/scenarios/mcp-tool-roundtrip.test.ts +29 -0
  83. package/src/scenarios/otel-mcp-semconv-projection.test.ts +85 -0
  84. package/src/scenarios/tool-catalog-compact-projection.test.ts +28 -14
  85. package/src/scenarios/tool-catalog-projection.test.ts +48 -12
  86. package/src/scenarios/tool-descriptor-shape.test.ts +34 -13
  87. package/src/scenarios/v2-a2a-agent-cards.test.ts +305 -0
  88. package/src/scenarios/v2-a2a-operation-map.test.ts +310 -0
  89. package/src/scenarios/v2-a2ui-v09-surface.test.ts +283 -0
  90. package/src/scenarios/v2-advertised-fixtures-exist.test.ts +2 -2
  91. package/src/scenarios/v2-artifact-a2a-shape.test.ts +122 -0
  92. package/src/scenarios/v2-auth-challenge.test.ts +119 -0
  93. package/src/scenarios/v2-bound-id-kinds.test.ts +1 -1
  94. package/src/scenarios/v2-callback-url-guarded.test.ts +2 -2
  95. package/src/scenarios/v2-capability-maturity-bounded.test.ts +108 -0
  96. package/src/scenarios/v2-configurable-closed.test.ts +1 -1
  97. package/src/scenarios/v2-content-locale-keys.test.ts +92 -0
  98. package/src/scenarios/v2-conversation-turn-parts.test.ts +74 -0
  99. package/src/scenarios/v2-credential-interrupt.test.ts +253 -0
  100. package/src/scenarios/v2-effect-identity-business-key.test.ts +2 -2
  101. package/src/scenarios/v2-fork-a-v1-run.test.ts +1 -1
  102. package/src/scenarios/v2-implementation-informational.test.ts +33 -0
  103. package/src/scenarios/v2-interop-trace-context.test.ts +351 -0
  104. package/src/scenarios/v2-lane-exp-only-bound.test.ts +236 -0
  105. package/src/scenarios/v2-lane-issuer-advertised.test.ts +52 -3
  106. package/src/scenarios/v2-mcp-client-results.test.ts +202 -0
  107. package/src/scenarios/v2-mcp-mount-map.test.ts +308 -0
  108. package/src/scenarios/v2-mcp-tasks.test.ts +503 -0
  109. package/src/scenarios/v2-oauth-client-pkce-state-iss.test.ts +203 -0
  110. package/src/scenarios/v2-oauth-mcp-reach-discovery.test.ts +186 -0
  111. package/src/scenarios/v2-oidc-id-token-audience.test.ts +108 -0
  112. package/src/scenarios/v2-protected-resource-metadata.test.ts +145 -0
  113. package/src/scenarios/v2-run-cancel.test.ts +1 -1
  114. package/src/scenarios/v2-run-fork-prefix.test.ts +4 -4
  115. package/src/scenarios/v2-run-pause-resume.test.ts +1 -1
  116. package/src/scenarios/v2-tool-catalog-annotations.test.ts +100 -0
  117. package/src/scenarios/v2-unmapped-type-refused.test.ts +1 -1
  118. package/src/scenarios/v2-v1-events-translated.test.ts +1 -1
  119. package/src/scenarios/v2-webhook-egress-refusal.test.ts +5 -5
  120. package/src/scenarios/v2-webhook-endpoint-verification.test.ts +124 -0
  121. package/src/scenarios/v2-webhook-message-id-stable.test.ts +124 -0
  122. package/src/scenarios/v2-webhook-secret-rotation.test.ts +132 -0
  123. package/src/scenarios/v2-webhook-standard-webhooks-delivery.test.ts +195 -0
package/fixtures.md CHANGED
@@ -50,6 +50,9 @@ All fixtures MUST advertise:
50
50
  | Approval (refine) | `conformance-approval-refine` | RFC 0183 — refine resolution carries action + refineFeedback |
51
51
  | Approval (edit-accept) | `conformance-approval-edit-accept` | RFC 0183 — edit-accept resolution carries action + editedArtifactData |
52
52
  | Clarification | `conformance-clarification` | Verifies HITL clarification interrupt + resume | `completed` after resolve | unbounded (suspends) |
53
+ | Clarification (nested schema) | `conformance-clarification-nested` | RFC 0199 §D.2(d) probe — an answer schema an MCP mount MUST NOT bridge in form mode | `completed` after resolve | unbounded (suspends) |
54
+ | Clarification (sensitive field) | `conformance-clarification-sensitive` | RFC 0199 §D.2(d) probe — `format: "password"`; form mode MUST be refused | `completed` after resolve | unbounded (suspends) |
55
+ | Credential | `conformance-credential` | RFC 0199 §C — one node declaring `auth { type: oauth2, provider: synthetic, scopes }` | `waiting-input` (credential interrupt), or the node fails without the facet | unbounded (suspends) |
53
56
  | Multi-node | `conformance-multi-node` | Verifies edge ordering + per-node events | `completed` | ≤ 10s |
54
57
  | Idempotent | `conformance-idempotent` | Verifies `Idempotency-Key` cache | `completed` | ≤ 5s |
55
58
  | Cancellable | `conformance-cancellable` | Verifies `:cancel` endpoint mid-run | `cancelled` after cancel | ≤ 60s (input-controlled) |
@@ -105,6 +108,7 @@ All fixtures MUST advertise:
105
108
  | Orchestrator Terminate | `conformance-orchestrator-terminate` | Phase 5 / CO-3 — terminate decision is final | `completed` | ≤ 30s |
106
109
  | Orchestrator Low-Confidence | `conformance-orchestrator-low-confidence` | Phase 5 / CP-1 — supervisor low-confidence suspend | `waiting-approval` (suspends) | unbounded (suspends) |
107
110
  | MCP Tool Roundtrip | `conformance-mcp-tool-roundtrip` | Track 6 — host invokes a tool on the conformance suite's synthetic MCP server; trust-boundary visibility in the event log. Node `core.conformance.mcp-invoke` is a **conformance-RESERVED** typeId (renamed 2026-08-17 from `core.ai.callPrompt` with a `config.mcp` shape that vendor pack-tier node never read — it requires `promptId` and ignores `config.mcp`, so the fixture could not execute as declared on the first host that shipped the vendor node): a host that consumes MCP MUST map it to its own MCP invoke bridge node and honour `config.mcp { tool, arguments }`; a host that does not consume MCP MUST NOT advertise this fixture (the host-mediated leg then records inapplicable) — the same treatment as `core.conformance.a2a-invoke` | `completed` | ≤ 30s |
111
+ | v2 `ctx.mcp` Client | `conformance-mcp-client` | RFC 0204 — one `core.conformance.mcp-client` node (conformance-RESERVED) calls the host's `ctx.mcp.<method>` with the run's inputs and completes with `outputs.result` = the resolved value verbatim; a rejection fails the node with its code. See §"The `ctx.mcp` fixture (RFC 0204)" | `completed`, or `failed` (`not_found`) for an unknown `serverId` | ≤ 30s |
108
112
  | A2A Task Roundtrip | `conformance-a2a-task-roundtrip` | Track 6 — host consumes the conformance suite's synthetic A2A peer; covers drift points #3 (`AUTH_REQUIRED`) and #4 (`REJECTED`). Node `core.conformance.a2a-invoke` is a **conformance-RESERVED** typeId (renamed 2026-08-16 from `core.a2a.invoke`, a multi-segment `core.*` id no host shipped and the corpus never defined): a host that consumes A2A peers MUST map it to its A2A bridge node (a host-extension node — `a2a-integration.md` §"State projection"); a host that does not consume A2A MUST NOT advertise this fixture | `failed` or `waiting-input` (per `driftScenario` input) | ≤ 30s |
109
113
  | WASM Pack Roundtrip | `conformance-wasm-pack-roundtrip` | RFC 0008 — invokes `vendor.openwop.rust-hello.greet` (loaded WASM pack); exercises required exports + at least one import | `completed` | ≤ 10s |
110
114
  | WASM Pack Memory-Cap Breach | `conformance-wasm-pack-memory-cap-breach` | RFC 0008 §K — invokes the deliberately-misbehaving `vendor.openwop.misbehaving.memory-bomb` pack (allocates 1 GiB beyond the host's `memoryPagesMax`). Host MUST emit `cap.breached` with `kind: "wasm-memory"` and drive the run to terminal `failed`. Misbehaving pack lives at `openwop-examples:examples/packs/rust-misbehaving-memory/` (repo-qualified per the 2026-06 monorepo split — the `openwop-examples` sibling repo) and is fixture-only (NOT signed for registry publication). | `failed` (with `cap.breached`) | ≤ 10s |
@@ -480,6 +484,35 @@ Hosts that don't ship a BYOK SecretResolver MAY return `404` / `422` on the star
480
484
 
481
485
  ---
482
486
 
487
+ ## `conformance-artifact-emit` (RFC 0205 — an artifact the suite can read back)
488
+
489
+ - **Purpose**: give `getArtifact` (`GET /runs/{runId}/artifacts/{artifactId}`) a target. No other fixture emits `artifact.created` with an id the suite can read (RFC 0205 register G2).
490
+ - **Opt-in**: a host advertises the fixture in `fixtures[]` only when it implements `getArtifact`; an unadvertised fixture makes `v2-artifact-a2a-shape.test.ts` record `inapplicable`.
491
+ - **Inputs**: none.
492
+ - **Behavior**:
493
+ 1. The single `conformance.artifact.emit` node produces one artifact of type `config.artifactType` whose JSON payload is `config.data`, and emits `artifact.created` with `artifactId`, `artifactType` and `nodeId`.
494
+ 2. The run reaches `completed`.
495
+ 3. `getArtifact` for that `artifactId` answers `200`. When the request's `Accept` prefers `application/a2a+json` and the host serves it, the body is an A2A `Artifact` (`schemas/v2/artifact.schema.json`) with that `artifactId`, carrying the payload as a `data` Part with `mediaType: application/json` (RFC 0205 §A.2).
496
+ - **Timing**: ≤ 10s.
497
+
498
+ ## `conformance-credential` (RFC 0199 — the `credential` interrupt)
499
+
500
+ - **Purpose**: give the `credential` interrupt, and RFC 0047 §C.3's fail-the-node default, one fixture. One `conformance.oauth.use` node declares `auth: { type: "oauth2", provider: "synthetic", scopes: ["openwop.read"] }`.
501
+ - **Opt-in**: a host advertises the fixture only when it advertises `oauth` with the `synthetic` provider; unadvertised makes `v2-credential-interrupt` record `blocked`.
502
+ - **Inputs**: none.
503
+ - **Behavior**:
504
+ 1. Run by a Subject with no credential for `synthetic`, the node either suspends on a `credential` interrupt (`oauth.credentialInterrupt` advertised — snapshot `waiting-input`, `CredentialData` carrying `provider`, `scopes`, `reason: "missing"` and an https host-owned `connectUrl`) or fails (RFC 0047 §C.3, unchanged).
505
+ 2. The grant completes through `connectUrl` and the host's own callback; the host then resolves the interrupt itself with `{ outcome: "authorized" }` and the run continues.
506
+ 3. A caller resolve of `authorized` while no credential resolves is `400 validation_error`; `declined` fails the node with `connector_auth_declined`.
507
+ 4. After the `oauth/expire-refresh` seam (and a provider that refuses the refresh), a terminal refresh failure emits `connector.auth-expired` and then either suspends with `reason: "expired"` or fails the node with `connector_auth_expired`.
508
+ - **Timing**: unbounded (it suspends).
509
+
510
+ ## `conformance-clarification-nested` / `conformance-clarification-sensitive` (RFC 0199 §D.2(d) probes)
511
+
512
+ - **Purpose**: the two schemas an MCP server mount MUST NOT bridge in form mode — one whose answer is an object (not a flat primitive), one marked `format: "password"`. `v2-mcp-mount-map` calls them as tools with and without `elicitation.url` and asserts no `mode: "form"` appears, with the flat `conformance-clarification` as the positive control.
513
+ - **Note**: the sensitive fixture exists to be REFUSED. Authoring a clarification that solicits credential material is itself forbidden (RFC 0199 §C.6); it ships only as the machine-checkable probe.
514
+ - **Timing**: unbounded (they suspend).
515
+
483
516
  ## NodeModule registration
484
517
 
485
518
  The fixtures reference these typeIds:
@@ -493,11 +526,47 @@ The fixtures reference these typeIds:
493
526
  | `core.approvalGate` | approval | Call `ctx.interrupt({kind: 'approval', ...})` |
494
527
  | `core.clarificationGate` | clarification | Call `ctx.interrupt({kind: 'clarification', ...})` |
495
528
  | `conformance.requiresMissing` | capability-missing | Declares `requires: ['conformance.never-provided']`; engine MUST refuse dispatch. Opt-in fixture registration is recommended so production deployments don't expose the fixture surface. |
529
+ | `conformance.artifact.emit` | artifact-emit | Opt-in (RFC 0205). Produce one artifact of `config.artifactType` with payload `config.data`; emit `artifact.created` naming it; readable through `getArtifact`. |
530
+ | `conformance.oauth.use` | credential | Opt-in (RFC 0199). Resolve the credential `config.auth` names for the run's Subject, refreshing host-side; no credential ⇒ a `credential` interrupt (with the facet) or a failed node. Never carries token material on any surface. |
496
531
 
497
532
  An OpenWOP-compliant server's NodeModule registry MUST include implementations for all six core typeIds before seeding fixtures. The `conformance.requiresMissing` fixture node is opt-in — see the row above.
498
533
 
499
534
  ---
500
535
 
536
+ ## Fixtures served over A2A and MCP (RFC 0208)
537
+
538
+ Two suite requirements (COMPATIBILITY §2.3), not spec MUSTs, let the major-2 interop scenarios drive a host's own A2A interface and MCP mount with the fixtures above:
539
+
540
+ - **A2A: one routed skill.** An A2A 1.0 `Message` carries no skill selector: the caller picks the agent, and the agent picks the skill. `v2-a2a-operation-map` therefore needs the JSONRPC interface the host's card lists to route exactly one skill, `conformance-approval` (skill id = workflowId, per the `a2a.card` `skills[]` row of `spec/v2/interop-map.json`). A card that lists another skill, or several, records `blocked` — the suite cannot choose one. A host routes a different workflow in production and points its interface at `conformance-approval` for a conformance cut.
541
+ - **MCP: fixtures as tools.** `v2-mcp-mount-map` calls `conformance-noop`, `conformance-failure` and `conformance-approval` as tools on the mount `mcp.serverUrls[0]` names, each under its workflowId. A fixture missing from `fixtures[]` records `blocked` for the leg that calls it.
542
+ - **MCP Tasks (RFC 0198).** `v2-mcp-tasks` also calls the approvers variant of the approval fixture (a host-defined approval gate whose `approversList` names a principal the suite's bearer is not, advertised under the id `v2-approver-enforced` names), `conformance-delay` (with `arguments.delayMs`) and `conformance-cancellable` (with `arguments.delayMs`) as tools, each under its workflowId, so the task, disconnect and SSE-cancel legs have a non-listed-approver, a short and a long run to drive. The disconnect and SSE-cancel legs also need `runList` with the `workflowId` filter, to find a run the client never received a handle for. A fixture missing from `fixtures[]` records `blocked` for the leg that calls it; without `runList` those two legs are `inapplicable`.
543
+
544
+ ---
545
+
546
+ ## The `ctx.mcp` fixture (RFC 0204)
547
+
548
+ `conformance-mcp-client` is what `v2-mcp-client-results` drives. Its node `core.conformance.mcp-client` is conformance-RESERVED: a host that advertises `mcp.client` maps it to a node that calls its own `ctx.mcp` (`spec/v2/core/host-services.md` §`mcp`) and nothing else, and a host that does not MUST NOT advertise the fixture.
549
+
550
+ - **Inputs.** `method` ∈ `callTool` | `listTools` | `readResource` | `serverHealth`; `serverId`; `name` and `arguments` (callTool); `cursor` (listTools); `uri` (readResource).
551
+ - **Output.** The node's `node.completed.outputs.result` MUST be the resolved value verbatim (the v2 RunSnapshot carries no run outputs, so the observation path is the event log). A rejection fails the node, and `node.failed.error.code` is the rejection's code (`not_found` for an unknown `serverId`).
552
+ - **Operator contract.** The host binds `serverId: "conformance"` to the suite's fake MCP server (`OPENWOP_MCP_FAKE_SERVER=true`; pin `OPENWOP_MCP_FAKE_SERVER_PORT`, or front it with `OPENWOP_MCP_FAKE_SERVER_URL`) and `"conformance.down"` to an address nothing answers. A binding that never reaches the suite's server records `blocked`.
553
+ - **What the fake server serves** (2026-07-28 only): `structured-echo` (a fixed `CallToolResult` with `structuredContent` and the caller's `nonce` in `_meta`), `always-error` (`isError: true`, fixed `content[]`), `conformance_readonly_claim_<nonce>` (annotated `readOnlyHint: true`; the nonce is per suite process), and a six-tool `tools/list` paged 3 + 3 with `nextCursor`, `ttlMs`, `cacheScope: "private"`, `outputSchema` and `annotations`.
554
+
555
+ RFC 0207's `v2-interop-trace-context` drives the same fixture twice more: its run-path carrier leg starts it with the suite's `traceparent` on `POST /runs`, and its `mcp-server-adopts-meta` leg calls it as a tool on the host's own MCP mount (a mount serves each advertised fixture under its workflowId) with `_meta.traceparent` and a different header, then reads which trace the run's outbound call to the fake server continued. A host whose mount does not list it falls back to the OTLP collector, else `inapplicable`.
556
+
557
+ For `v2-tool-catalog-annotations`' unclassified-MCP leg, a host that advertises `toolCatalog.sources ∋ "mcp"` projects the same `conformance` server into `GET /tools`.
558
+
559
+ ---
560
+
561
+ ## The per-agent card pack (RFC 0202)
562
+
563
+ `v2-a2a-agent-cards` reads the caller's `GET /agents`; it needs no workflow fixture. Two suite requirements (COMPATIBILITY §2.3), not spec MUSTs:
564
+
565
+ - **SR-1 canaries.** A host that installs the `core.conformance.agent-pack` agent pack (the pack `conformance-agent-pack-install` names in `metadata.requiresInstalledPack`) gives at least one of its agents a system-prompt body containing `OPENWOP-CONFORMANCE-CANARY-0202-PROMPT` and a handoff schema (`hasHandoffSchemas: true`) containing `OPENWOP-CONFORMANCE-CANARY-0202-HANDOFF`. The `card-sr1` leg asserts neither string reaches that agent's card; it is `inapplicable` when the caller's inventory carries no routed agent of the pack, `blocked` when the agent declares no handoff schema.
566
+ - **A tenant-B-only agent.** The `identical-refusal` leg needs an agent with an `a2aTenant` in the inventory of the tenant `OPENWOP_TEST_TENANT_B_API_KEY` binds, that the caller's inventory lacks. The suite finds it as the set difference of the two inventories, or takes `OPENWOP_TEST_TENANT_B_AGENT_ID`; without one the leg records `blocked`.
567
+
568
+ ---
569
+
501
570
  ## Versioning
502
571
 
503
572
  Each fixture's JSON has its own `version` field. The OpenWOP v1.0 conformance suite targets fixture version 1.0. Fixture spec breaking changes MUST bump the major; the suite MUST refuse to run against an unrecognized fixture version with a clear error message.
@@ -554,6 +623,30 @@ Negative manifests (credential material, mixed kinds, dual reach) are inline tes
554
623
 
555
624
  ---
556
625
 
626
+ ## Node-pack runtime fixtures
627
+
628
+ The `fixtures/node-pack-runtime/` sub-directory holds v2 node-pack manifests for RFC 0203's `runtime.mcpServer` — the inline MCP Registry record a `language: "remote"` runtime may carry. They are read only by the corpus-coherence test `src/coherence/node-pack-mcp-server-record.test.ts` (spec-repo CI; never a host bundle) and validated server-free against `../schemas/v2/node-pack-manifest.schema.json`. None is seeded into a server. Every fixture is the RFC's positive example with one change, so each negative fails for its reason and nothing else (the test asserts every error sits under `/runtime`).
629
+
630
+ | Fixture | Contract |
631
+ | --- | --- |
632
+ | `positive-rfc-example` | The RFC 0203 positive example, verbatim runtime. MUST validate; its `mcpServer` MUST validate against the vendored upstream schema. |
633
+ | `positive-every-optional-member` | Adds every optional member (`$schema`, `title`, `websiteUrl`, `repository` with `id` + `subfolder`). Same contract. |
634
+ | `negative-sse` | `remotes[0].type: "sse"`. MUST be refused (Streamable HTTP only). |
635
+ | `negative-packages` | A `packages[]` install instruction. MUST be refused. |
636
+ | `negative-headers` | `remotes[0].headers` carrying an `Authorization` value. MUST be refused. |
637
+ | `negative-variables` | `remotes[0].variables` (an `isSecret` input). MUST be refused. |
638
+ | `negative-http` | An `http://` remote URL. MUST be refused. |
639
+ | `negative-templated-url` | `https://{tenant}.acme.example/mcp`. MUST be refused. |
640
+ | `negative-two-remotes` | Two `remotes[]` entries. MUST be refused (exactly one). |
641
+ | `negative-version-range` | `version: "^1.4.0"`. MUST be refused (exact SemVer). |
642
+ | `negative-meta` | `_meta`. MUST be refused. |
643
+ | `negative-icons` | `icons`. MUST be refused. |
644
+ | `negative-non-remote-language` | `mcpServer` under `language: "wasm"`. MUST be refused by the schema's `if`/`else`; the same manifest without `mcpServer` validates. |
645
+ | `negative-entry-mismatch` | `entry` ≠ `remotes[0].url`. Schema-valid (JSON Schema cannot compare two values); MUST be refused by the manifest validator (`src/lib/node-pack-runtime.ts`) with `pack_validation_failed`. |
646
+ | `upstream/mcp-registry-server-2025-12-11.schema.json` | Byte-for-byte copy of `https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json`, sha256 `3fba09590c99f61735d234822279f4223fab9e300c0a81e81c91ab62a4114de0` (fetched 2026-09-22). The test fails if the bytes drift. Re-vendor only with an RFC that re-pins the subset (RFC 0203 G1). |
647
+
648
+ ---
649
+
557
650
  ## Trigger-event fixtures
558
651
 
559
652
  The `fixtures/trigger-events/` sub-directory holds canonical external-event ingestion documents (RFC 0099) used as schema-level proof points — validated server-free against `../schemas/trigger-event.schema.json` (`trigger-event-*`) and `../schemas/trigger-subscription-registration.schema.json` (`trigger-subscription-registration-*`) by the `fixtures-valid.test.ts` sweep + the `trigger-ingestion.test.ts` scenario. They are NOT seeded into a server.
@@ -569,8 +662,34 @@ Negative cases (cross-source sub-objects, raw attachment URLs, `Authorization` h
569
662
 
570
663
  ---
571
664
 
665
+ ## Interrupt-payload fixtures
666
+
667
+ The `fixtures/interrupt-payloads/` sub-directory (suite 2.36.0) holds canonical `InterruptPayload` documents — the `ctx.interrupt(payload)` wire shape — validated server-free against BOTH `../schemas/suspend-request.schema.json` and `../schemas/v2/suspend-request.schema.json` by the `fixtures-valid.test.ts` sweep. They are NOT seeded into a server. They pin the per-kind binding of `data` to `kind` (`spec/v1/interrupt.md` §Per-kind payloads; `spec/v2/core/interrupt.md`): before 2.36.0 the `data` union was an unbound `oneOf`, so the minimal `conversation.start` and `conversation.close` payloads (both `{ conversationId }`) matched two branches and failed, while a payload whose `data` belonged to another kind passed (COMPATIBILITY.md §3, 2026-09-22).
668
+
669
+ Unlike the other sub-directories, this one also carries **deliberately-invalid** files, prefixed `negative-`. The sweep asserts each is refused, and that its `data` validates under the kind the file name says it belongs to (`negative-<kind>-with-<data-kind>-data`), so a negative can only fail for the binding and never for being malformed on its own. The top-level `fixtures-valid.test.ts` workflow sweep reads the top level only, so these files never reach it.
670
+
671
+ | Fixture | Expect | Purpose |
672
+ | --------------------------------------------------------- | ------- | ------- |
673
+ | `interrupt-payload-approval` | valid | Minimal `kind:"approval"` — the four required `ApprovalData` fields. |
674
+ | `interrupt-payload-clarification` | valid | Minimal `kind:"clarification"` — one question. |
675
+ | `interrupt-payload-external-event` | valid | Minimal `kind:"external-event"` — `eventType` + `correlation`. |
676
+ | `interrupt-payload-custom` | valid | Minimal `kind:"custom"` — `customKind` only (`payload` is optional in the schema). |
677
+ | `interrupt-payload-conversation-start` | valid | Minimal `kind:"conversation.start"` — `{ conversationId }` only. **Failed before 2.36.0** (matched `ConversationStartData` and `ConversationCloseData`). |
678
+ | `interrupt-payload-conversation-exchange` | valid | Minimal `kind:"conversation.exchange"` — `conversationId` + `prompt`. |
679
+ | `interrupt-payload-conversation-close` | valid | Minimal `kind:"conversation.close"` — `{ conversationId }` only. **Failed before 2.36.0**, same double match. |
680
+ | `interrupt-payload-low-confidence` | valid | Minimal `kind:"low-confidence"` — `agentId` + `threshold` + `observed`. |
681
+ | `negative-approval-with-clarification-data` | refused | `kind:"approval"` carrying `ClarificationData`. Passed before 2.36.0. |
682
+ | `negative-conversation-start-with-conversation-exchange-data` | refused | `kind:"conversation.start"` carrying `ConversationExchangeData`. Passed before 2.36.0. |
683
+ | `negative-conversation-close-with-conversation-start-data` | refused | `kind:"conversation.close"` carrying a `ConversationStartData`-only field (`title`). Passed before 2.36.0. |
684
+ | `negative-custom-with-conversation-start-data` | refused | `kind:"custom"` carrying `{ conversationId }`. Refused before 2.36.0 too, but for the double match rather than the kind. |
685
+ | `negative-low-confidence-with-custom-data` | refused | `kind:"low-confidence"` carrying `CustomData`. Passed before 2.36.0. |
686
+
687
+ ---
688
+
572
689
  ## OAuth provider fixtures
573
690
 
691
+ RFC 0199 adds `provider.issuer`, `provider.pkce` and the two issuer-less providers (`issuerlessProviders[]`) the distinct-redirect-URI leg needs; the endpoints a host actually talks to are supplied at run time by the `oauth/authorize-start` seam, which points them at the suite's authorization-server double (`src/lib/oauth-as-double.ts`).
692
+
574
693
  The `fixtures/oauth-providers/` sub-directory holds synthetic OAuth provider definitions used to prove the RFC 0047 `host.oauth` authorization-code roundtrip end-to-end **without a live IdP**. They are NOT `WorkflowDefinition`s and are NOT seeded as workflows — they parameterize the behavioral roundtrip scenario, which drives the host's `POST /v1/host/sample/oauth/authorize-code-roundtrip` seam against the provider's `authUrl`/`tokenUrl` (served by a conformance test double).
575
694
 
576
695
  | Fixture | `provider.id` | Purpose |
@@ -603,6 +722,26 @@ Prompt-template fixtures are exercised by the server-free `fixtures-valid.test.t
603
722
 
604
723
  ---
605
724
 
725
+ ## A2UI v0.9 surface fixtures (RFC 0209)
726
+
727
+ The `fixtures/a2ui-v09/` sub-directory (suite 2.36.0) holds `ui.a2ui-surface` payloads at per-kind **schema version 2** — ordered runs of A2UI v0.9 server-to-client messages in the OpenWOP profile of the basic catalog (`schemas/v2/envelopes/ui.a2ui-surface.schema.json` `$defs/payloadV2`; `spec/v2/ext/a2uiSurface/README.md`). They are NOT `WorkflowDefinition`s and are NOT seeded into a server. The corpus gate (`src/coherence/a2ui-v09-profile.test.ts`) validates the positive against the profile and each of its messages against the vendored upstream schemas; each negative must fail the profile AND pass a copy of the profile with only the restriction under test removed, so it can fail for no other reason. `v2-a2ui-v09-surface.test.ts` emits the positive through the v2 emit-surface seam. Like `interrupt-payloads/`, this directory carries deliberately-invalid `negative-` files; the top-level workflow sweep never reads sub-directories.
728
+
729
+ | Fixture | Expect | Upstream A2UI | Purpose |
730
+ | --- | --- | --- | --- |
731
+ | `positive-approve-brief` | valid | valid | `createSurface` + `updateComponents` (`Column` root, `Text`, `TextField` with a `required` check, `DateTimeInput`, `ChoicePicker`, `CheckBox`, `Button` → `resume` with a bound `context`) + `updateDataModel`. |
732
+ | `negative-functioncall-openurl` | refused | accepted | `Button.action` is the `functionCall` arm calling `openUrl` (`a2ui-action-confinement`). |
733
+ | `negative-event-name-deleteall` | refused | accepted | `event.name: "deleteAll"`, outside `resume` / `exchange`. |
734
+ | `negative-textfield-obscured` | refused | accepted | `TextField.variant: "obscured"` (`a2ui-surface-no-secret-input`). |
735
+ | `negative-image-component` | refused | accepted | an `Image` component (URL fetch; `a2ui-surface-no-network-egress`). |
736
+ | `negative-theme-iconurl` | refused | accepted | `createSurface.theme.iconUrl`. |
737
+ | `negative-formatstring-label` | refused | accepted | a `Text.text` that is a `formatString` FunctionCall (`a2ui-surface-no-code-exec`). |
738
+ | `negative-extra-property` | refused | refused | an `onClick` property on a component (closed objects). |
739
+ | `negative-foreign-catalog` | refused | n/a | a `catalogId` outside the pinned basic catalog. |
740
+
741
+ The `fixtures/upstream/a2ui-v0.9/` sub-directory vendors `server_to_client.json`, `catalogs/basic/catalog.json` and `common_types.json` from a2ui.org byte for byte, pinned by SHA-256 (RFC 0209 §References; its `README.md` records the URLs, fetch date and the Apache-2.0 notice). They are read only by the corpus gate.
742
+
743
+ ---
744
+
606
745
  ## References
607
746
 
608
747
  - `README.md` — conformance suite operator docs
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openwop/openwop-conformance",
3
- "version": "2.35.0",
3
+ "version": "2.36.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.35.0"
59
+ "@openwop/spec-artifacts": "2.36.0"
60
60
  }
61
61
  }