From 66b341f9588297f4891ee120cc1887c892f0e531 Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Tue, 14 Apr 2026 18:04:52 +0200 Subject: [PATCH 1/9] feat(ocap-kernel): make vat global allowlist configurable and expand endowments Extract the hardcoded allowedGlobals from VatSupervisor into a dedicated endowments module with a configurable DEFAULT_ALLOWED_GLOBALS constant. The allowlist now covers all host/Web APIs that are absent from SES compartments (TextEncoder, TextDecoder, URL, URLSearchParams, atob, btoa, AbortController, AbortSignal, setTimeout, clearTimeout, Date). JS intrinsics (ArrayBuffer, BigInt, typed arrays, Intl) are excluded since they are already available in every SES Compartment. VatSupervisor now accepts an optional allowedGlobals parameter (defaults to DEFAULT_ALLOWED_GLOBALS) and logs a warning when a vat requests an unknown global. Includes e2e tests verifying that endowed globals work inside real SES compartments and that host APIs are genuinely absent when not endowed. Closes #813 Co-Authored-By: Claude Opus 4.6 (1M context) --- .../kernel-test/src/endowment-globals.test.ts | 128 ++++++++++++++++++ .../kernel-test/src/vats/endowment-globals.ts | 84 ++++++++++++ packages/ocap-kernel/src/index.test.ts | 1 + packages/ocap-kernel/src/index.ts | 1 + .../src/vats/VatSupervisor.test.ts | 77 +++++++++++ .../ocap-kernel/src/vats/VatSupervisor.ts | 25 ++-- packages/ocap-kernel/src/vats/endowments.ts | 31 +++++ 7 files changed, 336 insertions(+), 11 deletions(-) create mode 100644 packages/kernel-test/src/endowment-globals.test.ts create mode 100644 packages/kernel-test/src/vats/endowment-globals.ts create mode 100644 packages/ocap-kernel/src/vats/endowments.ts diff --git a/packages/kernel-test/src/endowment-globals.test.ts b/packages/kernel-test/src/endowment-globals.test.ts new file mode 100644 index 0000000000..f4b6f7b46c --- /dev/null +++ b/packages/kernel-test/src/endowment-globals.test.ts @@ -0,0 +1,128 @@ +import { makeSQLKernelDatabase } from '@metamask/kernel-store/sqlite/nodejs'; +import { waitUntilQuiescent } from '@metamask/kernel-utils'; +import type { KRef, VatId } from '@metamask/ocap-kernel'; +import { getWorkerFile } from '@ocap/nodejs-test-workers'; +import { describe, expect, it } from 'vitest'; + +import { + extractTestLogs, + getBundleSpec, + makeKernel, + makeTestLogger, +} from './utils.ts'; + +describe('global endowments', () => { + const vatId: VatId = 'v1'; + const v1Root: KRef = 'ko4'; + + const setup = async (globals: string[]) => { + const { logger, entries } = makeTestLogger(); + const database = await makeSQLKernelDatabase({}); + const kernel = await makeKernel( + database, + true, + logger, + getWorkerFile('mock-fetch'), + ); + + await kernel.launchSubcluster({ + bootstrap: 'main', + vats: { + main: { + bundleSpec: getBundleSpec('endowment-globals'), + parameters: {}, + globals, + }, + }, + }); + await waitUntilQuiescent(); + + return { kernel, entries }; + }; + + it('can use TextEncoder and TextDecoder', async () => { + const { kernel, entries } = await setup(['TextEncoder', 'TextDecoder']); + + await kernel.queueMessage(v1Root, 'testTextCodec', []); + await waitUntilQuiescent(); + + const logs = extractTestLogs(entries, vatId); + expect(logs).toContain('textCodec: hello'); + }); + + it('can use URL and URLSearchParams', async () => { + const { kernel, entries } = await setup(['URL', 'URLSearchParams']); + + await kernel.queueMessage(v1Root, 'testUrl', []); + await waitUntilQuiescent(); + + const logs = extractTestLogs(entries, vatId); + expect(logs).toContain('url: /path params: 10'); + }); + + it('can use atob and btoa', async () => { + const { kernel, entries } = await setup(['atob', 'btoa']); + + await kernel.queueMessage(v1Root, 'testBase64', []); + await waitUntilQuiescent(); + + const logs = extractTestLogs(entries, vatId); + expect(logs).toContain('base64: hello world'); + }); + + it('can use AbortController and AbortSignal', async () => { + const { kernel, entries } = await setup(['AbortController', 'AbortSignal']); + + await kernel.queueMessage(v1Root, 'testAbort', []); + await waitUntilQuiescent(); + + const logs = extractTestLogs(entries, vatId); + expect(logs).toContain('abort: before=false after=true'); + }); + + it('can use setTimeout and clearTimeout', async () => { + const { kernel, entries } = await setup(['setTimeout', 'clearTimeout']); + + await kernel.queueMessage(v1Root, 'testTimers', []); + await waitUntilQuiescent(); + + const logs = extractTestLogs(entries, vatId); + expect(logs).toContain('timer: fired'); + }); + + it('can use real Date (not tamed)', async () => { + const { kernel, entries } = await setup(['Date']); + + await kernel.queueMessage(v1Root, 'testDate', []); + await waitUntilQuiescent(); + + const logs = extractTestLogs(entries, vatId); + expect(logs).toContain('date: isReal=true'); + }); + + describe('host APIs are absent when not endowed', () => { + // These are Web/host APIs that are NOT JS intrinsics — they should + // be genuinely absent from a SES compartment unless explicitly endowed. + it.each([ + 'TextEncoder', + 'TextDecoder', + 'URL', + 'URLSearchParams', + 'atob', + 'btoa', + 'AbortController', + 'AbortSignal', + 'setTimeout', + 'clearTimeout', + ])('does not have %s without endowing it', async (name) => { + // Launch with no globals at all + const { kernel, entries } = await setup([]); + + await kernel.queueMessage(v1Root, 'checkGlobal', [name]); + await waitUntilQuiescent(); + + const logs = extractTestLogs(entries, vatId); + expect(logs).toContain(`checkGlobal: ${name}=false`); + }); + }); +}); diff --git a/packages/kernel-test/src/vats/endowment-globals.ts b/packages/kernel-test/src/vats/endowment-globals.ts new file mode 100644 index 0000000000..88a4f07b8d --- /dev/null +++ b/packages/kernel-test/src/vats/endowment-globals.ts @@ -0,0 +1,84 @@ +import { makeDefaultExo } from '@metamask/kernel-utils/exo'; + +import { unwrapTestLogger } from '../test-powers.ts'; +import type { TestPowers } from '../test-powers.ts'; + +/** + * Build a root object for a vat that exercises global endowments. + * + * @param vatPowers - The powers of the vat. + * @returns The root object. + */ +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +export function buildRootObject(vatPowers: TestPowers) { + const tlog = unwrapTestLogger(vatPowers, 'endowment-globals'); + + tlog('buildRootObject'); + + const root = makeDefaultExo('root', { + bootstrap: () => { + tlog('bootstrap'); + }, + + testTextCodec: () => { + const encoder = new TextEncoder(); + const encoded = encoder.encode('hello'); + const decoder = new TextDecoder(); + const decoded = decoder.decode(encoded); + tlog(`textCodec: ${decoded}`); + return decoded; + }, + + testUrl: () => { + const url = new URL('https://example.com/path?a=1'); + url.searchParams.set('b', '2'); + const params = new URLSearchParams('x=10&y=20'); + tlog(`url: ${url.pathname} params: ${params.get('x')}`); + return url.toString(); + }, + + testBase64: () => { + const encoded = btoa('hello world'); + const decoded = atob(encoded); + tlog(`base64: ${decoded}`); + return decoded; + }, + + testAbort: () => { + const controller = new AbortController(); + const { signal } = controller; + const { aborted } = signal; + controller.abort('test reason'); + tlog(`abort: before=${String(aborted)} after=${String(signal.aborted)}`); + return signal.aborted; + }, + + testTimers: async () => { + return new Promise((resolve) => { + setTimeout(() => { + tlog('timer: fired'); + resolve('fired'); + }, 10); + }); + }, + + testDate: () => { + const now = Date.now(); + const isReal = !Number.isNaN(now) && now > 0; + tlog(`date: isReal=${String(isReal)}`); + return isReal; + }, + + checkGlobal: (name: string) => { + // In a SES compartment, globalThis points to the compartment's own + // global object, so this correctly detects whether an endowment was + // provided. Intrinsics (e.g. ArrayBuffer) are always present; + // host/Web APIs (e.g. TextEncoder) are only present if endowed. + const exists = name in globalThis; + tlog(`checkGlobal: ${name}=${String(exists)}`); + return exists; + }, + }); + + return root; +} diff --git a/packages/ocap-kernel/src/index.test.ts b/packages/ocap-kernel/src/index.test.ts index 8161df04cf..ceb8a8c95c 100644 --- a/packages/ocap-kernel/src/index.test.ts +++ b/packages/ocap-kernel/src/index.test.ts @@ -7,6 +7,7 @@ describe('index', () => { expect(Object.keys(indexModule).sort()).toStrictEqual([ 'CapDataStruct', 'ClusterConfigStruct', + 'DEFAULT_ALLOWED_GLOBALS', 'Kernel', 'KernelStatusStruct', 'SubclusterStruct', diff --git a/packages/ocap-kernel/src/index.ts b/packages/ocap-kernel/src/index.ts index 246ad3aeb3..d1075ea61b 100644 --- a/packages/ocap-kernel/src/index.ts +++ b/packages/ocap-kernel/src/index.ts @@ -1,6 +1,7 @@ export { Kernel } from './Kernel.ts'; export { VatHandle } from './vats/VatHandle.ts'; export { VatSupervisor } from './vats/VatSupervisor.ts'; +export { DEFAULT_ALLOWED_GLOBALS } from './vats/endowments.ts'; export { initTransport } from './remotes/platform/transport.ts'; export type { IOChannel, IOChannelFactory } from './io/types.ts'; export type { diff --git a/packages/ocap-kernel/src/vats/VatSupervisor.test.ts b/packages/ocap-kernel/src/vats/VatSupervisor.test.ts index 2a327838c9..0b158cc03f 100644 --- a/packages/ocap-kernel/src/vats/VatSupervisor.test.ts +++ b/packages/ocap-kernel/src/vats/VatSupervisor.test.ts @@ -6,7 +6,9 @@ import { rpcErrors } from '@metamask/rpc-errors'; import { TestDuplexStream } from '@ocap/repo-tools/test-utils/streams'; import { describe, it, expect, vi } from 'vitest'; +import { DEFAULT_ALLOWED_GLOBALS } from './endowments.ts'; import { VatSupervisor } from './VatSupervisor.ts'; +import type { FetchBlob } from './VatSupervisor.ts'; vi.mock('./syscall.ts', () => ({ makeSupervisorSyscall: vi.fn(() => ({ @@ -28,12 +30,16 @@ const makeVatSupervisor = async ({ vatPowers, makePlatform, platformOptions, + allowedGlobals, + fetchBlob, }: { dispatch?: (input: unknown) => void | Promise; logger?: Logger; vatPowers?: Record; makePlatform?: PlatformFactory; platformOptions?: Record; + allowedGlobals?: Record; + fetchBlob?: FetchBlob; } = {}): Promise<{ supervisor: VatSupervisor; stream: TestDuplexStream; @@ -54,6 +60,8 @@ const makeVatSupervisor = async ({ vatPowers: vatPowers ?? {}, makePlatform: makePlatform ?? defaultMakePlatform, platformOptions: platformOptions ?? {}, + allowedGlobals, + fetchBlob, }), stream: kernelStream, }; @@ -163,4 +171,73 @@ describe('VatSupervisor', () => { expect(supervisor).toBeInstanceOf(VatSupervisor); }); }); + + describe('DEFAULT_ALLOWED_GLOBALS', () => { + it('contains the expected global names', () => { + expect(Object.keys(DEFAULT_ALLOWED_GLOBALS).sort()).toStrictEqual([ + 'AbortController', + 'AbortSignal', + 'Date', + 'TextDecoder', + 'TextEncoder', + 'URL', + 'URLSearchParams', + 'atob', + 'btoa', + 'clearTimeout', + 'setTimeout', + ]); + }); + + it('is frozen', () => { + expect(Object.isFrozen(DEFAULT_ALLOWED_GLOBALS)).toBe(true); + }); + }); + + describe('allowedGlobals configuration', () => { + it('accepts a custom allowedGlobals parameter', async () => { + const { supervisor } = await makeVatSupervisor({ + allowedGlobals: { CustomGlobal: 'custom-value' }, + }); + expect(supervisor).toBeInstanceOf(VatSupervisor); + }); + + it('logs a warning when a vat requests an unknown global', async () => { + const logger = { + warn: vi.fn(), + error: vi.fn(), + subLogger: vi.fn(() => logger), + } as unknown as Logger; + + const mockFetchBlob: FetchBlob = vi.fn().mockResolvedValue({ + ok: true, + text: vi.fn().mockResolvedValue(''), + }); + + const { stream } = await makeVatSupervisor({ + logger, + allowedGlobals: { Date: globalThis.Date }, + fetchBlob: mockFetchBlob, + }); + + await stream.receiveInput({ + id: 'test-init', + method: 'initVat', + params: { + vatConfig: { + bundleSpec: 'test.bundle', + parameters: {}, + globals: ['Date', 'UnknownThing'], + }, + state: [], + }, + jsonrpc: '2.0', + }); + await delay(50); + + expect(logger.warn).toHaveBeenCalledWith( + 'Vat "test-id" requested unknown global "UnknownThing"', + ); + }); + }); }); diff --git a/packages/ocap-kernel/src/vats/VatSupervisor.ts b/packages/ocap-kernel/src/vats/VatSupervisor.ts index d701fb247a..903a242cb5 100644 --- a/packages/ocap-kernel/src/vats/VatSupervisor.ts +++ b/packages/ocap-kernel/src/vats/VatSupervisor.ts @@ -28,6 +28,7 @@ import { } from '@metamask/utils'; import { loadBundle } from './bundle-loader.ts'; +import { DEFAULT_ALLOWED_GLOBALS } from './endowments.ts'; import { makeGCAndFinalize } from '../garbage-collection/gc-finalize.ts'; import { makeDummyMeterControl } from '../liveslots/meter-control.ts'; import { makeSupervisorSyscall } from '../liveslots/syscall.ts'; @@ -58,6 +59,7 @@ type SupervisorConstructorProps = { platformOptions?: Record; vatPowers?: Record | undefined; fetchBlob?: FetchBlob; + allowedGlobals?: Record; }; const marshal = makeMarshal(undefined, undefined, { @@ -104,6 +106,9 @@ export class VatSupervisor { /** Options to pass to the makePlatform function. */ readonly #platformOptions: Record; + /** The set of globals that vats are allowed to request. */ + readonly #allowedGlobals: Record; + /** * Construct a new VatSupervisor instance. * @@ -115,6 +120,7 @@ export class VatSupervisor { * @param params.fetchBlob - Function to fetch the user code bundle for this vat. * @param params.makePlatform - Function to create the platform for this vat. * @param params.platformOptions - Options to pass to the makePlatform function. + * @param params.allowedGlobals - Map of allowed globals. Defaults to {@link DEFAULT_ALLOWED_GLOBALS}. */ constructor({ id, @@ -126,6 +132,7 @@ export class VatSupervisor { }, platformOptions, fetchBlob, + allowedGlobals = DEFAULT_ALLOWED_GLOBALS, }: SupervisorConstructorProps) { this.id = id; this.#kernelStream = kernelStream; @@ -137,6 +144,7 @@ export class VatSupervisor { this.#fetchBlob = fetchBlob ?? defaultFetchBlob; this.#platformOptions = platformOptions ?? {}; this.#makePlatform = makePlatform; + this.#allowedGlobals = allowedGlobals; this.#rpcClient = new RpcClient( vatSyscallMethodSpecs, @@ -298,21 +306,16 @@ export class VatSupervisor { const { bundleSpec, parameters, platformConfig, globals } = vatConfig; - // Map of allowed global names to their values - const allowedGlobals: Record = { - Date: globalThis.Date, - TextEncoder: globalThis.TextEncoder, - TextDecoder: globalThis.TextDecoder, - setTimeout: globalThis.setTimeout.bind(globalThis), - clearTimeout: globalThis.clearTimeout.bind(globalThis), - }; - // Build additional endowments from globals list const requestedGlobals: Record = {}; if (globals) { for (const name of globals) { - if (hasProperty(allowedGlobals, name)) { - requestedGlobals[name] = allowedGlobals[name]; + if (hasProperty(this.#allowedGlobals, name)) { + requestedGlobals[name] = this.#allowedGlobals[name]; + } else { + this.#logger.warn( + `Vat "${this.id}" requested unknown global "${name}"`, + ); } } } diff --git a/packages/ocap-kernel/src/vats/endowments.ts b/packages/ocap-kernel/src/vats/endowments.ts new file mode 100644 index 0000000000..ade626a84c --- /dev/null +++ b/packages/ocap-kernel/src/vats/endowments.ts @@ -0,0 +1,31 @@ +/** + * The default set of host/Web API globals that vats may request as endowments. + * These are NOT ECMAScript intrinsics and are therefore absent from SES + * Compartments unless explicitly provided. + * + * JS intrinsics (e.g. `ArrayBuffer`, `BigInt`, `Intl`, typed arrays) are + * already available in every Compartment and do not need to be endowed. + * `Date` is an intrinsic too, but lockdown tames it (`Date.now()` returns + * `NaN`); passing it here restores the real implementation. + * + * Functions that require a specific `this` context are bound to `globalThis` + * before hardening. + */ +export const DEFAULT_ALLOWED_GLOBALS: Record = harden({ + // Timers (host API) + setTimeout: globalThis.setTimeout.bind(globalThis), + clearTimeout: globalThis.clearTimeout.bind(globalThis), + + // Date (intrinsic, but tamed by lockdown — endowing restores real Date.now) + Date: globalThis.Date, + + // Web APIs + TextEncoder: globalThis.TextEncoder, + TextDecoder: globalThis.TextDecoder, + URL: globalThis.URL, + URLSearchParams: globalThis.URLSearchParams, + atob: globalThis.atob.bind(globalThis), + btoa: globalThis.btoa.bind(globalThis), + AbortController: globalThis.AbortController, + AbortSignal: globalThis.AbortSignal, +}); From ac4fd52b3d94e304a116af45544ece5a9a9a94e2 Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Tue, 14 Apr 2026 18:14:42 +0200 Subject: [PATCH 2/9] fix: address review findings - harden(allowedGlobals) in constructor to prevent mutation of custom maps - move DEFAULT_ALLOWED_GLOBALS tests to co-located endowments.test.ts - add happy-path test: no warning when all globals are known - add tamed Date negative test: Date.now throws in secure mode without endowment Co-Authored-By: Claude Opus 4.6 (1M context) --- .../kernel-test/src/endowment-globals.test.ts | 8 +++ .../src/vats/VatSupervisor.test.ts | 59 +++++++++++-------- .../ocap-kernel/src/vats/VatSupervisor.ts | 2 +- .../ocap-kernel/src/vats/endowments.test.ts | 25 ++++++++ 4 files changed, 70 insertions(+), 24 deletions(-) create mode 100644 packages/ocap-kernel/src/vats/endowments.test.ts diff --git a/packages/kernel-test/src/endowment-globals.test.ts b/packages/kernel-test/src/endowment-globals.test.ts index f4b6f7b46c..0ec9036990 100644 --- a/packages/kernel-test/src/endowment-globals.test.ts +++ b/packages/kernel-test/src/endowment-globals.test.ts @@ -124,5 +124,13 @@ describe('global endowments', () => { const logs = extractTestLogs(entries, vatId); expect(logs).toContain(`checkGlobal: ${name}=false`); }); + + it('throws when calling tamed Date.now without endowing Date', async () => { + const { kernel } = await setup([]); + + await expect(kernel.queueMessage(v1Root, 'testDate', [])).rejects.toThrow( + 'secure mode', + ); + }); }); }); diff --git a/packages/ocap-kernel/src/vats/VatSupervisor.test.ts b/packages/ocap-kernel/src/vats/VatSupervisor.test.ts index 0b158cc03f..7e22247e0a 100644 --- a/packages/ocap-kernel/src/vats/VatSupervisor.test.ts +++ b/packages/ocap-kernel/src/vats/VatSupervisor.test.ts @@ -6,7 +6,6 @@ import { rpcErrors } from '@metamask/rpc-errors'; import { TestDuplexStream } from '@ocap/repo-tools/test-utils/streams'; import { describe, it, expect, vi } from 'vitest'; -import { DEFAULT_ALLOWED_GLOBALS } from './endowments.ts'; import { VatSupervisor } from './VatSupervisor.ts'; import type { FetchBlob } from './VatSupervisor.ts'; @@ -172,28 +171,6 @@ describe('VatSupervisor', () => { }); }); - describe('DEFAULT_ALLOWED_GLOBALS', () => { - it('contains the expected global names', () => { - expect(Object.keys(DEFAULT_ALLOWED_GLOBALS).sort()).toStrictEqual([ - 'AbortController', - 'AbortSignal', - 'Date', - 'TextDecoder', - 'TextEncoder', - 'URL', - 'URLSearchParams', - 'atob', - 'btoa', - 'clearTimeout', - 'setTimeout', - ]); - }); - - it('is frozen', () => { - expect(Object.isFrozen(DEFAULT_ALLOWED_GLOBALS)).toBe(true); - }); - }); - describe('allowedGlobals configuration', () => { it('accepts a custom allowedGlobals parameter', async () => { const { supervisor } = await makeVatSupervisor({ @@ -202,6 +179,42 @@ describe('VatSupervisor', () => { expect(supervisor).toBeInstanceOf(VatSupervisor); }); + it('does not warn when all requested globals are known', async () => { + const logger = { + warn: vi.fn(), + error: vi.fn(), + subLogger: vi.fn(() => logger), + } as unknown as Logger; + + const mockFetchBlob: FetchBlob = vi.fn().mockResolvedValue({ + ok: true, + text: vi.fn().mockResolvedValue(''), + }); + + const { stream } = await makeVatSupervisor({ + logger, + allowedGlobals: { Date: globalThis.Date }, + fetchBlob: mockFetchBlob, + }); + + await stream.receiveInput({ + id: 'test-init', + method: 'initVat', + params: { + vatConfig: { + bundleSpec: 'test.bundle', + parameters: {}, + globals: ['Date'], + }, + state: [], + }, + jsonrpc: '2.0', + }); + await delay(50); + + expect(logger.warn).not.toHaveBeenCalled(); + }); + it('logs a warning when a vat requests an unknown global', async () => { const logger = { warn: vi.fn(), diff --git a/packages/ocap-kernel/src/vats/VatSupervisor.ts b/packages/ocap-kernel/src/vats/VatSupervisor.ts index 903a242cb5..23925b35e7 100644 --- a/packages/ocap-kernel/src/vats/VatSupervisor.ts +++ b/packages/ocap-kernel/src/vats/VatSupervisor.ts @@ -144,7 +144,7 @@ export class VatSupervisor { this.#fetchBlob = fetchBlob ?? defaultFetchBlob; this.#platformOptions = platformOptions ?? {}; this.#makePlatform = makePlatform; - this.#allowedGlobals = allowedGlobals; + this.#allowedGlobals = harden(allowedGlobals); this.#rpcClient = new RpcClient( vatSyscallMethodSpecs, diff --git a/packages/ocap-kernel/src/vats/endowments.test.ts b/packages/ocap-kernel/src/vats/endowments.test.ts new file mode 100644 index 0000000000..55e7c394cc --- /dev/null +++ b/packages/ocap-kernel/src/vats/endowments.test.ts @@ -0,0 +1,25 @@ +import { describe, it, expect } from 'vitest'; + +import { DEFAULT_ALLOWED_GLOBALS } from './endowments.ts'; + +describe('DEFAULT_ALLOWED_GLOBALS', () => { + it('contains the expected global names', () => { + expect(Object.keys(DEFAULT_ALLOWED_GLOBALS).sort()).toStrictEqual([ + 'AbortController', + 'AbortSignal', + 'Date', + 'TextDecoder', + 'TextEncoder', + 'URL', + 'URLSearchParams', + 'atob', + 'btoa', + 'clearTimeout', + 'setTimeout', + ]); + }); + + it('is frozen', () => { + expect(Object.isFrozen(DEFAULT_ALLOWED_GLOBALS)).toBe(true); + }); +}); From 6c84dc33ac8f8b5d36b26bdf25caf03efddc2c33 Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Tue, 14 Apr 2026 18:15:05 +0200 Subject: [PATCH 3/9] docs: Update changelogs Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/ocap-kernel/CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/ocap-kernel/CHANGELOG.md b/packages/ocap-kernel/CHANGELOG.md index e3d26e6726..f2509031ee 100644 --- a/packages/ocap-kernel/CHANGELOG.md +++ b/packages/ocap-kernel/CHANGELOG.md @@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Export `DEFAULT_ALLOWED_GLOBALS` constant containing the default set of host/Web API endowments available to vats ([#931](https://github.com/MetaMask/ocap-kernel/pull/931)) +- Add configurable `allowedGlobals` parameter to `VatSupervisor` constructor, defaulting to `DEFAULT_ALLOWED_GLOBALS` ([#931](https://github.com/MetaMask/ocap-kernel/pull/931)) +- Expand available vat endowments with `URL`, `URLSearchParams`, `atob`, `btoa`, `AbortController`, and `AbortSignal` ([#931](https://github.com/MetaMask/ocap-kernel/pull/931)) +- Log a warning when a vat requests an unknown global ([#931](https://github.com/MetaMask/ocap-kernel/pull/931)) + ### Changed - Bound relay hints in OCAP URLs to a maximum of 3 and cap the relay pool at 20 entries with eviction of oldest non-bootstrap relays ([#929](https://github.com/MetaMask/ocap-kernel/pull/929)) From 2c71221f70086ec28fad50bbff83b84dbc56cbc1 Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Tue, 14 Apr 2026 18:19:50 +0200 Subject: [PATCH 4/9] docs: fix PR number in changelog Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/ocap-kernel/CHANGELOG.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/ocap-kernel/CHANGELOG.md b/packages/ocap-kernel/CHANGELOG.md index f2509031ee..3b76586173 100644 --- a/packages/ocap-kernel/CHANGELOG.md +++ b/packages/ocap-kernel/CHANGELOG.md @@ -9,10 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Export `DEFAULT_ALLOWED_GLOBALS` constant containing the default set of host/Web API endowments available to vats ([#931](https://github.com/MetaMask/ocap-kernel/pull/931)) -- Add configurable `allowedGlobals` parameter to `VatSupervisor` constructor, defaulting to `DEFAULT_ALLOWED_GLOBALS` ([#931](https://github.com/MetaMask/ocap-kernel/pull/931)) -- Expand available vat endowments with `URL`, `URLSearchParams`, `atob`, `btoa`, `AbortController`, and `AbortSignal` ([#931](https://github.com/MetaMask/ocap-kernel/pull/931)) -- Log a warning when a vat requests an unknown global ([#931](https://github.com/MetaMask/ocap-kernel/pull/931)) +- Export `DEFAULT_ALLOWED_GLOBALS` constant containing the default set of host/Web API endowments available to vats ([#933](https://github.com/MetaMask/ocap-kernel/pull/933)) +- Add configurable `allowedGlobals` parameter to `VatSupervisor` constructor, defaulting to `DEFAULT_ALLOWED_GLOBALS` ([#933](https://github.com/MetaMask/ocap-kernel/pull/933)) +- Expand available vat endowments with `URL`, `URLSearchParams`, `atob`, `btoa`, `AbortController`, and `AbortSignal` ([#933](https://github.com/MetaMask/ocap-kernel/pull/933)) +- Log a warning when a vat requests an unknown global ([#933](https://github.com/MetaMask/ocap-kernel/pull/933)) ### Changed From 81008159bf5e6748820f7876e0e07aa35dadd5b4 Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Tue, 14 Apr 2026 18:22:10 +0200 Subject: [PATCH 5/9] docs: consolidate changelog entries Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/ocap-kernel/CHANGELOG.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/ocap-kernel/CHANGELOG.md b/packages/ocap-kernel/CHANGELOG.md index 3b76586173..0f43434a7d 100644 --- a/packages/ocap-kernel/CHANGELOG.md +++ b/packages/ocap-kernel/CHANGELOG.md @@ -9,10 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Export `DEFAULT_ALLOWED_GLOBALS` constant containing the default set of host/Web API endowments available to vats ([#933](https://github.com/MetaMask/ocap-kernel/pull/933)) -- Add configurable `allowedGlobals` parameter to `VatSupervisor` constructor, defaulting to `DEFAULT_ALLOWED_GLOBALS` ([#933](https://github.com/MetaMask/ocap-kernel/pull/933)) -- Expand available vat endowments with `URL`, `URLSearchParams`, `atob`, `btoa`, `AbortController`, and `AbortSignal` ([#933](https://github.com/MetaMask/ocap-kernel/pull/933)) -- Log a warning when a vat requests an unknown global ([#933](https://github.com/MetaMask/ocap-kernel/pull/933)) +- Make vat global allowlist configurable and expand available endowments ([#933](https://github.com/MetaMask/ocap-kernel/pull/933)) + - Export `DEFAULT_ALLOWED_GLOBALS` with `URL`, `URLSearchParams`, `atob`, `btoa`, `AbortController`, and `AbortSignal` in addition to the existing globals + - Accept optional `allowedGlobals` on `VatSupervisor` for custom allowlists + - Log a warning when a vat requests an unknown global ### Changed From 09f3e017e6768edd3e7426dc53c13c407ec7bea9 Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Tue, 14 Apr 2026 18:57:38 +0200 Subject: [PATCH 6/9] feat(ocap-kernel): plumb allowedGlobalNames from Kernel.make() to VatSupervisor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add allowedGlobalNames option to Kernel.make() so the kernel owner can restrict which globals are available to vats. The names flow through VatManager → VatHandle → initVat RPC to VatSupervisor, which filters DEFAULT_ALLOWED_GLOBALS by the received names. When omitted, all defaults remain available. Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/ocap-kernel/src/Kernel.ts | 5 +++++ packages/ocap-kernel/src/rpc/vat/initVat.ts | 16 ++++++++++++++-- packages/ocap-kernel/src/vats/VatHandle.ts | 10 ++++++++++ packages/ocap-kernel/src/vats/VatManager.ts | 8 ++++++++ packages/ocap-kernel/src/vats/VatSupervisor.ts | 17 ++++++++++++++--- 5 files changed, 51 insertions(+), 5 deletions(-) diff --git a/packages/ocap-kernel/src/Kernel.ts b/packages/ocap-kernel/src/Kernel.ts index 44978e0da3..e6df93e02b 100644 --- a/packages/ocap-kernel/src/Kernel.ts +++ b/packages/ocap-kernel/src/Kernel.ts @@ -103,6 +103,7 @@ export class Kernel { * @param options.keySeed - Optional seed for libp2p key generation. * @param options.mnemonic - Optional BIP39 mnemonic for deriving the kernel identity. * @param options.ioChannelFactory - Optional factory for creating IO channels. + * @param options.allowedGlobalNames - Optional list of allowed global names for vat endowments. */ // eslint-disable-next-line no-restricted-syntax private constructor( @@ -114,6 +115,7 @@ export class Kernel { keySeed?: string | undefined; mnemonic?: string | undefined; ioChannelFactory?: IOChannelFactory; + allowedGlobalNames?: string[]; } = {}, ) { this.#platformServices = platformServices; @@ -145,6 +147,7 @@ export class Kernel { kernelStore: this.#kernelStore, kernelQueue: this.#kernelQueue, logger: this.#logger.subLogger({ tags: ['VatManager'] }), + allowedGlobalNames: options.allowedGlobalNames, }); this.#remoteManager = new RemoteManager({ @@ -229,6 +232,7 @@ export class Kernel { * @param options.mnemonic - Optional BIP39 mnemonic for deriving the kernel identity. * @param options.ioChannelFactory - Optional factory for creating IO channels. * @param options.systemSubclusters - Optional array of system subcluster configurations. + * @param options.allowedGlobalNames - Optional list of allowed global names for vat endowments. When set, only these names from DEFAULT_ALLOWED_GLOBALS are available to vats. * @returns A promise for the new kernel instance. */ static async make( @@ -241,6 +245,7 @@ export class Kernel { mnemonic?: string | undefined; ioChannelFactory?: IOChannelFactory; systemSubclusters?: SystemSubclusterConfig[]; + allowedGlobalNames?: string[]; } = {}, ): Promise { const kernel = new Kernel(platformServices, kernelDatabase, options); diff --git a/packages/ocap-kernel/src/rpc/vat/initVat.ts b/packages/ocap-kernel/src/rpc/vat/initVat.ts index e46a124c7f..51606c8d8d 100644 --- a/packages/ocap-kernel/src/rpc/vat/initVat.ts +++ b/packages/ocap-kernel/src/rpc/vat/initVat.ts @@ -1,5 +1,11 @@ import type { MethodSpec, Handler } from '@metamask/kernel-rpc-methods'; -import { array, object, string, tuple } from '@metamask/superstruct'; +import { + array, + exactOptional, + object, + string, + tuple, +} from '@metamask/superstruct'; import type { Infer } from '@metamask/superstruct'; import { VatDeliveryResultStruct } from './shared.ts'; @@ -9,6 +15,7 @@ import type { VatConfig, VatDeliveryResult } from '../../types.ts'; const paramsStruct = object({ vatConfig: VatConfigStruct, state: array(tuple([string(), string()])), + allowedGlobalNames: exactOptional(array(string())), }); type Params = Infer; @@ -28,6 +35,7 @@ export const initVatSpec: InitVatSpec = { export type InitVat = ( vatConfig: VatConfig, state: Map, + allowedGlobalNames: string[] | undefined, ) => Promise; type InitVatHooks = { @@ -45,6 +53,10 @@ export const initVatHandler: InitVatHandler = { ...initVatSpec, hooks: { initVat: true }, implementation: async ({ initVat }, params) => { - return await initVat(params.vatConfig, new Map(params.state)); + return await initVat( + params.vatConfig, + new Map(params.state), + params.allowedGlobalNames, + ); }, }; diff --git a/packages/ocap-kernel/src/vats/VatHandle.ts b/packages/ocap-kernel/src/vats/VatHandle.ts index ad004e38d9..4da7f6a8d9 100644 --- a/packages/ocap-kernel/src/vats/VatHandle.ts +++ b/packages/ocap-kernel/src/vats/VatHandle.ts @@ -45,6 +45,7 @@ type VatConstructorProps = { kernelStore: KernelStore; kernelQueue: KernelQueue; logger?: Logger | undefined; + allowedGlobalNames?: string[] | undefined; }; /** @@ -63,6 +64,9 @@ export class VatHandle implements EndpointHandle { /** Logger for outputting messages (such as errors) to the console */ readonly #logger: Logger | undefined; + /** Optional list of allowed global names for vat endowments */ + readonly #allowedGlobalNames: string[] | undefined; + /** Storage holding the kernel's persistent state */ readonly #kernelStore: KernelStore; @@ -89,6 +93,7 @@ export class VatHandle implements EndpointHandle { * @param params.kernelStore - The kernel's persistent state store. * @param params.kernelQueue - The kernel's queue. * @param params.logger - Optional logger for error and diagnostic output. + * @param params.allowedGlobalNames - Optional list of allowed global names for vat endowments. */ // eslint-disable-next-line no-restricted-syntax private constructor({ @@ -98,10 +103,12 @@ export class VatHandle implements EndpointHandle { kernelStore, kernelQueue, logger, + allowedGlobalNames, }: VatConstructorProps) { this.vatId = vatId; this.config = vatConfig; this.#logger = logger; + this.#allowedGlobalNames = allowedGlobalNames; this.#vatStream = vatStream; this.#kernelStore = kernelStore; this.#vatStore = kernelStore.makeVatStore(vatId); @@ -171,6 +178,9 @@ export class VatHandle implements EndpointHandle { params: { vatConfig: this.config, state: this.#vatStore.getKVData(), + ...(this.#allowedGlobalNames + ? { allowedGlobalNames: this.#allowedGlobalNames } + : {}), }, }); } diff --git a/packages/ocap-kernel/src/vats/VatManager.ts b/packages/ocap-kernel/src/vats/VatManager.ts index 1f1da7580a..d315c44dc3 100644 --- a/packages/ocap-kernel/src/vats/VatManager.ts +++ b/packages/ocap-kernel/src/vats/VatManager.ts @@ -25,6 +25,7 @@ type VatManagerOptions = { kernelStore: KernelStore; kernelQueue: KernelQueue; logger?: Logger; + allowedGlobalNames?: string[] | undefined; }; /** @@ -46,6 +47,9 @@ export class VatManager { /** Logger for outputting messages (such as errors) to the console */ readonly #logger: Logger; + /** Optional list of allowed global names for vat endowments */ + readonly #allowedGlobalNames: string[] | undefined; + /** * Creates a new VatManager instance. * @@ -54,18 +58,21 @@ export class VatManager { * @param options.kernelStore - The kernel's persistent state store. * @param options.kernelQueue - The kernel's message queue for scheduling deliveries. * @param options.logger - Logger instance for debugging and diagnostics. + * @param options.allowedGlobalNames - Optional list of allowed global names for vat endowments. */ constructor({ platformServices, kernelStore, kernelQueue, logger, + allowedGlobalNames, }: VatManagerOptions) { this.#vats = new Map(); this.#platformServices = platformServices; this.#kernelStore = kernelStore; this.#kernelQueue = kernelQueue; this.#logger = logger ?? new Logger('VatManager'); + this.#allowedGlobalNames = allowedGlobalNames; harden(this); } @@ -134,6 +141,7 @@ export class VatManager { kernelStore: this.#kernelStore, kernelQueue: this.#kernelQueue, logger: vatLogger, + allowedGlobalNames: this.#allowedGlobalNames, }); this.#vats.set(vatId, vat); } diff --git a/packages/ocap-kernel/src/vats/VatSupervisor.ts b/packages/ocap-kernel/src/vats/VatSupervisor.ts index 23925b35e7..b9bfa8d584 100644 --- a/packages/ocap-kernel/src/vats/VatSupervisor.ts +++ b/packages/ocap-kernel/src/vats/VatSupervisor.ts @@ -263,12 +263,13 @@ export class VatSupervisor { * * @param vatConfig - Configuration object describing the vat to be intialized. * @param state - A Map representing the current persistent state of the vat. - * + * @param allowedGlobalNames - Optional list of allowed global names to restrict the available endowments. * @returns a promise for a checkpoint of the new vat. */ async #initVat( vatConfig: VatConfig, state: Map, + allowedGlobalNames: string[] | undefined, ): Promise { if (this.#loaded) { throw Error( @@ -306,12 +307,22 @@ export class VatSupervisor { const { bundleSpec, parameters, platformConfig, globals } = vatConfig; + // If the kernel specified a restricted set of allowed global names, + // filter the full allowlist down to only those names. + const effectiveAllowedGlobals = allowedGlobalNames + ? Object.fromEntries( + allowedGlobalNames + .filter((name) => hasProperty(this.#allowedGlobals, name)) + .map((name) => [name, this.#allowedGlobals[name]]), + ) + : this.#allowedGlobals; + // Build additional endowments from globals list const requestedGlobals: Record = {}; if (globals) { for (const name of globals) { - if (hasProperty(this.#allowedGlobals, name)) { - requestedGlobals[name] = this.#allowedGlobals[name]; + if (hasProperty(effectiveAllowedGlobals, name)) { + requestedGlobals[name] = effectiveAllowedGlobals[name]; } else { this.#logger.warn( `Vat "${this.id}" requested unknown global "${name}"`, From 93076c6ceea8d5f282d69a6118520f518c559207 Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Tue, 14 Apr 2026 19:04:39 +0200 Subject: [PATCH 7/9] test(kernel-test): add e2e tests for kernel-level allowedGlobalNames Verify that Kernel.make({ allowedGlobalNames }) restricts which globals reach vats: a kernel that only allows TextEncoder/TextDecoder blocks URL even when the vat requests it, and omitting the option allows everything. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../kernel-test/src/endowment-globals.test.ts | 105 ++++++++++++++---- 1 file changed, 84 insertions(+), 21 deletions(-) diff --git a/packages/kernel-test/src/endowment-globals.test.ts b/packages/kernel-test/src/endowment-globals.test.ts index 0ec9036990..74a863c6fd 100644 --- a/packages/kernel-test/src/endowment-globals.test.ts +++ b/packages/kernel-test/src/endowment-globals.test.ts @@ -1,29 +1,44 @@ +import { NodejsPlatformServices } from '@metamask/kernel-node-runtime'; import { makeSQLKernelDatabase } from '@metamask/kernel-store/sqlite/nodejs'; import { waitUntilQuiescent } from '@metamask/kernel-utils'; +import { + Logger, + makeConsoleTransport, + makeArrayTransport, +} from '@metamask/logger'; +import type { LogEntry } from '@metamask/logger'; +import { Kernel } from '@metamask/ocap-kernel'; import type { KRef, VatId } from '@metamask/ocap-kernel'; import { getWorkerFile } from '@ocap/nodejs-test-workers'; import { describe, expect, it } from 'vitest'; -import { - extractTestLogs, - getBundleSpec, - makeKernel, - makeTestLogger, -} from './utils.ts'; +import { extractTestLogs, getBundleSpec } from './utils.ts'; describe('global endowments', () => { const vatId: VatId = 'v1'; const v1Root: KRef = 'ko4'; - const setup = async (globals: string[]) => { - const { logger, entries } = makeTestLogger(); + const setup = async ({ + globals, + allowedGlobalNames, + }: { + globals: string[]; + allowedGlobalNames?: string[]; + }) => { + const entries: LogEntry[] = []; + const logger = new Logger({ + transports: [makeConsoleTransport(), makeArrayTransport(entries)], + }); const database = await makeSQLKernelDatabase({}); - const kernel = await makeKernel( - database, - true, + const platformServices = new NodejsPlatformServices({ + logger: logger.subLogger({ tags: ['vat-worker-manager'] }), + workerFilePath: getWorkerFile('mock-fetch'), + }); + const kernel = await Kernel.make(platformServices, database, { + resetStorage: true, logger, - getWorkerFile('mock-fetch'), - ); + allowedGlobalNames, + }); await kernel.launchSubcluster({ bootstrap: 'main', @@ -41,7 +56,9 @@ describe('global endowments', () => { }; it('can use TextEncoder and TextDecoder', async () => { - const { kernel, entries } = await setup(['TextEncoder', 'TextDecoder']); + const { kernel, entries } = await setup({ + globals: ['TextEncoder', 'TextDecoder'], + }); await kernel.queueMessage(v1Root, 'testTextCodec', []); await waitUntilQuiescent(); @@ -51,7 +68,9 @@ describe('global endowments', () => { }); it('can use URL and URLSearchParams', async () => { - const { kernel, entries } = await setup(['URL', 'URLSearchParams']); + const { kernel, entries } = await setup({ + globals: ['URL', 'URLSearchParams'], + }); await kernel.queueMessage(v1Root, 'testUrl', []); await waitUntilQuiescent(); @@ -61,7 +80,7 @@ describe('global endowments', () => { }); it('can use atob and btoa', async () => { - const { kernel, entries } = await setup(['atob', 'btoa']); + const { kernel, entries } = await setup({ globals: ['atob', 'btoa'] }); await kernel.queueMessage(v1Root, 'testBase64', []); await waitUntilQuiescent(); @@ -71,7 +90,9 @@ describe('global endowments', () => { }); it('can use AbortController and AbortSignal', async () => { - const { kernel, entries } = await setup(['AbortController', 'AbortSignal']); + const { kernel, entries } = await setup({ + globals: ['AbortController', 'AbortSignal'], + }); await kernel.queueMessage(v1Root, 'testAbort', []); await waitUntilQuiescent(); @@ -81,7 +102,9 @@ describe('global endowments', () => { }); it('can use setTimeout and clearTimeout', async () => { - const { kernel, entries } = await setup(['setTimeout', 'clearTimeout']); + const { kernel, entries } = await setup({ + globals: ['setTimeout', 'clearTimeout'], + }); await kernel.queueMessage(v1Root, 'testTimers', []); await waitUntilQuiescent(); @@ -91,7 +114,7 @@ describe('global endowments', () => { }); it('can use real Date (not tamed)', async () => { - const { kernel, entries } = await setup(['Date']); + const { kernel, entries } = await setup({ globals: ['Date'] }); await kernel.queueMessage(v1Root, 'testDate', []); await waitUntilQuiescent(); @@ -116,7 +139,7 @@ describe('global endowments', () => { 'clearTimeout', ])('does not have %s without endowing it', async (name) => { // Launch with no globals at all - const { kernel, entries } = await setup([]); + const { kernel, entries } = await setup({ globals: [] }); await kernel.queueMessage(v1Root, 'checkGlobal', [name]); await waitUntilQuiescent(); @@ -126,11 +149,51 @@ describe('global endowments', () => { }); it('throws when calling tamed Date.now without endowing Date', async () => { - const { kernel } = await setup([]); + const { kernel } = await setup({ globals: [] }); await expect(kernel.queueMessage(v1Root, 'testDate', [])).rejects.toThrow( 'secure mode', ); }); }); + + describe('kernel-level allowedGlobalNames restriction', () => { + it('blocks a global when the kernel excludes it from allowedGlobalNames', async () => { + // Kernel only allows TextEncoder/TextDecoder — vat requests URL too + const { kernel, entries } = await setup({ + globals: ['TextEncoder', 'TextDecoder', 'URL'], + allowedGlobalNames: ['TextEncoder', 'TextDecoder'], + }); + + // TextEncoder works (allowed by kernel) + await kernel.queueMessage(v1Root, 'testTextCodec', []); + await waitUntilQuiescent(); + + const logs = extractTestLogs(entries, vatId); + expect(logs).toContain('textCodec: hello'); + + // URL is absent (kernel excluded it even though vat requested it) + await kernel.queueMessage(v1Root, 'checkGlobal', ['URL']); + await waitUntilQuiescent(); + + const logsAfter = extractTestLogs(entries, vatId); + expect(logsAfter).toContain('checkGlobal: URL=false'); + }); + + it('allows all globals when allowedGlobalNames is omitted', async () => { + // No kernel restriction — vat gets everything it asks for + const { kernel, entries } = await setup({ + globals: ['URL', 'TextEncoder'], + }); + + await kernel.queueMessage(v1Root, 'checkGlobal', ['URL']); + await waitUntilQuiescent(); + await kernel.queueMessage(v1Root, 'checkGlobal', ['TextEncoder']); + await waitUntilQuiescent(); + + const logs = extractTestLogs(entries, vatId); + expect(logs).toContain('checkGlobal: URL=true'); + expect(logs).toContain('checkGlobal: TextEncoder=true'); + }); + }); }); From 6ae261289035d0fdc4810f8729ce48eff26d7978 Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Tue, 14 Apr 2026 22:02:21 +0200 Subject: [PATCH 8/9] fix(ocap-kernel): throw on unknown globals instead of warning Requesting an unknown global now throws before vat code is evaluated, surfacing misconfigurations immediately rather than silently ignoring them. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/vats/VatSupervisor.test.ts | 55 ++++--------------- .../ocap-kernel/src/vats/VatSupervisor.ts | 2 +- 2 files changed, 11 insertions(+), 46 deletions(-) diff --git a/packages/ocap-kernel/src/vats/VatSupervisor.test.ts b/packages/ocap-kernel/src/vats/VatSupervisor.test.ts index 7e22247e0a..da181ea210 100644 --- a/packages/ocap-kernel/src/vats/VatSupervisor.test.ts +++ b/packages/ocap-kernel/src/vats/VatSupervisor.test.ts @@ -179,48 +179,8 @@ describe('VatSupervisor', () => { expect(supervisor).toBeInstanceOf(VatSupervisor); }); - it('does not warn when all requested globals are known', async () => { - const logger = { - warn: vi.fn(), - error: vi.fn(), - subLogger: vi.fn(() => logger), - } as unknown as Logger; - - const mockFetchBlob: FetchBlob = vi.fn().mockResolvedValue({ - ok: true, - text: vi.fn().mockResolvedValue(''), - }); - - const { stream } = await makeVatSupervisor({ - logger, - allowedGlobals: { Date: globalThis.Date }, - fetchBlob: mockFetchBlob, - }); - - await stream.receiveInput({ - id: 'test-init', - method: 'initVat', - params: { - vatConfig: { - bundleSpec: 'test.bundle', - parameters: {}, - globals: ['Date'], - }, - state: [], - }, - jsonrpc: '2.0', - }); - await delay(50); - - expect(logger.warn).not.toHaveBeenCalled(); - }); - - it('logs a warning when a vat requests an unknown global', async () => { - const logger = { - warn: vi.fn(), - error: vi.fn(), - subLogger: vi.fn(() => logger), - } as unknown as Logger; + it('throws when a vat requests an unknown global', async () => { + const dispatch = vi.fn(); const mockFetchBlob: FetchBlob = vi.fn().mockResolvedValue({ ok: true, @@ -228,7 +188,7 @@ describe('VatSupervisor', () => { }); const { stream } = await makeVatSupervisor({ - logger, + dispatch, allowedGlobals: { Date: globalThis.Date }, fetchBlob: mockFetchBlob, }); @@ -248,8 +208,13 @@ describe('VatSupervisor', () => { }); await delay(50); - expect(logger.warn).toHaveBeenCalledWith( - 'Vat "test-id" requested unknown global "UnknownThing"', + expect(dispatch).toHaveBeenCalledWith( + expect.objectContaining({ + id: 'test-init', + error: expect.objectContaining({ + message: expect.stringContaining('unknown global "UnknownThing"'), + }), + }), ); }); }); diff --git a/packages/ocap-kernel/src/vats/VatSupervisor.ts b/packages/ocap-kernel/src/vats/VatSupervisor.ts index b9bfa8d584..1a82f228b3 100644 --- a/packages/ocap-kernel/src/vats/VatSupervisor.ts +++ b/packages/ocap-kernel/src/vats/VatSupervisor.ts @@ -324,7 +324,7 @@ export class VatSupervisor { if (hasProperty(effectiveAllowedGlobals, name)) { requestedGlobals[name] = effectiveAllowedGlobals[name]; } else { - this.#logger.warn( + throw new Error( `Vat "${this.id}" requested unknown global "${name}"`, ); } From a105504157aeaedbaff2fa8dc389cb07a76f2ae8 Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Tue, 14 Apr 2026 22:22:05 +0200 Subject: [PATCH 9/9] fix(kernel-test): update e2e tests for throw-on-unknown-global behavior Co-Authored-By: Claude Opus 4.6 (1M context) --- .../kernel-test/src/endowment-globals.test.ts | 33 +++++++++---------- 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/packages/kernel-test/src/endowment-globals.test.ts b/packages/kernel-test/src/endowment-globals.test.ts index 74a863c6fd..050d3782bc 100644 --- a/packages/kernel-test/src/endowment-globals.test.ts +++ b/packages/kernel-test/src/endowment-globals.test.ts @@ -158,42 +158,39 @@ describe('global endowments', () => { }); describe('kernel-level allowedGlobalNames restriction', () => { - it('blocks a global when the kernel excludes it from allowedGlobalNames', async () => { - // Kernel only allows TextEncoder/TextDecoder — vat requests URL too + it('throws when a vat requests a global excluded by the kernel', async () => { + // Kernel only allows TextEncoder/TextDecoder — vat also requests URL + await expect( + setup({ + globals: ['TextEncoder', 'TextDecoder', 'URL'], + allowedGlobalNames: ['TextEncoder', 'TextDecoder'], + }), + ).rejects.toThrow('unknown global "URL"'); + }); + + it('initializes when all vat globals are within allowedGlobalNames', async () => { const { kernel, entries } = await setup({ - globals: ['TextEncoder', 'TextDecoder', 'URL'], + globals: ['TextEncoder', 'TextDecoder'], allowedGlobalNames: ['TextEncoder', 'TextDecoder'], }); - // TextEncoder works (allowed by kernel) await kernel.queueMessage(v1Root, 'testTextCodec', []); await waitUntilQuiescent(); const logs = extractTestLogs(entries, vatId); expect(logs).toContain('textCodec: hello'); - - // URL is absent (kernel excluded it even though vat requested it) - await kernel.queueMessage(v1Root, 'checkGlobal', ['URL']); - await waitUntilQuiescent(); - - const logsAfter = extractTestLogs(entries, vatId); - expect(logsAfter).toContain('checkGlobal: URL=false'); }); it('allows all globals when allowedGlobalNames is omitted', async () => { - // No kernel restriction — vat gets everything it asks for const { kernel, entries } = await setup({ - globals: ['URL', 'TextEncoder'], + globals: ['URL', 'URLSearchParams'], }); - await kernel.queueMessage(v1Root, 'checkGlobal', ['URL']); - await waitUntilQuiescent(); - await kernel.queueMessage(v1Root, 'checkGlobal', ['TextEncoder']); + await kernel.queueMessage(v1Root, 'testUrl', []); await waitUntilQuiescent(); const logs = extractTestLogs(entries, vatId); - expect(logs).toContain('checkGlobal: URL=true'); - expect(logs).toContain('checkGlobal: TextEncoder=true'); + expect(logs).toContain('url: /path params: 10'); }); }); });