@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.
Files changed (33) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +2 -2
  3. package/dist/spec-artifacts.lock.json +2 -2
  4. package/fixtures/conformance-multi-party-council-oversize.json +244 -0
  5. package/fixtures/conformance-multi-party-council.json +61 -0
  6. package/fixtures/conformance-nondeterminism.json +23 -0
  7. package/fixtures/conformance-wasm-pack-memory-cap-breach.json +23 -5
  8. package/fixtures/frontend-plugin-packs/ui/narrow.html +4 -0
  9. package/fixtures/frontend-plugin-packs/ui-plugin-pack-narrow.json +16 -0
  10. package/fixtures/wasm-packs/build.mjs +43 -0
  11. package/fixtures/wasm-packs/misbehaving-abi/module.wasm +0 -0
  12. package/fixtures/wasm-packs/misbehaving-abi/module.wat +24 -0
  13. package/fixtures/wasm-packs/misbehaving-abi/pack.json +27 -0
  14. package/fixtures/wasm-packs/misbehaving-memory/module.wasm +0 -0
  15. package/fixtures/wasm-packs/misbehaving-memory/module.wat +30 -0
  16. package/fixtures/wasm-packs/misbehaving-memory/pack.json +27 -0
  17. package/fixtures.md +32 -0
  18. package/package.json +2 -2
  19. package/requirements.json +251 -7
  20. package/scenario-majors.json +17 -2
  21. package/schemas/CORPUS-STAMP.json +29 -27
  22. package/src/lib/council-roster-witness.ts +144 -0
  23. package/src/lib/ui-plugin-boundary-witness.ts +180 -0
  24. package/src/lib/wasm-memory-cap-witness.ts +46 -0
  25. package/src/scenarios/v2-bound-id-path-projection.test.ts +20 -0
  26. package/src/scenarios/v2-envelope-contracts-advertisement.test.ts +8 -0
  27. package/src/scenarios/v2-multi-party-council.test.ts +56 -0
  28. package/src/scenarios/v2-nondeterminism-policy-advertisement.test.ts +11 -1
  29. package/src/scenarios/v2-nondeterminism-sources.test.ts +83 -0
  30. package/src/scenarios/v2-purpose-propagation-advertisement.test.ts +1 -1
  31. package/src/scenarios/v2-purpose-propagation-onward.test.ts +119 -0
  32. package/src/scenarios/v2-ui-plugin-boundary.test.ts +68 -0
  33. package/src/scenarios/v2-wasm-memory-cap.test.ts +34 -0
