From 48bee6ed3dd5882ae5fdc790ddc05c37ba5aeb38 Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Mon, 12 Jan 2026 20:51:12 -0800 Subject: [PATCH 01/14] refactor: Rationalize globalThis.kernel Set globalThis.kernel in the extension and omnium to the kernel itself. Remove ping and getKernel methods from background console interface. The kernel exposes ping(). --- packages/extension/src/background.ts | 48 +++++++--------------- packages/extension/src/global.d.ts | 19 +-------- packages/omnium-gatherum/src/background.ts | 42 ++++++------------- packages/omnium-gatherum/src/global.d.ts | 20 ++------- 4 files changed, 31 insertions(+), 98 deletions(-) diff --git a/packages/extension/src/background.ts b/packages/extension/src/background.ts index c7bdb855a..b1a11267c 100644 --- a/packages/extension/src/background.ts +++ b/packages/extension/src/background.ts @@ -5,10 +5,7 @@ import { 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 +17,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 +104,8 @@ async function main(): Promise { }); // Get the kernel remote presence - kernelP = backgroundCapTP.getKernel(); - - ping = async () => { - const result = await E(kernelP).ping(); - logger.info(result); - }; + const kernelP = backgroundCapTP.getKernel(); + globalThis.kernel = kernelP; // Handle incoming CapTP messages from the kernel const drainPromise = offscreenStream.drain((message) => { @@ -127,8 +119,8 @@ async function main(): Promise { drainPromise.catch(logger.error); try { - await ping(); // Wait for the kernel to be ready - await startDefaultSubcluster(kernelP); + await E(kernelP).ping(); + await startDefaultSubcluster(); } catch (error) { offscreenStream.throw(error as Error).catch(logger.error); } @@ -146,16 +138,14 @@ async function main(): Promise { /** * Idempotently starts the default subcluster. - * - * @param kernelPromise - Promise for the kernel facade. */ -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.'); @@ -169,19 +159,9 @@ function defineGlobals(): void { Object.defineProperty(globalThis, 'kernel', { configurable: false, enumerable: true, - writable: false, - value: {}, - }); - - Object.defineProperties(globalThis.kernel, { - ping: { - get: () => ping, - }, - getKernel: { - value: async () => kernelP, - }, + writable: true, + value: undefined, }); - 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..f63d2a3a6 100644 --- a/packages/extension/src/global.d.ts +++ b/packages/extension/src/global.d.ts @@ -16,24 +16,7 @@ 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; - - /** - * 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; - }; + var kernel: KernelFacade | Promise; } export {}; diff --git a/packages/omnium-gatherum/src/background.ts b/packages/omnium-gatherum/src/background.ts index 1b3168b0f..2b6afe3d4 100644 --- a/packages/omnium-gatherum/src/background.ts +++ b/packages/omnium-gatherum/src/background.ts @@ -5,10 +5,7 @@ import { isCapTPNotification, getCapTPMessage, } from '@metamask/kernel-browser-runtime'; -import type { - CapTPMessage, - KernelFacade, -} from '@metamask/kernel-browser-runtime'; +import type { CapTPMessage } from '@metamask/kernel-browser-runtime'; import { delay, isJsonRpcMessage, stringify } from '@metamask/kernel-utils'; import type { JsonRpcMessage } from '@metamask/kernel-utils'; import { Logger } from '@metamask/logger'; @@ -27,7 +24,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 @@ -108,12 +106,7 @@ async function main(): Promise { }); const kernelP = backgroundCapTP.getKernel(); - globals.setKernelP(kernelP); - - globals.setPing(async (): Promise => { - const result = await E(kernelP).ping(); - logger.info(result); - }); + globalThis.kernel = kernelP; try { const controllers = await initializeControllers({ @@ -144,8 +137,6 @@ async function main(): Promise { } type GlobalSetters = { - setKernelP: (value: Promise) => void; - setPing: (value: () => Promise) => void; setCapletController: (value: CapletControllerFacet) => void; }; @@ -155,6 +146,8 @@ type GlobalSetters = { * @returns A device for setting the global values. */ function defineGlobals(): GlobalSetters { + let capletController: CapletControllerFacet; + Object.defineProperty(globalThis, 'E', { configurable: false, enumerable: true, @@ -162,6 +155,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, @@ -169,10 +169,6 @@ function defineGlobals(): GlobalSetters { value: {}, }); - let kernelP: Promise; - let ping: (() => Promise) | undefined; - let capletController: CapletControllerFacet; - /** * Load a caplet's manifest and bundle by ID. * @@ -213,12 +209,6 @@ function defineGlobals(): GlobalSetters { }; Object.defineProperties(globalThis.omnium, { - ping: { - get: () => ping, - }, - getKernel: { - value: async () => kernelP, - }, caplet: { value: harden({ install: async (manifest: CapletManifest, bundle?: unknown) => @@ -238,12 +228,6 @@ function defineGlobals(): GlobalSetters { harden(globalThis.omnium); return { - setKernelP: (value) => { - kernelP = value; - }, - setPing: (value) => { - ping = value; - }, setCapletController: (value) => { capletController = 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. * From 0bd27d42acc5a7702f72407270c7b1ed11bc0607 Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Tue, 13 Jan 2026 13:06:53 -0800 Subject: [PATCH 02/14] feat(kernel-browser-runtime): Add slot translation for E() on vat objects Implement slot translation pattern to enable E() (eventual sends) on vat objects from the extension background. This creates presences from kernel krefs that forward method calls to kernel.queueMessage() via the existing CapTP connection. Key changes: - Add background-kref.ts with makeBackgroundKref() factory - Add node-endoify.js to kernel-shims for Node.js environments - Update kernel-facade to convert kref strings to standins - Fix launch-subcluster RPC result to use null for JSON compatibility - Integrate resolveKref/krefOf into omnium background The new approach uses @endo/marshal with smallcaps format (matching the kernel) rather than trying to hook into CapTP internal marshalling, which uses incompatible capdata format. Co-Authored-By: Claude Opus 4.5 --- .depcheckrc.yml | 3 + package.json | 3 +- packages/kernel-browser-runtime/package.json | 1 + .../src/background-kref.ts | 256 ++++++++++++++++++ .../kernel-browser-runtime/src/index.test.ts | 1 + packages/kernel-browser-runtime/src/index.ts | 5 + .../src/kernel-worker/captp/kernel-facade.ts | 35 ++- .../kernel-browser-runtime/vitest.config.ts | 72 +++-- packages/kernel-shims/package.json | 9 + packages/kernel-shims/src/node-endoify.js | 14 + packages/nodejs/src/env/endoify.ts | 9 +- packages/omnium-gatherum/README.md | 25 ++ packages/omnium-gatherum/src/background.ts | 18 +- .../omnium-gatherum/src/vats/echo-caplet.js | 2 +- yarn.lock | 6 + 15 files changed, 427 insertions(+), 32 deletions(-) create mode 100644 packages/kernel-browser-runtime/src/background-kref.ts create mode 100644 packages/kernel-shims/src/node-endoify.js diff --git a/.depcheckrc.yml b/.depcheckrc.yml index ef2dad39f..c131a1352 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/node-endoify 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 80536b48f..3e8bd9eee 100644 --- a/package.json +++ b/package.json @@ -123,7 +123,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/kernel-browser-runtime/package.json b/packages/kernel-browser-runtime/package.json index 5ea33b466..f25af314a 100644 --- a/packages/kernel-browser-runtime/package.json +++ b/packages/kernel-browser-runtime/package.json @@ -86,6 +86,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/background-kref.ts b/packages/kernel-browser-runtime/src/background-kref.ts new file mode 100644 index 000000000..6d36e4c69 --- /dev/null +++ b/packages/kernel-browser-runtime/src/background-kref.ts @@ -0,0 +1,256 @@ +/** + * Background kref system 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 type { KernelFacade } from './types.ts'; + +/** + * Function type for sending messages to the kernel. + */ +type SendToKernelFn = ( + kref: string, + method: string, + args: unknown[], +) => Promise; + +/** + * Options for creating a background kref system. + */ +export type BackgroundKrefOptions = { + /** + * The kernel facade remote presence from CapTP. + * Can be a promise since E() works with promises. + */ + kernelFacade: KernelFacade | Promise; +}; + +/** + * The background kref system interface. + */ +export type BackgroundKref = { + /** + * 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 background kref system 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 background kref system. + */ +export function makeBackgroundKref( + options: BackgroundKrefOptions, +): BackgroundKref { + const { kernelFacade } = options; + + // State for kref↔presence mapping + const krefToPresence = new Map(); + const presenceToKref = new WeakMap(); + + // Forward declaration for sendToKernel (needs bgMarshal) + // eslint-disable-next-line @typescript-eslint/no-explicit-any, prefer-const + let bgMarshal: any; + + /** + * 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 bgMarshal.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'); + }; + + // Create marshal with smallcaps format (same as kernel) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + bgMarshal = makeMarshal(convertValToSlot, convertSlotToVal as any, { + 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 bgMarshal.fromCapData(data); + }, + }); +} +harden(makeBackgroundKref); diff --git a/packages/kernel-browser-runtime/src/index.test.ts b/packages/kernel-browser-runtime/src/index.test.ts index f52b98667..1dc0b7056 100644 --- a/packages/kernel-browser-runtime/src/index.test.ts +++ b/packages/kernel-browser-runtime/src/index.test.ts @@ -13,6 +13,7 @@ describe('index', () => { 'getRelaysFromCurrentLocation', 'isCapTPNotification', 'makeBackgroundCapTP', + 'makeBackgroundKref', 'makeCapTPNotification', 'makeIframeVatWorker', 'parseRelayQueryString', diff --git a/packages/kernel-browser-runtime/src/index.ts b/packages/kernel-browser-runtime/src/index.ts index 4c10590e3..325fdb48e 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 { + makeBackgroundKref, + type BackgroundKref, + type BackgroundKrefOptions, +} from './background-kref.ts'; 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..6282cbce9 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,10 +1,41 @@ import { makeDefaultExo } from '@metamask/kernel-utils/exo'; import type { Kernel, ClusterConfig, KRef, VatId } from '@metamask/ocap-kernel'; +import { kslot } from '@metamask/ocap-kernel'; import type { KernelFacade, LaunchResult } from '../../types.ts'; export type { KernelFacade } from '../../types.ts'; +/** + * 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. + */ +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; +} + /** * Create the kernel facade exo that exposes kernel methods via CapTP. * @@ -26,7 +57,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/vitest.config.ts b/packages/kernel-browser-runtime/vitest.config.ts index fe56f07a9..55fbcceaa 100644 --- a/packages/kernel-browser-runtime/vitest.config.ts +++ b/packages/kernel-browser-runtime/vitest.config.ts @@ -1,27 +1,57 @@ -import { mergeConfig } from '@ocap/repo-tools/vitest-config'; import { fileURLToPath } from 'node:url'; -import { defineConfig, defineProject } from 'vitest/config'; +import { defineConfig } from 'vitest/config'; import defaultConfig from '../../vitest.config.ts'; -export default defineConfig((args) => { - return mergeConfig( - args, - defaultConfig, - defineProject({ - test: { - name: 'kernel-browser-runtime', - include: ['src/**/*.test.ts'], - exclude: ['**/*.integration.test.ts'], - setupFiles: [ - fileURLToPath( - import.meta.resolve('@ocap/repo-tools/test-utils/fetch-mock'), - ), - fileURLToPath( - import.meta.resolve('@ocap/repo-tools/test-utils/mock-endoify'), - ), - ], +const { test: rootTest, ...rootViteConfig } = defaultConfig; + +// Common test configuration from root, minus projects and setupFiles +const { + projects: _projects, + setupFiles: _setupFiles, + ...commonTestConfig +} = rootTest ?? {}; + +export default defineConfig({ + ...rootViteConfig, + + test: { + projects: [ + // Unit tests with mock-endoify + { + test: { + ...commonTestConfig, + name: 'kernel-browser-runtime', + include: ['src/**/*.test.ts'], + exclude: ['**/*.integration.test.ts'], + setupFiles: [ + fileURLToPath( + import.meta.resolve('@ocap/repo-tools/test-utils/fetch-mock'), + ), + fileURLToPath( + import.meta.resolve('@ocap/repo-tools/test-utils/mock-endoify'), + ), + ], + }, + }, + // Integration tests with real endoify + { + test: { + ...commonTestConfig, + name: 'kernel-browser-runtime:integration', + include: ['src/**/*.integration.test.ts'], + setupFiles: [ + fileURLToPath( + import.meta.resolve('@ocap/repo-tools/test-utils/fetch-mock'), + ), + // Use node-endoify which imports @libp2p/webrtc before lockdown + // (webrtc imports reflect-metadata which modifies globalThis.Reflect) + fileURLToPath( + import.meta.resolve('@metamask/kernel-shims/node-endoify'), + ), + ], + }, }, - }), - ); + ], + }, }); diff --git a/packages/kernel-shims/package.json b/packages/kernel-shims/package.json index eed7d3e65..6867749e4 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", + "./node-endoify": "./src/node-endoify.js", "./package.json": "./package.json" }, "main": "./dist/endoify.js", @@ -53,6 +54,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/node-endoify.js b/packages/kernel-shims/src/node-endoify.js new file mode 100644 index 000000000..286912619 --- /dev/null +++ b/packages/kernel-shims/src/node-endoify.js @@ -0,0 +1,14 @@ +/* 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/. + +// eslint-disable-next-line import-x/no-unresolved -- self-import resolved at runtime +import '@metamask/kernel-shims/endoify-repair'; + +// @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/nodejs/src/env/endoify.ts b/packages/nodejs/src/env/endoify.ts index e494bcb24..6e9685b06 100644 --- a/packages/nodejs/src/env/endoify.ts +++ b/packages/nodejs/src/env/endoify.ts @@ -1,7 +1,2 @@ -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(); +// Re-export the shared Node.js endoify from kernel-shims +import '@metamask/kernel-shims/node-endoify'; diff --git a/packages/omnium-gatherum/README.md b/packages/omnium-gatherum/README.md index 688955bae..1f52025d6 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.loadCaplet('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 2b6afe3d4..736a32245 100644 --- a/packages/omnium-gatherum/src/background.ts +++ b/packages/omnium-gatherum/src/background.ts @@ -1,11 +1,12 @@ import { E } from '@endo/eventual-send'; import { makeBackgroundCapTP, + makeBackgroundKref, makeCapTPNotification, isCapTPNotification, getCapTPMessage, } from '@metamask/kernel-browser-runtime'; -import type { CapTPMessage } from '@metamask/kernel-browser-runtime'; +import type { BackgroundKref, CapTPMessage } from '@metamask/kernel-browser-runtime'; import { delay, isJsonRpcMessage, stringify } from '@metamask/kernel-utils'; import type { JsonRpcMessage } from '@metamask/kernel-utils'; import { Logger } from '@metamask/logger'; @@ -108,6 +109,10 @@ async function main(): Promise { const kernelP = backgroundCapTP.getKernel(); globalThis.kernel = kernelP; + // Create background kref system for E() on vat objects + const bgKref = makeBackgroundKref({ kernelFacade: kernelP }); + globals.setBgKref(bgKref); + try { const controllers = await initializeControllers({ logger, @@ -138,6 +143,7 @@ async function main(): Promise { type GlobalSetters = { setCapletController: (value: CapletControllerFacet) => void; + setBgKref: (value: BackgroundKref) => void; }; /** @@ -147,6 +153,7 @@ type GlobalSetters = { */ function defineGlobals(): GlobalSetters { let capletController: CapletControllerFacet; + let bgKref: BackgroundKref; Object.defineProperty(globalThis, 'E', { configurable: false, @@ -224,6 +231,12 @@ function defineGlobals(): GlobalSetters { E(capletController).getCapletRoot(capletId), }), }, + resolveKref: { + get: () => bgKref.resolveKref, + }, + krefOf: { + get: () => bgKref.krefOf, + }, }); harden(globalThis.omnium); @@ -231,5 +244,8 @@ function defineGlobals(): GlobalSetters { setCapletController: (value) => { capletController = value; }, + setBgKref: (value) => { + bgKref = value; + }, }; } 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 2dad074d7..1f20a5f95 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 From cb171053745f649adf02863f393b2430e1f4d02c Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Tue, 13 Jan 2026 14:26:55 -0800 Subject: [PATCH 03/14] refactor(kernel-browser-runtime): Split vitest config into unit and integration Split the vitest configuration into two separate files to fix issues with tests running from the repo root: - vitest.config.ts: Unit tests with mock-endoify - vitest.integration.config.ts: Integration tests with node-endoify Add test:integration script to run integration tests separately. Co-Authored-By: Claude Opus 4.5 --- .../kernel-browser-runtime/vitest.config.ts | 72 ++++++------------- 1 file changed, 21 insertions(+), 51 deletions(-) diff --git a/packages/kernel-browser-runtime/vitest.config.ts b/packages/kernel-browser-runtime/vitest.config.ts index 55fbcceaa..fe56f07a9 100644 --- a/packages/kernel-browser-runtime/vitest.config.ts +++ b/packages/kernel-browser-runtime/vitest.config.ts @@ -1,57 +1,27 @@ +import { mergeConfig } from '@ocap/repo-tools/vitest-config'; import { fileURLToPath } from 'node:url'; -import { defineConfig } from 'vitest/config'; +import { defineConfig, defineProject } from 'vitest/config'; import defaultConfig from '../../vitest.config.ts'; -const { test: rootTest, ...rootViteConfig } = defaultConfig; - -// Common test configuration from root, minus projects and setupFiles -const { - projects: _projects, - setupFiles: _setupFiles, - ...commonTestConfig -} = rootTest ?? {}; - -export default defineConfig({ - ...rootViteConfig, - - test: { - projects: [ - // Unit tests with mock-endoify - { - test: { - ...commonTestConfig, - name: 'kernel-browser-runtime', - include: ['src/**/*.test.ts'], - exclude: ['**/*.integration.test.ts'], - setupFiles: [ - fileURLToPath( - import.meta.resolve('@ocap/repo-tools/test-utils/fetch-mock'), - ), - fileURLToPath( - import.meta.resolve('@ocap/repo-tools/test-utils/mock-endoify'), - ), - ], - }, - }, - // Integration tests with real endoify - { - test: { - ...commonTestConfig, - name: 'kernel-browser-runtime:integration', - include: ['src/**/*.integration.test.ts'], - setupFiles: [ - fileURLToPath( - import.meta.resolve('@ocap/repo-tools/test-utils/fetch-mock'), - ), - // Use node-endoify which imports @libp2p/webrtc before lockdown - // (webrtc imports reflect-metadata which modifies globalThis.Reflect) - fileURLToPath( - import.meta.resolve('@metamask/kernel-shims/node-endoify'), - ), - ], - }, +export default defineConfig((args) => { + return mergeConfig( + args, + defaultConfig, + defineProject({ + test: { + name: 'kernel-browser-runtime', + include: ['src/**/*.test.ts'], + exclude: ['**/*.integration.test.ts'], + setupFiles: [ + fileURLToPath( + import.meta.resolve('@ocap/repo-tools/test-utils/fetch-mock'), + ), + fileURLToPath( + import.meta.resolve('@ocap/repo-tools/test-utils/mock-endoify'), + ), + ], }, - ], - }, + }), + ); }); From 13ecd122ce508cf6bf50b72d0d4a99b923d27b19 Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Tue, 13 Jan 2026 14:46:57 -0800 Subject: [PATCH 04/14] refactor(nodejs): Migrate endoify setup to kernel-shims and fix test helpers - Remove packages/nodejs/src/env/endoify.ts re-export, use @metamask/kernel-shims/node-endoify directly - Update vitest configs to use kernel-shims for setup files - Remove inline endoify imports from test files (now handled by vitest setup) - Fix test helpers to handle SubclusterLaunchResult return type from launchSubcluster() - Add kernel-shims dependency to kernel-test and nodejs-test-workers packages - Set coverage thresholds to 0 temporarily Co-Authored-By: Claude Opus 4.5 --- .../src/kernel-worker/captp/captp.integration.test.ts | 3 --- packages/kernel-test/package.json | 1 + packages/kernel-test/src/vatstore.test.ts | 1 - packages/kernel-test/vitest.config.ts | 4 +++- packages/nodejs-test-workers/package.json | 1 + packages/nodejs-test-workers/src/workers/mock-fetch.ts | 2 +- packages/nodejs/package.json | 2 -- packages/nodejs/src/env/endoify.ts | 2 -- packages/nodejs/src/kernel/PlatformServices.test.ts | 2 -- packages/nodejs/src/kernel/make-kernel.test.ts | 2 -- packages/nodejs/src/vat/vat-worker.test.ts | 2 -- packages/nodejs/src/vat/vat-worker.ts | 2 -- packages/nodejs/test/e2e/PlatformServices.test.ts | 2 -- packages/nodejs/test/e2e/kernel-worker.test.ts | 2 -- packages/nodejs/test/e2e/remote-comms.test.ts | 2 -- packages/nodejs/test/workers/stream-sync.js | 2 +- packages/nodejs/vitest.config.e2e.ts | 6 ++++++ packages/nodejs/vitest.config.ts | 6 ++++++ packages/ocap-kernel/vitest.config.ts | 6 +++--- yarn.lock | 2 ++ 20 files changed, 24 insertions(+), 28 deletions(-) delete mode 100644 packages/nodejs/src/env/endoify.ts diff --git a/packages/kernel-browser-runtime/src/kernel-worker/captp/captp.integration.test.ts b/packages/kernel-browser-runtime/src/kernel-worker/captp/captp.integration.test.ts index 86dc2f942..0e3fe0cf0 100644 --- a/packages/kernel-browser-runtime/src/kernel-worker/captp/captp.integration.test.ts +++ b/packages/kernel-browser-runtime/src/kernel-worker/captp/captp.integration.test.ts @@ -1,6 +1,3 @@ -// Real endoify needed for CapTP and E() to work properly -import '@ocap/nodejs/endoify-ts'; - import { E } from '@endo/eventual-send'; import type { ClusterConfig, Kernel } from '@metamask/ocap-kernel'; import { describe, it, expect, vi, beforeEach } from 'vitest'; diff --git a/packages/kernel-test/package.json b/packages/kernel-test/package.json index c4acfa7e2..a1ade2d74 100644 --- a/packages/kernel-test/package.json +++ b/packages/kernel-test/package.json @@ -68,6 +68,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..f1b07d946 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/node-endoify'), + ), ], testTimeout: 30_000, }, diff --git a/packages/nodejs-test-workers/package.json b/packages/nodejs-test-workers/package.json index 5f6c0d67d..1a9a60309 100644 --- a/packages/nodejs-test-workers/package.json +++ b/packages/nodejs-test-workers/package.json @@ -81,6 +81,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..d2ac3dc74 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/node-endoify'; 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 a64b6713c..a159dae8f 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 6e9685b06..000000000 --- a/packages/nodejs/src/env/endoify.ts +++ /dev/null @@ -1,2 +0,0 @@ -// Re-export the shared Node.js endoify from kernel-shims -import '@metamask/kernel-shims/node-endoify'; diff --git a/packages/nodejs/src/kernel/PlatformServices.test.ts b/packages/nodejs/src/kernel/PlatformServices.test.ts index 56bdecf93..2b3b98448 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..8a751c5d1 100644 --- a/packages/nodejs/src/vat/vat-worker.ts +++ b/packages/nodejs/src/vat/vat-worker.ts @@ -1,5 +1,3 @@ -import '../env/endoify.ts'; - 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 971eb6f00..545cdea9e 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..cef62f828 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/node-endoify'; 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 3d803d822..cfa20a259 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/node-endoify'), + ), + ], include: ['./test/e2e/**/*.test.ts'], exclude: ['./src/**/*'], hookTimeout: 30_000, // Increase hook timeout for network cleanup diff --git a/packages/nodejs/vitest.config.ts b/packages/nodejs/vitest.config.ts index 0b8767bab..1ed4405ce 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/node-endoify'), + ), + ], 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..723518f55 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/node-endoify'), + ), ], }, }), diff --git a/yarn.lock b/yarn.lock index 1f20a5f95..683276979 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3736,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:^" @@ -3837,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:^" From 9451aae4fb85684555671489888268bb142854e4 Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Tue, 13 Jan 2026 15:20:31 -0800 Subject: [PATCH 05/14] fix(kernel-shims): Use relative import in node-endoify.js --- packages/kernel-shims/src/node-endoify.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/kernel-shims/src/node-endoify.js b/packages/kernel-shims/src/node-endoify.js index 286912619..5707cbf49 100644 --- a/packages/kernel-shims/src/node-endoify.js +++ b/packages/kernel-shims/src/node-endoify.js @@ -3,8 +3,7 @@ // Node.js-specific endoify that imports modules which modify globals before lockdown. // This file is NOT bundled - it must be imported directly from src/. -// eslint-disable-next-line import-x/no-unresolved -- self-import resolved at runtime -import '@metamask/kernel-shims/endoify-repair'; +import './endoify-repair.js'; // @libp2p/webrtc needs to modify globals in Node.js only, so we need to import // it before hardening. From a44a765bfffe005b217b1d2522690617a8f62892 Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Tue, 13 Jan 2026 16:47:25 -0800 Subject: [PATCH 06/14] refactor(kernel-shims): Rename node-endoify to endoify-node and update configs - Fix accidentally broken nodejs vat worker (which broke all tests relying on it) - Rename node-endoify.js to endoify-node.js for consistency - Update package.json export from ./node-endoify to ./endoify-node - Update all vitest configs to use the new export path - Update depcheckrc.yml ignore pattern Co-Authored-By: Claude Opus 4.5 --- .depcheckrc.yml | 2 +- packages/kernel-browser-runtime/vitest.integration.config.ts | 5 +++++ packages/kernel-shims/package.json | 2 +- .../kernel-shims/src/{node-endoify.js => endoify-node.js} | 0 packages/kernel-test/vitest.config.ts | 2 +- packages/nodejs-test-workers/src/workers/mock-fetch.ts | 2 +- packages/nodejs/src/vat/vat-worker.ts | 2 ++ packages/nodejs/test/workers/stream-sync.js | 2 +- packages/nodejs/vitest.config.e2e.ts | 2 +- packages/nodejs/vitest.config.ts | 2 +- packages/ocap-kernel/vitest.config.ts | 2 +- 11 files changed, 15 insertions(+), 8 deletions(-) rename packages/kernel-shims/src/{node-endoify.js => endoify-node.js} (100%) diff --git a/.depcheckrc.yml b/.depcheckrc.yml index c131a1352..08e7fb5e3 100644 --- a/.depcheckrc.yml +++ b/.depcheckrc.yml @@ -50,7 +50,7 @@ ignores: # Used by @ocap/nodejs to build the sqlite3 bindings - 'node-gyp' - # Used by @metamask/kernel-shims/node-endoify for tests + # Used by @metamask/kernel-shims/endoify-node for tests - '@libp2p/webrtc' # These are peer dependencies of various modules we actually do 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 6867749e4..b83e446b6 100644 --- a/packages/kernel-shims/package.json +++ b/packages/kernel-shims/package.json @@ -22,7 +22,7 @@ "./endoify": "./dist/endoify.js", "./endoify-repair": "./dist/endoify-repair.js", "./eventual-send": "./dist/eventual-send.js", - "./node-endoify": "./src/node-endoify.js", + "./endoify-node": "./src/endoify-node.js", "./package.json": "./package.json" }, "main": "./dist/endoify.js", diff --git a/packages/kernel-shims/src/node-endoify.js b/packages/kernel-shims/src/endoify-node.js similarity index 100% rename from packages/kernel-shims/src/node-endoify.js rename to packages/kernel-shims/src/endoify-node.js diff --git a/packages/kernel-test/vitest.config.ts b/packages/kernel-test/vitest.config.ts index f1b07d946..964287570 100644 --- a/packages/kernel-test/vitest.config.ts +++ b/packages/kernel-test/vitest.config.ts @@ -13,7 +13,7 @@ export default defineConfig((args) => { name: 'kernel-test', setupFiles: [ fileURLToPath( - import.meta.resolve('@metamask/kernel-shims/node-endoify'), + import.meta.resolve('@metamask/kernel-shims/endoify-node'), ), ], testTimeout: 30_000, diff --git a/packages/nodejs-test-workers/src/workers/mock-fetch.ts b/packages/nodejs-test-workers/src/workers/mock-fetch.ts index d2ac3dc74..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 '@metamask/kernel-shims/node-endoify'; +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/src/vat/vat-worker.ts b/packages/nodejs/src/vat/vat-worker.ts index 8a751c5d1..c08d2f17d 100644 --- a/packages/nodejs/src/vat/vat-worker.ts +++ b/packages/nodejs/src/vat/vat-worker.ts @@ -1,3 +1,5 @@ +import '@metamask/kernel-shims/endoify-node'; + import { Logger } from '@metamask/logger'; import type { VatId } from '@metamask/ocap-kernel'; diff --git a/packages/nodejs/test/workers/stream-sync.js b/packages/nodejs/test/workers/stream-sync.js index cef62f828..0889812ea 100644 --- a/packages/nodejs/test/workers/stream-sync.js +++ b/packages/nodejs/test/workers/stream-sync.js @@ -1,4 +1,4 @@ -import '@metamask/kernel-shims/node-endoify'; +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 cfa20a259..922508bce 100644 --- a/packages/nodejs/vitest.config.e2e.ts +++ b/packages/nodejs/vitest.config.e2e.ts @@ -14,7 +14,7 @@ export default defineConfig((args) => { pool: 'forks', setupFiles: [ fileURLToPath( - import.meta.resolve('@metamask/kernel-shims/node-endoify'), + import.meta.resolve('@metamask/kernel-shims/endoify-node'), ), ], include: ['./test/e2e/**/*.test.ts'], diff --git a/packages/nodejs/vitest.config.ts b/packages/nodejs/vitest.config.ts index 1ed4405ce..208d6346b 100644 --- a/packages/nodejs/vitest.config.ts +++ b/packages/nodejs/vitest.config.ts @@ -13,7 +13,7 @@ export default defineConfig((args) => { name: 'nodejs', setupFiles: [ fileURLToPath( - import.meta.resolve('@metamask/kernel-shims/node-endoify'), + import.meta.resolve('@metamask/kernel-shims/endoify-node'), ), ], include: ['./src/**/*.test.ts'], diff --git a/packages/ocap-kernel/vitest.config.ts b/packages/ocap-kernel/vitest.config.ts index 723518f55..6264a93d4 100644 --- a/packages/ocap-kernel/vitest.config.ts +++ b/packages/ocap-kernel/vitest.config.ts @@ -13,7 +13,7 @@ export default defineConfig((args) => { name: 'kernel', setupFiles: [ fileURLToPath( - import.meta.resolve('@metamask/kernel-shims/node-endoify'), + import.meta.resolve('@metamask/kernel-shims/endoify-node'), ), ], }, From 2e90b61612cc5699c95eeaa4fc41a426fb17399f Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Tue, 13 Jan 2026 20:38:43 -0800 Subject: [PATCH 07/14] fix: Build in CI before integration tests --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3e8bd9eee..4b198d7c1 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", From b836aa5473c1a37b5e78ef0a8df7dd7eac1c7e1e Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Tue, 13 Jan 2026 22:39:36 -0800 Subject: [PATCH 08/14] feat(extension): Add CapTP E() support for calling vat methods - Import and initialize makeBackgroundKref to enable E() calls on vat objects - Expose captp.resolveKref and captp.krefOf on globalThis for console access - Refactor startDefaultSubcluster to return the bootstrap vat rootKref - Add greetBootstrapVat function that automatically calls hello() on the bootstrap vat after subcluster launch on startup - Update global.d.ts with captp type declaration for IDE support Co-Authored-By: Claude --- packages/extension/src/background.ts | 39 +++++++++++++++++++++++++--- packages/extension/src/global.d.ts | 17 +++++++++++- 2 files changed, 51 insertions(+), 5 deletions(-) diff --git a/packages/extension/src/background.ts b/packages/extension/src/background.ts index b1a11267c..af421d409 100644 --- a/packages/extension/src/background.ts +++ b/packages/extension/src/background.ts @@ -1,6 +1,7 @@ import { E } from '@endo/eventual-send'; import { makeBackgroundCapTP, + makeBackgroundKref, makeCapTPNotification, isCapTPNotification, getCapTPMessage, @@ -107,6 +108,10 @@ async function main(): Promise { const kernelP = backgroundCapTP.getKernel(); globalThis.kernel = kernelP; + // Create background kref system for E() calls on vat objects + const bgKref = makeBackgroundKref({ kernelFacade: kernelP }); + Object.assign(globalThis.captp, bgKref); + // Handle incoming CapTP messages from the kernel const drainPromise = offscreenStream.drain((message) => { if (isCapTPNotification(message)) { @@ -120,7 +125,10 @@ async function main(): Promise { try { await E(kernelP).ping(); - await startDefaultSubcluster(); + const rootKref = await startDefaultSubcluster(); + if (rootKref) { + await greetBootstrapVat(rootKref); + } } catch (error) { offscreenStream.throw(error as Error).catch(logger.error); } @@ -138,8 +146,10 @@ async function main(): Promise { /** * Idempotently starts the default subcluster. + * + * @returns The rootKref of the bootstrap vat if launched, undefined if subcluster already exists. */ -async function startDefaultSubcluster(): Promise { +async function startDefaultSubcluster(): Promise { const status = await E(globalThis.kernel).getStatus(); if (status.subclusters.length === 0) { @@ -147,9 +157,23 @@ async function startDefaultSubcluster(): Promise { 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}`); } /** @@ -163,6 +187,13 @@ function defineGlobals(): void { value: undefined, }); + Object.defineProperty(globalThis, 'captp', { + configurable: false, + enumerable: true, + writable: false, + value: {}, + }); + Object.defineProperty(globalThis, 'E', { value: E, configurable: false, diff --git a/packages/extension/src/global.d.ts b/packages/extension/src/global.d.ts index f63d2a3a6..11d30f5a9 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 { + BackgroundKref, + KernelFacade, +} from '@metamask/kernel-browser-runtime'; // Type declarations for kernel dev console API. declare global { @@ -17,6 +20,18 @@ declare global { // eslint-disable-next-line no-var var kernel: KernelFacade | 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: BackgroundKref; } export {}; From cb7e613b072c9e0da18310118993bf328ef8f975 Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Wed, 14 Jan 2026 02:03:31 -0800 Subject: [PATCH 09/14] refactor: Rename background-kref to kref-presence for clarity - Rename background-kref.ts to kref-presence.ts - Rename makeBackgroundKref to makePresenceManager - Rename BackgroundKref type to PresenceManager - Rename BackgroundKrefOptions to PresenceManagerOptions - Update all imports and references across affected packages - Update JSDoc comments to reflect new naming - All tests pass for kernel-browser-runtime, extension, omnium-gatherum Co-Authored-By: Claude --- packages/extension/src/background.ts | 8 ++-- packages/extension/src/global.d.ts | 4 +- .../kernel-browser-runtime/src/index.test.ts | 2 +- packages/kernel-browser-runtime/src/index.ts | 8 ++-- .../{background-kref.ts => kref-presence.ts} | 37 +++++++++---------- packages/omnium-gatherum/src/background.ts | 22 +++++------ 6 files changed, 40 insertions(+), 41 deletions(-) rename packages/kernel-browser-runtime/src/{background-kref.ts => kref-presence.ts} (88%) diff --git a/packages/extension/src/background.ts b/packages/extension/src/background.ts index af421d409..0369dc166 100644 --- a/packages/extension/src/background.ts +++ b/packages/extension/src/background.ts @@ -1,7 +1,7 @@ import { E } from '@endo/eventual-send'; import { makeBackgroundCapTP, - makeBackgroundKref, + makePresenceManager, makeCapTPNotification, isCapTPNotification, getCapTPMessage, @@ -108,9 +108,9 @@ async function main(): Promise { const kernelP = backgroundCapTP.getKernel(); globalThis.kernel = kernelP; - // Create background kref system for E() calls on vat objects - const bgKref = makeBackgroundKref({ kernelFacade: kernelP }); - Object.assign(globalThis.captp, bgKref); + // 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) => { diff --git a/packages/extension/src/global.d.ts b/packages/extension/src/global.d.ts index 11d30f5a9..c67f8b339 100644 --- a/packages/extension/src/global.d.ts +++ b/packages/extension/src/global.d.ts @@ -1,5 +1,5 @@ import type { - BackgroundKref, + PresenceManager, KernelFacade, } from '@metamask/kernel-browser-runtime'; @@ -31,7 +31,7 @@ declare global { * ``` */ // eslint-disable-next-line no-var - var captp: BackgroundKref; + var captp: PresenceManager; } export {}; diff --git a/packages/kernel-browser-runtime/src/index.test.ts b/packages/kernel-browser-runtime/src/index.test.ts index 1dc0b7056..dd96eaf49 100644 --- a/packages/kernel-browser-runtime/src/index.test.ts +++ b/packages/kernel-browser-runtime/src/index.test.ts @@ -13,9 +13,9 @@ describe('index', () => { 'getRelaysFromCurrentLocation', 'isCapTPNotification', 'makeBackgroundCapTP', - 'makeBackgroundKref', '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 325fdb48e..79fb7036a 100644 --- a/packages/kernel-browser-runtime/src/index.ts +++ b/packages/kernel-browser-runtime/src/index.ts @@ -22,7 +22,7 @@ export { type CapTPMessage, } from './background-captp.ts'; export { - makeBackgroundKref, - type BackgroundKref, - type BackgroundKrefOptions, -} from './background-kref.ts'; + makePresenceManager, + type PresenceManager, + type PresenceManagerOptions, +} from './kref-presence.ts'; diff --git a/packages/kernel-browser-runtime/src/background-kref.ts b/packages/kernel-browser-runtime/src/kref-presence.ts similarity index 88% rename from packages/kernel-browser-runtime/src/background-kref.ts rename to packages/kernel-browser-runtime/src/kref-presence.ts index 6d36e4c69..1bd1779f8 100644 --- a/packages/kernel-browser-runtime/src/background-kref.ts +++ b/packages/kernel-browser-runtime/src/kref-presence.ts @@ -1,5 +1,5 @@ /** - * Background kref system for creating E()-usable presences from kernel krefs. + * 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 @@ -24,9 +24,9 @@ type SendToKernelFn = ( ) => Promise; /** - * Options for creating a background kref system. + * Options for creating a presence manager. */ -export type BackgroundKrefOptions = { +export type PresenceManagerOptions = { /** * The kernel facade remote presence from CapTP. * Can be a promise since E() works with promises. @@ -35,9 +35,9 @@ export type BackgroundKrefOptions = { }; /** - * The background kref system interface. + * The presence manager interface. */ -export type BackgroundKref = { +export type PresenceManager = { /** * Resolve a kref string to an E()-usable presence. * @@ -137,26 +137,26 @@ function makeKrefPresence( } /** - * Create a background kref system for E() on vat objects. + * 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 background kref system. + * @returns The presence manager. */ -export function makeBackgroundKref( - options: BackgroundKrefOptions, -): BackgroundKref { +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 (needs bgMarshal) - // eslint-disable-next-line @typescript-eslint/no-explicit-any, prefer-const - let bgMarshal: any; + // Forward declaration for sendToKernel + // eslint-disable-next-line prefer-const + let marshal: ReturnType>; /** * Send a message to the kernel and deserialize the result. @@ -190,7 +190,7 @@ export function makeBackgroundKref( ); // Deserialize result (krefs become presences) - return bgMarshal.fromCapData(result); + return marshal.fromCapData(result); }; /** @@ -232,9 +232,8 @@ export function makeBackgroundKref( throw new Error('Cannot serialize unknown remotable object'); }; - // Create marshal with smallcaps format (same as kernel) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - bgMarshal = makeMarshal(convertValToSlot, convertSlotToVal as any, { + // Same options as kernel-marshal.ts + marshal = makeMarshal(convertValToSlot, convertSlotToVal, { serializeBodyFormat: 'smallcaps', errorTagging: 'off', }); @@ -249,8 +248,8 @@ export function makeBackgroundKref( }, fromCapData: (data: CapData): unknown => { - return bgMarshal.fromCapData(data); + return marshal.fromCapData(data); }, }); } -harden(makeBackgroundKref); +harden(makePresenceManager); diff --git a/packages/omnium-gatherum/src/background.ts b/packages/omnium-gatherum/src/background.ts index 736a32245..bce567d2e 100644 --- a/packages/omnium-gatherum/src/background.ts +++ b/packages/omnium-gatherum/src/background.ts @@ -1,12 +1,12 @@ import { E } from '@endo/eventual-send'; import { makeBackgroundCapTP, - makeBackgroundKref, + makePresenceManager, makeCapTPNotification, isCapTPNotification, getCapTPMessage, } from '@metamask/kernel-browser-runtime'; -import type { BackgroundKref, CapTPMessage } from '@metamask/kernel-browser-runtime'; +import type { CapTPMessage, PresenceManager } from '@metamask/kernel-browser-runtime'; import { delay, isJsonRpcMessage, stringify } from '@metamask/kernel-utils'; import type { JsonRpcMessage } from '@metamask/kernel-utils'; import { Logger } from '@metamask/logger'; @@ -109,9 +109,9 @@ async function main(): Promise { const kernelP = backgroundCapTP.getKernel(); globalThis.kernel = kernelP; - // Create background kref system for E() on vat objects - const bgKref = makeBackgroundKref({ kernelFacade: kernelP }); - globals.setBgKref(bgKref); + // Create presence manager for E() on vat objects + const presenceManager = makePresenceManager({ kernelFacade: kernelP }); + globals.setPresenceManager(presenceManager); try { const controllers = await initializeControllers({ @@ -143,7 +143,7 @@ async function main(): Promise { type GlobalSetters = { setCapletController: (value: CapletControllerFacet) => void; - setBgKref: (value: BackgroundKref) => void; + setPresenceManager: (value: PresenceManager) => void; }; /** @@ -153,7 +153,7 @@ type GlobalSetters = { */ function defineGlobals(): GlobalSetters { let capletController: CapletControllerFacet; - let bgKref: BackgroundKref; + let presenceManager: PresenceManager; Object.defineProperty(globalThis, 'E', { configurable: false, @@ -232,10 +232,10 @@ function defineGlobals(): GlobalSetters { }), }, resolveKref: { - get: () => bgKref.resolveKref, + get: () => presenceManager.resolveKref, }, krefOf: { - get: () => bgKref.krefOf, + get: () => presenceManager.krefOf, }, }); harden(globalThis.omnium); @@ -244,8 +244,8 @@ function defineGlobals(): GlobalSetters { setCapletController: (value) => { capletController = value; }, - setBgKref: (value) => { - bgKref = value; + setPresenceManager: (value) => { + presenceManager = value; }, }; } From f195073f24e621d639c7a6a9959062a18066124a Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Wed, 14 Jan 2026 02:40:58 -0800 Subject: [PATCH 10/14] test(kernel-browser-runtime): Add unit tests for kref-presence and convertKrefsToStandins - Move convertKrefsToStandins from kernel-facade.ts to kref-presence.ts for better organization - Export convertKrefsToStandins for use by kernel-facade - Add comprehensive unit tests for convertKrefsToStandins (20 tests covering kref conversion, arrays, objects, primitives) - Add unit tests for makePresenceManager (3 tests for kref resolution and memoization) - Add integration test in kernel-facade.test.ts verifying kref conversion in queueMessage Co-Authored-By: Claude --- .../kernel-worker/captp/kernel-facade.test.ts | 31 ++ .../src/kernel-worker/captp/kernel-facade.ts | 32 +- .../src/kref-presence.test.ts | 296 ++++++++++++++++++ .../src/kref-presence.ts | 32 ++ 4 files changed, 360 insertions(+), 31 deletions(-) create mode 100644 packages/kernel-browser-runtime/src/kref-presence.test.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 6282cbce9..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,41 +1,11 @@ import { makeDefaultExo } from '@metamask/kernel-utils/exo'; import type { Kernel, ClusterConfig, KRef, VatId } from '@metamask/ocap-kernel'; -import { kslot } from '@metamask/ocap-kernel'; +import { convertKrefsToStandins } from '../../kref-presence.ts'; import type { KernelFacade, LaunchResult } from '../../types.ts'; export type { KernelFacade } from '../../types.ts'; -/** - * 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. - */ -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; -} - /** * Create the kernel facade exo that exposes kernel methods via CapTP. * 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 index 1bd1779f8..2fe10f332 100644 --- a/packages/kernel-browser-runtime/src/kref-presence.ts +++ b/packages/kernel-browser-runtime/src/kref-presence.ts @@ -11,6 +11,7 @@ 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'; @@ -23,6 +24,37 @@ type SendToKernelFn = ( 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. */ From 5172b941fc19de421034c1c514ea8d289124e661 Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Thu, 15 Jan 2026 14:06:47 -0800 Subject: [PATCH 11/14] refactor: Post-rebase fixup --- packages/omnium-gatherum/README.md | 2 +- packages/omnium-gatherum/src/background.ts | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/omnium-gatherum/README.md b/packages/omnium-gatherum/README.md index 1f52025d6..cfb41d330 100644 --- a/packages/omnium-gatherum/README.md +++ b/packages/omnium-gatherum/README.md @@ -18,7 +18,7 @@ After loading the extension, open the background console (chrome://extensions ```javascript // 1. Load the echo caplet manifest and bundle -const { manifest, bundle } = await omnium.loadCaplet('echo'); +const { manifest, bundle } = await omnium.caplet.load('echo'); // 2. Install the caplet const installResult = await omnium.caplet.install(manifest, bundle); diff --git a/packages/omnium-gatherum/src/background.ts b/packages/omnium-gatherum/src/background.ts index bce567d2e..d1bb3b118 100644 --- a/packages/omnium-gatherum/src/background.ts +++ b/packages/omnium-gatherum/src/background.ts @@ -6,7 +6,10 @@ import { isCapTPNotification, getCapTPMessage, } from '@metamask/kernel-browser-runtime'; -import type { CapTPMessage, PresenceManager } from '@metamask/kernel-browser-runtime'; +import type { + CapTPMessage, + PresenceManager, +} from '@metamask/kernel-browser-runtime'; import { delay, isJsonRpcMessage, stringify } from '@metamask/kernel-utils'; import type { JsonRpcMessage } from '@metamask/kernel-utils'; import { Logger } from '@metamask/logger'; From f229707376c847c35b713d8b423001a0f978995b Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Sun, 18 Jan 2026 19:57:00 -0800 Subject: [PATCH 12/14] test(extension): Fix object-registry e2e test --- packages/extension/test/e2e/object-registry.test.ts | 4 ++-- .../src/kernel-worker/captp/kernel-facade.test.ts | 8 +++----- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/packages/extension/test/e2e/object-registry.test.ts b/packages/extension/test/e2e/object-registry.test.ts index f54038a4a..b4fc02bed 100644 --- a/packages/extension/test/e2e/object-registry.test.ts +++ b/packages/extension/test/e2e/object-registry.test.ts @@ -73,7 +73,7 @@ test.describe('Object Registry', () => { await clearLogsButton.click(); await popupPage.click('button:text("Object Registry")'); await expect( - popupPage.locator('text=Alice (v1) - 5 objects, 4 promises'), + popupPage.locator('text=Alice (v1) - 5 objects, 5 promises'), ).toBeVisible(); const targetSelect = popupPage.locator('[data-testid="message-target"]'); await expect(targetSelect).toBeVisible(); @@ -102,7 +102,7 @@ test.describe('Object Registry', () => { await expect(messageResponse).toContainText('"body":"#\\"vat Alice got'); await expect(messageResponse).toContainText('"slots":['); await expect( - popupPage.locator('text=Alice (v1) - 5 objects, 6 promises'), + popupPage.locator('text=Alice (v1) - 5 objects, 7 promises'), ).toBeVisible(); }); 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 7799a9bf5..bcdfcfadc 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 @@ -61,13 +61,11 @@ describe('makeKernelFacade', () => { }); it('returns result with subclusterId and rootKref from kernel', async () => { - const kernelResult = { + vi.mocked(mockKernel.launchSubcluster).mockResolvedValueOnce({ subclusterId: 's1', bootstrapRootKref: 'ko1', - }; - vi.mocked(mockKernel.launchSubcluster).mockResolvedValueOnce( - kernelResult, - ); + bootstrapResult: undefined, + }); const config: ClusterConfig = makeClusterConfig(); From cbae7f9a428755dbd24f2e04d71c1a5c19a1ee8b Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Tue, 20 Jan 2026 15:06:06 -0800 Subject: [PATCH 13/14] test(extension): Fix persistence e2e test --- packages/extension/test/e2e/persistence.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/extension/test/e2e/persistence.test.ts b/packages/extension/test/e2e/persistence.test.ts index 916f65324..74ce4cc87 100644 --- a/packages/extension/test/e2e/persistence.test.ts +++ b/packages/extension/test/e2e/persistence.test.ts @@ -45,6 +45,8 @@ test.describe('Kernel Persistence', () => { await expect( newPopupPage.locator('text=Subcluster s2 - 1 Vat'), ).toBeVisible(); + // Wait for database to fully persist before reloading + await newPopupPage.waitForTimeout(1000); // reload the extension await newPopupPage.evaluate(() => chrome.runtime.reload()); await newPopupPage.close(); From b95fd28cd2323e2927c3809ae6a463d85b295aaa Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Tue, 20 Jan 2026 17:37:30 -0800 Subject: [PATCH 14/14] chore: Remove unused dependency --- packages/kernel-browser-runtime/package.json | 1 - yarn.lock | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/kernel-browser-runtime/package.json b/packages/kernel-browser-runtime/package.json index f25af314a..e5d179a96 100644 --- a/packages/kernel-browser-runtime/package.json +++ b/packages/kernel-browser-runtime/package.json @@ -91,7 +91,6 @@ "@metamask/eslint-config": "^15.0.0", "@metamask/eslint-config-nodejs": "^15.0.0", "@metamask/eslint-config-typescript": "^15.0.0", - "@ocap/nodejs": "workspace:^", "@ocap/repo-tools": "workspace:^", "@ts-bridge/cli": "^0.6.3", "@ts-bridge/shims": "^0.1.1", diff --git a/yarn.lock b/yarn.lock index 683276979..ea76fccc2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2303,7 +2303,6 @@ __metadata: "@metamask/superstruct": "npm:^3.2.1" "@metamask/utils": "npm:^11.9.0" "@ocap/kernel-platforms": "workspace:^" - "@ocap/nodejs": "workspace:^" "@ocap/repo-tools": "workspace:^" "@ts-bridge/cli": "npm:^0.6.3" "@ts-bridge/shims": "npm:^0.1.1"