@openwop/openwop-conformance 2.45.22 → 2.45.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/README.md +2 -2
- package/dist/spec-artifacts.lock.json +2 -2
- package/fixtures/conformance-multi-party-council-oversize.json +244 -0
- package/fixtures/conformance-multi-party-council.json +61 -0
- package/fixtures/conformance-nondeterminism.json +23 -0
- package/fixtures/conformance-wasm-pack-memory-cap-breach.json +23 -5
- package/fixtures/frontend-plugin-packs/ui/narrow.html +4 -0
- package/fixtures/frontend-plugin-packs/ui-plugin-pack-narrow.json +16 -0
- package/fixtures/wasm-packs/build.mjs +43 -0
- package/fixtures/wasm-packs/misbehaving-abi/module.wasm +0 -0
- package/fixtures/wasm-packs/misbehaving-abi/module.wat +24 -0
- package/fixtures/wasm-packs/misbehaving-abi/pack.json +27 -0
- package/fixtures/wasm-packs/misbehaving-memory/module.wasm +0 -0
- package/fixtures/wasm-packs/misbehaving-memory/module.wat +30 -0
- package/fixtures/wasm-packs/misbehaving-memory/pack.json +27 -0
- package/fixtures.md +32 -0
- package/package.json +2 -2
- package/requirements.json +251 -7
- package/scenario-majors.json +17 -2
- package/schemas/CORPUS-STAMP.json +29 -27
- package/src/lib/council-roster-witness.ts +144 -0
- package/src/lib/ui-plugin-boundary-witness.ts +180 -0
- package/src/lib/wasm-memory-cap-witness.ts +46 -0
- package/src/scenarios/v2-bound-id-path-projection.test.ts +20 -0
- package/src/scenarios/v2-envelope-contracts-advertisement.test.ts +8 -0
- package/src/scenarios/v2-multi-party-council.test.ts +56 -0
- package/src/scenarios/v2-nondeterminism-policy-advertisement.test.ts +11 -1
- package/src/scenarios/v2-nondeterminism-sources.test.ts +83 -0
- package/src/scenarios/v2-purpose-propagation-advertisement.test.ts +1 -1
- package/src/scenarios/v2-purpose-propagation-onward.test.ts +119 -0
- package/src/scenarios/v2-ui-plugin-boundary.test.ts +68 -0
- package/src/scenarios/v2-wasm-memory-cap.test.ts +34 -0
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"_comment": "Provenance of @openwop/spec-artifacts (RFC 0168 §D.2). files: SHA-256 per file; the conformance suite compares the installed peer against dist/spec-artifacts.lock.json at start.",
|
|
3
3
|
"package": "@openwop/spec-artifacts",
|
|
4
|
-
"version": "2.45.
|
|
5
|
-
"corpusTag": "v2.45.
|
|
4
|
+
"version": "2.45.23",
|
|
5
|
+
"corpusTag": "v2.45.23",
|
|
6
6
|
"files": {
|
|
7
7
|
"api/.redocly.lint-ignore.yaml": "bf5a8350b88a72fa43f59605ed8d903ed24b6cfccda5e45509c9f6ed9ee4e712",
|
|
8
8
|
"api/asyncapi.yaml": "d5ecb9ee6114582be3b1f662c84bfac9ae96dae7bacb853e461168f70a8e1c7d",
|
|
9
9
|
"api/grpc/openwop.proto": "c3e72bb17cba514ee98feb6434e6c9b6ea6795bfd086489ec69fd882dd1ad977",
|
|
10
10
|
"api/openapi.yaml": "ac6de5303872664b78ff419c035915593e01599e02835e777a0123de1baeb398",
|
|
11
11
|
"api/redocly.yaml": "b0604c89b2ca6d5076ec25725c539dad44a741a811fe524439ee6daef8baa09f",
|
|
12
|
-
"api/seams-v2.yaml": "
|
|
13
|
-
"api/v2/asyncapi.yaml": "
|
|
14
|
-
"api/v2/openapi.yaml": "
|
|
12
|
+
"api/seams-v2.yaml": "97c463b975fb6831ef19f415cf66abc4693f834bb2940b6d299d5cd30181ae9d",
|
|
13
|
+
"api/v2/asyncapi.yaml": "f5b14bc6edc7a7ad798cb280b1824aa46582a4cce450fb5ef079d84349b6e58e",
|
|
14
|
+
"api/v2/openapi.yaml": "391814e12addeb810f20a4818b2f49a103a6b3d6262679e696bb14c4699d6402",
|
|
15
15
|
"api/v2/redocly.yaml": "1e66b60e6118ad11a823bb620678be464d99dfe50a40e3e6f93ec9429b88b34c",
|
|
16
16
|
"schemas/README.md": "a829e3208413062388e52ab6ba714d35889c9c7645db38a290b0c4f4992cd3da",
|
|
17
17
|
"schemas/a2a-task-state.schema.json": "75d5049dea8bd873ff0e7546f1c60c8d36c219bec7084264360be54a8be30ae1",
|
|
@@ -117,8 +117,8 @@
|
|
|
117
117
|
"schemas/v2/artifact.schema.json": "85b9dc284d39b99d2a66fa8cbf47f2dd3d8526d3bd6cc411ad315b372de38975",
|
|
118
118
|
"schemas/v2/audit-verify-result.schema.json": "14af8e9283510f0a40e48a2059c929ee44e0fc9fa3cc66a95bfbe90b17c48297",
|
|
119
119
|
"schemas/v2/budget-policy.schema.json": "1de68095db0beb5c7a4be629de26e79f62ebbe748a88d46f36e768edfd1f0d68",
|
|
120
|
-
"schemas/v2/capabilities.schema.json": "
|
|
121
|
-
"schemas/v2/certification-bundle.schema.json": "
|
|
120
|
+
"schemas/v2/capabilities.schema.json": "d859a413ff463ec39acc32a9cb76f2e8c7419e45683d6b7f886861765a9c36c7",
|
|
121
|
+
"schemas/v2/certification-bundle.schema.json": "ee3b10966458a10568a32b8a7eb4ab7bdb2f2d00da3ef6b698cd06462d52de40",
|
|
122
122
|
"schemas/v2/channel-presence-payload.schema.json": "fc03df0c20cc12a25f7ee6a47503a4c5931d79fe3d5ff24815e8fd7c1bcb9a87",
|
|
123
123
|
"schemas/v2/channel-written-payload.schema.json": "ccecff3c71a3275ad8db035ac0e04db25e6f8d23cf091ddbcd5a60d1be1cbb6b",
|
|
124
124
|
"schemas/v2/chat-card-pack-manifest.schema.json": "92a57db2fd4b2463f14ef155fef8e3f18afc78960b646459dcf32efc79ff6bf5",
|
|
@@ -144,7 +144,7 @@
|
|
|
144
144
|
"schemas/v2/envelopes/schema.request.schema.json": "be2c5f7aa881cdd252ccff47ac02193659e1393633a1541f4297383205d384a2",
|
|
145
145
|
"schemas/v2/envelopes/schema.response.schema.json": "d049a081267c06c28879c331b147f3ed821b14ccfd93005c67aff64db0a8c68e",
|
|
146
146
|
"schemas/v2/envelopes/ui.a2ui-surface.schema.json": "e92a2548ba65454c9e0a34f72ea6768d18f59e61fa5da7668d0b2d5baddaddc8",
|
|
147
|
-
"schemas/v2/error-envelope.schema.json": "
|
|
147
|
+
"schemas/v2/error-envelope.schema.json": "fd561987db4d277d007666f533b53d6cf0e7eacb57f930f3ce7e431573120890",
|
|
148
148
|
"schemas/v2/eval-summary.schema.json": "2f384daf05b8f3b4f604688cae504d6d9971ba41b8c771e55fe9a17e09e86b7a",
|
|
149
149
|
"schemas/v2/export-bundle.schema.json": "bd28905a7729437ab7f2ad8bb33e13a371b2bf5605a7eb2962a0d246de312d69",
|
|
150
150
|
"schemas/v2/form-content-pack-manifest.schema.json": "a1942a124e250a6e9fbdb6b811c5a125e78fbcbcaa46dfc71d0636f24710a339",
|
|
@@ -192,7 +192,7 @@
|
|
|
192
192
|
"schemas/v2/trigger-event.schema.json": "710ecca8b3ecdad3a532a96768177f062192b24a589a4f798c7eb24bab64b30a",
|
|
193
193
|
"schemas/v2/trigger-subscription-registration.schema.json": "5f7959f3e13f49399783e47f73101d6af95e5abccea1af2f0014ccbe3e51006c",
|
|
194
194
|
"schemas/v2/trigger-subscription.schema.json": "06abe021712a4bfc5a107a067053cd3cc9704f47ae7066d8449a508c368a0ab2",
|
|
195
|
-
"schemas/v2/ui-plugin-message.schema.json": "
|
|
195
|
+
"schemas/v2/ui-plugin-message.schema.json": "2211d58ddc3c42586586a61f03e637212465aff6c922755131f0beab58823435",
|
|
196
196
|
"schemas/v2/webhook-dead-letter-page.schema.json": "78f3284722c2b42d52ff8a1e6d9b9092ba1ac35c2dbe036f3ff2470b34086d1e",
|
|
197
197
|
"schemas/v2/webhook-delivery.schema.json": "5f5361ede12098b5c2d685f0aa70fcb07219fd6b68c6ad6af393ca8dd266d56a",
|
|
198
198
|
"schemas/v2/webhook-verification.schema.json": "76e4e0d81143ffd2649bc7adba2aa34f2525e2c4e9f0300a425d908b3163196b",
|
|
@@ -206,16 +206,16 @@
|
|
|
206
206
|
"schemas/workload-identity.schema.json": "64ed93e12cc9f0657e56ff9b8f045e5d68d397df5276d103c07f42c7f34f0579",
|
|
207
207
|
"schemas/workspace-file-create.schema.json": "70a23b92c2958c0165d7d29aea9020a045dc7816af82c80c5f50d0aac19c4856",
|
|
208
208
|
"schemas/workspace-file.schema.json": "464de85c2a068243084ee9c1d969bc7cd5d8f7948574e58450d6493c38a0e1e4",
|
|
209
|
-
"spec/v1/alias-detectors.json": "
|
|
209
|
+
"spec/v1/alias-detectors.json": "22444c22bb88c568339ef512553f9fc38146e22b566d11ab8608f655d688d19c",
|
|
210
210
|
"spec/v1/capability-declaration-classes.json": "e7729aed5c4b4e1dd02abab0530f14cc95f5d4070fe51fb139e7f5cccefa00c6",
|
|
211
|
-
"spec/v1/core-standard-manifest.json": "
|
|
212
|
-
"spec/v1/deprecations.json": "
|
|
211
|
+
"spec/v1/core-standard-manifest.json": "c4f11f11dffb4b2492fa9f1870c3fb6c0cf66b4cd0f232c8e2b860ee931adb8b",
|
|
212
|
+
"spec/v1/deprecations.json": "e723bc11d7272c71789ad1303bb7f21809a8a024abf57594f9d5c35dc199ece9",
|
|
213
213
|
"spec/v1/deprecations.schema.json": "3e393c405d2a41b467d8c5e3c468549078df1ce6a6d2588fc488097b95d9b55b",
|
|
214
214
|
"spec/v1/end-of-support.json": "35f0bf3b70fe78b16e2a2fd81a153fc27e703f6c16376c6fe7cdf1f4f2c4e6d4",
|
|
215
215
|
"spec/v1/event-codemap.json": "3da60d884157793a360da532a9fcbbfb5285636db325a74cec94b34622186d97",
|
|
216
216
|
"spec/v1/event-codemap.schema.json": "d05933b2e88103aff51a2f774df97b9bda065e0fc88dbd9b114cc47a32189174",
|
|
217
217
|
"spec/v1/extensions.json": "79a60754aa16cbdbdb604f8e5c00038af535a1690a4d07bd5ff7c38f0256a7bc",
|
|
218
|
-
"spec/v1/gaps.json": "
|
|
218
|
+
"spec/v1/gaps.json": "8b4da733ecec873f0e5d060e83cbe044fe168e57940a8d3fc9ea7c3cc0de7240",
|
|
219
219
|
"spec/v1/gaps.schema.json": "8fd83259f556553c9df0f53e7a82ca8c2a4771d471197f69b4e2ae0ceeacfd66",
|
|
220
220
|
"spec/v1/migrations.json": "4c50760be70cb644d02427bcaf5749606700edf4513f423903cf3b1d12f5c300",
|
|
221
221
|
"spec/v1/migrations.schema.json": "886779aa6c22e646db097f5df210adb018a4dd14a7b815465a18c8a7056c8f72",
|
|
@@ -223,15 +223,15 @@
|
|
|
223
223
|
"spec/v1/spec-gaps.json": "ea9612af55bea9dae3e82223d4f714243f8f590b0193f1188257b13491a38afc",
|
|
224
224
|
"spec/v2/README.md": "ddea90d7cf14aefffe1a43eeeecd4222dadad1d1e5ac64dceb073990eb93d9ef",
|
|
225
225
|
"spec/v2/core/artifact-type-packs.md": "f734740cc3963f86c9f273a7dace26291bba2855161ddd3540618131a2c8d13e",
|
|
226
|
-
"spec/v2/core/capabilities.md": "
|
|
226
|
+
"spec/v2/core/capabilities.md": "8218c44f90782df9630dd276e004def162dad989464cba85e7fc42d987bb61f8",
|
|
227
227
|
"spec/v2/core/conformance.md": "3ca291e63bc70a6ef829ae8759ad0ded3741ea7aa507bbbdffc57de443ab2ae2",
|
|
228
228
|
"spec/v2/core/connection-packs.md": "c80f95bd6a03698f35c72cdb2b46bd973f49203bbd786c26760e35fa86e6f23d",
|
|
229
|
-
"spec/v2/core/conversation.md": "
|
|
230
|
-
"spec/v2/core/errors.md": "
|
|
231
|
-
"spec/v2/core/events.md": "
|
|
229
|
+
"spec/v2/core/conversation.md": "8d6abedf6af408a74577753bfca24719d318e2632e10b45ff5d63bdcf31a8aaa",
|
|
230
|
+
"spec/v2/core/errors.md": "e48838701b86bbfe0ffee77b18a8ffc4e5f04724b4318507d9771da653f8da0f",
|
|
231
|
+
"spec/v2/core/events.md": "3eb297c1ca8a4c20017248051fbe9c5319bb51ef1ba4cb476e29840cbbe141a6",
|
|
232
232
|
"spec/v2/core/execution.md": "8a0c9db1bbfb97c008ab9328b7333c1cc3318196e78bfbefb49e781bdf4f9d44",
|
|
233
233
|
"spec/v2/core/form-content-packs.md": "02931227899676fd4581a27017be2a6c4953904717be9b81c29d12f87ab3917d",
|
|
234
|
-
"spec/v2/core/headers.md": "
|
|
234
|
+
"spec/v2/core/headers.md": "18e52477898e67b0b3778d1fdd22e07bc43d561c03b501573404cd53245edef6",
|
|
235
235
|
"spec/v2/core/host-services.md": "73f26ea32891044131d63f9861d2cbddccb5d08bf76e29e47afef9bb5b646791",
|
|
236
236
|
"spec/v2/core/i18n.md": "882904b7d59f992ab1e5f7346167161a54cf665b8fee47e64a3862cf9133a580",
|
|
237
237
|
"spec/v2/core/idempotency.md": "adbd317a6ebb57c2759c076297b31f67def9e391576bccd0bbea787b13e85872",
|
|
@@ -241,10 +241,10 @@
|
|
|
241
241
|
"spec/v2/core/node-pack-runtimes.md": "e1088cb69b8c3c67256c494063328916c2980ab1aa97a7063de25bab82fdb1e5",
|
|
242
242
|
"spec/v2/core/oauth.md": "c069ae4f9aae2eafe14c07c0bf709aa7ade0dcc821973d740400d49d901f7061",
|
|
243
243
|
"spec/v2/core/overview.md": "999cb657711d6c28e62032fc31a013cc175f7a981a3b012e4bf4a90a2b84344c",
|
|
244
|
-
"spec/v2/core/packs.md": "
|
|
244
|
+
"spec/v2/core/packs.md": "74d9c804a71e0e0400e7a5667a918670bcb435be0702b4625ec774ef0f1aa359",
|
|
245
245
|
"spec/v2/core/persistence.md": "249a1de353f421cdf92b9006a13a10afdb002f4029eb6a13520cc95252edb2ec",
|
|
246
246
|
"spec/v2/core/portability.md": "ab13a6ca43544b9a6a91edfb377ec680c768d14be841c84fb79125b231426891",
|
|
247
|
-
"spec/v2/core/replay.md": "
|
|
247
|
+
"spec/v2/core/replay.md": "9e66e2029b65866187c0920428a8c5bf8ad1c2b39feefbd5c02f0e59909afc7d",
|
|
248
248
|
"spec/v2/core/runs.md": "1c292c0ab4a3119aeff3a6e9b6e99cff69782df78c3872179f9f0661b6204f23",
|
|
249
249
|
"spec/v2/core/security-defaults.md": "887d8c4bc592094e3b931c23f1c2175c18ebbd47528b074016f9d2091bc513b2",
|
|
250
250
|
"spec/v2/core/storage.md": "dc94b985847c1341c7a064f04a6010f0fdbe6c3ab3e555d4362878c81d764c18",
|
|
@@ -254,10 +254,10 @@
|
|
|
254
254
|
"spec/v2/core/workflow-chain-packs.md": "fdce7a8477e269950a2bac05ee6eddbd66eb0b670b464d00054f7196ec05d29c",
|
|
255
255
|
"spec/v2/corrections.json": "3a8d93e33865b692d26a3b6bfd3819766e206b2e0c2a69669baa4bf833d05a49",
|
|
256
256
|
"spec/v2/corrections.schema.json": "46bbffb954d13bd9995aebc912ceeb2544f7064331abf25fd6ab85c81a5cec94",
|
|
257
|
-
"spec/v2/declaration.json": "
|
|
257
|
+
"spec/v2/declaration.json": "396ac924068f25d4a178062a54e244f870c556c3db9fe7a98fcc933fb318ed30",
|
|
258
258
|
"spec/v2/declaration.schema.json": "ef951633e4070899f64828f5cf1b5f4870270a0360f81dbb49b5b4d6cd407550",
|
|
259
259
|
"spec/v2/eos-override.json": "4c72d2f6a658943b01c1de1643cf294642091933af7832f38ee5495df684cb2d",
|
|
260
|
-
"spec/v2/errors.json": "
|
|
260
|
+
"spec/v2/errors.json": "c3456c8cb1f36f4b6252bbc1fa3a643eabf28cdb7088e3743e50bd51a10c4936",
|
|
261
261
|
"spec/v2/errors.schema.json": "47404ad53e6147cf9017b813483712466573e419fb33e6c4b6c12d3e7deba1bc",
|
|
262
262
|
"spec/v2/event-codemap.json": "37869a03cdfd8758b57e6f350641a6ebbd383231cebda51f5473ad85f94ab74c",
|
|
263
263
|
"spec/v2/event-codemap.schema.json": "b173f2a9bcc0bf9b62a474ce3b91e1431d597e64fd1560fc58662cd8606eaf9c",
|
|
@@ -291,6 +291,7 @@
|
|
|
291
291
|
"spec/v2/facets/hostEvents.schema.json": "973ff452cb91356b4c625d0b06c8d2ef335e18183de09e1afde8acead22290b3",
|
|
292
292
|
"spec/v2/facets/interrupt.schema.json": "2110ef32c3f880bf706475ef4cf8ddf97ff06871b8e6003a5edbcf4439f1fb70",
|
|
293
293
|
"spec/v2/facets/mcp.schema.json": "7060657dfcff170f079beed160e3db7c1d3cc4410cd3cf64863baf6459f781cd",
|
|
294
|
+
"spec/v2/facets/nondeterminismPolicy.schema.json": "e9dfaa32e0778a1a837fdad3b8a0673fccda9579ab08c8bfe9777362dd15e989",
|
|
294
295
|
"spec/v2/facets/oauth.schema.json": "3a0cbe249d3bd5a5982349ae7ba2534776cc958e4a57e0fbeabf2bdab6e99e69",
|
|
295
296
|
"spec/v2/facets/packs.schema.json": "d544a280419c0052ce0f3c3b378f7aa04085cadc31df52a3c8f43da600b131b1",
|
|
296
297
|
"spec/v2/facets/replay.schema.json": "0a5aefb0108c708c6794fadd533cf256709704c4f16dbf44cd63fa8980c78018",
|
|
@@ -298,9 +299,10 @@
|
|
|
298
299
|
"spec/v2/facets/sandbox.schema.json": "e75f54486d387b2d089791aa93b5093cd1450f1920753a0f97fc23acdfe978ed",
|
|
299
300
|
"spec/v2/facets/schemaVersions.schema.json": "890d3416c4b66511d51f329abc68e6f467f4338f16807e489fe682999b2b56b9",
|
|
300
301
|
"spec/v2/facets/supportedEnvelopes.schema.json": "644f22cc2236e30a611b5b7f1ba0458ff1925d68b94c514d9672583224e126bd",
|
|
302
|
+
"spec/v2/facets/uiPlugins.schema.json": "1fd3f8440cfb6e4422553a7c4b5f091eb3e10a40da0d43e8b977a18929116758",
|
|
301
303
|
"spec/v2/facets/webhooks.schema.json": "54b576f1d150e28e1642693d846ce471c118e9c521bdf3924185305fbd1d0de4",
|
|
302
304
|
"spec/v2/facets/workflowChainPacks.schema.json": "ea625ce818fbeb746478f00f31f96ee9613d538e4409c258c1bfb517acdeaba9",
|
|
303
|
-
"spec/v2/generated/error-codes.md": "
|
|
305
|
+
"spec/v2/generated/error-codes.md": "eafb66f1632da783c9a0347a416ae32bcf322f41b9f8783375f579791e9cdcf0",
|
|
304
306
|
"spec/v2/harness-trust-anchors.json": "6007b92eae16a070fbc3777424efb0abfe865fda89d096963109b96282b91205",
|
|
305
307
|
"spec/v2/harness-trust-anchors.schema.json": "372b5d92337c893d3449eff3f6bfd009ac09fdac285296d8bdbe0a08fff9e34c",
|
|
306
308
|
"spec/v2/id-field-bindings.json": "aa3e7c3590a490a77286e3ae60c1ab7407e7d627e05f981c3e08f221af4bd5dc",
|
|
@@ -308,12 +310,12 @@
|
|
|
308
310
|
"spec/v2/interop-map.schema.json": "0017c882df34ee215e2e0729de051490e09d43aac3d2c949285698ed51832f12",
|
|
309
311
|
"spec/v2/migrations.json": "651954938a1e154c0b6f1e3dd6e105889d30de3bb65789f21de7f77a12869e9e",
|
|
310
312
|
"spec/v2/migrations.schema.json": "b91ade6320ad1b1185c9495b9424f3aa63438ed8e412c45171c227d82c0afd86",
|
|
311
|
-
"spec/v2/path-manifest.json": "
|
|
313
|
+
"spec/v2/path-manifest.json": "50fc251f296b06ec5fad35724f5c63b1a32e0e8fc222a85cd29b785cebdb9c98",
|
|
312
314
|
"spec/v2/peer-dependency-aliases.json": "d10299280abee08258502925bc327293ee413e0108cd6e6ec75ff6110653308d",
|
|
313
315
|
"spec/v2/profiles.json": "180beb9ef2d161766fa23b2987e7e829851e3d316273147cf6a4a3f324befd6b",
|
|
314
|
-
"spec/v2/release.json": "
|
|
316
|
+
"spec/v2/release.json": "f24ac113647fb96986e03c1355ae9b741ba447e6e1ef5653f955f07e777c93dd",
|
|
315
317
|
"spec/v2/retention-floors.json": "eaf3722d95c79947af1d4269ef85117e126518c588cfcf1a2b21b97269f51624",
|
|
316
|
-
"spec/v2/surface-baseline.json": "
|
|
318
|
+
"spec/v2/surface-baseline.json": "a2f690b3222532cc625d94f195d84d4b62a5b3e2b7ab02c164297e0483c32a9d"
|
|
317
319
|
},
|
|
318
|
-
"corpusCommit": "
|
|
320
|
+
"corpusCommit": "cc5dd653ba3aba746ed1b3fc8086df84803dbf46"
|
|
319
321
|
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The council roster (RFC 0239; `spec/v2/core/conversation.md`
|
|
3
|
+
* §multiPartyConversation), observed through normative operations only: run
|
|
4
|
+
* creation, the run snapshot and event poll, and `resolveInterrupt`.
|
|
5
|
+
*
|
|
6
|
+
* Each leg observes through the suite driver and returns findings or the
|
|
7
|
+
* reason it could not run, so the scenario and the self-test double run the
|
|
8
|
+
* same code. The `judge*` functions are pure.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { driver } from './driver.js';
|
|
12
|
+
import { v2Discovery } from './v2.js';
|
|
13
|
+
import { readErrorCode } from './error-envelope.js';
|
|
14
|
+
import { majorProfile } from './major-profile.js';
|
|
15
|
+
import { readEvents, readRun, type Ev } from './fixture-run-observer.js';
|
|
16
|
+
|
|
17
|
+
export const COUNCIL = 'conformance-multi-party-council';
|
|
18
|
+
export const OVERSIZE = 'conformance-multi-party-council-oversize';
|
|
19
|
+
export const OVERSIZE_COUNT = 64;
|
|
20
|
+
export const GATE_NODE = 'convo';
|
|
21
|
+
export const ROSTER = ['host:conformance-council-a', 'host:conformance-council-b', 'host:conformance-council-c'] as const;
|
|
22
|
+
export const INTRUDER = 'host:conformance-intruder';
|
|
23
|
+
|
|
24
|
+
export type Skip = { readonly kind: 'skip'; readonly skip: 'blocked' | 'inapplicable'; readonly reason: string };
|
|
25
|
+
export type LegOutcome = { readonly kind: 'observed'; readonly findings: string[] } | Skip;
|
|
26
|
+
const skip = (s: 'blocked' | 'inapplicable', reason: string): Skip => ({ kind: 'skip', skip: s, reason });
|
|
27
|
+
const P = majorProfile(2);
|
|
28
|
+
const WAITING = new Set(['waiting-input', 'waiting-approval', 'waiting-external', 'paused']);
|
|
29
|
+
const TERMINAL = new Set(['completed', 'failed', 'cancelled']);
|
|
30
|
+
|
|
31
|
+
const isRecord = (v: unknown): v is Record<string, unknown> => typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
32
|
+
|
|
33
|
+
/** `conversation.opened.participants` agent ids, in order. */
|
|
34
|
+
export function rosterOf(events: readonly Ev[]): string[] | null {
|
|
35
|
+
const opened = events.find((e) => e.type === 'conversation.opened');
|
|
36
|
+
if (!opened) return null;
|
|
37
|
+
const p = opened.payload['participants'];
|
|
38
|
+
return Array.isArray(p) ? p.map((x) => (isRecord(x) ? String(x['agentId']) : String(x))) : [];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** §A.2 — the configured roster is carried unchanged. */
|
|
42
|
+
export function judgeRoster(events: readonly Ev[]): string[] {
|
|
43
|
+
const got = rosterOf(events);
|
|
44
|
+
if (got === null) return ['no conversation.opened event was emitted'];
|
|
45
|
+
return JSON.stringify(got) === JSON.stringify([...ROSTER]) ? [] : [`conversation.opened.participants MUST carry the configured roster ${JSON.stringify(ROSTER)}; got ${JSON.stringify(got)}`];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** §B.2–§B.3 — the non-member turn is refused with its code, unconsumed. */
|
|
49
|
+
export function judgeRefusal(status: number, body: unknown, statusAfter: string | null, exchangedAfter: number): string[] {
|
|
50
|
+
const out: string[] = [];
|
|
51
|
+
const code = readErrorCode(body);
|
|
52
|
+
if (status !== 422 || code !== 'conversation_speaker_not_participant') out.push(`a turn from ${INTRUDER} MUST be refused 422 conversation_speaker_not_participant; got ${status} ${String(code)}`);
|
|
53
|
+
const speaker = isRecord(body) && isRecord(body['details']) ? body['details']['speakerId'] : undefined;
|
|
54
|
+
if (status === 422 && speaker !== INTRUDER) out.push(`details.speakerId MUST name the refused speaker ${INTRUDER}; got ${String(speaker)}`);
|
|
55
|
+
if (statusAfter === null || !WAITING.has(statusAfter)) out.push(`the refused turn MUST NOT be consumed: the run MUST still wait on the interrupt; it is ${String(statusAfter)}`);
|
|
56
|
+
if (exchangedAfter > 0) out.push(`a refused turn MUST NOT emit conversation.exchanged; ${exchangedAfter} were emitted`);
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** The control — a member's turn still resolves the interrupt. */
|
|
61
|
+
export function judgeMemberAccepted(status: number, exchanged: number): string[] {
|
|
62
|
+
if (status >= 200 && status < 300 && exchanged > 0) return [];
|
|
63
|
+
return [`after the refusal, a turn from ${ROSTER[0]} MUST resolve the interrupt and emit conversation.exchanged; got ${status} with ${exchanged} conversation.exchanged`];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** §C — the oversized roster is refused at creation with its code. */
|
|
67
|
+
export function judgeOversize(status: number, body: unknown, max: number): string[] {
|
|
68
|
+
const code = readErrorCode(body);
|
|
69
|
+
const out: string[] = [];
|
|
70
|
+
if (status !== 422 || code !== 'conversation_roster_exceeded') out.push(`a ${OVERSIZE_COUNT}-member roster over maxParticipants ${max} MUST be refused 422 conversation_roster_exceeded at run creation; got ${status} ${String(code)}`);
|
|
71
|
+
const d = isRecord(body) && isRecord(body['details']) ? body['details']['maxParticipants'] : undefined;
|
|
72
|
+
if (status === 422 && d !== max) out.push(`details.maxParticipants MUST be the advertised ${max}; got ${String(d)}`);
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ------------------------------------------------------------------ the legs
|
|
77
|
+
|
|
78
|
+
async function gate(fixture: string): Promise<{ family: Record<string, unknown> } | Skip> {
|
|
79
|
+
let doc: Record<string, unknown> | null = null;
|
|
80
|
+
try { doc = await v2Discovery(); } catch { doc = null; }
|
|
81
|
+
if (!doc) return skip('blocked', 'v2 discovery unreachable');
|
|
82
|
+
const family = doc['multiPartyConversation'];
|
|
83
|
+
if (!isRecord(family)) return skip('inapplicable', 'the host does not advertise multiPartyConversation');
|
|
84
|
+
const fixtures = Array.isArray(doc['fixtures']) ? (doc['fixtures'] as unknown[]) : [];
|
|
85
|
+
if (!fixtures.includes(fixture)) return skip('inapplicable', `the host does not advertise the ${fixture} fixture (RFC 0239 §E; RFC 0003)`);
|
|
86
|
+
return { family };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function waitRun(runId: string, timeoutMs = 15_000): Promise<string | null> {
|
|
90
|
+
const deadline = Date.now() + timeoutMs;
|
|
91
|
+
for (;;) {
|
|
92
|
+
const r = await readRun(P, runId);
|
|
93
|
+
const s = typeof r.snapshot['status'] === 'string' ? (r.snapshot['status'] as string) : null;
|
|
94
|
+
if ((s && (WAITING.has(s) || TERMINAL.has(s))) || Date.now() > deadline) return s;
|
|
95
|
+
await new Promise((res) => setTimeout(res, 250));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function openCouncil(): Promise<{ runId: string } | Skip> {
|
|
100
|
+
const g = await gate(COUNCIL);
|
|
101
|
+
if ('kind' in g) return g;
|
|
102
|
+
const created = await driver.post(P.runsPath, { workflowId: COUNCIL });
|
|
103
|
+
const runId = isRecord(created.json) ? created.json['runId'] : undefined;
|
|
104
|
+
if (created.status !== 201 || typeof runId !== 'string') return skip('blocked', `POST /runs for the advertised ${COUNCIL} answered ${created.status} ${String(readErrorCode(created.json) ?? '')}`.trim());
|
|
105
|
+
const s = await waitRun(runId);
|
|
106
|
+
if (s === null || !WAITING.has(s)) return skip('blocked', `the council run did not suspend on its conversation.exchange interrupt (status ${String(s)})`);
|
|
107
|
+
return { runId };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const resolve = (runId: string, speakerId: string) =>
|
|
111
|
+
driver.post(`${P.runsPath}/${P.idSegment(runId)}/interrupts/${encodeURIComponent(GATE_NODE)}`, { resumeValue: { role: 'agent', speakerId, content: 'openwop-conformance turn' } });
|
|
112
|
+
const exchangedCount = async (runId: string): Promise<number> => (await readEvents(P, runId)).filter((e) => e.type === 'conversation.exchanged').length;
|
|
113
|
+
|
|
114
|
+
/** §A.2, `openwop.requirement.0239.council.roster-carried`. */
|
|
115
|
+
export async function rosterLeg(): Promise<LegOutcome> {
|
|
116
|
+
const o = await openCouncil();
|
|
117
|
+
if ('kind' in o) return o;
|
|
118
|
+
return { kind: 'observed', findings: judgeRoster(await readEvents(P, o.runId)) };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** §B, `openwop.requirement.0239.council.speaker-refused`. */
|
|
122
|
+
export async function speakerLeg(): Promise<LegOutcome> {
|
|
123
|
+
const o = await openCouncil();
|
|
124
|
+
if ('kind' in o) return o;
|
|
125
|
+
const refused = await resolve(o.runId, INTRUDER);
|
|
126
|
+
const after = await readRun(P, o.runId);
|
|
127
|
+
const statusAfter = typeof after.snapshot['status'] === 'string' ? (after.snapshot['status'] as string) : null;
|
|
128
|
+
const findings = judgeRefusal(refused.status, refused.json, statusAfter, await exchangedCount(o.runId));
|
|
129
|
+
const member = await resolve(o.runId, ROSTER[0]);
|
|
130
|
+
await waitRun(o.runId, 5_000);
|
|
131
|
+
findings.push(...judgeMemberAccepted(member.status, await exchangedCount(o.runId)));
|
|
132
|
+
return { kind: 'observed', findings };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** §C, `openwop.requirement.0239.council.roster-exceeded`. */
|
|
136
|
+
export async function oversizeLeg(): Promise<LegOutcome> {
|
|
137
|
+
const g = await gate(OVERSIZE);
|
|
138
|
+
if ('kind' in g) return g;
|
|
139
|
+
const max = g.family['maxParticipants'];
|
|
140
|
+
if (typeof max !== 'number') return skip('inapplicable', 'the host advertises no multiPartyConversation.maxParticipants, so no roster is oversized');
|
|
141
|
+
if (max >= OVERSIZE_COUNT) return skip('inapplicable', `maxParticipants is ${max}; the ${OVERSIZE_COUNT}-member fixture cannot exceed it (RFC 0239 §Decisions 4, gap G2)`);
|
|
142
|
+
const created = await driver.post(P.runsPath, { workflowId: OVERSIZE });
|
|
143
|
+
return { kind: 'observed', findings: judgeOversize(created.status, created.json, max) };
|
|
144
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The front-end plugin boundary (RFC 0238; `spec/v2/core/packs.md` §Front-end
|
|
3
|
+
* plugin packs), observed through the two `uiPlugins.served` operations against
|
|
4
|
+
* the operator-installed `ui-plugin-pack-narrow` fixture.
|
|
5
|
+
*
|
|
6
|
+
* Two halves, as in the other shared witnesses: `observe*` assert nothing and
|
|
7
|
+
* return what the host answered (or why it could not be read); the `judge*`
|
|
8
|
+
* functions are pure and return findings. The scenario maps findings to
|
|
9
|
+
* requirement ids.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { findCanaryLeaks } from './canaries.js';
|
|
13
|
+
import { driver, type OpenWOPResponse } from './driver.js';
|
|
14
|
+
import { v2Discovery } from './v2.js';
|
|
15
|
+
|
|
16
|
+
export const FIXTURE_PACK = 'core.openwop.conformance.ui-plugin-narrow';
|
|
17
|
+
export const FIXTURE_PLUGIN = 'narrow';
|
|
18
|
+
export const FIXTURE_ARTIFACT = 'ui-plugin-narrow-artifact';
|
|
19
|
+
|
|
20
|
+
export const framePath = (pack = FIXTURE_PACK, plugin = FIXTURE_PLUGIN): string =>
|
|
21
|
+
`/host/ui-plugins/${encodeURIComponent(pack)}/${encodeURIComponent(plugin)}/frame`;
|
|
22
|
+
export const rpcPath = (pack = FIXTURE_PACK, plugin = FIXTURE_PLUGIN): string =>
|
|
23
|
+
`/host/ui-plugins/${encodeURIComponent(pack)}/${encodeURIComponent(plugin)}/rpc`;
|
|
24
|
+
|
|
25
|
+
/** A Content-Security-Policy header as directive → source/token list (names lower-cased). */
|
|
26
|
+
export function parseCsp(header: string | null): Map<string, string[]> {
|
|
27
|
+
const out = new Map<string, string[]>();
|
|
28
|
+
for (const part of (header ?? '').split(';')) {
|
|
29
|
+
const [name, ...tokens] = part.trim().split(/\s+/).filter(Boolean);
|
|
30
|
+
if (name && !out.has(name.toLowerCase())) out.set(name.toLowerCase(), tokens);
|
|
31
|
+
}
|
|
32
|
+
return out;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** §B.3 — the `sandbox` directive admits `allow-scripts` and never `allow-same-origin`. */
|
|
36
|
+
export function judgeIsolation(csp: string | null): string[] {
|
|
37
|
+
const p = parseCsp(csp);
|
|
38
|
+
const sandbox = p.get('sandbox');
|
|
39
|
+
if (sandbox === undefined) return [`the frame's Content-Security-Policy carries no sandbox directive (got ${JSON.stringify(csp)})`];
|
|
40
|
+
const out: string[] = [];
|
|
41
|
+
const tokens = sandbox.map((t) => t.toLowerCase());
|
|
42
|
+
if (tokens.includes('allow-same-origin')) out.push('the sandbox directive admits allow-same-origin, so the plugin shares the host origin');
|
|
43
|
+
if (!tokens.includes('allow-scripts')) out.push('the sandbox directive does not admit allow-scripts, so the plugin cannot run');
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** §B.4 — `default-src 'none'`, and no `connect-src` source beyond the declared `connectSrc`. */
|
|
48
|
+
export function judgeEgress(csp: string | null, declared: readonly string[] = []): string[] {
|
|
49
|
+
const p = parseCsp(csp);
|
|
50
|
+
const out: string[] = [];
|
|
51
|
+
const def = p.get('default-src');
|
|
52
|
+
if (def === undefined || def.length !== 1 || def[0] !== "'none'") out.push(`default-src MUST be 'none' (got ${def === undefined ? 'no default-src' : def.join(' ')})`);
|
|
53
|
+
const connect = (p.get('connect-src') ?? []).filter((s) => s !== "'none'");
|
|
54
|
+
const extra = connect.filter((s) => !declared.includes(s));
|
|
55
|
+
if (extra.length) out.push(`connect-src admits ${extra.join(' ')}, which the plugin did not declare`);
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface RpcReply { readonly status: number; readonly json: unknown; readonly text: string }
|
|
60
|
+
|
|
61
|
+
/** One `ui-plugin/1` request envelope. */
|
|
62
|
+
export const request = (id: number, method: string, params?: Record<string, unknown>): Record<string, unknown> =>
|
|
63
|
+
({ openwop: 'ui-plugin/1', type: 'request', id, method, ...(params ? { params } : {}) });
|
|
64
|
+
|
|
65
|
+
const field = (o: unknown, k: string): unknown => (o && typeof o === 'object' ? (o as Record<string, unknown>)[k] : undefined);
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* §C.4 — a method the plugin did not declare (or the host does not allow) is
|
|
69
|
+
* refused inside a `200` with `method_not_allowed`, never executed.
|
|
70
|
+
*/
|
|
71
|
+
export function judgeRefused(method: string, r: RpcReply): string[] {
|
|
72
|
+
const code = field(field(r.json, 'error'), 'code');
|
|
73
|
+
if (r.status === 200 && field(r.json, 'ok') === false && code === 'method_not_allowed') return [];
|
|
74
|
+
return [`${method}: a method outside the allowlists MUST return 200 with ok:false and method_not_allowed; got ${r.status} ${JSON.stringify(r.json)?.slice(0, 200)}`];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** §C.2 — a method outside the closed `ui-plugin/1` enum fails validation: `400`, never executed. */
|
|
78
|
+
export function judgeInvalid(method: string, r: RpcReply): string[] {
|
|
79
|
+
if (r.status === 400) return [];
|
|
80
|
+
if (r.status === 200 && field(r.json, 'ok') === false) return [];
|
|
81
|
+
return [`${method}: a request outside the ui-plugin/1 method enum MUST NOT execute; got ${r.status} ${JSON.stringify(r.json)?.slice(0, 200)}`];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** The control: the one declared method succeeds on the fixture artifact. */
|
|
85
|
+
export function judgeControl(r: RpcReply): string[] {
|
|
86
|
+
if (r.status === 200 && field(r.json, 'ok') === true) return [];
|
|
87
|
+
return [`artifact.read of ${FIXTURE_ARTIFACT}, the plugin's one declared method, MUST succeed; got ${r.status} ${JSON.stringify(r.json)?.slice(0, 200)}`];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** §C.6 — no response carries the BYOK canary. */
|
|
91
|
+
export function judgeNoCanary(replies: readonly RpcReply[]): string[] {
|
|
92
|
+
const out: string[] = [];
|
|
93
|
+
for (const r of replies) for (const leak of findCanaryLeaks(r.text)) out.push(`a dispatch response carries canary material (${leak.label})`);
|
|
94
|
+
return out;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ------------------------------------------------------------------ the legs
|
|
98
|
+
// Each leg observes through the suite driver and returns findings or the reason it
|
|
99
|
+
// could not run, so the scenario and the self-test double run the same code.
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
export type Skip = { readonly kind: 'skip'; readonly skip: 'blocked' | 'inapplicable'; readonly reason: string };
|
|
103
|
+
export type LegOutcome = { readonly kind: 'observed'; readonly findings: string[] } | Skip;
|
|
104
|
+
|
|
105
|
+
const skip = (s: 'blocked' | 'inapplicable', reason: string): Skip => ({ kind: 'skip', skip: s, reason });
|
|
106
|
+
const NOT_INSTALLED = "the ui-plugin-pack-narrow fixture is not installed (404); installing it is the operator's choice (RFC 0238 §D)";
|
|
107
|
+
|
|
108
|
+
async function http(fn: () => Promise<OpenWOPResponse>): Promise<OpenWOPResponse | null> { try { return await fn(); } catch { return null; } }
|
|
109
|
+
|
|
110
|
+
async function gate(): Promise<{ isolation: string } | Skip> {
|
|
111
|
+
let doc: Record<string, unknown> | null = null;
|
|
112
|
+
try { doc = await v2Discovery(); } catch { doc = null; }
|
|
113
|
+
if (!doc) return skip('blocked', 'v2 discovery unreachable');
|
|
114
|
+
const fam = doc['uiPlugins'] as { served?: unknown; isolation?: unknown } | undefined;
|
|
115
|
+
if (!fam || fam.served !== true) return skip('inapplicable', 'the host does not advertise uiPlugins.served');
|
|
116
|
+
return { isolation: typeof fam.isolation === 'string' ? fam.isolation : 'cross-origin-iframe' };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function frameCsp(): Promise<{ csp: string | null } | Skip> {
|
|
120
|
+
const r = await http(() => driver.get(framePath()));
|
|
121
|
+
if (r === null) return skip('blocked', 'GET …/frame could not be made');
|
|
122
|
+
if (r.status === 404) return skip('inapplicable', NOT_INSTALLED);
|
|
123
|
+
if (r.status !== 200) return skip('blocked', `GET …/frame answered ${r.status}`);
|
|
124
|
+
return { csp: r.headers.get('content-security-policy') };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function rpc(id: number, method: string, params?: Record<string, unknown>): Promise<RpcReply | null> {
|
|
128
|
+
const r = await http(() => driver.post(rpcPath(), { message: request(id, method, params) }));
|
|
129
|
+
return r === null ? null : { status: r.status, json: r.json, text: r.text };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** §B.3, `openwop.requirement.0238.ui-plugin.frame-isolated`. */
|
|
133
|
+
export async function isolationLeg(): Promise<LegOutcome> {
|
|
134
|
+
const g = await gate();
|
|
135
|
+
if ('kind' in g) return g;
|
|
136
|
+
if (g.isolation !== 'cross-origin-iframe') return skip('inapplicable', `uiPlugins.isolation is ${g.isolation}: only a cross-origin-iframe boundary is visible over HTTP (RFC 0238 §Decisions 3)`);
|
|
137
|
+
const f = await frameCsp();
|
|
138
|
+
return 'kind' in f ? f : { kind: 'observed', findings: judgeIsolation(f.csp) };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** §B.4, `openwop.requirement.0238.ui-plugin.frame-deny-egress`. */
|
|
142
|
+
export async function egressLeg(): Promise<LegOutcome> {
|
|
143
|
+
const g = await gate();
|
|
144
|
+
if ('kind' in g) return g;
|
|
145
|
+
const f = await frameCsp();
|
|
146
|
+
return 'kind' in f ? f : { kind: 'observed', findings: judgeEgress(f.csp) };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** §C.2/§C.4, `openwop.requirement.0238.ui-plugin.rpc-allowlist`. */
|
|
150
|
+
export async function allowlistLeg(): Promise<LegOutcome> {
|
|
151
|
+
const g = await gate();
|
|
152
|
+
if ('kind' in g) return g;
|
|
153
|
+
const control = await rpc(1, 'artifact.read', { artifactId: FIXTURE_ARTIFACT });
|
|
154
|
+
if (control === null) return skip('blocked', 'POST …/rpc could not be made');
|
|
155
|
+
if (control.status === 404) return skip('inapplicable', NOT_INSTALLED);
|
|
156
|
+
const findings = [...judgeControl(control)];
|
|
157
|
+
for (const [id, method, params] of [[2, 'artifact.write', { artifactId: FIXTURE_ARTIFACT, content: 'openwop-conformance' }], [3, 'host.navigate', { to: '/' }]] as const) {
|
|
158
|
+
const r = await rpc(id, method, params);
|
|
159
|
+
if (r === null) return skip('blocked', `the ${method} dispatch could not be made`);
|
|
160
|
+
findings.push(...judgeRefused(method, r));
|
|
161
|
+
}
|
|
162
|
+
const exec = await rpc(4, 'host.exec');
|
|
163
|
+
if (exec === null) return skip('blocked', 'the host.exec dispatch could not be made');
|
|
164
|
+
findings.push(...judgeInvalid('host.exec', exec));
|
|
165
|
+
return { kind: 'observed', findings };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** §C.6, `openwop.requirement.0238.ui-plugin.no-byok`. */
|
|
169
|
+
export async function noByokLeg(): Promise<LegOutcome> {
|
|
170
|
+
const g = await gate();
|
|
171
|
+
if ('kind' in g) return g;
|
|
172
|
+
const replies: RpcReply[] = [];
|
|
173
|
+
for (const [id, method] of [[1, 'artifact.read'], [2, 'artifact.write']] as const) {
|
|
174
|
+
const r = await rpc(id, method, { artifactId: FIXTURE_ARTIFACT });
|
|
175
|
+
if (r === null) return skip('blocked', `the ${method} dispatch could not be made`);
|
|
176
|
+
if (r.status === 404) return skip('inapplicable', NOT_INSTALLED);
|
|
177
|
+
replies.push(r);
|
|
178
|
+
}
|
|
179
|
+
return { kind: 'observed', findings: judgeNoCanary(replies) };
|
|
180
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The WASM memory ceiling (`spec/v2/core/node-pack-runtimes.md` §WASM; RFC 0008
|
|
3
|
+
* §K), observed through run creation and the event poll against the
|
|
4
|
+
* operator-installed `vendor.openwop.misbehaving` fixture pack
|
|
5
|
+
* (`conformance/fixtures/wasm-packs/misbehaving-memory/`), whose one node grows
|
|
6
|
+
* linear memory until the host refuses.
|
|
7
|
+
*
|
|
8
|
+
* `memoryCapLeg` observes and returns findings or the reason it could not run;
|
|
9
|
+
* `judgeMemoryCap` is pure. The scenario and the self-test double run the same code.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { v2Discovery } from './v2.js';
|
|
13
|
+
import { majorProfile } from './major-profile.js';
|
|
14
|
+
import { observeRun, type ObservedRun } from './fixture-run-observer.js';
|
|
15
|
+
|
|
16
|
+
export const MEMORY_CAP_FIXTURE = 'conformance-wasm-pack-memory-cap-breach';
|
|
17
|
+
export const BOMB_TYPE_ID = 'vendor.openwop.misbehaving.memory-bomb';
|
|
18
|
+
|
|
19
|
+
export type Skip = { readonly kind: 'skip'; readonly skip: 'blocked' | 'inapplicable'; readonly reason: string };
|
|
20
|
+
export type LegOutcome = { readonly kind: 'observed'; readonly findings: string[] } | Skip;
|
|
21
|
+
const skip = (s: 'blocked' | 'inapplicable', reason: string): Skip => ({ kind: 'skip', skip: s, reason });
|
|
22
|
+
const isRecord = (v: unknown): v is Record<string, unknown> => typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
23
|
+
|
|
24
|
+
/** A breach emits `cap.breached` `kind: wasm-memory` and the run ends `failed`. */
|
|
25
|
+
export function judgeMemoryCap(o: ObservedRun): string[] {
|
|
26
|
+
const out: string[] = [];
|
|
27
|
+
if (o.createStatus !== 201 && o.createStatus !== 202) return [`createRun of ${MEMORY_CAP_FIXTURE} MUST be accepted (got ${o.createStatus})`];
|
|
28
|
+
const breaches = o.events.filter((e) => e.type === 'cap.breached' && e.payload['kind'] === 'wasm-memory');
|
|
29
|
+
if (breaches.length === 0) out.push(`a module exceeding maxMemoryBytes MUST produce cap.breached with kind wasm-memory; the log has ${o.events.filter((e) => e.type === 'cap.breached').map((e) => String(e.payload['kind'])).join(', ') || 'no cap.breached'}`);
|
|
30
|
+
if (o.terminalStatus !== 'failed') out.push(`the run MUST end failed after the breach (got ${String(o.terminalStatus)})`);
|
|
31
|
+
return out;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** node-pack-runtimes.md §WASM bullet 2 — `openwop.requirement.node-pack-runtimes.wasm-memory-cap`. */
|
|
35
|
+
export async function memoryCapLeg(): Promise<LegOutcome> {
|
|
36
|
+
let doc: Record<string, unknown> | null = null;
|
|
37
|
+
try { doc = await v2Discovery(); } catch { doc = null; }
|
|
38
|
+
if (!doc) return skip('blocked', 'v2 discovery unreachable');
|
|
39
|
+
const fam = doc['nodePackRuntimes'];
|
|
40
|
+
const wasm = isRecord(fam) ? fam['wasm'] : undefined;
|
|
41
|
+
if (!isRecord(wasm)) return skip('inapplicable', 'the host does not advertise nodePackRuntimes.wasm');
|
|
42
|
+
if (typeof wasm['maxMemoryBytes'] !== 'number') return skip('inapplicable', 'the host advertises no nodePackRuntimes.wasm.maxMemoryBytes; the enforcement MUST binds only when it does');
|
|
43
|
+
const fixtures = Array.isArray(doc['fixtures']) ? (doc['fixtures'] as unknown[]) : [];
|
|
44
|
+
if (!fixtures.includes(MEMORY_CAP_FIXTURE)) return skip('inapplicable', `the host does not advertise ${MEMORY_CAP_FIXTURE}; the operator installs conformance/fixtures/wasm-packs/misbehaving-memory/ to run it`);
|
|
45
|
+
return { kind: 'observed', findings: judgeMemoryCap(await observeRun(majorProfile(2), MEMORY_CAP_FIXTURE, { timeoutMs: 30_000 })) };
|
|
46
|
+
}
|
|
@@ -38,6 +38,11 @@
|
|
|
38
38
|
* harness (`conformance/run.ts`) overwrites `OPENWOP_API_KEYS` with a `:*` key, so
|
|
39
39
|
* moving the tenant needs that line edited, not just the env var.
|
|
40
40
|
*
|
|
41
|
+
* Since 2.45.23 the leg also mints one run under `OPENWOP_TEST_TENANT_B_API_KEY`
|
|
42
|
+
* when it is set, so a host can bind tenant B to a non-clean tenant and make the
|
|
43
|
+
* leg bite in a production cut without moving the primary key (asked by
|
|
44
|
+
* openwop-app, whose primary production tenant is clean).
|
|
45
|
+
*
|
|
41
46
|
* The codec's own edge cases (marker escaping, UTF-8 vs UTF-16, malformed
|
|
42
47
|
* decode) are a unit concern and live in `src/lib/bound-id.test.ts`, which is
|
|
43
48
|
* sabotage-checked. This file asserts only what needs a HOST to answer.
|
|
@@ -166,5 +171,20 @@ describe('v2 bound-id path projection (identity.md §5)', () => {
|
|
|
166
171
|
runId.includes('~'),
|
|
167
172
|
req(ID_MINT, DOC, `a host MUST NOT mint a tenant-bound id containing ~ (RFC 0184 §A.2) — the created runId is ${runId}. A literal ~ is ambiguous with the path projection's escape marker; map the tenant key to a clean segment instead of projecting it`),
|
|
168
173
|
).toBe(false);
|
|
174
|
+
|
|
175
|
+
// The second-tenant credential, when configured, mints one more run, so a
|
|
176
|
+
// host can put tenant B in a non-clean tenant without moving the primary key
|
|
177
|
+
// every other scenario depends on. A refused create here skips only this half.
|
|
178
|
+
const other = process.env['OPENWOP_TEST_TENANT_B_API_KEY']?.trim();
|
|
179
|
+
if (other && other !== process.env['OPENWOP_API_KEY']?.trim()) {
|
|
180
|
+
const createdB = await http(() => driver.post('/runs', { workflowId: NOOP_WORKFLOW_ID }, { headers: { Authorization: `Bearer ${other}` } }));
|
|
181
|
+
const runIdB = (createdB?.json as { runId?: unknown } | null | undefined)?.runId;
|
|
182
|
+
if (createdB?.status === 201 && typeof runIdB === 'string') {
|
|
183
|
+
expect(
|
|
184
|
+
runIdB.includes('~'),
|
|
185
|
+
req(ID_MINT, DOC, `a host MUST NOT mint a tenant-bound id containing ~ (RFC 0184 §A.2) — the runId created under the second-tenant credential is ${runIdB}`),
|
|
186
|
+
).toBe(false);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
169
189
|
});
|
|
170
190
|
});
|
|
@@ -37,4 +37,12 @@ describe('v2 envelopeContracts advertisement (events.md §Envelope contracts)',
|
|
|
37
37
|
if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
|
|
38
38
|
for (const x of out.findings) expect(x.ok, req(ID_RECORD, x.doc, x.message)).toBe(true);
|
|
39
39
|
});
|
|
40
|
+
|
|
41
|
+
it('envelopeContracts.advertised is not false: a host that does not offer the family omits it', async () => {
|
|
42
|
+
const doc = await discovery();
|
|
43
|
+
if (!doc) return softSkip('blocked', UNREADABLE);
|
|
44
|
+
const rec = await familyAdvertised('envelopeContracts');
|
|
45
|
+
if (!rec) return softSkip('inapplicable', 'the host does not advertise envelopeContracts');
|
|
46
|
+
expect(rec['advertised'], req('openwop.requirement.0237.no-false-advertisement', 'RFC 0237 §C; capabilities.md §2', 'envelopeContracts.advertised: false is not a v2 state; a host that does not offer the family MUST omit it')).not.toBe(false);
|
|
47
|
+
});
|
|
40
48
|
});
|