@@ -0,0 +1,56 @@
1
+ /**
2
+ * v2 — a council's roster has an input seat and its refusals have codes
3
+ * (RFC 0239; `spec/v2/core/conversation.md` §multiPartyConversation). Gated on
4
+ * `multiPartyConversation` and the advertised council fixtures.
5
+ *
6
+ * The three roster MUSTs were witnessed only by the v1 seam
7
+ * `/v1/host/sample/conversation/multi-party/*`. Here every leg uses normative
8
+ * operations: run creation, the snapshot and event poll, and `resolveInterrupt`.
9
+ *
10
+ * roster-carried `conversation.opened.participants` equals the
11
+ * `core.conversationGate` roster of `conformance-multi-party-council`;
12
+ * speaker-refused resuming the exchange with a non-member `speakerId` is
13
+ * `422 conversation_speaker_not_participant`, unconsumed (the run
14
+ * still waits, no `conversation.exchanged`); a member's turn then
15
+ * resolves it (the control);
16
+ * roster-exceeded with `maxParticipants` below 64, creating a run of
17
+ * `conformance-multi-party-council-oversize` is
18
+ * `422 conversation_roster_exceeded`.
19
+ *
20
+ * The legs live in `lib/council-roster-witness.ts`, proven against a double in
21
+ * `lib/council-roster-witness.test.ts`.
22
+ *
23
+ * Dispositions: no `multiPartyConversation`, or the fixture not advertised ⇒
24
+ * `inapplicable`. roster-exceeded is `inapplicable` when `maxParticipants` is
25
+ * absent or 64 or more (RFC 0239 §Decisions 4).
26
+ *
27
+ * @see RFCS/0239-council-roster-input.md §A–§F
28
+ * @see spec/v2/core/conversation.md §multiPartyConversation
29
+ */
30
+
31
+ import { describe, it, expect } from 'vitest';
32
+ import { softSkip } from '../lib/soft-skip.js';
33
+ import { req } from '../lib/requirement-ids.js';
34
+ import { oversizeLeg, rosterLeg, speakerLeg } from '../lib/council-roster-witness.js';
35
+
36
+ const DOC = 'spec/v2/core/conversation.md §multiPartyConversation';
37
+
38
+ describe('v2 multi-party council (RFC 0239)', () => {
39
+ it('the configured roster is carried on conversation.opened', async () => {
40
+ const out = await rosterLeg();
41
+ if (out.kind === 'skip') return softSkip(out.skip, out.reason);
42
+ expect(out.findings.join('; '), req('openwop.requirement.0239.council.roster-carried', DOC, 'a host MUST accept participants in core.conversationGate config and carry them on conversation.opened')).toBe('');
43
+ });
44
+
45
+ it('a turn from outside the roster is refused and left unconsumed', async () => {
46
+ const out = await speakerLeg();
47
+ if (out.kind === 'skip') return softSkip(out.skip, out.reason);
48
+ expect(out.findings.join('; '), req('openwop.requirement.0239.council.speaker-refused', DOC, 'a host MUST refuse a resumed turn whose speakerId is off the roster with conversation_speaker_not_participant, leaving the interrupt open')).toBe('');
49
+ });
50
+
51
+ it('an oversized roster is refused at run creation', async () => {
52
+ const out = await oversizeLeg();
53
+ if (out.kind === 'skip') return softSkip(out.skip, out.reason);
54
+ expect(out.findings.join('; '), req('openwop.requirement.0239.council.roster-exceeded', DOC, 'a host MUST refuse at run creation, never truncate, a roster over maxParticipants, with conversation_roster_exceeded')).toBe('');
55
+ });
56
+ });
@@ -5,7 +5,9 @@
5
5
  *
6
6
  * record the record validates against its capabilities-schema seat;
7
7
  *
8
- * Not here: recording every declared source needs a `sources[]` seat and a named event (TODO §8).
8
+ * no-false `declared: false` is not a v2 state (RFC 0237 §C);
9
+ *
10
+ * Not here: the replay of each declared source is `v2-nondeterminism-sources`.
9
11
  *
10
12
  * Dispositions: discovery unreadable ⇒ `blocked`; `nondeterminismPolicy` absent, or the
11
13
  * facet a leg reads absent ⇒ `inapplicable`.
@@ -38,4 +40,12 @@ describe('v2 nondeterminismPolicy advertisement (replay.md §Declared nondetermi
38
40
  if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
39
41
  for (const x of out.findings) expect(x.ok, req(ID_RECORD, x.doc, x.message)).toBe(true);
40
42
  });
43
+
44
+ it('nondeterminismPolicy.declared is not false: a host that does not offer the family omits it', async () => {
45
+ const doc = await discovery();
46
+ if (!doc) return softSkip('blocked', UNREADABLE);
47
+ const rec = await familyAdvertised('nondeterminismPolicy');
48
+ if (!rec) return softSkip('inapplicable', 'the host does not advertise nondeterminismPolicy');
49
+ expect(rec['declared'], req('openwop.requirement.0237.no-false-advertisement', 'RFC 0237 §C; capabilities.md §2', 'nondeterminismPolicy.declared: false is not a v2 state; a host that does not offer the family MUST omit it')).not.toBe(false);
50
+ });
41
51
  });
