@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/CHANGELOG.md CHANGED
@@ -1,5 +1,32 @@
1
1
  # `@openwop/openwop-conformance` Changelog
2
2
 
3
+ ## [2.36.0] — 2026-09-23 — the MCP/A2A alignment program, measured: thirteen RFCs, thirty-seven new scenario and coherence files
4
+
5
+ - **RFC 0210 `Active` — the `exp-only` lane's window is a bound the host enforces, and the two corpus rows that keep the member honest.** New `src/scenarios/v2-lane-exp-only-bound.test.ts` (major 2), gated on an advertised `auth.lanes[]` member with `revocation: "exp-only"` (`inapplicable`, naming what IS advertised, otherwise) **and** `OPENWOP_TEST_OIDC_ISSUER_URL` naming a synthetic issuer the operator configured that lane to trust (`blocked` otherwise — without a token the host would ever accept, "it refused a string" witnesses nothing; a window under 120 s is also `blocked`, because the control token needs room inside it). Three legs, one issuer, one key, one token shape: a control credential at `exp − iat = window − 60` that MUST NOT be refused, a credential at `exp − iat = window + 60` (still unexpired) and one with no `iat` at all that MUST be refused `401 credential_lifetime_exceeded`, and a clock-skewed credential whose total lifetime is inside the window while its remaining lifetime is outside it — the only shape that separates the two bounds, and the one a host enforcing `exp − iat` alone serves. The control is what makes the refusals evidence; a host that refuses every token fails it. New `src/coherence/v2-lane-exp-only-schema.test.ts` (corpus gate, never reaches a bundle) mints `openwop.requirement.0210.exp-only-lane-restricted` — `exp-only` validates on `oauth2` and `oidc`, is refused on `api-key` and `session`, and **each negative is re-validated against a facet copy with only the §C.8 `if`/`then` removed, where it MUST pass**, so it cannot be failing for an unrelated defect in the fixture — and `openwop.requirement.0210.lane-rule-surfaces-agree`, which drives `scripts/check-lane-revocation-rules.mjs` red once per surface in a throwaway corpus copy (an enum member with no §2.2 row; a `LANE_RULES` entry the table does not license) rather than asserting exit 0 on a clean tree. `v2-lane-issuer-advertised.test.ts` gains `exp-only` in its `REVOCATION`, `WINDOWED` and `LANE_RULES` maps, in lockstep with §2.2's table and the facet enum.
6
+ - **RFC 0197 — one major-2 scenario and four coherence tests for “v2 surfaces are retired, never reshaped”.** New `src/scenarios/v2-capability-maturity-bounded.test.ts` (major 2, ungated — every v2 host advertises family records; a host advertising none records `blocked`, never a zero-assertion pass) mints `openwop.requirement.0197.maturity-not-overstated`: a family record MUST NOT advertise `status: "stable"` when the `spec/v2/declaration.json` the suite ships calls the family `experimental` or `deprecated`. One-sided on purpose — understating is always permitted — and the declaration is read through `SPEC_V2_DIR`, which resolves to the `@openwop/spec-artifacts` peer in the published layout, so an npm consumer measures against the same bound a contributor does. It can fail today: driven against `evidence/v2-host-bundles/myndhyve.json`'s discovery document, 38 families fail. Four corpus-gate tests mint the claims-check rows and drive each new gate red one input at a time: `src/coherence/v2-surface-monotone-gate.test.ts` (`0197.no-reshape`) — a removed enum member, a removed operation, a `required` entry added to a pre-existing object, an open object closed, and a narrowed `type`, each refused, against two positive controls: an OPTIONAL property added to a closed object passes (§B.5), and an RFC 0209-shaped `anyOf` branch re-cut of the root reports ZERO removals (§A.1 — a `$defs` name is not a surface); `src/coherence/v2-retirement-gate.test.ts` (`0197.retirement-predicate`, `0197.correction-unevidenced`) — nine sabotages against one base fixture the gate calls `retirable`, each of which must be held by ITS OWN predicate and no other, plus §A.4's refusal of a correction whose prior shape a bundle carried or whose census is null; `src/coherence/removal-dates-minor.test.ts` (`0197.retirement-predicate`, `0197.reader-accepts-retired`) — the negative control runs the SAME `removeIn: "2.99"` row twice, once under the pre-0197 `v2.0-cut` trigger (due at once, red) and once under `v2-minor` (not due, green), plus the persisted-class legs where losing the member or its `x-openwop-retired-in` annotation fails; `src/coherence/bundle-maturity-gate.test.ts` (`0197.bundle-maturity-bound` — deliberately NOT the host id, because a corpus ledger row under `maturity-not-overstated` would discharge §C's host-evidence bar without any host having done anything) — the committed bundle re-labelled onto suite 2.36.0 fails with 38, promoting one family in a fixture declaration drops it to 37, and the same content at suite 2.35.1 is reported, not failed (decisions log D1). Fixtures are built in tmpdirs rather than shipped: they only ever feed a spawned gate and would otherwise reach npm consumers who cannot use them (`v2-eos-clock`'s pattern). No scenario added to any profile floor; no pin bump (D3).
7
+ - **A lane may advertise only a revocation rule `identity.md` §2.2's row for that lane lists — a tightening, and it fails one committed host's next cut.** `v2-lane-issuer-advertised` gains a third leg minting `openwop.requirement.0170.lane-issuer-advertised.lane-rule`. §2.2 has stated the per-lane rule since RFC 0170 §B.3 and nothing measured it: the facet schema relates `revocation` to `lane` nowhere, so all nine members were valid on all ten lanes, and the two existing legs check only enum membership and window presence. That is how `short-lived` — a rule §2.2 states only for `mtls`, and there as an obligation on the party that **issues** the credential — came to be advertised on a production `oidc` lane whose host performs no revocation re-check at all. **Effect on today's committed bundles: the reference host (`api-key`, `session`, `saml`, `scim`, `workload`) and openwop-app (`api-key`, `session`, `anonymous`) pass unchanged; MyndHyve's next cut fails this row** until its advertisement is corrected (`INTEROP-MATRIX.md` §"Advertised revocation rule per lane"; RFC 0210 §F, register G5). Its committed 2.35.x bundle stays a valid measurement at its own suite version (`COMPATIBILITY.md` §2.3). The `anonymous` lane is exempt and the leg's `inapplicable` message says why: its §2.2 revocation cell is `—` while the facet schema REQUIRES the field, so the table names no legal value for it (RFC 0210 register G1). The map the leg enforces is not a second declaration — the new corpus gate `scripts/check-lane-revocation-rules.mjs` parses §2.2's table and fails when the table, the facet enum and this map disagree. Sabotage: pointing the leg at a host advertising `short-lived` on `oidc` turns the row `executed-fail`, naming the lane and the rule. No version bump (rides the open 2.36.0).
8
+ - **RFC 0200 — four new scenarios, one coherence test, and two re-cited legs for the host as an OAuth protected resource.** `v2-protected-resource-metadata` (gated on an `auth.lanes[]` member with `lane ∈ {oauth2, oidc}`; a `404` at the derived well-known URI is an `executed-fail`, never a soft-skip, because the lane is the advertisement and the document is owed): `0200.prm-served` — RFC 9728 metadata answered `200`, unauthenticated, at the RFC 9728 §3/§3.1 path-inserted URI; `0200.prm-consistent` — `resource` identical to the resource identifier, `authorization_servers` set-equal to the URL-form issuers of those lanes, `scopes_supported` (when present) a non-empty string array, and neither sender-constraint flag claimed unless every such lane requires that binding; `0200.mcp-mount-prm` — §A.4, further gated on a mount that really requires an OAuth lane (a host whose api-key lane also opens the mount records `inapplicable` with that reason rather than passing a leg whose premise does not hold). `v2-auth-challenge`: `0200.challenge-401` — `Bearer` + `resource_metadata` on a no-credential refusal with NO error code, `error="invalid_token"` once a credential was presented (RFC 6750 §3.1); `0200.challenge-403-scope` (harness-gated on `OPENWOP_TEST_LOW_SCOPE_KEY`) — `error="insufficient_scope"` with `scope` naming the operation's scope. `auth-challenge-no-oracle` (BOTH_MAJORS) — `0200.no-challenge-on-nondisclosure-404`: the non-disclosure `404` of the tool catalog and the RFC 0074 inventory stays a silent `404`, and a resource-binding `403` carries no `insufficient_scope`; every leg carries a positive control that the route is live for the same credential. `inbound-credential-no-passthrough` (BOTH_MAJORS) — `0200.inbound-credential-no-passthrough`: the suite's own API key plus a cookie and a proxy-auth canary go out on the run-creating request and must appear in no header or raw body captured at the suite-owned webhook receiver; an undelivered hop is `blocked`, never a pass. `v2-oidc-id-token-audience` — `0200.id-token-aud` (gated on an `oidc` lane and `OPENWOP_TEST_OIDC_ISSUER_URL`): a same-audience ID token is admissible (the gate, because from outside "does not trust this issuer" and "refuses ID tokens" are one observation) and one minted for another relying party at the same IdP is `401 audience_mismatch`. `src/coherence/openapi-security-declared.test.ts` mints the `(corpus)` row `0200.openapi-security-declared` by running `scripts/check-openapi-security.mjs`. The major-1 wrong-audience leg of `auth-oauth2-client-credentials` is split into its own `it` and re-cited to `0200.oauth2cc-wrong-aud-401` (non-gating: no v2 cut reaches a major-1 file), and `mcp-current-auth-boundary` gains a non-gating §A.4 leg. Every row was run red against a sabotaged v2-reference build before it was cited.
9
+ - **RFC 0199 — three major-2 scenarios and the OAuth legs on two existing ones.** `v2-oauth-client-pkce-state-iss` (gated on `oauth` + `authorization_code` + the seams profile + the advertised synthetic providers) mints `openwop.requirement.0199.pkce-s256`, `.state-single-use`, `.same-user-callback`, `.iss-validated` and `.redirect-uri-fixed`; `v2-credential-interrupt` (also gated on the `conformance-credential` fixture) mints `.credential-interrupt`, `.credential-resume-rechecked`, `.credential-declined` and — on a host advertising `oauth` WITHOUT `credentialInterrupt` — the RFC 0047 §C.3 regression row `.refresh-failure-fails-node`; `v2-oauth-mcp-reach-discovery` mints `.resource-indicator`, `.mcp-pkce-verified` and `.mcp-metadata-bound`. `v2-a2a-operation-map` gains `.a2a-auth-required` and `v2-mcp-mount-map` gains `.mcp-url-mode` and `.form-mode-no-secret`. New `src/lib/oauth-as-double.ts`: a suite-owned authorization-server double (RFC 8414 metadata with switchable `authorization_response_iss_parameter_supported` and `code_challenge_methods_supported`, a PKCE-checking token endpoint that COUNTS every request) and a protected-resource double (RFC 9728), both advertised through `resolvePublicFront` (`OPENWOP_OAUTH_AS_URL`, `_AS2_URL`, `_RESOURCE_URL`). Every "the host refused" row asserts zero token requests at a server the SUITE owns, never the host's own ledger, and carries a positive control on the same path. Every leg needs a fresh Subject from the credential mint seam (a Subject holding a credential would make the missing-credential legs vacuous) — `blocked` without it; the discovery legs are `blocked` without the public fronts, because a connection pack's endpoints are https by schema. New fixtures `conformance-credential`, `conformance-clarification-nested`, `conformance-clarification-sensitive`, catalogued in `fixtures.md`. All 22 sabotage cases were run against a deliberately broken v2-reference build and each turns its row red. `interrupt-approver-routing` registers `credential-reference.schema.json` before compiling the v1 `suspend-request` standalone (`CredentialData.credentialRef` `$ref`s it). No version bump (rides the open 2.36.0).
10
+ - **RFC 0207 — trace context across MCP and A2A.** New major-2 `v2-interop-trace-context` (six ids): `0207.mcp-traceparent-carried` — the host's MCP client reached through the production run path (`POST /runs` of `conformance-mcp-client` with the suite's `traceparent`, gated on `mcp.client`) and through the §23 invoke seam; `0207.a2a-traceparent-carried` — the §22 invoke seam into a suite-owned A2A peer; both read the carrier the peer RECORDED and apply the D4 rule (`lib/trace-context.ts` `classifyCarriers`): neither `params._meta.traceparent` / `Message.metadata.openwop.traceparent` nor the HTTP header ⇒ fail, a trace id that is not the suite's ⇒ fail, header-only ⇒ pass (logged `header-only`). `0207.mcp-server-adopts-meta` — `tools/call` on the host's mount with `_meta` trace T and header trace H; observed without OTLP through the outbound call of the run the mount starts (`conformance-mcp-client` as a tool), else through the OTLP collector, else `inapplicable`. `0207.mcp-malformed-ignored` / `.a2a-malformed-ignored` — a garbage value is a normal result, never an error. `0207.a2a-inbound-adopts` — OTLP only. New `otel-mcp-semconv-projection` in `BOTH_MAJORS` (`0207.mcp-semconv-stamped`; `inapplicable` when no `mcp.*` span arrives — the projection is a MAY). `cross-host-traceparent-propagation` (major 1, non-gating) rewritten from two `it.skip` placeholders into runnable legs against the fake MCP server / A2A peer. `debugBundle` gains a v1-only, non-gating span `traceId` / `kind` / `status` leg (`spans: []` ⇒ `inapplicable`). Sabotaged once each against the v2 reference host (neither carrier, fresh trace, prefixed `_meta` key, header adopted over `_meta`, malformed refused — each turns its row red; header-only passes).
11
+ - **RFC 0202 — `src/scenarios/v2-a2a-agent-cards.test.ts` (major 2), eight rows for per-agent A2A cards.** Gated on `a2a.agentCards` and a caller inventory with ≥1 `a2aTenant` entry (`inapplicable` otherwise). Rows `openwop.requirement.0202.advertisement` (profile `a2a-1.0`, `agents.manifestRuntime`, public card `extendedAgentCard: true`), `.routing-value` (schema-valid, stable across reads, unique, and containing no tenant/workspace/subject id the host disclosed on a run snapshot — an undisclosed identifier is not caught, G4), `.card-projection` (`name`/`version`/`description` from the entry; the host card's interface set, each with `tenant: R`; equal `capabilities`/`securitySchemes`/`securityRequirements`; ≥1 skill with the A2A REQUIRED fields, within `roster[].workflows` when the entry names them), `.card-sr1` (no `core.conformance.agent-pack` canary on the card; canaries defined in `fixtures.md` §"The per-agent card pack"), `.auth-before-resolve` (unauthenticated real vs never-minted `R`: identical refusal), `.identical-refusal` (`GetExtendedAgentCard` / `SendMessage` / `ListTasks` with tenant B's `R` vs a never-minted value: identical status and body apart from `id`, with a positive control that B's `R` resolves for B), `.public-card-no-r` (header-less and `A2A-Version: 1.0` public cards carry no `R`), `.tenant-of-record` (a task started through `R` is a run the caller reads and tenant B cannot). The two cross-tenant rows need `OPENWOP_TEST_TENANT_B_API_KEY` (optionally `OPENWOP_TEST_TENANT_B_AGENT_ID`); missing ⇒ `blocked`. Rides the open 2.36.0; no pin moves.
12
+ - **RFC 0204 — `ctx.mcp` returns MCP results; the catalog's annotation projection.** New major-2 `v2-mcp-client-results` (gated on `mcp.client` + the new fixture `conformance-mcp-client`, node `core.conformance.mcp-client`; `blocked` when the host claims `mcp.client` without the fixture or when its `conformance` binding never reaches the suite's server): `0204.call-tool-verbatim`, `.call-tool-iserror-resolves`, `.list-tools-page`, `.server-health`, `.reject-unknown-server` — each compares the node's `node.completed.outputs.result` with the JSON-RPC result the suite's own server recorded sending (`McpFakeServer.exchanges()`, new). New major-2 `v2-tool-catalog-annotations` (gated on `toolCatalog`, `inapplicable` — not `gateFamily` — when absent, so no strict v2 cut without the family needs a new opt-out): `0204.annotations-derived` (`inapplicable` when no descriptor carries `annotations`) and `.mcp-unclassified-write` (any projected `conformance_readonly_claim_*` MUST be `write`; `inapplicable` without the suite server or an `mcp` source). New coherence test `tool-descriptor-annotations` (`0204.annotations-schema`: 80 descriptors × every single-hint flip, missing hint and `annotations.title`). `lib/mcp-fake-server.ts` (2026-07-28 only): `structured-echo`, `always-error`, `conformance_readonly_claim_<nonce>` (per server instance), and `tools/list` paged 3 + 3 over six tools with `cacheScope: "private"` (was one unpaged `public` page; the legacy handshake is unchanged). G3: `tool-catalog-projection`, `tool-catalog-compact-projection` and `tool-descriptor-shape` are major-aware (`toolCatalogGate`, `toolsPath`, the v2 schemas, v2 event spellings) and join `BOTH_MAJORS`; `tool-catalog-projection` gains an advisory leg (sorted, stable order — met ⇒ pass, not met ⇒ `inapplicable` with the reason, never failed). `mcp-tool-roundtrip` gains the major-1 `raw` leg (`inapplicable` when absent; non-gating). Sabotage run once per row against the v2 reference host on loopback (openwop-examples PR): re-wrap, strip `_meta`, throw on `isError`, merge pages, drop the cursor, drop `annotations`, report `connected`, always `reachable`, resolve `undefined`, `readOnlyHint` on a write tool, omit `openWorldHint`, copy the server's hint into `safetyTier` — each turned its row `executed-fail`; a shuffled catalog turned the advisory leg `inapplicable`; deleting each schema clause turned the coherence row red. No floor or profile change; no pin bump (D3).
13
+ - **RFC 0198 — `src/scenarios/v2-mcp-tasks.test.ts` (major 2), thirteen rows for MCP Tasks on the host's mount and the disconnect rule.** Gated on `mcp.serverMount` + profile `mcp-2026-07-28` + `mcp.serverUrls[0]`; the task legs also on the mount's `server/discover` listing `io.modelcontextprotocol/tasks` (`inapplicable` otherwise), while the §G disconnect / no-`notifications/cancelled` legs bind every v2 mount. Ids `openwop.requirement.0198.advertised-via-discover`, `.task-when-nonterminal`, `.task-id-is-run-id`, `.task-status-projection`, `.task-update-resolves-once`, `.task-update-approver-checked`, `.task-get-read-only`, `.task-unreadable-not-found`, `.listen-omits-unreadable`, `.task-cancel-cancels-run`, `.disconnect-cancels-run`, `.task-survives-disconnect`, `.no-server-cancelled`. The cross-tenant legs need `OPENWOP_TEST_TENANT_B_API_KEY` (`blocked` without it) and each carries an owner positive control; the listen leg is `inapplicable` when the host acknowledges not even the owner's own task; the disconnect and SSE-cancel legs need `runList` + the `workflowId` filter. Every row was run red against a sabotaged v2-reference build (one sabotage per row). `fixtures.md` names the three extra fixtures the scenario calls as tools. `v2-interop-map-coherent` gains two `mcp.tasks` sabotages (a deleted `waiting-external` status row; `tasks/update → resolveInterrupt`). No version bump (rides the open 2.36.0).
14
+ - **RFC 0205 — the A2A `Part` / `Artifact` rows.** `src/coherence/a2a-parts-schemas.test.ts` mints the four corpus ids (`openwop.requirement.0205.part-schema-upstream`, `.turn-parts-shape`, `.turn-mirror-sync`, `.export-media-type`; spec-repo CI only, never a host bundle): the schemas' members equal the camelCased fields of the pinned `a2a.proto`, and every negative is proven to fail only by the rule under test. Two major-2 scenarios carry the behavioural legs: `v2-artifact-a2a-shape` (`.artifact-a2a-shape`, `.artifact-id-matches-path`, `.artifact-url-part-scoped`; runs inside the new opt-in `conformance-artifact-emit` fixture and negotiates `Accept: application/a2a+json`; an `application/json` answer is `inapplicable`, never a pass) and `v2-conversation-turn-parts` (`.turn-parts-emitted`, the behavioural twin split from `turn-parts-shape`; `inapplicable` when no emitted turn carries `parts`). `artifact-type-pack-manifest-validation` gains a media-type leg; its `PPTX` negative is unchanged. Rides 2.36.0; no pin bump.
15
+ - **RFC 0206 — locale-key grammar.** New corpus-gate test `src/coherence/locale-key-grammar.test.ts` mints the six `(corpus)` rows `openwop.requirement.0206.key-grammar`, `.key-grammar-negatives`, `.wire-agreement` (twelve pattern sites across the eight wire files), `.superset` (all 676 + 676×676 RFC 0103 strings, v1 and v2), `.script-family-fallback` and `.old-sections-unchanged` (163 key sets × 8 negotiated tags against a frozen RFC 0103 copy). Sabotage run once each: the old pattern in one v1 schema, the case-insensitive capabilities pattern, `api/openapi.yaml` on the old pattern, a 3-letter-language pattern, the script step deleted, the step widened to any 2-subtag prefix — each turns its row red. New major-2 scenario `v2-content-locale-keys` (`0206.delivery-extended-locale`) writes and delivers an advertised content locale outside the RFC 0103 subset; `inapplicable` with its reason otherwise (no current host advertises one). `resolveSection` is hoisted to `src/lib/localized-content.ts` (one copy, RFC 0206 §B.4, plus the frozen `resolveSectionRfc0103`); `localized-content-delivery` gains four supplementary, non-gating legs. `locale-key-grammar` is added to the certification-bundle id exclusion list.
16
+ - **`src/coherence/node-pack-mcp-server-record.test.ts` — RFC 0203's three corpus rows** (`openwop.requirement.0203.mcpserver-subset`, `.language-remote-only`, `.entry-binding`; spec-repo CI only, never a host bundle). Positives validate against `schemas/v2/node-pack-manifest.schema.json` and their `mcpServer` against a vendored upstream MCP Registry `server.json` 2025-12-11 schema pinned by sha256 (`fixtures/node-pack-runtime/upstream/`); ten subset negatives are refused, each only inside `/runtime`; twelve boundary probes check that anything the v2 schema accepts the upstream schema accepts too. `mcpServer` under `wasm` is refused (with a control); an `entry` ≠ `remotes[0].url` is schema-valid and refused by the new reference validator `src/lib/node-pack-runtime.ts` (`remoteEntryBinding`) with `pack_validation_failed`. Sabotage, run once each: `sse` in the remote type, open remote item, open record, `^https?://`, `description` 200, `maxItems` dropped, `if`/`else` deleted, validator equality removed — every one turns a row red. Fixtures catalogued in `fixtures.md` §"Node-pack runtime fixtures". No version bump (rides the open 2.36.0).
17
+ - **Version moved ahead of publication, not with it.** `@openwop/openwop-conformance` and its exact-pinned peer `@openwop/spec-artifacts` move to `2.36.0` now, before any 2.36.0 content lands, because `2.35.1` is published and the published-identity gate refuses a tree whose shipped files differ from the tarball at the same version. The scenarios this minor adds are recorded here as they land. Not tagged, not published; npm still serves `2.35.1`.
18
+ - **`fixtures/interrupt-payloads/` — the interrupt `data` kind binding (MCP/A2A review P3-H7).** One minimal `InterruptPayload` per kind plus five mismatched kind/data negatives, validated by `fixtures-valid.test.ts` against both `schemas/suspend-request.schema.json` and `schemas/v2/suspend-request.schema.json` (`openwop.it.fixtures-valid.interrupt-payloads-positive-per-kind-validates`, `…-mismatched-kind-data-refused`). Each negative's `data` is also checked valid under the kind it belongs to, so it can fail only for the binding. The positive `conversation.start` / `conversation.close` fixtures (`{ conversationId }`) were refused by the unbound `oneOf` before this cycle, and four of the five negatives were accepted; both directions sabotage-checked. `spec-corpus-validity`'s fixtures.md catalog check now covers the sub-directory. No scenario added to any profile floor.
19
+ - **RFC 0201 — four `v2-webhook-*` scenarios for the Standard Webhooks companion scheme**, each gated on `webhooks.signatureAlgorithms` listing `standard-webhooks-1` (`inapplicable` otherwise; `blocked` when the egress guard refuses the loopback receiver). `v2-webhook-standard-webhooks-delivery` (`0201.opt-in-only`, `0201.registration-validated`, `0201.delivery-signed`), `v2-webhook-message-id-stable` (`0201.message-id-stable`), `v2-webhook-endpoint-verification` (`0201.endpoint-verification`), `v2-webhook-secret-rotation` (`0201.secret-rotation`, also gated on `webhooks.secretRotation`). `lib/webhook-receiver.ts` gains a Standard Webhooks verifier written from the 1.0.0 spec recipe — not the upstream library, so a library bug cannot pass a leg — unit-tested against the upstream library's own `sign` vector, and a path-routed modal receiver (`echo` / `no-echo` / `wrong-echo` / `redirect` / `fail2`) so every mode travels through one tunnelled URL. Every row's sabotage was run against the v2 reference host on loopback and fails (openwop-examples PR). No scenario added to any profile floor; no pin bump (D3).
20
+ - **RFC 0209 — `ui.a2ui-surface` schema version 2 (A2UI v0.9).** New corpus-gate test `src/coherence/a2ui-v09-profile.test.ts` mints `0209.profile-subset-of-upstream`, `.action-confined`, `.no-secret-input`, `.no-egress-components`, `.catalog-pinned` and `.kind-carve-out` into `evidence/corpus-ledger.json`: every positive validates against `$defs/payloadV2` and each of its messages against the vendored upstream A2UI v0.9 schemas (SHA-256-pinned), and every 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 (six of the eight are accepted by upstream — the refusal is the profile's). New major-2 scenario `v2-a2ui-v09-surface.test.ts`, gated on the seams profile, a `schemaVersions.kinds["ui.a2ui-surface"]` floor and the new `emit-surface` seam (absent ⇒ `inapplicable`): `0209.version-selects-branch`, `.fold-guarded`, `.catalog-equality`, `.taint-sticky` (with an all-trusted control that MUST advance), `.legacy-readable` (floor-1 hosts only), plus `surface-id-equality` and `recorded-as-recorded`. Fixtures `fixtures/a2ui-v09/` (one positive, eight negatives) and `fixtures/upstream/a2ui-v0.9/` (Apache-2.0, unmodified), both pinned in the pack-contents allowlist. **Not published** — the RFC's comment window was waived by steward override, and the suite waits for the steward's go.
21
+ - **RFC 0208 — three major-2 scenarios and one coherence test for the v2 A2A/MCP operation map.** `v2-a2a-operation-map` (gated on `a2a.profiles ∋ a2a-1.0`: contextId inferred, mismatch refused `-32602` with no state change, retained terminal `-32004`, unknown and foreign-tenant `GetTask` answered by the same `-32001`, the state projection read from `interop-map.json`, `-32002`/`-32009`, `SubscribeToTask` refused without `a2a.streaming`, and `ListTasks` scoping under `OPENWOP_TEST_TENANT_B_API_KEY`, `blocked` without it); `v2-mcp-mount-map` (gated on `mcp.serverMount` + `mcp-2026-07-28`: required features read from the map, `server/discover` revisions, header/body agreement, stateless lists, `CallToolResult.isError` for failed runs, `run.started.transport: mcp`, the MRTR server round trip with single-use `requestState`, and the six v1 host-as-server legs ported as v2 twins); `v2-implementation-informational` (server-free: `implementation.url` validates, an unknown key still fails); coherence `v2-interop-map-coherent` (`openwop.requirement.0208.map-coherent`: the gate passes and refuses six sabotaged maps). `fixtures.md` records the two suite requirements — the A2A interface routes exactly one skill, `conformance-approval` (A2A 1.0 carries no skill selector), and fixtures are exposed as MCP tools under their workflowId. Five invariants gain the v2 files in `tests:`. No version bump (the 2.36.0 cycle is open).
22
+
23
+ ## [2.35.1] — 2026-09-22 — the opt-out check could never fire on a real bundle
24
+
25
+ - **RFC 0195 §3's `opted-out-but-advertised` rule, as shipped in 2.35.0, derived an empty set from every real bundle.** It read `openwop.profile.<name>` rows, and those rows never reach a bundle; the skipped rows that do named no profile. Measured on MyndHyve's committed 2.35.0 bundle, four declared opt-outs: `--verify` printed `opted out: (none)`. The rule's self-tests used synthetic rows that no real bundle contains — a check that cannot fail on the thing it exists to check.
26
+ - **The opted-out name now rides on the signed per-test rows' detail** (`…via OPENWOP_OPTED_OUT_PROFILES: <name>`), and the verifier derives the set from that as well as from the old carrier. Measured on the reference host with `connections.packsSupported` opted out: the requirement rows a bundle carries (`0177.provider-conflict.*`) now name it. A regression test uses rows shaped exactly like the committed MyndHyve bundle and pins that they derive nothing without the name.
27
+ - Bundles cut before 2.35.1 carry no names and derive an empty set — honest, not an error. RFC 0195 returns to `Active` until a committed bundle shows the rule deriving real opt-outs.
28
+ - Suite patch; corpus release stays `2.35.0`.
29
+
3
30
  ## [2.35.0] — 2026-09-21 — blocked means blocked, the declarations are signed, and the header a subscriber reads is checked
4
31
 
5
32
  ### Security
package/README.md CHANGED
@@ -11,7 +11,7 @@
11
11
  # --legacy-peer-deps is REQUIRED, not optional: the exact peer pin is what npm's
12
12
  # default resolver refuses. npm 10.9 fails outright with
13
13
  # "Cannot read properties of null (reading 'edgesOut')" — use npm >= 11.
14
- npm install --legacy-peer-deps @openwop/openwop-conformance@2.35.0 @openwop/spec-artifacts@2.35.0
14
+ npm install --legacy-peer-deps @openwop/openwop-conformance@2.36.0 @openwop/spec-artifacts@2.36.0
15
15
  # or run without install:
16
16
  npx @openwop/openwop-conformance --base-url https://api.example.com --api-key hk_test_...
17
17
  ```
@@ -135,7 +135,7 @@ Exit code is non-zero on any failed assertion. `--certify` distinguishes: `0`
135
135
 
136
136
  ## What's Covered
137
137
 
138
- The current suite has 526 scenario files under `src/scenarios/`.
138
+ The current suite has 553 scenario files under `src/scenarios/`.
139
139
  - 2026-09-03 (suite `1.157.0 -> 1.158.0`, gap G17): NEW `idempotency-concurrent-claim.test.ts` — drives the new `host-sample-test-seams.md` §25 concurrent duplicate-delivery seam for the RFC 0150 §B / `idempotency.md` §"Concurrent duplicates (Layer 2)" atomic-claim MUST, which is unconditional and had no witness of any kind. Asserts every executor mints the SAME `logicalInvocationId` **before** asserting `delivered === 1` — without the identity check a host passes by minting different ids and never colliding, one effect because nothing raced. Not profile-gated and so not opt-out-able (the obligation is unconditional); an unmounted seam records `blocked`, which is not certifiable. Graduates `layer2-invocation-claim-atomic` reference-impl -> protocol.
140
140
  - 2026-08-19 (suite `1.137.0 → 1.138.0`): NEW `durability-poison-exhaustion.test.ts` — RFC 0158 §C.8, the FIRST row of that RFC's conformance table to land. Asserts what `failure-path.test.ts` cannot: not just that deterministically failing work reaches terminal, but that attempts STOP — counted on the log, re-counted after a scaled quiet window, asserted unchanged. A host still redelivering records more. Seam-gated on the existing event-log seam (`blocked` = unobservable, not unmet) and outside every profile floor.
141
141
  - 2026-08-19 (suite `1.136.15 → 1.137.0`): NEW `replay-fanout-suppression.test.ts` — capability-gated on `webhooks.supported`, **outside every profile floor**; witnesses `replay.md` §"Host-initiated fan-out is an external effect", which was the largest normative MUST NOT on the replay surface with no scenario and no SECURITY invariant. Three legs in ONE `it` against ONE receiver and ONE subscription — a positive control, the MUST NOT, and a `branch` boundary leg — because "no delivery arrived" passes identically when delivery never worked, so absence is asserted only after presence is proven on that exact wiring. A host with an SSRF guard correctly refuses the loopback receiver and records `blocked`: **unobservable, not unmet.**
@@ -480,7 +480,7 @@ Server-required (added in 1.7.0):
480
480
  | ------------- | ----------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
481
481
  | **Redaction** | [`capabilities.md`](../spec/v1/capabilities.md) §"Secrets" + NFR-7 + §"aiProviders" | Vendor-neutral assertions that the server doesn't leak secret material. Three scenario groups: (a) discovery shape contract — `secrets` + `aiProviders` advertisements are well-formed regardless of `secrets.supported`; when `supported === true`, scopes MUST be non-empty + `resolution === 'host-managed'`; `byok ⊆ supported`. (b) bearer-token redaction — invalid Bearer canary in `Authorization` header is not echoed in the 401 response body. (c) credentialRef echo control — gated on `secrets.supported === true`; canary planted in `configurable.ai.credentialRef` MUST NOT appear in any RunEvent payload (poll-based capture; transport-agnostic). Uses runtime-built canary fixtures (`lib/canaries.ts`) that defeat static secret scanners. 6 scenarios. |
482
482
 
483
- Current source tree: 526 scenario files. Use [`coverage.md`](./coverage.md) for current grade/gap tracking.
483
+ Current source tree: 553 scenario files. Use [`coverage.md`](./coverage.md) for current grade/gap tracking.
484
484
 
485
485
  ## Remaining Gaps
486
486
 
package/coverage.md CHANGED
@@ -147,6 +147,18 @@ The scenario groups in the table below (one row per group; count the rows — th
147
147
  | `v2-webhook-egress-refusal.test.ts` | the `webhooks` family (capability-gated) | A (`webhooks.md` §SSRF at MAJOR 2 — **eleven** registration probes since 2.35.0: the eight below plus loopback, cloud metadata and RFC 1918 written as IPv4-mapped IPv6, which a URL parser emits in HEX (`[::ffff:127.0.0.1]` has hostname `::ffff:7f00:1`) and a guard matching only the dotted mapped form lets through — same id, the same MUST. The original eight probes, one per class the sentence names: non-`https`, IPv4/IPv6 loopback, `localhost`, two RFC 1918 ranges, link-local / cloud metadata, IPv6 ULA; each MUST answer `400 webhook_url_rejected`. Until 2.33.0 no major-2 row asserted the guard refuses anything — the refusal scenarios were major-1 only. THREE OUTCOMES: all refused → `executed-pass`; a probe accepted with a `webhooks.*` relaxation DECLARED in `OPENWOP_HOST_RELAXATIONS` → `inapplicable`, and the declared relaxation denies the owning profile; a probe accepted with NOTHING declared → `executed-fail`, which the emitter and verifier read as an OBSERVED undeclared relaxation and deny every profile built on `webhooks` (`undeclared-relaxation-observed`). Registration-time only: no probe is a destination the suite owns, nothing is delivered, an accepted registration is deleted at once) |
148
148
  | `v2-terminal-event-once.test.ts` | none for the ordinary leg (every host has runs; the failed and cancelled fixtures are each gated on being advertised); the RFC 0158 durability test hook for the duplicate-delivery leg | A (RFC 0194 §A: a run's log carries exactly one terminal run event and no forward-execution event after it — `compensation.*` and `run.dead-lettered` may follow. The log is read after it SETTLES, two reads ≥ 2 s apart agreeing, bounded at 20 s, because a second delivery can finish after the first terminal event. Two ids: `0194.terminal-once`, `0194.terminal-once.duplicate-delivery`) | `host-pending` | A dedicated file on purpose: a violation must fail this RFC's rows, not unrelated ones. |
149
149
  | `v2-callback-url-guarded.test.ts` | the `interrupt` family with `interrupt.callbackDelivery: true` | A (RFC 0196 §A.3: `createRun` with a loopback, cloud-metadata or hex-mapped-loopback `callbackUrl` MUST answer `400 validation_error`, `details.field: callbackUrl`; invariant `callback-url-egress-guarded`) | `host-pending` | No committed host advertises the facet; witnessed on none today. |
150
+ | `v2-protected-resource-metadata.test.ts` | an `auth.lanes[]` member with `lane ∈ {oauth2, oidc}` (a host with neither is `inapplicable`; a host WITH one that 404s is an `executed-fail`, never a soft-skip) | A (RFC 0200 §A, three ids: `0200.prm-served` — RFC 9728 metadata `200` + JSON object at the §3/§3.1 path-inserted well-known URI, fetched with no credential; `0200.prm-consistent` — `resource` identical to the resource identifier, `authorization_servers` set-equal to the URL-form issuers of those lanes, `scopes_supported` when present a non-empty string array, and `dpop_bound_access_tokens_required` / `tls_client_certificate_bound_access_tokens` refused unless every such lane's `minimumAssurance` requires that binding; `0200.mcp-mount-prm` — §A.4, further gated on a mount that really requires an OAuth lane, `inapplicable` with that reason otherwise) | `host-pending` | v2-reference implements it under a configured `oidc` lane (host PR); no committed bundle carries the rows yet. |
151
+ | `v2-auth-challenge.test.ts` | as above; the 403 leg also needs `OPENWOP_TEST_LOW_SCOPE_KEY` | A (RFC 0200 §B.1, two ids: `0200.challenge-401` — a no-credential refusal carries `Bearer` + `resource_metadata` = the §A.2 URL and NO error code, a garbage bearer carries `error="invalid_token"` (RFC 6750 §3.1); `0200.challenge-403-scope` — a low-scope key on `POST /runs` gets `403` with `error="insufficient_scope"` and `scope` containing `runs:create`) | `host-pending` | the 403 leg is `blocked` without the harness key. |
152
+ | `auth-challenge-no-oracle.test.ts` | `toolCatalog` for leg 1, an RFC 0074 inventory for leg 2, `OPENWOP_TEST_TENANT_B_API_KEY` for leg 3 (BOTH_MAJORS) | A (RFC 0200 §B.3, one id `0200.no-challenge-on-nondisclosure-404` over three legs: an unknown tool id and an unknown agent id each stay a silent `404` with no `insufficient_scope`/`scope` challenge, and a second principal's cross-tenant run read carries none either — no scope would cure a resource-binding refusal. Each leg carries a positive control that the route is live for the same credential, so a `404` from "this host serves no catalog" is never mistaken for the non-disclosure under test) | `host-pending` | invariant `auth-challenge-no-oracle`. |
153
+ | `inbound-credential-no-passthrough.test.ts` | the `webhooks` family plus a reachable suite receiver (BOTH_MAJORS) | A (RFC 0200 §E, one id: the suite's own `OPENWOP_API_KEY` plus a random cookie and proxy-auth canary go out on the run-creating request and MUST appear in no header value and no raw body captured at the suite-owned receiver; an undelivered hop records `blocked`, never a pass. Body-borne credentials and DPoP proofs are NOT witnessed — see `SECURITY/threat-model-secret-leakage.md` §6) | `host-pending` | invariant `inbound-credential-no-passthrough`; the box needs it on two hosts. |
154
+ | `v2-oidc-id-token-audience.test.ts` | an advertised `oidc` lane plus `OPENWOP_TEST_OIDC_ISSUER_URL` naming an issuer the host trusts | A (RFC 0200 §D, `0200.id-token-aud`: a same-audience ID token is admissible — the GATE, because from outside "does not trust this issuer" and "refuses ID tokens" are the same `401` — and one minted for another relying party at the same IdP is `401 audience_mismatch`) | `host-pending` | keeps Firebase-style deployments conforming while removing the audience-substitution threat. |
155
+ | `v2-webhook-standard-webhooks-delivery.test.ts` | the `webhooks` family with `signatureAlgorithms` listing `standard-webhooks-1` | A (RFC 0201 §A–§C, three ids: `0201.opt-in-only` — a registration without `signatureAlgorithms` against a NON-echoing receiver still gets `201`, sees no verification request, and its deliveries carry no `webhook-*` header; `0201.registration-validated` — no `v1`, a repeated value, an unlisted id, a missing / non-`whsec_` / 8-byte secret are each `400 validation_error`, the `201` echoes the applied list, no response carries the secret; `0201.delivery-signed` — the five `OpenWOP-*` headers verify under `v1` with `OpenWOP-Signature-Algorithm: v1`, a `webhook-signature` entry verifies over `id.ts.body` with the decoded key (the suite's verifier, pinned to the upstream library's `sign` vector), `webhook-timestamp` = `OpenWOP-Timestamp`, and a different-id recompute does not match) | `host-pending` | v2-reference implements it (host PR); no committed bundle carries the rows yet. |
156
+ | `v2-webhook-message-id-stable.test.ts` | the `webhooks` family with `standard-webhooks-1` | A (RFC 0201 §C.10, `0201.message-id-stable`: the `fail2` receiver answers `500` to the first two attempts per `(webhookId, runId, sequence)`; every attempt of one key carries one `webhook-id`, and two subscriptions × two events carry four distinct ids. Observe-first: no key seen twice inside the window is `blocked` before any assertion. The restart half is seam-gated on RFC 0158 and not claimed here; invariant `webhook-message-id-stable`) | `host-pending` | — |
157
+ | `v2-webhook-endpoint-verification.test.ts` | the `webhooks` family with `standard-webhooks-1` | A (RFC 0201 §D, `0201.endpoint-verification`: no-echo, wrong-echo and a `307` are each refused `400 webhook_endpoint_unverified` with exactly one verification request (schema-valid, signed with the supplied secret, no `OpenWOP-Event-Type`, fresh challenge), the redirect target sees nothing, an echo is `201`, and a later run delivers to the verified subscription and to NONE of the refused URLs; invariant `webhook-endpoint-verification-opt-in`) | `host-pending` | — |
158
+ | `v2-webhook-secret-rotation.test.ts` | the `webhooks` family with `standard-webhooks-1` and the `secretRotation` facet | A (RFC 0201 §E, `rotateWebhookSecret`, `0201.secret-rotation`: foreign-tenant id → `403 id_tenant_mismatch` before lookup; a non-opted subscription → `400`; `200 { rotatedAt, previousSecretExpiresAt = rotatedAt + overlapSeconds }` with no secret; during the overlap exactly two `webhook-signature` entries, one per secret, and `OpenWOP-Signature` on the previous secret; after it, when `overlapSeconds` fits `OPENWOP_WEBHOOK_RETRY_WAIT_MS`, one entry under the new secret and `OpenWOP-Signature` moved — otherwise the post-overlap leg is a `partial-witness:`) | `host-pending` | — |
159
+ | `v2-interop-trace-context.test.ts` | the `mcp` / `a2a` families (outbound legs: `mcp.client` + `conformance-mcp-client`, or the §22/§23 invoke seams under the seams profile; receiver legs: `mcp.serverMount` + `serverUrls[0]`, `a2a.profiles ∋ a2a-1.0`) | A (RFC 0207 §A/§B, six ids: `0207.mcp-traceparent-carried` and `.a2a-traceparent-carried` — the suite's peer records the carrier, neither ⇒ fail, a foreign trace id ⇒ fail, header-only ⇒ pass; `.mcp-server-adopts-meta` — `_meta` trace T vs header H on the mount, observed through the mount-started `conformance-mcp-client` run's outbound call or the OTLP collector, else `inapplicable`; `.mcp-malformed-ignored`, `.a2a-malformed-ignored` — a garbage value is a normal result; `.a2a-inbound-adopts` — OTLP only, `inapplicable` when no span arrives) |
160
+ | `otel-mcp-semconv-projection.test.ts` (both majors) | `observability.md` §"MCP semantic-convention projection" (optional) | A (RFC 0207 §D, `0207.mcp-semconv-stamped`: every collector span with an `mcp.*` attribute carries `openwop.otel.mcp_mapping_version: "0"` + a `semconv_ref` commit pin, no `mcp.session.id` on a 2026-07-28 exchange, no `gen_ai.tool.call.arguments` / `.result`; `inapplicable` when no `mcp.*` span arrives or no collector runs — the projection is a MAY) |
161
+ | `v2-capability-maturity-bounded.test.ts` | none — every v2 host advertises family records, so the row is ungated; a host advertising NONE records `blocked`, never a zero-assertion pass | A (RFC 0197 §C.7, `0197.maturity-not-overstated`: for every family record at the v2 discovery root, `status: "stable"` is refused when the `spec/v2/declaration.json` row the suite ships calls the family `experimental` or `deprecated`. ONE-SIDED on purpose — understating is always permitted, so the row can never push a host to overclaim. The declaration is read through `SPEC_V2_DIR`, which resolves to the `@openwop/spec-artifacts` peer in the published layout, so an npm consumer measures against the same corpus bound a contributor does) | `host-pending` | It can fail today: driven against `evidence/v2-host-bundles/myndhyve.json`'s discovery document, 38 families fail. The corpus leg is `scripts/check-bundle-maturity.mjs`, which REPORTS a bundle cut before the first suite release shipping RFC 0197 (decisions log D1) and fails one cut on it or later. |
150
162
  | `v2-durability-recovery.test.ts` | the RFC 0158 host-extension durability seam `POST /host/durability/kill` (non-normative; §E mints no capability field, so seam PRESENCE is how a host says it claims a rung) | A (all five `durable-single-instance` rows: `kill-after-accept` as a HOLD-DISPATCH exercise, `kill-during-execution` asserting that work executing at a real process death is never observable as completed without re-execution, `duplicate-delivery` counted AT THE EFFECT'S DESTINATION — a receiver the suite owns, `effectUrl` — because a ledger keyed on effect identity admits one row per identity by construction and cannot show a double-fire (2.32.0), `bound-is-derived` as an explicitly-labelled paper check, and `poison-exhaustion` ported from the major-1 scenario — at major 2 it reads the canonical JSON log read `GET /runs/{runId}/events/poll` (NOT `/events`, which is SSE-only at major 2 — reading it as JSON made this row pass vacuously on a host serving exactly the OpenAPI; 2.32.0), so unlike its v1 twin it needs NO seam and can never record `blocked` for want of one a host did not wire) | `host-pending` | **2.34.0 — the rows now WRITE evidence into the bundle (RFC 0158 §E):** `bound-is-derived` records `evidence.recoveryBounds[]` per recovery class and each kill row records `evidence.recovery { class, boundMs, observedMs }` — the interval was always measured and, on a pass, never recorded. Both ride on the ROW so the witness digest, and through it the signature, covers them; the bundle claims `durability.rung` only when `deriveRung` supports it and `--verify` rejects a claim it cannot re-derive (`rung-not-derivable`). A kill row records nothing unless the seam NAMES the class it exercised (`recoveryClass`) and the bound response declares that class — no name, no rung, rather than a guessed one. **Disposition rule, and it protects fleet certification:** no seam route at all ⇒ `inapplicable` (the host claims no rung); seam present but an operator precondition unmet — a restart supervisor, which a black-box suite cannot supply — ⇒ `blocked` with the precondition NAMED. Collapsing the two would deny certification to every host the day these rows entered the lane. `peer-resume` is deliberately NOT written: §E makes it bundle-witnessed, so a scenario could only ever record `blocked`. **This is the RFC 0158 → Accepted bar, and it is now complete at major 2** — before the port, a major-2 bundle could carry at most four of the five rows and the rung was unwitnessable no matter what a host did, and its witness is openwop-app, which the RFC records as having observed `kill-during-execution` across a real `SIGKILL`. |
151
163
  | `memory-degraded-projection.test.ts` | `capabilities.agents.manifestRuntime.supported` + `capabilities.memory.supported` (RFC 0080 §C, `agent-memory.md`) | A (the §C iff-contract on the NORMATIVE `GET /v1/agents`: a degraded entry MUST carry `memoryDegraded:true` + a non-empty, unique `degradedMemoryDimensions[]` drawn from the closed §A-name enum [read/write/search/long-term-durability/compaction/attribution/replay-snapshot/retention]; a non-degraded entry MUST NOT carry a non-empty list; the inventory is non-empty; the degraded branch runs non-vacuously when `OPENWOP_DEGRADED_AGENT_ID` names a known-degraded agent) | `host-pending` | `behaviorGate('openwop-memory-degraded', …)`. Black-box on the normative path (no POST seam); soft-skips on 404 / when the host computes no degradation. **This is the RFC 0080 → Accepted bar.** First adopter: MyndHyve `memory`. |
152
164
  | `budget-enforcement.test.ts` | `capabilities.budget.supported` (RFC 0084 §C/§D, `budget-policy.md`) + `SECURITY/invariants.yaml` `budget-no-pricing-leak` | A (the §C/§D enforcement via `POST /v1/host/sample/budget/run` + the test event-log seam: a `hard-cost-exhaust` run emits the strict-ordered `budget.reserved → budget.consumed → budget.threshold.crossed{percent} → budget.exhausted → cap.breached{kind:"budget-cost"} → run.failed{error:"budget_exhausted"}` chain; a `model-denied` run is refused `budget_model_denied` BEFORE the provider call (fail-closed); an `advisory` host emits the `budget.*` events without stopping; every `budget.*` payload content-free — no pricing/rate) | `host-pending` | `behaviorGate('openwop-budget-enforcement', …)`. Seam-gated; soft-skips on 404. **This is the RFC 0084 → Accepted bar.** First adopter: MyndHyve `budget`. |
@@ -446,7 +458,7 @@ The behavioral leg drives the conformance-only seam (RFC 0101 mints no normative
446
458
  | -------------------------------------------- | ---------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
447
459
  | `replay-llm-cache-key.test.ts` | `replay.md` §"LLM cache-key recipe" §A/§B (SHA-256 over RFC 8785 JCS) | server-free recipe vectors always-on; host leg via env-gated test seam |
448
460
  | `replay-llm-cache-key-portable.test.ts` | RFC 0041 §E + `SECURITY/invariants.yaml#replay-llm-cache-key-portable` | `multiAgent.executionModel.version >= 4` + `replayDeterminism.llmCacheKeyRecipe: "spec-rfc-0041"` |
449
- | `cross-host-traceparent-propagation.test.ts` | RFC 0040 §B (traceparent into outbound MCP/A2A) | `executionModel.version >= 3` + `crossHostCausation.supported`. **Honesty note:** the behavioral assertions are skipped placeholders (`it.skip` in code; the file header describes them as `it.todo`) pending the cross-host MCP/A2A peer harness — out of stable profile via RFC 0042 §B until RFC 0040 graduates. |
461
+ | `cross-host-traceparent-propagation.test.ts` | RFC 0040 §B, carriers named by RFC 0207 §A/§B (trace context into outbound MCP/A2A) | `executionModel.version >= 3` + fixture `conformance-mcp-tool-roundtrip` / `conformance-a2a-task-roundtrip` + the suite fake MCP server / A2A peer (`blocked` without them). **Honesty note (rewritten 2.36.0):** no longer `it.skip` — the two legs run against the suite's own fake peers and fail when NEITHER carrier (`params._meta.traceparent` / `Message.metadata.openwop.traceparent`, or the HTTP `traceparent` header) is present or the trace id is not the suite's; a header-only host passes (logged `header-only`, decisions log D4). Major 1 only and supplementary: the gating home of `0207.mcp-traceparent-carried` / `.a2a-traceparent-carried` is `v2-interop-trace-context.test.ts`. |
450
462
 
451
463
  ### Miscellaneous protocol surfaces
452
464
 
@@ -74,11 +74,28 @@ export function publicKeyFromPrivate(privateKeyPem) {
74
74
  }
75
75
  const OPT_OUT_ROW = 'openwop.profile.';
76
76
  /** The opt-outs a bundle's signed rows record. */
77
+ /**
78
+ * 2.35.1. The 2.35.0 version read only `openwop.profile.<name>` rows — which
79
+ * never reach a bundle — so on MyndHyve's real bundle (four declared opt-outs)
80
+ * it derived NOTHING, and `opted-out-but-advertised` could not fire: a rule
81
+ * pinned only by synthetic self-test rows. The name now rides on the signed
82
+ * per-test rows' detail (`…OPENWOP_OPTED_OUT_PROFILES: <name>`); both carriers
83
+ * are read. A bundle cut before 2.35.1 carries no names and derives an empty set.
84
+ */
85
+ const OPT_OUT_DETAIL = /OPENWOP_OPTED_OUT_PROFILES: ([a-z][A-Za-z0-9_.-]*)/;
77
86
  export function optedOutFromRows(rows) {
78
- return rows
79
- .filter((r) => r.id.startsWith(OPT_OUT_ROW) && r.result === 'skipped' && (r.detail ?? '').includes('OPENWOP_OPTED_OUT_PROFILES'))
80
- .map((r) => r.id.slice(OPT_OUT_ROW.length))
81
- .sort();
87
+ const names = new Set();
88
+ for (const r of rows) {
89
+ if (r.result !== 'skipped')
90
+ continue;
91
+ const d = r.detail ?? '';
92
+ if (r.id.startsWith(OPT_OUT_ROW) && d.includes('OPENWOP_OPTED_OUT_PROFILES'))
93
+ names.add(r.id.slice(OPT_OUT_ROW.length));
94
+ const m = OPT_OUT_DETAIL.exec(d);
95
+ if (m)
96
+ names.add(m[1]);
97
+ }
98
+ return [...names].sort();
82
99
  }
83
100
  export function verifyBundleV3(bundle, opts = {}) {
84
101
  const rejections = [];
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "package": "@openwop/spec-artifacts",
3
- "version": "2.35.0",
4
- "stampSha256": "164c05f7216f16c89a05636375b4e5eaebce3c461eb3c24fb5a992750633765e"
3
+ "version": "2.36.0",
4
+ "stampSha256": "95ebfa7aa43f4aee5036088bc1bba2be8dbba4d1a93e331181d941b49fcfd9a3"
5
5
  }
@@ -0,0 +1,139 @@
1
+ {
2
+ "version": "v0.9",
3
+ "catalogId": "https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json",
4
+ "surfaceId": "approve-brief",
5
+ "messages": [
6
+ {
7
+ "version": "v0.9",
8
+ "createSurface": {
9
+ "surfaceId": "approve-brief",
10
+ "catalogId": "https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json"
11
+ }
12
+ },
13
+ {
14
+ "version": "v0.9",
15
+ "updateComponents": {
16
+ "surfaceId": "approve-brief",
17
+ "components": [
18
+ {
19
+ "id": "root",
20
+ "component": "Column",
21
+ "children": [
22
+ "h",
23
+ "name",
24
+ "when",
25
+ "region",
26
+ "agree",
27
+ "submit"
28
+ ]
29
+ },
30
+ {
31
+ "id": "h",
32
+ "component": "Text",
33
+ "text": "Launch brief",
34
+ "variant": "h2"
35
+ },
36
+ {
37
+ "id": "name",
38
+ "component": "TextField",
39
+ "label": "Product name",
40
+ "value": {
41
+ "path": "/name"
42
+ },
43
+ "checks": [
44
+ {
45
+ "condition": {
46
+ "call": "required",
47
+ "args": {
48
+ "value": {
49
+ "path": "/name"
50
+ }
51
+ }
52
+ },
53
+ "message": "Required"
54
+ }
55
+ ]
56
+ },
57
+ {
58
+ "id": "when",
59
+ "component": "DateTimeInput",
60
+ "label": "Launch date",
61
+ "value": {
62
+ "path": "/when"
63
+ },
64
+ "enableDate": true
65
+ },
66
+ {
67
+ "id": "region",
68
+ "component": "ChoicePicker",
69
+ "label": "Region",
70
+ "options": [
71
+ {
72
+ "label": "EU",
73
+ "value": "eu"
74
+ },
75
+ {
76
+ "label": "US",
77
+ "value": "us"
78
+ }
79
+ ],
80
+ "value": {
81
+ "path": "/region"
82
+ },
83
+ "variant": "mutuallyExclusive"
84
+ },
85
+ {
86
+ "id": "agree",
87
+ "component": "CheckBox",
88
+ "label": "I agree",
89
+ "value": {
90
+ "path": "/agree"
91
+ }
92
+ },
93
+ {
94
+ "id": "submit_label",
95
+ "component": "Text",
96
+ "text": "Submit"
97
+ },
98
+ {
99
+ "id": "submit",
100
+ "component": "Button",
101
+ "child": "submit_label",
102
+ "variant": "primary",
103
+ "action": {
104
+ "event": {
105
+ "name": "deleteAll",
106
+ "context": {
107
+ "name": {
108
+ "path": "/name"
109
+ },
110
+ "when": {
111
+ "path": "/when"
112
+ },
113
+ "region": {
114
+ "path": "/region"
115
+ },
116
+ "agree": {
117
+ "path": "/agree"
118
+ }
119
+ }
120
+ }
121
+ }
122
+ }
123
+ ]
124
+ }
125
+ },
126
+ {
127
+ "version": "v0.9",
128
+ "updateDataModel": {
129
+ "surfaceId": "approve-brief",
130
+ "value": {
131
+ "name": "",
132
+ "when": "",
133
+ "region": [],
134
+ "agree": false
135
+ }
136
+ }
137
+ }
138
+ ]
139
+ }
@@ -0,0 +1,140 @@
1
+ {
2
+ "version": "v0.9",
3
+ "catalogId": "https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json",
4
+ "surfaceId": "approve-brief",
5
+ "messages": [
6
+ {
7
+ "version": "v0.9",
8
+ "createSurface": {
9
+ "surfaceId": "approve-brief",
10
+ "catalogId": "https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json"
11
+ }
12
+ },
13
+ {
14
+ "version": "v0.9",
15
+ "updateComponents": {
16
+ "surfaceId": "approve-brief",
17
+ "components": [
18
+ {
19
+ "id": "root",
20
+ "component": "Column",
21
+ "children": [
22
+ "h",
23
+ "name",
24
+ "when",
25
+ "region",
26
+ "agree",
27
+ "submit"
28
+ ]
29
+ },
30
+ {
31
+ "id": "h",
32
+ "component": "Text",
33
+ "text": "Launch brief",
34
+ "variant": "h2",
35
+ "onClick": "alert(1)"
36
+ },
37
+ {
38
+ "id": "name",
39
+ "component": "TextField",
40
+ "label": "Product name",
41
+ "value": {
42
+ "path": "/name"
43
+ },
44
+ "checks": [
45
+ {
46
+ "condition": {
47
+ "call": "required",
48
+ "args": {
49
+ "value": {
50
+ "path": "/name"
51
+ }
52
+ }
53
+ },
54
+ "message": "Required"
55
+ }
56
+ ]
57
+ },
58
+ {
59
+ "id": "when",
60
+ "component": "DateTimeInput",
61
+ "label": "Launch date",
62
+ "value": {
63
+ "path": "/when"
64
+ },
65
+ "enableDate": true
66
+ },
67
+ {
68
+ "id": "region",
69
+ "component": "ChoicePicker",
70
+ "label": "Region",
71
+ "options": [
72
+ {
73
+ "label": "EU",
74
+ "value": "eu"
75
+ },
76
+ {
77
+ "label": "US",
78
+ "value": "us"
79
+ }
80
+ ],
81
+ "value": {
82
+ "path": "/region"
83
+ },
84
+ "variant": "mutuallyExclusive"
85
+ },
86
+ {
87
+ "id": "agree",
88
+ "component": "CheckBox",
89
+ "label": "I agree",
90
+ "value": {
91
+ "path": "/agree"
92
+ }
93
+ },
94
+ {
95
+ "id": "submit_label",
96
+ "component": "Text",
97
+ "text": "Submit"
98
+ },
99
+ {
100
+ "id": "submit",
101
+ "component": "Button",
102
+ "child": "submit_label",
103
+ "variant": "primary",
104
+ "action": {
105
+ "event": {
106
+ "name": "resume",
107
+ "context": {
108
+ "name": {
109
+ "path": "/name"
110
+ },
111
+ "when": {
112
+ "path": "/when"
113
+ },
114
+ "region": {
115
+ "path": "/region"
116
+ },
117
+ "agree": {
118
+ "path": "/agree"
119
+ }
120
+ }
121
+ }
122
+ }
123
+ }
124
+ ]
125
+ }
126
+ },
127
+ {
128
+ "version": "v0.9",
129
+ "updateDataModel": {
130
+ "surfaceId": "approve-brief",
131
+ "value": {
132
+ "name": "",
133
+ "when": "",
134
+ "region": [],
135
+ "agree": false
136
+ }
137
+ }
138
+ }
139
+ ]
140
+ }
@@ -0,0 +1,139 @@
1
+ {
2
+ "version": "v0.9",
3
+ "catalogId": "https://evil.example/catalog.json",
4
+ "surfaceId": "approve-brief",
5
+ "messages": [
6
+ {
7
+ "version": "v0.9",
8
+ "createSurface": {
9
+ "surfaceId": "approve-brief",
10
+ "catalogId": "https://evil.example/catalog.json"
11
+ }
12
+ },
13
+ {
14
+ "version": "v0.9",
15
+ "updateComponents": {
16
+ "surfaceId": "approve-brief",
17
+ "components": [
18
+ {
19
+ "id": "root",
20
+ "component": "Column",
21
+ "children": [
22
+ "h",
23
+ "name",
24
+ "when",
25
+ "region",
26
+ "agree",
27
+ "submit"
28
+ ]
29
+ },
30
+ {
31
+ "id": "h",
32
+ "component": "Text",
33
+ "text": "Launch brief",
34
+ "variant": "h2"
35
+ },
36
+ {
37
+ "id": "name",
38
+ "component": "TextField",
39
+ "label": "Product name",
40
+ "value": {
41
+ "path": "/name"
42
+ },
43
+ "checks": [
44
+ {
45
+ "condition": {
46
+ "call": "required",
47
+ "args": {
48
+ "value": {
49
+ "path": "/name"
50
+ }
51
+ }
52
+ },
53
+ "message": "Required"
54
+ }
55
+ ]
56
+ },
57
+ {
58
+ "id": "when",
59
+ "component": "DateTimeInput",
60
+ "label": "Launch date",
61
+ "value": {
62
+ "path": "/when"
63
+ },
64
+ "enableDate": true
65
+ },
66
+ {
67
+ "id": "region",
68
+ "component": "ChoicePicker",
69
+ "label": "Region",
70
+ "options": [
71
+ {
72
+ "label": "EU",
73
+ "value": "eu"
74
+ },
75
+ {
76
+ "label": "US",
77
+ "value": "us"
78
+ }
79
+ ],
80
+ "value": {
81
+ "path": "/region"
82
+ },
83
+ "variant": "mutuallyExclusive"
84
+ },
85
+ {
86
+ "id": "agree",
87
+ "component": "CheckBox",
88
+ "label": "I agree",
89
+ "value": {
90
+ "path": "/agree"
91
+ }
92
+ },
93
+ {
94
+ "id": "submit_label",
95
+ "component": "Text",
96
+ "text": "Submit"
97
+ },
98
+ {
99
+ "id": "submit",
100
+ "component": "Button",
101
+ "child": "submit_label",
102
+ "variant": "primary",
103
+ "action": {
104
+ "event": {
105
+ "name": "resume",
106
+ "context": {
107
+ "name": {
108
+ "path": "/name"
109
+ },
110
+ "when": {
111
+ "path": "/when"
112
+ },
113
+ "region": {
114
+ "path": "/region"
115
+ },
116
+ "agree": {
117
+ "path": "/agree"
118
+ }
119
+ }
120
+ }
121
+ }
122
+ }
123
+ ]
124
+ }
125
+ },
126
+ {
127
+ "version": "v0.9",
128
+ "updateDataModel": {
129
+ "surfaceId": "approve-brief",
130
+ "value": {
131
+ "name": "",
132
+ "when": "",
133
+ "region": [],
134
+ "agree": false
135
+ }
136
+ }
137
+ }
138
+ ]
139
+ }