From bd475a51e96b8cfead9e9f54585e3d86425ba335 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/11] 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 | 43 +++++++--------------- 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, 30 insertions(+), 94 deletions(-) diff --git a/packages/extension/src/background.ts b/packages/extension/src/background.ts index 967103779..628221265 100644 --- a/packages/extension/src/background.ts +++ b/packages/extension/src/background.ts @@ -20,12 +20,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 +107,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) => { @@ -126,8 +121,8 @@ async function main(): Promise { }); drainPromise.catch(logger.error); - await ping(); // Wait for the kernel to be ready - await startDefaultSubcluster(kernelP); + await E(kernelP).ping(); + await startDefaultSubcluster(); try { await drainPromise; @@ -142,16 +137,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.'); @@ -165,19 +158,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 942bb4054..2802fc6b7 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'; @@ -32,7 +29,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 +111,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; // Create storage adapter const storageAdapter = makeChromeStorageAdapter(); @@ -168,8 +161,6 @@ async function main(): Promise { } type GlobalSetters = { - setKernelP: (value: Promise) => void; - setPing: (value: () => Promise) => void; setCapletController: (value: CapletControllerFacet) => void; }; @@ -179,6 +170,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, @@ -186,6 +179,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 +193,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 +233,6 @@ function defineGlobals(): GlobalSetters { }; Object.defineProperties(globalThis.omnium, { - ping: { - get: () => ping, - }, - getKernel: { - value: async () => kernelP, - }, caplet: { value: harden({ install: async (manifest: CapletManifest, bundle?: unknown) => @@ -262,12 +252,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 6144f482e2cf3f535fef76f3db1d6a0f209df0ba 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/11] 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 97e804803..61c46ef7e 100644 --- a/package.json +++ b/package.json @@ -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/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/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 eff3f6ba9..52330fb1e 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", @@ -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/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 2802fc6b7..5d3bf1098 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'; @@ -113,6 +114,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); + // Create storage adapter const storageAdapter = makeChromeStorageAdapter(); @@ -162,6 +167,7 @@ async function main(): Promise { type GlobalSetters = { setCapletController: (value: CapletControllerFacet) => void; + setBgKref: (value: BackgroundKref) => void; }; /** @@ -171,6 +177,7 @@ type GlobalSetters = { */ function defineGlobals(): GlobalSetters { let capletController: CapletControllerFacet; + let bgKref: BackgroundKref; Object.defineProperty(globalThis, 'E', { configurable: false, @@ -248,6 +255,12 @@ function defineGlobals(): GlobalSetters { E(capletController).getCapletRoot(capletId), }), }, + resolveKref: { + get: () => bgKref.resolveKref, + }, + krefOf: { + get: () => bgKref.krefOf, + }, }); harden(globalThis.omnium); @@ -255,5 +268,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 a41a32194..ff8f32d56 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 38a9014d1a99e62f22dd18ff6a42b1a9e97201dc 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/11] 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 f690022ec02f897edbc6ac2e0ebf4ccea25beb4b 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/11] 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 --- 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 ++ 19 files changed, 24 insertions(+), 25 deletions(-) delete mode 100644 packages/nodejs/src/env/endoify.ts 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..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 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..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 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 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 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..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 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..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 cd509ee6b..8fc8f9a3b 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/**/*'], env: { 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 ff8f32d56..f45b09313 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 6deaae249bbb0057d251ad47a3b527211a043253 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/11] 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 2098758327db55360084d65fb7a7d1d356ed5da6 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/11] 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 52330fb1e..8ac7a013d 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 8fc8f9a3b..b72ebb5cb 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 e7c6d82422606a2b4633a352d7d124e43bf155ba 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/11] 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 61c46ef7e..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", From f38f087df1e65bcfff7d105344ae3a302ceb74a5 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/11] 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 628221265..556f75e12 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, @@ -110,6 +111,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)) { @@ -122,7 +127,10 @@ async function main(): Promise { drainPromise.catch(logger.error); await E(kernelP).ping(); - await startDefaultSubcluster(); + const rootKref = await startDefaultSubcluster(); + if (rootKref) { + await greetBootstrapVat(rootKref); + } try { await drainPromise; @@ -137,8 +145,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) { @@ -146,9 +156,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}`); } /** @@ -162,6 +186,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 88e8d1dfcc7ce193e00144b17ff4dd4a2374cab4 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/11] 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 556f75e12..d8a06e60b 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, @@ -111,9 +111,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 5d3bf1098..7aab074e5 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'; @@ -114,9 +114,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); // Create storage adapter const storageAdapter = makeChromeStorageAdapter(); @@ -167,7 +167,7 @@ async function main(): Promise { type GlobalSetters = { setCapletController: (value: CapletControllerFacet) => void; - setBgKref: (value: BackgroundKref) => void; + setPresenceManager: (value: PresenceManager) => void; }; /** @@ -177,7 +177,7 @@ type GlobalSetters = { */ function defineGlobals(): GlobalSetters { let capletController: CapletControllerFacet; - let bgKref: BackgroundKref; + let presenceManager: PresenceManager; Object.defineProperty(globalThis, 'E', { configurable: false, @@ -256,10 +256,10 @@ function defineGlobals(): GlobalSetters { }), }, resolveKref: { - get: () => bgKref.resolveKref, + get: () => presenceManager.resolveKref, }, krefOf: { - get: () => bgKref.krefOf, + get: () => presenceManager.krefOf, }, }); harden(globalThis.omnium); @@ -268,8 +268,8 @@ function defineGlobals(): GlobalSetters { setCapletController: (value) => { capletController = value; }, - setBgKref: (value) => { - bgKref = value; + setPresenceManager: (value) => { + presenceManager = value; }, }; } From 5bb360f9cb15ffbb937ea15fa89f1fd640c85ac3 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/11] 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 c7dd594b616f1f0112e2101b362d8306285123f6 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/11] refactor: Post-rebase fixup --- packages/extension/src/background.ts | 5 +---- packages/omnium-gatherum/README.md | 2 +- packages/omnium-gatherum/src/background.ts | 5 ++++- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/extension/src/background.ts b/packages/extension/src/background.ts index d8a06e60b..5e302c12a 100644 --- a/packages/extension/src/background.ts +++ b/packages/extension/src/background.ts @@ -6,10 +6,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'; 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 7aab074e5..3862917e9 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';