@@ -0,0 +1,83 @@
1
+ /**
2
+ * v2 — declared nondeterminism replays its recorded values (RFC 0237 §B;
3
+ * `spec/v2/core/replay.md` §Declared nondeterminism; suite 2.45.23, target
4
+ * major 2; fixture-gated).
5
+ *
6
+ * A host listing `nondeterminismPolicy.sources` MUST record each value drawn
7
+ * from a listed source where it is read, and a `replay` fork MUST reproduce it
8
+ * rather than draw again. The log entry itself is host-internal; what a client
9
+ * sees is the fork's output. So the leg runs `conformance-nondeterminism` (one
10
+ * conformance-reserved node that writes one value per listed source to the run
11
+ * outputs), forks it in `replay` mode from `fromSeq: 0`, and requires each listed
12
+ * source's output to be byte-equal.
13
+ *
14
+ * 0237.declared-source-replays for every listed source, the fork's output
15
+ * equals the source run's.
16
+ *
17
+ * Dispositions: discovery unreadable ⇒ `blocked`; `nondeterminismPolicy.sources`
18
+ * absent, the fixture not advertised, or `replay` mode not served ⇒
19
+ * `inapplicable`; a create or fork the host refuses ⇒ `blocked`.
20
+ *
21
+ * @see RFCS/0237-nondeterminism-sources.md §B, §D
22
+ * @see spec/v2/core/replay.md §Declared nondeterminism
23
+ */
24
+
25
+ import { describe, it, expect } from 'vitest';
26
+ import { driver, type OpenWOPResponse } from '../lib/driver.js';
27
+ import { v2Discovery, familyAdvertised } from '../lib/v2.js';
28
+ import { isFixtureAdvertised } from '../lib/fixtures.js';
29
+ import { pollUntilTerminal } from '../lib/polling.js';
30
+ import { softSkip } from '../lib/soft-skip.js';
31
+ import { req } from '../lib/requirement-ids.js';
32
+ import { readErrorCode } from '../lib/error-envelope.js';
33
+
34
+ const DOC = 'spec/v2/core/replay.md §Declared nondeterminism';
35
+ const ID = 'openwop.requirement.0237.declared-source-replays';
36
+ const FIXTURE = 'conformance-nondeterminism';
37
+
38
+ async function http(fn: () => Promise<OpenWOPResponse>): Promise<OpenWOPResponse | null> {
39
+ try { return await fn(); } catch { return null; }
40
+ }
41
+
42
+ /** `run.completed.outputs`, read from the run's event log. */
43
+ async function outputsOf(runId: string): Promise<Record<string, unknown> | null> {
44
+ const r = await http(() => driver.get(`/runs/${encodeURIComponent(runId)}/events/poll?timeout=1`));
45
+ const events = (r?.json as { events?: Array<{ type?: string; payload?: { outputs?: unknown } }> } | null)?.events ?? [];
46
+ const done = events.find((e) => e.type === 'run.completed');
47
+ const out = done?.payload?.outputs;
48
+ return out !== null && typeof out === 'object' ? (out as Record<string, unknown>) : null;
49
+ }
50
+
51
+ describe('v2 declared nondeterminism replays (RFC 0237 §B — fixture-gated)', () => {
52
+ it('a replay fork reproduces every value drawn from a listed source', async () => {
53
+ let doc: Record<string, unknown> | null;
54
+ try { doc = await v2Discovery(); } catch { doc = null; }
55
+ if (!doc) return softSkip('blocked', 'v2 discovery unreachable');
56
+ const policy = await familyAdvertised('nondeterminismPolicy');
57
+ const sources = Array.isArray(policy?.['sources']) ? (policy['sources'] as unknown[]).filter((s): s is string => typeof s === 'string') : [];
58
+ if (sources.length === 0) return softSkip('inapplicable', 'the host lists no nondeterminismPolicy.sources');
59
+ if (!isFixtureAdvertised(FIXTURE)) return softSkip('inapplicable', `the host does not advertise the ${FIXTURE} fixture`);
60
+ const modes = (await familyAdvertised('replay'))?.['modes'];
61
+ if (!Array.isArray(modes) || !modes.includes('replay')) return softSkip('inapplicable', 'the host does not serve replay-mode forks');
62
+
63
+ const created = await http(() => driver.post('/runs', { workflowId: FIXTURE }));
64
+ const runId = (created?.json as { runId?: unknown } | null | undefined)?.runId;
65
+ if (created?.status !== 201 || typeof runId !== 'string') return softSkip('blocked', `POST /runs {workflowId: ${FIXTURE}} answered ${created?.status ?? 'no response'} ${readErrorCode(created?.json) ?? ''}`.trim());
66
+ const source = await pollUntilTerminal(runId, { timeoutMs: 20_000 });
67
+ if (source.status !== 'completed') return softSkip('blocked', `the ${FIXTURE} run ended ${source.status}, not completed`);
68
+ const first = await outputsOf(runId);
69
+ expect(first !== null, req(ID, DOC, `run.completed MUST carry outputs for ${FIXTURE}`)).toBe(true);
70
+
71
+ const fork = await http(() => driver.post(`/runs/${encodeURIComponent(runId)}:fork`, { mode: 'replay', fromSeq: 0 }));
72
+ const forkId = (fork?.json as { runId?: unknown } | null | undefined)?.runId;
73
+ if (fork === null || fork.status >= 300 || typeof forkId !== 'string') return softSkip('blocked', `the replay fork answered ${fork?.status ?? 'no response'} ${readErrorCode(fork?.json) ?? ''}`.trim());
74
+ const forked = await pollUntilTerminal(forkId, { timeoutMs: 20_000 });
75
+ if (forked.status !== 'completed') return softSkip('blocked', `the replay fork ended ${forked.status}, not completed`);
76
+ const second = await outputsOf(forkId);
77
+
78
+ for (const s of sources) {
79
+ expect(typeof first?.[s], req(ID, DOC, `the ${FIXTURE} outputs MUST carry a value for the listed source ${s} (got ${JSON.stringify(first)})`)).toBe('string');
80
+ expect(second?.[s], req(ID, DOC, `a replay fork MUST reproduce the value drawn from the listed source ${s}: the source run drew ${JSON.stringify(first?.[s])}, the fork ${JSON.stringify(second?.[s])}`)).toBe(first?.[s]);
81
+ }
82
+ }, 60_000);
83
+ });
@@ -6,7 +6,7 @@
6
6
  * record the record validates against its capabilities-schema seat;
