@openwop/openwop-conformance 2.45.24 → 2.45.26
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 +13 -0
- package/README.md +3 -3
- package/coverage.md +1 -1
- package/dist/spec-artifacts.lock.json +2 -2
- package/fixtures/conformance-wasm-pack-abi-mismatch.json +41 -0
- package/fixtures.md +2 -1
- package/package.json +2 -2
- package/requirements.json +208 -187
- package/scenario-majors.json +5 -2
- package/schemas/CORPUS-STAMP.json +15 -15
- package/src/lib/host-event-witness.ts +47 -1
- package/src/lib/wasm-abi-reject-witness.ts +66 -0
- package/src/scenarios/frontend-plugin-packs.test.ts +10 -167
- package/src/scenarios/v2-host-event-delivery.test.ts +82 -36
- package/src/scenarios/v2-wasm-abi-reject.test.ts +35 -0
package/scenario-majors.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$comment": "GENERATED by conformance/scripts/generate-scenario-majors.mjs (RFC 0168 §D.3). Do not edit; add a file to BOTH_MAJORS in the generator to target both majors.",
|
|
3
3
|
"counts": {
|
|
4
|
-
"files":
|
|
4
|
+
"files": 650,
|
|
5
5
|
"v1": 455,
|
|
6
|
-
"v2":
|
|
6
|
+
"v2": 210
|
|
7
7
|
},
|
|
8
8
|
"majors": {
|
|
9
9
|
"a2a-1-0-agent-card.test.ts": [
|
|
@@ -1818,6 +1818,9 @@
|
|
|
1818
1818
|
"v2-version-header-honored.test.ts": [
|
|
1819
1819
|
2
|
|
1820
1820
|
],
|
|
1821
|
+
"v2-wasm-abi-reject.test.ts": [
|
|
1822
|
+
2
|
|
1823
|
+
],
|
|
1821
1824
|
"v2-wasm-memory-cap.test.ts": [
|
|
1822
1825
|
2
|
|
1823
1826
|
],
|
|
@@ -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.26",
|
|
5
|
+
"corpusTag": "v2.45.26",
|
|
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": "d3b59eb1f87f42ba118bdbbdfb74e6f499081d8d5ed5fba3b06d5ae561bc90bd",
|
|
13
|
+
"api/v2/asyncapi.yaml": "dc2ed7cd47e0ca326e89badc1f0b0c3a64b2030859559786c711230e5e7474f1",
|
|
14
|
+
"api/v2/openapi.yaml": "3fbb8672105a847d457e13cde09bf65b02b02163cf1082f08b3cec68088a416a",
|
|
15
15
|
"api/v2/redocly.yaml": "1e66b60e6118ad11a823bb620678be464d99dfe50a40e3e6f93ec9429b88b34c",
|
|
16
16
|
"schemas/README.md": "276fc1d51eb08e14e9127b9fd27761d7046ee33c12000f3a64a801322db8bf58",
|
|
17
17
|
"schemas/a2a-task-state.schema.json": "75d5049dea8bd873ff0e7546f1c60c8d36c219bec7084264360be54a8be30ae1",
|
|
@@ -118,7 +118,7 @@
|
|
|
118
118
|
"schemas/v2/audit-verify-result.schema.json": "14af8e9283510f0a40e48a2059c929ee44e0fc9fa3cc66a95bfbe90b17c48297",
|
|
119
119
|
"schemas/v2/budget-policy.schema.json": "1de68095db0beb5c7a4be629de26e79f62ebbe748a88d46f36e768edfd1f0d68",
|
|
120
120
|
"schemas/v2/capabilities.schema.json": "d859a413ff463ec39acc32a9cb76f2e8c7419e45683d6b7f886861765a9c36c7",
|
|
121
|
-
"schemas/v2/certification-bundle.schema.json": "
|
|
121
|
+
"schemas/v2/certification-bundle.schema.json": "93fcdfe54b1db168b702bfaf632b60c7a8a602be92c3fe48c81cf25408c2332b",
|
|
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",
|
|
@@ -152,7 +152,7 @@
|
|
|
152
152
|
"schemas/v2/goal.schema.json": "ad104ed5a3866736b8fed9d7c0b23dc82af526b5bef4afe848f01fc3af2161e9",
|
|
153
153
|
"schemas/v2/heartbeat-evaluated.schema.json": "c897b0792fd518d6e12879851027252af1529f5e420c3eaf434ad7af284ecc05",
|
|
154
154
|
"schemas/v2/heartbeat-state-changed.schema.json": "eb68d0082aa996650e3a6b8ccf32457f959cebc7e6728cc7c331b9d171a4bef6",
|
|
155
|
-
"schemas/v2/host-event.schema.json": "
|
|
155
|
+
"schemas/v2/host-event.schema.json": "655b5475b1aeb41c8fc4ff7b6fc7e31a0fcc18fd59f368336ccbc138e6ba395e",
|
|
156
156
|
"schemas/v2/ids.schema.json": "65cfaa193be0ca9cf48af39c18c553979e3ec1f7fc04509aaa513b322d7b7e4a",
|
|
157
157
|
"schemas/v2/localized-content-language-settings.schema.json": "3c8350a178037a460f5d87466ccaf9ed5fe3d41497786d2504cc66e42152c8cc",
|
|
158
158
|
"schemas/v2/localized-content-page-response.schema.json": "ef48329749758afb21e4d3cbc3f017abb911f75726819457f35b9f1390332c75",
|
|
@@ -208,14 +208,14 @@
|
|
|
208
208
|
"schemas/workspace-file.schema.json": "464de85c2a068243084ee9c1d969bc7cd5d8f7948574e58450d6493c38a0e1e4",
|
|
209
209
|
"spec/v1/alias-detectors.json": "22444c22bb88c568339ef512553f9fc38146e22b566d11ab8608f655d688d19c",
|
|
210
210
|
"spec/v1/capability-declaration-classes.json": "e7729aed5c4b4e1dd02abab0530f14cc95f5d4070fe51fb139e7f5cccefa00c6",
|
|
211
|
-
"spec/v1/core-standard-manifest.json": "
|
|
211
|
+
"spec/v1/core-standard-manifest.json": "cd739e168fa36dd811fd9ed1f506e0716c592e7a61c0d12164f82732ca366177",
|
|
212
212
|
"spec/v1/deprecations.json": "e723bc11d7272c71789ad1303bb7f21809a8a024abf57594f9d5c35dc199ece9",
|
|
213
213
|
"spec/v1/deprecations.schema.json": "3e393c405d2a41b467d8c5e3c468549078df1ce6a6d2588fc488097b95d9b55b",
|
|
214
214
|
"spec/v1/end-of-support.json": "35f0bf3b70fe78b16e2a2fd81a153fc27e703f6c16376c6fe7cdf1f4f2c4e6d4",
|
|
215
215
|
"spec/v1/event-codemap.json": "3da60d884157793a360da532a9fcbbfb5285636db325a74cec94b34622186d97",
|
|
216
216
|
"spec/v1/event-codemap.schema.json": "d05933b2e88103aff51a2f774df97b9bda065e0fc88dbd9b114cc47a32189174",
|
|
217
217
|
"spec/v1/extensions.json": "79a60754aa16cbdbdb604f8e5c00038af535a1690a4d07bd5ff7c38f0256a7bc",
|
|
218
|
-
"spec/v1/gaps.json": "
|
|
218
|
+
"spec/v1/gaps.json": "a0aec8f5ddf1002d46a461a9c7d930c8f0656a19b68bb1f52d183b97cc1962cf",
|
|
219
219
|
"spec/v1/gaps.schema.json": "8fd83259f556553c9df0f53e7a82ca8c2a4771d471197f69b4e2ae0ceeacfd66",
|
|
220
220
|
"spec/v1/migrations.json": "4c50760be70cb644d02427bcaf5749606700edf4513f423903cf3b1d12f5c300",
|
|
221
221
|
"spec/v1/migrations.schema.json": "886779aa6c22e646db097f5df210adb018a4dd14a7b815465a18c8a7056c8f72",
|
|
@@ -228,10 +228,10 @@
|
|
|
228
228
|
"spec/v2/core/connection-packs.md": "c80f95bd6a03698f35c72cdb2b46bd973f49203bbd786c26760e35fa86e6f23d",
|
|
229
229
|
"spec/v2/core/conversation.md": "8d6abedf6af408a74577753bfca24719d318e2632e10b45ff5d63bdcf31a8aaa",
|
|
230
230
|
"spec/v2/core/errors.md": "e48838701b86bbfe0ffee77b18a8ffc4e5f04724b4318507d9771da653f8da0f",
|
|
231
|
-
"spec/v2/core/events.md": "
|
|
231
|
+
"spec/v2/core/events.md": "d0f902edc080e9789e4fd05850a1264d905ebe5b06045b6a7a22486e22ef3091",
|
|
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": "f69fe81e4a3ad21879bb85948660c16860eb05bf152928c31aa4898f6d7d1887",
|
|
235
235
|
"spec/v2/core/host-services.md": "168a40935a03867ee82461b5d83d05578be25ee3e5af4eb080587b891231e242",
|
|
236
236
|
"spec/v2/core/i18n.md": "882904b7d59f992ab1e5f7346167161a54cf665b8fee47e64a3862cf9133a580",
|
|
237
237
|
"spec/v2/core/idempotency.md": "adbd317a6ebb57c2759c076297b31f67def9e391576bccd0bbea787b13e85872",
|
|
@@ -310,12 +310,12 @@
|
|
|
310
310
|
"spec/v2/interop-map.schema.json": "0017c882df34ee215e2e0729de051490e09d43aac3d2c949285698ed51832f12",
|
|
311
311
|
"spec/v2/migrations.json": "651954938a1e154c0b6f1e3dd6e105889d30de3bb65789f21de7f77a12869e9e",
|
|
312
312
|
"spec/v2/migrations.schema.json": "b91ade6320ad1b1185c9495b9424f3aa63438ed8e412c45171c227d82c0afd86",
|
|
313
|
-
"spec/v2/path-manifest.json": "
|
|
313
|
+
"spec/v2/path-manifest.json": "7b5679a4dad98c80d855361d88a98c8a4e045af15e310862a05b285b815dc9fc",
|
|
314
314
|
"spec/v2/peer-dependency-aliases.json": "d10299280abee08258502925bc327293ee413e0108cd6e6ec75ff6110653308d",
|
|
315
315
|
"spec/v2/profiles.json": "180beb9ef2d161766fa23b2987e7e829851e3d316273147cf6a4a3f324befd6b",
|
|
316
|
-
"spec/v2/release.json": "
|
|
316
|
+
"spec/v2/release.json": "7ce8ba7234bfe7eff4c767cc0aa4ddea0495fae08212fe98f63ae312cab06c74",
|
|
317
317
|
"spec/v2/retention-floors.json": "eaf3722d95c79947af1d4269ef85117e126518c588cfcf1a2b21b97269f51624",
|
|
318
|
-
"spec/v2/surface-baseline.json": "
|
|
318
|
+
"spec/v2/surface-baseline.json": "da9da09f5133be09b75820d138c5b4422365b2b8d113597c7fa89c99913069b3"
|
|
319
319
|
},
|
|
320
|
-
"corpusCommit": "
|
|
320
|
+
"corpusCommit": "906642cfb7cea6d4be59f0471866f8ffa3e4c702"
|
|
321
321
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* self-test (`host-event-witness.test.ts`) can feed each one a defect and show
|
|
5
5
|
* it convicts.
|
|
6
6
|
*
|
|
7
|
-
* The scenario owns the I/O (the `/host/events` stream, the emit seam, the
|
|
7
|
+
* The scenario owns the I/O (the `/host/events` stream, the RFC 0241 trigger or the emit seam, the
|
|
8
8
|
* scoped webhook receiver). This file owns only "given what arrived, which
|
|
9
9
|
* `events.md` §Host events / `webhooks.md` §Delivery rule failed?".
|
|
10
10
|
*/
|
|
@@ -30,6 +30,52 @@ export function exampleType(types: readonly AdvertisedType[], delivery: Advertis
|
|
|
30
30
|
return types.find((t) => t.type.startsWith('example.') && t.delivery === delivery)?.type ?? null;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Any advertised type of the class, preferring the conformance `example.*` one. For a leg
|
|
35
|
+
* that only names a type and causes no event, so it runs on a host without the §G seam.
|
|
36
|
+
*/
|
|
37
|
+
export function advertisedTypeOf(types: readonly AdvertisedType[], delivery: AdvertisedType['delivery']): string | null {
|
|
38
|
+
return exampleType(types, delivery) ?? types.find((t) => t.delivery === delivery)?.type ?? null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** RFC 0241 §A — the reserved test types, one per delivery class. */
|
|
42
|
+
export const TEST_TYPES = { durable: 'host-test.durable-triggered', ephemeral: 'host-test.ephemeral-triggered' } as const;
|
|
43
|
+
|
|
44
|
+
/** The reserved test type of a class, when the host lists it (RFC 0241 §B.1: listing it binds the trigger). */
|
|
45
|
+
export function testTypeListed(types: readonly AdvertisedType[], delivery: AdvertisedType['delivery']): string | null {
|
|
46
|
+
return types.some((t) => t.type === TEST_TYPES[delivery] && t.delivery === delivery) ? TEST_TYPES[delivery] : null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** How the suite causes a host event: the normative trigger when a test type is listed, else the §G seam. */
|
|
50
|
+
export type Trigger = 'normative' | 'seam';
|
|
51
|
+
export function triggerOf(types: readonly AdvertisedType[], seamsProfile: boolean): Trigger | null {
|
|
52
|
+
if (testTypeListed(types, 'durable') !== null || testTypeListed(types, 'ephemeral') !== null) return 'normative';
|
|
53
|
+
return seamsProfile ? 'seam' : null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* RFC 0241 §B.1–§B.2 — `emitTestHostEvent` is served exactly for the listed classes: a class whose
|
|
58
|
+
* type is not listed is refused (`404`/`405` when none is listed, `400 validation_error` otherwise),
|
|
59
|
+
* and a listed class is accepted with `202` and its reserved type.
|
|
60
|
+
*/
|
|
61
|
+
export function judgeTriggerBinding(types: readonly AdvertisedType[], delivery: AdvertisedType['delivery'], status: number, code: string | undefined, returnedType: unknown): Finding[] {
|
|
62
|
+
const listed = testTypeListed(types, delivery) !== null;
|
|
63
|
+
const anyListed = triggerOf(types, false) === 'normative';
|
|
64
|
+
if (listed) {
|
|
65
|
+
return [status === 202 && returnedType === TEST_TYPES[delivery]
|
|
66
|
+
? ok(`emitTestHostEvent accepts the listed ${delivery} class`)
|
|
67
|
+
: bad(`emitTestHostEvent MUST accept the listed ${delivery} class with 202 and type ${TEST_TYPES[delivery]}; got ${status} ${String(code ?? returnedType ?? '')}`.trim())];
|
|
68
|
+
}
|
|
69
|
+
if (!anyListed) {
|
|
70
|
+
return [status === 404 || status === 405
|
|
71
|
+
? ok('emitTestHostEvent is not served while no host-test type is listed')
|
|
72
|
+
: bad(`a host listing no host-test.* type MUST NOT serve emitTestHostEvent; got ${status}`)];
|
|
73
|
+
}
|
|
74
|
+
return [status === 400 && code === 'validation_error'
|
|
75
|
+
? ok(`emitTestHostEvent refuses the unlisted ${delivery} class`)
|
|
76
|
+
: bad(`emitTestHostEvent MUST refuse the unlisted ${delivery} class with 400 validation_error; got ${status} ${code ?? ''}`.trim())];
|
|
77
|
+
}
|
|
78
|
+
|
|
33
79
|
/** The frame carrying `eventId`, parsed, or null. */
|
|
34
80
|
export function frameFor(frames: readonly Frame[], eventId: string): { frame: Frame; envelope: Record<string, unknown> } | null {
|
|
35
81
|
for (const frame of frames) {
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The WASM ABI check (`spec/v2/core/node-pack-runtimes.md` §WASM: a host "MUST
|
|
3
|
+
* reject at load a pack whose `openwop_abi_version()` is not listed"; RFC 0008
|
|
4
|
+
* §H), observed through discovery and run creation against the
|
|
5
|
+
* operator-installed `vendor.openwop.misbehaving-abi` fixture pack
|
|
6
|
+
* (`conformance/fixtures/wasm-packs/misbehaving-abi/`), whose module reports
|
|
7
|
+
* ABI version 999.
|
|
8
|
+
*
|
|
9
|
+
* Loading is not a protocol operation, so the refusal is read from what it
|
|
10
|
+
* makes impossible. A host advertising `conformance-wasm-pack-abi-mismatch`
|
|
11
|
+
* says its operator offered the pack to its loader. Rejected at load, the pack
|
|
12
|
+
* is absent from `nodePackRuntimes.wasm.loadedPacks` (when advertised), and its
|
|
13
|
+
* one node never runs: the fixture's run is refused at creation or ends
|
|
14
|
+
* without that node completing.
|
|
15
|
+
*
|
|
16
|
+
* `abiRejectLeg` observes and returns findings or the reason it could not run;
|
|
17
|
+
* `judgeAbiReject` is pure. The scenario and the self-test double run the same code.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { v2Discovery } from './v2.js';
|
|
21
|
+
import { majorProfile } from './major-profile.js';
|
|
22
|
+
import { completionsOf, observeRun, type ObservedRun } from './fixture-run-observer.js';
|
|
23
|
+
|
|
24
|
+
export const ABI_FIXTURE = 'conformance-wasm-pack-abi-mismatch';
|
|
25
|
+
export const ABI_PACK = 'vendor.openwop.misbehaving-abi';
|
|
26
|
+
export const ABI_NODE = 'abi';
|
|
27
|
+
|
|
28
|
+
export type Skip = { readonly kind: 'skip'; readonly skip: 'blocked' | 'inapplicable'; readonly reason: string };
|
|
29
|
+
export type LegOutcome = { readonly kind: 'observed'; readonly findings: string[] } | Skip;
|
|
30
|
+
const skip = (s: 'blocked' | 'inapplicable', reason: string): Skip => ({ kind: 'skip', skip: s, reason });
|
|
31
|
+
const isRecord = (v: unknown): v is Record<string, unknown> => typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
32
|
+
const TERMINAL = new Set(['completed', 'failed', 'cancelled']);
|
|
33
|
+
|
|
34
|
+
/** A create the host refused on its merits, not for want of access or by its own fault. */
|
|
35
|
+
const refusedAtCreate = (status: number): boolean => status >= 400 && status < 500 && status !== 401 && status !== 403 && status !== 429;
|
|
36
|
+
|
|
37
|
+
/** The pack is not loaded, and its node never completes. */
|
|
38
|
+
export function judgeAbiReject(loadedPacks: unknown, o: ObservedRun): string[] {
|
|
39
|
+
const out: string[] = [];
|
|
40
|
+
if (Array.isArray(loadedPacks) && loadedPacks.includes(ABI_PACK)) out.push(`nodePackRuntimes.wasm.loadedPacks MUST NOT list ${ABI_PACK}, whose openwop_abi_version() 999 no host lists; it does`);
|
|
41
|
+
if (o.createStatus === 201 || o.createStatus === 202) {
|
|
42
|
+
if (completionsOf(o.events, ABI_NODE).length > 0 || o.terminalStatus === 'completed') {
|
|
43
|
+
out.push(`the ${ABI_PACK} node ran (node.completed, run ${String(o.terminalStatus)}): a pack whose ABI version is not listed MUST be rejected at load, so its node cannot execute`);
|
|
44
|
+
} else if (o.terminalStatus === undefined || !TERMINAL.has(o.terminalStatus)) {
|
|
45
|
+
out.push(`the ${ABI_FIXTURE} run MUST be refused at creation or end without the node completing; it is still ${String(o.terminalStatus)}`);
|
|
46
|
+
}
|
|
47
|
+
} else if (!refusedAtCreate(o.createStatus)) {
|
|
48
|
+
out.push(`createRun of ${ABI_FIXTURE} answered ${o.createStatus}; a refusal of an unloaded node is a 4xx`);
|
|
49
|
+
}
|
|
50
|
+
return out;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** node-pack-runtimes.md §WASM bullet 1 — `openwop.requirement.node-pack-runtimes.wasm-abi-reject`. */
|
|
54
|
+
export async function abiRejectLeg(): Promise<LegOutcome> {
|
|
55
|
+
let doc: Record<string, unknown> | null = null;
|
|
56
|
+
try { doc = await v2Discovery(); } catch { doc = null; }
|
|
57
|
+
if (!doc) return skip('blocked', 'v2 discovery unreachable');
|
|
58
|
+
const fam = doc['nodePackRuntimes'];
|
|
59
|
+
const wasm = isRecord(fam) ? fam['wasm'] : undefined;
|
|
60
|
+
if (!isRecord(wasm)) return skip('inapplicable', 'the host does not advertise nodePackRuntimes.wasm');
|
|
61
|
+
const fixtures = Array.isArray(doc['fixtures']) ? (doc['fixtures'] as unknown[]) : [];
|
|
62
|
+
if (!fixtures.includes(ABI_FIXTURE)) return skip('inapplicable', `the host does not advertise ${ABI_FIXTURE}; the operator offers conformance/fixtures/wasm-packs/misbehaving-abi/ to the loader to run it`);
|
|
63
|
+
const o = await observeRun(majorProfile(2), ABI_FIXTURE, { timeoutMs: 30_000 });
|
|
64
|
+
if (o.createStatus === 401 || o.createStatus === 403 || o.createStatus === 429 || o.createStatus >= 500) return skip('blocked', `createRun of ${ABI_FIXTURE} answered ${o.createStatus}`);
|
|
65
|
+
return { kind: 'observed', findings: judgeAbiReject(wasm['loadedPacks'], o) };
|
|
66
|
+
}
|
|
@@ -5,25 +5,17 @@
|
|
|
5
5
|
* plus the manifest shape and the `ui-plugin/1` host-RPC + version-token concurrency
|
|
6
6
|
* contract.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
8
|
+
* The always-on schema layer (manifest and `ui-plugin/1` message shapes) reads
|
|
9
|
+
* only the corpus, so it lives in `src/coherence/frontend-plugin-schemas.test.ts`
|
|
10
|
+
* (RFC 0238 G3). What stays here reads a host:
|
|
9
11
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* B. Capability-gated behavioral leg — on a host advertising
|
|
19
|
-
* `capabilities.uiPlugins.supported: true` that exposes the
|
|
20
|
-
* `POST /v1/host/sample/ui-plugin/rpc` test seam, an undeclared-method request
|
|
21
|
-
* MUST be refused with `method_not_allowed` (`frontend-plugin-rpc-allowlist`), and
|
|
22
|
-
* a stale `artifact.write` MUST be refused with `artifact_conflict` + `currentVersion`
|
|
23
|
-
* and MUST NOT persist (§Concurrency). Hosts without the seam soft-skip (404);
|
|
24
|
-
* unadvertised hosts skip via the behavior gate. (No conformant host advertises
|
|
25
|
-
* `uiPlugins` yet — these legs soft-skip until the openwop-app reference host (ADR
|
|
26
|
-
* 0153) lands, the first witness that graduates the invariants to protocol.)
|
|
12
|
+
* - the `uiPlugins` isolation advertisement, on a host that advertises it; and
|
|
13
|
+
* - the capability-gated host-RPC legs over the v1 test seam
|
|
14
|
+
* `POST /v1/host/sample/ui-plugin/rpc`: an undeclared method is refused with
|
|
15
|
+
* `method_not_allowed` (`frontend-plugin-rpc-allowlist`), and a stale
|
|
16
|
+
* `artifact.write` is refused with `artifact_conflict` + `currentVersion` and
|
|
17
|
+
* does not persist. Hosts without the seam soft-skip (404). The v2 witness is
|
|
18
|
+
* `v2-ui-plugin-boundary` (RFC 0238).
|
|
27
19
|
*
|
|
28
20
|
* @see spec/v1/frontend-plugin-packs.md
|
|
29
21
|
* @see SECURITY/invariants.yaml ids: frontend-plugin-{isolation,egress,rpc-allowlist,no-byok}
|
|
@@ -31,161 +23,12 @@
|
|
|
31
23
|
*/
|
|
32
24
|
|
|
33
25
|
import { describe, it, expect } from 'vitest';
|
|
34
|
-
import { readFileSync } from 'node:fs';
|
|
35
|
-
import { join } from 'node:path';
|
|
36
|
-
import Ajv2020 from 'ajv/dist/2020.js';
|
|
37
|
-
import addFormats from 'ajv-formats';
|
|
38
|
-
import { SCHEMAS_DIR } from '../lib/paths.js';
|
|
39
26
|
import { driver } from '../lib/driver.js';
|
|
40
27
|
import { behaviorGate } from '../lib/behavior-gate.js';
|
|
41
28
|
import { readCapabilityFamily } from '../lib/discovery-capabilities.js';
|
|
42
29
|
import { req } from '../lib/requirement-ids.js';
|
|
43
30
|
import { softSkip } from '../lib/soft-skip.js';
|
|
44
31
|
|
|
45
|
-
const MANIFEST_SCHEMA = join(SCHEMAS_DIR, 'frontend-plugin-manifest.schema.json');
|
|
46
|
-
const MESSAGE_SCHEMA = join(SCHEMAS_DIR, 'ui-plugin-message.schema.json');
|
|
47
|
-
|
|
48
|
-
function validManifest(): Record<string, unknown> {
|
|
49
|
-
return {
|
|
50
|
-
name: 'vendor.acme.canvas-editor',
|
|
51
|
-
version: '1.0.0',
|
|
52
|
-
kind: 'frontend-plugin',
|
|
53
|
-
engines: { openwop: '>=1.2.0' },
|
|
54
|
-
uiPlugins: [
|
|
55
|
-
{
|
|
56
|
-
pluginId: 'app-builder',
|
|
57
|
-
surface: 'artifact-viewer',
|
|
58
|
-
entry: 'ui/app-builder.mjs',
|
|
59
|
-
hostApi: ['artifact.read', 'artifact.write'],
|
|
60
|
-
},
|
|
61
|
-
],
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
describe('frontend-plugin manifest: schema layer (always-on, server-free)', () => {
|
|
66
|
-
const ajv = new Ajv2020({ allErrors: true, strict: false });
|
|
67
|
-
addFormats(ajv);
|
|
68
|
-
const validate = ajv.compile(JSON.parse(readFileSync(MANIFEST_SCHEMA, 'utf8')));
|
|
69
|
-
|
|
70
|
-
it('a well-formed frontend-plugin manifest validates', () => {
|
|
71
|
-
expect(
|
|
72
|
-
validate(validManifest()),
|
|
73
|
-
req('openwop.it.frontend-plugin-packs.a-well-formed-frontend-plugin-manifest-validates', 'frontend-plugin-packs.md', `frontend-plugin-packs.md §The pack — a valid manifest MUST validate. Errors: ${JSON.stringify(validate.errors)}`),
|
|
74
|
-
).toBe(true);
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
it('a backend `runtime` member is rejected (a plugin is sandboxed UI, not a node entry)', () => {
|
|
78
|
-
const m = { ...validManifest(), runtime: { language: 'javascript', entry: 'index.mjs' } };
|
|
79
|
-
expect(
|
|
80
|
-
validate(m),
|
|
81
|
-
req('openwop.it.frontend-plugin-packs.a-backend-runtime-member-is-rejected-a-plugin-is-sandboxed-ui-not-a-node-entry', 'frontend-plugin-packs.md', 'node-packs.md §Pack kinds — a kind:"frontend-plugin" manifest carrying `runtime` MUST be rejected (pack_kind_invalid)'),
|
|
82
|
-
).toBe(false);
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
it('a uiPlugins[] entry missing `entry` is rejected', () => {
|
|
86
|
-
const m = validManifest();
|
|
87
|
-
delete (m.uiPlugins as Array<Record<string, unknown>>)[0].entry;
|
|
88
|
-
expect(validate(m), req('openwop.it.frontend-plugin-packs.a-uiplugins-entry-missing-entry-is-rejected', 'frontend-plugin-packs.md', 'a uiPlugins[] entry missing `entry` MUST NOT validate')).toBe(false);
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it('an `entry` path with `..` traversal is rejected', () => {
|
|
92
|
-
const m = validManifest();
|
|
93
|
-
(m.uiPlugins as Array<Record<string, unknown>>)[0].entry = '../escape.mjs';
|
|
94
|
-
expect(validate(m), req('openwop.it.frontend-plugin-packs.an-entry-path-with-traversal-is-rejected', 'frontend-plugin-packs.md', 'an `entry` path MUST NOT contain `..` (path-traversal)')).toBe(false);
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
it('a hostApi method outside the closed allowlist is rejected (frontend-plugin-rpc-allowlist)', () => {
|
|
98
|
-
const m = validManifest();
|
|
99
|
-
(m.uiPlugins as Array<Record<string, unknown>>)[0].hostApi = ['artifact.read', 'host.exec'];
|
|
100
|
-
expect(
|
|
101
|
-
validate(m),
|
|
102
|
-
req('openwop.it.frontend-plugin-packs.a-hostapi-method-outside-the-closed-allowlist-is-rejected-frontend-plugin-rpc-al', 'frontend-plugin-packs.md', 'frontend-plugin-packs.md §Host-RPC — only the closed allowlist methods are permitted; `host.exec` MUST NOT validate'),
|
|
103
|
-
).toBe(false);
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
it('an empty uiPlugins[] is rejected (a pack MUST declare at least one plugin)', () => {
|
|
107
|
-
const m = { ...validManifest(), uiPlugins: [] };
|
|
108
|
-
expect(validate(m), req('openwop.it.frontend-plugin-packs.an-empty-uiplugins-is-rejected-a-pack-must-declare-at-least-one-plugin', 'frontend-plugin-packs.md', 'a frontend-plugin pack MUST declare at least one uiPlugins[] entry')).toBe(false);
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
it('a canvas-preview entry with canvasTypes + host.announce validates (RFC 0130)', () => {
|
|
112
|
-
const m = validManifest();
|
|
113
|
-
(m.uiPlugins as Array<Record<string, unknown>>)[0] = {
|
|
114
|
-
pluginId: 'gantt-preview',
|
|
115
|
-
surface: 'canvas-preview',
|
|
116
|
-
canvasTypes: ['canvas.gantt'],
|
|
117
|
-
entry: 'ui/preview.html',
|
|
118
|
-
hostApi: ['artifact.read', 'host.announce'],
|
|
119
|
-
};
|
|
120
|
-
expect(
|
|
121
|
-
validate(m),
|
|
122
|
-
req('openwop.it.frontend-plugin-packs.a-canvas-preview-entry-with-canvastypes-host-announce-validates-rfc-0130', 'RFC 0130', `frontend-plugin-packs.md §The pack (RFC 0130) — a canvas-preview entry MUST validate. Errors: ${JSON.stringify(validate.errors)}`),
|
|
123
|
-
).toBe(true);
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
it('a surface outside the closed set is still rejected (RFC 0130 keeps the enum closed)', () => {
|
|
127
|
-
const m = validManifest();
|
|
128
|
-
(m.uiPlugins as Array<Record<string, unknown>>)[0].surface = 'omni-panel';
|
|
129
|
-
expect(
|
|
130
|
-
validate(m),
|
|
131
|
-
req('openwop.it.frontend-plugin-packs.a-surface-outside-the-closed-set-is-still-rejected-rfc-0130-keeps-the-enum-close', 'RFC 0130', 'frontend-plugin-packs.md §The pack — the surface enum stays closed; an unknown surface MUST NOT validate'),
|
|
132
|
-
).toBe(false);
|
|
133
|
-
});
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
describe('ui-plugin/1 message: schema layer (always-on, server-free)', () => {
|
|
137
|
-
const ajv = new Ajv2020({ allErrors: true, strict: false });
|
|
138
|
-
addFormats(ajv);
|
|
139
|
-
const validate = ajv.compile(JSON.parse(readFileSync(MESSAGE_SCHEMA, 'utf8')));
|
|
140
|
-
|
|
141
|
-
it('a valid artifact.write request carrying a version token validates', () => {
|
|
142
|
-
const reqBody = {
|
|
143
|
-
openwop: 'ui-plugin/1',
|
|
144
|
-
type: 'request',
|
|
145
|
-
id: 7,
|
|
146
|
-
method: 'artifact.write',
|
|
147
|
-
params: { artifactId: 'a-1', version: 'opaque-v1', payload: {} },
|
|
148
|
-
};
|
|
149
|
-
expect(validate(reqBody), req('openwop.it.frontend-plugin-packs.a-valid-artifact-write-request-carrying-a-version-token-validates', 'frontend-plugin-packs.md', `a valid artifact.write request MUST validate. Errors: ${JSON.stringify(validate.errors)}`)).toBe(true);
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
it('an artifact_conflict response carries currentVersion (version-token concurrency)', () => {
|
|
153
|
-
const res = {
|
|
154
|
-
openwop: 'ui-plugin/1',
|
|
155
|
-
type: 'response',
|
|
156
|
-
id: 7,
|
|
157
|
-
ok: false,
|
|
158
|
-
error: { code: 'artifact_conflict', currentVersion: 'opaque-v2' },
|
|
159
|
-
};
|
|
160
|
-
expect(
|
|
161
|
-
validate(res),
|
|
162
|
-
req('openwop.it.frontend-plugin-packs.an-artifact-conflict-response-carries-currentversion-version-token-concurrency', 'frontend-plugin-packs.md', `frontend-plugin-packs.md §Concurrency — a stale write surfaces artifact_conflict + currentVersion. Errors: ${JSON.stringify(validate.errors)}`),
|
|
163
|
-
).toBe(true);
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
it('a request with a method outside the allowlist is schema-rejected', () => {
|
|
167
|
-
const reqBody = { openwop: 'ui-plugin/1', type: 'request', id: 1, method: 'host.exec' };
|
|
168
|
-
expect(validate(reqBody), req('openwop.it.frontend-plugin-packs.a-request-with-a-method-outside-the-allowlist-is-schema-rejected', 'frontend-plugin-packs.md', 'a method outside the ui-plugin/1 allowlist MUST NOT validate')).toBe(false);
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
it('a message without the ui-plugin/1 protocol tag is rejected', () => {
|
|
172
|
-
const reqBody = { openwop: 'ui-plugin/2', type: 'request', id: 1, method: 'artifact.read' };
|
|
173
|
-
expect(validate(reqBody), req('openwop.it.frontend-plugin-packs.a-message-without-the-ui-plugin-1-protocol-tag-is-rejected', 'frontend-plugin-packs.md', 'a host MUST ignore messages whose ui-plugin tag it does not recognize')).toBe(false);
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
it('no credential-bearing field is admitted on the envelope (frontend-plugin-no-byok)', () => {
|
|
177
|
-
// additionalProperties:false on every envelope variant — a stray apiKey/token at the
|
|
178
|
-
// envelope root cannot ride the boundary.
|
|
179
|
-
for (const leak of ['apiKey', 'token', 'clientSecret', 'authorization']) {
|
|
180
|
-
const reqBody = { openwop: 'ui-plugin/1', type: 'request', id: 1, method: 'artifact.read', [leak]: 'xxx' };
|
|
181
|
-
expect(
|
|
182
|
-
validate(reqBody),
|
|
183
|
-
req('openwop.it.frontend-plugin-packs.no-credential-bearing-field-is-admitted-on-the-envelope-frontend-plugin-no-byok', 'frontend-plugin-packs.md', `frontend-plugin-no-byok — a credential-named envelope field ("${leak}") MUST NOT validate (additionalProperties:false)`),
|
|
184
|
-
).toBe(false);
|
|
185
|
-
}
|
|
186
|
-
});
|
|
187
|
-
});
|
|
188
|
-
|
|
189
32
|
describe('frontend-plugin: isolation advertisement (always-on, capability shape)', () => {
|
|
190
33
|
// RFC 0119: `isolation` is a categorical model, not a single browser const. A conformant host
|
|
191
34
|
// advertises a member of the enum (cross-origin-iframe default) or an x-host-* vendor model —
|