diff --git a/.depcheckrc.yml b/.depcheckrc.yml index ef2dad39f..08e7fb5e3 100644 --- a/.depcheckrc.yml +++ b/.depcheckrc.yml @@ -50,6 +50,9 @@ ignores: # Used by @ocap/nodejs to build the sqlite3 bindings - 'node-gyp' + # Used by @metamask/kernel-shims/endoify-node for tests + - '@libp2p/webrtc' + # These are peer dependencies of various modules we actually do # depend on, which have been elevated to full dependencies (even # though we don't actually depend on them) in order to work around a diff --git a/package.json b/package.json index 97e804803..062c1821c 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "lint:misc": "prettier --no-error-on-unmatched-pattern '**/*.json' '**/*.md' '**/*.html' '**/*.yml' '!**/CHANGELOG.old.md' '!.yarnrc.yml' '!CLAUDE.md' '!merged-packages/**' --ignore-path .gitignore --log-level error", "postinstall": "simple-git-hooks && yarn rebuild:native", "prepack": "./scripts/prepack.sh", - "pretest": "bash scripts/reset-coverage-thresholds.sh", + "pretest": "./scripts/reset-coverage-thresholds.sh", "rebuild:native": "./scripts/rebuild-native.sh", "test": "yarn pretest && vitest run", "test:ci": "vitest run --coverage false", @@ -122,7 +122,8 @@ "vite>sass>@parcel/watcher": false, "vitest>@vitest/browser>webdriverio>@wdio/utils>edgedriver": false, "vitest>@vitest/browser>webdriverio>@wdio/utils>geckodriver": false, - "vitest>@vitest/mocker>msw": false + "vitest>@vitest/mocker>msw": false, + "@ocap/cli>@metamask/kernel-shims>@libp2p/webrtc>@ipshipyard/node-datachannel": false } }, "resolutions": { diff --git a/packages/extension/src/background.ts b/packages/extension/src/background.ts index 967103779..5e302c12a 100644 --- a/packages/extension/src/background.ts +++ b/packages/extension/src/background.ts @@ -1,14 +1,12 @@ import { E } from '@endo/eventual-send'; import { makeBackgroundCapTP, + makePresenceManager, makeCapTPNotification, isCapTPNotification, getCapTPMessage, } from '@metamask/kernel-browser-runtime'; -import type { - KernelFacade, - CapTPMessage, -} from '@metamask/kernel-browser-runtime'; +import type { CapTPMessage } from '@metamask/kernel-browser-runtime'; import defaultSubcluster from '@metamask/kernel-browser-runtime/default-cluster'; import { delay, isJsonRpcMessage, stringify } from '@metamask/kernel-utils'; import type { JsonRpcMessage } from '@metamask/kernel-utils'; @@ -20,12 +18,11 @@ defineGlobals(); const OFFSCREEN_DOCUMENT_PATH = '/offscreen.html'; const logger = new Logger('background'); let bootPromise: Promise | null = null; -let kernelP: Promise; -let ping: () => Promise; // With this we can click the extension action button to wake up the service worker. chrome.action.onClicked.addListener(() => { - ping?.().catch(logger.error); + globalThis.kernel !== undefined && + E(globalThis.kernel).ping().catch(logger.error); }); // Install/update @@ -108,12 +105,12 @@ async function main(): Promise { }); // Get the kernel remote presence - kernelP = backgroundCapTP.getKernel(); + const kernelP = backgroundCapTP.getKernel(); + globalThis.kernel = kernelP; - ping = async () => { - const result = await E(kernelP).ping(); - logger.info(result); - }; + // Create presence manager for E() calls on vat objects + const presenceManager = makePresenceManager({ kernelFacade: kernelP }); + Object.assign(globalThis.captp, presenceManager); // Handle incoming CapTP messages from the kernel const drainPromise = offscreenStream.drain((message) => { @@ -126,8 +123,11 @@ async function main(): Promise { }); drainPromise.catch(logger.error); - await ping(); // Wait for the kernel to be ready - await startDefaultSubcluster(kernelP); + await E(kernelP).ping(); + const rootKref = await startDefaultSubcluster(); + if (rootKref) { + await greetBootstrapVat(rootKref); + } try { await drainPromise; @@ -143,19 +143,33 @@ async function main(): Promise { /** * Idempotently starts the default subcluster. * - * @param kernelPromise - Promise for the kernel facade. + * @returns The rootKref of the bootstrap vat if launched, undefined if subcluster already exists. */ -async function startDefaultSubcluster( - kernelPromise: Promise, -): Promise { - const status = await E(kernelPromise).getStatus(); +async function startDefaultSubcluster(): Promise { + const status = await E(globalThis.kernel).getStatus(); if (status.subclusters.length === 0) { - const result = await E(kernelPromise).launchSubcluster(defaultSubcluster); + const result = await E(globalThis.kernel).launchSubcluster( + defaultSubcluster, + ); logger.info(`Default subcluster launched: ${JSON.stringify(result)}`); - } else { - logger.info('Subclusters already exist. Not launching default subcluster.'); + return result.rootKref; } + logger.info('Subclusters already exist. Not launching default subcluster.'); + return undefined; +} + +/** + * Greets the bootstrap vat by calling its hello() method. + * + * @param rootKref - The kref of the bootstrap vat's root object. + */ +async function greetBootstrapVat(rootKref: string): Promise { + const rootPresence = captp.resolveKref(rootKref) as { + hello: (from: string) => string; + }; + const greeting = await E(rootPresence).hello('background'); + logger.info(`Got greeting from bootstrap vat: ${greeting}`); } /** @@ -165,19 +179,16 @@ function defineGlobals(): void { Object.defineProperty(globalThis, 'kernel', { configurable: false, enumerable: true, - writable: false, - value: {}, + writable: true, + value: undefined, }); - Object.defineProperties(globalThis.kernel, { - ping: { - get: () => ping, - }, - getKernel: { - value: async () => kernelP, - }, + Object.defineProperty(globalThis, 'captp', { + configurable: false, + enumerable: true, + writable: false, + value: {}, }); - harden(globalThis.kernel); Object.defineProperty(globalThis, 'E', { value: E, diff --git a/packages/extension/src/global.d.ts b/packages/extension/src/global.d.ts index 06dd91196..c67f8b339 100644 --- a/packages/extension/src/global.d.ts +++ b/packages/extension/src/global.d.ts @@ -1,4 +1,7 @@ -import type { KernelFacade } from '@metamask/kernel-browser-runtime'; +import type { + PresenceManager, + KernelFacade, +} from '@metamask/kernel-browser-runtime'; // Type declarations for kernel dev console API. declare global { @@ -16,24 +19,19 @@ declare global { var E: typeof import('@endo/eventual-send').E; // eslint-disable-next-line no-var - var kernel: { - /** - * Ping the kernel to verify connectivity. - */ - ping: () => Promise; + var kernel: KernelFacade | Promise; - /** - * Get the kernel remote presence for use with E(). - * - * @returns A promise for the kernel facade remote presence. - * @example - * ```typescript - * const kernel = await kernel.getKernel(); - * const status = await E(kernel).getStatus(); - * ``` - */ - getKernel: () => Promise; - }; + /** + * CapTP utilities for resolving krefs to E()-callable presences. + * + * @example + * ```typescript + * const alice = captp.resolveKref('ko1'); + * await E(alice).hello('console'); + * ``` + */ + // eslint-disable-next-line no-var + var captp: PresenceManager; } export {}; diff --git a/packages/kernel-browser-runtime/package.json b/packages/kernel-browser-runtime/package.json index 34bb97228..5ee1eb88a 100644 --- a/packages/kernel-browser-runtime/package.json +++ b/packages/kernel-browser-runtime/package.json @@ -85,6 +85,7 @@ "devDependencies": { "@arethetypeswrong/cli": "^0.17.4", "@endo/eventual-send": "^1.3.4", + "@libp2p/webrtc": "5.2.24", "@metamask/auto-changelog": "^5.3.0", "@metamask/eslint-config": "^15.0.0", "@metamask/eslint-config-nodejs": "^15.0.0", diff --git a/packages/kernel-browser-runtime/src/index.test.ts b/packages/kernel-browser-runtime/src/index.test.ts index f52b98667..dd96eaf49 100644 --- a/packages/kernel-browser-runtime/src/index.test.ts +++ b/packages/kernel-browser-runtime/src/index.test.ts @@ -15,6 +15,7 @@ describe('index', () => { 'makeBackgroundCapTP', 'makeCapTPNotification', 'makeIframeVatWorker', + 'makePresenceManager', 'parseRelayQueryString', 'receiveInternalConnections', 'rpcHandlers', diff --git a/packages/kernel-browser-runtime/src/index.ts b/packages/kernel-browser-runtime/src/index.ts index 4c10590e3..79fb7036a 100644 --- a/packages/kernel-browser-runtime/src/index.ts +++ b/packages/kernel-browser-runtime/src/index.ts @@ -21,3 +21,8 @@ export { type BackgroundCapTPOptions, type CapTPMessage, } from './background-captp.ts'; +export { + makePresenceManager, + type PresenceManager, + type PresenceManagerOptions, +} from './kref-presence.ts'; diff --git a/packages/kernel-browser-runtime/src/kernel-worker/captp/kernel-facade.test.ts b/packages/kernel-browser-runtime/src/kernel-worker/captp/kernel-facade.test.ts index e8306893f..7799a9bf5 100644 --- a/packages/kernel-browser-runtime/src/kernel-worker/captp/kernel-facade.test.ts +++ b/packages/kernel-browser-runtime/src/kernel-worker/captp/kernel-facade.test.ts @@ -122,6 +122,37 @@ describe('makeKernelFacade', () => { expect(mockKernel.queueMessage).toHaveBeenCalledTimes(1); }); + it('converts kref strings in args to standins', async () => { + const target: KRef = 'ko1'; + const method = 'sendTo'; + // Use ko refs only - kp refs become promise standins with different structure + const args = ['ko42', { target: 'ko99', data: 'hello' }]; + + await facade.queueMessage(target, method, args); + + // Verify the call was made + expect(mockKernel.queueMessage).toHaveBeenCalledTimes(1); + + // Get the actual args passed to kernel + const [, , processedArgs] = vi.mocked(mockKernel.queueMessage).mock + .calls[0]!; + + // First arg should be a standin with getKref method + expect(processedArgs[0]).toHaveProperty('getKref'); + expect((processedArgs[0] as { getKref: () => string }).getKref()).toBe( + 'ko42', + ); + + // Second arg should be an object with converted kref + const secondArg = processedArgs[1] as { + target: { getKref: () => string }; + data: string; + }; + expect(secondArg.target).toHaveProperty('getKref'); + expect(secondArg.target.getKref()).toBe('ko99'); + expect(secondArg.data).toBe('hello'); + }); + it('returns result from kernel', async () => { const expectedResult = { body: '#{"answer":42}', slots: [] }; vi.mocked(mockKernel.queueMessage).mockResolvedValueOnce(expectedResult); diff --git a/packages/kernel-browser-runtime/src/kernel-worker/captp/kernel-facade.ts b/packages/kernel-browser-runtime/src/kernel-worker/captp/kernel-facade.ts index 51d3cc9a4..af363fcb3 100644 --- a/packages/kernel-browser-runtime/src/kernel-worker/captp/kernel-facade.ts +++ b/packages/kernel-browser-runtime/src/kernel-worker/captp/kernel-facade.ts @@ -1,6 +1,7 @@ import { makeDefaultExo } from '@metamask/kernel-utils/exo'; import type { Kernel, ClusterConfig, KRef, VatId } from '@metamask/ocap-kernel'; +import { convertKrefsToStandins } from '../../kref-presence.ts'; import type { KernelFacade, LaunchResult } from '../../types.ts'; export type { KernelFacade } from '../../types.ts'; @@ -26,7 +27,9 @@ export function makeKernelFacade(kernel: Kernel): KernelFacade { }, queueMessage: async (target: KRef, method: string, args: unknown[]) => { - return kernel.queueMessage(target, method, args); + // Convert kref strings in args to standins for kernel-marshal + const processedArgs = convertKrefsToStandins(args) as unknown[]; + return kernel.queueMessage(target, method, processedArgs); }, getStatus: async () => { diff --git a/packages/kernel-browser-runtime/src/kref-presence.test.ts b/packages/kernel-browser-runtime/src/kref-presence.test.ts new file mode 100644 index 000000000..a62d0b685 --- /dev/null +++ b/packages/kernel-browser-runtime/src/kref-presence.test.ts @@ -0,0 +1,296 @@ +import { passStyleOf } from '@endo/marshal'; +import { krefOf as kernelKrefOf } from '@metamask/ocap-kernel'; +import type { SlotValue } from '@metamask/ocap-kernel'; +import { describe, it, expect, vi, beforeEach } from 'vitest'; + +import type { PresenceManager } from './kref-presence.ts'; +import { + convertKrefsToStandins, + makePresenceManager, +} from './kref-presence.ts'; +import type { KernelFacade } from './types.ts'; + +// EHandler type definition (copied to avoid import issues with mocking) +type EHandler = { + get?: (target: object, prop: PropertyKey) => Promise; + applyMethod?: ( + target: object, + prop: PropertyKey, + args: unknown[], + ) => Promise; + applyFunction?: (target: object, args: unknown[]) => Promise; +}; + +// Hoisted mock setup - these must be defined before vi.mock() is hoisted +const { MockHandledPromise, mockE } = vi.hoisted(() => { + /** + * Mock HandledPromise that supports resolveWithPresence. + */ + class MockHandledPromiseImpl extends Promise { + constructor( + executor: ( + resolve: (value: TResult | PromiseLike) => void, + reject: (reason?: unknown) => void, + resolveWithPresence: (handler: EHandler) => object, + ) => void, + _handler?: EHandler, + ) { + let presence: object | undefined; + + const resolveWithPresence = (handler: EHandler): object => { + // Create a simple presence object that can receive E() calls + presence = new Proxy( + {}, + { + get(_target, prop) { + if (prop === Symbol.toStringTag) { + return 'Alleged: VatObject'; + } + // Return a function that calls the handler + return async (...args: unknown[]) => { + if (typeof prop === 'string') { + return handler.applyMethod?.(presence!, prop, args); + } + return undefined; + }; + }, + }, + ); + return presence; + }; + + super((resolve, reject) => { + executor(resolve, reject, resolveWithPresence); + }); + } + } + + // Mock E() to intercept calls on presences + const mockEImpl = (target: object) => { + return new Proxy( + {}, + { + get(_proxyTarget, prop) { + if (typeof prop === 'string') { + // Return a function that, when called, invokes the presence's method + return (...args: unknown[]) => { + const method = (target as Record)[prop]; + if (typeof method === 'function') { + return (method as (...a: unknown[]) => unknown)(...args); + } + // Try to get it from the proxy + return (target as Record unknown>)[ + prop + ]?.(...args); + }; + } + return undefined; + }, + }, + ); + }; + + return { + MockHandledPromise: MockHandledPromiseImpl, + mockE: mockEImpl, + }; +}); + +// Apply mocks +vi.mock('@endo/eventual-send', () => ({ + E: mockE, + HandledPromise: MockHandledPromise, +})); + +describe('convertKrefsToStandins', () => { + describe('kref string conversion', () => { + it('converts ko kref string to standin', () => { + const result = convertKrefsToStandins('ko123') as SlotValue; + + expect(passStyleOf(result)).toBe('remotable'); + expect(kernelKrefOf(result)).toBe('ko123'); + }); + + it('converts kp kref string to standin promise', () => { + const result = convertKrefsToStandins('kp456'); + + expect(passStyleOf(result)).toBe('promise'); + expect(kernelKrefOf(result as Promise)).toBe('kp456'); + }); + + it('does not convert non-kref strings', () => { + expect(convertKrefsToStandins('hello')).toBe('hello'); + expect(convertKrefsToStandins('k123')).toBe('k123'); + expect(convertKrefsToStandins('kox')).toBe('kox'); + expect(convertKrefsToStandins('ko')).toBe('ko'); + expect(convertKrefsToStandins('kp')).toBe('kp'); + expect(convertKrefsToStandins('ko123x')).toBe('ko123x'); + }); + }); + + describe('array processing', () => { + it('recursively converts krefs in arrays', () => { + const result = convertKrefsToStandins(['ko1', 'ko2']) as unknown[]; + + expect(result).toHaveLength(2); + expect(kernelKrefOf(result[0] as SlotValue)).toBe('ko1'); + expect(kernelKrefOf(result[1] as SlotValue)).toBe('ko2'); + }); + + it('handles mixed arrays with krefs and primitives', () => { + const result = convertKrefsToStandins([ + 'ko1', + 42, + 'hello', + true, + ]) as unknown[]; + + expect(result).toHaveLength(4); + expect(kernelKrefOf(result[0] as SlotValue)).toBe('ko1'); + expect(result[1]).toBe(42); + expect(result[2]).toBe('hello'); + expect(result[3]).toBe(true); + }); + + it('handles empty arrays', () => { + const result = convertKrefsToStandins([]); + expect(result).toStrictEqual([]); + }); + + it('handles nested arrays', () => { + const result = convertKrefsToStandins([['ko1'], ['ko2']]) as unknown[][]; + + expect(kernelKrefOf(result[0]![0] as SlotValue)).toBe('ko1'); + expect(kernelKrefOf(result[1]![0] as SlotValue)).toBe('ko2'); + }); + }); + + describe('object processing', () => { + it('recursively converts krefs in objects', () => { + const result = convertKrefsToStandins({ + target: 'ko1', + promise: 'kp2', + }) as Record; + + expect(kernelKrefOf(result.target as SlotValue)).toBe('ko1'); + expect(kernelKrefOf(result.promise as Promise)).toBe('kp2'); + }); + + it('handles nested objects', () => { + const result = convertKrefsToStandins({ + outer: { + inner: 'ko42', + }, + }) as Record>; + + expect(kernelKrefOf(result.outer!.inner as SlotValue)).toBe('ko42'); + }); + + it('handles empty objects', () => { + const result = convertKrefsToStandins({}); + expect(result).toStrictEqual({}); + }); + + it('handles objects with mixed values', () => { + const result = convertKrefsToStandins({ + kref: 'ko1', + number: 123, + string: 'text', + boolean: false, + nullValue: null, + }) as Record; + + expect(kernelKrefOf(result.kref as SlotValue)).toBe('ko1'); + expect(result.number).toBe(123); + expect(result.string).toBe('text'); + expect(result.boolean).toBe(false); + expect(result.nullValue).toBeNull(); + }); + }); + + describe('primitive handling', () => { + it('passes through numbers unchanged', () => { + expect(convertKrefsToStandins(42)).toBe(42); + expect(convertKrefsToStandins(0)).toBe(0); + expect(convertKrefsToStandins(-1)).toBe(-1); + }); + + it('passes through booleans unchanged', () => { + expect(convertKrefsToStandins(true)).toBe(true); + expect(convertKrefsToStandins(false)).toBe(false); + }); + + it('passes through null unchanged', () => { + expect(convertKrefsToStandins(null)).toBeNull(); + }); + + it('passes through undefined unchanged', () => { + expect(convertKrefsToStandins(undefined)).toBeUndefined(); + }); + }); +}); + +describe('makePresenceManager', () => { + let mockKernelFacade: KernelFacade; + let presenceManager: PresenceManager; + + beforeEach(() => { + mockKernelFacade = { + ping: vi.fn(), + launchSubcluster: vi.fn(), + terminateSubcluster: vi.fn(), + queueMessage: vi.fn(), + getStatus: vi.fn(), + pingVat: vi.fn(), + getVatRoot: vi.fn(), + } as unknown as KernelFacade; + + presenceManager = makePresenceManager({ + kernelFacade: mockKernelFacade, + }); + }); + + describe('resolveKref', () => { + it('returns a presence object for a kref', () => { + const presence = presenceManager.resolveKref('ko42'); + + expect(presence).toBeDefined(); + expect(typeof presence).toBe('object'); + }); + + it('returns the same presence for the same kref (memoization)', () => { + const presence1 = presenceManager.resolveKref('ko42'); + const presence2 = presenceManager.resolveKref('ko42'); + + expect(presence1).toBe(presence2); + }); + + it('returns different presences for different krefs', () => { + const presence1 = presenceManager.resolveKref('ko1'); + const presence2 = presenceManager.resolveKref('ko2'); + + expect(presence1).not.toBe(presence2); + }); + }); + + describe('krefOf', () => { + it('returns the kref for a known presence', () => { + const presence = presenceManager.resolveKref('ko42'); + const kref = presenceManager.krefOf(presence); + + expect(kref).toBe('ko42'); + }); + + it('returns undefined for an unknown object', () => { + const unknownObject = { foo: 'bar' }; + const kref = presenceManager.krefOf(unknownObject); + + expect(kref).toBeUndefined(); + }); + }); + + // Note: fromCapData and E() handler tests require the full Endo runtime + // environment with proper SES lockdown. These behaviors are tested in + // captp.integration.test.ts which runs with the real Endo setup. + // Unit tests here focus on the kref↔presence mapping functionality. +}); diff --git a/packages/kernel-browser-runtime/src/kref-presence.ts b/packages/kernel-browser-runtime/src/kref-presence.ts new file mode 100644 index 000000000..2fe10f332 --- /dev/null +++ b/packages/kernel-browser-runtime/src/kref-presence.ts @@ -0,0 +1,287 @@ +/** + * Presence manager for creating E()-usable presences from kernel krefs. + * + * This module provides "slot translation" - converting kernel krefs (ko*, kp*) + * into presences that can receive eventual sends via E(). Method calls on these + * presences are forwarded to kernel.queueMessage() through the existing CapTP + * connection. + */ +import { E, HandledPromise } from '@endo/eventual-send'; +import type { EHandler } from '@endo/eventual-send'; +import { makeMarshal, Remotable } from '@endo/marshal'; +import type { CapData } from '@endo/marshal'; +import type { KRef } from '@metamask/ocap-kernel'; +import { kslot } from '@metamask/ocap-kernel'; + +import type { KernelFacade } from './types.ts'; + +/** + * Function type for sending messages to the kernel. + */ +type SendToKernelFn = ( + kref: string, + method: string, + args: unknown[], +) => Promise; + +/** + * Recursively convert kref strings in a value to kernel standins. + * + * When the background sends kref strings as arguments, we need to convert + * them to standin objects that kernel-marshal can serialize properly. + * + * @param value - The value to convert. + * @returns The value with kref strings converted to standins. + */ +export function convertKrefsToStandins(value: unknown): unknown { + // Check if it's a kref string (ko* or kp*) + if (typeof value === 'string' && /^k[op]\d+$/u.test(value)) { + return kslot(value); + } + // Recursively process arrays + if (Array.isArray(value)) { + return value.map(convertKrefsToStandins); + } + // Recursively process plain objects + if (typeof value === 'object' && value !== null) { + const result: Record = {}; + for (const [key, val] of Object.entries(value)) { + result[key] = convertKrefsToStandins(val); + } + return result; + } + // Return primitives as-is + return value; +} +harden(convertKrefsToStandins); + +/** + * Options for creating a presence manager. + */ +export type PresenceManagerOptions = { + /** + * The kernel facade remote presence from CapTP. + * Can be a promise since E() works with promises. + */ + kernelFacade: KernelFacade | Promise; +}; + +/** + * The presence manager interface. + */ +export type PresenceManager = { + /** + * Resolve a kref string to an E()-usable presence. + * + * @param kref - The kernel reference string (e.g., 'ko42', 'kp123'). + * @returns A presence that can receive E() calls. + */ + resolveKref: (kref: KRef) => object; + + /** + * Extract the kref from a presence. + * + * @param presence - A presence created by resolveKref. + * @returns The kref string, or undefined if not a kref presence. + */ + krefOf: (presence: object) => KRef | undefined; + + /** + * Deserialize a CapData result into presences. + * + * @param data - The CapData to deserialize. + * @returns The deserialized value with krefs converted to presences. + */ + fromCapData: (data: CapData) => unknown; +}; + +/** + * Create a remote kit for a kref, similar to CapTP's makeRemoteKit. + * Returns a settler that can create an E()-callable presence. + * + * @param kref - The kernel reference string. + * @param sendToKernel - Function to send messages to the kernel. + * @returns An object with a resolveWithPresence method. + */ +function makeKrefRemoteKit( + kref: string, + sendToKernel: SendToKernelFn, +): { resolveWithPresence: () => object } { + // Handler that intercepts E() calls on the presence + const handler: EHandler = { + async get(_target, prop) { + if (typeof prop !== 'string') { + return undefined; + } + // Property access: E(presence).prop returns a promise + return sendToKernel(kref, prop, []); + }, + async applyMethod(_target, prop, args) { + if (typeof prop !== 'string') { + throw new Error('Method name must be a string'); + } + // Method call: E(presence).method(args) + return sendToKernel(kref, prop, args); + }, + applyFunction(_target, _args) { + // Function call: E(presence)(args) - not supported for kref presences + throw new Error('Cannot call kref presence as a function'); + }, + }; + + let resolveWithPresenceFn: + | ((presenceHandler: EHandler) => object) + | undefined; + + // Create a HandledPromise to get access to resolveWithPresence + // We don't actually use the promise - we just need the resolver + // eslint-disable-next-line no-new, @typescript-eslint/no-floating-promises + new HandledPromise((_resolve, _reject, resolveWithPresence) => { + resolveWithPresenceFn = resolveWithPresence; + }, handler); + + return { + resolveWithPresence: () => { + if (!resolveWithPresenceFn) { + throw new Error('resolveWithPresence not initialized'); + } + return resolveWithPresenceFn(handler); + }, + }; +} + +/** + * Create an E()-usable presence for a kref. + * + * @param kref - The kernel reference string. + * @param iface - Interface name for the remotable. + * @param sendToKernel - Function to send messages to the kernel. + * @returns A presence that can receive E() calls. + */ +function makeKrefPresence( + kref: string, + iface: string, + sendToKernel: SendToKernelFn, +): object { + const kit = makeKrefRemoteKit(kref, sendToKernel); + // Wrap the presence in Remotable for proper pass-style + return Remotable(iface, undefined, kit.resolveWithPresence()); +} + +/** + * Create a presence manager for E() on vat objects. + * + * This creates presences from kernel krefs that forward method calls + * to kernel.queueMessage() via the existing CapTP connection. + * + * @param options - Options including the kernel facade. + * @returns The presence manager. + */ +export function makePresenceManager( + options: PresenceManagerOptions, +): PresenceManager { + const { kernelFacade } = options; + + // State for kref↔presence mapping + const krefToPresence = new Map(); + const presenceToKref = new WeakMap(); + + // Forward declaration for sendToKernel + // eslint-disable-next-line prefer-const + let marshal: ReturnType>; + + /** + * Send a message to the kernel and deserialize the result. + * + * @param kref - The target kernel reference. + * @param method - The method name to call. + * @param args - Arguments to pass to the method. + * @returns The deserialized result from the kernel. + */ + const sendToKernel: SendToKernelFn = async ( + kref: KRef, + method: string, + args: unknown[], + ): Promise => { + // Convert presence args to kref strings + const serializedArgs = args.map((arg) => { + if (typeof arg === 'object' && arg !== null) { + const argKref = presenceToKref.get(arg); + if (argKref) { + return argKref; // Pass kref string to kernel + } + } + return arg; // Pass primitive through + }); + + // Call kernel via existing CapTP + const result: CapData = await E(kernelFacade).queueMessage( + kref, + method, + serializedArgs, + ); + + // Deserialize result (krefs become presences) + return marshal.fromCapData(result); + }; + + /** + * Convert a kref slot to a presence. + * + * @param kref - The kernel reference string. + * @param iface - Optional interface name for the presence. + * @returns A presence object that can receive E() calls. + */ + const convertSlotToVal = (kref: KRef, iface?: string): object => { + let presence = krefToPresence.get(kref); + if (!presence) { + presence = makeKrefPresence( + kref, + iface ?? 'Alleged: VatObject', + sendToKernel, + ); + krefToPresence.set(kref, presence); + presenceToKref.set(presence, kref); + } + return presence; + }; + + /** + * Convert a presence to a kref slot. + * This is called by the marshal for pass-by-presence objects. + * Throws if the object is not a known kref presence. + * + * @param val - The value to convert to a kref. + * @returns The kernel reference string. + */ + const convertValToSlot = (val: unknown): KRef => { + if (typeof val === 'object' && val !== null) { + const kref = presenceToKref.get(val); + if (kref !== undefined) { + return kref; + } + } + throw new Error('Cannot serialize unknown remotable object'); + }; + + // Same options as kernel-marshal.ts + marshal = makeMarshal(convertValToSlot, convertSlotToVal, { + serializeBodyFormat: 'smallcaps', + errorTagging: 'off', + }); + + return harden({ + resolveKref: (kref: KRef): object => { + return convertSlotToVal(kref, 'Alleged: VatObject'); + }, + + krefOf: (presence: object): KRef | undefined => { + return presenceToKref.get(presence); + }, + + fromCapData: (data: CapData): unknown => { + return marshal.fromCapData(data); + }, + }); +} +harden(makePresenceManager); diff --git a/packages/kernel-browser-runtime/vitest.integration.config.ts b/packages/kernel-browser-runtime/vitest.integration.config.ts index 01ea8c4b3..6c20f76c6 100644 --- a/packages/kernel-browser-runtime/vitest.integration.config.ts +++ b/packages/kernel-browser-runtime/vitest.integration.config.ts @@ -18,6 +18,11 @@ export default defineConfig((args) => { fileURLToPath( import.meta.resolve('@ocap/repo-tools/test-utils/fetch-mock'), ), + // Use endoify-node which imports @libp2p/webrtc before lockdown + // (webrtc imports reflect-metadata which modifies globalThis.Reflect) + fileURLToPath( + import.meta.resolve('@metamask/kernel-shims/endoify-node'), + ), ], }, }), diff --git a/packages/kernel-shims/package.json b/packages/kernel-shims/package.json index eff3f6ba9..8ac7a013d 100644 --- a/packages/kernel-shims/package.json +++ b/packages/kernel-shims/package.json @@ -22,6 +22,7 @@ "./endoify": "./dist/endoify.js", "./endoify-repair": "./dist/endoify-repair.js", "./eventual-send": "./dist/eventual-send.js", + "./endoify-node": "./src/endoify-node.js", "./package.json": "./package.json" }, "main": "./dist/endoify.js", @@ -52,6 +53,14 @@ "@endo/lockdown": "^1.0.18", "ses": "^1.14.0" }, + "peerDependencies": { + "@libp2p/webrtc": "^5.0.0" + }, + "peerDependenciesMeta": { + "@libp2p/webrtc": { + "optional": true + } + }, "devDependencies": { "@endo/bundle-source": "^4.1.2", "@metamask/auto-changelog": "^5.3.0", diff --git a/packages/kernel-shims/src/endoify-node.js b/packages/kernel-shims/src/endoify-node.js new file mode 100644 index 000000000..5707cbf49 --- /dev/null +++ b/packages/kernel-shims/src/endoify-node.js @@ -0,0 +1,13 @@ +/* global hardenIntrinsics */ + +// Node.js-specific endoify that imports modules which modify globals before lockdown. +// This file is NOT bundled - it must be imported directly from src/. + +import './endoify-repair.js'; + +// @libp2p/webrtc needs to modify globals in Node.js only, so we need to import +// it before hardening. +// eslint-disable-next-line import-x/no-unresolved -- peer dependency +import '@libp2p/webrtc'; + +hardenIntrinsics(); diff --git a/packages/kernel-test/package.json b/packages/kernel-test/package.json index 5eb71fbac..d10c186f2 100644 --- a/packages/kernel-test/package.json +++ b/packages/kernel-test/package.json @@ -67,6 +67,7 @@ "@metamask/eslint-config": "^15.0.0", "@metamask/eslint-config-nodejs": "^15.0.0", "@metamask/eslint-config-typescript": "^15.0.0", + "@metamask/kernel-shims": "workspace:^", "@ocap/cli": "workspace:^", "@ocap/repo-tools": "workspace:^", "@typescript-eslint/eslint-plugin": "^8.29.0", diff --git a/packages/kernel-test/src/vatstore.test.ts b/packages/kernel-test/src/vatstore.test.ts index 991903cea..3b0a88775 100644 --- a/packages/kernel-test/src/vatstore.test.ts +++ b/packages/kernel-test/src/vatstore.test.ts @@ -1,4 +1,3 @@ -import '@ocap/nodejs/endoify-ts'; import type { VatStore, VatCheckpoint } from '@metamask/kernel-store'; import { makeSQLKernelDatabase } from '@metamask/kernel-store/sqlite/nodejs'; import type { ClusterConfig } from '@metamask/ocap-kernel'; diff --git a/packages/kernel-test/vitest.config.ts b/packages/kernel-test/vitest.config.ts index 47cf711f6..964287570 100644 --- a/packages/kernel-test/vitest.config.ts +++ b/packages/kernel-test/vitest.config.ts @@ -12,7 +12,9 @@ export default defineConfig((args) => { test: { name: 'kernel-test', setupFiles: [ - fileURLToPath(import.meta.resolve('@ocap/nodejs/endoify-ts')), + fileURLToPath( + import.meta.resolve('@metamask/kernel-shims/endoify-node'), + ), ], testTimeout: 30_000, }, diff --git a/packages/nodejs-test-workers/package.json b/packages/nodejs-test-workers/package.json index 60a72f88e..51357c716 100644 --- a/packages/nodejs-test-workers/package.json +++ b/packages/nodejs-test-workers/package.json @@ -80,6 +80,7 @@ "node": "^20.11 || >=22" }, "dependencies": { + "@metamask/kernel-shims": "workspace:^", "@metamask/logger": "workspace:^", "@metamask/ocap-kernel": "workspace:^", "@ocap/nodejs": "workspace:^" diff --git a/packages/nodejs-test-workers/src/workers/mock-fetch.ts b/packages/nodejs-test-workers/src/workers/mock-fetch.ts index ccca51833..58afd4844 100644 --- a/packages/nodejs-test-workers/src/workers/mock-fetch.ts +++ b/packages/nodejs-test-workers/src/workers/mock-fetch.ts @@ -1,4 +1,4 @@ -import '@ocap/nodejs/endoify-mjs'; +import '@metamask/kernel-shims/endoify-node'; import { Logger } from '@metamask/logger'; import type { VatId } from '@metamask/ocap-kernel'; import { makeNodeJsVatSupervisor } from '@ocap/nodejs'; diff --git a/packages/nodejs/package.json b/packages/nodejs/package.json index ec7cebc4a..34416e409 100644 --- a/packages/nodejs/package.json +++ b/packages/nodejs/package.json @@ -23,8 +23,6 @@ "default": "./dist/index.cjs" } }, - "./endoify-mjs": "./dist/env/endoify.mjs", - "./endoify-ts": "./src/env/endoify.ts", "./package.json": "./package.json" }, "files": [ diff --git a/packages/nodejs/src/env/endoify.ts b/packages/nodejs/src/env/endoify.ts deleted file mode 100644 index e494bcb24..000000000 --- a/packages/nodejs/src/env/endoify.ts +++ /dev/null @@ -1,7 +0,0 @@ -import '@metamask/kernel-shims/endoify-repair'; - -// @libp2p/webrtc needs to modify globals in Node.js only, so we need to import -// it before hardening. -import '@libp2p/webrtc'; - -hardenIntrinsics(); diff --git a/packages/nodejs/src/kernel/PlatformServices.test.ts b/packages/nodejs/src/kernel/PlatformServices.test.ts index 609613990..e648193f7 100644 --- a/packages/nodejs/src/kernel/PlatformServices.test.ts +++ b/packages/nodejs/src/kernel/PlatformServices.test.ts @@ -1,5 +1,3 @@ -import '../env/endoify.ts'; - import { makeCounter } from '@metamask/kernel-utils'; import type { VatId } from '@metamask/ocap-kernel'; import { Worker as NodeWorker } from 'node:worker_threads'; diff --git a/packages/nodejs/src/kernel/make-kernel.test.ts b/packages/nodejs/src/kernel/make-kernel.test.ts index b54e57ef7..2fdfdb43d 100644 --- a/packages/nodejs/src/kernel/make-kernel.test.ts +++ b/packages/nodejs/src/kernel/make-kernel.test.ts @@ -1,5 +1,3 @@ -import '../env/endoify.ts'; - import { Kernel } from '@metamask/ocap-kernel'; import { describe, expect, it, vi } from 'vitest'; diff --git a/packages/nodejs/src/vat/vat-worker.test.ts b/packages/nodejs/src/vat/vat-worker.test.ts index 3df85e695..763215216 100644 --- a/packages/nodejs/src/vat/vat-worker.test.ts +++ b/packages/nodejs/src/vat/vat-worker.test.ts @@ -1,5 +1,3 @@ -import '../env/endoify.ts'; - import { makeCounter } from '@metamask/kernel-utils'; import type { VatId } from '@metamask/ocap-kernel'; import { makePromiseKitMock } from '@ocap/repo-tools/test-utils'; diff --git a/packages/nodejs/src/vat/vat-worker.ts b/packages/nodejs/src/vat/vat-worker.ts index 4eccdb196..c08d2f17d 100644 --- a/packages/nodejs/src/vat/vat-worker.ts +++ b/packages/nodejs/src/vat/vat-worker.ts @@ -1,4 +1,4 @@ -import '../env/endoify.ts'; +import '@metamask/kernel-shims/endoify-node'; import { Logger } from '@metamask/logger'; import type { VatId } from '@metamask/ocap-kernel'; diff --git a/packages/nodejs/test/e2e/PlatformServices.test.ts b/packages/nodejs/test/e2e/PlatformServices.test.ts index 2bd4fef41..14f444fb7 100644 --- a/packages/nodejs/test/e2e/PlatformServices.test.ts +++ b/packages/nodejs/test/e2e/PlatformServices.test.ts @@ -1,5 +1,3 @@ -import '../../src/env/endoify.ts'; - import { makeCounter } from '@metamask/kernel-utils'; import type { VatId } from '@metamask/ocap-kernel'; import { NodeWorkerDuplexStream } from '@metamask/streams'; diff --git a/packages/nodejs/test/e2e/kernel-worker.test.ts b/packages/nodejs/test/e2e/kernel-worker.test.ts index ba61e57cc..7573bf33e 100644 --- a/packages/nodejs/test/e2e/kernel-worker.test.ts +++ b/packages/nodejs/test/e2e/kernel-worker.test.ts @@ -1,5 +1,3 @@ -import '../../src/env/endoify.ts'; - import { Kernel } from '@metamask/ocap-kernel'; import type { ClusterConfig } from '@metamask/ocap-kernel'; import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; diff --git a/packages/nodejs/test/e2e/remote-comms.test.ts b/packages/nodejs/test/e2e/remote-comms.test.ts index b5127c09b..faeb2cae2 100644 --- a/packages/nodejs/test/e2e/remote-comms.test.ts +++ b/packages/nodejs/test/e2e/remote-comms.test.ts @@ -1,5 +1,3 @@ -import '../../src/env/endoify.ts'; - import type { Libp2p } from '@libp2p/interface'; import { makeSQLKernelDatabase } from '@metamask/kernel-store/sqlite/nodejs'; import { Kernel, kunser, makeKernelStore } from '@metamask/ocap-kernel'; diff --git a/packages/nodejs/test/workers/stream-sync.js b/packages/nodejs/test/workers/stream-sync.js index 9b39391ad..0889812ea 100644 --- a/packages/nodejs/test/workers/stream-sync.js +++ b/packages/nodejs/test/workers/stream-sync.js @@ -1,4 +1,4 @@ -import '../../dist/env/endoify.mjs'; +import '@metamask/kernel-shims/endoify-node'; import { makeStreams } from '../../dist/vat/streams.mjs'; main().catch(console.error); diff --git a/packages/nodejs/vitest.config.e2e.ts b/packages/nodejs/vitest.config.e2e.ts index cd509ee6b..b72ebb5cb 100644 --- a/packages/nodejs/vitest.config.e2e.ts +++ b/packages/nodejs/vitest.config.e2e.ts @@ -1,4 +1,5 @@ import { mergeConfig } from '@ocap/repo-tools/vitest-config'; +import { fileURLToPath } from 'node:url'; import { defineConfig, defineProject } from 'vitest/config'; import defaultConfig from '../../vitest.config.ts'; @@ -11,6 +12,11 @@ export default defineConfig((args) => { test: { name: 'nodejs:e2e', pool: 'forks', + setupFiles: [ + fileURLToPath( + import.meta.resolve('@metamask/kernel-shims/endoify-node'), + ), + ], include: ['./test/e2e/**/*.test.ts'], exclude: ['./src/**/*'], env: { diff --git a/packages/nodejs/vitest.config.ts b/packages/nodejs/vitest.config.ts index 0b8767bab..208d6346b 100644 --- a/packages/nodejs/vitest.config.ts +++ b/packages/nodejs/vitest.config.ts @@ -1,4 +1,5 @@ import { mergeConfig } from '@ocap/repo-tools/vitest-config'; +import { fileURLToPath } from 'node:url'; import { defineConfig, defineProject } from 'vitest/config'; import defaultConfig from '../../vitest.config.ts'; @@ -10,6 +11,11 @@ export default defineConfig((args) => { defineProject({ test: { name: 'nodejs', + setupFiles: [ + fileURLToPath( + import.meta.resolve('@metamask/kernel-shims/endoify-node'), + ), + ], include: ['./src/**/*.test.ts'], exclude: ['./test/e2e/'], }, diff --git a/packages/ocap-kernel/vitest.config.ts b/packages/ocap-kernel/vitest.config.ts index e049418f5..6264a93d4 100644 --- a/packages/ocap-kernel/vitest.config.ts +++ b/packages/ocap-kernel/vitest.config.ts @@ -12,9 +12,9 @@ export default defineConfig((args) => { test: { name: 'kernel', setupFiles: [ - // This is actually a circular dependency relationship, but it's fine because we're - // targeting the TypeScript source file and not listing @ocap/nodejs in package.json. - fileURLToPath(import.meta.resolve('@ocap/nodejs/endoify-ts')), + fileURLToPath( + import.meta.resolve('@metamask/kernel-shims/endoify-node'), + ), ], }, }), diff --git a/packages/omnium-gatherum/README.md b/packages/omnium-gatherum/README.md index 688955bae..cfb41d330 100644 --- a/packages/omnium-gatherum/README.md +++ b/packages/omnium-gatherum/README.md @@ -10,6 +10,31 @@ or `npm install @ocap/omnium-gatherum` +## Usage + +### Installing and using the `echo` caplet + +After loading the extension, open the background console (chrome://extensions → Omnium → "Inspect views: service worker") and run the following: + +```javascript +// 1. Load the echo caplet manifest and bundle +const { manifest, bundle } = await omnium.caplet.load('echo'); + +// 2. Install the caplet +const installResult = await omnium.caplet.install(manifest, bundle); + +// 3. Get the caplet's root kref +const capletInfo = await omnium.caplet.get(installResult.capletId); +const rootKref = capletInfo.rootKref; + +// 4. Resolve the kref to an E()-usable presence +const echoRoot = omnium.resolveKref(rootKref); + +// 5. Call the echo method +const result = await E(echoRoot).echo('Hello, world!'); +console.log(result); // "echo: Hello, world!" +``` + ## Contributing This package is part of a monorepo. Instructions for contributing can be found in the [monorepo README](https://github.com/MetaMask/ocap-kernel#readme). diff --git a/packages/omnium-gatherum/src/background.ts b/packages/omnium-gatherum/src/background.ts index 942bb4054..3862917e9 100644 --- a/packages/omnium-gatherum/src/background.ts +++ b/packages/omnium-gatherum/src/background.ts @@ -1,13 +1,14 @@ import { E } from '@endo/eventual-send'; import { makeBackgroundCapTP, + makePresenceManager, makeCapTPNotification, isCapTPNotification, getCapTPMessage, } from '@metamask/kernel-browser-runtime'; import type { CapTPMessage, - KernelFacade, + PresenceManager, } from '@metamask/kernel-browser-runtime'; import { delay, isJsonRpcMessage, stringify } from '@metamask/kernel-utils'; import type { JsonRpcMessage } from '@metamask/kernel-utils'; @@ -32,7 +33,8 @@ let bootPromise: Promise | null = null; // With this we can click the extension action button to wake up the service worker. chrome.action.onClicked.addListener(() => { - omnium.ping?.().catch(logger.error); + globalThis.kernel !== undefined && + E(globalThis.kernel).ping().catch(logger.error); }); // Install/update @@ -113,12 +115,11 @@ async function main(): Promise { }); const kernelP = backgroundCapTP.getKernel(); - globals.setKernelP(kernelP); + globalThis.kernel = kernelP; - globals.setPing(async (): Promise => { - const result = await E(kernelP).ping(); - logger.info(result); - }); + // Create presence manager for E() on vat objects + const presenceManager = makePresenceManager({ kernelFacade: kernelP }); + globals.setPresenceManager(presenceManager); // Create storage adapter const storageAdapter = makeChromeStorageAdapter(); @@ -168,9 +169,8 @@ async function main(): Promise { } type GlobalSetters = { - setKernelP: (value: Promise) => void; - setPing: (value: () => Promise) => void; setCapletController: (value: CapletControllerFacet) => void; + setPresenceManager: (value: PresenceManager) => void; }; /** @@ -179,6 +179,9 @@ type GlobalSetters = { * @returns A device for setting the global values. */ function defineGlobals(): GlobalSetters { + let capletController: CapletControllerFacet; + let presenceManager: PresenceManager; + Object.defineProperty(globalThis, 'E', { configurable: false, enumerable: true, @@ -186,6 +189,13 @@ function defineGlobals(): GlobalSetters { value: E, }); + Object.defineProperty(globalThis, 'kernel', { + configurable: false, + enumerable: true, + writable: true, + value: undefined, + }); + Object.defineProperty(globalThis, 'omnium', { configurable: false, enumerable: true, @@ -193,10 +203,6 @@ function defineGlobals(): GlobalSetters { value: {}, }); - let kernelP: Promise; - let ping: (() => Promise) | undefined; - let capletController: CapletControllerFacet; - /** * Load a caplet's manifest and bundle by ID. * @@ -237,12 +243,6 @@ function defineGlobals(): GlobalSetters { }; Object.defineProperties(globalThis.omnium, { - ping: { - get: () => ping, - }, - getKernel: { - value: async () => kernelP, - }, caplet: { value: harden({ install: async (manifest: CapletManifest, bundle?: unknown) => @@ -258,18 +258,21 @@ function defineGlobals(): GlobalSetters { E(capletController).getCapletRoot(capletId), }), }, + resolveKref: { + get: () => presenceManager.resolveKref, + }, + krefOf: { + get: () => presenceManager.krefOf, + }, }); harden(globalThis.omnium); return { - setKernelP: (value) => { - kernelP = value; - }, - setPing: (value) => { - ping = value; - }, setCapletController: (value) => { capletController = value; }, + setPresenceManager: (value) => { + presenceManager = value; + }, }; } diff --git a/packages/omnium-gatherum/src/global.d.ts b/packages/omnium-gatherum/src/global.d.ts index ae1c07853..e330a9b0f 100644 --- a/packages/omnium-gatherum/src/global.d.ts +++ b/packages/omnium-gatherum/src/global.d.ts @@ -22,24 +22,10 @@ declare global { var E: typeof import('@endo/eventual-send').E; // eslint-disable-next-line no-var - var omnium: { - /** - * Ping the kernel to verify connectivity. - */ - ping: () => Promise; - - /** - * Get the kernel remote presence for use with E(). - * - * @returns A promise for the kernel facade remote presence. - * @example - * ```typescript - * const kernel = await omnium.getKernel(); - * const status = await E(kernel).getStatus(); - * ``` - */ - getKernel: () => Promise; + var kernel: KernelFacade | Promise; + // eslint-disable-next-line no-var + var omnium: { /** * Load a caplet's manifest and bundle by ID. * diff --git a/packages/omnium-gatherum/src/vats/echo-caplet.js b/packages/omnium-gatherum/src/vats/echo-caplet.js index d6c03d660..83d99f828 100644 --- a/packages/omnium-gatherum/src/vats/echo-caplet.js +++ b/packages/omnium-gatherum/src/vats/echo-caplet.js @@ -42,7 +42,7 @@ export function buildRootObject(vatPowers, _parameters, _baggage) { */ echo(message) { logger.log('Echoing message:', message); - return `Echo: ${message}`; + return `echo: ${message}`; }, }); } diff --git a/yarn.lock b/yarn.lock index a41a32194..f45b09313 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2285,6 +2285,7 @@ __metadata: "@endo/captp": "npm:^4.4.8" "@endo/eventual-send": "npm:^1.3.4" "@endo/marshal": "npm:^1.8.0" + "@libp2p/webrtc": "npm:5.2.24" "@metamask/auto-changelog": "npm:^5.3.0" "@metamask/eslint-config": "npm:^15.0.0" "@metamask/eslint-config-nodejs": "npm:^15.0.0" @@ -2451,6 +2452,11 @@ __metadata: typescript-eslint: "npm:^8.29.0" vite: "npm:^7.3.0" vitest: "npm:^4.0.16" + peerDependencies: + "@libp2p/webrtc": ^5.0.0 + peerDependenciesMeta: + "@libp2p/webrtc": + optional: true languageName: unknown linkType: soft @@ -3730,6 +3736,7 @@ __metadata: "@metamask/eslint-config": "npm:^15.0.0" "@metamask/eslint-config-nodejs": "npm:^15.0.0" "@metamask/eslint-config-typescript": "npm:^15.0.0" + "@metamask/kernel-shims": "workspace:^" "@metamask/kernel-store": "workspace:^" "@metamask/kernel-utils": "workspace:^" "@metamask/logger": "workspace:^" @@ -3831,6 +3838,7 @@ __metadata: "@metamask/eslint-config": "npm:^15.0.0" "@metamask/eslint-config-nodejs": "npm:^15.0.0" "@metamask/eslint-config-typescript": "npm:^15.0.0" + "@metamask/kernel-shims": "workspace:^" "@metamask/logger": "workspace:^" "@metamask/ocap-kernel": "workspace:^" "@ocap/nodejs": "workspace:^"