7
7
  * propagates-onward-boolean purposePropagation.propagatesOnward is a boolean;
8
8
  *
9
- * Not here: propagation itself needs a carrier the suite can receive with a label seat (TODO §8).
9
+ * Not here: propagation itself is `v2-purpose-propagation-onward`, which observes the host's onward A2A hop through the §22 seam.
10
10
  *
11
11
  * Dispositions: discovery unreadable ⇒ `blocked`; `purposePropagation` absent, or the
12
12
  * facet a leg reads absent ⇒ `inapplicable`.
@@ -0,0 +1,119 @@
1
+ /**
2
+ * v2 — purpose labels on the onward hop (`spec/v2/core/security-defaults.md`
3
+ * §Onward hops; suite 2.45.23, target major 2; seam-gated).
4
+ *
5
+ * A host advertising `purposePropagation` MUST re-emit a `permittedPurposes`
6
+ * label it received on every onward hop of the same data, narrowing and never
7
+ * widening, and MUST treat `[]` as no onward use. The v1 twin read a
8
+ * self-reported forward seam; this file observes a real onward hop instead.
9
+ * The suite plays both ends: it hands the host a label through the §22 `invoke`
10
+ * seam (`permittedPurposes`), and its fake A2A peer receives what the host's
11
+ * production A2A client actually sends.
12
+ *
13
+ * onward-reemit-no-widen the onward message carries
14
+ * `metadata.openwop.permittedPurposes`, a subset of
15
+ * the label sent;
16
+ * empty-no-onward with `[]` the host makes no onward call, while an
17
+ * unlabelled control call through the same seam
18
+ * reaches the peer (non-arrival is evidence, not a
19
+ * dead pipe).
20
+ *
21
+ * Not here: the derived-output rule (a merge of labelled inputs carries their
22
+ * intersection) needs two inputs on one hop; the seam forwards one.
23
+ *
24
+ * Dispositions: discovery unreadable ⇒ `blocked`; `purposePropagation` absent,
25
+ * `propagatesOnward` false, `a2a` absent or no seams profile ⇒ `inapplicable`;
26
+ * seam unwired ⇒ `seamAbsent`.
27
+ *
28
+ * @see spec/v2/core/security-defaults.md §Onward hops
29
+ * @see spec/v1/host-sample-test-seams.md §22
30
+ */
31
+
32
+ import { describe, it, expect } from 'vitest';
33
+ import { driver, type OpenWOPResponse } from '../lib/driver.js';
34
+ import { v2Discovery, familyAdvertised } from '../lib/v2.js';
35
+ import { SEAMS_PREFIX, seamsProfileAdvertised } from '../lib/seams.js';
36
+ import { softSkip, seamAbsent } from '../lib/soft-skip.js';
37
+ import { req } from '../lib/requirement-ids.js';
38
+ import { A2AFakePeer, type A2APeerInvocation } from '../lib/a2a-fake-peer.js';
39
+
40
+ export const REQUIRES_HOST_CALLBACK = "the host's A2A client calls the suite's fake A2A peer";
41
+
42
+ const DOC = 'spec/v2/core/security-defaults.md §Onward hops';
43
+ const ID_REEMIT = 'openwop.requirement.purpose-propagation.onward-reemit-no-widen';
44
+ const ID_EMPTY = 'openwop.requirement.purpose-propagation.empty-no-onward';
45
+ const SEAM = `${SEAMS_PREFIX}/sample/a2a/invoke`;
46
+ const LABEL = ['openwop-conformance-analytics', 'openwop-conformance-support'];
47
+
48
+ async function http(fn: () => Promise<OpenWOPResponse>): Promise<OpenWOPResponse | null> {
49
+ try { return await fn(); } catch { return null; }
50
+ }
51
+
52
+ type Skip = { kind: 'blocked' | 'inapplicable' | 'seam'; reason: string };
53
+ const skip = (s: Skip): undefined => (s.kind === 'seam' ? seamAbsent(s.reason) : softSkip(s.kind, s.reason));
54
+
55
+ async function gate(): Promise<Skip | null> {
56
+ let doc: Record<string, unknown> | null;
57
+ try { doc = await v2Discovery(); } catch { doc = null; }
58
+ if (!doc) return { kind: 'blocked', reason: 'v2 discovery unreachable' };
59
+ const pp = await familyAdvertised('purposePropagation');
60
+ if (pp === null) return { kind: 'inapplicable', reason: 'the host does not advertise purposePropagation' };
61
+ if (pp['propagatesOnward'] === false) return { kind: 'inapplicable', reason: 'purposePropagation.propagatesOnward is false — the host has no onward hop' };
62
+ if (!(await familyAdvertised('a2a'))) return { kind: 'inapplicable', reason: 'a2a not advertised — the onward hop observed here is the host A2A client' };
63
+ if (!seamsProfileAdvertised(doc)) return { kind: 'inapplicable', reason: 'the onward hop is driven through the §22 invoke seam — the seams profile is not advertised' };
64
+ return null;
65
+ }
66
+
67
+ /** The message sends the peer received. */
68
+ const sends = (peer: A2AFakePeer): A2APeerInvocation[] => peer.invocations().filter((i) => i.rpcMethod === 'SendMessage' || i.rpcMethod === 'message/send');
69
+
70
+ function labelOf(inv: A2APeerInvocation): unknown {
71
+ const body = inv.body as { params?: { message?: { metadata?: { openwop?: { permittedPurposes?: unknown } } } } } | undefined;
72
+ return body?.params?.message?.metadata?.openwop?.permittedPurposes;
73
+ }
74
+
75
+ async function invoke(peer: A2AFakePeer, permittedPurposes?: string[]): Promise<OpenWOPResponse | Skip> {
76
+ const res = await http(() => driver.post(SEAM, { peerUrl: peer.hostFacingEndpoint(), ...(permittedPurposes ? { permittedPurposes } : {}) }));
77
+ if (res === null) return { kind: 'blocked', reason: `${SEAM} unreachable (fetch failed)` };
78
+ if (res.status === 404 || res.status === 405) return { kind: 'seam', reason: `${SEAM} not mounted (${res.status}) — host-sample-test-seams.md §22` };
79
+ return res;
80
+ }
81
+
82
+ describe('v2 purpose labels on the onward hop (security-defaults.md §Onward hops — seam-gated)', () => {
83
+ it('a received label is re-emitted on the onward message, never widened', async () => {
84
+ const g = await gate();
85
+ if (g) return skip(g);
86
+ const peer = new A2AFakePeer({ protocolVersions: ['1.0'] });
87
+ await peer.start();
88
+ try {
89
+ const res = await invoke(peer, LABEL);
90
+ if (!('status' in res)) return skip(res);
91
+ const sent = sends(peer)[0];
92
+ if (sent === undefined) return softSkip('blocked', `the seam answered ${res.status} and the suite peer received no message — the host's A2A client never reached it`);
93
+ const label = labelOf(sent);
94
+ expect(Array.isArray(label), req(ID_REEMIT, DOC, `the onward message MUST carry the received label as metadata.openwop.permittedPurposes (got ${JSON.stringify(label)})`)).toBe(true);
95
+ const widened = (Array.isArray(label) ? label : []).filter((p) => !LABEL.includes(String(p)));
96
+ expect(widened, req(ID_REEMIT, DOC, `the onward label MUST NOT widen the received one (${JSON.stringify(LABEL)}); it added ${JSON.stringify(widened)}`)).toEqual([]);
97
+ } finally {
98
+ await peer.stop();
99
+ }
100
+ });
101
+
102
+ it('an empty label means no onward call, while an unlabelled control call goes through', async () => {
103
+ const g = await gate();
104
+ if (g) return skip(g);
105
+ const peer = new A2AFakePeer({ protocolVersions: ['1.0'] });
106
+ await peer.start();
107
+ try {
108
+ const control = await invoke(peer);
109
+ if (!('status' in control)) return skip(control);
110
+ const before = sends(peer).length;
111
+ if (before === 0) return softSkip('blocked', `the unlabelled control call answered ${control.status} but reached no peer, so a missing call under [] would prove nothing`);
112
+ const res = await invoke(peer, []);
113
+ if (!('status' in res)) return skip(res);
114
+ expect(sends(peer).length - before, req(ID_EMPTY, DOC, `a host MUST treat [] as no onward use: the seam answered ${res.status}, and the peer received ${sends(peer).length - before} message(s) for the []-labelled data`)).toBe(0);
115
+ } finally {
116
+ await peer.stop();
117
+ }
118
+ });
119
+ });
@@ -0,0 +1,68 @@
1
+ /**
2
+ * v2 — the front-end plugin boundary is observable (RFC 0238; `spec/v2/core/packs.md`
3
+ * §Front-end plugin packs). Gated on `uiPlugins.served`.
4
+ *
5
+ * The four `frontend-plugin-*` SECURITY invariants were witnessed only by the v1
6
+ * sample seam `POST /v1/host/sample/ui-plugin/rpc`; v2 mounts no plugin seam. This
7
+ * scenario reads the two normative operations instead, against the operator-installed
8
+ * `ui-plugin-pack-narrow` fixture (one plugin, `narrow`, declaring only
9
+ * `artifact.read` and no `connectSrc`):
10
+ *
11
+ * frame-isolated the frame's Content-Security-Policy `sandbox` admits
12
+ * `allow-scripts` and never `allow-same-origin`
13
+ * (`cross-origin-iframe` hosts only);
14
+ * frame-deny-egress the same policy sets `default-src 'none'` and admits no
15
+ * `connect-src` source;
16
+ * rpc-allowlist `artifact.write` and `host.navigate` (in the enum, not
17
+ * declared) return `method_not_allowed`; `host.exec` (outside
18
+ * the enum) is not executed; `artifact.read` succeeds (control);
19
+ * no-byok no dispatch response carries canary material.
20
+ *
21
+ * The legs live in `lib/ui-plugin-boundary-witness.ts`, proven against a double in
22
+ * `lib/ui-plugin-boundary-witness.test.ts`.
23
+ *
24
+ * Dispositions: no `uiPlugins.served` ⇒ every leg `inapplicable`. The fixture not
25
+ * installed (its frame or dispatch answers 404) ⇒ `inapplicable`: installing it is the
26
+ * operator's choice (RFC 0238 §D). An isolation other than `cross-origin-iframe` ⇒ the
27
+ * isolation leg `inapplicable` (RFC 0238 §Decisions 3).
28
+ *
29
+ * @see RFCS/0238-ui-plugin-observation-path.md §B, §C, §F
30
+ * @see spec/v2/core/packs.md §Front-end plugin packs
31
+ */
32
+
33
+ import { describe, it, expect } from 'vitest';
34
+ import { softSkip } from '../lib/soft-skip.js';
35
+ import { req } from '../lib/requirement-ids.js';
36
+ import { allowlistLeg, egressLeg, isolationLeg, noByokLeg } from '../lib/ui-plugin-boundary-witness.js';
37
+
38
+ const DOC = 'spec/v2/core/packs.md §Front-end plugin packs';
39
+ const ID_ISOLATED = 'openwop.requirement.0238.ui-plugin.frame-isolated';
40
+ const ID_EGRESS = 'openwop.requirement.0238.ui-plugin.frame-deny-egress';
41
+ const ID_ALLOWLIST = 'openwop.requirement.0238.ui-plugin.rpc-allowlist';
42
+ const ID_NO_BYOK = 'openwop.requirement.0238.ui-plugin.no-byok';
43
+
44
+ describe('v2 ui-plugin boundary (RFC 0238)', () => {
45
+ it('the frame is sandboxed without same-origin', async () => {
46
+ const out = await isolationLeg();
47
+ if (out.kind === 'skip') return softSkip(out.skip, out.reason);
48
+ expect(out.findings.join('; '), req(ID_ISOLATED, DOC, 'a cross-origin-iframe plugin frame MUST carry a sandbox directive admitting allow-scripts and never allow-same-origin')).toBe('');
49
+ });
50
+
51
+ it('the frame denies egress', async () => {
52
+ const out = await egressLeg();
53
+ if (out.kind === 'skip') return softSkip(out.skip, out.reason);
54
+ expect(out.findings.join('; '), req(ID_EGRESS, DOC, "the frame MUST set default-src 'none' and admit no connect-src source the plugin did not declare")).toBe('');
55
+ });
56
+
57
+ it('the dispatch refuses methods outside the allowlists and runs the declared one', async () => {
58
+ const out = await allowlistLeg();
59
+ if (out.kind === 'skip') return softSkip(out.skip, out.reason);
60
+ expect(out.findings.join('; '), req(ID_ALLOWLIST, DOC, "a method outside the plugin's declared hostApi or the advertised uiPlugins.hostApi MUST return method_not_allowed and MUST NOT execute")).toBe('');
61
+ });
62
+
63
+ it('no dispatch response carries credential material', async () => {
64
+ const out = await noByokLeg();
65
+ if (out.kind === 'skip') return softSkip(out.skip, out.reason);
66
+ expect(out.findings.join('; '), req(ID_NO_BYOK, DOC, 'a ui-plugin/1 response MUST NOT carry credential material across the boundary')).toBe('');
67
+ });
68
+ });
@@ -0,0 +1,34 @@
1
+ /**
2
+ * v2 — a host enforcing its advertised WASM memory ceiling
3
+ * (`spec/v2/core/node-pack-runtimes.md` §WASM: "When it advertises
4
+ * `maxMemoryBytes` it MUST enforce it and emit `cap.breached` with
5
+ * `kind: "wasm-memory"` on a breach").
6
+ *
7
+ * The v1 twin, `wasm-pack-memory-cap`, runs at major 1 only and needed a
8
+ * Rust-built pack. Here the operator installs the hand-written fixture pack at
9
+ * `conformance/fixtures/wasm-packs/misbehaving-memory/` (`vendor.openwop.misbehaving`,
10
+ * node `vendor.openwop.misbehaving.memory-bomb`, which grows memory until the
11
+ * host refuses) and advertises `conformance-wasm-pack-memory-cap-breach`.
12
+ *
13
+ * The leg lives in `lib/wasm-memory-cap-witness.ts`, proven against a double in
14
+ * `lib/wasm-memory-cap-witness.test.ts`.
15
+ *
16
+ * Dispositions: no `nodePackRuntimes.wasm`, no `maxMemoryBytes`, or the fixture
17
+ * not advertised ⇒ `inapplicable`.
18
+ *
19
+ * @see spec/v2/core/node-pack-runtimes.md §WASM
20
+ * @see RFCS/0008-wasm-abi.md §K
21
+ */
22
+
23
+ import { describe, it, expect } from 'vitest';
24
+ import { softSkip } from '../lib/soft-skip.js';
25
+ import { req } from '../lib/requirement-ids.js';
26
+ import { memoryCapLeg } from '../lib/wasm-memory-cap-witness.js';
27
+
28
+ describe('v2 WASM memory ceiling (node-pack-runtimes.md §WASM)', () => {
29
+ it('a module exceeding maxMemoryBytes produces cap.breached wasm-memory and fails the run', async () => {
30
+ const out = await memoryCapLeg();
31
+ if (out.kind === 'skip') return softSkip(out.skip, out.reason);
32
+ expect(out.findings.join('; '), req('openwop.requirement.node-pack-runtimes.wasm-memory-cap', 'spec/v2/core/node-pack-runtimes.md §WASM', 'a host advertising maxMemoryBytes MUST enforce it and emit cap.breached with kind wasm-memory on a breach')).toBe('');
33
+ });
34
+ });