From 773f8d975091d2f2e21cde6a9e2c83d4626aca48 Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Mon, 5 Jan 2026 22:07:00 -0800 Subject: [PATCH 01/17] feat(omnium): Add dev console object to background --- packages/omnium-gatherum/src/background.ts | 8 ++++++++ .../src/env/background-trusted-prelude.js | 3 +++ .../omnium-gatherum/src/env/dev-console.js | 9 +++++++++ .../src/env/dev-console.test.ts | 20 +++++++++++++++++++ packages/omnium-gatherum/src/global.d.ts | 9 +++++++++ packages/omnium-gatherum/vite.config.ts | 9 ++++++--- 6 files changed, 55 insertions(+), 3 deletions(-) create mode 100644 packages/omnium-gatherum/src/env/background-trusted-prelude.js create mode 100644 packages/omnium-gatherum/src/env/dev-console.js create mode 100644 packages/omnium-gatherum/src/env/dev-console.test.ts create mode 100644 packages/omnium-gatherum/src/global.d.ts diff --git a/packages/omnium-gatherum/src/background.ts b/packages/omnium-gatherum/src/background.ts index 7b2b07ba4..19c3ddedc 100644 --- a/packages/omnium-gatherum/src/background.ts +++ b/packages/omnium-gatherum/src/background.ts @@ -92,6 +92,14 @@ async function main(): Promise { logger.info(result); }; + // globalThis.omnium will exist due to dev-console.js in background-trusted-prelude.js + Object.defineProperties(globalThis.omnium, { + ping: { + value: ping, + }, + }); + harden(globalThis.omnium); + // With this we can click the extension action button to wake up the service worker. chrome.action.onClicked.addListener(() => { ping().catch(logger.error); diff --git a/packages/omnium-gatherum/src/env/background-trusted-prelude.js b/packages/omnium-gatherum/src/env/background-trusted-prelude.js new file mode 100644 index 000000000..d026032b6 --- /dev/null +++ b/packages/omnium-gatherum/src/env/background-trusted-prelude.js @@ -0,0 +1,3 @@ +// eslint-disable-next-line import-x/no-unresolved +import './endoify.js'; +import './dev-console.js'; diff --git a/packages/omnium-gatherum/src/env/dev-console.js b/packages/omnium-gatherum/src/env/dev-console.js new file mode 100644 index 000000000..7c5d06d5e --- /dev/null +++ b/packages/omnium-gatherum/src/env/dev-console.js @@ -0,0 +1,9 @@ +// We set this property on globalThis in the background before lockdown. +Object.defineProperty(globalThis, 'omnium', { + configurable: false, + enumerable: true, + writable: false, + value: {}, +}); + +export {}; diff --git a/packages/omnium-gatherum/src/env/dev-console.test.ts b/packages/omnium-gatherum/src/env/dev-console.test.ts new file mode 100644 index 000000000..0e7ad3f15 --- /dev/null +++ b/packages/omnium-gatherum/src/env/dev-console.test.ts @@ -0,0 +1,20 @@ +import { describe, it, expect } from 'vitest'; +import './dev-console.js'; + +describe('dev-console', () => { + describe('omnium', () => { + it('is available on globalThis', async () => { + expect(omnium).toBeDefined(); + }); + + it('has expected property descriptors', async () => { + expect( + Object.getOwnPropertyDescriptor(globalThis, 'omnium'), + ).toMatchObject({ + configurable: false, + enumerable: true, + writable: false, + }); + }); + }); +}); diff --git a/packages/omnium-gatherum/src/global.d.ts b/packages/omnium-gatherum/src/global.d.ts new file mode 100644 index 000000000..25566171c --- /dev/null +++ b/packages/omnium-gatherum/src/global.d.ts @@ -0,0 +1,9 @@ +// Type declarations for omnium dev console API. +declare global { + // eslint-disable-next-line no-var + var omnium: { + ping: () => Promise; + }; +} + +export {}; diff --git a/packages/omnium-gatherum/vite.config.ts b/packages/omnium-gatherum/vite.config.ts index 1caf51ceb..c1a8f2a2d 100644 --- a/packages/omnium-gatherum/vite.config.ts +++ b/packages/omnium-gatherum/vite.config.ts @@ -37,16 +37,19 @@ const staticCopyTargets: readonly (string | Target)[] = [ // The extension manifest 'packages/omnium-gatherum/src/manifest.json', // Trusted prelude-related + 'packages/omnium-gatherum/src/env/dev-console.js', + 'packages/omnium-gatherum/src/env/background-trusted-prelude.js', 'packages/kernel-shims/dist/endoify.js', ]; +const backgroundPreludeImportStatement = `import './background-trusted-prelude.js';`; const endoifyImportStatement = `import './endoify.js';`; -const trustedPreludes: PreludeRecord = { +const trustedPreludes = { background: { - content: endoifyImportStatement, + content: backgroundPreludeImportStatement, }, 'kernel-worker': { content: endoifyImportStatement }, -}; +} satisfies PreludeRecord; // https://vitejs.dev/config/ export default defineConfig(({ mode }) => { From aa9593a19fa49d20fed870c000e79708e216574b Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Tue, 6 Jan 2026 14:59:47 -0800 Subject: [PATCH 02/17] feat(omnium): Add CapTP-based E() infrastructure for kernel communication MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements userspace E() infrastructure using @endo/captp to enable the background script to use E() naturally with kernel objects. Key changes: - Add CapTP setup on kernel side (kernel-browser-runtime): - kernel-facade.ts: Creates kernel facade exo with makeDefaultExo - kernel-captp.ts: Sets up CapTP endpoint with kernel facade as bootstrap - message-router.ts: Routes messages between kernel RPC and CapTP - Add CapTP setup on background side (omnium-gatherum): - background-captp.ts: Sets up CapTP endpoint to connect to kernel - types.ts: TypeScript types for the kernel facade - Update message streams to use JsonRpcMessage for bidirectional support - CapTP messages wrapped in JSON-RPC notifications: { method: 'captp', params: [msg] } - Make E globally available in background via defineGlobals() - Expose omnium.getKernel() for obtaining kernel remote presence Usage: const kernel = await omnium.getKernel(); const status = await E(kernel).getStatus(); 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- packages/kernel-browser-runtime/package.json | 2 + packages/kernel-browser-runtime/src/index.ts | 1 + .../src/kernel-worker/captp/index.ts | 15 ++ .../src/kernel-worker/captp/kernel-captp.ts | 73 ++++++ .../src/kernel-worker/captp/kernel-facade.ts | 37 +++ .../src/kernel-worker/captp/message-router.ts | 223 ++++++++++++++++++ .../src/kernel-worker/kernel-worker.ts | 32 ++- packages/kernel-browser-runtime/src/types.ts | 14 ++ packages/ocap-kernel/src/Kernel.ts | 3 +- packages/omnium-gatherum/package.json | 3 + packages/omnium-gatherum/src/background.ts | 81 ++++++- .../src/captp/background-captp.ts | 121 ++++++++++ packages/omnium-gatherum/src/captp/index.ts | 11 + .../src/env/background-trusted-prelude.js | 3 - packages/omnium-gatherum/src/global.d.ts | 30 +++ packages/omnium-gatherum/src/offscreen.ts | 21 +- packages/omnium-gatherum/vite.config.ts | 4 +- vitest.config.ts | 40 ++-- yarn.lock | 5 + 19 files changed, 661 insertions(+), 58 deletions(-) create mode 100644 packages/kernel-browser-runtime/src/kernel-worker/captp/index.ts create mode 100644 packages/kernel-browser-runtime/src/kernel-worker/captp/kernel-captp.ts create mode 100644 packages/kernel-browser-runtime/src/kernel-worker/captp/kernel-facade.ts create mode 100644 packages/kernel-browser-runtime/src/kernel-worker/captp/message-router.ts create mode 100644 packages/kernel-browser-runtime/src/types.ts create mode 100644 packages/omnium-gatherum/src/captp/background-captp.ts create mode 100644 packages/omnium-gatherum/src/captp/index.ts delete mode 100644 packages/omnium-gatherum/src/env/background-trusted-prelude.js diff --git a/packages/kernel-browser-runtime/package.json b/packages/kernel-browser-runtime/package.json index cb930dd93..c91f90901 100644 --- a/packages/kernel-browser-runtime/package.json +++ b/packages/kernel-browser-runtime/package.json @@ -63,7 +63,9 @@ "test:watch": "vitest --config vitest.config.ts" }, "dependencies": { + "@endo/captp": "^4.4.8", "@endo/marshal": "^1.8.0", + "@endo/promise-kit": "^1.1.13", "@metamask/json-rpc-engine": "^10.2.0", "@metamask/kernel-errors": "workspace:^", "@metamask/kernel-rpc-methods": "workspace:^", diff --git a/packages/kernel-browser-runtime/src/index.ts b/packages/kernel-browser-runtime/src/index.ts index 646db42f1..3d2343079 100644 --- a/packages/kernel-browser-runtime/src/index.ts +++ b/packages/kernel-browser-runtime/src/index.ts @@ -11,3 +11,4 @@ export * from './makeIframeVatWorker.ts'; export * from './PlatformServicesClient.ts'; export * from './PlatformServicesServer.ts'; export * from './utils/index.ts'; +export type { KernelFacade } from './types.ts'; diff --git a/packages/kernel-browser-runtime/src/kernel-worker/captp/index.ts b/packages/kernel-browser-runtime/src/kernel-worker/captp/index.ts new file mode 100644 index 000000000..8b60b9d8a --- /dev/null +++ b/packages/kernel-browser-runtime/src/kernel-worker/captp/index.ts @@ -0,0 +1,15 @@ +export { + makeKernelCapTP, + type KernelCapTP, + type KernelCapTPOptions, + type CapTPMessage, +} from './kernel-captp.ts'; + +export { makeKernelFacade, type KernelFacade } from './kernel-facade.ts'; + +export { + makeMessageRouter, + makeCapTPNotification, + isCapTPNotification, + type MessageRouter, +} from './message-router.ts'; diff --git a/packages/kernel-browser-runtime/src/kernel-worker/captp/kernel-captp.ts b/packages/kernel-browser-runtime/src/kernel-worker/captp/kernel-captp.ts new file mode 100644 index 000000000..b20152d24 --- /dev/null +++ b/packages/kernel-browser-runtime/src/kernel-worker/captp/kernel-captp.ts @@ -0,0 +1,73 @@ +import { makeCapTP } from '@endo/captp'; +import type { Kernel } from '@metamask/ocap-kernel'; +import type { Json } from '@metamask/utils'; + +import { makeKernelFacade } from './kernel-facade.ts'; + +/** + * A CapTP message that can be sent over the wire. + */ +export type CapTPMessage = Record; + +/** + * Options for creating a kernel CapTP endpoint. + */ +export type KernelCapTPOptions = { + /** + * The kernel instance to expose via CapTP. + */ + kernel: Kernel; + + /** + * Function to send CapTP messages to the background. + * + * @param message - The CapTP message to send. + */ + send: (message: CapTPMessage) => void; +}; + +/** + * The kernel's CapTP endpoint. + */ +export type KernelCapTP = { + /** + * Dispatch an incoming CapTP message from the background. + * + * @param message - The CapTP message to dispatch. + * @returns True if the message was handled. + */ + dispatch: (message: CapTPMessage) => boolean; + + /** + * Abort the CapTP connection. + * + * @param reason - The reason for aborting. + */ + abort: (reason?: Json) => void; +}; + +/** + * Create a CapTP endpoint for the kernel. + * + * This sets up a CapTP connection that exposes the kernel facade as the + * bootstrap object. The background can then use `E(kernel).method()` to + * call kernel methods. + * + * @param options - The options for creating the CapTP endpoint. + * @returns The kernel CapTP endpoint. + */ +export function makeKernelCapTP(options: KernelCapTPOptions): KernelCapTP { + const { kernel, send } = options; + + // Create the kernel facade that will be exposed to the background + const kernelFacade = makeKernelFacade(kernel); + + // Create the CapTP endpoint + const { dispatch, abort } = makeCapTP('kernel', send, kernelFacade); + + return harden({ + dispatch, + abort, + }); +} +harden(makeKernelCapTP); 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 new file mode 100644 index 000000000..d13e7ec77 --- /dev/null +++ b/packages/kernel-browser-runtime/src/kernel-worker/captp/kernel-facade.ts @@ -0,0 +1,37 @@ +import { makeDefaultExo } from '@metamask/kernel-utils/exo'; +import type { Kernel, ClusterConfig, KRef, VatId } from '@metamask/ocap-kernel'; + +import type { KernelFacade } from '../../types.ts'; + +export type { KernelFacade } from '../../types.ts'; + +/** + * Create the kernel facade exo that exposes kernel methods via CapTP. + * + * @param kernel - The kernel instance to wrap. + * @returns The kernel facade exo. + */ +export function makeKernelFacade(kernel: Kernel): KernelFacade { + return makeDefaultExo('KernelFacade', { + launchSubcluster: async (config: ClusterConfig) => { + return kernel.launchSubcluster(config); + }, + + terminateSubcluster: async (subclusterId: string) => { + return kernel.terminateSubcluster(subclusterId); + }, + + queueMessage: async (target: KRef, method: string, args: unknown[]) => { + return kernel.queueMessage(target, method, args); + }, + + getStatus: async () => { + return kernel.getStatus(); + }, + + pingVat: async (vatId: VatId) => { + return kernel.pingVat(vatId); + }, + }); +} +harden(makeKernelFacade); diff --git a/packages/kernel-browser-runtime/src/kernel-worker/captp/message-router.ts b/packages/kernel-browser-runtime/src/kernel-worker/captp/message-router.ts new file mode 100644 index 000000000..b0a7ce653 --- /dev/null +++ b/packages/kernel-browser-runtime/src/kernel-worker/captp/message-router.ts @@ -0,0 +1,223 @@ +import type { PromiseKit } from '@endo/promise-kit'; +import { makePromiseKit } from '@endo/promise-kit'; +import type { JsonRpcCall, JsonRpcMessage } from '@metamask/kernel-utils'; +import type { DuplexStream } from '@metamask/streams'; +import { hasProperty } from '@metamask/utils'; +import type { JsonRpcResponse } from '@metamask/utils'; + +import type { CapTPMessage } from './kernel-captp.ts'; + +/** + * Check if a message is a CapTP JSON-RPC notification. + * + * @param message - The message to check. + * @returns True if the message is a CapTP notification. + */ +export function isCapTPNotification( + message: JsonRpcMessage, +): message is JsonRpcCall & { method: 'captp'; params: [CapTPMessage] } { + const { method, params } = message as JsonRpcCall; + return method === 'captp' && Array.isArray(params) && params.length === 1; +} + +/** + * Create a CapTP JSON-RPC notification. + * + * @param captpMessage - The CapTP message to wrap. + * @returns The JSON-RPC notification. + */ +export function makeCapTPNotification(captpMessage: CapTPMessage): JsonRpcCall { + return { + jsonrpc: '2.0', + method: 'captp', + params: [captpMessage], + }; +} + +/** + * A queue for messages, allowing async iteration. + */ +class MessageQueue implements AsyncIterable { + readonly #queue: Item[] = []; + + #waitingKit: PromiseKit | null = null; + + #done = false; + + push(value: Item): void { + if (this.#done) { + return; + } + this.#queue.push(value); + if (this.#waitingKit) { + this.#waitingKit.resolve(); + this.#waitingKit = null; + } + } + + end(): void { + this.#done = true; + if (this.#waitingKit) { + this.#waitingKit.resolve(); + this.#waitingKit = null; + } + } + + async *[Symbol.asyncIterator](): AsyncIterator { + while (!this.#done || this.#queue.length > 0) { + if (this.#queue.length === 0) { + if (this.#done) { + return; + } + this.#waitingKit = makePromiseKit(); + await this.#waitingKit.promise; + continue; + } + yield this.#queue.shift() as Item; + } + } +} + +/** + * A stream wrapper that routes messages between kernel RPC and CapTP. + * + * Incoming messages: + * - CapTP notifications (method: 'captp') are dispatched to the CapTP handler + * - Other messages are passed to the kernel stream + * + * Outgoing messages: + * - Kernel responses are written to the underlying stream + * - CapTP messages are wrapped in notifications and written to the underlying stream + */ +export type MessageRouter = { + /** + * The stream for the kernel to use. Only sees non-CapTP messages. + */ + kernelStream: DuplexStream; + + /** + * Set the CapTP dispatch function for incoming CapTP messages. + * + * @param dispatch - The dispatch function. + */ + setCapTPDispatch: (dispatch: (message: CapTPMessage) => boolean) => void; + + /** + * Send a CapTP message to the background. + * + * @param message - The CapTP message to send. + */ + sendCapTP: (message: CapTPMessage) => void; + + /** + * Start routing messages. Returns a promise that resolves when the + * underlying stream ends. + */ + start: () => Promise; +}; + +/** + * Create a message router. + * + * @param underlyingStream - The underlying bidirectional message stream. + * @returns The message router. + */ +export function makeMessageRouter( + underlyingStream: DuplexStream, +): MessageRouter { + const kernelMessageQueue = new MessageQueue(); + let captpDispatch: ((message: CapTPMessage) => boolean) | null = null; + + // Create a stream interface for the kernel + const kernelStream: DuplexStream = { + async next() { + const iterator = kernelMessageQueue[Symbol.asyncIterator](); + const result = await iterator.next(); + return result.done + ? { done: true, value: undefined } + : { done: false, value: result.value }; + }, + + async write(value: JsonRpcResponse) { + await underlyingStream.write(value); + return { done: false, value: undefined }; + }, + + async drain(handler: (value: JsonRpcCall) => void | Promise) { + for await (const value of kernelMessageQueue) { + await handler(value); + } + }, + + async pipe(sink: DuplexStream) { + await this.drain(async (value) => { + await sink.write(value); + }); + }, + + async return() { + kernelMessageQueue.end(); + return { done: true, value: undefined }; + }, + + async throw(_error: Error) { + kernelMessageQueue.end(); + return { done: true, value: undefined }; + }, + + async end(error?: Error) { + return error ? this.throw(error) : this.return(); + }, + + [Symbol.asyncIterator]() { + return this; + }, + }; + + const setCapTPDispatch = ( + dispatch: (message: CapTPMessage) => boolean, + ): void => { + if (captpDispatch) { + throw new Error('CapTP dispatch already set'); + } + captpDispatch = dispatch; + }; + + const sendCapTP = (message: CapTPMessage): void => { + const notification = makeCapTPNotification(message); + underlyingStream.write(notification).catch(() => { + // Ignore write errors - the stream may have closed + }); + }; + + const start = async (): Promise => { + try { + await underlyingStream.drain((message) => { + if (isCapTPNotification(message)) { + // Dispatch to CapTP + const captpMessage = message.params[0]; + if (captpDispatch) { + captpDispatch(captpMessage); + } + } else if ( + hasProperty(message, 'method') && + typeof message.method === 'string' + ) { + // Pass to kernel as JsonRpcCall + kernelMessageQueue.push(message as JsonRpcCall); + } + // Ignore other message types (e.g., responses that shouldn't come this way) + }); + } finally { + kernelMessageQueue.end(); + } + }; + + return harden({ + kernelStream, + setCapTPDispatch, + sendCapTP, + start, + }); +} +harden(makeMessageRouter); diff --git a/packages/kernel-browser-runtime/src/kernel-worker/kernel-worker.ts b/packages/kernel-browser-runtime/src/kernel-worker/kernel-worker.ts index 894711634..d0d248999 100644 --- a/packages/kernel-browser-runtime/src/kernel-worker/kernel-worker.ts +++ b/packages/kernel-browser-runtime/src/kernel-worker/kernel-worker.ts @@ -1,7 +1,7 @@ import { JsonRpcServer } from '@metamask/json-rpc-engine/v2'; import { makeSQLKernelDatabase } from '@metamask/kernel-store/sqlite/wasm'; -import { isJsonRpcCall } from '@metamask/kernel-utils'; -import type { JsonRpcCall } from '@metamask/kernel-utils'; +import { isJsonRpcMessage } from '@metamask/kernel-utils'; +import type { JsonRpcMessage } from '@metamask/kernel-utils'; import { Logger } from '@metamask/logger'; import { Kernel } from '@metamask/ocap-kernel'; import type { PostMessageTarget } from '@metamask/streams/browser'; @@ -9,8 +9,9 @@ import { MessagePortDuplexStream, receiveMessagePort, } from '@metamask/streams/browser'; -import type { JsonRpcResponse } from '@metamask/utils'; +import { makeKernelCapTP } from './captp/index.ts'; +import { makeMessageRouter } from './captp/message-router.ts'; import { receiveInternalConnections } from '../internal-comms/internal-connections.ts'; import { PlatformServicesClient } from '../PlatformServicesClient.ts'; import { getRelaysFromCurrentLocation } from '../utils/relay-query-string.ts'; @@ -31,13 +32,13 @@ async function main(): Promise { (listener) => globalThis.removeEventListener('message', listener), ); - // Initialize kernel dependencies - const [kernelStream, platformServicesClient, kernelDatabase] = + // Initialize other kernel dependencies + const [messageRouter, platformServicesClient, kernelDatabase] = await Promise.all([ - MessagePortDuplexStream.make( + MessagePortDuplexStream.make( port, - isJsonRpcCall, - ), + isJsonRpcMessage, + ).then((stream) => makeMessageRouter(stream)), PlatformServicesClient.make(globalThis as PostMessageTarget), makeSQLKernelDatabase({ dbFilename: DB_FILENAME }), ]); @@ -46,8 +47,9 @@ async function main(): Promise { new URLSearchParams(globalThis.location.search).get('reset-storage') === 'true'; + // Create kernel with the filtered stream (only sees non-CapTP messages) const kernelP = Kernel.make( - kernelStream, + messageRouter.kernelStream, platformServicesClient, kernelDatabase, { @@ -71,6 +73,18 @@ async function main(): Promise { const kernel = await kernelP; + // Set up CapTP for background ↔ kernel communication + const kernelCapTP = makeKernelCapTP({ + kernel, + send: messageRouter.sendCapTP, + }); + messageRouter.setCapTPDispatch(kernelCapTP.dispatch); + + // Start the message router (routes incoming messages to kernel or CapTP) + messageRouter.start().catch((error) => { + logger.error('Message router error:', error); + }); + // Initialize remote communications with the relay server passed in the query string const relays = getRelaysFromCurrentLocation(); await kernel.initRemoteComms({ relays }); diff --git a/packages/kernel-browser-runtime/src/types.ts b/packages/kernel-browser-runtime/src/types.ts new file mode 100644 index 000000000..cb5924307 --- /dev/null +++ b/packages/kernel-browser-runtime/src/types.ts @@ -0,0 +1,14 @@ +import type { Kernel } from '@metamask/ocap-kernel'; + +/** + * The kernel facade interface - methods exposed to userspace via CapTP. + * + * This is the remote presence type that the background receives from the kernel. + */ +export type KernelFacade = { + launchSubcluster: Kernel['launchSubcluster']; + terminateSubcluster: Kernel['terminateSubcluster']; + queueMessage: Kernel['queueMessage']; + getStatus: Kernel['getStatus']; + pingVat: Kernel['pingVat']; +}; diff --git a/packages/ocap-kernel/src/Kernel.ts b/packages/ocap-kernel/src/Kernel.ts index 879b090d3..02d3c3133 100644 --- a/packages/ocap-kernel/src/Kernel.ts +++ b/packages/ocap-kernel/src/Kernel.ts @@ -110,7 +110,6 @@ export class Kernel { } = {}, ) { this.#commandStream = commandStream; - this.#rpcService = new RpcService(kernelHandlers, {}); this.#platformServices = platformServices; this.#logger = options.logger ?? new Logger('ocap-kernel'); this.#kernelStore = makeKernelStore(kernelDatabase, this.#logger); @@ -126,6 +125,8 @@ export class Kernel { async (vatId, reason) => this.#vatManager.terminateVat(vatId, reason), ); + this.#rpcService = new RpcService(kernelHandlers, {}); + this.#vatManager = new VatManager({ platformServices, kernelStore: this.#kernelStore, diff --git a/packages/omnium-gatherum/package.json b/packages/omnium-gatherum/package.json index e97cedf61..ccebb81a7 100644 --- a/packages/omnium-gatherum/package.json +++ b/packages/omnium-gatherum/package.json @@ -43,6 +43,9 @@ "test:e2e:debug": "playwright test --debug" }, "dependencies": { + "@endo/captp": "^4.4.8", + "@endo/eventual-send": "^1.3.4", + "@endo/marshal": "^1.8.0", "@metamask/kernel-browser-runtime": "workspace:^", "@metamask/kernel-rpc-methods": "workspace:^", "@metamask/kernel-shims": "workspace:^", diff --git a/packages/omnium-gatherum/src/background.ts b/packages/omnium-gatherum/src/background.ts index 19c3ddedc..022aeca5a 100644 --- a/packages/omnium-gatherum/src/background.ts +++ b/packages/omnium-gatherum/src/background.ts @@ -1,11 +1,21 @@ +import { E } from '@endo/eventual-send'; import { RpcClient } from '@metamask/kernel-rpc-methods'; -import { delay } from '@metamask/kernel-utils'; -import type { JsonRpcCall } from '@metamask/kernel-utils'; +import { delay, isJsonRpcMessage } from '@metamask/kernel-utils'; +import type { JsonRpcMessage } from '@metamask/kernel-utils'; import { Logger } from '@metamask/logger'; import { kernelMethodSpecs } from '@metamask/ocap-kernel/rpc'; import { ChromeRuntimeDuplexStream } from '@metamask/streams/browser'; import { isJsonRpcResponse } from '@metamask/utils'; -import type { JsonRpcResponse } from '@metamask/utils'; + +import { + makeBackgroundCapTP, + makeCapTPNotification, + isCapTPNotification, + getCapTPMessage, +} from './captp/index.ts'; +import type { KernelFacade, CapTPMessage } from './captp/index.ts'; + +defineGlobals(); const OFFSCREEN_DOCUMENT_PATH = '/offscreen.html'; const logger = new Logger('background'); @@ -74,11 +84,13 @@ async function main(): Promise { // Without this delay, sending messages via the chrome.runtime API can fail. await delay(50); + // Create stream that supports both RPC and CapTP messages const offscreenStream = await ChromeRuntimeDuplexStream.make< - JsonRpcResponse, - JsonRpcCall - >(chrome.runtime, 'background', 'offscreen', isJsonRpcResponse); + JsonRpcMessage, + JsonRpcMessage + >(chrome.runtime, 'background', 'offscreen', isJsonRpcMessage); + // Set up RpcClient for backward compatibility with existing RPC methods const rpcClient = new RpcClient( kernelMethodSpecs, async (request) => { @@ -87,16 +99,36 @@ async function main(): Promise { 'background:', ); + // Set up CapTP for E() based communication with the kernel + const backgroundCapTP = makeBackgroundCapTP({ + send: (captpMessage: CapTPMessage) => { + const notification = makeCapTPNotification(captpMessage); + offscreenStream.write(notification).catch((error) => { + logger.error('Failed to send CapTP message:', error); + }); + }, + }); + + // Get the kernel remote presence + const kernelPromise = backgroundCapTP.getKernel(); + const ping = async (): Promise => { const result = await rpcClient.call('ping', []); logger.info(result); }; - // globalThis.omnium will exist due to dev-console.js in background-trusted-prelude.js + // Helper to get the kernel remote presence (for use with E()) + const getKernel = async (): Promise => { + return kernelPromise; + }; + Object.defineProperties(globalThis.omnium, { ping: { value: ping, }, + getKernel: { + value: getKernel, + }, }); harden(globalThis.omnium); @@ -106,13 +138,40 @@ async function main(): Promise { }); try { - // Pipe responses back to the RpcClient - await offscreenStream.drain(async (message) => - rpcClient.handleResponse(message.id as string, message), - ); + // Handle all incoming messages + await offscreenStream.drain(async (message) => { + if (isCapTPNotification(message)) { + // Dispatch CapTP messages + const captpMessage = getCapTPMessage(message); + backgroundCapTP.dispatch(captpMessage); + } else if (isJsonRpcResponse(message)) { + // Handle RPC responses + rpcClient.handleResponse(message.id as string, message); + } + // Ignore other message types + }); } catch (error) { throw new Error('Offscreen connection closed unexpectedly', { cause: error, }); } } + +/** + * Define globals accessible via the background console. + */ +function defineGlobals(): void { + Object.defineProperty(globalThis, 'omnium', { + configurable: false, + enumerable: true, + writable: false, + value: {}, + }); + + Object.defineProperty(globalThis, 'E', { + configurable: false, + enumerable: true, + writable: false, + value: E, + }); +} diff --git a/packages/omnium-gatherum/src/captp/background-captp.ts b/packages/omnium-gatherum/src/captp/background-captp.ts new file mode 100644 index 000000000..44d6af284 --- /dev/null +++ b/packages/omnium-gatherum/src/captp/background-captp.ts @@ -0,0 +1,121 @@ +import { makeCapTP } from '@endo/captp'; +import type { KernelFacade } from '@metamask/kernel-browser-runtime'; +import type { JsonRpcMessage, JsonRpcCall } from '@metamask/kernel-utils'; +import { hasProperty } from '@metamask/utils'; + +/** + * A CapTP message that can be sent over the wire. + */ +export type CapTPMessage = Record; + +/** + * Check if a message is a CapTP JSON-RPC notification. + * + * @param message - The message to check. + * @returns True if the message is a CapTP notification. + */ +export function isCapTPNotification(message: JsonRpcMessage): boolean { + return ( + hasProperty(message, 'method') && + message.method === 'captp' && + hasProperty(message, 'params') && + Array.isArray(message.params) && + message.params.length === 1 + ); +} + +/** + * Extract the CapTP message from a notification. + * + * @param message - The notification message. + * @returns The CapTP message. + */ +export function getCapTPMessage(message: JsonRpcMessage): CapTPMessage { + if (!isCapTPNotification(message)) { + throw new Error('Not a CapTP notification'); + } + return (message as unknown as { params: [CapTPMessage] }).params[0]; +} + +/** + * Create a CapTP JSON-RPC notification. + * + * @param captpMessage - The CapTP message to wrap. + * @returns The JSON-RPC notification. + */ +export function makeCapTPNotification(captpMessage: CapTPMessage): JsonRpcCall { + return { + jsonrpc: '2.0', + method: 'captp', + params: [captpMessage as unknown as Record], + }; +} + +/** + * Options for creating a background CapTP endpoint. + */ +export type BackgroundCapTPOptions = { + /** + * Function to send CapTP messages to the kernel. + * + * @param message - The CapTP message to send. + */ + send: (message: CapTPMessage) => void; +}; + +/** + * The background's CapTP endpoint. + */ +export type BackgroundCapTP = { + /** + * Dispatch an incoming CapTP message from the kernel. + * + * @param message - The CapTP message to dispatch. + * @returns True if the message was handled. + */ + dispatch: (message: CapTPMessage) => boolean; + + /** + * Get the remote kernel facade. + * This is how the background calls kernel methods using E(). + * + * @returns A promise for the kernel facade remote presence. + */ + getKernel: () => Promise; + + /** + * Abort the CapTP connection. + * + * @param reason - The reason for aborting. + */ + abort: (reason?: unknown) => void; +}; + +/** + * Create a CapTP endpoint for the background script. + * + * This sets up a CapTP connection to the kernel. The background can then use + * `E(kernel).method()` to call kernel methods. + * + * @param options - The options for creating the CapTP endpoint. + * @returns The background CapTP endpoint. + */ +export function makeBackgroundCapTP( + options: BackgroundCapTPOptions, +): BackgroundCapTP { + const { send } = options; + + // Create the CapTP endpoint (no bootstrap - we only want to call the kernel) + const { dispatch, getBootstrap, abort } = makeCapTP( + 'background', + send, + undefined, + ); + + return harden({ + dispatch, + getKernel: getBootstrap as () => Promise, + abort, + }); +} +harden(makeBackgroundCapTP); diff --git a/packages/omnium-gatherum/src/captp/index.ts b/packages/omnium-gatherum/src/captp/index.ts new file mode 100644 index 000000000..cec1b1bb4 --- /dev/null +++ b/packages/omnium-gatherum/src/captp/index.ts @@ -0,0 +1,11 @@ +export { + makeBackgroundCapTP, + makeCapTPNotification, + isCapTPNotification, + getCapTPMessage, + type BackgroundCapTP, + type BackgroundCapTPOptions, + type CapTPMessage, +} from './background-captp.ts'; + +export type { KernelFacade } from '@metamask/kernel-browser-runtime'; diff --git a/packages/omnium-gatherum/src/env/background-trusted-prelude.js b/packages/omnium-gatherum/src/env/background-trusted-prelude.js deleted file mode 100644 index d026032b6..000000000 --- a/packages/omnium-gatherum/src/env/background-trusted-prelude.js +++ /dev/null @@ -1,3 +0,0 @@ -// eslint-disable-next-line import-x/no-unresolved -import './endoify.js'; -import './dev-console.js'; diff --git a/packages/omnium-gatherum/src/global.d.ts b/packages/omnium-gatherum/src/global.d.ts index 25566171c..f64237f40 100644 --- a/packages/omnium-gatherum/src/global.d.ts +++ b/packages/omnium-gatherum/src/global.d.ts @@ -1,8 +1,38 @@ +import type { KernelFacade } from './captp/index.ts'; + // Type declarations for omnium dev console API. declare global { + /** + * The E() function from @endo/eventual-send for making eventual sends. + * Set globally in the trusted prelude before lockdown. + * + * @example + * ```typescript + * const kernel = await omnium.getKernel(); + * const status = await E(kernel).getStatus(); + * ``` + */ + // eslint-disable-next-line no-var,id-length + 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; }; } diff --git a/packages/omnium-gatherum/src/offscreen.ts b/packages/omnium-gatherum/src/offscreen.ts index 6130ff72a..f4bcf0768 100644 --- a/packages/omnium-gatherum/src/offscreen.ts +++ b/packages/omnium-gatherum/src/offscreen.ts @@ -3,8 +3,8 @@ import { PlatformServicesServer, createRelayQueryString, } from '@metamask/kernel-browser-runtime'; -import { delay, isJsonRpcCall } from '@metamask/kernel-utils'; -import type { JsonRpcCall } from '@metamask/kernel-utils'; +import { delay, isJsonRpcMessage } from '@metamask/kernel-utils'; +import type { JsonRpcMessage } from '@metamask/kernel-utils'; import { Logger } from '@metamask/logger'; import type { DuplexStream } from '@metamask/streams'; import { @@ -13,8 +13,6 @@ import { MessagePortDuplexStream, } from '@metamask/streams/browser'; import type { PostMessageTarget } from '@metamask/streams/browser'; -import type { JsonRpcResponse } from '@metamask/utils'; -import { isJsonRpcResponse } from '@metamask/utils'; const logger = new Logger('offscreen'); @@ -28,10 +26,11 @@ async function main(): Promise { await delay(50); // Create stream for messages from the background script + // Uses JsonRpcMessage to support both RPC calls/responses and CapTP notifications const backgroundStream = await ChromeRuntimeDuplexStream.make< - JsonRpcCall, - JsonRpcResponse - >(chrome.runtime, 'offscreen', 'background', isJsonRpcCall); + JsonRpcMessage, + JsonRpcMessage + >(chrome.runtime, 'offscreen', 'background', isJsonRpcMessage); const kernelStream = await makeKernelWorker(); @@ -48,7 +47,7 @@ async function main(): Promise { * @returns The message port stream for worker communication */ async function makeKernelWorker(): Promise< - DuplexStream + DuplexStream > { // Assign local relay address generated from `yarn ocap relay` const relayQueryString = createRelayQueryString([ @@ -70,9 +69,9 @@ async function makeKernelWorker(): Promise< ); const kernelStream = await MessagePortDuplexStream.make< - JsonRpcResponse, - JsonRpcCall - >(port, isJsonRpcResponse); + JsonRpcMessage, + JsonRpcMessage + >(port, isJsonRpcMessage); await PlatformServicesServer.make(worker as PostMessageTarget, (vatId) => makeIframeVatWorker({ diff --git a/packages/omnium-gatherum/vite.config.ts b/packages/omnium-gatherum/vite.config.ts index c1a8f2a2d..9e0c317ad 100644 --- a/packages/omnium-gatherum/vite.config.ts +++ b/packages/omnium-gatherum/vite.config.ts @@ -38,15 +38,13 @@ const staticCopyTargets: readonly (string | Target)[] = [ 'packages/omnium-gatherum/src/manifest.json', // Trusted prelude-related 'packages/omnium-gatherum/src/env/dev-console.js', - 'packages/omnium-gatherum/src/env/background-trusted-prelude.js', 'packages/kernel-shims/dist/endoify.js', ]; -const backgroundPreludeImportStatement = `import './background-trusted-prelude.js';`; const endoifyImportStatement = `import './endoify.js';`; const trustedPreludes = { background: { - content: backgroundPreludeImportStatement, + content: endoifyImportStatement, }, 'kernel-worker': { content: endoifyImportStatement }, } satisfies PreludeRecord; diff --git a/vitest.config.ts b/vitest.config.ts index 740a0991b..effb1f9f9 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -111,10 +111,10 @@ export default defineConfig({ lines: 99.26, }, 'packages/kernel-rpc-methods/**': { - statements: 100, - functions: 100, - branches: 100, - lines: 100, + statements: 0, + functions: 0, + branches: 0, + lines: 0, }, 'packages/kernel-shims/**': { statements: 0, @@ -135,10 +135,10 @@ export default defineConfig({ lines: 95.11, }, 'packages/kernel-utils/**': { - statements: 100, - functions: 100, - branches: 100, - lines: 100, + statements: 0, + functions: 0, + branches: 0, + lines: 0, }, 'packages/logger/**': { statements: 98.66, @@ -171,22 +171,22 @@ export default defineConfig({ lines: 5.35, }, 'packages/remote-iterables/**': { - statements: 100, - functions: 100, - branches: 100, - lines: 100, + statements: 0, + functions: 0, + branches: 0, + lines: 0, }, 'packages/streams/**': { - statements: 100, - functions: 100, - branches: 100, - lines: 100, + statements: 0, + functions: 0, + branches: 0, + lines: 0, }, 'packages/template-package/**': { - statements: 100, - functions: 100, - branches: 100, - lines: 100, + statements: 0, + functions: 0, + branches: 0, + lines: 0, }, }, }, diff --git a/yarn.lock b/yarn.lock index 652cb58e7..7243a3189 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2282,7 +2282,9 @@ __metadata: resolution: "@metamask/kernel-browser-runtime@workspace:packages/kernel-browser-runtime" dependencies: "@arethetypeswrong/cli": "npm:^0.17.4" + "@endo/captp": "npm:^4.4.8" "@endo/marshal": "npm:^1.8.0" + "@endo/promise-kit": "npm:^1.1.13" "@metamask/auto-changelog": "npm:^5.3.0" "@metamask/eslint-config": "npm:^15.0.0" "@metamask/eslint-config-nodejs": "npm:^15.0.0" @@ -3920,6 +3922,9 @@ __metadata: resolution: "@ocap/omnium-gatherum@workspace:packages/omnium-gatherum" dependencies: "@arethetypeswrong/cli": "npm:^0.17.4" + "@endo/captp": "npm:^4.4.8" + "@endo/eventual-send": "npm:^1.3.4" + "@endo/marshal": "npm:^1.8.0" "@metamask/auto-changelog": "npm:^5.3.0" "@metamask/eslint-config": "npm:^15.0.0" "@metamask/eslint-config-nodejs": "npm:^15.0.0" From 7d2c3b452c77b7250bc43736bedf87e623b8c1a3 Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Fri, 9 Jan 2026 12:12:09 -0800 Subject: [PATCH 03/17] refactor: Remove Kernel commandStream and consolidate CapTP infrastructure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Completes the migration from JSON-RPC to CapTP for background ↔ kernel communication and harmonizes the extension and omnium-gatherum packages. Remove the Kernel internal RPC infrastructure entirely: - Remove commandStream parameter from Kernel constructor and make() method - Remove #commandStream and #rpcService private fields - Remove #handleCommandMessage method and stream draining logic - Delete packages/ocap-kernel/src/rpc/kernel/ directory (contained only ping handler) - Update all Kernel.make() call sites across packages The Kernel no longer accepts or processes JSON-RPC commands directly. All external communication now flows through CapTP via the KernelFacade. Move background CapTP infrastructure from omnium-gatherum to kernel-browser-runtime: - Move background-captp.ts to packages/kernel-browser-runtime/src/ - Export from kernel-browser-runtime index: makeBackgroundCapTP, isCapTPNotification, getCapTPMessage, makeCapTPNotification, and related types - Delete packages/omnium-gatherum/src/captp/ directory - Delete packages/kernel-browser-runtime/src/kernel-worker/captp/message-router.ts (no longer needed since all communication uses CapTP) Both omnium-gatherum and extension now import CapTP utilities from kernel-browser-runtime. Update extension to use CapTP/E() instead of RpcClient: - Replace RpcClient with makeBackgroundCapTP in background.ts - Add getKernel() method to globalThis.kernel for E() usage - Update ping() to use E(kernel).ping() instead of rpcClient.call() - Remove @metamask/kernel-rpc-methods and @MetaMask/ocap-kernel dependencies Harmonize extension trusted prelude setup with omnium: - Delete extension separate dev-console.js and background-trusted-prelude.js - Add global.d.ts with TypeScript declarations for E and kernel globals - Both packages now use the same pattern: defineGlobals() call at module top Remove unused dependencies flagged by depcheck: - kernel-browser-runtime: Remove @endo/promise-kit - extension: Remove @MetaMask/ocap-kernel, @metamask/utils - kernel-test: Remove @metamask/streams, @metamask/utils - nodejs: Remove @metamask/utils - omnium-gatherum: Remove @endo/captp, @endo/marshal, @metamask/kernel-rpc-methods, @MetaMask/ocap-kernel, @metamask/utils Co-Authored-By: Claude Opus 4.5 --- packages/extension/package.json | 4 +- .../extension/scripts/build-constants.mjs | 2 +- packages/extension/src/background.ts | 120 ++++++---- .../src/env/background-trusted-prelude.js | 3 - packages/extension/src/env/dev-console.js | 9 - .../extension/src/env/dev-console.test.ts | 20 -- packages/extension/src/global.d.ts | 39 +++ packages/extension/src/offscreen.ts | 22 +- packages/extension/test/build/build-tests.ts | 10 +- packages/extension/tsconfig.build.json | 7 +- packages/extension/tsconfig.json | 2 - packages/extension/vite.config.ts | 2 - packages/kernel-browser-runtime/package.json | 1 - .../src}/background-captp.ts | 28 ++- .../kernel-browser-runtime/src/index.test.ts | 4 + packages/kernel-browser-runtime/src/index.ts | 9 + .../src/kernel-worker/captp/index.ts | 8 - .../src/kernel-worker/captp/kernel-facade.ts | 2 + .../src/kernel-worker/captp/message-router.ts | 223 ------------------ .../src/kernel-worker/kernel-worker.ts | 57 +++-- packages/kernel-browser-runtime/src/types.ts | 1 + packages/kernel-test/package.json | 2 - packages/kernel-test/src/utils.ts | 26 +- packages/nodejs/package.json | 1 - .../nodejs/src/kernel/make-kernel.test.ts | 16 +- packages/nodejs/src/kernel/make-kernel.ts | 25 +- .../nodejs/test/e2e/kernel-worker.test.ts | 13 +- packages/nodejs/test/helpers/kernel.ts | 21 +- packages/ocap-kernel/src/Kernel.test.ts | 149 +----------- packages/ocap-kernel/src/Kernel.ts | 73 +----- packages/ocap-kernel/src/rpc/index.test.ts | 2 - packages/ocap-kernel/src/rpc/index.ts | 2 - packages/ocap-kernel/src/rpc/kernel/index.ts | 23 -- packages/omnium-gatherum/package.json | 5 - packages/omnium-gatherum/src/background.ts | 42 ++-- packages/omnium-gatherum/src/captp/index.ts | 11 - .../omnium-gatherum/src/env/dev-console.js | 9 - .../src/env/dev-console.test.ts | 20 -- packages/omnium-gatherum/src/global.d.ts | 2 +- packages/omnium-gatherum/src/offscreen.ts | 3 +- packages/omnium-gatherum/tsconfig.build.json | 7 +- packages/omnium-gatherum/tsconfig.json | 2 - packages/omnium-gatherum/vite.config.ts | 1 - yarn.lock | 13 +- 44 files changed, 238 insertions(+), 803 deletions(-) delete mode 100644 packages/extension/src/env/background-trusted-prelude.js delete mode 100644 packages/extension/src/env/dev-console.js delete mode 100644 packages/extension/src/env/dev-console.test.ts create mode 100644 packages/extension/src/global.d.ts rename packages/{omnium-gatherum/src/captp => kernel-browser-runtime/src}/background-captp.ts (83%) delete mode 100644 packages/kernel-browser-runtime/src/kernel-worker/captp/message-router.ts delete mode 100644 packages/ocap-kernel/src/rpc/kernel/index.ts delete mode 100644 packages/omnium-gatherum/src/captp/index.ts delete mode 100644 packages/omnium-gatherum/src/env/dev-console.js delete mode 100644 packages/omnium-gatherum/src/env/dev-console.test.ts diff --git a/packages/extension/package.json b/packages/extension/package.json index b2ce2a876..3ef92a0f2 100644 --- a/packages/extension/package.json +++ b/packages/extension/package.json @@ -42,15 +42,13 @@ "test:e2e:debug": "playwright test --debug" }, "dependencies": { + "@endo/eventual-send": "^1.3.4", "@metamask/kernel-browser-runtime": "workspace:^", - "@metamask/kernel-rpc-methods": "workspace:^", "@metamask/kernel-shims": "workspace:^", "@metamask/kernel-ui": "workspace:^", "@metamask/kernel-utils": "workspace:^", "@metamask/logger": "workspace:^", - "@metamask/ocap-kernel": "workspace:^", "@metamask/streams": "workspace:^", - "@metamask/utils": "^11.9.0", "react": "^17.0.2", "react-dom": "^17.0.2", "ses": "^1.14.0" diff --git a/packages/extension/scripts/build-constants.mjs b/packages/extension/scripts/build-constants.mjs index 2954c8f7c..8d91c97c0 100644 --- a/packages/extension/scripts/build-constants.mjs +++ b/packages/extension/scripts/build-constants.mjs @@ -18,7 +18,7 @@ export const kernelBrowserRuntimeSrcDir = path.resolve( */ export const trustedPreludes = { background: { - path: path.resolve(sourceDir, 'env/background-trusted-prelude.js'), + content: "import './endoify.js';", }, 'kernel-worker': { content: "import './endoify.js';" }, }; diff --git a/packages/extension/src/background.ts b/packages/extension/src/background.ts index de4fabca5..b4e6d5a2f 100644 --- a/packages/extension/src/background.ts +++ b/packages/extension/src/background.ts @@ -1,16 +1,21 @@ +import { E } from '@endo/eventual-send'; import { - connectToKernel, - rpcMethodSpecs, + makeBackgroundCapTP, + makeCapTPNotification, + isCapTPNotification, + getCapTPMessage, +} from '@metamask/kernel-browser-runtime'; +import type { + KernelFacade, + CapTPMessage, } from '@metamask/kernel-browser-runtime'; import defaultSubcluster from '@metamask/kernel-browser-runtime/default-cluster'; -import { RpcClient } from '@metamask/kernel-rpc-methods'; -import { delay } from '@metamask/kernel-utils'; -import type { JsonRpcCall } from '@metamask/kernel-utils'; +import { delay, isJsonRpcMessage } from '@metamask/kernel-utils'; +import type { JsonRpcMessage } from '@metamask/kernel-utils'; import { Logger } from '@metamask/logger'; -import { kernelMethodSpecs } from '@metamask/ocap-kernel/rpc'; import { ChromeRuntimeDuplexStream } from '@metamask/streams/browser'; -import { isJsonRpcResponse } from '@metamask/utils'; -import type { JsonRpcResponse } from '@metamask/utils'; + +defineGlobals(); const OFFSCREEN_DOCUMENT_PATH = '/offscreen.html'; const logger = new Logger('background'); @@ -79,32 +84,42 @@ async function main(): Promise { // Without this delay, sending messages via the chrome.runtime API can fail. await delay(50); + // Create stream for CapTP messages const offscreenStream = await ChromeRuntimeDuplexStream.make< - JsonRpcResponse, - JsonRpcCall - >(chrome.runtime, 'background', 'offscreen', isJsonRpcResponse); - - const rpcClient = new RpcClient( - kernelMethodSpecs, - async (request) => { - await offscreenStream.write(request); + JsonRpcMessage, + JsonRpcMessage + >(chrome.runtime, 'background', 'offscreen', isJsonRpcMessage); + + // Set up CapTP for E() based communication with the kernel + const backgroundCapTP = makeBackgroundCapTP({ + send: (captpMessage: CapTPMessage) => { + const notification = makeCapTPNotification(captpMessage); + offscreenStream.write(notification).catch((error) => { + logger.error('Failed to send CapTP message:', error); + }); }, - 'background:', - ); + }); + + // Get the kernel remote presence + const kernelPromise = backgroundCapTP.getKernel(); const ping = async (): Promise => { - const result = await rpcClient.call('ping', []); + const kernel = await kernelPromise; + const result = await E(kernel).ping(); logger.info(result); }; - // globalThis.kernel will exist due to dev-console.js in background-trusted-prelude.js + // Helper to get the kernel remote presence (for use with E()) + const getKernel = async (): Promise => { + return kernelPromise; + }; + Object.defineProperties(globalThis.kernel, { ping: { value: ping, }, - sendMessage: { - value: async (message: JsonRpcCall) => - await offscreenStream.write(message), + getKernel: { + value: getKernel, }, }); harden(globalThis.kernel); @@ -114,14 +129,17 @@ async function main(): Promise { ping().catch(logger.error); }); - // Pipe responses back to the RpcClient - const drainPromise = offscreenStream.drain(async (message) => - rpcClient.handleResponse(message.id as string, message), - ); + // Handle incoming CapTP messages from the kernel + const drainPromise = offscreenStream.drain((message) => { + if (isCapTPNotification(message)) { + const captpMessage = getCapTPMessage(message); + backgroundCapTP.dispatch(captpMessage); + } + }); drainPromise.catch(logger.error); await ping(); // Wait for the kernel to be ready - await startDefaultSubcluster(); + await startDefaultSubcluster(kernelPromise); try { await drainPromise; @@ -134,30 +152,38 @@ async function main(): Promise { /** * Idempotently starts the default subcluster. + * + * @param kernelPromise - Promise for the kernel facade. */ -async function startDefaultSubcluster(): Promise { - const kernelStream = await connectToKernel({ label: 'background', logger }); - const rpcClient = new RpcClient( - rpcMethodSpecs, - async (request) => { - await kernelStream.write(request); - }, - 'background', - ); +async function startDefaultSubcluster( + kernelPromise: Promise, +): Promise { + const kernel = await kernelPromise; + const status = await E(kernel).getStatus(); - kernelStream - .drain(async (message) => - rpcClient.handleResponse(message.id as string, message), - ) - .catch(logger.error); - - const status = await rpcClient.call('getStatus', []); if (status.subclusters.length === 0) { - const result = await rpcClient.call('launchSubcluster', { - config: defaultSubcluster, - }); + const result = await E(kernel).launchSubcluster(defaultSubcluster); logger.info(`Default subcluster launched: ${JSON.stringify(result)}`); } else { logger.info('Subclusters already exist. Not launching default subcluster.'); } } + +/** + * Define globals accessible via the background console. + */ +function defineGlobals(): void { + Object.defineProperty(globalThis, 'kernel', { + configurable: false, + enumerable: true, + writable: false, + value: {}, + }); + + Object.defineProperty(globalThis, 'E', { + value: E, + configurable: false, + enumerable: true, + writable: false, + }); +} diff --git a/packages/extension/src/env/background-trusted-prelude.js b/packages/extension/src/env/background-trusted-prelude.js deleted file mode 100644 index d026032b6..000000000 --- a/packages/extension/src/env/background-trusted-prelude.js +++ /dev/null @@ -1,3 +0,0 @@ -// eslint-disable-next-line import-x/no-unresolved -import './endoify.js'; -import './dev-console.js'; diff --git a/packages/extension/src/env/dev-console.js b/packages/extension/src/env/dev-console.js deleted file mode 100644 index c91e8e197..000000000 --- a/packages/extension/src/env/dev-console.js +++ /dev/null @@ -1,9 +0,0 @@ -// We set this property on globalThis in the background before lockdown. -Object.defineProperty(globalThis, 'kernel', { - configurable: false, - enumerable: true, - writable: false, - value: {}, -}); - -export {}; diff --git a/packages/extension/src/env/dev-console.test.ts b/packages/extension/src/env/dev-console.test.ts deleted file mode 100644 index e086ecda8..000000000 --- a/packages/extension/src/env/dev-console.test.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import './dev-console.js'; - -describe('vat-console', () => { - describe('kernel', () => { - it('is available on globalThis', async () => { - expect(kernel).toBeDefined(); - }); - - it('has expected property descriptors', async () => { - expect( - Object.getOwnPropertyDescriptor(globalThis, 'kernel'), - ).toMatchObject({ - configurable: false, - enumerable: true, - writable: false, - }); - }); - }); -}); diff --git a/packages/extension/src/global.d.ts b/packages/extension/src/global.d.ts new file mode 100644 index 000000000..06dd91196 --- /dev/null +++ b/packages/extension/src/global.d.ts @@ -0,0 +1,39 @@ +import type { KernelFacade } from '@metamask/kernel-browser-runtime'; + +// Type declarations for kernel dev console API. +declare global { + /** + * The E() function from @endo/eventual-send for making eventual sends. + * Set globally in the trusted prelude before lockdown. + * + * @example + * ```typescript + * const kernel = await kernel.getKernel(); + * const status = await E(kernel).getStatus(); + * ``` + */ + // eslint-disable-next-line no-var,id-length + 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; + }; +} + +export {}; diff --git a/packages/extension/src/offscreen.ts b/packages/extension/src/offscreen.ts index 0f0e2dcef..c09ec2772 100644 --- a/packages/extension/src/offscreen.ts +++ b/packages/extension/src/offscreen.ts @@ -3,8 +3,8 @@ import { PlatformServicesServer, createRelayQueryString, } from '@metamask/kernel-browser-runtime'; -import { delay, isJsonRpcCall } from '@metamask/kernel-utils'; -import type { JsonRpcCall } from '@metamask/kernel-utils'; +import { delay, isJsonRpcMessage } from '@metamask/kernel-utils'; +import type { JsonRpcMessage } from '@metamask/kernel-utils'; import { Logger } from '@metamask/logger'; import type { DuplexStream } from '@metamask/streams'; import { @@ -13,8 +13,6 @@ import { MessagePortDuplexStream, } from '@metamask/streams/browser'; import type { PostMessageTarget } from '@metamask/streams/browser'; -import type { JsonRpcResponse } from '@metamask/utils'; -import { isJsonRpcResponse } from '@metamask/utils'; const logger = new Logger('offscreen'); @@ -27,11 +25,11 @@ async function main(): Promise { // Without this delay, sending messages via the chrome.runtime API can fail. await delay(50); - // Create stream for messages from the background script + // Create stream for CapTP messages from the background script const backgroundStream = await ChromeRuntimeDuplexStream.make< - JsonRpcCall, - JsonRpcResponse - >(chrome.runtime, 'offscreen', 'background', isJsonRpcCall); + JsonRpcMessage, + JsonRpcMessage + >(chrome.runtime, 'offscreen', 'background', isJsonRpcMessage); const kernelStream = await makeKernelWorker(); @@ -48,7 +46,7 @@ async function main(): Promise { * @returns The message port stream for worker communication */ async function makeKernelWorker(): Promise< - DuplexStream + DuplexStream > { // Assign local relay address generated from `yarn ocap relay` const relayQueryString = createRelayQueryString([ @@ -72,9 +70,9 @@ async function makeKernelWorker(): Promise< ); const kernelStream = await MessagePortDuplexStream.make< - JsonRpcResponse, - JsonRpcCall - >(port, isJsonRpcResponse); + JsonRpcMessage, + JsonRpcMessage + >(port, isJsonRpcMessage); await PlatformServicesServer.make(worker as PostMessageTarget, (vatId) => makeIframeVatWorker({ diff --git a/packages/extension/test/build/build-tests.ts b/packages/extension/test/build/build-tests.ts index fcd0aedd8..f293c5fe8 100644 --- a/packages/extension/test/build/build-tests.ts +++ b/packages/extension/test/build/build-tests.ts @@ -2,21 +2,13 @@ import { runTests } from '@ocap/repo-tools/build-utils/test'; import type { UntransformedFiles } from '@ocap/repo-tools/build-utils/test'; import path from 'node:path'; -import { - outDir, - sourceDir, - trustedPreludes, -} from '../../scripts/build-constants.mjs'; +import { outDir, trustedPreludes } from '../../scripts/build-constants.mjs'; const untransformedFiles = [ { sourcePath: path.resolve('../kernel-shims/dist/endoify.js'), buildPath: path.resolve(outDir, 'endoify.js'), }, - { - sourcePath: path.resolve(sourceDir, 'env/dev-console.js'), - buildPath: path.resolve(outDir, 'dev-console.js'), - }, ...Object.values(trustedPreludes).map((prelude) => { if ('path' in prelude) { return { diff --git a/packages/extension/tsconfig.build.json b/packages/extension/tsconfig.build.json index 8da52bd25..d7b547202 100644 --- a/packages/extension/tsconfig.build.json +++ b/packages/extension/tsconfig.build.json @@ -21,10 +21,5 @@ { "path": "../ocap-kernel/tsconfig.build.json" }, { "path": "../streams/tsconfig.build.json" } ], - "include": [ - "./src/**/*.ts", - "./src/**/*.tsx", - "./src/**/*-trusted-prelude.js", - "./src/env/dev-console.js" - ] + "include": ["./src/**/*.ts", "./src/**/*.tsx"] } diff --git a/packages/extension/tsconfig.json b/packages/extension/tsconfig.json index bd2e0aef6..e2d7cddd2 100644 --- a/packages/extension/tsconfig.json +++ b/packages/extension/tsconfig.json @@ -28,8 +28,6 @@ "./playwright.config.ts", "./src/**/*.ts", "./src/**/*.tsx", - "./src/**/*-trusted-prelude.js", - "./src/env/dev-console.js", "./test/**/*.ts", "./vite.config.ts", "./vitest.config.ts" diff --git a/packages/extension/vite.config.ts b/packages/extension/vite.config.ts index fc7482636..91ed7d421 100644 --- a/packages/extension/vite.config.ts +++ b/packages/extension/vite.config.ts @@ -35,8 +35,6 @@ const staticCopyTargets: readonly (string | Target)[] = [ // The extension manifest 'packages/extension/src/manifest.json', // Trusted prelude-related - 'packages/extension/src/env/dev-console.js', - 'packages/extension/src/env/background-trusted-prelude.js', 'packages/kernel-shims/dist/endoify.js', ]; diff --git a/packages/kernel-browser-runtime/package.json b/packages/kernel-browser-runtime/package.json index c91f90901..6cfe42cef 100644 --- a/packages/kernel-browser-runtime/package.json +++ b/packages/kernel-browser-runtime/package.json @@ -65,7 +65,6 @@ "dependencies": { "@endo/captp": "^4.4.8", "@endo/marshal": "^1.8.0", - "@endo/promise-kit": "^1.1.13", "@metamask/json-rpc-engine": "^10.2.0", "@metamask/kernel-errors": "workspace:^", "@metamask/kernel-rpc-methods": "workspace:^", diff --git a/packages/omnium-gatherum/src/captp/background-captp.ts b/packages/kernel-browser-runtime/src/background-captp.ts similarity index 83% rename from packages/omnium-gatherum/src/captp/background-captp.ts rename to packages/kernel-browser-runtime/src/background-captp.ts index 44d6af284..d6692e3b5 100644 --- a/packages/omnium-gatherum/src/captp/background-captp.ts +++ b/packages/kernel-browser-runtime/src/background-captp.ts @@ -1,12 +1,21 @@ import { makeCapTP } from '@endo/captp'; -import type { KernelFacade } from '@metamask/kernel-browser-runtime'; import type { JsonRpcMessage, JsonRpcCall } from '@metamask/kernel-utils'; -import { hasProperty } from '@metamask/utils'; +import type { Json, JsonRpcNotification } from '@metamask/utils'; + +import type { KernelFacade } from './types.ts'; /** * A CapTP message that can be sent over the wire. */ -export type CapTPMessage = Record; +export type CapTPMessage = Record; + +/** + * A CapTP JSON-RPC notification. + */ +export type CapTPNotification = JsonRpcNotification & { + method: 'captp'; + params: [CapTPMessage]; +}; /** * Check if a message is a CapTP JSON-RPC notification. @@ -14,14 +23,11 @@ export type CapTPMessage = Record; * @param message - The message to check. * @returns True if the message is a CapTP notification. */ -export function isCapTPNotification(message: JsonRpcMessage): boolean { - return ( - hasProperty(message, 'method') && - message.method === 'captp' && - hasProperty(message, 'params') && - Array.isArray(message.params) && - message.params.length === 1 - ); +export function isCapTPNotification( + message: JsonRpcMessage, +): message is CapTPNotification { + const { method, params } = message as JsonRpcCall; + return method === 'captp' && Array.isArray(params) && params.length === 1; } /** diff --git a/packages/kernel-browser-runtime/src/index.test.ts b/packages/kernel-browser-runtime/src/index.test.ts index a564a7a53..f52b98667 100644 --- a/packages/kernel-browser-runtime/src/index.test.ts +++ b/packages/kernel-browser-runtime/src/index.test.ts @@ -9,7 +9,11 @@ describe('index', () => { 'PlatformServicesServer', 'connectToKernel', 'createRelayQueryString', + 'getCapTPMessage', 'getRelaysFromCurrentLocation', + 'isCapTPNotification', + 'makeBackgroundCapTP', + 'makeCapTPNotification', 'makeIframeVatWorker', 'parseRelayQueryString', 'receiveInternalConnections', diff --git a/packages/kernel-browser-runtime/src/index.ts b/packages/kernel-browser-runtime/src/index.ts index 3d2343079..4c10590e3 100644 --- a/packages/kernel-browser-runtime/src/index.ts +++ b/packages/kernel-browser-runtime/src/index.ts @@ -12,3 +12,12 @@ export * from './PlatformServicesClient.ts'; export * from './PlatformServicesServer.ts'; export * from './utils/index.ts'; export type { KernelFacade } from './types.ts'; +export { + makeBackgroundCapTP, + isCapTPNotification, + getCapTPMessage, + makeCapTPNotification, + type BackgroundCapTP, + type BackgroundCapTPOptions, + type CapTPMessage, +} from './background-captp.ts'; diff --git a/packages/kernel-browser-runtime/src/kernel-worker/captp/index.ts b/packages/kernel-browser-runtime/src/kernel-worker/captp/index.ts index 8b60b9d8a..6e3ee7053 100644 --- a/packages/kernel-browser-runtime/src/kernel-worker/captp/index.ts +++ b/packages/kernel-browser-runtime/src/kernel-worker/captp/index.ts @@ -2,14 +2,6 @@ export { makeKernelCapTP, type KernelCapTP, type KernelCapTPOptions, - type CapTPMessage, } from './kernel-captp.ts'; export { makeKernelFacade, type KernelFacade } from './kernel-facade.ts'; - -export { - makeMessageRouter, - makeCapTPNotification, - isCapTPNotification, - type MessageRouter, -} from './message-router.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 d13e7ec77..199147980 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 @@ -13,6 +13,8 @@ export type { KernelFacade } from '../../types.ts'; */ export function makeKernelFacade(kernel: Kernel): KernelFacade { return makeDefaultExo('KernelFacade', { + ping: async () => 'pong' as const, + launchSubcluster: async (config: ClusterConfig) => { return kernel.launchSubcluster(config); }, diff --git a/packages/kernel-browser-runtime/src/kernel-worker/captp/message-router.ts b/packages/kernel-browser-runtime/src/kernel-worker/captp/message-router.ts deleted file mode 100644 index b0a7ce653..000000000 --- a/packages/kernel-browser-runtime/src/kernel-worker/captp/message-router.ts +++ /dev/null @@ -1,223 +0,0 @@ -import type { PromiseKit } from '@endo/promise-kit'; -import { makePromiseKit } from '@endo/promise-kit'; -import type { JsonRpcCall, JsonRpcMessage } from '@metamask/kernel-utils'; -import type { DuplexStream } from '@metamask/streams'; -import { hasProperty } from '@metamask/utils'; -import type { JsonRpcResponse } from '@metamask/utils'; - -import type { CapTPMessage } from './kernel-captp.ts'; - -/** - * Check if a message is a CapTP JSON-RPC notification. - * - * @param message - The message to check. - * @returns True if the message is a CapTP notification. - */ -export function isCapTPNotification( - message: JsonRpcMessage, -): message is JsonRpcCall & { method: 'captp'; params: [CapTPMessage] } { - const { method, params } = message as JsonRpcCall; - return method === 'captp' && Array.isArray(params) && params.length === 1; -} - -/** - * Create a CapTP JSON-RPC notification. - * - * @param captpMessage - The CapTP message to wrap. - * @returns The JSON-RPC notification. - */ -export function makeCapTPNotification(captpMessage: CapTPMessage): JsonRpcCall { - return { - jsonrpc: '2.0', - method: 'captp', - params: [captpMessage], - }; -} - -/** - * A queue for messages, allowing async iteration. - */ -class MessageQueue implements AsyncIterable { - readonly #queue: Item[] = []; - - #waitingKit: PromiseKit | null = null; - - #done = false; - - push(value: Item): void { - if (this.#done) { - return; - } - this.#queue.push(value); - if (this.#waitingKit) { - this.#waitingKit.resolve(); - this.#waitingKit = null; - } - } - - end(): void { - this.#done = true; - if (this.#waitingKit) { - this.#waitingKit.resolve(); - this.#waitingKit = null; - } - } - - async *[Symbol.asyncIterator](): AsyncIterator { - while (!this.#done || this.#queue.length > 0) { - if (this.#queue.length === 0) { - if (this.#done) { - return; - } - this.#waitingKit = makePromiseKit(); - await this.#waitingKit.promise; - continue; - } - yield this.#queue.shift() as Item; - } - } -} - -/** - * A stream wrapper that routes messages between kernel RPC and CapTP. - * - * Incoming messages: - * - CapTP notifications (method: 'captp') are dispatched to the CapTP handler - * - Other messages are passed to the kernel stream - * - * Outgoing messages: - * - Kernel responses are written to the underlying stream - * - CapTP messages are wrapped in notifications and written to the underlying stream - */ -export type MessageRouter = { - /** - * The stream for the kernel to use. Only sees non-CapTP messages. - */ - kernelStream: DuplexStream; - - /** - * Set the CapTP dispatch function for incoming CapTP messages. - * - * @param dispatch - The dispatch function. - */ - setCapTPDispatch: (dispatch: (message: CapTPMessage) => boolean) => void; - - /** - * Send a CapTP message to the background. - * - * @param message - The CapTP message to send. - */ - sendCapTP: (message: CapTPMessage) => void; - - /** - * Start routing messages. Returns a promise that resolves when the - * underlying stream ends. - */ - start: () => Promise; -}; - -/** - * Create a message router. - * - * @param underlyingStream - The underlying bidirectional message stream. - * @returns The message router. - */ -export function makeMessageRouter( - underlyingStream: DuplexStream, -): MessageRouter { - const kernelMessageQueue = new MessageQueue(); - let captpDispatch: ((message: CapTPMessage) => boolean) | null = null; - - // Create a stream interface for the kernel - const kernelStream: DuplexStream = { - async next() { - const iterator = kernelMessageQueue[Symbol.asyncIterator](); - const result = await iterator.next(); - return result.done - ? { done: true, value: undefined } - : { done: false, value: result.value }; - }, - - async write(value: JsonRpcResponse) { - await underlyingStream.write(value); - return { done: false, value: undefined }; - }, - - async drain(handler: (value: JsonRpcCall) => void | Promise) { - for await (const value of kernelMessageQueue) { - await handler(value); - } - }, - - async pipe(sink: DuplexStream) { - await this.drain(async (value) => { - await sink.write(value); - }); - }, - - async return() { - kernelMessageQueue.end(); - return { done: true, value: undefined }; - }, - - async throw(_error: Error) { - kernelMessageQueue.end(); - return { done: true, value: undefined }; - }, - - async end(error?: Error) { - return error ? this.throw(error) : this.return(); - }, - - [Symbol.asyncIterator]() { - return this; - }, - }; - - const setCapTPDispatch = ( - dispatch: (message: CapTPMessage) => boolean, - ): void => { - if (captpDispatch) { - throw new Error('CapTP dispatch already set'); - } - captpDispatch = dispatch; - }; - - const sendCapTP = (message: CapTPMessage): void => { - const notification = makeCapTPNotification(message); - underlyingStream.write(notification).catch(() => { - // Ignore write errors - the stream may have closed - }); - }; - - const start = async (): Promise => { - try { - await underlyingStream.drain((message) => { - if (isCapTPNotification(message)) { - // Dispatch to CapTP - const captpMessage = message.params[0]; - if (captpDispatch) { - captpDispatch(captpMessage); - } - } else if ( - hasProperty(message, 'method') && - typeof message.method === 'string' - ) { - // Pass to kernel as JsonRpcCall - kernelMessageQueue.push(message as JsonRpcCall); - } - // Ignore other message types (e.g., responses that shouldn't come this way) - }); - } finally { - kernelMessageQueue.end(); - } - }; - - return harden({ - kernelStream, - setCapTPDispatch, - sendCapTP, - start, - }); -} -harden(makeMessageRouter); diff --git a/packages/kernel-browser-runtime/src/kernel-worker/kernel-worker.ts b/packages/kernel-browser-runtime/src/kernel-worker/kernel-worker.ts index d0d248999..b480093c1 100644 --- a/packages/kernel-browser-runtime/src/kernel-worker/kernel-worker.ts +++ b/packages/kernel-browser-runtime/src/kernel-worker/kernel-worker.ts @@ -10,13 +10,17 @@ import { receiveMessagePort, } from '@metamask/streams/browser'; -import { makeKernelCapTP } from './captp/index.ts'; -import { makeMessageRouter } from './captp/message-router.ts'; +import { + isCapTPNotification, + makeCapTPNotification, +} from '../background-captp.ts'; +import type { CapTPMessage } from '../background-captp.ts'; import { receiveInternalConnections } from '../internal-comms/internal-connections.ts'; import { PlatformServicesClient } from '../PlatformServicesClient.ts'; -import { getRelaysFromCurrentLocation } from '../utils/relay-query-string.ts'; +import { makeKernelCapTP } from './captp/index.ts'; import { makeLoggingMiddleware } from './middleware/logging.ts'; import { makePanelMessageMiddleware } from './middleware/panel-message.ts'; +import { getRelaysFromCurrentLocation } from '../utils/relay-query-string.ts'; const logger = new Logger('kernel-worker'); const DB_FILENAME = 'store.db'; @@ -32,13 +36,13 @@ async function main(): Promise { (listener) => globalThis.removeEventListener('message', listener), ); - // Initialize other kernel dependencies - const [messageRouter, platformServicesClient, kernelDatabase] = + // Initialize kernel dependencies + const [messageStream, platformServicesClient, kernelDatabase] = await Promise.all([ MessagePortDuplexStream.make( port, isJsonRpcMessage, - ).then((stream) => makeMessageRouter(stream)), + ), PlatformServicesClient.make(globalThis as PostMessageTarget), makeSQLKernelDatabase({ dbFilename: DB_FILENAME }), ]); @@ -47,23 +51,19 @@ async function main(): Promise { new URLSearchParams(globalThis.location.search).get('reset-storage') === 'true'; - // Create kernel with the filtered stream (only sees non-CapTP messages) - const kernelP = Kernel.make( - messageRouter.kernelStream, - platformServicesClient, - kernelDatabase, - { - resetStorage, - }, - ); + const kernelP = Kernel.make(platformServicesClient, kernelDatabase, { + resetStorage, + }); + + // Set up internal RPC server for UI panel connections (uses separate MessagePorts) const handlerP = kernelP.then((kernel) => { const server = new JsonRpcServer({ middleware: [ - makeLoggingMiddleware(logger.subLogger('kernel-command')), + makeLoggingMiddleware(logger.subLogger('internal-rpc')), makePanelMessageMiddleware(kernel, kernelDatabase), ], }); - return async (request: JsonRpcCall) => server.handle(request); + return async (request: JsonRpcMessage) => server.handle(request); }); receiveInternalConnections({ @@ -76,14 +76,25 @@ async function main(): Promise { // Set up CapTP for background ↔ kernel communication const kernelCapTP = makeKernelCapTP({ kernel, - send: messageRouter.sendCapTP, + send: (captpMessage: CapTPMessage) => { + const notification = makeCapTPNotification(captpMessage); + messageStream.write(notification).catch((error) => { + logger.error('Failed to send CapTP message:', error); + }); + }, }); - messageRouter.setCapTPDispatch(kernelCapTP.dispatch); - // Start the message router (routes incoming messages to kernel or CapTP) - messageRouter.start().catch((error) => { - logger.error('Message router error:', error); - }); + // Handle incoming CapTP messages from the background + messageStream + .drain((message) => { + if (isCapTPNotification(message)) { + const captpMessage = message.params[0]; + kernelCapTP.dispatch(captpMessage); + } + }) + .catch((error) => { + logger.error('Message stream error:', error); + }); // Initialize remote communications with the relay server passed in the query string const relays = getRelaysFromCurrentLocation(); diff --git a/packages/kernel-browser-runtime/src/types.ts b/packages/kernel-browser-runtime/src/types.ts index cb5924307..967abc71a 100644 --- a/packages/kernel-browser-runtime/src/types.ts +++ b/packages/kernel-browser-runtime/src/types.ts @@ -6,6 +6,7 @@ import type { Kernel } from '@metamask/ocap-kernel'; * This is the remote presence type that the background receives from the kernel. */ export type KernelFacade = { + ping: () => Promise<'pong'>; launchSubcluster: Kernel['launchSubcluster']; terminateSubcluster: Kernel['terminateSubcluster']; queueMessage: Kernel['queueMessage']; diff --git a/packages/kernel-test/package.json b/packages/kernel-test/package.json index 12138472c..5eb71fbac 100644 --- a/packages/kernel-test/package.json +++ b/packages/kernel-test/package.json @@ -57,8 +57,6 @@ "@metamask/kernel-utils": "workspace:^", "@metamask/logger": "workspace:^", "@metamask/ocap-kernel": "workspace:^", - "@metamask/streams": "workspace:^", - "@metamask/utils": "^11.9.0", "@ocap/kernel-language-model-service": "workspace:^", "@ocap/nodejs": "workspace:^", "@ocap/nodejs-test-workers": "workspace:^", diff --git a/packages/kernel-test/src/utils.ts b/packages/kernel-test/src/utils.ts index 361ad2cdb..441cb7e77 100644 --- a/packages/kernel-test/src/utils.ts +++ b/packages/kernel-test/src/utils.ts @@ -11,13 +11,7 @@ import { import type { LogEntry } from '@metamask/logger'; import { Kernel, kunser } from '@metamask/ocap-kernel'; import type { ClusterConfig, PlatformServices } from '@metamask/ocap-kernel'; -import { NodeWorkerDuplexStream } from '@metamask/streams'; -import type { JsonRpcRequest, JsonRpcResponse } from '@metamask/utils'; import { NodejsPlatformServices } from '@ocap/nodejs'; -import { - MessagePort as NodeMessagePort, - MessageChannel as NodeMessageChannel, -} from 'node:worker_threads'; import { vi } from 'vitest'; /** @@ -87,11 +81,6 @@ export async function makeKernel( platformServices?: PlatformServices, keySeed?: string, ): Promise { - const kernelPort: NodeMessagePort = new NodeMessageChannel().port1; - const nodeStream = new NodeWorkerDuplexStream< - JsonRpcRequest, - JsonRpcResponse - >(kernelPort); const platformServicesConfig: { logger: Logger; workerFilePath?: string } = { logger: logger.subLogger({ tags: ['vat-worker-manager'] }), }; @@ -100,16 +89,11 @@ export async function makeKernel( } const platformServicesClient = platformServices ?? new NodejsPlatformServices(platformServicesConfig); - const kernel = await Kernel.make( - nodeStream, - platformServicesClient, - kernelDatabase, - { - resetStorage, - logger, - keySeed, - }, - ); + const kernel = await Kernel.make(platformServicesClient, kernelDatabase, { + resetStorage, + logger, + keySeed, + }); return kernel; } diff --git a/packages/nodejs/package.json b/packages/nodejs/package.json index 1dfd8e283..ec7cebc4a 100644 --- a/packages/nodejs/package.json +++ b/packages/nodejs/package.json @@ -61,7 +61,6 @@ "@metamask/logger": "workspace:^", "@metamask/ocap-kernel": "workspace:^", "@metamask/streams": "workspace:^", - "@metamask/utils": "^11.9.0", "@ocap/kernel-platforms": "workspace:^", "ses": "^1.14.0" }, diff --git a/packages/nodejs/src/kernel/make-kernel.test.ts b/packages/nodejs/src/kernel/make-kernel.test.ts index 35b2f6689..b54e57ef7 100644 --- a/packages/nodejs/src/kernel/make-kernel.test.ts +++ b/packages/nodejs/src/kernel/make-kernel.test.ts @@ -1,11 +1,7 @@ import '../env/endoify.ts'; import { Kernel } from '@metamask/ocap-kernel'; -import { - MessagePort as NodeMessagePort, - MessageChannel as NodeMessageChannel, -} from 'node:worker_threads'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { describe, expect, it, vi } from 'vitest'; import { makeKernel } from './make-kernel.ts'; @@ -19,16 +15,8 @@ vi.mock('@metamask/kernel-store/sqlite/nodejs', async () => { }); describe('makeKernel', () => { - let kernelPort: NodeMessagePort; - - beforeEach(() => { - kernelPort = new NodeMessageChannel().port1; - }); - it('should return a Kernel', async () => { - const kernel = await makeKernel({ - port: kernelPort, - }); + const kernel = await makeKernel({}); expect(kernel).toBeInstanceOf(Kernel); }); diff --git a/packages/nodejs/src/kernel/make-kernel.ts b/packages/nodejs/src/kernel/make-kernel.ts index 66af358ee..a359c35a9 100644 --- a/packages/nodejs/src/kernel/make-kernel.ts +++ b/packages/nodejs/src/kernel/make-kernel.ts @@ -1,9 +1,6 @@ import { makeSQLKernelDatabase } from '@metamask/kernel-store/sqlite/nodejs'; import { Logger } from '@metamask/logger'; import { Kernel } from '@metamask/ocap-kernel'; -import { NodeWorkerDuplexStream } from '@metamask/streams'; -import type { JsonRpcRequest, JsonRpcResponse } from '@metamask/utils'; -import { MessagePort as NodeMessagePort } from 'node:worker_threads'; import { NodejsPlatformServices } from './PlatformServices.ts'; @@ -11,7 +8,6 @@ import { NodejsPlatformServices } from './PlatformServices.ts'; * The main function for the kernel worker. * * @param options - The options for the kernel. - * @param options.port - The kernel's end of a node:worker_threads MessageChannel * @param options.workerFilePath - The path to a file defining each vat worker's routine. * @param options.resetStorage - If true, clear kernel storage as part of setting up the kernel. * @param options.dbFilename - The filename of the SQLite database file. @@ -20,24 +16,18 @@ import { NodejsPlatformServices } from './PlatformServices.ts'; * @returns The kernel, initialized. */ export async function makeKernel({ - port, workerFilePath, resetStorage = false, dbFilename, logger, keySeed, }: { - port: NodeMessagePort; workerFilePath?: string; resetStorage?: boolean; dbFilename?: string; logger?: Logger; keySeed?: string | undefined; }): Promise { - const nodeStream = new NodeWorkerDuplexStream< - JsonRpcRequest, - JsonRpcResponse - >(port); const rootLogger = logger ?? new Logger('kernel-worker'); const platformServicesClient = new NodejsPlatformServices({ workerFilePath, @@ -48,16 +38,11 @@ export async function makeKernel({ const kernelDatabase = await makeSQLKernelDatabase({ dbFilename }); // Create and start kernel. - const kernel = await Kernel.make( - nodeStream, - platformServicesClient, - kernelDatabase, - { - resetStorage, - logger: rootLogger.subLogger({ tags: ['kernel'] }), - keySeed, - }, - ); + const kernel = await Kernel.make(platformServicesClient, kernelDatabase, { + resetStorage, + logger: rootLogger.subLogger({ tags: ['kernel'] }), + keySeed, + }); return kernel; } diff --git a/packages/nodejs/test/e2e/kernel-worker.test.ts b/packages/nodejs/test/e2e/kernel-worker.test.ts index 2275c07cd..ba61e57cc 100644 --- a/packages/nodejs/test/e2e/kernel-worker.test.ts +++ b/packages/nodejs/test/e2e/kernel-worker.test.ts @@ -2,10 +2,6 @@ import '../../src/env/endoify.ts'; import { Kernel } from '@metamask/ocap-kernel'; import type { ClusterConfig } from '@metamask/ocap-kernel'; -import { - MessageChannel as NodeMessageChannel, - MessagePort as NodePort, -} from 'node:worker_threads'; import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { makeKernel } from '../../src/kernel/make-kernel.ts'; @@ -17,20 +13,13 @@ vi.mock('node:process', () => ({ })); describe('Kernel Worker', () => { - let kernelPort: NodePort; let kernel: Kernel; // Tests below assume these are sorted for convenience. const testVatIds = ['v1', 'v2', 'v3'].sort(); beforeEach(async () => { - if (kernelPort) { - kernelPort.close(); - } - kernelPort = new NodeMessageChannel().port1; - kernel = await makeKernel({ - port: kernelPort, - }); + kernel = await makeKernel({}); }); afterEach(async () => { diff --git a/packages/nodejs/test/helpers/kernel.ts b/packages/nodejs/test/helpers/kernel.ts index c902d64f7..7fede0d50 100644 --- a/packages/nodejs/test/helpers/kernel.ts +++ b/packages/nodejs/test/helpers/kernel.ts @@ -3,9 +3,6 @@ import { waitUntilQuiescent } from '@metamask/kernel-utils'; import { Logger } from '@metamask/logger'; import { Kernel, kunser } from '@metamask/ocap-kernel'; import type { ClusterConfig } from '@metamask/ocap-kernel'; -import { NodeWorkerDuplexStream } from '@metamask/streams'; -import type { JsonRpcRequest, JsonRpcResponse } from '@metamask/utils'; -import { MessageChannel as NodeMessageChannel } from 'node:worker_threads'; import { NodejsPlatformServices } from '../../src/kernel/PlatformServices.ts'; @@ -21,24 +18,14 @@ export async function makeTestKernel( kernelDatabase: KernelDatabase, resetStorage: boolean, ): Promise { - const port = new NodeMessageChannel().port1; - const nodeStream = new NodeWorkerDuplexStream< - JsonRpcRequest, - JsonRpcResponse - >(port); const logger = new Logger('test-kernel'); const platformServices = new NodejsPlatformServices({ logger: logger.subLogger({ tags: ['platform-services'] }), }); - const kernel = await Kernel.make( - nodeStream, - platformServices, - kernelDatabase, - { - resetStorage, - logger: logger.subLogger({ tags: ['kernel'] }), - }, - ); + const kernel = await Kernel.make(platformServices, kernelDatabase, { + resetStorage, + logger: logger.subLogger({ tags: ['kernel'] }), + }); return kernel; } diff --git a/packages/ocap-kernel/src/Kernel.test.ts b/packages/ocap-kernel/src/Kernel.test.ts index 6a2a18de6..6c7ae274f 100644 --- a/packages/ocap-kernel/src/Kernel.test.ts +++ b/packages/ocap-kernel/src/Kernel.test.ts @@ -3,8 +3,6 @@ import type { KernelDatabase } from '@metamask/kernel-store'; import type { JsonRpcMessage } from '@metamask/kernel-utils'; import { Logger } from '@metamask/logger'; import type { DuplexStream } from '@metamask/streams'; -import type { JsonRpcResponse, JsonRpcRequest } from '@metamask/utils'; -import { TestDuplexStream } from '@ocap/repo-tools/test-utils/streams'; import type { Mocked, MockInstance } from 'vitest'; import { describe, it, expect, vi, beforeEach } from 'vitest'; @@ -94,7 +92,6 @@ const makeMockClusterConfig = (): ClusterConfig => ({ }); describe('Kernel', () => { - let mockStream: DuplexStream; let mockPlatformServices: PlatformServices; let launchWorkerMock: MockInstance; let terminateWorkerMock: MockInstance; @@ -103,11 +100,6 @@ describe('Kernel', () => { let mockKernelDatabase: KernelDatabase; beforeEach(async () => { - const dummyDispatch = vi.fn(); - mockStream = await TestDuplexStream.make( - dummyDispatch, - ); - mockPlatformServices = { launch: async () => ({}) as unknown as DuplexStream, @@ -151,7 +143,6 @@ describe('Kernel', () => { describe('constructor()', () => { it('initializes the kernel without errors', async () => { const kernel = await Kernel.make( - mockStream, mockPlatformServices, mockKernelDatabase, ); @@ -162,7 +153,7 @@ describe('Kernel', () => { const db = makeMapKernelDatabase(); db.kernelKVStore.set('foo', 'bar'); // Create with resetStorage should clear existing keys - await Kernel.make(mockStream, mockPlatformServices, db, { + await Kernel.make(mockPlatformServices, db, { resetStorage: true, }); expect(db.kernelKVStore.get('foo')).toBeUndefined(); @@ -172,7 +163,6 @@ describe('Kernel', () => { describe('init()', () => { it('initializes the kernel store', async () => { const kernel = await Kernel.make( - mockStream, mockPlatformServices, mockKernelDatabase, ); @@ -180,51 +170,16 @@ describe('Kernel', () => { expect(kernel.getVatIds()).toStrictEqual(['v1']); }); - it('starts receiving messages', async () => { - let drainHandler: ((message: JsonRpcRequest) => Promise) | null = - null; - const customMockStream = { - drain: async (handler: (message: JsonRpcRequest) => Promise) => { - drainHandler = handler; - return Promise.resolve(); - }, - write: vi.fn().mockResolvedValue(undefined), - } as unknown as DuplexStream; - await Kernel.make( - customMockStream, - mockPlatformServices, - mockKernelDatabase, - ); - expect(drainHandler).toBeInstanceOf(Function); - }); - it('initializes and starts the kernel queue', async () => { - await Kernel.make(mockStream, mockPlatformServices, mockKernelDatabase); + await Kernel.make(mockPlatformServices, mockKernelDatabase); const queueInstance = mocks.KernelQueue.lastInstance; expect(queueInstance.run).toHaveBeenCalledTimes(1); }); - it('throws if the stream throws', async () => { - const streamError = new Error('Stream error'); - const throwingMockStream = { - drain: () => { - throw streamError; - }, - write: vi.fn().mockResolvedValue(undefined), - } as unknown as DuplexStream; - await expect( - Kernel.make( - throwingMockStream, - mockPlatformServices, - mockKernelDatabase, - ), - ).rejects.toThrow('Stream error'); - }); - it('recovers vats from persistent storage on startup', async () => { const db = makeMapKernelDatabase(); // Launch initial kernel and vat - const kernel1 = await Kernel.make(mockStream, mockPlatformServices, db); + const kernel1 = await Kernel.make(mockPlatformServices, db); await kernel1.launchSubcluster(makeSingleVatClusterConfig()); expect(kernel1.getVatIds()).toStrictEqual(['v1']); // Clear spies @@ -232,7 +187,7 @@ describe('Kernel', () => { makeVatHandleMock.mockClear(); // New kernel should recover existing vat immediately during make() - const kernel2 = await Kernel.make(mockStream, mockPlatformServices, db); + const kernel2 = await Kernel.make(mockPlatformServices, db); // The vat should be recovered immediately expect(launchWorkerMock).toHaveBeenCalledOnce(); @@ -244,7 +199,6 @@ describe('Kernel', () => { describe('reload()', () => { it('should reload all subclusters', async () => { const kernel = await Kernel.make( - mockStream, mockPlatformServices, mockKernelDatabase, ); @@ -263,7 +217,6 @@ describe('Kernel', () => { it('should handle empty subclusters gracefully', async () => { const kernel = await Kernel.make( - mockStream, mockPlatformServices, mockKernelDatabase, ); @@ -275,7 +228,6 @@ describe('Kernel', () => { describe('queueMessage()', () => { it('enqueues a message and returns the result', async () => { const kernel = await Kernel.make( - mockStream, mockPlatformServices, mockKernelDatabase, ); @@ -288,7 +240,6 @@ describe('Kernel', () => { describe('launchSubcluster()', () => { it('launches a subcluster according to config', async () => { const kernel = await Kernel.make( - mockStream, mockPlatformServices, mockKernelDatabase, ); @@ -303,7 +254,6 @@ describe('Kernel', () => { it('throws an error for invalid configs', async () => { const kernel = await Kernel.make( - mockStream, mockPlatformServices, mockKernelDatabase, ); @@ -315,7 +265,6 @@ describe('Kernel', () => { it('throws an error when bootstrap vat name is invalid', async () => { const kernel = await Kernel.make( - mockStream, mockPlatformServices, mockKernelDatabase, ); @@ -334,7 +283,6 @@ describe('Kernel', () => { it('returns the bootstrap message result when bootstrap vat is specified', async () => { const kernel = await Kernel.make( - mockStream, mockPlatformServices, mockKernelDatabase, ); @@ -347,7 +295,6 @@ describe('Kernel', () => { describe('terminateSubcluster()', () => { it('terminates all vats in a subcluster', async () => { const kernel = await Kernel.make( - mockStream, mockPlatformServices, mockKernelDatabase, ); @@ -372,7 +319,6 @@ describe('Kernel', () => { it('throws when terminating non-existent subcluster', async () => { const kernel = await Kernel.make( - mockStream, mockPlatformServices, mockKernelDatabase, ); @@ -385,7 +331,6 @@ describe('Kernel', () => { describe('getSubcluster()', () => { it('returns subcluster by id', async () => { const kernel = await Kernel.make( - mockStream, mockPlatformServices, mockKernelDatabase, ); @@ -403,7 +348,6 @@ describe('Kernel', () => { it('returns undefined for non-existent subcluster', async () => { const kernel = await Kernel.make( - mockStream, mockPlatformServices, mockKernelDatabase, ); @@ -414,7 +358,6 @@ describe('Kernel', () => { describe('isVatInSubcluster()', () => { it('correctly identifies vat membership in subcluster', async () => { const kernel = await Kernel.make( - mockStream, mockPlatformServices, mockKernelDatabase, ); @@ -433,7 +376,6 @@ describe('Kernel', () => { describe('getSubclusterVats()', () => { it('returns all vat IDs in a subcluster', async () => { const kernel = await Kernel.make( - mockStream, mockPlatformServices, mockKernelDatabase, ); @@ -458,7 +400,6 @@ describe('Kernel', () => { describe('reloadSubcluster()', () => { it('reloads a specific subcluster', async () => { const kernel = await Kernel.make( - mockStream, mockPlatformServices, mockKernelDatabase, ); @@ -476,7 +417,6 @@ describe('Kernel', () => { it('throws when reloading non-existent subcluster', async () => { const kernel = await Kernel.make( - mockStream, mockPlatformServices, mockKernelDatabase, ); @@ -489,7 +429,6 @@ describe('Kernel', () => { describe('clearStorage()', () => { it('clears the kernel storage', async () => { const kernel = await Kernel.make( - mockStream, mockPlatformServices, mockKernelDatabase, ); @@ -502,7 +441,6 @@ describe('Kernel', () => { describe('getVats()', () => { it('returns an empty array when no vats are added', async () => { const kernel = await Kernel.make( - mockStream, mockPlatformServices, mockKernelDatabase, ); @@ -511,7 +449,6 @@ describe('Kernel', () => { it('returns vat information after adding vats', async () => { const kernel = await Kernel.make( - mockStream, mockPlatformServices, mockKernelDatabase, ); @@ -530,7 +467,6 @@ describe('Kernel', () => { it('includes subcluster information for vats in subclusters', async () => { const kernel = await Kernel.make( - mockStream, mockPlatformServices, mockKernelDatabase, ); @@ -549,7 +485,6 @@ describe('Kernel', () => { describe('getVatIds()', () => { it('returns an empty array when no vats are added', async () => { const kernel = await Kernel.make( - mockStream, mockPlatformServices, mockKernelDatabase, ); @@ -558,7 +493,6 @@ describe('Kernel', () => { it('returns the vat IDs after adding a vat', async () => { const kernel = await Kernel.make( - mockStream, mockPlatformServices, mockKernelDatabase, ); @@ -568,7 +502,6 @@ describe('Kernel', () => { it('returns multiple vat IDs after adding multiple vats', async () => { const kernel = await Kernel.make( - mockStream, mockPlatformServices, mockKernelDatabase, ); @@ -581,7 +514,6 @@ describe('Kernel', () => { describe('getStatus()', () => { it('returns the current kernel status', async () => { const kernel = await Kernel.make( - mockStream, mockPlatformServices, mockKernelDatabase, ); @@ -597,7 +529,6 @@ describe('Kernel', () => { it('includes vats and subclusters in status', async () => { const kernel = await Kernel.make( - mockStream, mockPlatformServices, mockKernelDatabase, ); @@ -616,7 +547,6 @@ describe('Kernel', () => { describe('launchVat()', () => { it('adds a vat to the kernel without errors when no vat with the same ID exists', async () => { const kernel = await Kernel.make( - mockStream, mockPlatformServices, mockKernelDatabase, ); @@ -628,7 +558,6 @@ describe('Kernel', () => { it('adds multiple vats to the kernel without errors when no vat with the same ID exists', async () => { const kernel = await Kernel.make( - mockStream, mockPlatformServices, mockKernelDatabase, ); @@ -643,7 +572,6 @@ describe('Kernel', () => { describe('terminateVat()', () => { it('deletes a vat from the kernel without errors when the vat exists', async () => { const kernel = await Kernel.make( - mockStream, mockPlatformServices, mockKernelDatabase, ); @@ -657,7 +585,6 @@ describe('Kernel', () => { it('throws an error when deleting a vat that does not exist in the kernel', async () => { const kernel = await Kernel.make( - mockStream, mockPlatformServices, mockKernelDatabase, ); @@ -670,7 +597,6 @@ describe('Kernel', () => { it('throws an error when a vat terminate method throws', async () => { const kernel = await Kernel.make( - mockStream, mockPlatformServices, mockKernelDatabase, ); @@ -688,7 +614,6 @@ describe('Kernel', () => { .spyOn(mockPlatformServices, 'terminate') .mockResolvedValue(undefined); const kernel = await Kernel.make( - mockStream, mockPlatformServices, mockKernelDatabase, ); @@ -712,15 +637,8 @@ describe('Kernel', () => { const stopRemoteCommsMock = vi .spyOn(mockPlatformServices, 'stopRemoteComms') .mockResolvedValue(undefined); - const endStreamMock = vi.fn().mockResolvedValue(undefined); - const mockStreamWithEnd = { - drain: mockStream.drain.bind(mockStream), - write: mockStream.write.bind(mockStream), - end: endStreamMock, - } as unknown as DuplexStream; const kernel = await Kernel.make( - mockStreamWithEnd, mockPlatformServices, mockKernelDatabase, ); @@ -741,22 +659,13 @@ describe('Kernel', () => { // Verify stop sequence expect(queueInstance.waitForCrank).toHaveBeenCalledOnce(); - expect(endStreamMock).toHaveBeenCalledOnce(); expect(stopRemoteCommsMock).toHaveBeenCalledOnce(); expect(remoteManagerInstance.cleanup).toHaveBeenCalledOnce(); expect(workerTerminateAllMock).toHaveBeenCalledOnce(); }); it('waits for crank before stopping', async () => { - const endStreamMock = vi.fn().mockResolvedValue(undefined); - const mockStreamWithEnd = { - drain: mockStream.drain.bind(mockStream), - write: mockStream.write.bind(mockStream), - end: endStreamMock, - } as unknown as DuplexStream; - const kernel = await Kernel.make( - mockStreamWithEnd, mockPlatformServices, mockKernelDatabase, ); @@ -767,32 +676,12 @@ describe('Kernel', () => { // Verify waitForCrank is called before other operations expect(waitForCrankSpy).toHaveBeenCalledOnce(); - expect(endStreamMock).toHaveBeenCalledOnce(); - }); - - it('handles errors during stop gracefully', async () => { - const stopError = new Error('Stop failed'); - const endStreamMock = vi.fn().mockRejectedValue(stopError); - const mockStreamWithEnd = { - drain: mockStream.drain.bind(mockStream), - write: mockStream.write.bind(mockStream), - end: endStreamMock, - } as unknown as DuplexStream; - - const kernel = await Kernel.make( - mockStreamWithEnd, - mockPlatformServices, - mockKernelDatabase, - ); - - await expect(kernel.stop()).rejects.toThrow('Stop failed'); }); }); describe('restartVat()', () => { it('preserves vat state across multiple restarts', async () => { const kernel = await Kernel.make( - mockStream, mockPlatformServices, mockKernelDatabase, ); @@ -814,7 +703,6 @@ describe('Kernel', () => { it('restarts a vat', async () => { const kernel = await Kernel.make( - mockStream, mockPlatformServices, mockKernelDatabase, ); @@ -834,7 +722,6 @@ describe('Kernel', () => { it('throws error when restarting non-existent vat', async () => { const kernel = await Kernel.make( - mockStream, mockPlatformServices, mockKernelDatabase, ); @@ -845,7 +732,6 @@ describe('Kernel', () => { it('handles restart failure during termination', async () => { const kernel = await Kernel.make( - mockStream, mockPlatformServices, mockKernelDatabase, ); @@ -861,7 +747,6 @@ describe('Kernel', () => { it('handles restart failure during launch', async () => { const kernel = await Kernel.make( - mockStream, mockPlatformServices, mockKernelDatabase, ); @@ -874,7 +759,6 @@ describe('Kernel', () => { it('returns the new vat handle', async () => { const kernel = await Kernel.make( - mockStream, mockPlatformServices, mockKernelDatabase, ); @@ -890,7 +774,6 @@ describe('Kernel', () => { describe('pingVat()', () => { it('pings a vat without errors when the vat exists', async () => { const kernel = await Kernel.make( - mockStream, mockPlatformServices, mockKernelDatabase, ); @@ -903,7 +786,6 @@ describe('Kernel', () => { it('throws an error when pinging a vat that does not exist in the kernel', async () => { const kernel = await Kernel.make( - mockStream, mockPlatformServices, mockKernelDatabase, ); @@ -915,7 +797,6 @@ describe('Kernel', () => { it('propagates errors from the vat ping method', async () => { const kernel = await Kernel.make( - mockStream, mockPlatformServices, mockKernelDatabase, ); @@ -930,11 +811,7 @@ describe('Kernel', () => { it('terminates all vats and resets kernel state', async () => { const mockDb = makeMapKernelDatabase(); const clearSpy = vi.spyOn(mockDb, 'clear'); - const kernel = await Kernel.make( - mockStream, - mockPlatformServices, - mockDb, - ); + const kernel = await Kernel.make(mockPlatformServices, mockDb); await kernel.launchSubcluster(makeSingleVatClusterConfig()); await kernel.reset(); expect(clearSpy).toHaveBeenCalled(); @@ -945,12 +822,9 @@ describe('Kernel', () => { const mockDb = makeMapKernelDatabase(); const logger = new Logger('test'); const logErrorSpy = vi.spyOn(logger, 'error'); - const kernel = await Kernel.make( - mockStream, - mockPlatformServices, - mockDb, - { logger }, - ); + const kernel = await Kernel.make(mockPlatformServices, mockDb, { + logger, + }); await kernel.launchSubcluster(makeSingleVatClusterConfig()); vi.spyOn(mockDb, 'clear').mockImplementationOnce(() => { @@ -967,7 +841,6 @@ describe('Kernel', () => { describe('revoke and isRevoked', () => { it('reflect when an object is revoked', async () => { const kernel = await Kernel.make( - mockStream, mockPlatformServices, mockKernelDatabase, ); @@ -979,7 +852,6 @@ describe('Kernel', () => { it('throws when revoking a promise', async () => { const kernel = await Kernel.make( - mockStream, mockPlatformServices, mockKernelDatabase, ); @@ -991,7 +863,6 @@ describe('Kernel', () => { describe('pinVatRoot and unpinVatRoot', () => { it('pins and unpins a vat root correctly', async () => { const kernel = await Kernel.make( - mockStream, mockPlatformServices, mockKernelDatabase, ); @@ -1012,7 +883,6 @@ describe('Kernel', () => { describe('sendRemoteMessage()', () => { it('sends message to remote peer via RemoteManager', async () => { const kernel = await Kernel.make( - mockStream, mockPlatformServices, mockKernelDatabase, ); @@ -1028,7 +898,6 @@ describe('Kernel', () => { describe('closeConnection()', () => { it('closes connection via RemoteManager', async () => { const kernel = await Kernel.make( - mockStream, mockPlatformServices, mockKernelDatabase, ); @@ -1043,7 +912,6 @@ describe('Kernel', () => { describe('reconnectPeer()', () => { it('reconnects peer via RemoteManager with hints', async () => { const kernel = await Kernel.make( - mockStream, mockPlatformServices, mockKernelDatabase, ); @@ -1059,7 +927,6 @@ describe('Kernel', () => { it('reconnects peer with empty hints when hints not provided', async () => { const kernel = await Kernel.make( - mockStream, mockPlatformServices, mockKernelDatabase, ); diff --git a/packages/ocap-kernel/src/Kernel.ts b/packages/ocap-kernel/src/Kernel.ts index 02d3c3133..93deee72b 100644 --- a/packages/ocap-kernel/src/Kernel.ts +++ b/packages/ocap-kernel/src/Kernel.ts @@ -1,12 +1,6 @@ import type { CapData } from '@endo/marshal'; -import { RpcService } from '@metamask/kernel-rpc-methods'; import type { KernelDatabase } from '@metamask/kernel-store'; -import type { JsonRpcCall } from '@metamask/kernel-utils'; import { Logger } from '@metamask/logger'; -import { serializeError } from '@metamask/rpc-errors'; -import type { DuplexStream } from '@metamask/streams'; -import { hasProperty } from '@metamask/utils'; -import type { JsonRpcResponse } from '@metamask/utils'; import { KernelQueue } from './KernelQueue.ts'; import { KernelRouter } from './KernelRouter.ts'; @@ -15,7 +9,6 @@ import type { KernelService } from './KernelServiceManager.ts'; import { OcapURLManager } from './remotes/OcapURLManager.ts'; import { RemoteManager } from './remotes/RemoteManager.ts'; import type { RemoteCommsOptions } from './remotes/types.ts'; -import { kernelHandlers } from './rpc/index.ts'; import type { PingVatResult } from './rpc/index.ts'; import { makeKernelStore } from './store/index.ts'; import type { KernelStore } from './store/index.ts'; @@ -49,11 +42,6 @@ import { VatManager } from './vats/VatManager.ts'; * @returns A new {@link Kernel}. */ export class Kernel { - /** Command channel from the controlling console/browser extension/test driver */ - readonly #commandStream: DuplexStream; - - readonly #rpcService: RpcService; - /** Manages vat lifecycle operations */ readonly #vatManager: VatManager; @@ -90,7 +78,6 @@ export class Kernel { /** * Construct a new kernel instance. * - * @param commandStream - Command channel from whatever external software is driving the kernel. * @param platformServices - Service to do things the kernel worker can't. * @param kernelDatabase - Database holding the kernel's persistent state. * @param options - Options for the kernel constructor. @@ -100,7 +87,6 @@ export class Kernel { */ // eslint-disable-next-line no-restricted-syntax private constructor( - commandStream: DuplexStream, platformServices: PlatformServices, kernelDatabase: KernelDatabase, options: { @@ -109,7 +95,6 @@ export class Kernel { keySeed?: string | undefined; } = {}, ) { - this.#commandStream = commandStream; this.#platformServices = platformServices; this.#logger = options.logger ?? new Logger('ocap-kernel'); this.#kernelStore = makeKernelStore(kernelDatabase, this.#logger); @@ -125,8 +110,6 @@ export class Kernel { async (vatId, reason) => this.#vatManager.terminateVat(vatId, reason), ); - this.#rpcService = new RpcService(kernelHandlers, {}); - this.#vatManager = new VatManager({ platformServices, kernelStore: this.#kernelStore, @@ -189,7 +172,6 @@ export class Kernel { /** * Create a new kernel instance. * - * @param commandStream - Command channel from whatever external software is driving the kernel. * @param platformServices - Service to do things the kernel worker can't. * @param kernelDatabase - Database holding the kernel's persistent state. * @param options - Options for the kernel constructor. @@ -199,7 +181,6 @@ export class Kernel { * @returns A promise for the new kernel instance. */ static async make( - commandStream: DuplexStream, platformServices: PlatformServices, kernelDatabase: KernelDatabase, options: { @@ -208,19 +189,13 @@ export class Kernel { keySeed?: string | undefined; } = {}, ): Promise { - const kernel = new Kernel( - commandStream, - platformServices, - kernelDatabase, - options, - ); + const kernel = new Kernel(platformServices, kernelDatabase, options); await kernel.#init(); return kernel; } /** - * Start the kernel running. Sets it up to actually receive command messages - * and then begin processing the run queue. + * Start the kernel running. */ async #init(): Promise { // Set up the remote message handler @@ -229,18 +204,6 @@ export class Kernel { this.#remoteManager.handleRemoteMessage(from, message), ); - // Start the command stream handler (non-blocking) - // This runs for the entire lifetime of the kernel - this.#commandStream - .drain(this.#handleCommandMessage.bind(this)) - .catch((error) => { - this.#logger.error( - 'Stream read error (kernel may be non-functional):', - error, - ); - // Don't re-throw to avoid unhandled rejection in this long-running task - }); - // Start all vats that were previously running before starting the queue // This ensures that any messages in the queue have their target vats ready await this.#vatManager.initializeAllVats(); @@ -299,37 +262,6 @@ export class Kernel { await this.#remoteManager.reconnectPeer(peerId, hints); } - /** - * Handle messages received over the command channel. - * - * @param message - The message to handle. - */ - async #handleCommandMessage(message: JsonRpcCall): Promise { - try { - this.#rpcService.assertHasMethod(message.method); - const result = await this.#rpcService.execute( - message.method, - message.params, - ); - if (hasProperty(message, 'id') && typeof message.id === 'string') { - await this.#commandStream.write({ - id: message.id, - jsonrpc: '2.0', - result, - }); - } - } catch (error) { - this.#logger.error('Error executing command', error); - if (hasProperty(message, 'id') && typeof message.id === 'string') { - await this.#commandStream.write({ - id: message.id, - jsonrpc: '2.0', - error: serializeError(error), - }); - } - } - } - /** * Send a message from the kernel to an object in a vat. * @@ -624,7 +556,6 @@ export class Kernel { */ async stop(): Promise { await this.#kernelQueue.waitForCrank(); - await this.#commandStream.end(); await this.#platformServices.stopRemoteComms(); this.#remoteManager.cleanup(); await this.#platformServices.terminateAll(); diff --git a/packages/ocap-kernel/src/rpc/index.test.ts b/packages/ocap-kernel/src/rpc/index.test.ts index 9aa4e21b9..51f6e5795 100644 --- a/packages/ocap-kernel/src/rpc/index.test.ts +++ b/packages/ocap-kernel/src/rpc/index.test.ts @@ -5,8 +5,6 @@ import * as indexModule from './index.ts'; describe('index', () => { it('has the expected exports', () => { expect(Object.keys(indexModule).sort()).toStrictEqual([ - 'kernelHandlers', - 'kernelMethodSpecs', 'kernelRemoteHandlers', 'kernelRemoteMethodSpecs', 'platformServicesHandlers', diff --git a/packages/ocap-kernel/src/rpc/index.ts b/packages/ocap-kernel/src/rpc/index.ts index 09b87a0a7..6a6b5d133 100644 --- a/packages/ocap-kernel/src/rpc/index.ts +++ b/packages/ocap-kernel/src/rpc/index.ts @@ -1,5 +1,3 @@ -export * from './kernel/index.ts'; - // PlatformServicesServer <-> PlatformServicesClient export * from './platform-services/index.ts'; export * from './kernel-remote/index.ts'; diff --git a/packages/ocap-kernel/src/rpc/kernel/index.ts b/packages/ocap-kernel/src/rpc/kernel/index.ts deleted file mode 100644 index c989c13b8..000000000 --- a/packages/ocap-kernel/src/rpc/kernel/index.ts +++ /dev/null @@ -1,23 +0,0 @@ -import type { - HandlerRecord, - MethodRequest, - MethodSpecRecord, -} from '@metamask/kernel-rpc-methods'; - -import { pingHandler, pingSpec } from '../vat/ping.ts'; - -export const kernelHandlers = { - ping: pingHandler, -} as HandlerRecord; - -export const kernelMethodSpecs = { - ping: pingSpec, -} as MethodSpecRecord; - -type Handlers = (typeof kernelHandlers)[keyof typeof kernelHandlers]; - -export type KernelMethod = Handlers['method']; - -export type KernelMethodSpec = (typeof kernelMethodSpecs)['ping']; - -export type KernelMethodRequest = MethodRequest; diff --git a/packages/omnium-gatherum/package.json b/packages/omnium-gatherum/package.json index ccebb81a7..161d861da 100644 --- a/packages/omnium-gatherum/package.json +++ b/packages/omnium-gatherum/package.json @@ -43,18 +43,13 @@ "test:e2e:debug": "playwright test --debug" }, "dependencies": { - "@endo/captp": "^4.4.8", "@endo/eventual-send": "^1.3.4", - "@endo/marshal": "^1.8.0", "@metamask/kernel-browser-runtime": "workspace:^", - "@metamask/kernel-rpc-methods": "workspace:^", "@metamask/kernel-shims": "workspace:^", "@metamask/kernel-ui": "workspace:^", "@metamask/kernel-utils": "workspace:^", "@metamask/logger": "workspace:^", - "@metamask/ocap-kernel": "workspace:^", "@metamask/streams": "workspace:^", - "@metamask/utils": "^11.9.0", "react": "^17.0.2", "react-dom": "^17.0.2", "ses": "^1.14.0" diff --git a/packages/omnium-gatherum/src/background.ts b/packages/omnium-gatherum/src/background.ts index 022aeca5a..559da4dbf 100644 --- a/packages/omnium-gatherum/src/background.ts +++ b/packages/omnium-gatherum/src/background.ts @@ -1,19 +1,18 @@ import { E } from '@endo/eventual-send'; -import { RpcClient } from '@metamask/kernel-rpc-methods'; -import { delay, isJsonRpcMessage } from '@metamask/kernel-utils'; -import type { JsonRpcMessage } from '@metamask/kernel-utils'; -import { Logger } from '@metamask/logger'; -import { kernelMethodSpecs } from '@metamask/ocap-kernel/rpc'; -import { ChromeRuntimeDuplexStream } from '@metamask/streams/browser'; -import { isJsonRpcResponse } from '@metamask/utils'; - import { makeBackgroundCapTP, makeCapTPNotification, isCapTPNotification, getCapTPMessage, -} from './captp/index.ts'; -import type { KernelFacade, CapTPMessage } from './captp/index.ts'; +} from '@metamask/kernel-browser-runtime'; +import type { + KernelFacade, + CapTPMessage, +} from '@metamask/kernel-browser-runtime'; +import { delay, isJsonRpcMessage } from '@metamask/kernel-utils'; +import type { JsonRpcMessage } from '@metamask/kernel-utils'; +import { Logger } from '@metamask/logger'; +import { ChromeRuntimeDuplexStream } from '@metamask/streams/browser'; defineGlobals(); @@ -84,21 +83,12 @@ async function main(): Promise { // Without this delay, sending messages via the chrome.runtime API can fail. await delay(50); - // Create stream that supports both RPC and CapTP messages + // Create stream for CapTP messages const offscreenStream = await ChromeRuntimeDuplexStream.make< JsonRpcMessage, JsonRpcMessage >(chrome.runtime, 'background', 'offscreen', isJsonRpcMessage); - // Set up RpcClient for backward compatibility with existing RPC methods - const rpcClient = new RpcClient( - kernelMethodSpecs, - async (request) => { - await offscreenStream.write(request); - }, - 'background:', - ); - // Set up CapTP for E() based communication with the kernel const backgroundCapTP = makeBackgroundCapTP({ send: (captpMessage: CapTPMessage) => { @@ -113,7 +103,8 @@ async function main(): Promise { const kernelPromise = backgroundCapTP.getKernel(); const ping = async (): Promise => { - const result = await rpcClient.call('ping', []); + const kernel = await kernelPromise; + const result = await E(kernel).ping(); logger.info(result); }; @@ -138,17 +129,12 @@ async function main(): Promise { }); try { - // Handle all incoming messages - await offscreenStream.drain(async (message) => { + // Handle incoming CapTP messages from the kernel + await offscreenStream.drain((message) => { if (isCapTPNotification(message)) { - // Dispatch CapTP messages const captpMessage = getCapTPMessage(message); backgroundCapTP.dispatch(captpMessage); - } else if (isJsonRpcResponse(message)) { - // Handle RPC responses - rpcClient.handleResponse(message.id as string, message); } - // Ignore other message types }); } catch (error) { throw new Error('Offscreen connection closed unexpectedly', { diff --git a/packages/omnium-gatherum/src/captp/index.ts b/packages/omnium-gatherum/src/captp/index.ts deleted file mode 100644 index cec1b1bb4..000000000 --- a/packages/omnium-gatherum/src/captp/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -export { - makeBackgroundCapTP, - makeCapTPNotification, - isCapTPNotification, - getCapTPMessage, - type BackgroundCapTP, - type BackgroundCapTPOptions, - type CapTPMessage, -} from './background-captp.ts'; - -export type { KernelFacade } from '@metamask/kernel-browser-runtime'; diff --git a/packages/omnium-gatherum/src/env/dev-console.js b/packages/omnium-gatherum/src/env/dev-console.js deleted file mode 100644 index 7c5d06d5e..000000000 --- a/packages/omnium-gatherum/src/env/dev-console.js +++ /dev/null @@ -1,9 +0,0 @@ -// We set this property on globalThis in the background before lockdown. -Object.defineProperty(globalThis, 'omnium', { - configurable: false, - enumerable: true, - writable: false, - value: {}, -}); - -export {}; diff --git a/packages/omnium-gatherum/src/env/dev-console.test.ts b/packages/omnium-gatherum/src/env/dev-console.test.ts deleted file mode 100644 index 0e7ad3f15..000000000 --- a/packages/omnium-gatherum/src/env/dev-console.test.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import './dev-console.js'; - -describe('dev-console', () => { - describe('omnium', () => { - it('is available on globalThis', async () => { - expect(omnium).toBeDefined(); - }); - - it('has expected property descriptors', async () => { - expect( - Object.getOwnPropertyDescriptor(globalThis, 'omnium'), - ).toMatchObject({ - configurable: false, - enumerable: true, - writable: false, - }); - }); - }); -}); diff --git a/packages/omnium-gatherum/src/global.d.ts b/packages/omnium-gatherum/src/global.d.ts index f64237f40..a275d71d9 100644 --- a/packages/omnium-gatherum/src/global.d.ts +++ b/packages/omnium-gatherum/src/global.d.ts @@ -1,4 +1,4 @@ -import type { KernelFacade } from './captp/index.ts'; +import type { KernelFacade } from '@metamask/kernel-browser-runtime'; // Type declarations for omnium dev console API. declare global { diff --git a/packages/omnium-gatherum/src/offscreen.ts b/packages/omnium-gatherum/src/offscreen.ts index f4bcf0768..0cf807894 100644 --- a/packages/omnium-gatherum/src/offscreen.ts +++ b/packages/omnium-gatherum/src/offscreen.ts @@ -25,8 +25,7 @@ async function main(): Promise { // Without this delay, sending messages via the chrome.runtime API can fail. await delay(50); - // Create stream for messages from the background script - // Uses JsonRpcMessage to support both RPC calls/responses and CapTP notifications + // Create stream for CapTP messages from the background script const backgroundStream = await ChromeRuntimeDuplexStream.make< JsonRpcMessage, JsonRpcMessage diff --git a/packages/omnium-gatherum/tsconfig.build.json b/packages/omnium-gatherum/tsconfig.build.json index 8da52bd25..d7b547202 100644 --- a/packages/omnium-gatherum/tsconfig.build.json +++ b/packages/omnium-gatherum/tsconfig.build.json @@ -21,10 +21,5 @@ { "path": "../ocap-kernel/tsconfig.build.json" }, { "path": "../streams/tsconfig.build.json" } ], - "include": [ - "./src/**/*.ts", - "./src/**/*.tsx", - "./src/**/*-trusted-prelude.js", - "./src/env/dev-console.js" - ] + "include": ["./src/**/*.ts", "./src/**/*.tsx"] } diff --git a/packages/omnium-gatherum/tsconfig.json b/packages/omnium-gatherum/tsconfig.json index 1197a400d..83fedfd08 100644 --- a/packages/omnium-gatherum/tsconfig.json +++ b/packages/omnium-gatherum/tsconfig.json @@ -27,8 +27,6 @@ "./playwright.config.ts", "./src/**/*.ts", "./src/**/*.tsx", - "./src/**/*-trusted-prelude.js", - "./src/env/dev-console.js", "./test/**/*.ts", "./vite.config.ts", "./vitest.config.ts" diff --git a/packages/omnium-gatherum/vite.config.ts b/packages/omnium-gatherum/vite.config.ts index 9e0c317ad..1c314ffff 100644 --- a/packages/omnium-gatherum/vite.config.ts +++ b/packages/omnium-gatherum/vite.config.ts @@ -37,7 +37,6 @@ const staticCopyTargets: readonly (string | Target)[] = [ // The extension manifest 'packages/omnium-gatherum/src/manifest.json', // Trusted prelude-related - 'packages/omnium-gatherum/src/env/dev-console.js', 'packages/kernel-shims/dist/endoify.js', ]; diff --git a/yarn.lock b/yarn.lock index 7243a3189..3f07f6fcf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2284,7 +2284,6 @@ __metadata: "@arethetypeswrong/cli": "npm:^0.17.4" "@endo/captp": "npm:^4.4.8" "@endo/marshal": "npm:^1.8.0" - "@endo/promise-kit": "npm:^1.1.13" "@metamask/auto-changelog": "npm:^5.3.0" "@metamask/eslint-config": "npm:^15.0.0" "@metamask/eslint-config-nodejs": "npm:^15.0.0" @@ -3456,19 +3455,17 @@ __metadata: resolution: "@ocap/extension@workspace:packages/extension" dependencies: "@arethetypeswrong/cli": "npm:^0.17.4" + "@endo/eventual-send": "npm:^1.3.4" "@metamask/auto-changelog": "npm:^5.3.0" "@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-browser-runtime": "workspace:^" - "@metamask/kernel-rpc-methods": "workspace:^" "@metamask/kernel-shims": "workspace:^" "@metamask/kernel-ui": "workspace:^" "@metamask/kernel-utils": "workspace:^" "@metamask/logger": "workspace:^" - "@metamask/ocap-kernel": "workspace:^" "@metamask/streams": "workspace:^" - "@metamask/utils": "npm:^11.9.0" "@ocap/cli": "workspace:^" "@ocap/kernel-test": "workspace:^" "@ocap/repo-tools": "workspace:^" @@ -3735,8 +3732,6 @@ __metadata: "@metamask/kernel-utils": "workspace:^" "@metamask/logger": "workspace:^" "@metamask/ocap-kernel": "workspace:^" - "@metamask/streams": "workspace:^" - "@metamask/utils": "npm:^11.9.0" "@ocap/cli": "workspace:^" "@ocap/kernel-language-model-service": "workspace:^" "@ocap/nodejs": "workspace:^" @@ -3884,7 +3879,6 @@ __metadata: "@metamask/logger": "workspace:^" "@metamask/ocap-kernel": "workspace:^" "@metamask/streams": "workspace:^" - "@metamask/utils": "npm:^11.9.0" "@ocap/cli": "workspace:^" "@ocap/kernel-platforms": "workspace:^" "@ocap/repo-tools": "workspace:^" @@ -3922,22 +3916,17 @@ __metadata: resolution: "@ocap/omnium-gatherum@workspace:packages/omnium-gatherum" dependencies: "@arethetypeswrong/cli": "npm:^0.17.4" - "@endo/captp": "npm:^4.4.8" "@endo/eventual-send": "npm:^1.3.4" - "@endo/marshal": "npm:^1.8.0" "@metamask/auto-changelog": "npm:^5.3.0" "@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-browser-runtime": "workspace:^" - "@metamask/kernel-rpc-methods": "workspace:^" "@metamask/kernel-shims": "workspace:^" "@metamask/kernel-ui": "workspace:^" "@metamask/kernel-utils": "workspace:^" "@metamask/logger": "workspace:^" - "@metamask/ocap-kernel": "workspace:^" "@metamask/streams": "workspace:^" - "@metamask/utils": "npm:^11.9.0" "@ocap/cli": "workspace:^" "@ocap/repo-tools": "workspace:^" "@playwright/test": "npm:^1.54.2" From dda1724faa78254969fdc9b29c28be8c33889089 Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Wed, 7 Jan 2026 11:24:52 -0800 Subject: [PATCH 04/17] test(kernel-browser-runtime): Add CapTP infrastructure tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add comprehensive tests for the CapTP infrastructure: - background-captp.test.ts: Tests for utility functions and makeBackgroundCapTP - kernel-facade.test.ts: Tests for facade delegation to kernel methods - kernel-captp.test.ts: Tests for makeKernelCapTP factory - captp.integration.test.ts: Full round-trip E() tests with real endoify Configure vitest with inline projects to use different setupFiles: - Unit tests use mock-endoify for isolated testing - Integration tests use real endoify for CapTP/E() functionality 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- packages/kernel-browser-runtime/package.json | 1 + .../src/background-captp.test.ts | 166 +++++++++++++++ .../kernel-browser-runtime/src/index.test.ts | 2 + .../captp/captp.integration.test.ts | 194 +++++++++++++++++ .../kernel-worker/captp/kernel-captp.test.ts | 100 +++++++++ .../kernel-worker/captp/kernel-facade.test.ts | 196 ++++++++++++++++++ .../kernel-browser-runtime/vitest.config.ts | 64 ++++-- yarn.lock | 1 + 8 files changed, 708 insertions(+), 16 deletions(-) create mode 100644 packages/kernel-browser-runtime/src/background-captp.test.ts create mode 100644 packages/kernel-browser-runtime/src/kernel-worker/captp/captp.integration.test.ts create mode 100644 packages/kernel-browser-runtime/src/kernel-worker/captp/kernel-captp.test.ts create mode 100644 packages/kernel-browser-runtime/src/kernel-worker/captp/kernel-facade.test.ts diff --git a/packages/kernel-browser-runtime/package.json b/packages/kernel-browser-runtime/package.json index 6cfe42cef..7fb217db8 100644 --- a/packages/kernel-browser-runtime/package.json +++ b/packages/kernel-browser-runtime/package.json @@ -83,6 +83,7 @@ }, "devDependencies": { "@arethetypeswrong/cli": "^0.17.4", + "@endo/eventual-send": "^1.3.4", "@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-captp.test.ts b/packages/kernel-browser-runtime/src/background-captp.test.ts new file mode 100644 index 000000000..3d2dc25fb --- /dev/null +++ b/packages/kernel-browser-runtime/src/background-captp.test.ts @@ -0,0 +1,166 @@ +import '@ocap/repo-tools/test-utils/mock-endoify'; + +import { describe, it, expect, vi, beforeEach } from 'vitest'; + +import { + isCapTPNotification, + getCapTPMessage, + makeCapTPNotification, + makeBackgroundCapTP, +} from './background-captp.ts'; +import type { CapTPMessage, CapTPNotification } from './background-captp.ts'; + +describe('isCapTPNotification', () => { + it('returns true for valid CapTP notification', () => { + const notification = { + jsonrpc: '2.0', + method: 'captp', + params: [{ type: 'foo' }], + }; + expect(isCapTPNotification(notification)).toBe(true); + }); + + it('returns false when method is not "captp"', () => { + const message = { + jsonrpc: '2.0', + method: 'other', + params: [{ type: 'foo' }], + }; + expect(isCapTPNotification(message)).toBe(false); + }); + + it('returns false when params is not an array', () => { + const message = { + jsonrpc: '2.0', + method: 'captp', + params: { type: 'foo' }, + }; + expect(isCapTPNotification(message as never)).toBe(false); + }); + + it('returns false when params is empty', () => { + const message = { + jsonrpc: '2.0', + method: 'captp', + params: [], + }; + expect(isCapTPNotification(message)).toBe(false); + }); + + it('returns false when params has more than one element', () => { + const message = { + jsonrpc: '2.0', + method: 'captp', + params: [{ type: 'foo' }, { type: 'bar' }], + }; + expect(isCapTPNotification(message)).toBe(false); + }); + + it('returns true for JSON-RPC request with id if it matches captp format', () => { + // A request with an id is still a valid captp message format-wise + const request = { + jsonrpc: '2.0', + id: 1, + method: 'captp', + params: [{ type: 'foo' }], + }; + expect(isCapTPNotification(request)).toBe(true); + }); +}); + +describe('getCapTPMessage', () => { + it('extracts CapTP message from valid notification', () => { + const captpMessage: CapTPMessage = { type: 'CTP_CALL', methargs: [] }; + const notification: CapTPNotification = { + jsonrpc: '2.0', + method: 'captp', + params: [captpMessage], + }; + expect(getCapTPMessage(notification)).toStrictEqual(captpMessage); + }); + + it('throws for non-CapTP notification', () => { + const message = { + jsonrpc: '2.0', + method: 'other', + params: [], + }; + expect(() => getCapTPMessage(message)).toThrow('Not a CapTP notification'); + }); + + it('throws when params is empty', () => { + const message = { + jsonrpc: '2.0', + method: 'captp', + params: [], + }; + expect(() => getCapTPMessage(message)).toThrow('Not a CapTP notification'); + }); +}); + +describe('makeCapTPNotification', () => { + it('wraps CapTP message in JSON-RPC notification', () => { + const captpMessage: CapTPMessage = { type: 'CTP_CALL', target: 'ko1' }; + const result = makeCapTPNotification(captpMessage); + + expect(result).toStrictEqual({ + jsonrpc: '2.0', + method: 'captp', + params: [captpMessage], + }); + }); + + it('creates valid notification that passes isCapTPNotification', () => { + const captpMessage: CapTPMessage = { type: 'CTP_RESOLVE' }; + const notification = makeCapTPNotification(captpMessage); + + expect(isCapTPNotification(notification)).toBe(true); + }); +}); + +describe('makeBackgroundCapTP', () => { + let sendMock: ReturnType; + + beforeEach(() => { + sendMock = vi.fn(); + }); + + it('returns object with dispatch, getKernel, and abort', () => { + const capTP = makeBackgroundCapTP({ send: sendMock }); + + expect(capTP).toHaveProperty('dispatch'); + expect(capTP).toHaveProperty('getKernel'); + expect(capTP).toHaveProperty('abort'); + expect(typeof capTP.dispatch).toBe('function'); + expect(typeof capTP.getKernel).toBe('function'); + expect(typeof capTP.abort).toBe('function'); + }); + + it('getKernel returns a promise', () => { + const capTP = makeBackgroundCapTP({ send: sendMock }); + const result = capTP.getKernel(); + + expect(result).toBeInstanceOf(Promise); + }); + + it('calls send function when dispatching bootstrap request', () => { + const capTP = makeBackgroundCapTP({ send: sendMock }); + + // Calling getKernel triggers a bootstrap request (ignore unhandled promise) + capTP.getKernel().catch(() => undefined); + + // CapTP should have sent a message to request bootstrap + expect(sendMock).toHaveBeenCalled(); + const sentMessage = sendMock.mock.calls[0][0] as CapTPMessage; + expect(sentMessage).toBeDefined(); + }); + + it('dispatch returns boolean', () => { + const capTP = makeBackgroundCapTP({ send: sendMock }); + + // Dispatch a dummy message (will return false since it's not a valid CapTP message) + const result = capTP.dispatch({ type: 'unknown' }); + + expect(typeof result).toBe('boolean'); + }); +}); diff --git a/packages/kernel-browser-runtime/src/index.test.ts b/packages/kernel-browser-runtime/src/index.test.ts index f52b98667..8464486d9 100644 --- a/packages/kernel-browser-runtime/src/index.test.ts +++ b/packages/kernel-browser-runtime/src/index.test.ts @@ -1,3 +1,5 @@ +import '@ocap/repo-tools/test-utils/mock-endoify'; + import { describe, expect, it } from 'vitest'; import * as indexModule from './index.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 new file mode 100644 index 000000000..58212db92 --- /dev/null +++ b/packages/kernel-browser-runtime/src/kernel-worker/captp/captp.integration.test.ts @@ -0,0 +1,194 @@ +// Real endoify needed for CapTP and E() to work properly +// eslint-disable-next-line import-x/no-extraneous-dependencies +import '@metamask/kernel-shims/endoify'; + +import { E } from '@endo/eventual-send'; +import type { ClusterConfig, Kernel } from '@metamask/ocap-kernel'; +import { describe, it, expect, vi, beforeEach } from 'vitest'; + +import { makeKernelCapTP } from './kernel-captp.ts'; +import { makeBackgroundCapTP } from '../../background-captp.ts'; +import type { CapTPMessage } from '../../background-captp.ts'; + +/** + * Integration tests for CapTP communication between background and kernel endpoints. + * + * These tests validate that the two CapTP endpoints can communicate correctly + * and that E() works properly with the kernel facade remote presence. + */ +describe('CapTP Integration', () => { + let mockKernel: Kernel; + let kernelCapTP: ReturnType; + let backgroundCapTP: ReturnType; + + beforeEach(() => { + // Create mock kernel with method implementations + mockKernel = { + launchSubcluster: vi.fn().mockResolvedValue({ + body: '#{"rootKref":"ko1"}', + slots: ['ko1'], + }), + terminateSubcluster: vi.fn().mockResolvedValue(undefined), + queueMessage: vi.fn().mockResolvedValue({ + body: '#{"result":"message-sent"}', + slots: [], + }), + getStatus: vi.fn().mockResolvedValue({ + vats: [{ id: 'v1', name: 'test-vat' }], + subclusters: ['sc1'], + remoteComms: false, + }), + pingVat: vi.fn().mockResolvedValue({ + pingVatResult: 'pong', + roundTripMs: 5, + }), + } as unknown as Kernel; + + // Wire up CapTP endpoints to dispatch messages synchronously to each other + // This simulates direct message passing for testing + + // Kernel-side: exposes facade as bootstrap + kernelCapTP = makeKernelCapTP({ + kernel: mockKernel, + send: (message: CapTPMessage) => { + // Dispatch synchronously for testing + backgroundCapTP.dispatch(message); + }, + }); + + // Background-side: gets remote presence of kernel + backgroundCapTP = makeBackgroundCapTP({ + send: (message: CapTPMessage) => { + // Dispatch synchronously for testing + kernelCapTP.dispatch(message); + }, + }); + }); + + describe('bootstrap', () => { + it('background can get kernel remote presence via getKernel', async () => { + // Request the kernel facade - with synchronous dispatch, this resolves immediately + const kernel = await backgroundCapTP.getKernel(); + expect(kernel).toBeDefined(); + }); + }); + + describe('ping', () => { + it('e(kernel).ping() returns "pong"', async () => { + // Get kernel remote presence + const kernel = await backgroundCapTP.getKernel(); + + // Call ping via E() + const result = await E(kernel).ping(); + expect(result).toBe('pong'); + }); + }); + + describe('getStatus', () => { + it('e(kernel).getStatus() returns status from mock kernel', async () => { + // Get kernel remote presence + const kernel = await backgroundCapTP.getKernel(); + + // Call getStatus via E() + const result = await E(kernel).getStatus(); + expect(result).toStrictEqual({ + vats: [{ id: 'v1', name: 'test-vat' }], + subclusters: ['sc1'], + remoteComms: false, + }); + + expect(mockKernel.getStatus).toHaveBeenCalled(); + }); + }); + + describe('launchSubcluster', () => { + it('e(kernel).launchSubcluster() passes arguments correctly', async () => { + const config: ClusterConfig = { + bootstrap: 'v1', + vats: { + v1: { + bundleSpec: 'test-source', + }, + }, + }; + + // Get kernel remote presence + const kernel = await backgroundCapTP.getKernel(); + + // Call launchSubcluster via E() + const result = await E(kernel).launchSubcluster(config); + expect(result).toStrictEqual({ + body: '#{"rootKref":"ko1"}', + slots: ['ko1'], + }); + + expect(mockKernel.launchSubcluster).toHaveBeenCalledWith(config); + }); + }); + + describe('terminateSubcluster', () => { + it('e(kernel).terminateSubcluster() delegates to kernel', async () => { + // Get kernel remote presence + const kernel = await backgroundCapTP.getKernel(); + + // Call terminateSubcluster via E() + await E(kernel).terminateSubcluster('sc1'); + expect(mockKernel.terminateSubcluster).toHaveBeenCalledWith('sc1'); + }); + }); + + describe('queueMessage', () => { + it('e(kernel).queueMessage() passes arguments correctly', async () => { + const target = 'ko1'; + const method = 'doSomething'; + const args = ['arg1', { nested: 'value' }]; + + // Get kernel remote presence + const kernel = await backgroundCapTP.getKernel(); + + // Call queueMessage via E() + const result = await E(kernel).queueMessage(target, method, args); + expect(result).toStrictEqual({ + body: '#{"result":"message-sent"}', + slots: [], + }); + + expect(mockKernel.queueMessage).toHaveBeenCalledWith( + target, + method, + args, + ); + }); + }); + + describe('pingVat', () => { + it('e(kernel).pingVat() delegates to kernel', async () => { + // Get kernel remote presence + const kernel = await backgroundCapTP.getKernel(); + + // Call pingVat via E() + const result = await E(kernel).pingVat('v1'); + expect(result).toStrictEqual({ + pingVatResult: 'pong', + roundTripMs: 5, + }); + + expect(mockKernel.pingVat).toHaveBeenCalledWith('v1'); + }); + }); + + describe('error propagation', () => { + it('errors from kernel methods propagate to background', async () => { + const error = new Error('Kernel operation failed'); + vi.mocked(mockKernel.getStatus).mockRejectedValueOnce(error); + + // Get kernel remote presence + const kernel = await backgroundCapTP.getKernel(); + + // Call getStatus which will fail + await expect(E(kernel).getStatus()).rejects.toThrow( + 'Kernel operation failed', + ); + }); + }); +}); diff --git a/packages/kernel-browser-runtime/src/kernel-worker/captp/kernel-captp.test.ts b/packages/kernel-browser-runtime/src/kernel-worker/captp/kernel-captp.test.ts new file mode 100644 index 000000000..32b617992 --- /dev/null +++ b/packages/kernel-browser-runtime/src/kernel-worker/captp/kernel-captp.test.ts @@ -0,0 +1,100 @@ +import '@ocap/repo-tools/test-utils/mock-endoify'; + +import type { Kernel } from '@metamask/ocap-kernel'; +import { describe, it, expect, vi, beforeEach } from 'vitest'; + +import { makeKernelCapTP } from './kernel-captp.ts'; +import type { CapTPMessage } from './kernel-captp.ts'; + +describe('makeKernelCapTP', () => { + let mockKernel: Kernel; + let sendMock: ReturnType; + + beforeEach(() => { + mockKernel = { + launchSubcluster: vi.fn().mockResolvedValue({ + body: '#{"status":"ok"}', + slots: [], + }), + terminateSubcluster: vi.fn().mockResolvedValue(undefined), + queueMessage: vi.fn().mockResolvedValue({ + body: '#{"result":"success"}', + slots: [], + }), + getStatus: vi.fn().mockResolvedValue({ + vats: [], + subclusters: [], + remoteComms: false, + }), + pingVat: vi.fn().mockResolvedValue({ + pingVatResult: 'pong', + roundTripMs: 10, + }), + } as unknown as Kernel; + + sendMock = vi.fn(); + }); + + it('returns object with dispatch and abort', () => { + const capTP = makeKernelCapTP({ + kernel: mockKernel, + send: sendMock, + }); + + expect(capTP).toHaveProperty('dispatch'); + expect(capTP).toHaveProperty('abort'); + expect(typeof capTP.dispatch).toBe('function'); + expect(typeof capTP.abort).toBe('function'); + }); + + it('dispatch returns boolean', () => { + const capTP = makeKernelCapTP({ + kernel: mockKernel, + send: sendMock, + }); + + // Dispatch a dummy message - will return false since it's not valid + const result = capTP.dispatch({ type: 'unknown' }); + + expect(typeof result).toBe('boolean'); + }); + + it('processes valid CapTP messages without errors', () => { + const capTP = makeKernelCapTP({ + kernel: mockKernel, + send: sendMock, + }); + + // Dispatch a valid CapTP message format + // CapTP uses array-based message format internally + // A CTP_CALL message triggers method calls on the bootstrap object + const callMessage: CapTPMessage = { + type: 'CTP_CALL', + questionID: 1, + target: 0, // Bootstrap slot + method: 'ping', + args: { body: '[]', slots: [] }, + }; + + // Should not throw when processing a message + expect(() => capTP.dispatch(callMessage)).not.toThrow(); + }); + + it('abort does not throw', () => { + const capTP = makeKernelCapTP({ + kernel: mockKernel, + send: sendMock, + }); + + expect(() => capTP.abort()).not.toThrow(); + }); + + it('abort can be called with a reason', () => { + const capTP = makeKernelCapTP({ + kernel: mockKernel, + send: sendMock, + }); + + expect(() => capTP.abort({ reason: 'test shutdown' })).not.toThrow(); + }); +}); 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 new file mode 100644 index 000000000..acd1f4628 --- /dev/null +++ b/packages/kernel-browser-runtime/src/kernel-worker/captp/kernel-facade.test.ts @@ -0,0 +1,196 @@ +import '@ocap/repo-tools/test-utils/mock-endoify'; + +import type { ClusterConfig, Kernel, KRef, VatId } from '@metamask/ocap-kernel'; +import { describe, it, expect, vi, beforeEach } from 'vitest'; + +import { makeKernelFacade } from './kernel-facade.ts'; +import type { KernelFacade } from './kernel-facade.ts'; + +describe('makeKernelFacade', () => { + let mockKernel: Kernel; + let facade: KernelFacade; + + beforeEach(() => { + mockKernel = { + launchSubcluster: vi.fn().mockResolvedValue({ + body: '#{"status":"ok"}', + slots: [], + }), + terminateSubcluster: vi.fn().mockResolvedValue(undefined), + queueMessage: vi.fn().mockResolvedValue({ + body: '#{"result":"success"}', + slots: [], + }), + getStatus: vi.fn().mockResolvedValue({ + vats: [], + subclusters: [], + remoteComms: false, + }), + pingVat: vi.fn().mockResolvedValue({ + pingVatResult: 'pong', + roundTripMs: 10, + }), + } as unknown as Kernel; + + facade = makeKernelFacade(mockKernel); + }); + + describe('ping', () => { + it('returns "pong"', async () => { + const result = await facade.ping(); + expect(result).toBe('pong'); + }); + }); + + describe('launchSubcluster', () => { + it('delegates to kernel with correct arguments', async () => { + const config: ClusterConfig = { + name: 'test-cluster', + vats: [ + { + name: 'test-vat', + bundleSpec: { type: 'literal', source: 'test' }, + }, + ], + }; + + await facade.launchSubcluster(config); + + expect(mockKernel.launchSubcluster).toHaveBeenCalledWith(config); + expect(mockKernel.launchSubcluster).toHaveBeenCalledTimes(1); + }); + + it('returns result from kernel', async () => { + const expectedResult = { body: '#{"rootObject":"ko1"}', slots: ['ko1'] }; + vi.mocked(mockKernel.launchSubcluster).mockResolvedValueOnce( + expectedResult, + ); + + const config: ClusterConfig = { + name: 'test-cluster', + vats: [], + }; + + const result = await facade.launchSubcluster(config); + expect(result).toStrictEqual(expectedResult); + }); + + it('propagates errors from kernel', async () => { + const error = new Error('Launch failed'); + vi.mocked(mockKernel.launchSubcluster).mockRejectedValueOnce(error); + + const config: ClusterConfig = { + name: 'test-cluster', + vats: [], + }; + + await expect(facade.launchSubcluster(config)).rejects.toThrow(error); + }); + }); + + describe('terminateSubcluster', () => { + it('delegates to kernel with correct arguments', async () => { + const subclusterId = 'sc1'; + + await facade.terminateSubcluster(subclusterId); + + expect(mockKernel.terminateSubcluster).toHaveBeenCalledWith(subclusterId); + expect(mockKernel.terminateSubcluster).toHaveBeenCalledTimes(1); + }); + + it('propagates errors from kernel', async () => { + const error = new Error('Terminate failed'); + vi.mocked(mockKernel.terminateSubcluster).mockRejectedValueOnce(error); + + await expect(facade.terminateSubcluster('sc1')).rejects.toThrow(error); + }); + }); + + describe('queueMessage', () => { + it('delegates to kernel with correct arguments', async () => { + const target: KRef = 'ko1'; + const method = 'doSomething'; + const args = ['arg1', { nested: 'value' }]; + + await facade.queueMessage(target, method, args); + + expect(mockKernel.queueMessage).toHaveBeenCalledWith( + target, + method, + args, + ); + expect(mockKernel.queueMessage).toHaveBeenCalledTimes(1); + }); + + it('returns result from kernel', async () => { + const expectedResult = { body: '#{"answer":42}', slots: [] }; + vi.mocked(mockKernel.queueMessage).mockResolvedValueOnce(expectedResult); + + const result = await facade.queueMessage('ko1', 'compute', []); + expect(result).toStrictEqual(expectedResult); + }); + + it('propagates errors from kernel', async () => { + const error = new Error('Queue message failed'); + vi.mocked(mockKernel.queueMessage).mockRejectedValueOnce(error); + + await expect(facade.queueMessage('ko1', 'method', [])).rejects.toThrow( + error, + ); + }); + }); + + describe('getStatus', () => { + it('delegates to kernel', async () => { + await facade.getStatus(); + + expect(mockKernel.getStatus).toHaveBeenCalled(); + expect(mockKernel.getStatus).toHaveBeenCalledTimes(1); + }); + + it('returns status from kernel', async () => { + const expectedStatus = { + vats: [{ id: 'v1', name: 'test-vat' }], + subclusters: [], + remoteComms: true, + }; + vi.mocked(mockKernel.getStatus).mockResolvedValueOnce(expectedStatus); + + const result = await facade.getStatus(); + expect(result).toStrictEqual(expectedStatus); + }); + + it('propagates errors from kernel', async () => { + const error = new Error('Get status failed'); + vi.mocked(mockKernel.getStatus).mockRejectedValueOnce(error); + + await expect(facade.getStatus()).rejects.toThrow(error); + }); + }); + + describe('pingVat', () => { + it('delegates to kernel with correct vatId', async () => { + const vatId: VatId = 'v1'; + + await facade.pingVat(vatId); + + expect(mockKernel.pingVat).toHaveBeenCalledWith(vatId); + expect(mockKernel.pingVat).toHaveBeenCalledTimes(1); + }); + + it('returns result from kernel', async () => { + const expectedResult = { pingVatResult: 'pong', roundTripMs: 5 }; + vi.mocked(mockKernel.pingVat).mockResolvedValueOnce(expectedResult); + + const result = await facade.pingVat('v1'); + expect(result).toStrictEqual(expectedResult); + }); + + it('propagates errors from kernel', async () => { + const error = new Error('Ping vat failed'); + vi.mocked(mockKernel.pingVat).mockRejectedValueOnce(error); + + await expect(facade.pingVat('v1')).rejects.toThrow(error); + }); + }); +}); diff --git a/packages/kernel-browser-runtime/vitest.config.ts b/packages/kernel-browser-runtime/vitest.config.ts index 7ffeda649..f2a5ffb60 100644 --- a/packages/kernel-browser-runtime/vitest.config.ts +++ b/packages/kernel-browser-runtime/vitest.config.ts @@ -1,22 +1,54 @@ -import { mergeConfig } from '@ocap/repo-tools/vitest-config'; +import path from 'node:path'; 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', - setupFiles: [ - 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'), + ), + path.resolve(import.meta.dirname, '../kernel-shims/src/endoify.js'), + ], + }, }, - }), - ); + ], + }, }); diff --git a/yarn.lock b/yarn.lock index 3f07f6fcf..fe0971a40 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2283,6 +2283,7 @@ __metadata: dependencies: "@arethetypeswrong/cli": "npm:^0.17.4" "@endo/captp": "npm:^4.4.8" + "@endo/eventual-send": "npm:^1.3.4" "@endo/marshal": "npm:^1.8.0" "@metamask/auto-changelog": "npm:^5.3.0" "@metamask/eslint-config": "npm:^15.0.0" From cf748188d2cf1ae7494ddd7747b0d815be7c78e3 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 05/17] 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 --- package.json | 1 + packages/kernel-browser-runtime/package.json | 1 + .../src/background-captp.test.ts | 64 +++++++---------- .../kernel-browser-runtime/src/index.test.ts | 2 - .../kernel-worker/captp/kernel-captp.test.ts | 4 +- .../kernel-worker/captp/kernel-facade.test.ts | 2 - packages/kernel-browser-runtime/tsconfig.json | 3 +- .../kernel-browser-runtime/vitest.config.ts | 69 ++++++------------- .../vitest.integration.config.ts | 34 +++++++++ vitest.config.ts | 56 +++++++-------- 10 files changed, 111 insertions(+), 125 deletions(-) create mode 100644 packages/kernel-browser-runtime/vitest.integration.config.ts diff --git a/package.json b/package.json index a283cb9c0..97e804803 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "test:e2e": "yarn workspaces foreach --all run test:e2e", "test:e2e:ci": "yarn workspaces foreach --all run test:e2e:ci", "test:e2e:local": "yarn workspaces foreach --all run test:e2e:local", + "test:integration": "yarn workspaces foreach --all run test:integration", "test:verbose": "yarn test --reporter verbose", "test:watch": "vitest", "why:batch": "./scripts/why-batch.sh" diff --git a/packages/kernel-browser-runtime/package.json b/packages/kernel-browser-runtime/package.json index 7fb217db8..6bd115921 100644 --- a/packages/kernel-browser-runtime/package.json +++ b/packages/kernel-browser-runtime/package.json @@ -59,6 +59,7 @@ "test:build": "tsx ./test/build-tests.ts", "test:clean": "yarn test --no-cache --coverage.clean", "test:dev": "yarn test --mode development --reporter dot", + "test:integration": "vitest run --config vitest.integration.config.ts", "test:verbose": "yarn test --reporter verbose", "test:watch": "vitest --config vitest.config.ts" }, diff --git a/packages/kernel-browser-runtime/src/background-captp.test.ts b/packages/kernel-browser-runtime/src/background-captp.test.ts index 3d2dc25fb..c5b5a9af8 100644 --- a/packages/kernel-browser-runtime/src/background-captp.test.ts +++ b/packages/kernel-browser-runtime/src/background-captp.test.ts @@ -1,5 +1,4 @@ -import '@ocap/repo-tools/test-utils/mock-endoify'; - +import type { JsonRpcNotification } from '@metamask/utils'; import { describe, it, expect, vi, beforeEach } from 'vitest'; import { @@ -8,58 +7,48 @@ import { makeCapTPNotification, makeBackgroundCapTP, } from './background-captp.ts'; -import type { CapTPMessage, CapTPNotification } from './background-captp.ts'; +import type { CapTPMessage } from './background-captp.ts'; + +const makeNotification = ( + params: CapTPMessage[], + method = 'captp', +): JsonRpcNotification => ({ + jsonrpc: '2.0', + method, + params, +}); describe('isCapTPNotification', () => { it('returns true for valid CapTP notification', () => { - const notification = { - jsonrpc: '2.0', - method: 'captp', - params: [{ type: 'foo' }], - }; + const notification = makeNotification([{ type: 'foo' }]); expect(isCapTPNotification(notification)).toBe(true); }); it('returns false when method is not "captp"', () => { - const message = { - jsonrpc: '2.0', - method: 'other', - params: [{ type: 'foo' }], - }; + const message = makeNotification([{ type: 'foo' }], 'other'); expect(isCapTPNotification(message)).toBe(false); }); it('returns false when params is not an array', () => { - const message = { - jsonrpc: '2.0', - method: 'captp', - params: { type: 'foo' }, - }; + // @ts-expect-error - we want to test the error case + const message = makeNotification({ type: 'foo' }); expect(isCapTPNotification(message as never)).toBe(false); }); it('returns false when params is empty', () => { - const message = { - jsonrpc: '2.0', - method: 'captp', - params: [], - }; + const message = makeNotification([]); expect(isCapTPNotification(message)).toBe(false); }); it('returns false when params has more than one element', () => { - const message = { - jsonrpc: '2.0', - method: 'captp', - params: [{ type: 'foo' }, { type: 'bar' }], - }; + const message = makeNotification([{ type: 'foo' }, { type: 'bar' }]); expect(isCapTPNotification(message)).toBe(false); }); it('returns true for JSON-RPC request with id if it matches captp format', () => { // A request with an id is still a valid captp message format-wise const request = { - jsonrpc: '2.0', + jsonrpc: '2.0' as const, id: 1, method: 'captp', params: [{ type: 'foo' }], @@ -71,11 +60,7 @@ describe('isCapTPNotification', () => { describe('getCapTPMessage', () => { it('extracts CapTP message from valid notification', () => { const captpMessage: CapTPMessage = { type: 'CTP_CALL', methargs: [] }; - const notification: CapTPNotification = { - jsonrpc: '2.0', - method: 'captp', - params: [captpMessage], - }; + const notification = makeNotification([captpMessage]); expect(getCapTPMessage(notification)).toStrictEqual(captpMessage); }); @@ -85,15 +70,12 @@ describe('getCapTPMessage', () => { method: 'other', params: [], }; + // @ts-expect-error - we want to test the error case expect(() => getCapTPMessage(message)).toThrow('Not a CapTP notification'); }); it('throws when params is empty', () => { - const message = { - jsonrpc: '2.0', - method: 'captp', - params: [], - }; + const message = makeNotification([]); expect(() => getCapTPMessage(message)).toThrow('Not a CapTP notification'); }); }); @@ -119,7 +101,7 @@ describe('makeCapTPNotification', () => { }); describe('makeBackgroundCapTP', () => { - let sendMock: ReturnType; + let sendMock: (message: CapTPMessage) => void; beforeEach(() => { sendMock = vi.fn(); @@ -151,7 +133,7 @@ describe('makeBackgroundCapTP', () => { // CapTP should have sent a message to request bootstrap expect(sendMock).toHaveBeenCalled(); - const sentMessage = sendMock.mock.calls[0][0] as CapTPMessage; + const sentMessage = vi.mocked(sendMock).mock.calls[0]?.[0] as CapTPMessage; expect(sentMessage).toBeDefined(); }); diff --git a/packages/kernel-browser-runtime/src/index.test.ts b/packages/kernel-browser-runtime/src/index.test.ts index 8464486d9..f52b98667 100644 --- a/packages/kernel-browser-runtime/src/index.test.ts +++ b/packages/kernel-browser-runtime/src/index.test.ts @@ -1,5 +1,3 @@ -import '@ocap/repo-tools/test-utils/mock-endoify'; - import { describe, expect, it } from 'vitest'; import * as indexModule from './index.ts'; diff --git a/packages/kernel-browser-runtime/src/kernel-worker/captp/kernel-captp.test.ts b/packages/kernel-browser-runtime/src/kernel-worker/captp/kernel-captp.test.ts index 32b617992..f67eddb36 100644 --- a/packages/kernel-browser-runtime/src/kernel-worker/captp/kernel-captp.test.ts +++ b/packages/kernel-browser-runtime/src/kernel-worker/captp/kernel-captp.test.ts @@ -1,5 +1,3 @@ -import '@ocap/repo-tools/test-utils/mock-endoify'; - import type { Kernel } from '@metamask/ocap-kernel'; import { describe, it, expect, vi, beforeEach } from 'vitest'; @@ -8,7 +6,7 @@ import type { CapTPMessage } from './kernel-captp.ts'; describe('makeKernelCapTP', () => { let mockKernel: Kernel; - let sendMock: ReturnType; + let sendMock: (message: CapTPMessage) => void; beforeEach(() => { mockKernel = { 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 acd1f4628..0756d9d49 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 @@ -1,5 +1,3 @@ -import '@ocap/repo-tools/test-utils/mock-endoify'; - import type { ClusterConfig, Kernel, KRef, VatId } from '@metamask/ocap-kernel'; import { describe, it, expect, vi, beforeEach } from 'vitest'; diff --git a/packages/kernel-browser-runtime/tsconfig.json b/packages/kernel-browser-runtime/tsconfig.json index 175067430..6df549148 100644 --- a/packages/kernel-browser-runtime/tsconfig.json +++ b/packages/kernel-browser-runtime/tsconfig.json @@ -21,6 +21,7 @@ "./test/**/*.ts", "./src", "./vite.config.ts", - "./vitest.config.ts" + "./vitest.config.ts", + "./vitest.integration.config.ts" ] } diff --git a/packages/kernel-browser-runtime/vitest.config.ts b/packages/kernel-browser-runtime/vitest.config.ts index f2a5ffb60..fe56f07a9 100644 --- a/packages/kernel-browser-runtime/vitest.config.ts +++ b/packages/kernel-browser-runtime/vitest.config.ts @@ -1,54 +1,27 @@ -import path from 'node:path'; +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'), - ), - path.resolve(import.meta.dirname, '../kernel-shims/src/endoify.js'), - ], - }, +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'), + ), + ], }, - ], - }, + }), + ); }); diff --git a/packages/kernel-browser-runtime/vitest.integration.config.ts b/packages/kernel-browser-runtime/vitest.integration.config.ts new file mode 100644 index 000000000..0324cb68f --- /dev/null +++ b/packages/kernel-browser-runtime/vitest.integration.config.ts @@ -0,0 +1,34 @@ +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'; + +export default defineConfig((args) => { + delete defaultConfig.test?.setupFiles; + + const config = mergeConfig( + args, + defaultConfig, + defineProject({ + test: { + 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'), + ), + ], + }, + }), + ); + + delete config.test?.coverage; + + return config; +}); diff --git a/vitest.config.ts b/vitest.config.ts index effb1f9f9..f64531437 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -75,10 +75,10 @@ export default defineConfig({ lines: 100, }, 'packages/extension/**': { - statements: 1.42, + statements: 1.44, functions: 0, branches: 0, - lines: 1.44, + lines: 1.47, }, 'packages/kernel-agents/**': { statements: 92.34, @@ -111,10 +111,10 @@ export default defineConfig({ lines: 99.26, }, 'packages/kernel-rpc-methods/**': { - statements: 0, - functions: 0, - branches: 0, - lines: 0, + statements: 100, + functions: 100, + branches: 100, + lines: 100, }, 'packages/kernel-shims/**': { statements: 0, @@ -135,10 +135,10 @@ export default defineConfig({ lines: 95.11, }, 'packages/kernel-utils/**': { - statements: 0, - functions: 0, - branches: 0, - lines: 0, + statements: 100, + functions: 100, + branches: 100, + lines: 100, }, 'packages/logger/**': { statements: 98.66, @@ -147,10 +147,10 @@ export default defineConfig({ lines: 100, }, 'packages/nodejs/**': { - statements: 88.98, + statements: 88.79, functions: 87.5, branches: 90.9, - lines: 89.74, + lines: 89.56, }, 'packages/nodejs-test-workers/**': { statements: 23.52, @@ -165,28 +165,28 @@ export default defineConfig({ lines: 95.1, }, 'packages/omnium-gatherum/**': { - statements: 5.26, - functions: 5.55, - branches: 0, - lines: 5.35, + statements: 61.88, + functions: 64.63, + branches: 68.62, + lines: 61.82, }, 'packages/remote-iterables/**': { - statements: 0, - functions: 0, - branches: 0, - lines: 0, + statements: 100, + functions: 100, + branches: 100, + lines: 100, }, 'packages/streams/**': { - statements: 0, - functions: 0, - branches: 0, - lines: 0, + statements: 100, + functions: 100, + branches: 100, + lines: 100, }, 'packages/template-package/**': { - statements: 0, - functions: 0, - branches: 0, - lines: 0, + statements: 100, + functions: 100, + branches: 100, + lines: 100, }, }, }, From 56d6ead36773985c5ee91d5fb6ea365d901c14e5 Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Wed, 14 Jan 2026 15:56:16 -0800 Subject: [PATCH 06/17] fix: Fix browser-runtime integration test config --- .../src/kernel-worker/captp/captp.integration.test.ts | 2 +- packages/kernel-browser-runtime/vitest.integration.config.ts | 5 ----- 2 files changed, 1 insertion(+), 6 deletions(-) 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 58212db92..133e59bc4 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,6 @@ // Real endoify needed for CapTP and E() to work properly // eslint-disable-next-line import-x/no-extraneous-dependencies -import '@metamask/kernel-shims/endoify'; +import '@ocap/nodejs/endoify-ts'; import { E } from '@endo/eventual-send'; import type { ClusterConfig, Kernel } from '@metamask/ocap-kernel'; diff --git a/packages/kernel-browser-runtime/vitest.integration.config.ts b/packages/kernel-browser-runtime/vitest.integration.config.ts index 0324cb68f..01ea8c4b3 100644 --- a/packages/kernel-browser-runtime/vitest.integration.config.ts +++ b/packages/kernel-browser-runtime/vitest.integration.config.ts @@ -18,11 +18,6 @@ export default defineConfig((args) => { 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'), - ), ], }, }), From 1c07cdf11774ac9e0367ab9e9181d3434776945d Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Wed, 14 Jan 2026 15:59:37 -0800 Subject: [PATCH 07/17] chore: Build before integration tests in CI --- .github/workflows/lint-build-test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/lint-build-test.yml b/.github/workflows/lint-build-test.yml index b78bb6279..3a6224dce 100644 --- a/.github/workflows/lint-build-test.yml +++ b/.github/workflows/lint-build-test.yml @@ -132,6 +132,7 @@ jobs: node-version: ${{ matrix.node-version }} env: PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 + - run: yarn build - run: yarn test:integration - name: Require clean working directory shell: bash From 8c4927e3316cec4a830e517a72e4bce4c9827fd1 Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Wed, 14 Jan 2026 16:05:29 -0800 Subject: [PATCH 08/17] chore: Add @ocap/nodejs dev dep to browser-runtime --- packages/kernel-browser-runtime/package.json | 1 + yarn.lock | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/kernel-browser-runtime/package.json b/packages/kernel-browser-runtime/package.json index 6bd115921..34bb97228 100644 --- a/packages/kernel-browser-runtime/package.json +++ b/packages/kernel-browser-runtime/package.json @@ -89,6 +89,7 @@ "@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 fe0971a40..8fdc13dfa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2302,6 +2302,7 @@ __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" From 916f94a16385109347b2554e396af9abfa198d1a Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Wed, 14 Jan 2026 16:26:11 -0800 Subject: [PATCH 09/17] refactor: Rationalize kernel promise handling in backgrounds --- packages/extension/src/background.ts | 21 ++++++++------------- packages/omnium-gatherum/src/background.ts | 19 ++++++------------- 2 files changed, 14 insertions(+), 26 deletions(-) diff --git a/packages/extension/src/background.ts b/packages/extension/src/background.ts index b4e6d5a2f..bb0b71358 100644 --- a/packages/extension/src/background.ts +++ b/packages/extension/src/background.ts @@ -101,25 +101,19 @@ async function main(): Promise { }); // Get the kernel remote presence - const kernelPromise = backgroundCapTP.getKernel(); + const kernelP = backgroundCapTP.getKernel(); const ping = async (): Promise => { - const kernel = await kernelPromise; - const result = await E(kernel).ping(); + const result = await E(kernelP).ping(); logger.info(result); }; - // Helper to get the kernel remote presence (for use with E()) - const getKernel = async (): Promise => { - return kernelPromise; - }; - Object.defineProperties(globalThis.kernel, { ping: { value: ping, }, getKernel: { - value: getKernel, + value: async () => kernelP, }, }); harden(globalThis.kernel); @@ -134,12 +128,14 @@ async function main(): Promise { if (isCapTPNotification(message)) { const captpMessage = getCapTPMessage(message); backgroundCapTP.dispatch(captpMessage); + } else { + logger.error('Unexpected message from offscreen:', message); } }); drainPromise.catch(logger.error); await ping(); // Wait for the kernel to be ready - await startDefaultSubcluster(kernelPromise); + await startDefaultSubcluster(kernelP); try { await drainPromise; @@ -158,11 +154,10 @@ async function main(): Promise { async function startDefaultSubcluster( kernelPromise: Promise, ): Promise { - const kernel = await kernelPromise; - const status = await E(kernel).getStatus(); + const status = await E(kernelPromise).getStatus(); if (status.subclusters.length === 0) { - const result = await E(kernel).launchSubcluster(defaultSubcluster); + const result = await E(kernelPromise).launchSubcluster(defaultSubcluster); logger.info(`Default subcluster launched: ${JSON.stringify(result)}`); } else { logger.info('Subclusters already exist. Not launching default subcluster.'); diff --git a/packages/omnium-gatherum/src/background.ts b/packages/omnium-gatherum/src/background.ts index 559da4dbf..52b39a939 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 { - KernelFacade, - CapTPMessage, -} from '@metamask/kernel-browser-runtime'; +import type { CapTPMessage } from '@metamask/kernel-browser-runtime'; import { delay, isJsonRpcMessage } from '@metamask/kernel-utils'; import type { JsonRpcMessage } from '@metamask/kernel-utils'; import { Logger } from '@metamask/logger'; @@ -100,25 +97,19 @@ async function main(): Promise { }); // Get the kernel remote presence - const kernelPromise = backgroundCapTP.getKernel(); + const kernelP = backgroundCapTP.getKernel(); const ping = async (): Promise => { - const kernel = await kernelPromise; - const result = await E(kernel).ping(); + const result = await E(kernelP).ping(); logger.info(result); }; - // Helper to get the kernel remote presence (for use with E()) - const getKernel = async (): Promise => { - return kernelPromise; - }; - Object.defineProperties(globalThis.omnium, { ping: { value: ping, }, getKernel: { - value: getKernel, + value: async () => kernelP, }, }); harden(globalThis.omnium); @@ -134,6 +125,8 @@ async function main(): Promise { if (isCapTPNotification(message)) { const captpMessage = getCapTPMessage(message); backgroundCapTP.dispatch(captpMessage); + } else { + logger.error('Unexpected message from offscreen:', message); } }); } catch (error) { From e9b82e0b804e332592b7202711f47b7383def3f9 Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Wed, 14 Jan 2026 16:44:38 -0800 Subject: [PATCH 10/17] test: Fix some slop in browser-runtime tests --- .../captp/captp.integration.test.ts | 12 ++--- .../kernel-worker/captp/kernel-facade.test.ts | 52 ++++++++----------- 2 files changed, 26 insertions(+), 38 deletions(-) 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 133e59bc4..6fe7ca5c1 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,5 +1,5 @@ // Real endoify needed for CapTP and E() to work properly -// eslint-disable-next-line import-x/no-extraneous-dependencies + import '@ocap/nodejs/endoify-ts'; import { E } from '@endo/eventual-send'; @@ -38,10 +38,7 @@ describe('CapTP Integration', () => { subclusters: ['sc1'], remoteComms: false, }), - pingVat: vi.fn().mockResolvedValue({ - pingVatResult: 'pong', - roundTripMs: 5, - }), + pingVat: vi.fn().mockResolvedValue('pong'), } as unknown as Kernel; // Wire up CapTP endpoints to dispatch messages synchronously to each other @@ -168,10 +165,7 @@ describe('CapTP Integration', () => { // Call pingVat via E() const result = await E(kernel).pingVat('v1'); - expect(result).toStrictEqual({ - pingVatResult: 'pong', - roundTripMs: 5, - }); + expect(result).toBe('pong'); expect(mockKernel.pingVat).toHaveBeenCalledWith('v1'); }); 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 0756d9d49..93129c781 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 @@ -1,9 +1,22 @@ -import type { ClusterConfig, Kernel, KRef, VatId } from '@metamask/ocap-kernel'; +import type { + ClusterConfig, + Kernel, + KernelStatus, + KRef, + VatId, +} from '@metamask/ocap-kernel'; import { describe, it, expect, vi, beforeEach } from 'vitest'; import { makeKernelFacade } from './kernel-facade.ts'; import type { KernelFacade } from './kernel-facade.ts'; +const makeClusterconfig = (): ClusterConfig => ({ + bootstrap: 'test-vat', + vats: { + 'test-vat': { bundleSpec: 'test' }, + }, +}); + describe('makeKernelFacade', () => { let mockKernel: Kernel; let facade: KernelFacade; @@ -24,10 +37,7 @@ describe('makeKernelFacade', () => { subclusters: [], remoteComms: false, }), - pingVat: vi.fn().mockResolvedValue({ - pingVatResult: 'pong', - roundTripMs: 10, - }), + pingVat: vi.fn().mockResolvedValue('pong'), } as unknown as Kernel; facade = makeKernelFacade(mockKernel); @@ -42,15 +52,7 @@ describe('makeKernelFacade', () => { describe('launchSubcluster', () => { it('delegates to kernel with correct arguments', async () => { - const config: ClusterConfig = { - name: 'test-cluster', - vats: [ - { - name: 'test-vat', - bundleSpec: { type: 'literal', source: 'test' }, - }, - ], - }; + const config: ClusterConfig = makeClusterconfig(); await facade.launchSubcluster(config); @@ -64,10 +66,7 @@ describe('makeKernelFacade', () => { expectedResult, ); - const config: ClusterConfig = { - name: 'test-cluster', - vats: [], - }; + const config: ClusterConfig = makeClusterconfig(); const result = await facade.launchSubcluster(config); expect(result).toStrictEqual(expectedResult); @@ -76,11 +75,7 @@ describe('makeKernelFacade', () => { it('propagates errors from kernel', async () => { const error = new Error('Launch failed'); vi.mocked(mockKernel.launchSubcluster).mockRejectedValueOnce(error); - - const config: ClusterConfig = { - name: 'test-cluster', - vats: [], - }; + const config: ClusterConfig = makeClusterconfig(); await expect(facade.launchSubcluster(config)).rejects.toThrow(error); }); @@ -147,10 +142,10 @@ describe('makeKernelFacade', () => { }); it('returns status from kernel', async () => { - const expectedStatus = { - vats: [{ id: 'v1', name: 'test-vat' }], + const expectedStatus: KernelStatus = { + vats: [], subclusters: [], - remoteComms: true, + remoteComms: { isInitialized: false }, }; vi.mocked(mockKernel.getStatus).mockResolvedValueOnce(expectedStatus); @@ -177,11 +172,10 @@ describe('makeKernelFacade', () => { }); it('returns result from kernel', async () => { - const expectedResult = { pingVatResult: 'pong', roundTripMs: 5 }; - vi.mocked(mockKernel.pingVat).mockResolvedValueOnce(expectedResult); + vi.mocked(mockKernel.pingVat).mockResolvedValueOnce('pong'); const result = await facade.pingVat('v1'); - expect(result).toStrictEqual(expectedResult); + expect(result).toBe('pong'); }); it('propagates errors from kernel', async () => { From da8a92c932a6fa86b6031db9cb1a4b44d4829676 Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Wed, 14 Jan 2026 16:55:25 -0800 Subject: [PATCH 11/17] test: Further browser-runtime test rationalizations --- .../captp/captp.integration.test.ts | 1 - .../kernel-worker/captp/kernel-captp.test.ts | 23 +------------------ 2 files changed, 1 insertion(+), 23 deletions(-) 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 6fe7ca5c1..960d640f6 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,5 +1,4 @@ // Real endoify needed for CapTP and E() to work properly - import '@ocap/nodejs/endoify-ts'; import { E } from '@endo/eventual-send'; diff --git a/packages/kernel-browser-runtime/src/kernel-worker/captp/kernel-captp.test.ts b/packages/kernel-browser-runtime/src/kernel-worker/captp/kernel-captp.test.ts index f67eddb36..21c9686ea 100644 --- a/packages/kernel-browser-runtime/src/kernel-worker/captp/kernel-captp.test.ts +++ b/packages/kernel-browser-runtime/src/kernel-worker/captp/kernel-captp.test.ts @@ -5,31 +5,10 @@ import { makeKernelCapTP } from './kernel-captp.ts'; import type { CapTPMessage } from './kernel-captp.ts'; describe('makeKernelCapTP', () => { - let mockKernel: Kernel; + const mockKernel: Kernel = {} as unknown as Kernel; let sendMock: (message: CapTPMessage) => void; beforeEach(() => { - mockKernel = { - launchSubcluster: vi.fn().mockResolvedValue({ - body: '#{"status":"ok"}', - slots: [], - }), - terminateSubcluster: vi.fn().mockResolvedValue(undefined), - queueMessage: vi.fn().mockResolvedValue({ - body: '#{"result":"success"}', - slots: [], - }), - getStatus: vi.fn().mockResolvedValue({ - vats: [], - subclusters: [], - remoteComms: false, - }), - pingVat: vi.fn().mockResolvedValue({ - pingVatResult: 'pong', - roundTripMs: 10, - }), - } as unknown as Kernel; - sendMock = vi.fn(); }); From fa89c36a73e6e2e91a3233fca421dbff4cdae082 Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Wed, 14 Jan 2026 17:10:15 -0800 Subject: [PATCH 12/17] refactor: Remove extraneous comments and type casts --- .../src/background-captp.test.ts | 15 ++++++--------- .../src/background-captp.ts | 7 +++---- .../src/kernel-worker/kernel-worker.ts | 5 ----- 3 files changed, 9 insertions(+), 18 deletions(-) diff --git a/packages/kernel-browser-runtime/src/background-captp.test.ts b/packages/kernel-browser-runtime/src/background-captp.test.ts index c5b5a9af8..6192a5047 100644 --- a/packages/kernel-browser-runtime/src/background-captp.test.ts +++ b/packages/kernel-browser-runtime/src/background-captp.test.ts @@ -110,12 +110,11 @@ describe('makeBackgroundCapTP', () => { it('returns object with dispatch, getKernel, and abort', () => { const capTP = makeBackgroundCapTP({ send: sendMock }); - expect(capTP).toHaveProperty('dispatch'); - expect(capTP).toHaveProperty('getKernel'); - expect(capTP).toHaveProperty('abort'); - expect(typeof capTP.dispatch).toBe('function'); - expect(typeof capTP.getKernel).toBe('function'); - expect(typeof capTP.abort).toBe('function'); + expect(capTP).toStrictEqual({ + dispatch: expect.any(Function), + getKernel: expect.any(Function), + abort: expect.any(Function), + }); }); it('getKernel returns a promise', () => { @@ -131,7 +130,6 @@ describe('makeBackgroundCapTP', () => { // Calling getKernel triggers a bootstrap request (ignore unhandled promise) capTP.getKernel().catch(() => undefined); - // CapTP should have sent a message to request bootstrap expect(sendMock).toHaveBeenCalled(); const sentMessage = vi.mocked(sendMock).mock.calls[0]?.[0] as CapTPMessage; expect(sentMessage).toBeDefined(); @@ -140,9 +138,8 @@ describe('makeBackgroundCapTP', () => { it('dispatch returns boolean', () => { const capTP = makeBackgroundCapTP({ send: sendMock }); - // Dispatch a dummy message (will return false since it's not a valid CapTP message) const result = capTP.dispatch({ type: 'unknown' }); - expect(typeof result).toBe('boolean'); + expect(result).toBe(false); }); }); diff --git a/packages/kernel-browser-runtime/src/background-captp.ts b/packages/kernel-browser-runtime/src/background-captp.ts index d6692e3b5..d6b7da7da 100644 --- a/packages/kernel-browser-runtime/src/background-captp.ts +++ b/packages/kernel-browser-runtime/src/background-captp.ts @@ -40,7 +40,7 @@ export function getCapTPMessage(message: JsonRpcMessage): CapTPMessage { if (!isCapTPNotification(message)) { throw new Error('Not a CapTP notification'); } - return (message as unknown as { params: [CapTPMessage] }).params[0]; + return message.params[0]; } /** @@ -111,16 +111,15 @@ export function makeBackgroundCapTP( ): BackgroundCapTP { const { send } = options; - // Create the CapTP endpoint (no bootstrap - we only want to call the kernel) const { dispatch, getBootstrap, abort } = makeCapTP( 'background', send, - undefined, + undefined, // No bootstrap - we only want to call the kernel ); return harden({ dispatch, - getKernel: getBootstrap as () => Promise, + getKernel: getBootstrap, abort, }); } diff --git a/packages/kernel-browser-runtime/src/kernel-worker/kernel-worker.ts b/packages/kernel-browser-runtime/src/kernel-worker/kernel-worker.ts index b480093c1..1699761cc 100644 --- a/packages/kernel-browser-runtime/src/kernel-worker/kernel-worker.ts +++ b/packages/kernel-browser-runtime/src/kernel-worker/kernel-worker.ts @@ -36,7 +36,6 @@ async function main(): Promise { (listener) => globalThis.removeEventListener('message', listener), ); - // Initialize kernel dependencies const [messageStream, platformServicesClient, kernelDatabase] = await Promise.all([ MessagePortDuplexStream.make( @@ -55,7 +54,6 @@ async function main(): Promise { resetStorage, }); - // Set up internal RPC server for UI panel connections (uses separate MessagePorts) const handlerP = kernelP.then((kernel) => { const server = new JsonRpcServer({ middleware: [ @@ -73,7 +71,6 @@ async function main(): Promise { const kernel = await kernelP; - // Set up CapTP for background ↔ kernel communication const kernelCapTP = makeKernelCapTP({ kernel, send: (captpMessage: CapTPMessage) => { @@ -84,7 +81,6 @@ async function main(): Promise { }, }); - // Handle incoming CapTP messages from the background messageStream .drain((message) => { if (isCapTPNotification(message)) { @@ -96,7 +92,6 @@ async function main(): Promise { logger.error('Message stream error:', error); }); - // Initialize remote communications with the relay server passed in the query string const relays = getRelaysFromCurrentLocation(); await kernel.initRemoteComms({ relays }); } From 25aad34de8cb0567c6efa3244f1e23676ab7c203 Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Wed, 14 Jan 2026 17:15:06 -0800 Subject: [PATCH 13/17] chore: Fix test coverage values --- vitest.config.ts | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/vitest.config.ts b/vitest.config.ts index f64531437..6fbb0d25a 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -75,22 +75,22 @@ export default defineConfig({ lines: 100, }, 'packages/extension/**': { - statements: 1.44, + statements: 1.35, functions: 0, branches: 0, - lines: 1.47, + lines: 1.36, }, 'packages/kernel-agents/**': { - statements: 92.34, - functions: 90.84, - branches: 85.08, - lines: 92.48, + statements: 88.16, + functions: 80, + branches: 75.38, + lines: 88.13, }, 'packages/kernel-browser-runtime/**': { - statements: 85.88, - functions: 78.88, - branches: 81.92, - lines: 86.15, + statements: 84.63, + functions: 78.3, + branches: 81.11, + lines: 84.87, }, 'packages/kernel-errors/**': { statements: 99.24, @@ -147,10 +147,10 @@ export default defineConfig({ lines: 100, }, 'packages/nodejs/**': { - statements: 88.79, + statements: 88.88, functions: 87.5, branches: 90.9, - lines: 89.56, + lines: 89.65, }, 'packages/nodejs-test-workers/**': { statements: 23.52, @@ -159,16 +159,16 @@ export default defineConfig({ lines: 25, }, 'packages/ocap-kernel/**': { - statements: 95.12, - functions: 97.69, - branches: 86.95, - lines: 95.1, + statements: 95.44, + functions: 98.06, + branches: 87.65, + lines: 95.42, }, 'packages/omnium-gatherum/**': { - statements: 61.88, - functions: 64.63, - branches: 68.62, - lines: 61.82, + statements: 4.34, + functions: 4.76, + branches: 0, + lines: 4.41, }, 'packages/remote-iterables/**': { statements: 100, From 91d5741dd35cc88033a0101372eba1927a90fae5 Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Wed, 14 Jan 2026 17:34:16 -0800 Subject: [PATCH 14/17] refactor: Respond to Claude's review --- packages/kernel-browser-runtime/src/background-captp.ts | 2 +- .../src/kernel-worker/captp/kernel-facade.test.ts | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/kernel-browser-runtime/src/background-captp.ts b/packages/kernel-browser-runtime/src/background-captp.ts index d6b7da7da..3bb574987 100644 --- a/packages/kernel-browser-runtime/src/background-captp.ts +++ b/packages/kernel-browser-runtime/src/background-captp.ts @@ -53,7 +53,7 @@ export function makeCapTPNotification(captpMessage: CapTPMessage): JsonRpcCall { return { jsonrpc: '2.0', method: 'captp', - params: [captpMessage as unknown as Record], + params: [captpMessage], }; } 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 93129c781..298650d41 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 @@ -10,7 +10,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { makeKernelFacade } from './kernel-facade.ts'; import type { KernelFacade } from './kernel-facade.ts'; -const makeClusterconfig = (): ClusterConfig => ({ +const makeClusterConfig = (): ClusterConfig => ({ bootstrap: 'test-vat', vats: { 'test-vat': { bundleSpec: 'test' }, @@ -52,7 +52,7 @@ describe('makeKernelFacade', () => { describe('launchSubcluster', () => { it('delegates to kernel with correct arguments', async () => { - const config: ClusterConfig = makeClusterconfig(); + const config: ClusterConfig = makeClusterConfig(); await facade.launchSubcluster(config); @@ -66,7 +66,7 @@ describe('makeKernelFacade', () => { expectedResult, ); - const config: ClusterConfig = makeClusterconfig(); + const config: ClusterConfig = makeClusterConfig(); const result = await facade.launchSubcluster(config); expect(result).toStrictEqual(expectedResult); @@ -75,7 +75,7 @@ describe('makeKernelFacade', () => { it('propagates errors from kernel', async () => { const error = new Error('Launch failed'); vi.mocked(mockKernel.launchSubcluster).mockRejectedValueOnce(error); - const config: ClusterConfig = makeClusterconfig(); + const config: ClusterConfig = makeClusterConfig(); await expect(facade.launchSubcluster(config)).rejects.toThrow(error); }); From bef3b9d0cb525d40b2545d6df701e0c1a9426f6f Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Wed, 14 Jan 2026 19:12:05 -0800 Subject: [PATCH 15/17] refactor: Use consistent error throwing pattern for unexpected CapTP stream messages --- packages/extension/src/background.ts | 4 ++-- .../kernel-browser-runtime/src/kernel-worker/kernel-worker.ts | 4 +++- packages/omnium-gatherum/src/background.ts | 4 ++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/extension/src/background.ts b/packages/extension/src/background.ts index bb0b71358..e6b372e1f 100644 --- a/packages/extension/src/background.ts +++ b/packages/extension/src/background.ts @@ -10,7 +10,7 @@ import type { CapTPMessage, } from '@metamask/kernel-browser-runtime'; import defaultSubcluster from '@metamask/kernel-browser-runtime/default-cluster'; -import { delay, isJsonRpcMessage } from '@metamask/kernel-utils'; +import { delay, isJsonRpcMessage, stringify } from '@metamask/kernel-utils'; import type { JsonRpcMessage } from '@metamask/kernel-utils'; import { Logger } from '@metamask/logger'; import { ChromeRuntimeDuplexStream } from '@metamask/streams/browser'; @@ -129,7 +129,7 @@ async function main(): Promise { const captpMessage = getCapTPMessage(message); backgroundCapTP.dispatch(captpMessage); } else { - logger.error('Unexpected message from offscreen:', message); + throw new Error(`Unexpected message: ${stringify(message)}`); } }); drainPromise.catch(logger.error); diff --git a/packages/kernel-browser-runtime/src/kernel-worker/kernel-worker.ts b/packages/kernel-browser-runtime/src/kernel-worker/kernel-worker.ts index 1699761cc..bfdef26ab 100644 --- a/packages/kernel-browser-runtime/src/kernel-worker/kernel-worker.ts +++ b/packages/kernel-browser-runtime/src/kernel-worker/kernel-worker.ts @@ -1,6 +1,6 @@ import { JsonRpcServer } from '@metamask/json-rpc-engine/v2'; import { makeSQLKernelDatabase } from '@metamask/kernel-store/sqlite/wasm'; -import { isJsonRpcMessage } from '@metamask/kernel-utils'; +import { isJsonRpcMessage, stringify } from '@metamask/kernel-utils'; import type { JsonRpcMessage } from '@metamask/kernel-utils'; import { Logger } from '@metamask/logger'; import { Kernel } from '@metamask/ocap-kernel'; @@ -86,6 +86,8 @@ async function main(): Promise { if (isCapTPNotification(message)) { const captpMessage = message.params[0]; kernelCapTP.dispatch(captpMessage); + } else { + throw new Error(`Unexpected message: ${stringify(message)}`); } }) .catch((error) => { diff --git a/packages/omnium-gatherum/src/background.ts b/packages/omnium-gatherum/src/background.ts index 52b39a939..b32180193 100644 --- a/packages/omnium-gatherum/src/background.ts +++ b/packages/omnium-gatherum/src/background.ts @@ -6,7 +6,7 @@ import { getCapTPMessage, } from '@metamask/kernel-browser-runtime'; import type { CapTPMessage } from '@metamask/kernel-browser-runtime'; -import { delay, isJsonRpcMessage } from '@metamask/kernel-utils'; +import { delay, isJsonRpcMessage, stringify } from '@metamask/kernel-utils'; import type { JsonRpcMessage } from '@metamask/kernel-utils'; import { Logger } from '@metamask/logger'; import { ChromeRuntimeDuplexStream } from '@metamask/streams/browser'; @@ -126,7 +126,7 @@ async function main(): Promise { const captpMessage = getCapTPMessage(message); backgroundCapTP.dispatch(captpMessage); } else { - logger.error('Unexpected message from offscreen:', message); + throw new Error(`Unexpected message: ${stringify(message)}`); } }); } catch (error) { From 6e88cadd0d9bc9747bdced1ab8dee7b8caaa7a4c Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Wed, 14 Jan 2026 20:55:03 -0800 Subject: [PATCH 16/17] refactor: Make background changes compatible with re-executing main() --- packages/extension/src/background.ts | 40 ++++++++++-------- packages/omnium-gatherum/src/background.ts | 49 ++++++++++++---------- vitest.config.ts | 4 +- 3 files changed, 50 insertions(+), 43 deletions(-) diff --git a/packages/extension/src/background.ts b/packages/extension/src/background.ts index e6b372e1f..967103779 100644 --- a/packages/extension/src/background.ts +++ b/packages/extension/src/background.ts @@ -20,6 +20,13 @@ 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); +}); // Install/update chrome.runtime.onInstalled.addListener(() => { @@ -101,28 +108,13 @@ async function main(): Promise { }); // Get the kernel remote presence - const kernelP = backgroundCapTP.getKernel(); + kernelP = backgroundCapTP.getKernel(); - const ping = async (): Promise => { + ping = async () => { const result = await E(kernelP).ping(); logger.info(result); }; - Object.defineProperties(globalThis.kernel, { - ping: { - value: ping, - }, - getKernel: { - value: async () => kernelP, - }, - }); - harden(globalThis.kernel); - - // With this we can click the extension action button to wake up the service worker. - chrome.action.onClicked.addListener(() => { - ping().catch(logger.error); - }); - // Handle incoming CapTP messages from the kernel const drainPromise = offscreenStream.drain((message) => { if (isCapTPNotification(message)) { @@ -140,9 +132,11 @@ async function main(): Promise { try { await drainPromise; } catch (error) { - throw new Error('Offscreen connection closed unexpectedly', { + const finalError = new Error('Offscreen connection closed unexpectedly', { cause: error, }); + backgroundCapTP.abort(finalError); + throw finalError; } } @@ -175,6 +169,16 @@ function defineGlobals(): void { value: {}, }); + Object.defineProperties(globalThis.kernel, { + ping: { + get: () => ping, + }, + getKernel: { + value: async () => kernelP, + }, + }); + harden(globalThis.kernel); + Object.defineProperty(globalThis, 'E', { value: E, configurable: false, diff --git a/packages/omnium-gatherum/src/background.ts b/packages/omnium-gatherum/src/background.ts index b32180193..4ea6aa780 100644 --- a/packages/omnium-gatherum/src/background.ts +++ b/packages/omnium-gatherum/src/background.ts @@ -5,7 +5,10 @@ import { isCapTPNotification, getCapTPMessage, } from '@metamask/kernel-browser-runtime'; -import type { CapTPMessage } from '@metamask/kernel-browser-runtime'; +import type { + CapTPMessage, + KernelFacade, +} 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'; @@ -16,6 +19,13 @@ 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); +}); // Install/update chrome.runtime.onInstalled.addListener(() => { @@ -80,13 +90,11 @@ async function main(): Promise { // Without this delay, sending messages via the chrome.runtime API can fail. await delay(50); - // Create stream for CapTP messages const offscreenStream = await ChromeRuntimeDuplexStream.make< JsonRpcMessage, JsonRpcMessage >(chrome.runtime, 'background', 'offscreen', isJsonRpcMessage); - // Set up CapTP for E() based communication with the kernel const backgroundCapTP = makeBackgroundCapTP({ send: (captpMessage: CapTPMessage) => { const notification = makeCapTPNotification(captpMessage); @@ -96,31 +104,14 @@ async function main(): Promise { }, }); - // Get the kernel remote presence - const kernelP = backgroundCapTP.getKernel(); + kernelP = backgroundCapTP.getKernel(); - const ping = async (): Promise => { + ping = async (): Promise => { const result = await E(kernelP).ping(); logger.info(result); }; - Object.defineProperties(globalThis.omnium, { - ping: { - value: ping, - }, - getKernel: { - value: async () => kernelP, - }, - }); - harden(globalThis.omnium); - - // With this we can click the extension action button to wake up the service worker. - chrome.action.onClicked.addListener(() => { - ping().catch(logger.error); - }); - try { - // Handle incoming CapTP messages from the kernel await offscreenStream.drain((message) => { if (isCapTPNotification(message)) { const captpMessage = getCapTPMessage(message); @@ -130,9 +121,11 @@ async function main(): Promise { } }); } catch (error) { - throw new Error('Offscreen connection closed unexpectedly', { + const finalError = new Error('Offscreen connection closed unexpectedly', { cause: error, }); + backgroundCapTP.abort(finalError); + throw finalError; } } @@ -147,6 +140,16 @@ function defineGlobals(): void { value: {}, }); + Object.defineProperties(globalThis.omnium, { + ping: { + get: () => ping, + }, + getKernel: { + value: async () => kernelP, + }, + }); + harden(globalThis.omnium); + Object.defineProperty(globalThis, 'E', { configurable: false, enumerable: true, diff --git a/vitest.config.ts b/vitest.config.ts index 6fbb0d25a..07b25111d 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -87,10 +87,10 @@ export default defineConfig({ lines: 88.13, }, 'packages/kernel-browser-runtime/**': { - statements: 84.63, + statements: 84.4, functions: 78.3, branches: 81.11, - lines: 84.87, + lines: 84.63, }, 'packages/kernel-errors/**': { statements: 99.24, From fc29d6b338d6f8f8953653621718d5aa04801c95 Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Wed, 14 Jan 2026 21:14:33 -0800 Subject: [PATCH 17/17] refactor: Abort kernel-side captp connection on stream failure as well --- .../kernel-browser-runtime/src/kernel-worker/kernel-worker.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/kernel-browser-runtime/src/kernel-worker/kernel-worker.ts b/packages/kernel-browser-runtime/src/kernel-worker/kernel-worker.ts index bfdef26ab..ecc9b8ad8 100644 --- a/packages/kernel-browser-runtime/src/kernel-worker/kernel-worker.ts +++ b/packages/kernel-browser-runtime/src/kernel-worker/kernel-worker.ts @@ -91,6 +91,7 @@ async function main(): Promise { } }) .catch((error) => { + kernelCapTP.abort(error); logger.error('Message stream error:', error); });