From 00e620405364acd231b32e8ec8899e8a1667ec44 Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Tue, 27 Jan 2026 20:54:53 -0800 Subject: [PATCH 01/41] feat(ocap-kernel): Add system vat infrastructure Add core infrastructure for system vats - vats that run without compartment isolation directly in the host process. System vats enable the host application to use E() on vat object presences directly. This commit adds: - SystemVatId type and isSystemVatId() type guard - SystemSubclusterConfig and related types for configuring system subclusters - SystemVatSyscall: Handles syscalls from system vats (similar to VatSyscall) - SystemVatHandle: EndpointHandle implementation for system vats (kernel-side) - SystemVatSupervisor: Runtime-agnostic supervisor using liveslots without compartment isolation and with ephemeral (non-persistent) vatstore System vats: - Do NOT execute in a compartment - run directly in host process - Do NOT participate in kernel persistence machinery - Use an ephemeral Map-based vatstore - Receive buildRootObject function directly instead of loading bundles Co-Authored-By: Claude Opus 4.5 --- packages/ocap-kernel/src/types.ts | 58 +++- .../ocap-kernel/src/vats/SystemVatHandle.ts | 242 ++++++++++++++ .../src/vats/SystemVatSupervisor.ts | 311 ++++++++++++++++++ .../ocap-kernel/src/vats/SystemVatSyscall.ts | 287 ++++++++++++++++ 4 files changed, 894 insertions(+), 4 deletions(-) create mode 100644 packages/ocap-kernel/src/vats/SystemVatHandle.ts create mode 100644 packages/ocap-kernel/src/vats/SystemVatSupervisor.ts create mode 100644 packages/ocap-kernel/src/vats/SystemVatSyscall.ts diff --git a/packages/ocap-kernel/src/types.ts b/packages/ocap-kernel/src/types.ts index e5ad35dbe..00b7a92f5 100644 --- a/packages/ocap-kernel/src/types.ts +++ b/packages/ocap-kernel/src/types.ts @@ -40,8 +40,10 @@ import { Fail } from './utils/assert.ts'; export type VatId = string; export type RemoteId = string; -export type EndpointId = VatId | RemoteId; +export type SystemVatId = `sv${number}`; +export type EndpointId = VatId | RemoteId | SystemVatId; export type SubclusterId = string; +export type SystemSubclusterId = string; export type KRef = string; export type VRef = string; @@ -221,10 +223,13 @@ export const isRemoteId = (value: unknown): value is RemoteId => value.at(0) === 'r' && value.slice(1) === String(Number(value.slice(1))); -export const isEndpointId = (value: unknown): value is EndpointId => +export const isSystemVatId = (value: unknown): value is SystemVatId => typeof value === 'string' && - (value.at(0) === 'v' || value.at(0) === 'r') && - value.slice(1) === String(Number(value.slice(1))); + value.startsWith('sv') && + value.slice(2) === String(Number(value.slice(2))); + +export const isEndpointId = (value: unknown): value is EndpointId => + isVatId(value) || isRemoteId(value) || isSystemVatId(value); /** * Assert that a value is a valid vat id. @@ -247,6 +252,10 @@ export function insistEndpointId(value: unknown): asserts value is EndpointId { } export const VatIdStruct = define('VatId', isVatId); +export const SystemVatIdStruct = define( + 'SystemVatId', + isSystemVatId, +); export const isSubclusterId = (value: unknown): value is SubclusterId => typeof value === 'string' && @@ -422,6 +431,47 @@ export type ClusterConfig = Infer; export const isClusterConfig = (value: unknown): value is ClusterConfig => is(value, ClusterConfigStruct); +/** + * Function signature for building the root object of a system vat. + * System vats don't load bundles; they provide this function directly. + */ +export type SystemVatBuildRootObject = ( + vatPowers: Record, + parameters: Record | undefined, +) => object; + +/** + * Configuration for a single system vat within a system subcluster. + */ +export type SystemVatConfig = { + buildRootObject: SystemVatBuildRootObject; + parameters?: Record; +}; + +/** + * Configuration for launching a system subcluster. + * System subclusters contain vats that run without compartment isolation + * directly in the host process. + */ +export type SystemSubclusterConfig = { + /** The name of the bootstrap vat within the subcluster. */ + bootstrap: string; + /** Map of vat names to their configurations. */ + vats: Record; + /** Optional list of kernel service names to provide to the bootstrap vat. */ + services?: string[]; +}; + +/** + * Result of launching a system subcluster. + */ +export type SystemSubclusterLaunchResult = { + /** The ID of the launched system subcluster. */ + systemSubclusterId: SystemSubclusterId; + /** Map of vat names to their system vat IDs. */ + vatIds: Record; +}; + export const SubclusterStruct = object({ id: SubclusterIdStruct, config: ClusterConfigStruct, diff --git a/packages/ocap-kernel/src/vats/SystemVatHandle.ts b/packages/ocap-kernel/src/vats/SystemVatHandle.ts new file mode 100644 index 000000000..0658ff153 --- /dev/null +++ b/packages/ocap-kernel/src/vats/SystemVatHandle.ts @@ -0,0 +1,242 @@ +import type { + VatDeliveryObject, + VatOneResolution, + VatSyscallObject, + Message as SwingSetMessage, +} from '@agoric/swingset-liveslots'; +import type { Logger } from '@metamask/logger'; + +import type { KernelQueue } from '../KernelQueue.ts'; +import { kser, makeError } from '../liveslots/kernel-marshal.ts'; +import type { KernelStore } from '../store/index.ts'; +import type { + Message, + SystemVatId, + VRef, + CrankResults, + EndpointHandle, +} from '../types.ts'; +import { SystemVatSyscall } from './SystemVatSyscall.ts'; + +/** + * Delivery callback type - called by kernel to deliver messages to the system vat. + */ +export type SystemVatDeliverFn = ( + delivery: VatDeliveryObject, +) => Promise; + +/** + * Syscall callback type - called by system vat to send syscalls to kernel. + */ +export type SystemVatSyscallFn = (syscall: VatSyscallObject) => void; + +type SystemVatHandleProps = { + systemVatId: SystemVatId; + kernelStore: KernelStore; + kernelQueue: KernelQueue; + deliver: SystemVatDeliverFn; + logger?: Logger | undefined; +}; + +/** + * Handles communication with and lifecycle management of a system vat. + * + * System vats run without compartment isolation directly in the host process. + * They don't participate in kernel persistence machinery (no vatstore). + */ +export class SystemVatHandle implements EndpointHandle { + /** The ID of the system vat this handles */ + readonly systemVatId: SystemVatId; + + /** Logger for outputting messages (such as errors) to the console */ + readonly #logger: Logger | undefined; + + /** Storage holding the kernel's persistent state */ + readonly #kernelStore: KernelStore; + + /** The kernel's queue */ + readonly #kernelQueue: KernelQueue; + + /** The system vat's syscall handler */ + readonly #systemVatSyscall: SystemVatSyscall; + + /** Callback to deliver messages to the system vat supervisor */ + readonly #deliver: SystemVatDeliverFn; + + /** Flag indicating if this handle is active */ + #isActive: boolean = true; + + /** + * Construct a new SystemVatHandle instance. + * + * @param props - Named constructor parameters. + * @param props.systemVatId - The system vat ID. + * @param props.kernelStore - The kernel's persistent state store. + * @param props.kernelQueue - The kernel's queue. + * @param props.deliver - Callback function to deliver messages to the system vat. + * @param props.logger - Optional logger for error and diagnostic output. + */ + constructor({ + systemVatId, + kernelStore, + kernelQueue, + deliver, + logger, + }: SystemVatHandleProps) { + this.systemVatId = systemVatId; + this.#logger = logger; + this.#kernelStore = kernelStore; + this.#kernelQueue = kernelQueue; + this.#deliver = deliver; + this.#systemVatSyscall = new SystemVatSyscall({ + systemVatId, + kernelQueue, + kernelStore, + isActive: () => this.#isActive, + logger: this.#logger?.subLogger({ tags: ['syscall'] }), + }); + + harden(this); + } + + /** + * Get a syscall handler function to pass to the system vat supervisor. + * + * @returns A function that handles syscalls from the system vat. + */ + getSyscallHandler(): (syscall: VatSyscallObject) => void { + return (syscall: VatSyscallObject) => { + this.#systemVatSyscall.handleSyscall(syscall); + }; + } + + /** + * Make a 'message' delivery to the system vat. + * + * @param target - The VRef of the object to which the message is addressed. + * @param message - The message to deliver. + * @returns The crank results. + */ + async deliverMessage(target: VRef, message: Message): Promise { + // Convert our Message type to SwingSet's Message type for delivery + const swingSetMessage: SwingSetMessage = { + methargs: message.methargs, + result: message.result ?? null, + }; + const deliveryError = await this.#deliver( + harden(['message', target, swingSetMessage]), + ); + return this.#getDeliveryCrankResults(deliveryError); + } + + /** + * Make a 'notify' delivery to the system vat. + * + * @param resolutions - One or more promise resolutions to deliver. + * @returns The crank results. + */ + async deliverNotify(resolutions: VatOneResolution[]): Promise { + const deliveryError = await this.#deliver(harden(['notify', resolutions])); + return this.#getDeliveryCrankResults(deliveryError); + } + + /** + * Make a 'dropExports' delivery to the system vat. + * + * @param vrefs - The VRefs of the exports to be dropped. + * @returns The crank results. + */ + async deliverDropExports(vrefs: VRef[]): Promise { + const deliveryError = await this.#deliver(harden(['dropExports', vrefs])); + return this.#getDeliveryCrankResults(deliveryError); + } + + /** + * Make a 'retireExports' delivery to the system vat. + * + * @param vrefs - The VRefs of the exports to be retired. + * @returns The crank results. + */ + async deliverRetireExports(vrefs: VRef[]): Promise { + const deliveryError = await this.#deliver(harden(['retireExports', vrefs])); + return this.#getDeliveryCrankResults(deliveryError); + } + + /** + * Make a 'retireImports' delivery to the system vat. + * + * @param vrefs - The VRefs of the imports to be retired. + * @returns The crank results. + */ + async deliverRetireImports(vrefs: VRef[]): Promise { + const deliveryError = await this.#deliver(harden(['retireImports', vrefs])); + return this.#getDeliveryCrankResults(deliveryError); + } + + /** + * Make a 'bringOutYourDead' delivery to the system vat. + * + * @returns The crank results. + */ + async deliverBringOutYourDead(): Promise { + const deliveryError = await this.#deliver(harden(['bringOutYourDead'])); + return this.#getDeliveryCrankResults(deliveryError); + } + + /** + * Terminates the system vat handle. + * + * @param terminating - If true, the vat is being killed permanently. + * @param _error - The error to terminate with (unused for system vats). + */ + async terminate(terminating: boolean, _error?: Error): Promise { + this.#isActive = false; + if (terminating) { + // Reject promises exported to other vats for which this vat is the decider + const failure = kser(new Error('system vat terminated')); + for (const kpid of this.#kernelStore.getPromisesByDecider( + this.systemVatId, + )) { + this.#kernelQueue.resolvePromises(this.systemVatId, [ + [kpid, true, failure], + ]); + } + // Note: System vats don't have a vatStore to delete + this.#kernelStore.deleteEndpoint(this.systemVatId); + } + } + + /** + * Get the crank outcome for a delivery. + * + * @param deliveryError - The error from delivery, if any. + * @returns The crank outcome. + */ + #getDeliveryCrankResults(deliveryError: string | null): CrankResults { + const results: CrankResults = { + didDelivery: this.systemVatId, + }; + + // These conditionals express a priority order: the consequences of an + // illegal syscall take precedence over a vat requesting termination, etc. + if (this.#systemVatSyscall.illegalSyscall) { + results.abort = true; + const { info } = this.#systemVatSyscall.illegalSyscall; + results.terminate = { vatId: this.systemVatId, reject: true, info }; + } else if (deliveryError) { + results.abort = true; + const info = makeError(deliveryError); + results.terminate = { vatId: this.systemVatId, reject: true, info }; + } else if (this.#systemVatSyscall.vatRequestedTermination) { + if (this.#systemVatSyscall.vatRequestedTermination.reject) { + results.abort = true; + } + results.terminate = { + vatId: this.systemVatId, + ...this.#systemVatSyscall.vatRequestedTermination, + }; + } + + return harden(results); + } +} diff --git a/packages/ocap-kernel/src/vats/SystemVatSupervisor.ts b/packages/ocap-kernel/src/vats/SystemVatSupervisor.ts new file mode 100644 index 000000000..01782afef --- /dev/null +++ b/packages/ocap-kernel/src/vats/SystemVatSupervisor.ts @@ -0,0 +1,311 @@ +import { makeLiveSlots as localMakeLiveSlots } from '@agoric/swingset-liveslots'; +import type { + VatDeliveryObject, + VatSyscallObject, + VatSyscallResult, +} from '@agoric/swingset-liveslots'; +import { makeMarshal } from '@endo/marshal'; +import type { CapData } from '@endo/marshal'; +import type { KVStore } from '@metamask/kernel-store'; +import { waitUntilQuiescent } from '@metamask/kernel-utils'; +import type { Logger } from '@metamask/logger'; +import type { Json } from '@metamask/utils'; + +import { makeGCAndFinalize } from '../garbage-collection/gc-finalize.ts'; +import { makeDummyMeterControl } from '../liveslots/meter-control.ts'; +import type { + DispatchFn, + MakeLiveSlotsFn, + GCTools, + Syscall, + SyscallResult, +} from '../liveslots/types.ts'; +import type { SystemVatId, SystemVatBuildRootObject } from '../types.ts'; + +const makeLiveSlots: MakeLiveSlotsFn = localMakeLiveSlots; + +const marshal = makeMarshal(undefined, undefined, { + serializeBodyFormat: 'smallcaps', +}); + +/** + * Syscall executor type - synchronously handles syscalls from the system vat. + */ +export type SystemVatExecuteSyscall = ( + vso: VatSyscallObject, +) => VatSyscallResult; + +type SystemVatSupervisorProps = { + id: SystemVatId; + buildRootObject: SystemVatBuildRootObject; + vatPowers: Record; + parameters: Record | undefined; + executeSyscall: SystemVatExecuteSyscall; + logger: Logger; +}; + +/** + * A non-persistent KV store for system vats. + * System vats don't participate in kernel persistence machinery, + * so their vatstore is ephemeral (Map-based). + * + * @returns An ephemeral KVStore implementation. + */ +function makeEphemeralVatKVStore(): KVStore { + const data = new Map(); + + return harden({ + has(key: string): boolean { + return data.has(key); + }, + get(key: string): string | undefined { + return data.get(key); + }, + getRequired(key: string): string { + const value = data.get(key); + if (value === undefined) { + throw Error(`key "${key}" not found`); + } + return value; + }, + set(key: string, value: string): void { + data.set(key, value); + }, + delete(key: string): void { + data.delete(key); + }, + getNextKey(previousKey: string): string | undefined { + const keys = [...data.keys()].sort(); + const index = keys.indexOf(previousKey); + if (index === -1) { + // If key not found, find first key greater than previousKey + return keys.find((k) => k > previousKey); + } + return keys[index + 1]; + }, + getKeys(_start: string, _end: string): Iterable { + throw new Error('getKeys not supported for ephemeral store'); + }, + getPrefixedKeys(_prefix: string): Iterable { + throw new Error('getPrefixedKeys not supported for ephemeral store'); + }, + }); +} + +/** + * Supervises a system vat's execution. + * + * System vats run without compartment isolation directly in the host process. + * They don't load bundles via importBundle; instead, they receive a + * buildRootObject function directly. They use an ephemeral vatstore since + * they don't participate in kernel persistence machinery. + */ +export class SystemVatSupervisor { + /** The ID of the system vat being supervised */ + readonly id: SystemVatId; + + /** The logger for this system vat */ + readonly #logger: Logger; + + /** Function to dispatch deliveries into liveslots */ + #dispatch: DispatchFn | null = null; + + /** Flag indicating if the system vat has been initialized */ + #initialized: boolean = false; + + /** + * Construct a new SystemVatSupervisor instance. + * + * @param props - Named constructor parameters. + * @param props.id - The ID of the system vat being supervised. + * @param props.buildRootObject - Function to build the vat's root object. + * @param props.vatPowers - External capabilities for this system vat. + * @param props.parameters - Parameters to pass to buildRootObject. + * @param props.executeSyscall - Function to execute syscalls synchronously. + * @param props.logger - The logger for this system vat. + */ + constructor({ + id, + buildRootObject, + vatPowers, + parameters, + executeSyscall, + logger, + }: SystemVatSupervisorProps) { + this.id = id; + this.#logger = logger; + + // Initialize the system vat synchronously during construction + this.#initializeVat(buildRootObject, vatPowers, parameters, executeSyscall); + } + + /** + * Initialize the system vat by creating liveslots with the provided buildRootObject. + * + * @param buildRootObject - Function to build the vat's root object. + * @param vatPowers - External capabilities for this system vat. + * @param parameters - Parameters to pass to buildRootObject. + * @param executeSyscall - Function to execute syscalls synchronously. + */ + #initializeVat( + buildRootObject: SystemVatBuildRootObject, + vatPowers: Record, + parameters: Record | undefined, + executeSyscall: SystemVatExecuteSyscall, + ): void { + if (this.#initialized) { + throw Error('SystemVatSupervisor already initialized'); + } + this.#initialized = true; + + const kvStore = makeEphemeralVatKVStore(); + const syscall = this.#makeSyscall(executeSyscall, kvStore); + const liveSlotsOptions = {}; + + const gcTools: GCTools = harden({ + WeakRef, + FinalizationRegistry, + waitUntilQuiescent, + gcAndFinalize: makeGCAndFinalize( + this.#logger.subLogger({ tags: ['gc'] }), + ), + meterControl: makeDummyMeterControl(), + }); + + // For system vats, buildVatNamespace returns the buildRootObject directly + // without loading a bundle via importBundle + const buildVatNamespace = async ( + lsEndowments: Record, + _inescapableGlobalProperties: object, + ): Promise> => { + // Provide liveslots endowments as vatPowers to the buildRootObject + const combinedVatPowers = { + ...vatPowers, + ...lsEndowments, + }; + + // Return a namespace object with buildRootObject that wraps the provided one + // to inject the combined vatPowers + return { + buildRootObject: (innerVatPowers: Record) => { + // Merge the inner vatPowers (from liveslots) with our combined powers + const finalVatPowers = { ...combinedVatPowers, ...innerVatPowers }; + return buildRootObject(finalVatPowers, parameters); + }, + }; + }; + + const liveslots = makeLiveSlots( + syscall, + this.id, + vatPowers, + liveSlotsOptions, + gcTools, + this.#logger.subLogger({ tags: ['liveslots'] }), + buildVatNamespace, + ); + + this.#dispatch = liveslots.dispatch; + } + + /** + * Create a syscall interface for the system vat. + * + * @param executeSyscall - Function to execute syscalls to the kernel. + * @param kv - The ephemeral KV store for this system vat. + * @returns A syscall object for liveslots. + */ + #makeSyscall(executeSyscall: SystemVatExecuteSyscall, kv: KVStore): Syscall { + const doSyscall = (vso: VatSyscallObject): SyscallResult => { + let syscallResult; + try { + syscallResult = executeSyscall(vso); + } catch (problem) { + this.#logger.warn(`system vat got error during syscall:`, problem); + throw problem; + } + const [type, ...rest] = syscallResult; + switch (type) { + case 'ok': { + const [data] = rest; + return data; + } + case 'error': { + const [problem] = rest; + throw Error(`syscall.${vso[0]} failed: ${problem as string}`); + } + default: + throw Error(`unknown result type ${type as string}`); + } + }; + + return harden({ + send: (target: string, methargs: CapData, result?: string) => + doSyscall(['send', target, { methargs, result }]), + subscribe: (vpid: string) => doSyscall(['subscribe', vpid]), + resolve: (resolutions) => doSyscall(['resolve', resolutions]), + exit: (isFailure: boolean, info: CapData) => + doSyscall(['exit', isFailure, info]), + dropImports: (vrefs: string[]) => doSyscall(['dropImports', vrefs]), + retireImports: (vrefs: string[]) => doSyscall(['retireImports', vrefs]), + retireExports: (vrefs: string[]) => doSyscall(['retireExports', vrefs]), + abandonExports: (vrefs: string[]) => doSyscall(['abandonExports', vrefs]), + callNow: () => { + throw Error(`callNow not supported for system vats`); + }, + // System vats use an ephemeral vatstore (non-persistent) + vatstoreGet: (key: string) => kv.get(key), + vatstoreGetNextKey: (priorKey: string) => kv.getNextKey(priorKey), + vatstoreSet: (key: string, value: string) => kv.set(key, value), + vatstoreDelete: (key: string) => kv.delete(key), + }); + } + + /** + * Start the system vat by dispatching the startVat delivery. + * + * @returns A promise that resolves when the vat has started, or null on error. + */ + async start(): Promise { + if (!this.#dispatch) { + throw new Error('SystemVatSupervisor not initialized'); + } + + let deliveryError: string | null = null; + try { + const serParam = marshal.toCapData(harden(undefined)) as CapData; + await this.#dispatch(harden(['startVat', serParam])); + } catch (error) { + deliveryError = error instanceof Error ? error.message : String(error); + this.#logger.error( + `Start error in system vat ${this.id}:`, + deliveryError, + ); + } + return deliveryError; + } + + /** + * Deliver a message to the system vat. + * + * @param delivery - The delivery object to dispatch. + * @returns A promise that resolves to the delivery error (null if success). + */ + async deliver(delivery: VatDeliveryObject): Promise { + if (!this.#dispatch) { + throw new Error('SystemVatSupervisor not initialized'); + } + + let deliveryError: string | null = null; + try { + await this.#dispatch(harden(delivery)); + } catch (error) { + deliveryError = error instanceof Error ? error.message : String(error); + this.#logger.error( + `Delivery error in system vat ${this.id}:`, + deliveryError, + ); + } + return deliveryError; + } +} diff --git a/packages/ocap-kernel/src/vats/SystemVatSyscall.ts b/packages/ocap-kernel/src/vats/SystemVatSyscall.ts new file mode 100644 index 000000000..712d6b8e6 --- /dev/null +++ b/packages/ocap-kernel/src/vats/SystemVatSyscall.ts @@ -0,0 +1,287 @@ +import type { + SwingSetCapData, + VatOneResolution, + VatSyscallObject, + VatSyscallResult, +} from '@agoric/swingset-liveslots'; +import { Logger } from '@metamask/logger'; + +import { + performDropImports, + performRetireImports, + performExportCleanup, +} from '../garbage-collection/gc-handlers.ts'; +import type { KernelQueue } from '../KernelQueue.ts'; +import { makeError } from '../liveslots/kernel-marshal.ts'; +import type { KernelStore } from '../store/index.ts'; +import { coerceMessage } from '../types.ts'; +import type { Message, SystemVatId, KRef } from '../types.ts'; + +type SystemVatSyscallProps = { + systemVatId: SystemVatId; + kernelQueue: KernelQueue; + kernelStore: KernelStore; + isActive: () => boolean; + logger?: Logger | undefined; +}; + +/** + * Handles syscalls from a system vat. + * + * Similar to VatSyscall but for system vats. System vats run without + * compartment isolation directly in the host process and don't participate + * in kernel persistence machinery. + */ +export class SystemVatSyscall { + /** The ID of the system vat */ + readonly systemVatId: SystemVatId; + + /** The kernel's run queue */ + readonly #kernelQueue: KernelQueue; + + /** The kernel's store */ + readonly #kernelStore: KernelStore; + + /** Logger for outputting messages (such as errors) to the console */ + readonly #logger: Logger | undefined; + + /** Function to check if the system vat is still active */ + readonly #isActive: () => boolean; + + /** The illegal syscall that was received */ + illegalSyscall: { vatId: SystemVatId; info: SwingSetCapData } | undefined; + + /** The error when delivery failed */ + deliveryError: string | undefined; + + /** The termination request that was received from the vat with syscall.exit() */ + vatRequestedTermination: + | { reject: boolean; info: SwingSetCapData } + | undefined; + + /** + * Construct a new SystemVatSyscall instance. + * + * @param props - The properties for the SystemVatSyscall. + * @param props.systemVatId - The ID of the system vat. + * @param props.kernelQueue - The kernel's run queue. + * @param props.kernelStore - The kernel's store. + * @param props.isActive - Function to check if the system vat is still active. + * @param props.logger - The logger for the SystemVatSyscall. + */ + constructor({ + systemVatId, + kernelQueue, + kernelStore, + isActive, + logger, + }: SystemVatSyscallProps) { + this.systemVatId = systemVatId; + this.#kernelQueue = kernelQueue; + this.#kernelStore = kernelStore; + this.#isActive = isActive; + this.#logger = logger; + } + + /** + * Handle a 'send' syscall from the system vat. + * + * @param target - The target of the message send. + * @param message - The message that was sent. + */ + #handleSyscallSend(target: KRef, message: Message): void { + this.#kernelQueue.enqueueSend(target, message); + } + + /** + * Handle a 'resolve' syscall from the system vat. + * + * @param resolutions - One or more promise resolutions. + */ + #handleSyscallResolve(resolutions: VatOneResolution[]): void { + this.#kernelQueue.resolvePromises(this.systemVatId, resolutions); + } + + /** + * Handle a 'subscribe' syscall from the system vat. + * + * @param kpid - The KRef of the promise being subscribed to. + */ + #handleSyscallSubscribe(kpid: KRef): void { + const kp = this.#kernelStore.getKernelPromise(kpid); + if (kp.state === 'unresolved') { + this.#kernelStore.addPromiseSubscriber(this.systemVatId, kpid); + } else { + this.#kernelQueue.enqueueNotify(this.systemVatId, kpid); + } + } + + /** + * Handle a 'dropImports' syscall from the system vat. + * + * @param krefs - The KRefs of the imports to be dropped. + */ + #handleSyscallDropImports(krefs: KRef[]): void { + performDropImports(krefs, this.systemVatId, this.#kernelStore); + } + + /** + * Handle a 'retireImports' syscall from the system vat. + * + * @param krefs - The KRefs of the imports to be retired. + */ + #handleSyscallRetireImports(krefs: KRef[]): void { + performRetireImports(krefs, this.systemVatId, this.#kernelStore); + } + + /** + * Handle retiring or abandoning exports syscall from the system vat. + * + * @param krefs - The KRefs of the exports to be retired/abandoned. + * @param checkReachable - If true, verify the object is not reachable + * (retire). If false, ignore reachability (abandon). + */ + #handleSyscallExportCleanup(krefs: KRef[], checkReachable: boolean): void { + performExportCleanup( + krefs, + checkReachable, + this.systemVatId, + this.#kernelStore, + ); + + const action = checkReachable ? 'retire' : 'abandon'; + for (const kref of krefs) { + this.#logger?.debug(`${action}Exports: deleted object ${kref}`); + } + } + + /** + * Handle a syscall from the system vat. + * + * @param vso - The syscall that was received. + * @returns The result of the syscall. + */ + handleSyscall(vso: VatSyscallObject): VatSyscallResult { + try { + this.illegalSyscall = undefined; + this.vatRequestedTermination = undefined; + + // Check if the system vat is still active + if (!this.#isActive()) { + this.#recordVatFatalSyscall('system vat not found'); + return harden(['error', 'system vat not found']); + } + + const kso: VatSyscallObject = this.#kernelStore.translateSyscallVtoK( + this.systemVatId, + vso, + ); + const [op] = kso; + const { systemVatId } = this; + switch (op) { + case 'send': { + const [, target, message] = kso; + this.#logger?.log( + `@@@@ ${systemVatId} syscall send ${target}<-${JSON.stringify(message)}`, + ); + this.#handleSyscallSend(target, coerceMessage(message)); + break; + } + case 'subscribe': { + const [, promise] = kso; + this.#logger?.log(`@@@@ ${systemVatId} syscall subscribe ${promise}`); + this.#handleSyscallSubscribe(promise); + break; + } + case 'resolve': { + const [, resolutions] = kso; + this.#logger?.log( + `@@@@ ${systemVatId} syscall resolve ${JSON.stringify(resolutions)}`, + ); + this.#handleSyscallResolve(resolutions as VatOneResolution[]); + break; + } + case 'exit': { + const [, isFailure, info] = kso; + this.#logger?.log( + `@@@@ ${systemVatId} syscall exit fail=${isFailure} ${JSON.stringify(info)}`, + ); + this.vatRequestedTermination = { reject: isFailure, info }; + break; + } + case 'dropImports': { + const [, refs] = kso; + this.#logger?.log( + `@@@@ ${systemVatId} syscall dropImports ${JSON.stringify(refs)}`, + ); + this.#handleSyscallDropImports(refs); + break; + } + case 'retireImports': { + const [, refs] = kso; + this.#logger?.log( + `@@@@ ${systemVatId} syscall retireImports ${JSON.stringify(refs)}`, + ); + this.#handleSyscallRetireImports(refs); + break; + } + case 'retireExports': { + const [, refs] = kso; + this.#logger?.log( + `@@@@ ${systemVatId} syscall retireExports ${JSON.stringify(refs)}`, + ); + this.#handleSyscallExportCleanup(refs, true); + break; + } + case 'abandonExports': { + const [, refs] = kso; + this.#logger?.log( + `@@@@ ${systemVatId} syscall abandonExports ${JSON.stringify(refs)}`, + ); + this.#handleSyscallExportCleanup(refs, false); + break; + } + case 'callNow': + case 'vatstoreGet': + case 'vatstoreGetNextKey': + case 'vatstoreSet': + case 'vatstoreDelete': { + // System vats don't support vatstore operations (they're non-durable) + this.#logger?.warn( + `system vat ${systemVatId} issued unsupported syscall ${op}`, + vso, + ); + break; + } + default: + // Compile-time exhaustiveness check + this.#logger?.warn( + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + `system vat ${systemVatId} issued unknown syscall ${op}`, + vso, + ); + break; + } + return harden(['ok', null]); + } catch (error) { + this.#logger?.error( + `Fatal syscall error in system vat ${this.systemVatId}`, + error, + ); + this.#recordVatFatalSyscall('syscall translation error: prepare to die'); + return harden([ + 'error', + error instanceof Error ? error.message : String(error), + ]); + } + } + + /** + * Log a fatal syscall error and set the illegalSyscall property. + * + * @param error - The error message to log. + */ + #recordVatFatalSyscall(error: string): void { + this.illegalSyscall = { vatId: this.systemVatId, info: makeError(error) }; + } +} From d7c7b42959cb2db1d0bbcdc442a41868fed5afdb Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Tue, 27 Jan 2026 20:57:40 -0800 Subject: [PATCH 02/41] feat(ocap-kernel): Add KernelFacetService and SystemSubclusterManager Add components for managing system subclusters: - KernelFacetService: A service object exposed as vatpower to system subcluster bootstrap vats. Provides privileged kernel operations: - launchSubcluster() - launches dynamic subclusters, returns presences - terminateSubcluster() - reloadSubcluster() - getSubcluster() / getSubclusters() - getStatus() - SystemSubclusterManager: Manages system subcluster lifecycle: - Launches system vats with correct vatpowers - Bootstrap vat receives kernel facet - Coordinates with kernel store for clist management - Tracks active system subclusters and their vats Co-Authored-By: Claude Opus 4.5 --- .../src/services/KernelFacetService.ts | 143 ++++++++ .../src/vats/SystemSubclusterManager.ts | 336 ++++++++++++++++++ 2 files changed, 479 insertions(+) create mode 100644 packages/ocap-kernel/src/services/KernelFacetService.ts create mode 100644 packages/ocap-kernel/src/vats/SystemSubclusterManager.ts diff --git a/packages/ocap-kernel/src/services/KernelFacetService.ts b/packages/ocap-kernel/src/services/KernelFacetService.ts new file mode 100644 index 000000000..88a7ce2da --- /dev/null +++ b/packages/ocap-kernel/src/services/KernelFacetService.ts @@ -0,0 +1,143 @@ +import type { CapData } from '@endo/marshal'; +import { makeDefaultExo } from '@metamask/kernel-utils'; +import type { Logger } from '@metamask/logger'; + +import { kslot } from '../liveslots/kernel-marshal.ts'; +import type { SlotValue } from '../liveslots/kernel-marshal.ts'; +import type { + KRef, + ClusterConfig, + Subcluster, + KernelStatus, +} from '../types.ts'; + +/** + * Dependencies required by KernelFacetService. + */ +export type KernelFacetDependencies = { + launchSubcluster: (config: ClusterConfig) => Promise<{ + subclusterId: string; + bootstrapRootKref: KRef; + bootstrapResult: CapData | undefined; + }>; + terminateSubcluster: (subclusterId: string) => Promise; + reloadSubcluster: (subclusterId: string) => Promise; + getSubcluster: (subclusterId: string) => Subcluster | undefined; + getSubclusters: () => Subcluster[]; + getStatus: () => Promise; + logger?: Logger; +}; + +/** + * Result of launching a subcluster via the kernel facet. + * Contains the root object as a slot value (which will become a presence). + */ +export type KernelFacetLaunchResult = { + subclusterId: string; + root: SlotValue; +}; + +/** + * Creates a kernel facet service object that provides privileged kernel + * operations to system subclusters. + * + * The kernel facet is provided as a vatpower to the bootstrap vat of a + * system subcluster. It enables the bootstrap vat to: + * - Launch dynamic subclusters (and receive E()-callable presences) + * - Terminate subclusters + * - Reload subclusters + * - Query kernel status + * + * @param deps - Dependencies for the kernel facet service. + * @returns The kernel facet service object. + */ +export function makeKernelFacetService(deps: KernelFacetDependencies): object { + const { + launchSubcluster, + terminateSubcluster, + reloadSubcluster, + getSubcluster, + getSubclusters, + getStatus, + logger, + } = deps; + + const kernelFacet = makeDefaultExo('kernelFacet', { + /** + * Launch a dynamic subcluster. + * + * @param config - Configuration for the subcluster. + * @returns A promise for the launch result containing subclusterId and root presence. + */ + async launchSubcluster( + config: ClusterConfig, + ): Promise { + logger?.log(`kernelFacet: launching subcluster`, config.bootstrap); + const result = await launchSubcluster(config); + logger?.log( + `kernelFacet: launched subcluster ${result.subclusterId} with root ${result.bootstrapRootKref}`, + ); + + // Convert the kref to a slot value that will become a presence + // when marshalled/delivered to the system vat + return { + subclusterId: result.subclusterId, + root: kslot(result.bootstrapRootKref, 'vatRoot'), + }; + }, + + /** + * Terminate a subcluster. + * + * @param subclusterId - ID of the subcluster to terminate. + */ + async terminateSubcluster(subclusterId: string): Promise { + logger?.log(`kernelFacet: terminating subcluster ${subclusterId}`); + await terminateSubcluster(subclusterId); + logger?.log(`kernelFacet: terminated subcluster ${subclusterId}`); + }, + + /** + * Reload a subcluster by terminating and relaunching all its vats. + * + * @param subclusterId - ID of the subcluster to reload. + * @returns The reloaded subcluster information. + */ + async reloadSubcluster(subclusterId: string): Promise { + logger?.log(`kernelFacet: reloading subcluster ${subclusterId}`); + const result = await reloadSubcluster(subclusterId); + logger?.log(`kernelFacet: reloaded subcluster, new id: ${result.id}`); + return result; + }, + + /** + * Get information about a specific subcluster. + * + * @param subclusterId - ID of the subcluster to query. + * @returns The subcluster information or undefined if not found. + */ + getSubcluster(subclusterId: string): Subcluster | undefined { + return getSubcluster(subclusterId); + }, + + /** + * Get information about all subclusters. + * + * @returns Array of all subcluster information records. + */ + getSubclusters(): Subcluster[] { + return getSubclusters(); + }, + + /** + * Get the current kernel status. + * + * @returns A promise for the kernel status. + */ + async getStatus(): Promise { + return getStatus(); + }, + }); + + return kernelFacet; +} diff --git a/packages/ocap-kernel/src/vats/SystemSubclusterManager.ts b/packages/ocap-kernel/src/vats/SystemSubclusterManager.ts new file mode 100644 index 000000000..fb0e7441d --- /dev/null +++ b/packages/ocap-kernel/src/vats/SystemSubclusterManager.ts @@ -0,0 +1,336 @@ +import type { Logger } from '@metamask/logger'; + +import type { KernelQueue } from '../KernelQueue.ts'; +import { kslot } from '../liveslots/kernel-marshal.ts'; +import type { SlotValue } from '../liveslots/kernel-marshal.ts'; +import { makeKernelFacetService } from '../services/KernelFacetService.ts'; +import type { KernelFacetDependencies } from '../services/KernelFacetService.ts'; +import type { KernelStore } from '../store/index.ts'; +import type { + SystemVatId, + SystemSubclusterId, + SystemSubclusterConfig, + SystemSubclusterLaunchResult, + KRef, +} from '../types.ts'; +import { ROOT_OBJECT_VREF } from '../types.ts'; +import type { SystemVatDeliverFn } from './SystemVatHandle.ts'; +import { SystemVatHandle } from './SystemVatHandle.ts'; +import { SystemVatSupervisor } from './SystemVatSupervisor.ts'; + +/** + * Callback type for connecting a system vat supervisor to the kernel. + * Called when a system vat is launched. + */ +export type SystemVatConnectFn = ( + systemVatId: SystemVatId, + deliver: SystemVatDeliverFn, +) => SystemVatHandle; + +type SystemSubclusterManagerOptions = { + kernelStore: KernelStore; + kernelQueue: KernelQueue; + kernelFacetDeps: KernelFacetDependencies; + logger?: Logger; +}; + +type SystemSubclusterRecord = { + id: SystemSubclusterId; + config: SystemSubclusterConfig; + vatIds: Record; + handles: Map; + supervisors: Map; +}; + +/** + * Manages system subclusters - subclusters whose vats run without compartment + * isolation directly in the host process. + * + * System vats: + * - Run without compartment isolation + * - Don't participate in kernel persistence machinery + * - The bootstrap vat receives a kernel facet as a vatpower + */ +export class SystemSubclusterManager { + /** Storage holding the kernel's persistent state */ + readonly #kernelStore: KernelStore; + + /** The kernel's run queue */ + readonly #kernelQueue: KernelQueue; + + /** Dependencies for creating kernel facet services */ + readonly #kernelFacetDeps: KernelFacetDependencies; + + /** Logger for outputting messages to the console */ + readonly #logger: Logger | undefined; + + /** Counter for allocating system vat IDs */ + #nextSystemVatId: number = 0; + + /** Counter for allocating system subcluster IDs */ + #nextSystemSubclusterId: number = 0; + + /** Active system subclusters */ + readonly #subclusters: Map = + new Map(); + + /** + * Creates a new SystemSubclusterManager instance. + * + * @param options - Constructor options. + * @param options.kernelStore - The kernel's persistent state store. + * @param options.kernelQueue - The kernel's message queue. + * @param options.kernelFacetDeps - Dependencies for the kernel facet service. + * @param options.logger - Logger instance for debugging and diagnostics. + */ + constructor({ + kernelStore, + kernelQueue, + kernelFacetDeps, + logger, + }: SystemSubclusterManagerOptions) { + this.#kernelStore = kernelStore; + this.#kernelQueue = kernelQueue; + this.#kernelFacetDeps = kernelFacetDeps; + this.#logger = logger; + harden(this); + } + + /** + * Allocate a new system vat ID. + * + * @returns A new system vat ID. + */ + #allocateSystemVatId(): SystemVatId { + const id = `sv${this.#nextSystemVatId}`; + this.#nextSystemVatId += 1; + return id; + } + + /** + * Allocate a new system subcluster ID. + * + * @returns A new system subcluster ID. + */ + #allocateSystemSubclusterId(): SystemSubclusterId { + const id = `ss${this.#nextSystemSubclusterId}`; + this.#nextSystemSubclusterId += 1; + return id; + } + + /** + * Launch a system subcluster. + * + * @param config - Configuration for the system subcluster. + * @returns A promise for the launch result. + */ + async launchSystemSubcluster( + config: SystemSubclusterConfig, + ): Promise { + await this.#kernelQueue.waitForCrank(); + + if (!config.vats[config.bootstrap]) { + throw Error(`invalid bootstrap vat name ${config.bootstrap}`); + } + + const systemSubclusterId = this.#allocateSystemSubclusterId(); + const vatIds: Record = {}; + const handles = new Map(); + const supervisors = new Map(); + const rootKrefs: Record = {}; + + // Create kernel facet for the bootstrap vat + const kernelFacet = makeKernelFacetService(this.#kernelFacetDeps); + + // Launch all system vats + for (const [vatName, vatConfig] of Object.entries(config.vats)) { + const systemVatId = this.#allocateSystemVatId(); + vatIds[vatName] = systemVatId; + + // Initialize the endpoint in the kernel store + this.#kernelStore.initEndpoint(systemVatId); + + // Determine vatpowers - bootstrap vat gets the kernel facet + const isBootstrap = vatName === config.bootstrap; + const vatPowers: Record = isBootstrap + ? { kernelFacet } + : {}; + + // Create the system vat handle (kernel-side) + // We need the deliver function from the supervisor, so we create + // a deferred connection + let supervisorDeliver: SystemVatDeliverFn | null = null; + const deliver: SystemVatDeliverFn = async (delivery) => { + if (!supervisorDeliver) { + throw new Error('System vat supervisor not connected'); + } + return supervisorDeliver(delivery); + }; + + const handle = new SystemVatHandle({ + systemVatId, + kernelStore: this.#kernelStore, + kernelQueue: this.#kernelQueue, + deliver, + logger: this.#logger?.subLogger({ tags: [systemVatId] }), + }); + handles.set(systemVatId, handle); + + // Create the supervisor (which runs liveslots) + const supervisorLogger = this.#logger?.subLogger({ + tags: [systemVatId, 'supervisor'], + }); + if (!supervisorLogger) { + throw new Error('Logger required for system vat supervisor'); + } + const supervisor = new SystemVatSupervisor({ + id: systemVatId, + buildRootObject: vatConfig.buildRootObject, + vatPowers, + parameters: vatConfig.parameters, + executeSyscall: (vso) => + handle.getSyscallHandler()(vso) ?? harden(['ok', null]), + logger: supervisorLogger, + }); + supervisors.set(systemVatId, supervisor); + + // Connect the supervisor's deliver function to the handle + supervisorDeliver = supervisor.deliver.bind(supervisor); + + // Start the vat + const startError = await supervisor.start(); + if (startError) { + throw new Error(`Failed to start system vat ${vatName}: ${startError}`); + } + + // Get the root kref (the root object is exported at o+0) + const existingRootKref = this.#kernelStore.erefToKref( + systemVatId, + ROOT_OBJECT_VREF, + ); + if (existingRootKref) { + rootKrefs[vatName] = existingRootKref; + } else { + // Initialize the root object in the clist + const newRootKref = this.#kernelStore.initKernelObject(systemVatId); + this.#kernelStore.addCListEntry( + systemVatId, + newRootKref, + ROOT_OBJECT_VREF, + ); + rootKrefs[vatName] = newRootKref; + } + } + + // Store the subcluster record + const record: SystemSubclusterRecord = { + id: systemSubclusterId, + config, + vatIds, + handles, + supervisors, + }; + this.#subclusters.set(systemSubclusterId, record); + + // Build roots object for bootstrap + const roots: Record = {}; + for (const [vatName, kref] of Object.entries(rootKrefs)) { + roots[vatName] = kslot(kref, 'vatRoot'); + } + + // Build services object + const services: Record = {}; + if (config.services) { + for (const serviceName of config.services) { + const serviceKref = this.#kernelStore.kv.get( + `kernelService.${serviceName}`, + ); + if (serviceKref) { + services[serviceName] = kslot(serviceKref); + } else { + this.#logger?.warn(`Kernel service '${serviceName}' not found`); + } + } + } + + // Call bootstrap on the bootstrap vat's root object + const bootstrapVatId = vatIds[config.bootstrap]; + if (!bootstrapVatId) { + throw new Error(`Bootstrap vat ID not found for ${config.bootstrap}`); + } + + await this.#kernelQueue.enqueueMessage( + rootKrefs[config.bootstrap] as KRef, + 'bootstrap', + [roots, services], + ); + + return { + systemSubclusterId, + vatIds, + }; + } + + /** + * Terminate a system subcluster. + * + * @param systemSubclusterId - ID of the system subcluster to terminate. + */ + async terminateSystemSubcluster( + systemSubclusterId: SystemSubclusterId, + ): Promise { + await this.#kernelQueue.waitForCrank(); + + const record = this.#subclusters.get(systemSubclusterId); + if (!record) { + throw Error(`System subcluster ${systemSubclusterId} not found`); + } + + // Terminate all handles + for (const handle of record.handles.values()) { + await handle.terminate(true); + } + + this.#subclusters.delete(systemSubclusterId); + } + + /** + * Get a system vat handle by ID. + * + * @param systemVatId - The system vat ID. + * @returns The system vat handle or undefined if not found. + */ + getSystemVatHandle(systemVatId: SystemVatId): SystemVatHandle | undefined { + for (const record of this.#subclusters.values()) { + const handle = record.handles.get(systemVatId); + if (handle) { + return handle; + } + } + return undefined; + } + + /** + * Get all system vat IDs. + * + * @returns Array of all system vat IDs. + */ + getSystemVatIds(): SystemVatId[] { + const ids: SystemVatId[] = []; + for (const record of this.#subclusters.values()) { + ids.push(...record.handles.keys()); + } + return ids; + } + + /** + * Check if a system vat is active. + * + * @param systemVatId - The system vat ID to check. + * @returns True if the system vat is active. + */ + isSystemVatActive(systemVatId: SystemVatId): boolean { + return this.getSystemVatHandle(systemVatId) !== undefined; + } +} +harden(SystemSubclusterManager); From 65aac807a20dc1f725396d998c5ed1c9c22e0188 Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Tue, 27 Jan 2026 21:00:48 -0800 Subject: [PATCH 03/41] feat(ocap-kernel): Update Kernel.ts for system subclusters - Add SystemSubclusterManager to Kernel class - Update #getEndpoint() to handle system vat IDs (sv${number}) - Add launchSystemSubcluster() and terminateSystemSubcluster() methods - Rename makeKernelFacetService to makeKernelFacet (utility, not a service) - Move kernel-facet.ts to vats/ directory (closer to where it is used) The kernel facet is a utility that creates an object providing privileged kernel operations to system subcluster bootstrap vats, not a platform-level service like those in KernelServiceManager. Co-Authored-By: Claude Opus 4.5 --- packages/ocap-kernel/src/Kernel.ts | 64 ++++++++++++++++++- .../src/vats/SystemSubclusterManager.ts | 6 +- .../kernel-facet.ts} | 11 ++-- 3 files changed, 70 insertions(+), 11 deletions(-) rename packages/ocap-kernel/src/{services/KernelFacetService.ts => vats/kernel-facet.ts} (92%) diff --git a/packages/ocap-kernel/src/Kernel.ts b/packages/ocap-kernel/src/Kernel.ts index e759f5001..6bcef262a 100644 --- a/packages/ocap-kernel/src/Kernel.ts +++ b/packages/ocap-kernel/src/Kernel.ts @@ -14,18 +14,22 @@ import { makeKernelStore } from './store/index.ts'; import type { KernelStore } from './store/index.ts'; import type { VatId, + SystemVatId, EndpointId, KRef, PlatformServices, ClusterConfig, + SystemSubclusterConfig, VatConfig, KernelStatus, Subcluster, SubclusterLaunchResult, + SystemSubclusterLaunchResult, EndpointHandle, } from './types.ts'; -import { isVatId, isRemoteId } from './types.ts'; +import { isVatId, isRemoteId, isSystemVatId } from './types.ts'; import { SubclusterManager } from './vats/SubclusterManager.ts'; +import { SystemSubclusterManager } from './vats/SystemSubclusterManager.ts'; import type { VatHandle } from './vats/VatHandle.ts'; import { VatManager } from './vats/VatManager.ts'; @@ -49,6 +53,9 @@ export class Kernel { /** Manages subcluster operations */ readonly #subclusterManager: SubclusterManager; + /** Manages system subcluster operations */ + readonly #systemSubclusterManager: SystemSubclusterManager; + /** Manages remote kernel connections */ readonly #remoteManager: RemoteManager; @@ -155,6 +162,21 @@ export class Kernel { queueMessage: this.queueMessage.bind(this), }); + this.#systemSubclusterManager = new SystemSubclusterManager({ + kernelStore: this.#kernelStore, + kernelQueue: this.#kernelQueue, + kernelFacetDeps: { + launchSubcluster: this.launchSubcluster.bind(this), + terminateSubcluster: this.terminateSubcluster.bind(this), + reloadSubcluster: this.reloadSubcluster.bind(this), + getSubcluster: this.getSubcluster.bind(this), + getSubclusters: this.getSubclusters.bind(this), + getStatus: this.getStatus.bind(this), + logger: this.#logger.subLogger({ tags: ['KernelFacet'] }), + }, + logger: this.#logger.subLogger({ tags: ['SystemSubclusterManager'] }), + }); + this.#kernelRouter = new KernelRouter( this.#kernelStore, this.#kernelQueue, @@ -305,6 +327,35 @@ export class Kernel { return this.#subclusterManager.launchSubcluster(config); } + /** + * Launches a system subcluster. + * + * System subclusters contain vats that run without compartment isolation + * directly in the host process. The bootstrap vat receives a kernel facet + * as a vatpower, enabling it to launch dynamic subclusters and receive + * E()-callable presences. + * + * @param config - Configuration for the system subcluster. + * @returns A promise for the launch result containing system subcluster ID and vat IDs. + */ + async launchSystemSubcluster( + config: SystemSubclusterConfig, + ): Promise { + return this.#systemSubclusterManager.launchSystemSubcluster(config); + } + + /** + * Terminates a system subcluster. + * + * @param systemSubclusterId - The ID of the system subcluster to terminate. + * @returns A promise that resolves when termination is complete. + */ + async terminateSystemSubcluster(systemSubclusterId: string): Promise { + return this.#systemSubclusterManager.terminateSystemSubcluster( + systemSubclusterId, + ); + } + /** * Terminates a named sub-cluster of vats. * @@ -401,7 +452,7 @@ export class Kernel { * * @param endpointId - The ID of the endpoint to retrieve. * @returns The endpoint handle for the given ID. - * @throws If the endpoint ID is invalid (neither a vat ID nor a remote ID). + * @throws If the endpoint ID is invalid (neither a vat ID, remote ID, nor system vat ID). */ #getEndpoint(endpointId: EndpointId): EndpointHandle { if (isVatId(endpointId)) { @@ -410,6 +461,15 @@ export class Kernel { if (isRemoteId(endpointId)) { return this.#remoteManager.getRemote(endpointId); } + if (isSystemVatId(endpointId)) { + const systemVatId = endpointId as SystemVatId; + const handle = + this.#systemSubclusterManager.getSystemVatHandle(systemVatId); + if (!handle) { + throw Error(`system vat ${systemVatId} not found`); + } + return handle; + } // eslint-disable-next-line @typescript-eslint/restrict-template-expressions throw Error(`invalid endpoint ID ${endpointId}`); } diff --git a/packages/ocap-kernel/src/vats/SystemSubclusterManager.ts b/packages/ocap-kernel/src/vats/SystemSubclusterManager.ts index fb0e7441d..ca135fb19 100644 --- a/packages/ocap-kernel/src/vats/SystemSubclusterManager.ts +++ b/packages/ocap-kernel/src/vats/SystemSubclusterManager.ts @@ -1,10 +1,10 @@ import type { Logger } from '@metamask/logger'; import type { KernelQueue } from '../KernelQueue.ts'; +import { makeKernelFacet } from './kernel-facet.ts'; +import type { KernelFacetDependencies } from './kernel-facet.ts'; import { kslot } from '../liveslots/kernel-marshal.ts'; import type { SlotValue } from '../liveslots/kernel-marshal.ts'; -import { makeKernelFacetService } from '../services/KernelFacetService.ts'; -import type { KernelFacetDependencies } from '../services/KernelFacetService.ts'; import type { KernelStore } from '../store/index.ts'; import type { SystemVatId, @@ -140,7 +140,7 @@ export class SystemSubclusterManager { const rootKrefs: Record = {}; // Create kernel facet for the bootstrap vat - const kernelFacet = makeKernelFacetService(this.#kernelFacetDeps); + const kernelFacet = makeKernelFacet(this.#kernelFacetDeps); // Launch all system vats for (const [vatName, vatConfig] of Object.entries(config.vats)) { diff --git a/packages/ocap-kernel/src/services/KernelFacetService.ts b/packages/ocap-kernel/src/vats/kernel-facet.ts similarity index 92% rename from packages/ocap-kernel/src/services/KernelFacetService.ts rename to packages/ocap-kernel/src/vats/kernel-facet.ts index 88a7ce2da..50fd6c8d1 100644 --- a/packages/ocap-kernel/src/services/KernelFacetService.ts +++ b/packages/ocap-kernel/src/vats/kernel-facet.ts @@ -12,7 +12,7 @@ import type { } from '../types.ts'; /** - * Dependencies required by KernelFacetService. + * Dependencies required to create a kernel facet. */ export type KernelFacetDependencies = { launchSubcluster: (config: ClusterConfig) => Promise<{ @@ -38,8 +38,7 @@ export type KernelFacetLaunchResult = { }; /** - * Creates a kernel facet service object that provides privileged kernel - * operations to system subclusters. + * Creates a kernel facet object that provides privileged kernel operations. * * The kernel facet is provided as a vatpower to the bootstrap vat of a * system subcluster. It enables the bootstrap vat to: @@ -48,10 +47,10 @@ export type KernelFacetLaunchResult = { * - Reload subclusters * - Query kernel status * - * @param deps - Dependencies for the kernel facet service. - * @returns The kernel facet service object. + * @param deps - Dependencies for creating the kernel facet. + * @returns The kernel facet object. */ -export function makeKernelFacetService(deps: KernelFacetDependencies): object { +export function makeKernelFacet(deps: KernelFacetDependencies): object { const { launchSubcluster, terminateSubcluster, From a5dedcc63d3da26d41ad31c8c666288e53354311 Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Tue, 27 Jan 2026 21:10:49 -0800 Subject: [PATCH 04/41] feat(kernel-browser-runtime): Add host subcluster utilities Add utilities for creating host subclusters in browser runtime: - makeHostSubcluster() factory function - HostSubclusterConfig, HostSubclusterVat, HostSubclusterResult types - Export new system vat types from @metamask/ocap-kernel Co-Authored-By: Claude Opus 4.5 --- .../src/host-subcluster/index.ts | 15 ++++++ .../host-subcluster/make-host-subcluster.ts | 49 +++++++++++++++++++ .../src/host-subcluster/types.ts | 44 +++++++++++++++++ packages/kernel-browser-runtime/src/index.ts | 1 + packages/ocap-kernel/src/index.ts | 10 ++++ 5 files changed, 119 insertions(+) create mode 100644 packages/kernel-browser-runtime/src/host-subcluster/index.ts create mode 100644 packages/kernel-browser-runtime/src/host-subcluster/make-host-subcluster.ts create mode 100644 packages/kernel-browser-runtime/src/host-subcluster/types.ts diff --git a/packages/kernel-browser-runtime/src/host-subcluster/index.ts b/packages/kernel-browser-runtime/src/host-subcluster/index.ts new file mode 100644 index 000000000..2d8c14fc2 --- /dev/null +++ b/packages/kernel-browser-runtime/src/host-subcluster/index.ts @@ -0,0 +1,15 @@ +/** + * Host subcluster utilities for browser runtime. + * + * The host subcluster enables the background script to use E() on vat object + * presences directly, replacing CapTP. The background becomes the bootstrap + * vat of a system subcluster and receives a kernel facet as a vatpower. + */ + +export type { + HostSubclusterConfig, + HostSubclusterResult, + HostSubclusterVat, +} from './types.ts'; + +export { makeHostSubcluster } from './make-host-subcluster.ts'; diff --git a/packages/kernel-browser-runtime/src/host-subcluster/make-host-subcluster.ts b/packages/kernel-browser-runtime/src/host-subcluster/make-host-subcluster.ts new file mode 100644 index 000000000..1470f4067 --- /dev/null +++ b/packages/kernel-browser-runtime/src/host-subcluster/make-host-subcluster.ts @@ -0,0 +1,49 @@ +import type { SystemSubclusterConfig } from '@metamask/ocap-kernel'; + +import type { + HostSubclusterResult, + MakeHostSubclusterOptions, +} from './types.ts'; + +/** + * Create and launch the host subcluster. + * + * The host subcluster is a system subcluster that runs in the host process + * (e.g., the browser extension background script). The bootstrap vat receives + * a kernel facet as a vatpower, enabling it to launch dynamic subclusters and + * receive E()-callable presences. + * + * @param options - Configuration options. + * @param options.kernel - The kernel instance. + * @param options.config - Configuration for the host subcluster. + * @returns A promise for the launch result. + */ +export async function makeHostSubcluster( + options: MakeHostSubclusterOptions, +): Promise { + const { kernel, config } = options; + + // Convert HostSubclusterConfig to SystemSubclusterConfig + const systemConfig: SystemSubclusterConfig = { + bootstrap: config.bootstrap, + vats: {}, + ...(config.services !== undefined && { services: config.services }), + }; + + for (const [vatName, vatConfig] of Object.entries(config.vats)) { + systemConfig.vats[vatName] = { + buildRootObject: vatConfig.buildRootObject, + ...(vatConfig.parameters !== undefined && { + parameters: vatConfig.parameters, + }), + }; + } + + // Launch the system subcluster + const result = await kernel.launchSystemSubcluster(systemConfig); + + return { + systemSubclusterId: result.systemSubclusterId, + vatIds: result.vatIds as Record, + }; +} diff --git a/packages/kernel-browser-runtime/src/host-subcluster/types.ts b/packages/kernel-browser-runtime/src/host-subcluster/types.ts new file mode 100644 index 000000000..7386d1c60 --- /dev/null +++ b/packages/kernel-browser-runtime/src/host-subcluster/types.ts @@ -0,0 +1,44 @@ +import type { Kernel, SystemVatBuildRootObject } from '@metamask/ocap-kernel'; +import type { Json } from '@metamask/utils'; + +/** + * Configuration for a single vat within the host subcluster. + */ +export type HostSubclusterVat = { + /** Function to build the vat's root object. */ + buildRootObject: SystemVatBuildRootObject; + /** Optional parameters to pass to buildRootObject. */ + parameters?: Record; +}; + +/** + * Configuration for the host subcluster. + */ +export type HostSubclusterConfig = { + /** The name of the bootstrap vat (must exist in vats). */ + bootstrap: string; + /** Map of vat names to their configurations. */ + vats: Record; + /** Optional list of kernel service names to provide to the bootstrap vat. */ + services?: string[]; +}; + +/** + * Result of launching the host subcluster. + */ +export type HostSubclusterResult = { + /** The system subcluster ID. */ + systemSubclusterId: string; + /** Map of vat names to their system vat IDs. */ + vatIds: Record; +}; + +/** + * Options for creating the host subcluster. + */ +export type MakeHostSubclusterOptions = { + /** The kernel instance. */ + kernel: Kernel; + /** Configuration for the host subcluster. */ + config: HostSubclusterConfig; +}; diff --git a/packages/kernel-browser-runtime/src/index.ts b/packages/kernel-browser-runtime/src/index.ts index 4c10590e3..24f1b0f56 100644 --- a/packages/kernel-browser-runtime/src/index.ts +++ b/packages/kernel-browser-runtime/src/index.ts @@ -1,4 +1,5 @@ export * from './rpc-handlers/index.ts'; +export * from './host-subcluster/index.ts'; export { connectToKernel, receiveInternalConnections, diff --git a/packages/ocap-kernel/src/index.ts b/packages/ocap-kernel/src/index.ts index 2e4aa3532..1cfd41223 100644 --- a/packages/ocap-kernel/src/index.ts +++ b/packages/ocap-kernel/src/index.ts @@ -13,6 +13,13 @@ export type { Subcluster, SubclusterId, SubclusterLaunchResult, + // System vat types + SystemVatId, + SystemSubclusterId, + SystemSubclusterConfig, + SystemVatConfig, + SystemVatBuildRootObject, + SystemSubclusterLaunchResult, } from './types.ts'; export type { RemoteMessageHandler, @@ -30,6 +37,9 @@ export { CapDataStruct, KernelStatusStruct, SubclusterStruct, + // System vat exports + isSystemVatId, + SystemVatIdStruct, } from './types.ts'; export { kunser, kser, kslot, krefOf } from './liveslots/kernel-marshal.ts'; export type { SlotValue } from './liveslots/kernel-marshal.ts'; From 42e0d47dca8eed8fa726a2e36f5aa19a7d5864cb Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Tue, 27 Jan 2026 21:17:52 -0800 Subject: [PATCH 05/41] refactor(ocap-kernel): Move kernel-facet.ts to src root - Move kernel-facet.ts from src/vats/ to src/ - Update import paths in SystemSubclusterManager.ts - Add SystemVatId type assertion for template literal Co-Authored-By: Claude Opus 4.5 --- .../src/{vats => }/kernel-facet.ts | 33 ++++++++----------- .../src/vats/SystemSubclusterManager.ts | 8 ++--- 2 files changed, 17 insertions(+), 24 deletions(-) rename packages/ocap-kernel/src/{vats => }/kernel-facet.ts (83%) diff --git a/packages/ocap-kernel/src/vats/kernel-facet.ts b/packages/ocap-kernel/src/kernel-facet.ts similarity index 83% rename from packages/ocap-kernel/src/vats/kernel-facet.ts rename to packages/ocap-kernel/src/kernel-facet.ts index 50fd6c8d1..6b1e953bc 100644 --- a/packages/ocap-kernel/src/vats/kernel-facet.ts +++ b/packages/ocap-kernel/src/kernel-facet.ts @@ -1,30 +1,23 @@ -import type { CapData } from '@endo/marshal'; import { makeDefaultExo } from '@metamask/kernel-utils'; import type { Logger } from '@metamask/logger'; -import { kslot } from '../liveslots/kernel-marshal.ts'; -import type { SlotValue } from '../liveslots/kernel-marshal.ts'; -import type { - KRef, - ClusterConfig, - Subcluster, - KernelStatus, -} from '../types.ts'; +import type { Kernel } from './Kernel.ts'; +import { kslot } from './liveslots/kernel-marshal.ts'; +import type { SlotValue } from './liveslots/kernel-marshal.ts'; +import type { ClusterConfig, Subcluster, KernelStatus } from './types.ts'; /** * Dependencies required to create a kernel facet. */ -export type KernelFacetDependencies = { - launchSubcluster: (config: ClusterConfig) => Promise<{ - subclusterId: string; - bootstrapRootKref: KRef; - bootstrapResult: CapData | undefined; - }>; - terminateSubcluster: (subclusterId: string) => Promise; - reloadSubcluster: (subclusterId: string) => Promise; - getSubcluster: (subclusterId: string) => Subcluster | undefined; - getSubclusters: () => Subcluster[]; - getStatus: () => Promise; +export type KernelFacetDependencies = Pick< + Kernel, + | 'launchSubcluster' + | 'terminateSubcluster' + | 'reloadSubcluster' + | 'getSubcluster' + | 'getSubclusters' + | 'getStatus' +> & { logger?: Logger; }; diff --git a/packages/ocap-kernel/src/vats/SystemSubclusterManager.ts b/packages/ocap-kernel/src/vats/SystemSubclusterManager.ts index ca135fb19..ddef5acb7 100644 --- a/packages/ocap-kernel/src/vats/SystemSubclusterManager.ts +++ b/packages/ocap-kernel/src/vats/SystemSubclusterManager.ts @@ -1,8 +1,10 @@ import type { Logger } from '@metamask/logger'; +import { makeKernelFacet } from '../kernel-facet.ts'; +import type { KernelFacetDependencies } from '../kernel-facet.ts'; import type { KernelQueue } from '../KernelQueue.ts'; -import { makeKernelFacet } from './kernel-facet.ts'; -import type { KernelFacetDependencies } from './kernel-facet.ts'; +import type { SystemVatDeliverFn } from './SystemVatHandle.ts'; +import { SystemVatHandle } from './SystemVatHandle.ts'; import { kslot } from '../liveslots/kernel-marshal.ts'; import type { SlotValue } from '../liveslots/kernel-marshal.ts'; import type { KernelStore } from '../store/index.ts'; @@ -14,8 +16,6 @@ import type { KRef, } from '../types.ts'; import { ROOT_OBJECT_VREF } from '../types.ts'; -import type { SystemVatDeliverFn } from './SystemVatHandle.ts'; -import { SystemVatHandle } from './SystemVatHandle.ts'; import { SystemVatSupervisor } from './SystemVatSupervisor.ts'; /** From f5f4cc39868d94eff8809f4a6cdbbcdaf3307e35 Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Tue, 27 Jan 2026 21:34:14 -0800 Subject: [PATCH 06/41] test: Add unit tests for system vat infrastructure Add comprehensive unit tests for: - SystemVatSyscall: syscall handling, error handling, termination - SystemVatHandle: message/notify delivery, GC, crank results - SystemVatSupervisor: liveslots integration, ephemeral vatstore - kernel-facet: makeKernelFacet utility function - SystemSubclusterManager: subcluster lifecycle, vat ID allocation - makeHostSubcluster: config conversion, kernel integration Also update index.test.ts files to include new exports. Co-Authored-By: Claude Opus 4.5 --- .../make-host-subcluster.test.ts | 166 ++++++++ .../kernel-browser-runtime/src/index.test.ts | 1 + packages/ocap-kernel/src/index.test.ts | 2 + packages/ocap-kernel/src/kernel-facet.test.ts | 298 +++++++++++++ .../src/vats/SystemSubclusterManager.test.ts | 359 ++++++++++++++++ .../src/vats/SystemSubclusterManager.ts | 1 + .../src/vats/SystemVatHandle.test.ts | 323 ++++++++++++++ .../src/vats/SystemVatSupervisor.test.ts | 402 ++++++++++++++++++ .../src/vats/SystemVatSyscall.test.ts | 323 ++++++++++++++ userspace-as-vat.md | 336 +++++++++++++++ 10 files changed, 2211 insertions(+) create mode 100644 packages/kernel-browser-runtime/src/host-subcluster/make-host-subcluster.test.ts create mode 100644 packages/ocap-kernel/src/kernel-facet.test.ts create mode 100644 packages/ocap-kernel/src/vats/SystemSubclusterManager.test.ts create mode 100644 packages/ocap-kernel/src/vats/SystemVatHandle.test.ts create mode 100644 packages/ocap-kernel/src/vats/SystemVatSupervisor.test.ts create mode 100644 packages/ocap-kernel/src/vats/SystemVatSyscall.test.ts create mode 100644 userspace-as-vat.md diff --git a/packages/kernel-browser-runtime/src/host-subcluster/make-host-subcluster.test.ts b/packages/kernel-browser-runtime/src/host-subcluster/make-host-subcluster.test.ts new file mode 100644 index 000000000..3aa07da56 --- /dev/null +++ b/packages/kernel-browser-runtime/src/host-subcluster/make-host-subcluster.test.ts @@ -0,0 +1,166 @@ +import type { Kernel, SystemSubclusterConfig } from '@metamask/ocap-kernel'; +import { describe, it, expect, vi, beforeEach } from 'vitest'; + +import { makeHostSubcluster } from './make-host-subcluster.ts'; +import type { HostSubclusterConfig } from './types.ts'; + +describe('makeHostSubcluster', () => { + let kernel: Kernel; + const buildRootObject = vi.fn(() => ({ test: () => 'test' })); + + beforeEach(() => { + vi.clearAllMocks(); + + kernel = { + launchSystemSubcluster: vi.fn().mockResolvedValue({ + systemSubclusterId: 'ss0', + vatIds: { testVat: 'sv0' }, + }), + } as unknown as Kernel; + }); + + it('calls kernel.launchSystemSubcluster with converted config', async () => { + const config: HostSubclusterConfig = { + bootstrap: 'testVat', + vats: { + testVat: { buildRootObject }, + }, + }; + + await makeHostSubcluster({ kernel, config }); + + expect(kernel.launchSystemSubcluster).toHaveBeenCalledWith({ + bootstrap: 'testVat', + vats: { + testVat: { buildRootObject }, + }, + }); + }); + + it('returns systemSubclusterId and vatIds', async () => { + const config: HostSubclusterConfig = { + bootstrap: 'testVat', + vats: { + testVat: { buildRootObject }, + }, + }; + + const result = await makeHostSubcluster({ kernel, config }); + + expect(result.systemSubclusterId).toBe('ss0'); + expect(result.vatIds).toStrictEqual({ testVat: 'sv0' }); + }); + + it('converts multiple vats', async () => { + const config: HostSubclusterConfig = { + bootstrap: 'bootstrap', + vats: { + bootstrap: { buildRootObject }, + worker: { buildRootObject }, + }, + }; + + await makeHostSubcluster({ kernel, config }); + + const calledConfig = ( + kernel.launchSystemSubcluster as ReturnType + ).mock.calls[0][0] as SystemSubclusterConfig; + expect(calledConfig.vats.bootstrap).toBeDefined(); + expect(calledConfig.vats.worker).toBeDefined(); + }); + + it('includes parameters when provided', async () => { + const config: HostSubclusterConfig = { + bootstrap: 'testVat', + vats: { + testVat: { + buildRootObject, + parameters: { key: 'value' }, + }, + }, + }; + + await makeHostSubcluster({ kernel, config }); + + const calledConfig = ( + kernel.launchSystemSubcluster as ReturnType + ).mock.calls[0][0] as SystemSubclusterConfig; + expect(calledConfig.vats.testVat?.parameters).toStrictEqual({ + key: 'value', + }); + }); + + it('omits parameters when undefined', async () => { + const config: HostSubclusterConfig = { + bootstrap: 'testVat', + vats: { + testVat: { buildRootObject }, + }, + }; + + await makeHostSubcluster({ kernel, config }); + + const calledConfig = ( + kernel.launchSystemSubcluster as ReturnType + ).mock.calls[0][0] as SystemSubclusterConfig; + expect( + Object.prototype.hasOwnProperty.call( + calledConfig.vats.testVat, + 'parameters', + ), + ).toBe(false); + }); + + it('includes services when provided', async () => { + const config: HostSubclusterConfig = { + bootstrap: 'testVat', + vats: { + testVat: { buildRootObject }, + }, + services: ['platformService'], + }; + + await makeHostSubcluster({ kernel, config }); + + const calledConfig = ( + kernel.launchSystemSubcluster as ReturnType + ).mock.calls[0][0] as SystemSubclusterConfig; + expect(calledConfig.services).toStrictEqual(['platformService']); + }); + + it('omits services when undefined', async () => { + const config: HostSubclusterConfig = { + bootstrap: 'testVat', + vats: { + testVat: { buildRootObject }, + }, + }; + + await makeHostSubcluster({ kernel, config }); + + const calledConfig = ( + kernel.launchSystemSubcluster as ReturnType + ).mock.calls[0][0] as SystemSubclusterConfig; + expect(Object.prototype.hasOwnProperty.call(calledConfig, 'services')).toBe( + false, + ); + }); + + it('propagates errors from kernel.launchSystemSubcluster', async () => { + const error = new Error('Launch failed'); + ( + kernel.launchSystemSubcluster as ReturnType + ).mockRejectedValueOnce(error); + + const config: HostSubclusterConfig = { + bootstrap: 'testVat', + vats: { + testVat: { buildRootObject }, + }, + }; + + await expect(makeHostSubcluster({ kernel, config })).rejects.toThrow( + 'Launch failed', + ); + }); +}); diff --git a/packages/kernel-browser-runtime/src/index.test.ts b/packages/kernel-browser-runtime/src/index.test.ts index f52b98667..e8496b2ce 100644 --- a/packages/kernel-browser-runtime/src/index.test.ts +++ b/packages/kernel-browser-runtime/src/index.test.ts @@ -14,6 +14,7 @@ describe('index', () => { 'isCapTPNotification', 'makeBackgroundCapTP', 'makeCapTPNotification', + 'makeHostSubcluster', 'makeIframeVatWorker', 'parseRelayQueryString', 'receiveInternalConnections', diff --git a/packages/ocap-kernel/src/index.test.ts b/packages/ocap-kernel/src/index.test.ts index 6f862268b..62d8d9d0d 100644 --- a/packages/ocap-kernel/src/index.test.ts +++ b/packages/ocap-kernel/src/index.test.ts @@ -10,12 +10,14 @@ describe('index', () => { 'Kernel', 'KernelStatusStruct', 'SubclusterStruct', + 'SystemVatIdStruct', 'VatConfigStruct', 'VatHandle', 'VatIdStruct', 'VatSupervisor', 'generateMnemonic', 'initTransport', + 'isSystemVatId', 'isValidMnemonic', 'isVatConfig', 'isVatId', diff --git a/packages/ocap-kernel/src/kernel-facet.test.ts b/packages/ocap-kernel/src/kernel-facet.test.ts new file mode 100644 index 000000000..a2ac6b79c --- /dev/null +++ b/packages/ocap-kernel/src/kernel-facet.test.ts @@ -0,0 +1,298 @@ +import type { Logger } from '@metamask/logger'; +import { describe, it, expect, vi, beforeEach } from 'vitest'; + +import type { KernelFacetDependencies } from './kernel-facet.ts'; +import { makeKernelFacet } from './kernel-facet.ts'; +import type { SlotValue } from './liveslots/kernel-marshal.ts'; +import { krefOf } from './liveslots/kernel-marshal.ts'; +import type { ClusterConfig, KernelStatus, Subcluster } from './types.ts'; + +describe('makeKernelFacet', () => { + let deps: KernelFacetDependencies; + let logger: Logger; + + beforeEach(() => { + logger = { + debug: vi.fn(), + error: vi.fn(), + log: vi.fn(), + warn: vi.fn(), + subLogger: vi.fn(() => logger), + } as unknown as Logger; + + deps = { + launchSubcluster: vi.fn().mockResolvedValue({ + subclusterId: 's1', + bootstrapRootKref: 'ko1', + }), + terminateSubcluster: vi.fn().mockResolvedValue(undefined), + reloadSubcluster: vi.fn().mockResolvedValue({ + id: 's2', + config: { bootstrap: 'test', vats: {} }, + vats: {}, + }), + getSubcluster: vi.fn().mockReturnValue({ + id: 's1', + config: { bootstrap: 'test', vats: {} }, + vats: {}, + }), + getSubclusters: vi + .fn() + .mockReturnValue([ + { id: 's1', config: { bootstrap: 'test', vats: {} }, vats: {} }, + ]), + getStatus: vi.fn().mockResolvedValue({ + initialized: true, + cranksExecuted: 10, + cranksPending: 0, + vatCount: 2, + endpointCount: 3, + }), + logger, + }; + }); + + it('creates a kernel facet object', () => { + const facet = makeKernelFacet(deps); + expect(facet).toBeDefined(); + expect(typeof facet).toBe('object'); + }); + + describe('launchSubcluster', () => { + it('calls the launchSubcluster dependency', async () => { + const facet = makeKernelFacet(deps) as { + launchSubcluster: (config: ClusterConfig) => Promise; + }; + const config: ClusterConfig = { + bootstrap: 'myVat', + vats: { myVat: { sourceSpec: 'test.js' } }, + }; + + await facet.launchSubcluster(config); + + expect(deps.launchSubcluster).toHaveBeenCalledWith(config); + }); + + it('returns subclusterId and root as slot value', async () => { + const facet = makeKernelFacet(deps) as { + launchSubcluster: ( + config: ClusterConfig, + ) => Promise<{ subclusterId: string; root: SlotValue }>; + }; + const config: ClusterConfig = { + bootstrap: 'myVat', + vats: { myVat: { sourceSpec: 'test.js' } }, + }; + + const result = await facet.launchSubcluster(config); + + expect(result.subclusterId).toBe('s1'); + // The root is a slot value (remotable) that carries the kref + expect(krefOf(result.root)).toBe('ko1'); + }); + + it('logs launch events', async () => { + const facet = makeKernelFacet(deps) as { + launchSubcluster: (config: ClusterConfig) => Promise; + }; + const config: ClusterConfig = { + bootstrap: 'myVat', + vats: { myVat: { sourceSpec: 'test.js' } }, + }; + + await facet.launchSubcluster(config); + + expect(logger.log).toHaveBeenCalledWith( + expect.stringContaining('launching subcluster'), + 'myVat', + ); + expect(logger.log).toHaveBeenCalledWith( + expect.stringContaining('launched subcluster s1'), + ); + }); + }); + + describe('terminateSubcluster', () => { + it('calls the terminateSubcluster dependency', async () => { + const facet = makeKernelFacet(deps) as { + terminateSubcluster: (id: string) => Promise; + }; + + await facet.terminateSubcluster('s1'); + + expect(deps.terminateSubcluster).toHaveBeenCalledWith('s1'); + }); + + it('logs termination events', async () => { + const facet = makeKernelFacet(deps) as { + terminateSubcluster: (id: string) => Promise; + }; + + await facet.terminateSubcluster('s1'); + + expect(logger.log).toHaveBeenCalledWith( + expect.stringContaining('terminating subcluster s1'), + ); + expect(logger.log).toHaveBeenCalledWith( + expect.stringContaining('terminated subcluster s1'), + ); + }); + }); + + describe('reloadSubcluster', () => { + it('calls the reloadSubcluster dependency', async () => { + const facet = makeKernelFacet(deps) as { + reloadSubcluster: (id: string) => Promise; + }; + + await facet.reloadSubcluster('s1'); + + expect(deps.reloadSubcluster).toHaveBeenCalledWith('s1'); + }); + + it('returns the reloaded subcluster', async () => { + const facet = makeKernelFacet(deps) as { + reloadSubcluster: (id: string) => Promise; + }; + + const result = await facet.reloadSubcluster('s1'); + + expect(result.id).toBe('s2'); + }); + + it('logs reload events', async () => { + const facet = makeKernelFacet(deps) as { + reloadSubcluster: (id: string) => Promise; + }; + + await facet.reloadSubcluster('s1'); + + expect(logger.log).toHaveBeenCalledWith( + expect.stringContaining('reloading subcluster s1'), + ); + expect(logger.log).toHaveBeenCalledWith( + expect.stringContaining('reloaded subcluster'), + ); + }); + }); + + describe('getSubcluster', () => { + it('calls the getSubcluster dependency', () => { + const facet = makeKernelFacet(deps) as { + getSubcluster: (id: string) => Subcluster | undefined; + }; + + facet.getSubcluster('s1'); + + expect(deps.getSubcluster).toHaveBeenCalledWith('s1'); + }); + + it('returns the subcluster', () => { + const facet = makeKernelFacet(deps) as { + getSubcluster: (id: string) => Subcluster | undefined; + }; + + const result = facet.getSubcluster('s1'); + + expect(result?.id).toBe('s1'); + }); + + it('returns undefined for unknown subcluster', () => { + vi.spyOn(deps, 'getSubcluster') + .mockImplementation() + .mockReturnValue(undefined); + const facet = makeKernelFacet(deps) as { + getSubcluster: (id: string) => Subcluster | undefined; + }; + + const result = facet.getSubcluster('unknown'); + + expect(result).toBeUndefined(); + }); + }); + + describe('getSubclusters', () => { + it('calls the getSubclusters dependency', () => { + const facet = makeKernelFacet(deps) as { + getSubclusters: () => Subcluster[]; + }; + + facet.getSubclusters(); + + expect(deps.getSubclusters).toHaveBeenCalled(); + }); + + it('returns all subclusters', () => { + const facet = makeKernelFacet(deps) as { + getSubclusters: () => Subcluster[]; + }; + + const result = facet.getSubclusters(); + + expect(result).toHaveLength(1); + expect(result[0]?.id).toBe('s1'); + }); + }); + + describe('getStatus', () => { + it('calls the getStatus dependency', async () => { + const facet = makeKernelFacet(deps) as { + getStatus: () => Promise; + }; + + await facet.getStatus(); + + expect(deps.getStatus).toHaveBeenCalled(); + }); + + it('returns kernel status', async () => { + const facet = makeKernelFacet(deps) as { + getStatus: () => Promise; + }; + + const result = await facet.getStatus(); + + expect(result.initialized).toBe(true); + expect(result.cranksExecuted).toBe(10); + expect(result.cranksPending).toBe(0); + expect(result.vatCount).toBe(2); + expect(result.endpointCount).toBe(3); + }); + }); + + describe('without logger', () => { + it('does not throw when logger is undefined', async () => { + const depsWithoutLogger: KernelFacetDependencies = { + launchSubcluster: vi.fn().mockResolvedValue({ + subclusterId: 's1', + bootstrapRootKref: 'ko1', + }), + terminateSubcluster: vi.fn().mockResolvedValue(undefined), + reloadSubcluster: vi.fn().mockResolvedValue({ + id: 's2', + config: { bootstrap: 'test', vats: {} }, + vats: {}, + }), + getSubcluster: vi.fn().mockReturnValue(undefined), + getSubclusters: vi.fn().mockReturnValue([]), + getStatus: vi.fn().mockResolvedValue({ + initialized: true, + cranksExecuted: 0, + cranksPending: 0, + vatCount: 0, + endpointCount: 0, + }), + }; + + const facet = makeKernelFacet(depsWithoutLogger) as { + launchSubcluster: (config: ClusterConfig) => Promise; + }; + + const result = await facet.launchSubcluster({ + bootstrap: 'test', + vats: { test: { sourceSpec: 'test.js' } }, + }); + expect(result).toBeDefined(); + }); + }); +}); diff --git a/packages/ocap-kernel/src/vats/SystemSubclusterManager.test.ts b/packages/ocap-kernel/src/vats/SystemSubclusterManager.test.ts new file mode 100644 index 000000000..39f6f6bfd --- /dev/null +++ b/packages/ocap-kernel/src/vats/SystemSubclusterManager.test.ts @@ -0,0 +1,359 @@ +import type { Logger } from '@metamask/logger'; +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import type { MockInstance } from 'vitest'; + +import type { KernelFacetDependencies } from '../kernel-facet.ts'; +import type { KernelQueue } from '../KernelQueue.ts'; +import type { KernelStore } from '../store/index.ts'; +import type { + SystemSubclusterConfig, + SystemVatBuildRootObject, + SystemVatId, +} from '../types.ts'; +import { SystemSubclusterManager } from './SystemSubclusterManager.ts'; + +// Mock liveslots +const mockDispatch = vi.fn(); +vi.mock('@agoric/swingset-liveslots', () => ({ + makeLiveSlots: vi.fn(() => ({ + dispatch: mockDispatch, + })), +})); + +describe('SystemSubclusterManager', () => { + let kernelStore: KernelStore; + let kernelQueue: KernelQueue; + let kernelFacetDeps: KernelFacetDependencies; + let logger: Logger; + let manager: SystemSubclusterManager; + + const buildRootObject: SystemVatBuildRootObject = vi.fn(() => ({ + bootstrap: vi.fn(), + test: () => 'test', + })); + + beforeEach(() => { + vi.clearAllMocks(); + mockDispatch.mockResolvedValue(undefined); + + kernelStore = { + initEndpoint: vi.fn(), + erefToKref: vi.fn().mockReturnValue(null), + initKernelObject: vi.fn().mockReturnValue('ko1'), + addCListEntry: vi.fn(), + translateSyscallVtoK: vi.fn((_, vso) => vso), + getKernelPromise: vi.fn(() => ({ state: 'unresolved' })), + addPromiseSubscriber: vi.fn(), + clearReachableFlag: vi.fn(), + getReachableFlag: vi.fn(), + forgetKref: vi.fn(), + getPromisesByDecider: vi.fn(() => []), + deleteEndpoint: vi.fn(), + kv: { + get: vi.fn().mockReturnValue(undefined), + }, + } as unknown as KernelStore; + + kernelQueue = { + waitForCrank: vi.fn().mockResolvedValue(undefined), + enqueueSend: vi.fn(), + resolvePromises: vi.fn(), + enqueueNotify: vi.fn(), + enqueueMessage: vi.fn(), + } as unknown as KernelQueue; + + kernelFacetDeps = { + launchSubcluster: vi.fn().mockResolvedValue({ + subclusterId: 's1', + bootstrapRootKref: 'ko2', + }), + terminateSubcluster: vi.fn().mockResolvedValue(undefined), + reloadSubcluster: vi.fn().mockResolvedValue({ id: 's2' }), + getSubcluster: vi.fn().mockReturnValue(undefined), + getSubclusters: vi.fn().mockReturnValue([]), + getStatus: vi.fn().mockResolvedValue({ initialized: true }), + }; + + logger = { + debug: vi.fn(), + error: vi.fn(), + log: vi.fn(), + warn: vi.fn(), + subLogger: vi.fn(() => logger), + } as unknown as Logger; + + manager = new SystemSubclusterManager({ + kernelStore, + kernelQueue, + kernelFacetDeps, + logger, + }); + }); + + describe('launchSystemSubcluster', () => { + const config: SystemSubclusterConfig = { + bootstrap: 'testVat', + vats: { + testVat: { buildRootObject }, + }, + }; + + it('waits for crank before launching', async () => { + await manager.launchSystemSubcluster(config); + + expect(kernelQueue.waitForCrank).toHaveBeenCalled(); + }); + + it('throws if bootstrap vat is not in vats config', async () => { + const badConfig: SystemSubclusterConfig = { + bootstrap: 'missing', + vats: { + testVat: { buildRootObject }, + }, + }; + + await expect(manager.launchSystemSubcluster(badConfig)).rejects.toThrow( + 'invalid bootstrap vat name missing', + ); + }); + + it('allocates system vat IDs starting from sv0', async () => { + const result = await manager.launchSystemSubcluster(config); + + expect(result.vatIds.testVat).toBe('sv0'); + }); + + it('allocates incrementing system vat IDs', async () => { + const result1 = await manager.launchSystemSubcluster(config); + const result2 = await manager.launchSystemSubcluster(config); + + expect(result1.vatIds.testVat).toBe('sv0'); + expect(result2.vatIds.testVat).toBe('sv1'); + }); + + it('allocates system subcluster IDs starting from ss0', async () => { + const result = await manager.launchSystemSubcluster(config); + + expect(result.systemSubclusterId).toBe('ss0'); + }); + + it('initializes endpoints for each vat', async () => { + await manager.launchSystemSubcluster(config); + + expect(kernelStore.initEndpoint).toHaveBeenCalledWith('sv0'); + }); + + it('initializes kernel objects for vat roots', async () => { + await manager.launchSystemSubcluster(config); + + expect(kernelStore.initKernelObject).toHaveBeenCalledWith('sv0'); + }); + + it('adds clist entries for root objects', async () => { + await manager.launchSystemSubcluster(config); + + expect(kernelStore.addCListEntry).toHaveBeenCalledWith( + 'sv0', + 'ko1', + 'o+0', + ); + }); + + it('enqueues bootstrap message to root object', async () => { + await manager.launchSystemSubcluster(config); + + expect(kernelQueue.enqueueMessage).toHaveBeenCalledWith( + 'ko1', + 'bootstrap', + expect.any(Array), + ); + }); + + it('launches multiple vats in a subcluster', async () => { + const multiVatConfig: SystemSubclusterConfig = { + bootstrap: 'bootstrap', + vats: { + bootstrap: { buildRootObject }, + worker: { buildRootObject }, + }, + }; + + const result = await manager.launchSystemSubcluster(multiVatConfig); + + expect(result.vatIds.bootstrap).toBe('sv0'); + expect(result.vatIds.worker).toBe('sv1'); + expect(kernelStore.initEndpoint).toHaveBeenCalledTimes(2); + }); + + it('uses existing root kref if available', async () => { + (kernelStore.erefToKref as unknown as MockInstance).mockReturnValueOnce( + 'ko-existing', + ); + + await manager.launchSystemSubcluster(config); + + expect(kernelStore.initKernelObject).not.toHaveBeenCalled(); + expect(kernelQueue.enqueueMessage).toHaveBeenCalledWith( + 'ko-existing', + 'bootstrap', + expect.any(Array), + ); + }); + + it('warns if requested service is not found', async () => { + const configWithServices: SystemSubclusterConfig = { + bootstrap: 'testVat', + vats: { testVat: { buildRootObject } }, + services: ['unknownService'], + }; + + await manager.launchSystemSubcluster(configWithServices); + + expect(logger.warn).toHaveBeenCalledWith( + "Kernel service 'unknownService' not found", + ); + }); + + it('includes services in bootstrap message when available', async () => { + (kernelStore.kv.get as unknown as MockInstance).mockReturnValueOnce( + 'ko-service', + ); + + const configWithServices: SystemSubclusterConfig = { + bootstrap: 'testVat', + vats: { testVat: { buildRootObject } }, + services: ['myService'], + }; + + await manager.launchSystemSubcluster(configWithServices); + + expect(kernelQueue.enqueueMessage).toHaveBeenCalledWith( + 'ko1', + 'bootstrap', + [ + expect.anything(), + expect.objectContaining({ myService: expect.anything() }), + ], + ); + }); + }); + + describe('terminateSystemSubcluster', () => { + it('waits for crank before terminating', async () => { + const config: SystemSubclusterConfig = { + bootstrap: 'testVat', + vats: { testVat: { buildRootObject } }, + }; + const result = await manager.launchSystemSubcluster(config); + + await manager.terminateSystemSubcluster(result.systemSubclusterId); + + expect(kernelQueue.waitForCrank).toHaveBeenCalled(); + }); + + it('throws if subcluster is not found', async () => { + await expect( + manager.terminateSystemSubcluster('ss-nonexistent'), + ).rejects.toThrow('System subcluster ss-nonexistent not found'); + }); + + it('removes subcluster from active subclusters', async () => { + const config: SystemSubclusterConfig = { + bootstrap: 'testVat', + vats: { testVat: { buildRootObject } }, + }; + const result = await manager.launchSystemSubcluster(config); + + expect(manager.isSystemVatActive('sv0' as SystemVatId)).toBe(true); + + await manager.terminateSystemSubcluster(result.systemSubclusterId); + + expect(manager.isSystemVatActive('sv0' as SystemVatId)).toBe(false); + }); + }); + + describe('getSystemVatHandle', () => { + it('returns handle for active system vat', async () => { + const config: SystemSubclusterConfig = { + bootstrap: 'testVat', + vats: { testVat: { buildRootObject } }, + }; + await manager.launchSystemSubcluster(config); + + const handle = manager.getSystemVatHandle('sv0' as SystemVatId); + + expect(handle).toBeDefined(); + expect(handle?.systemVatId).toBe('sv0'); + }); + + it('returns undefined for unknown system vat', () => { + const handle = manager.getSystemVatHandle('sv-unknown' as SystemVatId); + + expect(handle).toBeUndefined(); + }); + }); + + describe('getSystemVatIds', () => { + it('returns empty array when no subclusters exist', () => { + const ids = manager.getSystemVatIds(); + + expect(ids).toStrictEqual([]); + }); + + it('returns all system vat IDs', async () => { + const config: SystemSubclusterConfig = { + bootstrap: 'v1', + vats: { + v1: { buildRootObject }, + v2: { buildRootObject }, + }, + }; + await manager.launchSystemSubcluster(config); + + const ids = manager.getSystemVatIds(); + + expect(ids).toContain('sv0'); + expect(ids).toContain('sv1'); + expect(ids).toHaveLength(2); + }); + }); + + describe('isSystemVatActive', () => { + it('returns false for unknown system vat', () => { + const isActive = manager.isSystemVatActive('sv-unknown' as SystemVatId); + + expect(isActive).toBe(false); + }); + + it('returns true for active system vat', async () => { + const config: SystemSubclusterConfig = { + bootstrap: 'testVat', + vats: { testVat: { buildRootObject } }, + }; + await manager.launchSystemSubcluster(config); + + const isActive = manager.isSystemVatActive('sv0' as SystemVatId); + + expect(isActive).toBe(true); + }); + }); + + describe('without logger', () => { + it('throws when trying to launch without logger', async () => { + const managerWithoutLogger = new SystemSubclusterManager({ + kernelStore, + kernelQueue, + kernelFacetDeps, + }); + + const config: SystemSubclusterConfig = { + bootstrap: 'testVat', + vats: { testVat: { buildRootObject } }, + }; + + await expect( + managerWithoutLogger.launchSystemSubcluster(config), + ).rejects.toThrow('Logger required for system vat supervisor'); + }); + }); +}); diff --git a/packages/ocap-kernel/src/vats/SystemSubclusterManager.ts b/packages/ocap-kernel/src/vats/SystemSubclusterManager.ts index ddef5acb7..07e5f1d48 100644 --- a/packages/ocap-kernel/src/vats/SystemSubclusterManager.ts +++ b/packages/ocap-kernel/src/vats/SystemSubclusterManager.ts @@ -102,6 +102,7 @@ export class SystemSubclusterManager { * @returns A new system vat ID. */ #allocateSystemVatId(): SystemVatId { + // TypeScript cannot narrow template literal types from string interpolation const id = `sv${this.#nextSystemVatId}`; this.#nextSystemVatId += 1; return id; diff --git a/packages/ocap-kernel/src/vats/SystemVatHandle.test.ts b/packages/ocap-kernel/src/vats/SystemVatHandle.test.ts new file mode 100644 index 000000000..d13e911d2 --- /dev/null +++ b/packages/ocap-kernel/src/vats/SystemVatHandle.test.ts @@ -0,0 +1,323 @@ +import type { + VatDeliveryObject, + VatOneResolution, +} from '@agoric/swingset-liveslots'; +import type { Logger } from '@metamask/logger'; +import type { MockInstance } from 'vitest'; +import { describe, it, expect, vi, beforeEach } from 'vitest'; + +import type { KernelQueue } from '../KernelQueue.ts'; +import type { KernelStore } from '../store/index.ts'; +import type { Message, SystemVatId, VRef } from '../types.ts'; +import type { SystemVatDeliverFn } from './SystemVatHandle.ts'; +import { SystemVatHandle } from './SystemVatHandle.ts'; + +describe('SystemVatHandle', () => { + let kernelStore: KernelStore; + let kernelQueue: KernelQueue; + let logger: Logger; + let deliver: SystemVatDeliverFn; + let systemVatHandle: SystemVatHandle; + const systemVatId: SystemVatId = 'sv0'; + + beforeEach(() => { + kernelStore = { + translateSyscallVtoK: vi.fn((_, vso) => vso), + getKernelPromise: vi.fn(() => ({ state: 'unresolved' })), + addPromiseSubscriber: vi.fn(), + clearReachableFlag: vi.fn(), + getReachableFlag: vi.fn(), + forgetKref: vi.fn(), + getPromisesByDecider: vi.fn(() => []), + deleteEndpoint: vi.fn(), + } as unknown as KernelStore; + kernelQueue = { + enqueueSend: vi.fn(), + resolvePromises: vi.fn(), + enqueueNotify: vi.fn(), + } as unknown as KernelQueue; + logger = { + debug: vi.fn(), + error: vi.fn(), + log: vi.fn(), + warn: vi.fn(), + subLogger: vi.fn(() => logger), + } as unknown as Logger; + deliver = vi.fn().mockResolvedValue(null); + systemVatHandle = new SystemVatHandle({ + systemVatId, + kernelStore, + kernelQueue, + deliver, + logger, + }); + }); + + describe('constructor', () => { + it('exposes the system vat ID', () => { + expect(systemVatHandle.systemVatId).toBe(systemVatId); + }); + }); + + describe('getSyscallHandler', () => { + it('returns a function that handles syscalls', () => { + const handler = systemVatHandle.getSyscallHandler(); + expect(typeof handler).toBe('function'); + + // Test that it can handle a syscall + handler(['send', 'o+1', { methargs: { body: '[]', slots: [] } }]); + expect(kernelQueue.enqueueSend).toHaveBeenCalled(); + }); + }); + + describe('deliverMessage', () => { + it('calls deliver with message delivery', async () => { + const target: VRef = 'o+0'; + const message: Message = { + methargs: { body: '["test"]', slots: [] }, + result: 'p-1', + }; + + await systemVatHandle.deliverMessage(target, message); + + expect(deliver).toHaveBeenCalledWith([ + 'message', + target, + { methargs: message.methargs, result: message.result }, + ]); + }); + + it('converts undefined result to null', async () => { + const target: VRef = 'o+0'; + const message: Message = { + methargs: { body: '["test"]', slots: [] }, + }; + + await systemVatHandle.deliverMessage(target, message); + + expect(deliver).toHaveBeenCalledWith([ + 'message', + target, + { methargs: message.methargs, result: null }, + ]); + }); + + it('returns crank results with didDelivery', async () => { + const target: VRef = 'o+0'; + const message: Message = { + methargs: { body: '[]', slots: [] }, + }; + + const result = await systemVatHandle.deliverMessage(target, message); + + expect(result.didDelivery).toBe(systemVatId); + }); + }); + + describe('deliverNotify', () => { + it('calls deliver with notify delivery', async () => { + const resolutions: VatOneResolution[] = [ + ['p-1', false, { body: '"resolved"', slots: [] }], + ]; + + await systemVatHandle.deliverNotify(resolutions); + + expect(deliver).toHaveBeenCalledWith(['notify', resolutions]); + }); + + it('returns crank results with didDelivery', async () => { + const resolutions: VatOneResolution[] = [ + ['p-1', false, { body: '"resolved"', slots: [] }], + ]; + + const result = await systemVatHandle.deliverNotify(resolutions); + + expect(result.didDelivery).toBe(systemVatId); + }); + }); + + describe('deliverDropExports', () => { + it('calls deliver with dropExports delivery', async () => { + const vrefs: VRef[] = ['o+1', 'o+2']; + + await systemVatHandle.deliverDropExports(vrefs); + + expect(deliver).toHaveBeenCalledWith(['dropExports', vrefs]); + }); + }); + + describe('deliverRetireExports', () => { + it('calls deliver with retireExports delivery', async () => { + const vrefs: VRef[] = ['o+1', 'o+2']; + + await systemVatHandle.deliverRetireExports(vrefs); + + expect(deliver).toHaveBeenCalledWith(['retireExports', vrefs]); + }); + }); + + describe('deliverRetireImports', () => { + it('calls deliver with retireImports delivery', async () => { + const vrefs: VRef[] = ['o-1', 'o-2']; + + await systemVatHandle.deliverRetireImports(vrefs); + + expect(deliver).toHaveBeenCalledWith(['retireImports', vrefs]); + }); + }); + + describe('deliverBringOutYourDead', () => { + it('calls deliver with bringOutYourDead delivery', async () => { + await systemVatHandle.deliverBringOutYourDead(); + + expect(deliver).toHaveBeenCalledWith(['bringOutYourDead']); + }); + }); + + describe('terminate', () => { + it('marks handle as inactive when terminating=true', async () => { + await systemVatHandle.terminate(true); + + // Subsequent syscalls should fail because handle is inactive + const handler = systemVatHandle.getSyscallHandler(); + handler(['send', 'o+1', { methargs: { body: '[]', slots: [] } }]); + + // The syscall should have been rejected due to inactive status + expect(kernelQueue.enqueueSend).not.toHaveBeenCalled(); + }); + + it('rejects promises for which this vat is decider when terminating', async () => { + ( + kernelStore.getPromisesByDecider as unknown as MockInstance + ).mockReturnValueOnce(['kp1', 'kp2']); + + await systemVatHandle.terminate(true); + + expect(kernelQueue.resolvePromises).toHaveBeenCalledWith( + systemVatId, + expect.arrayContaining([ + expect.arrayContaining(['kp1', true, expect.anything()]), + ]), + ); + expect(kernelQueue.resolvePromises).toHaveBeenCalledWith( + systemVatId, + expect.arrayContaining([ + expect.arrayContaining(['kp2', true, expect.anything()]), + ]), + ); + }); + + it('deletes endpoint when terminating', async () => { + await systemVatHandle.terminate(true); + + expect(kernelStore.deleteEndpoint).toHaveBeenCalledWith(systemVatId); + }); + + it('does not delete endpoint when not terminating', async () => { + await systemVatHandle.terminate(false); + + expect(kernelStore.deleteEndpoint).not.toHaveBeenCalled(); + }); + }); + + describe('crank results', () => { + it('returns abort and terminate on delivery error', async () => { + (deliver as unknown as MockInstance).mockResolvedValueOnce( + 'delivery failed', + ); + + const result = await systemVatHandle.deliverMessage('o+0', { + methargs: { body: '[]', slots: [] }, + }); + + expect(result.abort).toBe(true); + expect(result.terminate).toStrictEqual({ + vatId: systemVatId, + reject: true, + info: expect.objectContaining({ + body: expect.stringContaining('delivery failed'), + }), + }); + }); + + it('returns abort and terminate on illegal syscall', async () => { + // Create a new handle with a syscall that triggers illegal syscall + const illegalSyscallKernelStore = { + ...kernelStore, + translateSyscallVtoK: vi.fn(() => { + throw new Error('illegal'); + }), + } as unknown as KernelStore; + + const handle = new SystemVatHandle({ + systemVatId, + kernelStore: illegalSyscallKernelStore, + kernelQueue, + deliver: vi.fn().mockImplementation(async (del: VatDeliveryObject) => { + // Simulate the vat making a syscall during delivery + if (del[0] === 'message') { + handle.getSyscallHandler()([ + 'send', + 'o+1', + { methargs: { body: '[]', slots: [] } }, + ]); + } + return null; + }), + logger, + }); + + const result = await handle.deliverMessage('o+0', { + methargs: { body: '[]', slots: [] }, + }); + + expect(result.abort).toBe(true); + expect(result.terminate).toBeDefined(); + expect(result.terminate?.reject).toBe(true); + }); + + it('returns terminate without abort on graceful exit', async () => { + // Create a handle that will request termination gracefully + const handle = new SystemVatHandle({ + systemVatId, + kernelStore, + kernelQueue, + deliver: vi.fn().mockImplementation(async () => { + // Simulate the vat calling syscall.exit(false, info) + handle.getSyscallHandler()([ + 'exit', + false, + { body: '"goodbye"', slots: [] }, + ]); + return null; + }), + logger, + }); + + const result = await handle.deliverMessage('o+0', { + methargs: { body: '[]', slots: [] }, + }); + + expect(result.abort).toBeUndefined(); + expect(result.terminate).toStrictEqual({ + vatId: systemVatId, + reject: false, + info: { body: '"goodbye"', slots: [] }, + }); + }); + }); + + describe('logging', () => { + it('creates handle without logger', () => { + const handleWithoutLogger = new SystemVatHandle({ + systemVatId, + kernelStore, + kernelQueue, + deliver, + }); + + // Handle is created successfully + expect(handleWithoutLogger.systemVatId).toBe(systemVatId); + }); + }); +}); diff --git a/packages/ocap-kernel/src/vats/SystemVatSupervisor.test.ts b/packages/ocap-kernel/src/vats/SystemVatSupervisor.test.ts new file mode 100644 index 000000000..7466ca477 --- /dev/null +++ b/packages/ocap-kernel/src/vats/SystemVatSupervisor.test.ts @@ -0,0 +1,402 @@ +import type { VatDeliveryObject } from '@agoric/swingset-liveslots'; +import { Logger } from '@metamask/logger'; +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import type { MockInstance } from 'vitest'; + +import type { SystemVatBuildRootObject, SystemVatId } from '../types.ts'; +import type { SystemVatExecuteSyscall } from './SystemVatSupervisor.ts'; +import { SystemVatSupervisor } from './SystemVatSupervisor.ts'; + +// Mock liveslots +const mockDispatch = vi.fn(); +vi.mock('@agoric/swingset-liveslots', () => ({ + makeLiveSlots: vi.fn(() => ({ + dispatch: mockDispatch, + })), +})); + +describe('SystemVatSupervisor', () => { + let buildRootObject: SystemVatBuildRootObject; + let vatPowers: Record; + let executeSyscall: SystemVatExecuteSyscall; + let logger: Logger; + const systemVatId: SystemVatId = 'sv0'; + + beforeEach(() => { + vi.clearAllMocks(); + mockDispatch.mockResolvedValue(undefined); + + buildRootObject = vi.fn(() => ({ + test: () => 'test result', + })); + vatPowers = { testPower: 'power' }; + executeSyscall = vi.fn().mockReturnValue(['ok', null]); + logger = { + debug: vi.fn(), + error: vi.fn(), + log: vi.fn(), + warn: vi.fn(), + subLogger: vi.fn(() => logger), + } as unknown as Logger; + }); + + describe('constructor', () => { + it('creates a SystemVatSupervisor with the given ID', () => { + const supervisor = new SystemVatSupervisor({ + id: systemVatId, + buildRootObject, + vatPowers, + parameters: undefined, + executeSyscall, + logger, + }); + + expect(supervisor.id).toBe(systemVatId); + }); + + it('initializes liveslots during construction', async () => { + const { makeLiveSlots } = await import('@agoric/swingset-liveslots'); + + const supervisor = new SystemVatSupervisor({ + id: systemVatId, + buildRootObject, + vatPowers, + parameters: undefined, + executeSyscall, + logger, + }); + + expect(supervisor.id).toBe(systemVatId); + expect(makeLiveSlots).toHaveBeenCalled(); + }); + + it('passes vatPowers to liveslots', async () => { + const { makeLiveSlots } = await import('@agoric/swingset-liveslots'); + + const supervisor = new SystemVatSupervisor({ + id: systemVatId, + buildRootObject, + vatPowers: { customPower: 'custom' }, + parameters: undefined, + executeSyscall, + logger, + }); + + expect(supervisor.id).toBe(systemVatId); + expect(makeLiveSlots).toHaveBeenCalledWith( + expect.anything(), + systemVatId, + { customPower: 'custom' }, + expect.anything(), + expect.anything(), + expect.anything(), + expect.anything(), + ); + }); + }); + + describe('start', () => { + it('dispatches startVat delivery', async () => { + const supervisor = new SystemVatSupervisor({ + id: systemVatId, + buildRootObject, + vatPowers, + parameters: undefined, + executeSyscall, + logger, + }); + + await supervisor.start(); + + expect(mockDispatch).toHaveBeenCalledWith( + expect.arrayContaining(['startVat', expect.anything()]), + ); + }); + + it('returns null on successful start', async () => { + const supervisor = new SystemVatSupervisor({ + id: systemVatId, + buildRootObject, + vatPowers, + parameters: undefined, + executeSyscall, + logger, + }); + + const result = await supervisor.start(); + + expect(result).toBeNull(); + }); + + it('returns error message on failed start', async () => { + mockDispatch.mockRejectedValueOnce(new Error('start failed')); + + const supervisor = new SystemVatSupervisor({ + id: systemVatId, + buildRootObject, + vatPowers, + parameters: undefined, + executeSyscall, + logger, + }); + + const result = await supervisor.start(); + + expect(result).toBe('start failed'); + expect(logger.error).toHaveBeenCalledWith( + expect.stringContaining('Start error'), + 'start failed', + ); + }); + }); + + describe('deliver', () => { + it('dispatches message deliveries', async () => { + const supervisor = new SystemVatSupervisor({ + id: systemVatId, + buildRootObject, + vatPowers, + parameters: undefined, + executeSyscall, + logger, + }); + + const delivery: VatDeliveryObject = [ + 'message', + 'o+0', + { methargs: { body: '[]', slots: [] }, result: null }, + ]; + await supervisor.deliver(delivery); + + expect(mockDispatch).toHaveBeenCalledWith(delivery); + }); + + it('dispatches notify deliveries', async () => { + const supervisor = new SystemVatSupervisor({ + id: systemVatId, + buildRootObject, + vatPowers, + parameters: undefined, + executeSyscall, + logger, + }); + + const delivery: VatDeliveryObject = [ + 'notify', + [['p-1', false, { body: '"resolved"', slots: [] }]], + ]; + await supervisor.deliver(delivery); + + expect(mockDispatch).toHaveBeenCalledWith(delivery); + }); + + it('returns null on successful delivery', async () => { + const supervisor = new SystemVatSupervisor({ + id: systemVatId, + buildRootObject, + vatPowers, + parameters: undefined, + executeSyscall, + logger, + }); + + const delivery: VatDeliveryObject = [ + 'message', + 'o+0', + { methargs: { body: '[]', slots: [] }, result: null }, + ]; + const result = await supervisor.deliver(delivery); + + expect(result).toBeNull(); + }); + + it('returns error message on failed delivery', async () => { + mockDispatch.mockRejectedValueOnce(new Error('delivery failed')); + + const supervisor = new SystemVatSupervisor({ + id: systemVatId, + buildRootObject, + vatPowers, + parameters: undefined, + executeSyscall, + logger, + }); + + const delivery: VatDeliveryObject = [ + 'message', + 'o+0', + { methargs: { body: '[]', slots: [] }, result: null }, + ]; + const result = await supervisor.deliver(delivery); + + expect(result).toBe('delivery failed'); + expect(logger.error).toHaveBeenCalledWith( + expect.stringContaining('Delivery error'), + 'delivery failed', + ); + }); + }); + + describe('syscall handling', () => { + it('passes syscalls to executeSyscall callback', async () => { + const { makeLiveSlots } = await import('@agoric/swingset-liveslots'); + + const supervisor = new SystemVatSupervisor({ + id: systemVatId, + buildRootObject, + vatPowers, + parameters: undefined, + executeSyscall, + logger, + }); + expect(supervisor.id).toBe(systemVatId); + + // Get the syscall object passed to makeLiveSlots + const syscall = (makeLiveSlots as unknown as MockInstance).mock + .calls[0][0]; + + // Test the send syscall + syscall.send('o+1', { body: '[]', slots: [] }, 'p-1'); + + expect(executeSyscall).toHaveBeenCalledWith([ + 'send', + 'o+1', + { methargs: { body: '[]', slots: [] }, result: 'p-1' }, + ]); + }); + + it('throws on syscall error', async () => { + const { makeLiveSlots } = await import('@agoric/swingset-liveslots'); + + const failingExecuteSyscall = vi + .fn() + .mockReturnValue(['error', 'syscall failed']); + + const supervisor = new SystemVatSupervisor({ + id: systemVatId, + buildRootObject, + vatPowers, + parameters: undefined, + executeSyscall: failingExecuteSyscall, + logger, + }); + expect(supervisor.id).toBe(systemVatId); + + // Get the syscall object passed to makeLiveSlots + const syscall = (makeLiveSlots as unknown as MockInstance).mock + .calls[0][0]; + + expect(() => + syscall.send('o+1', { body: '[]', slots: [] }, 'p-1'), + ).toThrow('syscall.send failed: syscall failed'); + }); + + it('throws for callNow syscall', async () => { + const { makeLiveSlots } = await import('@agoric/swingset-liveslots'); + + const supervisor = new SystemVatSupervisor({ + id: systemVatId, + buildRootObject, + vatPowers, + parameters: undefined, + executeSyscall, + logger, + }); + expect(supervisor.id).toBe(systemVatId); + + // Get the syscall object passed to makeLiveSlots + const syscall = (makeLiveSlots as unknown as MockInstance).mock + .calls[0][0]; + + expect(() => syscall.callNow()).toThrow( + 'callNow not supported for system vats', + ); + }); + }); + + describe('ephemeral vatstore', () => { + it('provides ephemeral vatstore operations', async () => { + const { makeLiveSlots } = await import('@agoric/swingset-liveslots'); + + // eslint-disable-next-line no-new + new SystemVatSupervisor({ + id: systemVatId, + buildRootObject, + vatPowers, + parameters: undefined, + executeSyscall, + logger, + }); + + // Get the syscall object passed to makeLiveSlots + const syscall = (makeLiveSlots as unknown as MockInstance).mock + .calls[0][0]; + + // Test vatstore operations + expect(syscall.vatstoreGet('key')).toBeUndefined(); + + syscall.vatstoreSet('key', 'value'); + expect(syscall.vatstoreGet('key')).toBe('value'); + + syscall.vatstoreDelete('key'); + expect(syscall.vatstoreGet('key')).toBeUndefined(); + }); + + it('provides getNextKey for ephemeral vatstore', async () => { + const { makeLiveSlots } = await import('@agoric/swingset-liveslots'); + + // eslint-disable-next-line no-new + new SystemVatSupervisor({ + id: systemVatId, + buildRootObject, + vatPowers, + parameters: undefined, + executeSyscall, + logger, + }); + + // Get the syscall object passed to makeLiveSlots + const syscall = (makeLiveSlots as unknown as MockInstance).mock + .calls[0][0]; + + syscall.vatstoreSet('a', '1'); + syscall.vatstoreSet('b', '2'); + syscall.vatstoreSet('c', '3'); + + expect(syscall.vatstoreGetNextKey('a')).toBe('b'); + expect(syscall.vatstoreGetNextKey('b')).toBe('c'); + expect(syscall.vatstoreGetNextKey('c')).toBeUndefined(); + }); + }); + + describe('parameters', () => { + it('passes parameters to buildRootObject', async () => { + const { makeLiveSlots } = await import('@agoric/swingset-liveslots'); + + // eslint-disable-next-line no-new + new SystemVatSupervisor({ + id: systemVatId, + buildRootObject, + vatPowers, + parameters: { testParam: 'testValue' }, + executeSyscall, + logger, + }); + + // Get the buildVatNamespace function passed to makeLiveSlots + const buildVatNamespace = (makeLiveSlots as unknown as MockInstance).mock + .calls[0][6]; + + // Call buildVatNamespace to get the namespace + const namespace = await buildVatNamespace({}, {}); + + // Call buildRootObject from the namespace + (namespace.buildRootObject as CallableFunction)({}); + + // Verify buildRootObject was called with parameters + expect(buildRootObject).toHaveBeenCalledWith(expect.anything(), { + testParam: 'testValue', + }); + }); + }); +}); diff --git a/packages/ocap-kernel/src/vats/SystemVatSyscall.test.ts b/packages/ocap-kernel/src/vats/SystemVatSyscall.test.ts new file mode 100644 index 000000000..910b39e53 --- /dev/null +++ b/packages/ocap-kernel/src/vats/SystemVatSyscall.test.ts @@ -0,0 +1,323 @@ +import type { + Message, + VatOneResolution, + VatSyscallObject, +} from '@agoric/swingset-liveslots'; +import type { Logger } from '@metamask/logger'; +import type { MockInstance } from 'vitest'; +import { describe, it, expect, vi, beforeEach } from 'vitest'; + +import type { KernelQueue } from '../KernelQueue.ts'; +import type { KernelStore } from '../store/index.ts'; +import type { SystemVatId } from '../types.ts'; +import { SystemVatSyscall } from './SystemVatSyscall.ts'; + +describe('SystemVatSyscall', () => { + let kernelQueue: KernelQueue; + let kernelStore: KernelStore; + let logger: Logger; + let isActive: () => boolean; + let systemVatSyscall: SystemVatSyscall; + const systemVatId: SystemVatId = 'sv0'; + + beforeEach(() => { + kernelQueue = { + enqueueSend: vi.fn(), + resolvePromises: vi.fn(), + enqueueNotify: vi.fn(), + } as unknown as KernelQueue; + kernelStore = { + translateSyscallVtoK: vi.fn((_: string, vso: VatSyscallObject) => vso), + getKernelPromise: vi.fn(), + addPromiseSubscriber: vi.fn(), + clearReachableFlag: vi.fn(), + getReachableFlag: vi.fn(), + forgetKref: vi.fn(), + } as unknown as KernelStore; + logger = { + debug: vi.fn(), + error: vi.fn(), + log: vi.fn(), + warn: vi.fn(), + subLogger: vi.fn(() => logger), + } as unknown as Logger; + isActive = vi.fn(() => true); + systemVatSyscall = new SystemVatSyscall({ + systemVatId, + kernelQueue, + kernelStore, + isActive, + logger, + }); + }); + + it('enqueues run for send syscall', () => { + const target = 'o+1'; + const message = {} as unknown as Message; + const vso = ['send', target, message] as unknown as VatSyscallObject; + systemVatSyscall.handleSyscall(vso); + expect(kernelQueue.enqueueSend).toHaveBeenCalledWith(target, message); + }); + + it('calls resolvePromises for resolve syscall', () => { + const resolution = ['kp1', false, {}] as unknown as VatOneResolution; + const vso = ['resolve', [resolution]] as unknown as VatSyscallObject; + systemVatSyscall.handleSyscall(vso); + expect(kernelQueue.resolvePromises).toHaveBeenCalledWith(systemVatId, [ + resolution, + ]); + }); + + describe('subscribe syscall', () => { + it('subscribes to unresolved promise', () => { + ( + kernelStore.getKernelPromise as unknown as MockInstance + ).mockReturnValueOnce({ + state: 'unresolved', + }); + const vso = ['subscribe', 'kp1'] as unknown as VatSyscallObject; + systemVatSyscall.handleSyscall(vso); + expect(kernelStore.addPromiseSubscriber).toHaveBeenCalledWith( + systemVatId, + 'kp1', + ); + }); + + it('notifies for resolved promise', () => { + ( + kernelStore.getKernelPromise as unknown as MockInstance + ).mockReturnValueOnce({ + state: 'fulfilled', + }); + const vso = ['subscribe', 'kp1'] as unknown as VatSyscallObject; + systemVatSyscall.handleSyscall(vso); + expect(kernelQueue.enqueueNotify).toHaveBeenCalledWith( + systemVatId, + 'kp1', + ); + }); + }); + + describe('dropImports syscall', () => { + it('clears reachable flags for valid imports', () => { + const vso = [ + 'dropImports', + ['o-1', 'o-2'], + ] as unknown as VatSyscallObject; + systemVatSyscall.handleSyscall(vso); + expect(kernelStore.clearReachableFlag).toHaveBeenCalledWith( + systemVatId, + 'o-1', + ); + expect(kernelStore.clearReachableFlag).toHaveBeenCalledWith( + systemVatId, + 'o-2', + ); + }); + + it.each([ + [ + 'o+1', + `system vat ${systemVatId} issued invalid syscall dropImports for o+1`, + ], + [ + 'p-1', + `system vat ${systemVatId} issued invalid syscall dropImports for p-1`, + ], + ])('returns error for invalid ref %s', (ref, errMsg) => { + ( + kernelStore.translateSyscallVtoK as unknown as MockInstance + ).mockImplementationOnce(() => { + throw new Error(errMsg); + }); + const vso = ['dropImports', [ref]] as unknown as VatSyscallObject; + const result = systemVatSyscall.handleSyscall(vso); + expect(result).toStrictEqual(['error', errMsg]); + }); + }); + + describe('retireImports syscall', () => { + it('forgets kref when not reachable', () => { + ( + kernelStore.getReachableFlag as unknown as MockInstance + ).mockReturnValueOnce(false); + const vso = ['retireImports', ['o-1']] as unknown as VatSyscallObject; + systemVatSyscall.handleSyscall(vso); + expect(kernelStore.forgetKref).toHaveBeenCalledWith(systemVatId, 'o-1'); + }); + + it('returns error if still reachable', () => { + ( + kernelStore.translateSyscallVtoK as unknown as MockInstance + ).mockImplementationOnce(() => { + ( + kernelStore.getReachableFlag as unknown as MockInstance + ).mockReturnValueOnce(true); + throw new Error('syscall.retireImports but o-1 is still reachable'); + }); + const vso = ['retireImports', ['o-1']] as unknown as VatSyscallObject; + const result = systemVatSyscall.handleSyscall(vso); + expect(result).toStrictEqual([ + 'error', + 'syscall.retireImports but o-1 is still reachable', + ]); + }); + }); + + describe('exportCleanup syscalls', () => { + it('retires exports when not reachable', () => { + ( + kernelStore.getReachableFlag as unknown as MockInstance + ).mockReturnValueOnce(false); + const vso = ['retireExports', ['o+1']] as unknown as VatSyscallObject; + systemVatSyscall.handleSyscall(vso); + expect(kernelStore.forgetKref).toHaveBeenCalledWith(systemVatId, 'o+1'); + expect(logger.debug).toHaveBeenCalledWith( + 'retireExports: deleted object o+1', + ); + }); + + it('returns error for reachable exports', () => { + ( + kernelStore.translateSyscallVtoK as unknown as MockInstance + ).mockImplementationOnce(() => { + ( + kernelStore.getReachableFlag as unknown as MockInstance + ).mockReturnValueOnce(true); + throw new Error('syscall.retireExports but o+1 is still reachable'); + }); + const vso = ['retireExports', ['o+1']] as unknown as VatSyscallObject; + const result = systemVatSyscall.handleSyscall(vso); + expect(result).toStrictEqual([ + 'error', + 'syscall.retireExports but o+1 is still reachable', + ]); + }); + + it('abandons exports without reachability check', () => { + const vso = ['abandonExports', ['o+1']] as unknown as VatSyscallObject; + systemVatSyscall.handleSyscall(vso); + expect(kernelStore.forgetKref).toHaveBeenCalledWith(systemVatId, 'o+1'); + expect(logger.debug).toHaveBeenCalledWith( + 'abandonExports: deleted object o+1', + ); + }); + + it('returns error for invalid abandonExports refs', () => { + ( + kernelStore.translateSyscallVtoK as unknown as MockInstance + ).mockImplementationOnce(() => { + throw new Error( + `system vat ${systemVatId} issued invalid syscall abandonExports for o-1`, + ); + }); + const vso = ['abandonExports', ['o-1']] as unknown as VatSyscallObject; + const result = systemVatSyscall.handleSyscall(vso); + expect(result).toStrictEqual([ + 'error', + `system vat ${systemVatId} issued invalid syscall abandonExports for o-1`, + ]); + }); + }); + + describe('exit syscall', () => { + it('records vat termination request', () => { + const vso = [ + 'exit', + true, + { message: 'error' }, + ] as unknown as VatSyscallObject; + systemVatSyscall.handleSyscall(vso); + expect(systemVatSyscall.vatRequestedTermination).toStrictEqual({ + reject: true, + info: { message: 'error' }, + }); + }); + }); + + describe('error handling', () => { + it('handles system vat not active error', () => { + (isActive as unknown as MockInstance).mockReturnValueOnce(false); + const vso = ['send', 'o+1', {}] as unknown as VatSyscallObject; + const result = systemVatSyscall.handleSyscall(vso); + + expect(result).toStrictEqual(['error', 'system vat not found']); + expect(systemVatSyscall.illegalSyscall).toBeDefined(); + }); + + it('handles general syscall errors', () => { + const error = new Error('test error'); + ( + kernelStore.translateSyscallVtoK as unknown as MockInstance + ).mockImplementationOnce(() => { + throw error; + }); + + const vso = ['send', 'o+1', {}] as unknown as VatSyscallObject; + const result = systemVatSyscall.handleSyscall(vso); + + expect(result).toStrictEqual(['error', 'test error']); + expect(logger.error).toHaveBeenCalledWith( + `Fatal syscall error in system vat ${systemVatId}`, + error, + ); + }); + }); + + describe('unsupported syscalls', () => { + it.each([ + ['vatstoreGet'], + ['vatstoreGetNextKey'], + ['vatstoreSet'], + ['vatstoreDelete'], + ['callNow'], + ])('%s warns about unsupported syscall', (op) => { + const spy = vi.spyOn(logger, 'warn'); + const vso = [op, []] as unknown as VatSyscallObject; + systemVatSyscall.handleSyscall(vso); + expect(spy).toHaveBeenCalledWith( + expect.stringContaining('unsupported syscall'), + vso, + ); + spy.mockRestore(); + }); + }); + + describe('unknown syscalls', () => { + it('warns about unknown syscall', () => { + const spy = vi.spyOn(logger, 'warn'); + const vso = ['unknownOp', []] as unknown as VatSyscallObject; + systemVatSyscall.handleSyscall(vso); + expect(spy).toHaveBeenCalledWith( + expect.stringContaining('unknown syscall'), + vso, + ); + spy.mockRestore(); + }); + }); + + describe('logging', () => { + it('is disabled if logger is undefined', () => { + const logSpy = vi.spyOn(console, 'log'); + const syscallWithoutLogger = new SystemVatSyscall({ + systemVatId, + kernelQueue, + kernelStore, + isActive, + }); + syscallWithoutLogger.handleSyscall([ + 'send', + 'o+1', + {}, + ] as VatSyscallObject); + expect(logSpy).not.toHaveBeenCalled(); + expect(logger.log).not.toHaveBeenCalled(); + }); + }); + + describe('systemVatId property', () => { + it('exposes the system vat ID', () => { + expect(systemVatSyscall.systemVatId).toBe(systemVatId); + }); + }); +}); diff --git a/userspace-as-vat.md b/userspace-as-vat.md new file mode 100644 index 000000000..5d034010a --- /dev/null +++ b/userspace-as-vat.md @@ -0,0 +1,336 @@ +# Plan: System Vats/Subclusters for ocap-kernel + +## Overview + +Enable user space (omnium) to use `E()` on vat object presences directly by making the background (and optionally UI) part of a **system subcluster** called the "host subcluster". + +## Problem Statement + +Currently, the background communicates with the kernel via CapTP and receives kref strings. To call methods on vat objects, it must use `kernel.queueMessage(kref, method, args)` which returns more kref strings. The `rekm/kref-presence` branch attempted to solve this by creating "dummy" presences that forward to `queueMessage()`, but this approach is complex and doesn't integrate well with the kernel's reference management. + +## Solution + +Introduce **system subclusters** - subclusters whose vats run without compartment isolation in the host process. The first system subcluster is the **host subcluster**: + +- **System vats** run without compartments, directly in the host process (e.g., background service worker) +- **System subclusters** are configurable like dynamic subclusters, with a bootstrap vat and optional additional vats +- The **host subcluster** bootstrap vat receives a kernel facet as a vatpower +- The bootstrap vat controls how kernel access is shared with other vats in the subcluster + +This enables: +- E()-callable presences from krefs +- Third-party handoff between vats +- Promise pipelining +- Proper integration with kernel GC + +## Terminology + +- **System vat**: A vat that runs without compartment isolation in the host process +- **System subcluster**: A subcluster composed of system vats +- **Host subcluster**: The specific system subcluster for the host application (omnium) +- **Dynamic vat/subcluster**: Regular vats that run in compartments (existing behavior) + +## Key Constraints + +1. System vats do NOT execute in a compartment - run directly in host process +2. System vats do NOT participate in kernel persistence machinery +3. System subcluster bootstrap vat receives a kernel facet as a vatpower +4. System vats do NOT export durable capabilities +5. Both browser and Node.js runtimes must be supported + +## Architecture + +``` +Host Process (Background/Node.js) Kernel ++---------------------------------------+ +---------------------------+ +| Host Subcluster (System Subcluster) | | | +| +-----------------------------------+ | | | +| | Bootstrap Vat (e.g., background) | | | SystemVatHandle (per vat)| +| | - receives kernel facet vatpower |<--->| - EndpointHandle impl | +| | - uses E() on presences | | | - VRef<->KRef xlat | +| +-----------------------------------+ | | | +| +-----------------------------------+ | | | +| | Other vats (e.g., UI) |<--->| SystemVatHandle (per vat)| +| | - receives refs from bootstrap | | | | +| +-----------------------------------+ | | | +| | | | +| SystemVatSupervisor (per vat) | | KernelRouter | +| - liveslots (no compartment) | | - routes to system vats | +| - dispatch function | | | +| - syscall interface | | KernelFacet service | ++---------------------------------------+ +---------------------------+ +``` + +## Key Design Decisions + +### D1: Liveslots Without Compartment +Run liveslots in the host process WITHOUT compartment isolation. The `buildVatNamespace` callback returns the vat module directly (no `importBundle`). This provides: +- VRef allocation and clist management +- Presence creation for imported objects +- Syscall interface +- Promise tracking + +### D2: System Vat ID Format +Use `sv0`, `sv1`, etc. (prefix "sv" for "system vat") to distinguish from dynamic vats (`v0`, `v1`). + +### D3: Kernel Facet as Vatpower (Bootstrap Only) +The kernel facet is a vatpower passed ONLY to the system subcluster's bootstrap vat: +- `launchSubcluster(config)` - launch dynamic subclusters, returns presences +- `terminateSubcluster(id)` +- `getStatus()` +- Other privileged operations + +Other vats in the system subcluster receive access via normal vat-to-vat communication from bootstrap. + +### D4: Configurable System Subcluster +System subclusters are configurable like dynamic subclusters: +- Define bootstrap vat and additional vats +- Bootstrap vat receives kernel facet vatpower +- Bootstrap message passes roots to all vats in subcluster +- Bootstrap vat controls access distribution + +### D5: Both Runtimes Supported +Implementation must work for both: +- Browser: `packages/kernel-browser-runtime` +- Node.js: `packages/nodejs` + +Core system vat logic in `packages/ocap-kernel` is runtime-agnostic. + +## Implementation Phases + +### Phase 1: Core Infrastructure (packages/ocap-kernel) + +**1.1: Add system vat types to types.ts** +- Add `SystemVatId` type (`sv${number}`) +- Add `isSystemVatId()` type guard +- Update `EndpointId` to include `SystemVatId` +- Add `SystemSubclusterConfig` type (extends ClusterConfig with system vat specifics) + +**1.2: Create SystemVatHandle** +File: `packages/ocap-kernel/src/vats/SystemVatHandle.ts` + +Similar to `VatHandle` but: +- Does NOT manage vatstore persistence (system vats are non-durable) +- Simpler `#getDeliveryCrankResults()` - no vatstore checkpoints +- Communication via callback functions instead of streams (runtime provides transport) + +**1.3: Create SystemVatSyscall** +File: `packages/ocap-kernel/src/vats/SystemVatSyscall.ts` + +Reuse logic from `VatSyscall.ts` but: +- No persistence concerns +- Simpler state tracking + +**1.4: Create KernelFacetService** +File: `packages/ocap-kernel/src/services/KernelFacetService.ts` + +Provides privileged kernel operations as a remotable object: +- `launchSubcluster(config)` - launch dynamic subclusters +- `terminateSubcluster(id)` +- `getStatus()` +- `reloadSubcluster(id)` + +**1.5: Create SystemSubclusterManager** +File: `packages/ocap-kernel/src/vats/SystemSubclusterManager.ts` + +Manages system subclusters: +- Launches system vats with correct vatpowers +- Bootstrap vat receives kernel facet +- Coordinates with SubclusterManager for tracking + +**1.6: Update Kernel.ts** +- Add `launchSystemSubcluster()` method +- Update `#getEndpoint()` to handle system vat IDs +- Register KernelFacetService during initialization +- Accept system vat connection callbacks from runtime + +### Phase 2: Shared System Vat Supervisor (new package or in ocap-kernel) + +**2.1: Create SystemVatSupervisor** +File: `packages/ocap-kernel/src/vats/SystemVatSupervisor.ts` + +Runtime-agnostic supervisor for system vats: +- Uses liveslots without compartment +- `buildVatNamespace` returns provided module directly +- Non-persistent vatstore (Map-based) +- Accepts syscall callback for kernel communication +- Provides dispatch function for deliveries + +This is in ocap-kernel because it's runtime-agnostic - runtimes just provide the transport. + +### Phase 3: Browser Runtime (packages/kernel-browser-runtime) + +**3.1: Create host subcluster utilities** +File: `packages/kernel-browser-runtime/src/host-subcluster/index.ts` + +- `makeHostSubcluster()` factory function +- Sets up SystemVatSupervisor for each vat in host subcluster +- Provides transport (MessagePort) between supervisors and kernel + +**3.2: Update kernel-worker initialization** +File: `packages/kernel-browser-runtime/src/kernel-worker/kernel-worker.ts` + +- Remove CapTP setup +- Accept host subcluster vat connections +- Register SystemVatHandles with kernel + +**3.3: Remove CapTP code** +Files to remove: +- `packages/kernel-browser-runtime/src/background-captp.ts` +- `packages/kernel-browser-runtime/src/kernel-worker/captp/` directory + +### Phase 4: Node.js Runtime (packages/nodejs) + +**4.1: Create host subcluster utilities** +File: `packages/nodejs/src/host-subcluster/index.ts` + +- Similar to browser but using appropriate transport (direct calls or MessageChannel) +- `makeHostSubcluster()` factory function + +**4.2: Update Node.js kernel initialization** +- Support host subcluster configuration +- Register SystemVatHandles + +### Phase 5: Integration + +**5.1: Update omnium-gatherum** +- Use `makeHostSubcluster()` instead of `makeBackgroundCapTP()` +- Background code becomes bootstrap vat's `buildRootObject` +- UI (if in host subcluster) becomes another vat +- Bootstrap vat distributes kernel access as needed + +## Critical Files + +### Files to Create +| File | Purpose | +|------|---------| +| `packages/ocap-kernel/src/vats/SystemVatHandle.ts` | EndpointHandle for system vats (kernel-side) | +| `packages/ocap-kernel/src/vats/SystemVatSyscall.ts` | Syscall handler for system vats | +| `packages/ocap-kernel/src/vats/SystemVatSupervisor.ts` | Liveslots supervisor (runtime-agnostic) | +| `packages/ocap-kernel/src/vats/SystemSubclusterManager.ts` | Manages system subcluster lifecycle | +| `packages/ocap-kernel/src/services/KernelFacetService.ts` | Kernel facet exposed to bootstrap vat | +| `packages/kernel-browser-runtime/src/host-subcluster/index.ts` | Browser host subcluster setup | +| `packages/nodejs/src/host-subcluster/index.ts` | Node.js host subcluster setup | + +### Files to Modify +| File | Changes | +|------|---------| +| `packages/ocap-kernel/src/types.ts` | Add `SystemVatId`, `isSystemVatId()`, `SystemSubclusterConfig` | +| `packages/ocap-kernel/src/Kernel.ts` | Add `launchSystemSubcluster()`, update `#getEndpoint()` | +| `packages/kernel-browser-runtime/src/kernel-worker/kernel-worker.ts` | Remove CapTP, add system vat support | + +### Files to Remove +| File | Reason | +|------|--------| +| `packages/kernel-browser-runtime/src/background-captp.ts` | Replaced by host subcluster | +| `packages/kernel-browser-runtime/src/kernel-worker/captp/` | Replaced by host subcluster | + +## Key Implementation Details + +### buildVatNamespace Without Compartment + +```typescript +// In SystemVatSupervisor +const buildVatNamespace = async ( + lsEndowments: Record, + _inescapableGlobalProperties: object, +): Promise> => { + // NO importBundle - return the system vat module directly + return { + buildRootObject: this.#buildRootObject, + }; +}; +``` + +### System Subcluster Configuration + +```typescript +type SystemSubclusterConfig = { + bootstrap: string; // Name of bootstrap vat + vats: Record; +}; + +// Example usage +const hostSubclusterConfig = { + bootstrap: 'background', + vats: { + background: { buildRootObject: backgroundBuildRootObject }, + ui: { buildRootObject: uiBuildRootObject }, + }, +}; +``` + +### Syscall Flow + +``` +System Vat Code -> E(presence).method(args) + -> liveslots marshals to VRef + -> syscall.send(vref, methargs, resultVRef) + -> SystemVatSupervisor.executeSyscall() + -> [transport callback] -> SystemVatHandle + -> SystemVatSyscall.handleSyscall() (VRef->KRef) + -> KernelQueue.enqueueSend() +``` + +### Delivery Flow + +``` +KernelQueue -> KernelRouter.deliver() + -> SystemVatHandle.deliverMessage(vref, message) + -> [transport callback] -> SystemVatSupervisor + -> liveslots.dispatch(['message', ...]) + -> System vat code method invoked with presence args +``` + +### Host Subcluster Bootstrap + +```typescript +// Bootstrap vat receives kernel facet as vatpower +export function buildRootObject({ kernelFacet }, parameters) { + return makeDefaultExo('hostRoot', { + async bootstrap(roots, services) { + // roots contains presences to other vats in host subcluster + // e.g., roots.ui is the UI vat's root object + + // Launch a dynamic subcluster + const result = await E(kernelFacet).launchSubcluster(dynamicConfig); + // result.root is an E()-callable presence! + + // Pass reference to UI vat if needed + await E(roots.ui).setKernel(kernelFacet); + } + }); +} +``` + +### Obtaining Presences from Kernel Facet + +When `E(kernelFacet).launchSubcluster(config)` is called: +1. KernelFacetService's method calls kernel, gets result with root kref +2. Result serialized with kref in slots via kernel-marshal +3. Delivered to system vat via `deliverNotify` +4. Liveslots sees kref slot, creates presence via c-list +5. Bootstrap vat receives E()-callable presence directly + +## Verification + +### Unit Tests +- `SystemVatHandle` tests: Mock supervisor, test delivery/syscall handling +- `SystemVatSupervisor` tests: Mock kernel connection, test liveslots integration +- `SystemSubclusterManager` tests: Test subcluster lifecycle +- `KernelFacetService` tests: Test service methods + +### Integration Tests +- Full host subcluster lifecycle with real kernel +- Multiple vats in host subcluster communicating +- Bootstrap vat distributing kernel access to other vats +- Third-party handoff between dynamic and system vats +- Promise pipelining through system vats + +### E2E Tests +- Migrate/adapt tests from `rekm/kref-presence` branch +- Test behaviors from `kernel-to-host-captp.test.ts` +- Browser: background + UI as host subcluster +- Node.js: equivalent host subcluster tests From a6e2883bb58c6dc6daa9886c2ca2ee39ff670548 Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Tue, 27 Jan 2026 21:45:07 -0800 Subject: [PATCH 07/41] refactor(ocap-kernel): Simplify type annotations in SystemSubclusterManager Use direct type annotations on variable declarations instead of relying on type inference for template literal types. Co-Authored-By: Claude --- packages/ocap-kernel/src/vats/SystemSubclusterManager.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/ocap-kernel/src/vats/SystemSubclusterManager.ts b/packages/ocap-kernel/src/vats/SystemSubclusterManager.ts index 07e5f1d48..90d41a3bd 100644 --- a/packages/ocap-kernel/src/vats/SystemSubclusterManager.ts +++ b/packages/ocap-kernel/src/vats/SystemSubclusterManager.ts @@ -102,8 +102,7 @@ export class SystemSubclusterManager { * @returns A new system vat ID. */ #allocateSystemVatId(): SystemVatId { - // TypeScript cannot narrow template literal types from string interpolation - const id = `sv${this.#nextSystemVatId}`; + const id: SystemVatId = `sv${this.#nextSystemVatId}`; this.#nextSystemVatId += 1; return id; } @@ -114,7 +113,7 @@ export class SystemSubclusterManager { * @returns A new system subcluster ID. */ #allocateSystemSubclusterId(): SystemSubclusterId { - const id = `ss${this.#nextSystemSubclusterId}`; + const id: SystemSubclusterId = `ss${this.#nextSystemSubclusterId}`; this.#nextSystemSubclusterId += 1; return id; } From 9fa93e02df934fed54bcbec3928bed6a75d18a0c Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Tue, 27 Jan 2026 21:56:01 -0800 Subject: [PATCH 08/41] feat(kernel-browser-runtime): Use kernel host subcluster for CapTP bootstrap Update kernel-worker to launch a kernel host system subcluster instead of using a plain kernel facade for CapTP. The kernel host vat: - Is a proper system vat with liveslots reference management - Receives the kernel facet as a vatpower - Uses E() to call kernel facet methods - Returns proper presences for dynamic subcluster roots This enables the background to receive E()-callable presences when launching dynamic subclusters through the kernel host. Co-Authored-By: Claude --- .../src/kernel-worker/kernel-host-vat.test.ts | 94 ++++++++++++ .../src/kernel-worker/kernel-host-vat.ts | 145 ++++++++++++++++++ .../src/kernel-worker/kernel-worker.ts | 47 ++++-- 3 files changed, 275 insertions(+), 11 deletions(-) create mode 100644 packages/kernel-browser-runtime/src/kernel-worker/kernel-host-vat.test.ts create mode 100644 packages/kernel-browser-runtime/src/kernel-worker/kernel-host-vat.ts diff --git a/packages/kernel-browser-runtime/src/kernel-worker/kernel-host-vat.test.ts b/packages/kernel-browser-runtime/src/kernel-worker/kernel-host-vat.test.ts new file mode 100644 index 000000000..833d9b99f --- /dev/null +++ b/packages/kernel-browser-runtime/src/kernel-worker/kernel-host-vat.test.ts @@ -0,0 +1,94 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; + +import type { KernelHostRoot } from './kernel-host-vat.ts'; +import { makeKernelHostSubclusterConfig } from './kernel-host-vat.ts'; + +describe('makeKernelHostSubclusterConfig', () => { + const mockKernelFacet = { + launchSubcluster: vi.fn(), + terminateSubcluster: vi.fn(), + getStatus: vi.fn(), + reloadSubcluster: vi.fn(), + getSubcluster: vi.fn(), + getSubclusters: vi.fn(), + }; + + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('returns a valid system subcluster config', () => { + const onRootCreated = vi.fn(); + const config = makeKernelHostSubclusterConfig(onRootCreated); + + expect(config.bootstrap).toBe('kernelHost'); + expect(config.vats.kernelHost).toBeDefined(); + expect(config.vats.kernelHost.buildRootObject).toBeTypeOf('function'); + }); + + it('invokes onRootCreated callback when buildRootObject is called', () => { + const onRootCreated = vi.fn(); + const config = makeKernelHostSubclusterConfig(onRootCreated); + + const root = config.vats.kernelHost.buildRootObject({ + kernelFacet: mockKernelFacet, + }); + + expect(onRootCreated).toHaveBeenCalledWith(root); + }); + + describe('kernel host root', () => { + let root: KernelHostRoot; + + beforeEach(() => { + const onRootCreated = vi.fn(); + const config = makeKernelHostSubclusterConfig(onRootCreated); + root = config.vats.kernelHost.buildRootObject({ + kernelFacet: mockKernelFacet, + }) as KernelHostRoot; + }); + + it('creates root with expected methods', () => { + expect(root.ping).toBeTypeOf('function'); + expect(root.launchSubcluster).toBeTypeOf('function'); + expect(root.terminateSubcluster).toBeTypeOf('function'); + expect(root.getStatus).toBeTypeOf('function'); + expect(root.reloadSubcluster).toBeTypeOf('function'); + expect(root.getSubcluster).toBeTypeOf('function'); + expect(root.getSubclusters).toBeTypeOf('function'); + }); + + it('ping returns pong', async () => { + const result = await root.ping(); + expect(result).toBe('pong'); + }); + + // Note: launchSubcluster, terminateSubcluster, getStatus, reloadSubcluster + // use E() which requires endo initialization. These are integration tested + // via the full system tests rather than unit tests. + + it('getSubcluster calls kernel facet synchronously', () => { + mockKernelFacet.getSubcluster.mockReturnValue({ + id: 's1', + config: { bootstrap: 'test', vats: {} }, + vats: {}, + }); + + const result = root.getSubcluster('s1'); + + expect(mockKernelFacet.getSubcluster).toHaveBeenCalledWith('s1'); + expect(result?.id).toBe('s1'); + }); + + it('getSubclusters calls kernel facet synchronously', () => { + mockKernelFacet.getSubclusters.mockReturnValue([ + { id: 's1', config: { bootstrap: 'test', vats: {} }, vats: {} }, + ]); + + const result = root.getSubclusters(); + + expect(mockKernelFacet.getSubclusters).toHaveBeenCalled(); + expect(result).toHaveLength(1); + }); + }); +}); diff --git a/packages/kernel-browser-runtime/src/kernel-worker/kernel-host-vat.ts b/packages/kernel-browser-runtime/src/kernel-worker/kernel-host-vat.ts new file mode 100644 index 000000000..265db51ec --- /dev/null +++ b/packages/kernel-browser-runtime/src/kernel-worker/kernel-host-vat.ts @@ -0,0 +1,145 @@ +import { E } from '@endo/eventual-send'; +import { makeDefaultExo } from '@metamask/kernel-utils/exo'; +import type { + SystemVatBuildRootObject, + SystemSubclusterConfig, + ClusterConfig, + KernelStatus, + Subcluster, +} from '@metamask/ocap-kernel'; +import type { SlotValue } from '@metamask/ocap-kernel'; + +/** + * The kernel host vat's root object interface. + * + * This is the interface exposed by the kernel host vat to external clients + * (like the background service worker) via CapTP. + */ +export type KernelHostRoot = { + /** + * Ping the kernel host. + * + * @returns 'pong' to confirm the host is responsive. + */ + ping: () => Promise<'pong'>; + + /** + * Launch a dynamic subcluster. + * + * @param config - Configuration for the subcluster. + * @returns The launch result with subcluster ID and root presence. + */ + launchSubcluster: ( + config: ClusterConfig, + ) => Promise<{ subclusterId: string; root: SlotValue }>; + + /** + * Terminate a subcluster. + * + * @param subclusterId - The ID of the subcluster to terminate. + */ + terminateSubcluster: (subclusterId: string) => Promise; + + /** + * Get kernel status. + * + * @returns The current kernel status. + */ + getStatus: () => Promise; + + /** + * Reload a subcluster. + * + * @param subclusterId - The ID of the subcluster to reload. + * @returns The reloaded subcluster. + */ + reloadSubcluster: (subclusterId: string) => Promise; + + /** + * Get a subcluster by ID. + * + * @param subclusterId - The ID of the subcluster. + * @returns The subcluster or undefined if not found. + */ + getSubcluster: (subclusterId: string) => Subcluster | undefined; + + /** + * Get all subclusters. + * + * @returns Array of all subclusters. + */ + getSubclusters: () => Subcluster[]; +}; + +/** + * The kernel facet interface as provided to system vat bootstrap. + * This is the vatpower passed to the kernel host vat. + */ +type KernelFacet = { + launchSubcluster: ( + config: ClusterConfig, + ) => Promise<{ subclusterId: string; root: SlotValue }>; + terminateSubcluster: (subclusterId: string) => Promise; + getStatus: () => Promise; + reloadSubcluster: (subclusterId: string) => Promise; + getSubcluster: (subclusterId: string) => Subcluster | undefined; + getSubclusters: () => Subcluster[]; +}; + +/** + * Create the configuration for launching the kernel host subcluster. + * + * @param onRootCreated - Callback invoked when the root object is created. + * @returns The system subcluster configuration. + */ +export function makeKernelHostSubclusterConfig( + onRootCreated: (root: KernelHostRoot) => void, +): SystemSubclusterConfig { + const buildRootObject: SystemVatBuildRootObject = (vatPowers) => { + const kernelFacet = vatPowers.kernelFacet as KernelFacet; + + const root = makeDefaultExo('KernelHostRoot', { + ping: async () => 'pong' as const, + + launchSubcluster: async (config: ClusterConfig) => { + // Use E() to call kernel facet - this gives us proper reference handling + return E(kernelFacet).launchSubcluster(config); + }, + + terminateSubcluster: async (subclusterId: string) => { + return E(kernelFacet).terminateSubcluster(subclusterId); + }, + + getStatus: async () => { + return E(kernelFacet).getStatus(); + }, + + reloadSubcluster: async (subclusterId: string) => { + return E(kernelFacet).reloadSubcluster(subclusterId); + }, + + getSubcluster: (subclusterId: string) => { + // Synchronous method - call directly + return kernelFacet.getSubcluster(subclusterId); + }, + + getSubclusters: () => { + // Synchronous method - call directly + return kernelFacet.getSubclusters(); + }, + }) as KernelHostRoot; + + // Capture the root object for external use (e.g., CapTP bootstrap) + onRootCreated(root); + + return root; + }; + + return { + bootstrap: 'kernelHost', + vats: { + kernelHost: { buildRootObject }, + }, + }; +} +harden(makeKernelHostSubclusterConfig); 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 ecc9b8ad8..fb1be1e11 100644 --- a/packages/kernel-browser-runtime/src/kernel-worker/kernel-worker.ts +++ b/packages/kernel-browser-runtime/src/kernel-worker/kernel-worker.ts @@ -1,3 +1,4 @@ +import { makeCapTP } from '@endo/captp'; import { JsonRpcServer } from '@metamask/json-rpc-engine/v2'; import { makeSQLKernelDatabase } from '@metamask/kernel-store/sqlite/wasm'; import { isJsonRpcMessage, stringify } from '@metamask/kernel-utils'; @@ -17,7 +18,8 @@ import { import type { CapTPMessage } from '../background-captp.ts'; import { receiveInternalConnections } from '../internal-comms/internal-connections.ts'; import { PlatformServicesClient } from '../PlatformServicesClient.ts'; -import { makeKernelCapTP } from './captp/index.ts'; +import type { KernelHostRoot } from './kernel-host-vat.ts'; +import { makeKernelHostSubclusterConfig } from './kernel-host-vat.ts'; import { makeLoggingMiddleware } from './middleware/logging.ts'; import { makePanelMessageMiddleware } from './middleware/panel-message.ts'; import { getRelaysFromCurrentLocation } from '../utils/relay-query-string.ts'; @@ -71,27 +73,50 @@ async function main(): Promise { const kernel = await kernelP; - const kernelCapTP = makeKernelCapTP({ - kernel, - send: (captpMessage: CapTPMessage) => { - const notification = makeCapTPNotification(captpMessage); - messageStream.write(notification).catch((error) => { - logger.error('Failed to send CapTP message:', error); - }); - }, + // Launch the kernel host subcluster to get a proper system vat for CapTP + let kernelHostRoot: KernelHostRoot | undefined; + const hostSubclusterConfig = makeKernelHostSubclusterConfig((root) => { + kernelHostRoot = root; }); + try { + await kernel.launchSystemSubcluster(hostSubclusterConfig); + logger.log('Launched kernel host subcluster'); + } catch (error) { + logger.error('Failed to launch kernel host subcluster:', error); + throw error; + } + + if (!kernelHostRoot) { + throw new Error('Kernel host root was not captured during launch'); + } + + // Create CapTP with the kernel host vat root as the bootstrap + // This gives background proper presences for dynamic subcluster roots + const sendCapTPMessage = (captpMessage: CapTPMessage): void => { + const notification = makeCapTPNotification(captpMessage); + messageStream.write(notification).catch((error) => { + logger.error('Failed to send CapTP message:', error); + }); + }; + + const { dispatch: dispatchCapTP, abort: abortCapTP } = makeCapTP( + 'kernel', + sendCapTPMessage, + kernelHostRoot, + ); + messageStream .drain((message) => { if (isCapTPNotification(message)) { const captpMessage = message.params[0]; - kernelCapTP.dispatch(captpMessage); + dispatchCapTP(captpMessage); } else { throw new Error(`Unexpected message: ${stringify(message)}`); } }) .catch((error) => { - kernelCapTP.abort(error); + abortCapTP(error); logger.error('Message stream error:', error); }); From 30714aecf53915927a01b53e4334d4756a1f6e33 Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Tue, 27 Jan 2026 22:00:57 -0800 Subject: [PATCH 09/41] refactor: Extract and export KernelFacet type from ocap-kernel - Add KernelFacet type derived from KernelFacetDependencies with launchSubcluster overridden to return KernelFacetLaunchResult - Export KernelFacet and KernelFacetLaunchResult from ocap-kernel index - Update kernel-host-vat.ts to import KernelFacet from ocap-kernel instead of declaring a local duplicate type Co-Authored-By: Claude --- .../src/kernel-worker/kernel-host-vat.ts | 22 +++------------- packages/ocap-kernel/src/index.ts | 1 + packages/ocap-kernel/src/kernel-facet.ts | 26 ++++++++++++++++++- 3 files changed, 29 insertions(+), 20 deletions(-) diff --git a/packages/kernel-browser-runtime/src/kernel-worker/kernel-host-vat.ts b/packages/kernel-browser-runtime/src/kernel-worker/kernel-host-vat.ts index 265db51ec..4ca817ffe 100644 --- a/packages/kernel-browser-runtime/src/kernel-worker/kernel-host-vat.ts +++ b/packages/kernel-browser-runtime/src/kernel-worker/kernel-host-vat.ts @@ -6,8 +6,9 @@ import type { ClusterConfig, KernelStatus, Subcluster, + KernelFacet, + KernelFacetLaunchResult, } from '@metamask/ocap-kernel'; -import type { SlotValue } from '@metamask/ocap-kernel'; /** * The kernel host vat's root object interface. @@ -29,9 +30,7 @@ export type KernelHostRoot = { * @param config - Configuration for the subcluster. * @returns The launch result with subcluster ID and root presence. */ - launchSubcluster: ( - config: ClusterConfig, - ) => Promise<{ subclusterId: string; root: SlotValue }>; + launchSubcluster: (config: ClusterConfig) => Promise; /** * Terminate a subcluster. @@ -71,21 +70,6 @@ export type KernelHostRoot = { getSubclusters: () => Subcluster[]; }; -/** - * The kernel facet interface as provided to system vat bootstrap. - * This is the vatpower passed to the kernel host vat. - */ -type KernelFacet = { - launchSubcluster: ( - config: ClusterConfig, - ) => Promise<{ subclusterId: string; root: SlotValue }>; - terminateSubcluster: (subclusterId: string) => Promise; - getStatus: () => Promise; - reloadSubcluster: (subclusterId: string) => Promise; - getSubcluster: (subclusterId: string) => Subcluster | undefined; - getSubclusters: () => Subcluster[]; -}; - /** * Create the configuration for launching the kernel host subcluster. * diff --git a/packages/ocap-kernel/src/index.ts b/packages/ocap-kernel/src/index.ts index 1cfd41223..689e64455 100644 --- a/packages/ocap-kernel/src/index.ts +++ b/packages/ocap-kernel/src/index.ts @@ -43,6 +43,7 @@ export { } from './types.ts'; export { kunser, kser, kslot, krefOf } from './liveslots/kernel-marshal.ts'; export type { SlotValue } from './liveslots/kernel-marshal.ts'; +export type { KernelFacet, KernelFacetLaunchResult } from './kernel-facet.ts'; export { makeKernelStore } from './store/index.ts'; export type { KernelStore } from './store/index.ts'; export { parseRef } from './store/utils/parse-ref.ts'; diff --git a/packages/ocap-kernel/src/kernel-facet.ts b/packages/ocap-kernel/src/kernel-facet.ts index 6b1e953bc..55bad2678 100644 --- a/packages/ocap-kernel/src/kernel-facet.ts +++ b/packages/ocap-kernel/src/kernel-facet.ts @@ -30,6 +30,30 @@ export type KernelFacetLaunchResult = { root: SlotValue; }; +/** + * The kernel facet interface. + * + * This is the interface provided as a vatpower to the bootstrap vat of a + * system subcluster. It enables privileged kernel operations. + * + * Derived from KernelFacetDependencies but with launchSubcluster overridden + * to return KernelFacetLaunchResult (root as SlotValue) instead of + * SubclusterLaunchResult (bootstrapRootKref as string). + */ +export type KernelFacet = Omit< + KernelFacetDependencies, + 'logger' | 'launchSubcluster' +> & { + /** + * Launch a dynamic subcluster. + * Returns root as a SlotValue (which becomes a presence when delivered). + * + * @param config - Configuration for the subcluster. + * @returns A promise for the launch result containing subclusterId and root presence. + */ + launchSubcluster: (config: ClusterConfig) => Promise; +}; + /** * Creates a kernel facet object that provides privileged kernel operations. * @@ -43,7 +67,7 @@ export type KernelFacetLaunchResult = { * @param deps - Dependencies for creating the kernel facet. * @returns The kernel facet object. */ -export function makeKernelFacet(deps: KernelFacetDependencies): object { +export function makeKernelFacet(deps: KernelFacetDependencies): KernelFacet { const { launchSubcluster, terminateSubcluster, From 04a3f87b8c3e5ff373bef7e3b734f0158fed92e5 Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Wed, 28 Jan 2026 09:58:08 -0800 Subject: [PATCH 10/41] refactor: Fix type errors in kernel-host-vat.test.ts --- .../src/kernel-worker/kernel-host-vat.test.ts | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/packages/kernel-browser-runtime/src/kernel-worker/kernel-host-vat.test.ts b/packages/kernel-browser-runtime/src/kernel-worker/kernel-host-vat.test.ts index 833d9b99f..d4ab415ec 100644 --- a/packages/kernel-browser-runtime/src/kernel-worker/kernel-host-vat.test.ts +++ b/packages/kernel-browser-runtime/src/kernel-worker/kernel-host-vat.test.ts @@ -23,16 +23,19 @@ describe('makeKernelHostSubclusterConfig', () => { expect(config.bootstrap).toBe('kernelHost'); expect(config.vats.kernelHost).toBeDefined(); - expect(config.vats.kernelHost.buildRootObject).toBeTypeOf('function'); + expect(config.vats?.kernelHost?.buildRootObject).toBeTypeOf('function'); }); it('invokes onRootCreated callback when buildRootObject is called', () => { const onRootCreated = vi.fn(); const config = makeKernelHostSubclusterConfig(onRootCreated); - const root = config.vats.kernelHost.buildRootObject({ - kernelFacet: mockKernelFacet, - }); + const root = config.vats?.kernelHost?.buildRootObject( + { + kernelFacet: mockKernelFacet, + }, + {}, + ); expect(onRootCreated).toHaveBeenCalledWith(root); }); @@ -43,9 +46,12 @@ describe('makeKernelHostSubclusterConfig', () => { beforeEach(() => { const onRootCreated = vi.fn(); const config = makeKernelHostSubclusterConfig(onRootCreated); - root = config.vats.kernelHost.buildRootObject({ - kernelFacet: mockKernelFacet, - }) as KernelHostRoot; + root = config.vats?.kernelHost?.buildRootObject( + { + kernelFacet: mockKernelFacet, + }, + {}, + ) as KernelHostRoot; }); it('creates root with expected methods', () => { From 9746e0b56831b87cbdd294db37d75b3d73ddbe46 Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Wed, 28 Jan 2026 11:11:05 -0800 Subject: [PATCH 11/41] refactor(ocap-kernel): Unify VatSyscall for regular and system vats - Consolidate VatSyscall and SystemVatSyscall into a single VatSyscall class that handles both regular and system vats via constructor parameters - Add isActive callback and vatLabel parameters to VatSyscall - Fix CrankResults.terminate.vatId type to accept EndpointId - Document ephemeral KVStore and implement getKeys/getPrefixedKeys - Simplify buildVatNamespace vatPowers merge with clear documentation - Make logger required at type level in SystemSubclusterManagerOptions - Delete EndpointSyscall.ts and SystemVatSyscall.ts (no longer needed) Co-Authored-By: Claude --- packages/ocap-kernel/src/types.ts | 2 +- .../src/vats/SystemSubclusterManager.test.ts | 19 -- .../src/vats/SystemSubclusterManager.ts | 17 +- .../ocap-kernel/src/vats/SystemVatHandle.ts | 21 +- .../src/vats/SystemVatSupervisor.ts | 54 ++- .../src/vats/SystemVatSyscall.test.ts | 323 ------------------ .../ocap-kernel/src/vats/SystemVatSyscall.ts | 287 ---------------- packages/ocap-kernel/src/vats/VatHandle.ts | 1 + .../ocap-kernel/src/vats/VatSyscall.test.ts | 67 ++-- packages/ocap-kernel/src/vats/VatSyscall.ts | 54 ++- 10 files changed, 142 insertions(+), 703 deletions(-) delete mode 100644 packages/ocap-kernel/src/vats/SystemVatSyscall.test.ts delete mode 100644 packages/ocap-kernel/src/vats/SystemVatSyscall.ts diff --git a/packages/ocap-kernel/src/types.ts b/packages/ocap-kernel/src/types.ts index 00b7a92f5..910b605f9 100644 --- a/packages/ocap-kernel/src/types.ts +++ b/packages/ocap-kernel/src/types.ts @@ -563,7 +563,7 @@ export const isGCAction = (value: unknown): value is GCAction => export type CrankResults = { didDelivery?: EndpointId; // the endpoint to which we made a delivery abort?: boolean; // changes should be discarded, not committed - terminate?: { vatId: VatId; reject: boolean; info: SwingSetCapData }; + terminate?: { vatId: EndpointId; reject: boolean; info: SwingSetCapData }; }; export type VatDeliveryResult = [VatCheckpoint, string | null]; diff --git a/packages/ocap-kernel/src/vats/SystemSubclusterManager.test.ts b/packages/ocap-kernel/src/vats/SystemSubclusterManager.test.ts index 39f6f6bfd..fdf4dd671 100644 --- a/packages/ocap-kernel/src/vats/SystemSubclusterManager.test.ts +++ b/packages/ocap-kernel/src/vats/SystemSubclusterManager.test.ts @@ -337,23 +337,4 @@ describe('SystemSubclusterManager', () => { expect(isActive).toBe(true); }); }); - - describe('without logger', () => { - it('throws when trying to launch without logger', async () => { - const managerWithoutLogger = new SystemSubclusterManager({ - kernelStore, - kernelQueue, - kernelFacetDeps, - }); - - const config: SystemSubclusterConfig = { - bootstrap: 'testVat', - vats: { testVat: { buildRootObject } }, - }; - - await expect( - managerWithoutLogger.launchSystemSubcluster(config), - ).rejects.toThrow('Logger required for system vat supervisor'); - }); - }); }); diff --git a/packages/ocap-kernel/src/vats/SystemSubclusterManager.ts b/packages/ocap-kernel/src/vats/SystemSubclusterManager.ts index 90d41a3bd..20a3f875a 100644 --- a/packages/ocap-kernel/src/vats/SystemSubclusterManager.ts +++ b/packages/ocap-kernel/src/vats/SystemSubclusterManager.ts @@ -31,7 +31,8 @@ type SystemSubclusterManagerOptions = { kernelStore: KernelStore; kernelQueue: KernelQueue; kernelFacetDeps: KernelFacetDependencies; - logger?: Logger; + /** Logger is required for system subclusters since supervisors need it for liveslots. */ + logger: Logger; }; type SystemSubclusterRecord = { @@ -62,7 +63,7 @@ export class SystemSubclusterManager { readonly #kernelFacetDeps: KernelFacetDependencies; /** Logger for outputting messages to the console */ - readonly #logger: Logger | undefined; + readonly #logger: Logger; /** Counter for allocating system vat IDs */ #nextSystemVatId: number = 0; @@ -172,17 +173,11 @@ export class SystemSubclusterManager { kernelStore: this.#kernelStore, kernelQueue: this.#kernelQueue, deliver, - logger: this.#logger?.subLogger({ tags: [systemVatId] }), + logger: this.#logger.subLogger({ tags: [systemVatId] }), }); handles.set(systemVatId, handle); // Create the supervisor (which runs liveslots) - const supervisorLogger = this.#logger?.subLogger({ - tags: [systemVatId, 'supervisor'], - }); - if (!supervisorLogger) { - throw new Error('Logger required for system vat supervisor'); - } const supervisor = new SystemVatSupervisor({ id: systemVatId, buildRootObject: vatConfig.buildRootObject, @@ -190,7 +185,7 @@ export class SystemSubclusterManager { parameters: vatConfig.parameters, executeSyscall: (vso) => handle.getSyscallHandler()(vso) ?? harden(['ok', null]), - logger: supervisorLogger, + logger: this.#logger.subLogger({ tags: [systemVatId, 'supervisor'] }), }); supervisors.set(systemVatId, supervisor); @@ -248,7 +243,7 @@ export class SystemSubclusterManager { if (serviceKref) { services[serviceName] = kslot(serviceKref); } else { - this.#logger?.warn(`Kernel service '${serviceName}' not found`); + this.#logger.warn(`Kernel service '${serviceName}' not found`); } } } diff --git a/packages/ocap-kernel/src/vats/SystemVatHandle.ts b/packages/ocap-kernel/src/vats/SystemVatHandle.ts index 0658ff153..9eb80329f 100644 --- a/packages/ocap-kernel/src/vats/SystemVatHandle.ts +++ b/packages/ocap-kernel/src/vats/SystemVatHandle.ts @@ -16,7 +16,7 @@ import type { CrankResults, EndpointHandle, } from '../types.ts'; -import { SystemVatSyscall } from './SystemVatSyscall.ts'; +import { VatSyscall } from './VatSyscall.ts'; /** * Delivery callback type - called by kernel to deliver messages to the system vat. @@ -58,7 +58,7 @@ export class SystemVatHandle implements EndpointHandle { readonly #kernelQueue: KernelQueue; /** The system vat's syscall handler */ - readonly #systemVatSyscall: SystemVatSyscall; + readonly #vatSyscall: VatSyscall; /** Callback to deliver messages to the system vat supervisor */ readonly #deliver: SystemVatDeliverFn; @@ -88,11 +88,12 @@ export class SystemVatHandle implements EndpointHandle { this.#kernelStore = kernelStore; this.#kernelQueue = kernelQueue; this.#deliver = deliver; - this.#systemVatSyscall = new SystemVatSyscall({ - systemVatId, + this.#vatSyscall = new VatSyscall({ + vatId: systemVatId, kernelQueue, kernelStore, isActive: () => this.#isActive, + vatLabel: 'system vat', logger: this.#logger?.subLogger({ tags: ['syscall'] }), }); @@ -106,7 +107,7 @@ export class SystemVatHandle implements EndpointHandle { */ getSyscallHandler(): (syscall: VatSyscallObject) => void { return (syscall: VatSyscallObject) => { - this.#systemVatSyscall.handleSyscall(syscall); + this.#vatSyscall.handleSyscall(syscall); }; } @@ -219,21 +220,21 @@ export class SystemVatHandle implements EndpointHandle { // These conditionals express a priority order: the consequences of an // illegal syscall take precedence over a vat requesting termination, etc. - if (this.#systemVatSyscall.illegalSyscall) { + if (this.#vatSyscall.illegalSyscall) { results.abort = true; - const { info } = this.#systemVatSyscall.illegalSyscall; + const { info } = this.#vatSyscall.illegalSyscall; results.terminate = { vatId: this.systemVatId, reject: true, info }; } else if (deliveryError) { results.abort = true; const info = makeError(deliveryError); results.terminate = { vatId: this.systemVatId, reject: true, info }; - } else if (this.#systemVatSyscall.vatRequestedTermination) { - if (this.#systemVatSyscall.vatRequestedTermination.reject) { + } else if (this.#vatSyscall.vatRequestedTermination) { + if (this.#vatSyscall.vatRequestedTermination.reject) { results.abort = true; } results.terminate = { vatId: this.systemVatId, - ...this.#systemVatSyscall.vatRequestedTermination, + ...this.#vatSyscall.vatRequestedTermination, }; } diff --git a/packages/ocap-kernel/src/vats/SystemVatSupervisor.ts b/packages/ocap-kernel/src/vats/SystemVatSupervisor.ts index 01782afef..5a883d1a2 100644 --- a/packages/ocap-kernel/src/vats/SystemVatSupervisor.ts +++ b/packages/ocap-kernel/src/vats/SystemVatSupervisor.ts @@ -46,8 +46,16 @@ type SystemVatSupervisorProps = { /** * A non-persistent KV store for system vats. - * System vats don't participate in kernel persistence machinery, - * so their vatstore is ephemeral (Map-based). + * + * System vats don't participate in kernel persistence machinery, so their + * vatstore is ephemeral (Map-based). This store is still required because + * liveslots uses the vatstore internally for: + * - Virtual object tracking and lifecycle management + * - Promise resolution bookkeeping + * - Reference counting and garbage collection coordination + * + * The data in this store is lost when the system vat terminates, which is + * acceptable since system vats are not designed to persist across restarts. * * @returns An ephemeral KVStore implementation. */ @@ -83,11 +91,21 @@ function makeEphemeralVatKVStore(): KVStore { } return keys[index + 1]; }, - getKeys(_start: string, _end: string): Iterable { - throw new Error('getKeys not supported for ephemeral store'); + *getKeys(start: string, end: string): Iterable { + const keys = [...data.keys()].sort(); + for (const key of keys) { + if (key >= start && key < end) { + yield key; + } + } }, - getPrefixedKeys(_prefix: string): Iterable { - throw new Error('getPrefixedKeys not supported for ephemeral store'); + *getPrefixedKeys(prefix: string): Iterable { + const keys = [...data.keys()].sort(); + for (const key of keys) { + if (key.startsWith(prefix)) { + yield key; + } + } }, }); } @@ -173,23 +191,25 @@ export class SystemVatSupervisor { }); // For system vats, buildVatNamespace returns the buildRootObject directly - // without loading a bundle via importBundle + // without loading a bundle via importBundle. + // + // Liveslots invokes buildVatNamespace, then calls the returned buildRootObject. + // VatPowers are merged in three stages: + // 1. External vatPowers (e.g., kernelFacet for bootstrap vat) + // 2. lsEndowments from liveslots (D, etc.) provided to buildVatNamespace + // 3. innerVatPowers from liveslots (exitVat, etc.) provided to buildRootObject + // Later sources override earlier ones. const buildVatNamespace = async ( lsEndowments: Record, _inescapableGlobalProperties: object, ): Promise> => { - // Provide liveslots endowments as vatPowers to the buildRootObject - const combinedVatPowers = { - ...vatPowers, - ...lsEndowments, - }; - - // Return a namespace object with buildRootObject that wraps the provided one - // to inject the combined vatPowers return { buildRootObject: (innerVatPowers: Record) => { - // Merge the inner vatPowers (from liveslots) with our combined powers - const finalVatPowers = { ...combinedVatPowers, ...innerVatPowers }; + const finalVatPowers = { + ...vatPowers, + ...lsEndowments, + ...innerVatPowers, + }; return buildRootObject(finalVatPowers, parameters); }, }; diff --git a/packages/ocap-kernel/src/vats/SystemVatSyscall.test.ts b/packages/ocap-kernel/src/vats/SystemVatSyscall.test.ts deleted file mode 100644 index 910b39e53..000000000 --- a/packages/ocap-kernel/src/vats/SystemVatSyscall.test.ts +++ /dev/null @@ -1,323 +0,0 @@ -import type { - Message, - VatOneResolution, - VatSyscallObject, -} from '@agoric/swingset-liveslots'; -import type { Logger } from '@metamask/logger'; -import type { MockInstance } from 'vitest'; -import { describe, it, expect, vi, beforeEach } from 'vitest'; - -import type { KernelQueue } from '../KernelQueue.ts'; -import type { KernelStore } from '../store/index.ts'; -import type { SystemVatId } from '../types.ts'; -import { SystemVatSyscall } from './SystemVatSyscall.ts'; - -describe('SystemVatSyscall', () => { - let kernelQueue: KernelQueue; - let kernelStore: KernelStore; - let logger: Logger; - let isActive: () => boolean; - let systemVatSyscall: SystemVatSyscall; - const systemVatId: SystemVatId = 'sv0'; - - beforeEach(() => { - kernelQueue = { - enqueueSend: vi.fn(), - resolvePromises: vi.fn(), - enqueueNotify: vi.fn(), - } as unknown as KernelQueue; - kernelStore = { - translateSyscallVtoK: vi.fn((_: string, vso: VatSyscallObject) => vso), - getKernelPromise: vi.fn(), - addPromiseSubscriber: vi.fn(), - clearReachableFlag: vi.fn(), - getReachableFlag: vi.fn(), - forgetKref: vi.fn(), - } as unknown as KernelStore; - logger = { - debug: vi.fn(), - error: vi.fn(), - log: vi.fn(), - warn: vi.fn(), - subLogger: vi.fn(() => logger), - } as unknown as Logger; - isActive = vi.fn(() => true); - systemVatSyscall = new SystemVatSyscall({ - systemVatId, - kernelQueue, - kernelStore, - isActive, - logger, - }); - }); - - it('enqueues run for send syscall', () => { - const target = 'o+1'; - const message = {} as unknown as Message; - const vso = ['send', target, message] as unknown as VatSyscallObject; - systemVatSyscall.handleSyscall(vso); - expect(kernelQueue.enqueueSend).toHaveBeenCalledWith(target, message); - }); - - it('calls resolvePromises for resolve syscall', () => { - const resolution = ['kp1', false, {}] as unknown as VatOneResolution; - const vso = ['resolve', [resolution]] as unknown as VatSyscallObject; - systemVatSyscall.handleSyscall(vso); - expect(kernelQueue.resolvePromises).toHaveBeenCalledWith(systemVatId, [ - resolution, - ]); - }); - - describe('subscribe syscall', () => { - it('subscribes to unresolved promise', () => { - ( - kernelStore.getKernelPromise as unknown as MockInstance - ).mockReturnValueOnce({ - state: 'unresolved', - }); - const vso = ['subscribe', 'kp1'] as unknown as VatSyscallObject; - systemVatSyscall.handleSyscall(vso); - expect(kernelStore.addPromiseSubscriber).toHaveBeenCalledWith( - systemVatId, - 'kp1', - ); - }); - - it('notifies for resolved promise', () => { - ( - kernelStore.getKernelPromise as unknown as MockInstance - ).mockReturnValueOnce({ - state: 'fulfilled', - }); - const vso = ['subscribe', 'kp1'] as unknown as VatSyscallObject; - systemVatSyscall.handleSyscall(vso); - expect(kernelQueue.enqueueNotify).toHaveBeenCalledWith( - systemVatId, - 'kp1', - ); - }); - }); - - describe('dropImports syscall', () => { - it('clears reachable flags for valid imports', () => { - const vso = [ - 'dropImports', - ['o-1', 'o-2'], - ] as unknown as VatSyscallObject; - systemVatSyscall.handleSyscall(vso); - expect(kernelStore.clearReachableFlag).toHaveBeenCalledWith( - systemVatId, - 'o-1', - ); - expect(kernelStore.clearReachableFlag).toHaveBeenCalledWith( - systemVatId, - 'o-2', - ); - }); - - it.each([ - [ - 'o+1', - `system vat ${systemVatId} issued invalid syscall dropImports for o+1`, - ], - [ - 'p-1', - `system vat ${systemVatId} issued invalid syscall dropImports for p-1`, - ], - ])('returns error for invalid ref %s', (ref, errMsg) => { - ( - kernelStore.translateSyscallVtoK as unknown as MockInstance - ).mockImplementationOnce(() => { - throw new Error(errMsg); - }); - const vso = ['dropImports', [ref]] as unknown as VatSyscallObject; - const result = systemVatSyscall.handleSyscall(vso); - expect(result).toStrictEqual(['error', errMsg]); - }); - }); - - describe('retireImports syscall', () => { - it('forgets kref when not reachable', () => { - ( - kernelStore.getReachableFlag as unknown as MockInstance - ).mockReturnValueOnce(false); - const vso = ['retireImports', ['o-1']] as unknown as VatSyscallObject; - systemVatSyscall.handleSyscall(vso); - expect(kernelStore.forgetKref).toHaveBeenCalledWith(systemVatId, 'o-1'); - }); - - it('returns error if still reachable', () => { - ( - kernelStore.translateSyscallVtoK as unknown as MockInstance - ).mockImplementationOnce(() => { - ( - kernelStore.getReachableFlag as unknown as MockInstance - ).mockReturnValueOnce(true); - throw new Error('syscall.retireImports but o-1 is still reachable'); - }); - const vso = ['retireImports', ['o-1']] as unknown as VatSyscallObject; - const result = systemVatSyscall.handleSyscall(vso); - expect(result).toStrictEqual([ - 'error', - 'syscall.retireImports but o-1 is still reachable', - ]); - }); - }); - - describe('exportCleanup syscalls', () => { - it('retires exports when not reachable', () => { - ( - kernelStore.getReachableFlag as unknown as MockInstance - ).mockReturnValueOnce(false); - const vso = ['retireExports', ['o+1']] as unknown as VatSyscallObject; - systemVatSyscall.handleSyscall(vso); - expect(kernelStore.forgetKref).toHaveBeenCalledWith(systemVatId, 'o+1'); - expect(logger.debug).toHaveBeenCalledWith( - 'retireExports: deleted object o+1', - ); - }); - - it('returns error for reachable exports', () => { - ( - kernelStore.translateSyscallVtoK as unknown as MockInstance - ).mockImplementationOnce(() => { - ( - kernelStore.getReachableFlag as unknown as MockInstance - ).mockReturnValueOnce(true); - throw new Error('syscall.retireExports but o+1 is still reachable'); - }); - const vso = ['retireExports', ['o+1']] as unknown as VatSyscallObject; - const result = systemVatSyscall.handleSyscall(vso); - expect(result).toStrictEqual([ - 'error', - 'syscall.retireExports but o+1 is still reachable', - ]); - }); - - it('abandons exports without reachability check', () => { - const vso = ['abandonExports', ['o+1']] as unknown as VatSyscallObject; - systemVatSyscall.handleSyscall(vso); - expect(kernelStore.forgetKref).toHaveBeenCalledWith(systemVatId, 'o+1'); - expect(logger.debug).toHaveBeenCalledWith( - 'abandonExports: deleted object o+1', - ); - }); - - it('returns error for invalid abandonExports refs', () => { - ( - kernelStore.translateSyscallVtoK as unknown as MockInstance - ).mockImplementationOnce(() => { - throw new Error( - `system vat ${systemVatId} issued invalid syscall abandonExports for o-1`, - ); - }); - const vso = ['abandonExports', ['o-1']] as unknown as VatSyscallObject; - const result = systemVatSyscall.handleSyscall(vso); - expect(result).toStrictEqual([ - 'error', - `system vat ${systemVatId} issued invalid syscall abandonExports for o-1`, - ]); - }); - }); - - describe('exit syscall', () => { - it('records vat termination request', () => { - const vso = [ - 'exit', - true, - { message: 'error' }, - ] as unknown as VatSyscallObject; - systemVatSyscall.handleSyscall(vso); - expect(systemVatSyscall.vatRequestedTermination).toStrictEqual({ - reject: true, - info: { message: 'error' }, - }); - }); - }); - - describe('error handling', () => { - it('handles system vat not active error', () => { - (isActive as unknown as MockInstance).mockReturnValueOnce(false); - const vso = ['send', 'o+1', {}] as unknown as VatSyscallObject; - const result = systemVatSyscall.handleSyscall(vso); - - expect(result).toStrictEqual(['error', 'system vat not found']); - expect(systemVatSyscall.illegalSyscall).toBeDefined(); - }); - - it('handles general syscall errors', () => { - const error = new Error('test error'); - ( - kernelStore.translateSyscallVtoK as unknown as MockInstance - ).mockImplementationOnce(() => { - throw error; - }); - - const vso = ['send', 'o+1', {}] as unknown as VatSyscallObject; - const result = systemVatSyscall.handleSyscall(vso); - - expect(result).toStrictEqual(['error', 'test error']); - expect(logger.error).toHaveBeenCalledWith( - `Fatal syscall error in system vat ${systemVatId}`, - error, - ); - }); - }); - - describe('unsupported syscalls', () => { - it.each([ - ['vatstoreGet'], - ['vatstoreGetNextKey'], - ['vatstoreSet'], - ['vatstoreDelete'], - ['callNow'], - ])('%s warns about unsupported syscall', (op) => { - const spy = vi.spyOn(logger, 'warn'); - const vso = [op, []] as unknown as VatSyscallObject; - systemVatSyscall.handleSyscall(vso); - expect(spy).toHaveBeenCalledWith( - expect.stringContaining('unsupported syscall'), - vso, - ); - spy.mockRestore(); - }); - }); - - describe('unknown syscalls', () => { - it('warns about unknown syscall', () => { - const spy = vi.spyOn(logger, 'warn'); - const vso = ['unknownOp', []] as unknown as VatSyscallObject; - systemVatSyscall.handleSyscall(vso); - expect(spy).toHaveBeenCalledWith( - expect.stringContaining('unknown syscall'), - vso, - ); - spy.mockRestore(); - }); - }); - - describe('logging', () => { - it('is disabled if logger is undefined', () => { - const logSpy = vi.spyOn(console, 'log'); - const syscallWithoutLogger = new SystemVatSyscall({ - systemVatId, - kernelQueue, - kernelStore, - isActive, - }); - syscallWithoutLogger.handleSyscall([ - 'send', - 'o+1', - {}, - ] as VatSyscallObject); - expect(logSpy).not.toHaveBeenCalled(); - expect(logger.log).not.toHaveBeenCalled(); - }); - }); - - describe('systemVatId property', () => { - it('exposes the system vat ID', () => { - expect(systemVatSyscall.systemVatId).toBe(systemVatId); - }); - }); -}); diff --git a/packages/ocap-kernel/src/vats/SystemVatSyscall.ts b/packages/ocap-kernel/src/vats/SystemVatSyscall.ts deleted file mode 100644 index 712d6b8e6..000000000 --- a/packages/ocap-kernel/src/vats/SystemVatSyscall.ts +++ /dev/null @@ -1,287 +0,0 @@ -import type { - SwingSetCapData, - VatOneResolution, - VatSyscallObject, - VatSyscallResult, -} from '@agoric/swingset-liveslots'; -import { Logger } from '@metamask/logger'; - -import { - performDropImports, - performRetireImports, - performExportCleanup, -} from '../garbage-collection/gc-handlers.ts'; -import type { KernelQueue } from '../KernelQueue.ts'; -import { makeError } from '../liveslots/kernel-marshal.ts'; -import type { KernelStore } from '../store/index.ts'; -import { coerceMessage } from '../types.ts'; -import type { Message, SystemVatId, KRef } from '../types.ts'; - -type SystemVatSyscallProps = { - systemVatId: SystemVatId; - kernelQueue: KernelQueue; - kernelStore: KernelStore; - isActive: () => boolean; - logger?: Logger | undefined; -}; - -/** - * Handles syscalls from a system vat. - * - * Similar to VatSyscall but for system vats. System vats run without - * compartment isolation directly in the host process and don't participate - * in kernel persistence machinery. - */ -export class SystemVatSyscall { - /** The ID of the system vat */ - readonly systemVatId: SystemVatId; - - /** The kernel's run queue */ - readonly #kernelQueue: KernelQueue; - - /** The kernel's store */ - readonly #kernelStore: KernelStore; - - /** Logger for outputting messages (such as errors) to the console */ - readonly #logger: Logger | undefined; - - /** Function to check if the system vat is still active */ - readonly #isActive: () => boolean; - - /** The illegal syscall that was received */ - illegalSyscall: { vatId: SystemVatId; info: SwingSetCapData } | undefined; - - /** The error when delivery failed */ - deliveryError: string | undefined; - - /** The termination request that was received from the vat with syscall.exit() */ - vatRequestedTermination: - | { reject: boolean; info: SwingSetCapData } - | undefined; - - /** - * Construct a new SystemVatSyscall instance. - * - * @param props - The properties for the SystemVatSyscall. - * @param props.systemVatId - The ID of the system vat. - * @param props.kernelQueue - The kernel's run queue. - * @param props.kernelStore - The kernel's store. - * @param props.isActive - Function to check if the system vat is still active. - * @param props.logger - The logger for the SystemVatSyscall. - */ - constructor({ - systemVatId, - kernelQueue, - kernelStore, - isActive, - logger, - }: SystemVatSyscallProps) { - this.systemVatId = systemVatId; - this.#kernelQueue = kernelQueue; - this.#kernelStore = kernelStore; - this.#isActive = isActive; - this.#logger = logger; - } - - /** - * Handle a 'send' syscall from the system vat. - * - * @param target - The target of the message send. - * @param message - The message that was sent. - */ - #handleSyscallSend(target: KRef, message: Message): void { - this.#kernelQueue.enqueueSend(target, message); - } - - /** - * Handle a 'resolve' syscall from the system vat. - * - * @param resolutions - One or more promise resolutions. - */ - #handleSyscallResolve(resolutions: VatOneResolution[]): void { - this.#kernelQueue.resolvePromises(this.systemVatId, resolutions); - } - - /** - * Handle a 'subscribe' syscall from the system vat. - * - * @param kpid - The KRef of the promise being subscribed to. - */ - #handleSyscallSubscribe(kpid: KRef): void { - const kp = this.#kernelStore.getKernelPromise(kpid); - if (kp.state === 'unresolved') { - this.#kernelStore.addPromiseSubscriber(this.systemVatId, kpid); - } else { - this.#kernelQueue.enqueueNotify(this.systemVatId, kpid); - } - } - - /** - * Handle a 'dropImports' syscall from the system vat. - * - * @param krefs - The KRefs of the imports to be dropped. - */ - #handleSyscallDropImports(krefs: KRef[]): void { - performDropImports(krefs, this.systemVatId, this.#kernelStore); - } - - /** - * Handle a 'retireImports' syscall from the system vat. - * - * @param krefs - The KRefs of the imports to be retired. - */ - #handleSyscallRetireImports(krefs: KRef[]): void { - performRetireImports(krefs, this.systemVatId, this.#kernelStore); - } - - /** - * Handle retiring or abandoning exports syscall from the system vat. - * - * @param krefs - The KRefs of the exports to be retired/abandoned. - * @param checkReachable - If true, verify the object is not reachable - * (retire). If false, ignore reachability (abandon). - */ - #handleSyscallExportCleanup(krefs: KRef[], checkReachable: boolean): void { - performExportCleanup( - krefs, - checkReachable, - this.systemVatId, - this.#kernelStore, - ); - - const action = checkReachable ? 'retire' : 'abandon'; - for (const kref of krefs) { - this.#logger?.debug(`${action}Exports: deleted object ${kref}`); - } - } - - /** - * Handle a syscall from the system vat. - * - * @param vso - The syscall that was received. - * @returns The result of the syscall. - */ - handleSyscall(vso: VatSyscallObject): VatSyscallResult { - try { - this.illegalSyscall = undefined; - this.vatRequestedTermination = undefined; - - // Check if the system vat is still active - if (!this.#isActive()) { - this.#recordVatFatalSyscall('system vat not found'); - return harden(['error', 'system vat not found']); - } - - const kso: VatSyscallObject = this.#kernelStore.translateSyscallVtoK( - this.systemVatId, - vso, - ); - const [op] = kso; - const { systemVatId } = this; - switch (op) { - case 'send': { - const [, target, message] = kso; - this.#logger?.log( - `@@@@ ${systemVatId} syscall send ${target}<-${JSON.stringify(message)}`, - ); - this.#handleSyscallSend(target, coerceMessage(message)); - break; - } - case 'subscribe': { - const [, promise] = kso; - this.#logger?.log(`@@@@ ${systemVatId} syscall subscribe ${promise}`); - this.#handleSyscallSubscribe(promise); - break; - } - case 'resolve': { - const [, resolutions] = kso; - this.#logger?.log( - `@@@@ ${systemVatId} syscall resolve ${JSON.stringify(resolutions)}`, - ); - this.#handleSyscallResolve(resolutions as VatOneResolution[]); - break; - } - case 'exit': { - const [, isFailure, info] = kso; - this.#logger?.log( - `@@@@ ${systemVatId} syscall exit fail=${isFailure} ${JSON.stringify(info)}`, - ); - this.vatRequestedTermination = { reject: isFailure, info }; - break; - } - case 'dropImports': { - const [, refs] = kso; - this.#logger?.log( - `@@@@ ${systemVatId} syscall dropImports ${JSON.stringify(refs)}`, - ); - this.#handleSyscallDropImports(refs); - break; - } - case 'retireImports': { - const [, refs] = kso; - this.#logger?.log( - `@@@@ ${systemVatId} syscall retireImports ${JSON.stringify(refs)}`, - ); - this.#handleSyscallRetireImports(refs); - break; - } - case 'retireExports': { - const [, refs] = kso; - this.#logger?.log( - `@@@@ ${systemVatId} syscall retireExports ${JSON.stringify(refs)}`, - ); - this.#handleSyscallExportCleanup(refs, true); - break; - } - case 'abandonExports': { - const [, refs] = kso; - this.#logger?.log( - `@@@@ ${systemVatId} syscall abandonExports ${JSON.stringify(refs)}`, - ); - this.#handleSyscallExportCleanup(refs, false); - break; - } - case 'callNow': - case 'vatstoreGet': - case 'vatstoreGetNextKey': - case 'vatstoreSet': - case 'vatstoreDelete': { - // System vats don't support vatstore operations (they're non-durable) - this.#logger?.warn( - `system vat ${systemVatId} issued unsupported syscall ${op}`, - vso, - ); - break; - } - default: - // Compile-time exhaustiveness check - this.#logger?.warn( - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - `system vat ${systemVatId} issued unknown syscall ${op}`, - vso, - ); - break; - } - return harden(['ok', null]); - } catch (error) { - this.#logger?.error( - `Fatal syscall error in system vat ${this.systemVatId}`, - error, - ); - this.#recordVatFatalSyscall('syscall translation error: prepare to die'); - return harden([ - 'error', - error instanceof Error ? error.message : String(error), - ]); - } - } - - /** - * Log a fatal syscall error and set the illegalSyscall property. - * - * @param error - The error message to log. - */ - #recordVatFatalSyscall(error: string): void { - this.illegalSyscall = { vatId: this.systemVatId, info: makeError(error) }; - } -} diff --git a/packages/ocap-kernel/src/vats/VatHandle.ts b/packages/ocap-kernel/src/vats/VatHandle.ts index 2b3c97635..a95e03a3a 100644 --- a/packages/ocap-kernel/src/vats/VatHandle.ts +++ b/packages/ocap-kernel/src/vats/VatHandle.ts @@ -107,6 +107,7 @@ export class VatHandle implements EndpointHandle { vatId, kernelQueue, kernelStore, + isActive: () => kernelStore.isVatActive(vatId), logger: this.#logger?.subLogger({ tags: ['syscall'] }), }); diff --git a/packages/ocap-kernel/src/vats/VatSyscall.test.ts b/packages/ocap-kernel/src/vats/VatSyscall.test.ts index 06f0fb02c..706ae0680 100644 --- a/packages/ocap-kernel/src/vats/VatSyscall.test.ts +++ b/packages/ocap-kernel/src/vats/VatSyscall.test.ts @@ -15,6 +15,7 @@ describe('VatSyscall', () => { let kernelQueue: KernelQueue; let kernelStore: KernelStore; let logger: Logger; + let isActive: ReturnType>; let vatSys: VatSyscall; beforeEach(() => { @@ -31,7 +32,6 @@ describe('VatSyscall', () => { getReachableFlag: vi.fn(), forgetKref: vi.fn(), getVatConfig: vi.fn(() => ({})), - isVatActive: vi.fn(() => true), } as unknown as KernelStore; logger = { debug: vi.fn(), @@ -40,10 +40,17 @@ describe('VatSyscall', () => { warn: vi.fn(), subLogger: vi.fn(() => logger), } as unknown as Logger; - vatSys = new VatSyscall({ vatId: 'v1', kernelQueue, kernelStore, logger }); + isActive = vi.fn(() => true); + vatSys = new VatSyscall({ + vatId: 'v1', + kernelQueue, + kernelStore, + isActive, + logger, + }); }); - it('enqueues run for send syscall', async () => { + it('enqueues run for send syscall', () => { const target = 'o+1'; const message = {} as unknown as Message; const vso = ['send', target, message] as unknown as VatSyscallObject; @@ -51,7 +58,7 @@ describe('VatSyscall', () => { expect(kernelQueue.enqueueSend).toHaveBeenCalledWith(target, message); }); - it('calls resolvePromises for resolve syscall', async () => { + it('calls resolvePromises for resolve syscall', () => { const resolution = ['kp1', false, {}] as unknown as VatOneResolution; const vso = ['resolve', [resolution]] as unknown as VatSyscallObject; vatSys.handleSyscall(vso); @@ -61,7 +68,7 @@ describe('VatSyscall', () => { }); describe('subscribe syscall', () => { - it('subscribes to unresolved promise', async () => { + it('subscribes to unresolved promise', () => { ( kernelStore.getKernelPromise as unknown as MockInstance ).mockReturnValueOnce({ @@ -75,7 +82,7 @@ describe('VatSyscall', () => { ); }); - it('notifies for resolved promise', async () => { + it('notifies for resolved promise', () => { ( kernelStore.getKernelPromise as unknown as MockInstance ).mockReturnValueOnce({ @@ -88,7 +95,7 @@ describe('VatSyscall', () => { }); describe('dropImports syscall', () => { - it('clears reachable flags for valid imports', async () => { + it('clears reachable flags for valid imports', () => { const vso = [ 'dropImports', ['o-1', 'o-2'], @@ -101,7 +108,7 @@ describe('VatSyscall', () => { it.each([ ['o+1', 'vat v1 issued invalid syscall dropImports for o+1'], ['p-1', 'vat v1 issued invalid syscall dropImports for p-1'], - ])('returns error for invalid ref %s', async (ref, errMsg) => { + ])('returns error for invalid ref %s', (ref, errMsg) => { ( kernelStore.translateSyscallVtoK as unknown as MockInstance ).mockImplementationOnce(() => { @@ -114,7 +121,7 @@ describe('VatSyscall', () => { }); describe('retireImports syscall', () => { - it('forgets kref when not reachable', async () => { + it('forgets kref when not reachable', () => { ( kernelStore.getReachableFlag as unknown as MockInstance ).mockReturnValueOnce(false); @@ -123,7 +130,7 @@ describe('VatSyscall', () => { expect(kernelStore.forgetKref).toHaveBeenCalledWith('v1', 'o-1'); }); - it('returns error if still reachable', async () => { + it('returns error if still reachable', () => { ( kernelStore.translateSyscallVtoK as unknown as MockInstance ).mockImplementationOnce(() => { @@ -142,7 +149,7 @@ describe('VatSyscall', () => { }); describe('exportCleanup syscalls', () => { - it('retires exports when not reachable', async () => { + it('retires exports when not reachable', () => { ( kernelStore.getReachableFlag as unknown as MockInstance ).mockReturnValueOnce(false); @@ -154,7 +161,7 @@ describe('VatSyscall', () => { ); }); - it('returns error for reachable exports', async () => { + it('returns error for reachable exports', () => { ( kernelStore.translateSyscallVtoK as unknown as MockInstance ).mockImplementationOnce(() => { @@ -171,7 +178,7 @@ describe('VatSyscall', () => { ]); }); - it('abandons exports without reachability check', async () => { + it('abandons exports without reachability check', () => { const vso = ['abandonExports', ['o+1']] as unknown as VatSyscallObject; vatSys.handleSyscall(vso); expect(kernelStore.forgetKref).toHaveBeenCalledWith('v1', 'o+1'); @@ -180,7 +187,7 @@ describe('VatSyscall', () => { ); }); - it('returns error for invalid abandonExports refs', async () => { + it('returns error for invalid abandonExports refs', () => { ( kernelStore.translateSyscallVtoK as unknown as MockInstance ).mockImplementationOnce(() => { @@ -196,7 +203,7 @@ describe('VatSyscall', () => { }); describe('exit syscall', () => { - it('records vat termination request', async () => { + it('records vat termination request', () => { const vso = [ 'exit', true, @@ -211,10 +218,8 @@ describe('VatSyscall', () => { }); describe('error handling', () => { - it('handles vat not found error', async () => { - (kernelStore.isVatActive as unknown as MockInstance).mockReturnValueOnce( - false, - ); + it('handles vat not found error', () => { + isActive.mockReturnValueOnce(false); const vso = ['send', 'o+1', {}] as unknown as VatSyscallObject; const result = vatSys.handleSyscall(vso); @@ -222,7 +227,7 @@ describe('VatSyscall', () => { expect(vatSys.illegalSyscall).toBeDefined(); }); - it('handles general syscall errors', async () => { + it('handles general syscall errors', () => { const error = new Error('test error'); ( kernelStore.translateSyscallVtoK as unknown as MockInstance @@ -249,7 +254,7 @@ describe('VatSyscall', () => { ['vatstoreDelete', 'invalid syscall vatstoreDelete'], ['callNow', 'invalid syscall callNow'], ['unknownOp', 'unknown syscall unknownOp'], - ])('%s should warn', async (op, message) => { + ])('%s warns', (op, message) => { const spy = vi.spyOn(logger, 'warn'); const vso = [op, []] as unknown as VatSyscallObject; vatSys.handleSyscall(vso); @@ -259,16 +264,34 @@ describe('VatSyscall', () => { }); describe('logging', () => { - it('is disabled if logger is undefined', async () => { + it('is disabled if logger is undefined', () => { const logSpy = vi.spyOn(console, 'log'); const vatSyscall = new VatSyscall({ vatId: 'v1', kernelQueue, kernelStore, + isActive, }); vatSyscall.handleSyscall(['send', 'o+1', {}] as VatSyscallObject); expect(logSpy).not.toHaveBeenCalled(); expect(logger.log).not.toHaveBeenCalled(); }); }); + + describe('vatLabel', () => { + it('uses custom label in error messages', () => { + const systemVatSyscall = new VatSyscall({ + vatId: 'sv0', + kernelQueue, + kernelStore, + isActive: () => false, + vatLabel: 'system vat', + logger, + }); + const vso = ['send', 'o+1', {}] as unknown as VatSyscallObject; + const result = systemVatSyscall.handleSyscall(vso); + + expect(result).toStrictEqual(['error', 'system vat not found']); + }); + }); }); diff --git a/packages/ocap-kernel/src/vats/VatSyscall.ts b/packages/ocap-kernel/src/vats/VatSyscall.ts index e77cbc87d..2749d7899 100644 --- a/packages/ocap-kernel/src/vats/VatSyscall.ts +++ b/packages/ocap-kernel/src/vats/VatSyscall.ts @@ -15,24 +15,26 @@ import type { KernelQueue } from '../KernelQueue.ts'; import { makeError } from '../liveslots/kernel-marshal.ts'; import type { KernelStore } from '../store/index.ts'; import { coerceMessage } from '../types.ts'; -import type { Message, VatId, KRef } from '../types.ts'; +import type { Message, EndpointId, KRef } from '../types.ts'; type VatSyscallProps = { - vatId: VatId; + vatId: EndpointId; kernelQueue: KernelQueue; kernelStore: KernelStore; + isActive: () => boolean; + vatLabel?: string; logger?: Logger | undefined; }; /** - * A VatSyscall is a class that handles syscalls from a vat. + * Handles syscalls from a vat (regular or system). * * This class is responsible for handling syscalls from a vat, including * sending messages, resolving promises, and dropping imports. */ export class VatSyscall { /** The ID of the vat */ - readonly vatId: VatId; + readonly vatId: EndpointId; /** The kernel's run queue */ readonly #kernelQueue: KernelQueue; @@ -40,11 +42,17 @@ export class VatSyscall { /** The kernel's store */ readonly #kernelStore: KernelStore; + /** Function to check if the vat is still active */ + readonly #isActive: () => boolean; + + /** Label for this vat type (for error messages) */ + readonly #vatLabel: string; + /** Logger for outputting messages (such as errors) to the console */ readonly #logger: Logger | undefined; /** The illegal syscall that was received */ - illegalSyscall: { vatId: VatId; info: SwingSetCapData } | undefined; + illegalSyscall: { vatId: EndpointId; info: SwingSetCapData } | undefined; /** The error when delivery failed */ deliveryError: string | undefined; @@ -61,12 +69,23 @@ export class VatSyscall { * @param props.vatId - The ID of the vat. * @param props.kernelQueue - The kernel's run queue. * @param props.kernelStore - The kernel's store. + * @param props.isActive - Function to check if the vat is still active. + * @param props.vatLabel - Label for this vat type (for error messages). * @param props.logger - The logger for the VatSyscall. */ - constructor({ vatId, kernelQueue, kernelStore, logger }: VatSyscallProps) { + constructor({ + vatId, + kernelQueue, + kernelStore, + isActive, + vatLabel = 'vat', + logger, + }: VatSyscallProps) { this.vatId = vatId; this.#kernelQueue = kernelQueue; this.#kernelStore = kernelStore; + this.#isActive = isActive; + this.#vatLabel = vatLabel; this.#logger = logger; } @@ -151,9 +170,9 @@ export class VatSyscall { this.vatRequestedTermination = undefined; // This is a safety check - this case should never happen - if (!this.#kernelStore.isVatActive(this.vatId)) { - this.#recordVatFatalSyscall('vat not found'); - return harden(['error', 'vat not found']); + if (!this.#isActive()) { + this.#recordVatFatalSyscall(`${this.#vatLabel} not found`); + return harden(['error', `${this.#vatLabel} not found`]); } const kso: VatSyscallObject = this.#kernelStore.translateSyscallVtoK( @@ -238,18 +257,27 @@ export class VatSyscall { case 'vatstoreGetNextKey': case 'vatstoreSet': case 'vatstoreDelete': { - this.#logger?.warn(`vat ${vatId} issued invalid syscall ${op} `, vso); + this.#logger?.warn( + `${this.#vatLabel} ${vatId} issued invalid syscall ${op} `, + vso, + ); break; } default: // Compile-time exhaustiveness check - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - this.#logger?.warn(`vat ${vatId} issued unknown syscall ${op} `, vso); + this.#logger?.warn( + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + `${this.#vatLabel} ${vatId} issued unknown syscall ${op} `, + vso, + ); break; } return harden(['ok', null]); } catch (error) { - this.#logger?.error(`Fatal syscall error in vat ${this.vatId}`, error); + this.#logger?.error( + `Fatal syscall error in ${this.#vatLabel} ${this.vatId}`, + error, + ); this.#recordVatFatalSyscall('syscall translation error: prepare to die'); return harden([ 'error', From baf4eccbb8536b61ad1150d689245a4eed74cc18 Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Wed, 28 Jan 2026 11:26:55 -0800 Subject: [PATCH 12/41] refactor(kernel-browser-runtime): Remove unused captp directory The kernel-worker/captp/ directory contained makeKernelCapTP and makeKernelFacade which have been replaced by the kernel host subcluster. CapTP now uses the kernel host vat root directly as the bootstrap object. Files removed: - kernel-captp.ts, kernel-captp.test.ts - kernel-facade.ts, kernel-facade.test.ts - captp.integration.test.ts - index.ts Note: background-captp.ts is kept as it provides the CapTP message utilities used by both kernel-worker and background. Co-Authored-By: Claude --- .../captp/captp.integration.test.ts | 190 ----------------- .../src/kernel-worker/captp/index.ts | 7 - .../kernel-worker/captp/kernel-captp.test.ts | 77 ------- .../src/kernel-worker/captp/kernel-captp.ts | 68 ------ .../kernel-worker/captp/kernel-facade.test.ts | 196 ------------------ .../src/kernel-worker/captp/kernel-facade.ts | 47 ----- 6 files changed, 585 deletions(-) delete mode 100644 packages/kernel-browser-runtime/src/kernel-worker/captp/captp.integration.test.ts delete mode 100644 packages/kernel-browser-runtime/src/kernel-worker/captp/index.ts delete mode 100644 packages/kernel-browser-runtime/src/kernel-worker/captp/kernel-captp.test.ts delete mode 100644 packages/kernel-browser-runtime/src/kernel-worker/captp/kernel-captp.ts delete mode 100644 packages/kernel-browser-runtime/src/kernel-worker/captp/kernel-facade.test.ts delete mode 100644 packages/kernel-browser-runtime/src/kernel-worker/captp/kernel-facade.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 deleted file mode 100644 index 0e3fe0cf0..000000000 --- a/packages/kernel-browser-runtime/src/kernel-worker/captp/captp.integration.test.ts +++ /dev/null @@ -1,190 +0,0 @@ -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({ - subclusterId: 'sc1', - bootstrapRootKref: 'ko1', - bootstrapResult: { - body: '#{"result":"ok"}', - slots: [], - }, - }), - 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('pong'), - } 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); - - // The kernel facade now returns LaunchResult instead of CapData - expect(result).toStrictEqual({ - subclusterId: 'sc1', - rootKref: '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).toBe('pong'); - - 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/index.ts b/packages/kernel-browser-runtime/src/kernel-worker/captp/index.ts deleted file mode 100644 index 6e3ee7053..000000000 --- a/packages/kernel-browser-runtime/src/kernel-worker/captp/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -export { - makeKernelCapTP, - type KernelCapTP, - type KernelCapTPOptions, -} from './kernel-captp.ts'; - -export { makeKernelFacade, type KernelFacade } from './kernel-facade.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 deleted file mode 100644 index fbd1eb0d2..000000000 --- a/packages/kernel-browser-runtime/src/kernel-worker/captp/kernel-captp.test.ts +++ /dev/null @@ -1,77 +0,0 @@ -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 '../../types.ts'; - -describe('makeKernelCapTP', () => { - const mockKernel: Kernel = {} as unknown as Kernel; - let sendMock: (message: CapTPMessage) => void; - - beforeEach(() => { - 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-captp.ts b/packages/kernel-browser-runtime/src/kernel-worker/captp/kernel-captp.ts deleted file mode 100644 index 16587a100..000000000 --- a/packages/kernel-browser-runtime/src/kernel-worker/captp/kernel-captp.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { makeCapTP } from '@endo/captp'; -import type { Kernel } from '@metamask/ocap-kernel'; - -import { makeKernelFacade } from './kernel-facade.ts'; -import type { CapTPMessage } from '../../types.ts'; - -/** - * 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?: unknown) => 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.test.ts b/packages/kernel-browser-runtime/src/kernel-worker/captp/kernel-facade.test.ts deleted file mode 100644 index cdaf77703..000000000 --- a/packages/kernel-browser-runtime/src/kernel-worker/captp/kernel-facade.test.ts +++ /dev/null @@ -1,196 +0,0 @@ -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; - - beforeEach(() => { - mockKernel = { - launchSubcluster: vi.fn().mockResolvedValue({ - subclusterId: 'sc1', - bootstrapRootKref: 'ko1', - }), - 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('pong'), - } 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 = makeClusterConfig(); - - await facade.launchSubcluster(config); - - expect(mockKernel.launchSubcluster).toHaveBeenCalledWith(config); - expect(mockKernel.launchSubcluster).toHaveBeenCalledTimes(1); - }); - - it('returns result with subclusterId and rootKref from kernel', async () => { - const kernelResult = { - subclusterId: 's1', - bootstrapRootKref: 'ko1', - bootstrapResult: { body: '#null', slots: [] }, - }; - vi.mocked(mockKernel.launchSubcluster).mockResolvedValueOnce( - kernelResult, - ); - - const config: ClusterConfig = makeClusterConfig(); - - const result = await facade.launchSubcluster(config); - - expect(result).toStrictEqual({ - subclusterId: 's1', - rootKref: 'ko1', - }); - }); - - it('propagates errors from kernel', async () => { - const error = new Error('Launch failed'); - vi.mocked(mockKernel.launchSubcluster).mockRejectedValueOnce(error); - const config: ClusterConfig = makeClusterConfig(); - - 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: KernelStatus = { - vats: [], - subclusters: [], - remoteComms: { isInitialized: false }, - }; - 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 () => { - vi.mocked(mockKernel.pingVat).mockResolvedValueOnce('pong'); - - const result = await facade.pingVat('v1'); - expect(result).toBe('pong'); - }); - - 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/src/kernel-worker/captp/kernel-facade.ts b/packages/kernel-browser-runtime/src/kernel-worker/captp/kernel-facade.ts deleted file mode 100644 index 51d3cc9a4..000000000 --- a/packages/kernel-browser-runtime/src/kernel-worker/captp/kernel-facade.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { makeDefaultExo } from '@metamask/kernel-utils/exo'; -import type { Kernel, ClusterConfig, KRef, VatId } from '@metamask/ocap-kernel'; - -import type { KernelFacade, LaunchResult } 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', { - ping: async () => 'pong' as const, - - launchSubcluster: async (config: ClusterConfig): Promise => { - const { subclusterId, bootstrapRootKref } = - await kernel.launchSubcluster(config); - return { subclusterId, rootKref: bootstrapRootKref }; - }, - - 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); - }, - - getVatRoot: async (krefString: string) => { - // Return wrapped kref for future CapTP marshalling to presence - // TODO: Enable custom CapTP marshalling tables to convert this to a presence - return { kref: krefString }; - }, - }); -} -harden(makeKernelFacade); From 9bc5267837e250da7f3730ec927d514dab5ec04f Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Wed, 28 Jan 2026 11:34:18 -0800 Subject: [PATCH 13/41] feat(nodejs): Add host subcluster utilities Add host subcluster utilities for Node.js runtime that enable launching system subclusters with kernel facet access. - Add makeKernelHostSubclusterConfig() to create kernel host vat config - Add makeHostSubcluster() to launch and get root object - Export KernelHostRoot and HostSubclusterResult types - Add comprehensive unit tests Co-Authored-By: Claude Opus 4.5 --- .../nodejs/src/host-subcluster/index.test.ts | 152 +++++++++++++++ packages/nodejs/src/host-subcluster/index.ts | 176 ++++++++++++++++++ packages/nodejs/src/index.ts | 8 + 3 files changed, 336 insertions(+) create mode 100644 packages/nodejs/src/host-subcluster/index.test.ts create mode 100644 packages/nodejs/src/host-subcluster/index.ts diff --git a/packages/nodejs/src/host-subcluster/index.test.ts b/packages/nodejs/src/host-subcluster/index.test.ts new file mode 100644 index 000000000..27c103e1c --- /dev/null +++ b/packages/nodejs/src/host-subcluster/index.test.ts @@ -0,0 +1,152 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; + +import type { KernelHostRoot } from './index.ts'; +import { makeKernelHostSubclusterConfig, makeHostSubcluster } from './index.ts'; + +describe('makeKernelHostSubclusterConfig', () => { + const mockKernelFacet = { + launchSubcluster: vi.fn(), + terminateSubcluster: vi.fn(), + getStatus: vi.fn(), + reloadSubcluster: vi.fn(), + getSubcluster: vi.fn(), + getSubclusters: vi.fn(), + }; + + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('returns a valid system subcluster config', () => { + const onRootCreated = vi.fn(); + const config = makeKernelHostSubclusterConfig(onRootCreated); + + expect(config.bootstrap).toBe('kernelHost'); + expect(config.vats.kernelHost).toBeDefined(); + expect(config.vats?.kernelHost?.buildRootObject).toBeTypeOf('function'); + }); + + it('invokes onRootCreated callback when buildRootObject is called', () => { + const onRootCreated = vi.fn(); + const config = makeKernelHostSubclusterConfig(onRootCreated); + + const root = config.vats?.kernelHost?.buildRootObject( + { + kernelFacet: mockKernelFacet, + }, + {}, + ); + + expect(onRootCreated).toHaveBeenCalledWith(root); + }); + + describe('kernel host root', () => { + let root: KernelHostRoot; + + beforeEach(() => { + const onRootCreated = vi.fn(); + const config = makeKernelHostSubclusterConfig(onRootCreated); + root = config.vats?.kernelHost?.buildRootObject( + { + kernelFacet: mockKernelFacet, + }, + {}, + ) as KernelHostRoot; + }); + + it('creates root with expected methods', () => { + expect(root.ping).toBeTypeOf('function'); + expect(root.launchSubcluster).toBeTypeOf('function'); + expect(root.terminateSubcluster).toBeTypeOf('function'); + expect(root.getStatus).toBeTypeOf('function'); + expect(root.reloadSubcluster).toBeTypeOf('function'); + expect(root.getSubcluster).toBeTypeOf('function'); + expect(root.getSubclusters).toBeTypeOf('function'); + }); + + it('ping returns pong', async () => { + const result = await root.ping(); + expect(result).toBe('pong'); + }); + + // Note: launchSubcluster, terminateSubcluster, getStatus, reloadSubcluster + // use E() which requires endo initialization. These are integration tested + // via the full system tests rather than unit tests. + + it('getSubcluster calls kernel facet synchronously', () => { + mockKernelFacet.getSubcluster.mockReturnValue({ + id: 's1', + config: { bootstrap: 'test', vats: {} }, + vats: {}, + }); + + const result = root.getSubcluster('s1'); + + expect(mockKernelFacet.getSubcluster).toHaveBeenCalledWith('s1'); + expect(result?.id).toBe('s1'); + }); + + it('getSubclusters calls kernel facet synchronously', () => { + mockKernelFacet.getSubclusters.mockReturnValue([ + { id: 's1', config: { bootstrap: 'test', vats: {} }, vats: {} }, + ]); + + const result = root.getSubclusters(); + + expect(mockKernelFacet.getSubclusters).toHaveBeenCalled(); + expect(result).toHaveLength(1); + }); + }); +}); + +describe('makeHostSubcluster', () => { + it('launches system subcluster and returns result', async () => { + const mockKernel = { + launchSystemSubcluster: vi.fn(async (config) => { + // Simulate the kernel calling buildRootObject + const mockKernelFacet = { + launchSubcluster: vi.fn(), + terminateSubcluster: vi.fn(), + getStatus: vi.fn(), + reloadSubcluster: vi.fn(), + getSubcluster: vi.fn(), + getSubclusters: vi.fn(), + }; + config.vats?.kernelHost?.buildRootObject( + { kernelFacet: mockKernelFacet }, + {}, + ); + return { systemSubclusterId: 'ss0', vatIds: { kernelHost: 'sv0' } }; + }), + }; + + const result = await makeHostSubcluster(mockKernel as never); + + expect(mockKernel.launchSystemSubcluster).toHaveBeenCalledWith( + expect.objectContaining({ + bootstrap: 'kernelHost', + vats: expect.objectContaining({ + kernelHost: expect.objectContaining({ + buildRootObject: expect.any(Function), + }), + }), + }), + ); + expect(result.systemSubclusterId).toBe('ss0'); + expect(result.kernelHostRoot).toBeDefined(); + expect(result.kernelHostRoot.ping).toBeTypeOf('function'); + }); + + it('throws if root object not captured', async () => { + const mockKernel = { + launchSystemSubcluster: vi.fn(async () => ({ + systemSubclusterId: 'ss0', + vatIds: { kernelHost: 'sv0' }, + })), + }; + + await expect(makeHostSubcluster(mockKernel as never)).rejects.toThrow( + 'Failed to capture kernel host root object', + ); + }); +}); diff --git a/packages/nodejs/src/host-subcluster/index.ts b/packages/nodejs/src/host-subcluster/index.ts new file mode 100644 index 000000000..9a1df45b7 --- /dev/null +++ b/packages/nodejs/src/host-subcluster/index.ts @@ -0,0 +1,176 @@ +import { E } from '@endo/eventual-send'; +import { makeDefaultExo } from '@metamask/kernel-utils/exo'; +import type { + SystemVatBuildRootObject, + SystemSubclusterConfig, + ClusterConfig, + KernelStatus, + Subcluster, + KernelFacet, + KernelFacetLaunchResult, + Kernel, +} from '@metamask/ocap-kernel'; + +/** + * The kernel host vat's root object interface. + * + * This is the interface exposed by the kernel host vat to the host application. + */ +export type KernelHostRoot = { + /** + * Ping the kernel host. + * + * @returns 'pong' to confirm the host is responsive. + */ + ping: () => Promise<'pong'>; + + /** + * Launch a dynamic subcluster. + * + * @param config - Configuration for the subcluster. + * @returns The launch result with subcluster ID and root presence. + */ + launchSubcluster: (config: ClusterConfig) => Promise; + + /** + * Terminate a subcluster. + * + * @param subclusterId - The ID of the subcluster to terminate. + */ + terminateSubcluster: (subclusterId: string) => Promise; + + /** + * Get kernel status. + * + * @returns The current kernel status. + */ + getStatus: () => Promise; + + /** + * Reload a subcluster. + * + * @param subclusterId - The ID of the subcluster to reload. + * @returns The reloaded subcluster. + */ + reloadSubcluster: (subclusterId: string) => Promise; + + /** + * Get a subcluster by ID. + * + * @param subclusterId - The ID of the subcluster. + * @returns The subcluster or undefined if not found. + */ + getSubcluster: (subclusterId: string) => Subcluster | undefined; + + /** + * Get all subclusters. + * + * @returns Array of all subclusters. + */ + getSubclusters: () => Subcluster[]; +}; + +/** + * Create the configuration for launching the kernel host subcluster. + * + * @param onRootCreated - Callback invoked when the root object is created. + * @returns The system subcluster configuration. + */ +export function makeKernelHostSubclusterConfig( + onRootCreated: (root: KernelHostRoot) => void, +): SystemSubclusterConfig { + const buildRootObject: SystemVatBuildRootObject = (vatPowers) => { + const kernelFacet = vatPowers.kernelFacet as KernelFacet; + + const root = makeDefaultExo('KernelHostRoot', { + ping: async () => 'pong' as const, + + launchSubcluster: async (config: ClusterConfig) => { + // Use E() to call kernel facet - this gives us proper reference handling + return E(kernelFacet).launchSubcluster(config); + }, + + terminateSubcluster: async (subclusterId: string) => { + return E(kernelFacet).terminateSubcluster(subclusterId); + }, + + getStatus: async () => { + return E(kernelFacet).getStatus(); + }, + + reloadSubcluster: async (subclusterId: string) => { + return E(kernelFacet).reloadSubcluster(subclusterId); + }, + + getSubcluster: (subclusterId: string) => { + // Synchronous method - call directly + return kernelFacet.getSubcluster(subclusterId); + }, + + getSubclusters: () => { + // Synchronous method - call directly + return kernelFacet.getSubclusters(); + }, + }) as KernelHostRoot; + + // Capture the root object for external use + onRootCreated(root); + + return root; + }; + + return { + bootstrap: 'kernelHost', + vats: { + kernelHost: { buildRootObject }, + }, + }; +} +harden(makeKernelHostSubclusterConfig); + +/** + * Result of launching the host subcluster. + */ +export type HostSubclusterResult = { + /** + * The system subcluster ID. + */ + systemSubclusterId: string; + + /** + * The kernel host root object for interacting with the kernel. + */ + kernelHostRoot: KernelHostRoot; +}; + +/** + * Launch the host subcluster on a kernel. + * + * This creates a system subcluster with a kernel host vat that provides + * privileged kernel operations. The returned root object can be used directly + * to interact with the kernel (e.g., launch subclusters, get status). + * + * @param kernel - The kernel instance to launch the host subcluster on. + * @returns The host subcluster result with the system subcluster ID and root object. + */ +export async function makeHostSubcluster( + kernel: Kernel, +): Promise { + let kernelHostRoot: KernelHostRoot | undefined; + + const hostSubclusterConfig = makeKernelHostSubclusterConfig((root) => { + kernelHostRoot = root; + }); + + const result = await kernel.launchSystemSubcluster(hostSubclusterConfig); + + if (!kernelHostRoot) { + throw new Error('Failed to capture kernel host root object'); + } + + return harden({ + systemSubclusterId: result.systemSubclusterId, + kernelHostRoot, + }); +} +harden(makeHostSubcluster); diff --git a/packages/nodejs/src/index.ts b/packages/nodejs/src/index.ts index 6af1ec51b..fa7d8400c 100644 --- a/packages/nodejs/src/index.ts +++ b/packages/nodejs/src/index.ts @@ -1,3 +1,11 @@ export { NodejsPlatformServices } from './kernel/PlatformServices.ts'; export { makeKernel } from './kernel/make-kernel.ts'; export { makeNodeJsVatSupervisor } from './vat/make-supervisor.ts'; +export { + makeHostSubcluster, + makeKernelHostSubclusterConfig, +} from './host-subcluster/index.ts'; +export type { + KernelHostRoot, + HostSubclusterResult, +} from './host-subcluster/index.ts'; From 6c75628ab4b31bc83b0fc70cf47024efc2b574c2 Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Wed, 28 Jan 2026 13:56:09 -0800 Subject: [PATCH 14/41] feat(kernel-facet): Add getVatRoot and rootKref for presence restoration Enable omnium-gatherum to store kref strings and restore presences after restart. KernelFacetLaunchResult now includes rootKref alongside the root SlotValue. The new getVatRoot(kref) method converts stored krefs back to slot values that become presences when marshalled via CapTP/liveslots. Co-Authored-By: Claude Opus 4.5 --- .../src/kernel-worker/kernel-host-vat.ts | 15 ++++ packages/kernel-browser-runtime/src/types.ts | 84 ++++++++++++++++--- packages/nodejs/src/host-subcluster/index.ts | 15 ++++ packages/ocap-kernel/src/kernel-facet.ts | 36 +++++++- 4 files changed, 139 insertions(+), 11 deletions(-) diff --git a/packages/kernel-browser-runtime/src/kernel-worker/kernel-host-vat.ts b/packages/kernel-browser-runtime/src/kernel-worker/kernel-host-vat.ts index 4ca817ffe..f1ecee673 100644 --- a/packages/kernel-browser-runtime/src/kernel-worker/kernel-host-vat.ts +++ b/packages/kernel-browser-runtime/src/kernel-worker/kernel-host-vat.ts @@ -68,6 +68,16 @@ export type KernelHostRoot = { * @returns Array of all subclusters. */ getSubclusters: () => Subcluster[]; + + /** + * Convert a kref string to a presence. + * + * Use this to restore a presence from a stored kref string after restart. + * + * @param kref - The kref string to convert. + * @returns The presence for the given kref. + */ + getVatRoot: (kref: string) => unknown; }; /** @@ -111,6 +121,11 @@ export function makeKernelHostSubclusterConfig( // Synchronous method - call directly return kernelFacet.getSubclusters(); }, + + getVatRoot: async (kref: string) => { + // Convert kref to slot value, which becomes a presence via CapTP + return kernelFacet.getVatRoot(kref); + }, }) as KernelHostRoot; // Capture the root object for external use (e.g., CapTP bootstrap) diff --git a/packages/kernel-browser-runtime/src/types.ts b/packages/kernel-browser-runtime/src/types.ts index 02d014d2b..74bb5bdb0 100644 --- a/packages/kernel-browser-runtime/src/types.ts +++ b/packages/kernel-browser-runtime/src/types.ts @@ -1,4 +1,9 @@ -import type { Kernel, ClusterConfig } from '@metamask/ocap-kernel'; +import type { + ClusterConfig, + Subcluster, + KernelStatus, + KernelFacetLaunchResult, +} from '@metamask/ocap-kernel'; import type { Json } from '@metamask/utils'; /** @@ -7,9 +12,9 @@ import type { Json } from '@metamask/utils'; export type CapTPMessage = Record; /** - * Result of launching a subcluster. + * Result of launching a subcluster (legacy format with kref string). * - * The rootKref contains the kref string for the bootstrap vat's root object. + * @deprecated Use KernelFacetLaunchResult instead, which returns a presence. */ export type LaunchResult = { subclusterId: string; @@ -19,14 +24,73 @@ export type LaunchResult = { /** * The kernel facade interface - methods exposed to userspace via CapTP. * - * This is the remote presence type that the background receives from the kernel. + * This is the remote presence type that the background receives from the kernel + * via the kernel host vat. The kernel host vat runs as a system vat inside + * the kernel worker and serves as the CapTP bootstrap object. + * + * Note: launchSubcluster now returns a presence in the `root` field instead of + * a kref string. When received via CapTP, this becomes an E()-callable presence. */ export type KernelFacade = { + /** + * Ping the kernel host. + * + * @returns 'pong' to confirm the host is responsive. + */ ping: () => Promise<'pong'>; - launchSubcluster: (config: ClusterConfig) => Promise; - terminateSubcluster: Kernel['terminateSubcluster']; - queueMessage: Kernel['queueMessage']; - getStatus: Kernel['getStatus']; - pingVat: Kernel['pingVat']; - getVatRoot: (krefString: string) => Promise; + + /** + * Launch a dynamic subcluster. + * + * @param config - Configuration for the subcluster. + * @returns The launch result with subcluster ID and root presence. + */ + launchSubcluster: (config: ClusterConfig) => Promise; + + /** + * Terminate a subcluster. + * + * @param subclusterId - The ID of the subcluster to terminate. + */ + terminateSubcluster: (subclusterId: string) => Promise; + + /** + * Get kernel status. + * + * @returns The current kernel status. + */ + getStatus: () => Promise; + + /** + * Reload a subcluster. + * + * @param subclusterId - The ID of the subcluster to reload. + * @returns The reloaded subcluster. + */ + reloadSubcluster: (subclusterId: string) => Promise; + + /** + * Get a subcluster by ID. + * + * @param subclusterId - The ID of the subcluster. + * @returns The subcluster or undefined if not found. + */ + getSubcluster: (subclusterId: string) => Subcluster | undefined; + + /** + * Get all subclusters. + * + * @returns Array of all subclusters. + */ + getSubclusters: () => Subcluster[]; + + /** + * Convert a kref string to a presence. + * + * Use this to restore a presence from a stored kref string after restart. + * + * @param kref - The kref string to convert. + * @returns The presence for the given kref. + */ + getVatRoot: (kref: string) => unknown; }; diff --git a/packages/nodejs/src/host-subcluster/index.ts b/packages/nodejs/src/host-subcluster/index.ts index 9a1df45b7..c5a3b0e5d 100644 --- a/packages/nodejs/src/host-subcluster/index.ts +++ b/packages/nodejs/src/host-subcluster/index.ts @@ -68,6 +68,16 @@ export type KernelHostRoot = { * @returns Array of all subclusters. */ getSubclusters: () => Subcluster[]; + + /** + * Convert a kref string to a presence. + * + * Use this to restore a presence from a stored kref string after restart. + * + * @param kref - The kref string to convert. + * @returns The presence for the given kref. + */ + getVatRoot: (kref: string) => unknown; }; /** @@ -111,6 +121,11 @@ export function makeKernelHostSubclusterConfig( // Synchronous method - call directly return kernelFacet.getSubclusters(); }, + + getVatRoot: async (kref: string) => { + // Convert kref to slot value, which becomes a presence via liveslots + return kernelFacet.getVatRoot(kref); + }, }) as KernelHostRoot; // Capture the root object for external use diff --git a/packages/ocap-kernel/src/kernel-facet.ts b/packages/ocap-kernel/src/kernel-facet.ts index 55bad2678..e58995032 100644 --- a/packages/ocap-kernel/src/kernel-facet.ts +++ b/packages/ocap-kernel/src/kernel-facet.ts @@ -23,11 +23,22 @@ export type KernelFacetDependencies = Pick< /** * Result of launching a subcluster via the kernel facet. - * Contains the root object as a slot value (which will become a presence). + * Contains the root object as a slot value (which will become a presence) + * and the root kref string for storage purposes. */ export type KernelFacetLaunchResult = { + /** The ID of the launched subcluster. */ subclusterId: string; + /** + * The root object as a slot value (becomes a presence when marshalled). + * Use this directly with E() for immediate operations. + */ root: SlotValue; + /** + * The root kref string for storage purposes. + * Store this value to restore the presence after restart using getSubclusterRoot(). + */ + rootKref: string; }; /** @@ -52,6 +63,16 @@ export type KernelFacet = Omit< * @returns A promise for the launch result containing subclusterId and root presence. */ launchSubcluster: (config: ClusterConfig) => Promise; + + /** + * Convert a kref string to a slot value (presence). + * + * Use this to restore a presence from a stored kref string after restart. + * + * @param kref - The kref string to convert. + * @returns The slot value that will become a presence when marshalled. + */ + getVatRoot: (kref: string) => SlotValue; }; /** @@ -99,6 +120,7 @@ export function makeKernelFacet(deps: KernelFacetDependencies): KernelFacet { return { subclusterId: result.subclusterId, root: kslot(result.bootstrapRootKref, 'vatRoot'), + rootKref: result.bootstrapRootKref, }; }, @@ -153,6 +175,18 @@ export function makeKernelFacet(deps: KernelFacetDependencies): KernelFacet { async getStatus(): Promise { return getStatus(); }, + + /** + * Convert a kref string to a slot value (presence). + * + * Use this to restore a presence from a stored kref string after restart. + * + * @param kref - The kref string to convert. + * @returns The slot value that will become a presence when marshalled. + */ + getVatRoot(kref: string): SlotValue { + return kslot(kref, 'vatRoot'); + }, }); return kernelFacet; From 69c5d8c407550998763e7e3c27da5d055416085b Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Wed, 28 Jan 2026 14:52:58 -0800 Subject: [PATCH 15/41] test(nodejs): Add e2e tests for system subclusters Add test vats for verifying system subcluster functionality: - alice-vat.js: orchestrates third-party handoff - bob-vat.js: creates greeter exos for handoff tests - carol-vat.js: receives and uses exos from other vats - promise-vat.js: tests deferred promises, rejection, pipelining Add e2e test suites: - system-subcluster.test.ts: full test suite for third-party handoff, promise handling, and kref restoration - system-subcluster-simple.test.ts: diagnostic tests to isolate system subcluster launch behavior Note: Tests are work-in-progress for debugging system subcluster launch timing issues. Co-Authored-By: Claude Opus 4.5 --- .../test/e2e/system-subcluster-simple.test.ts | 65 +++++ .../nodejs/test/e2e/system-subcluster.test.ts | 269 ++++++++++++++++++ packages/nodejs/test/vats/alice-vat.js | 42 +++ packages/nodejs/test/vats/bob-vat.js | 34 +++ packages/nodejs/test/vats/carol-vat.js | 60 ++++ packages/nodejs/test/vats/promise-vat.js | 124 ++++++++ 6 files changed, 594 insertions(+) create mode 100644 packages/nodejs/test/e2e/system-subcluster-simple.test.ts create mode 100644 packages/nodejs/test/e2e/system-subcluster.test.ts create mode 100644 packages/nodejs/test/vats/alice-vat.js create mode 100644 packages/nodejs/test/vats/bob-vat.js create mode 100644 packages/nodejs/test/vats/carol-vat.js create mode 100644 packages/nodejs/test/vats/promise-vat.js diff --git a/packages/nodejs/test/e2e/system-subcluster-simple.test.ts b/packages/nodejs/test/e2e/system-subcluster-simple.test.ts new file mode 100644 index 000000000..bb93768dc --- /dev/null +++ b/packages/nodejs/test/e2e/system-subcluster-simple.test.ts @@ -0,0 +1,65 @@ +import { makeDefaultExo } from '@metamask/kernel-utils/exo'; +import type { + Kernel, + ClusterConfig, + SystemSubclusterConfig, +} from '@metamask/ocap-kernel'; +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; + +import { makeKernel } from '../../src/kernel/make-kernel.ts'; + +describe('system subcluster simple tests', { timeout: 60_000 }, () => { + let kernel: Kernel; + + beforeEach(async () => { + kernel = await makeKernel({}); + }); + + afterEach(async () => { + await kernel.clearStorage(); + }); + + // First test: verify regular dynamic subcluster works + it('launches a regular dynamic subcluster', async () => { + const config: ClusterConfig = { + bootstrap: 'bob', + vats: { + bob: { + bundleSpec: 'http://localhost:3000/bob-vat.bundle', + }, + }, + }; + + const result = await kernel.launchSubcluster(config); + expect(result.subclusterId).toBeDefined(); + expect(result.bootstrapRootKref).toBeDefined(); + }); + + // Second test: verify we can get kernel status + it('gets kernel status', async () => { + const status = await kernel.getStatus(); + expect(status).toBeDefined(); + expect(status.subclusters).toBeDefined(); + }); + + // Third test: launch a system subcluster + it('launches a system subcluster', async () => { + const config: SystemSubclusterConfig = { + bootstrap: 'testVat', + vats: { + testVat: { + buildRootObject: (_vatPowers, _params) => { + return makeDefaultExo('testRoot', { + bootstrap: () => undefined, + ping: () => 'pong', + }); + }, + }, + }, + }; + + const result = await kernel.launchSystemSubcluster(config); + expect(result.systemSubclusterId).toBeDefined(); + expect(result.vatIds.testVat).toBe('sv0'); + }); +}); diff --git a/packages/nodejs/test/e2e/system-subcluster.test.ts b/packages/nodejs/test/e2e/system-subcluster.test.ts new file mode 100644 index 000000000..d43d874db --- /dev/null +++ b/packages/nodejs/test/e2e/system-subcluster.test.ts @@ -0,0 +1,269 @@ +import { E } from '@endo/eventual-send'; +import type { Kernel, ClusterConfig } from '@metamask/ocap-kernel'; +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; + +import { makeHostSubcluster } from '../../src/host-subcluster/index.ts'; +import type { KernelHostRoot } from '../../src/host-subcluster/index.ts'; +import { makeKernel } from '../../src/kernel/make-kernel.ts'; + +type Bob = { + makeGreeter: (greeting: string) => Promise; +}; +type Carol = { + receiveAndGreet: (greeter: Greeter, name: string) => Promise; +}; +type Greeter = { + greet: (name: string) => Promise; +}; + +type PromiseVat = { + makeGreeter: (greeting: string) => Promise; + makeDeferredPromise: () => Promise; + resolveDeferredPromise: (value: unknown) => void; + rejectDeferredPromise: (reason: string) => void; + getRejectingPromise: (reason: string) => Promise; + awaitPromiseArg: (promiseArg: Promise) => Promise; +}; + +describe('system subcluster e2e tests', { timeout: 30_000 }, () => { + let kernel: Kernel; + let kernelHostRoot: KernelHostRoot; + + beforeEach(async () => { + kernel = await makeKernel({}); + const hostResult = await makeHostSubcluster(kernel); + kernelHostRoot = hostResult.kernelHostRoot; + }); + + afterEach(async () => { + await kernel.clearStorage(); + }); + + describe('basic operations', () => { + it('pings the kernel host', async () => { + const result = await kernelHostRoot.ping(); + expect(result).toBe('pong'); + }); + + it('gets kernel status', async () => { + const status = await kernelHostRoot.getStatus(); + expect(status).toBeDefined(); + expect(status.vats).toBeDefined(); + expect(status.subclusters).toBeDefined(); + }); + + it('launches a subcluster and receives E()-callable presence', async () => { + const config: ClusterConfig = { + bootstrap: 'bob', + vats: { + bob: { + bundleSpec: 'http://localhost:3000/bob-vat.bundle', + }, + }, + }; + + const result = await kernelHostRoot.launchSubcluster(config); + + expect(result.subclusterId).toBeDefined(); + expect(result.root).toBeDefined(); + expect(result.rootKref).toBeDefined(); + + // The root should be E()-callable + const bob = result.root as Bob; + const greeter = await E(bob).makeGreeter('Hello'); + expect(greeter).toBeDefined(); + }); + + it('terminates a subcluster', async () => { + const config: ClusterConfig = { + bootstrap: 'bob', + vats: { + bob: { + bundleSpec: 'http://localhost:3000/bob-vat.bundle', + }, + }, + }; + + const result = await kernelHostRoot.launchSubcluster(config); + const subcluster = kernelHostRoot.getSubcluster(result.subclusterId); + expect(subcluster).toBeDefined(); + + await kernelHostRoot.terminateSubcluster(result.subclusterId); + + const terminatedSubcluster = kernelHostRoot.getSubcluster( + result.subclusterId, + ); + expect(terminatedSubcluster).toBeUndefined(); + }); + }); + + describe('third-party handoff', () => { + it('host orchestrates handoff between two vats', async () => { + // Launch Bob and Carol in the same subcluster + const config: ClusterConfig = { + bootstrap: 'bob', + vats: { + bob: { + bundleSpec: 'http://localhost:3000/bob-vat.bundle', + }, + carol: { + bundleSpec: 'http://localhost:3000/carol-vat.bundle', + }, + }, + }; + + const launchResult = await kernelHostRoot.launchSubcluster(config); + const bob = launchResult.root as Bob; + + // Get Carol's root object + const subcluster = kernelHostRoot.getSubcluster( + launchResult.subclusterId, + ); + expect(subcluster).toBeDefined(); + const carolVatId = subcluster?.vats.find( + (vatId) => vatId !== subcluster.vats[0], + ); + expect(carolVatId).toBeDefined(); + const carolKref = kernel.pinVatRoot(carolVatId!); + const carol = kernelHostRoot.getVatRoot(carolKref) as Carol; + + // Host orchestrates: get exo from Bob, pass to Carol + const greeter = await E(bob).makeGreeter('Greetings'); + const result = await E(carol).receiveAndGreet(greeter, 'Universe'); + + expect(result).toBe('Greetings, Universe!'); + }); + + it('host passes presence between two separate subclusters', async () => { + // Launch Bob in one subcluster + const bobConfig: ClusterConfig = { + bootstrap: 'bob', + vats: { + bob: { + bundleSpec: 'http://localhost:3000/bob-vat.bundle', + }, + }, + }; + const bobResult = await kernelHostRoot.launchSubcluster(bobConfig); + const bob = bobResult.root as Bob; + + // Launch Carol in another subcluster + const carolConfig: ClusterConfig = { + bootstrap: 'carol', + vats: { + carol: { + bundleSpec: 'http://localhost:3000/carol-vat.bundle', + }, + }, + }; + const carolResult = await kernelHostRoot.launchSubcluster(carolConfig); + const carol = carolResult.root as Carol; + + // Host orchestrates cross-subcluster handoff + const greeter = await E(bob).makeGreeter('Cross-cluster'); + const result = await E(carol).receiveAndGreet(greeter, 'Test'); + + expect(result).toBe('Cross-cluster, Test!'); + }); + }); + + describe('promise handling', () => { + it('supports promise pipelining (E() on unresolved promise)', async () => { + const config: ClusterConfig = { + bootstrap: 'promiseVat', + vats: { + promiseVat: { + bundleSpec: 'http://localhost:3000/promise-vat.bundle', + }, + }, + }; + + const result = await kernelHostRoot.launchSubcluster(config); + const promiseVat = result.root as PromiseVat; + + // Get a promise for an exo (without awaiting) + const exoPromise = E(promiseVat).makeGreeter('Hi'); + + // Pipeline: call method on the unresolved promise + const greetingPromise = E(exoPromise).greet('World'); + + // Both should resolve correctly + const greeting = await greetingPromise; + expect(greeting).toBe('Hi, World!'); + }); + + it('handles deferred promise resolution', async () => { + const config: ClusterConfig = { + bootstrap: 'promiseVat', + vats: { + promiseVat: { + bundleSpec: 'http://localhost:3000/promise-vat.bundle', + }, + }, + }; + + const result = await kernelHostRoot.launchSubcluster(config); + const promiseVat = result.root as PromiseVat; + + // Get a deferred promise (unresolved) + const deferredPromise = E(promiseVat).makeDeferredPromise(); + + // Resolve it + await E(promiseVat).resolveDeferredPromise('resolved value'); + + // The deferred promise should now resolve + const resolvedValue = await deferredPromise; + expect(resolvedValue).toBe('resolved value'); + }); + + it('handles deferred promise rejection', async () => { + const config: ClusterConfig = { + bootstrap: 'promiseVat', + vats: { + promiseVat: { + bundleSpec: 'http://localhost:3000/promise-vat.bundle', + }, + }, + }; + + const result = await kernelHostRoot.launchSubcluster(config); + const promiseVat = result.root as PromiseVat; + + // Get a deferred promise (unresolved) + const deferredPromise = E(promiseVat).makeDeferredPromise(); + + // Reject it + await E(promiseVat).rejectDeferredPromise('error reason'); + + // Rejections from vats are delivered as Error objects + const rejection = await deferredPromise; + expect(rejection).toBeInstanceOf(Error); + expect((rejection as Error).message).toBe('error reason'); + }); + }); + + describe('kref to presence restoration', () => { + it('converts stored kref back to E()-callable presence', async () => { + const config: ClusterConfig = { + bootstrap: 'bob', + vats: { + bob: { + bundleSpec: 'http://localhost:3000/bob-vat.bundle', + }, + }, + }; + + // Launch and get rootKref for storage + const result = await kernelHostRoot.launchSubcluster(config); + const storedKref = result.rootKref; + + // Later: restore presence from kref + const restoredBob = kernelHostRoot.getVatRoot(storedKref) as Bob; + + // The restored presence should be E()-callable + const greeter = await E(restoredBob).makeGreeter('Restored'); + const greeting = await E(greeter).greet('World'); + expect(greeting).toBe('Restored, World!'); + }); + }); +}); diff --git a/packages/nodejs/test/vats/alice-vat.js b/packages/nodejs/test/vats/alice-vat.js new file mode 100644 index 000000000..343cdefcf --- /dev/null +++ b/packages/nodejs/test/vats/alice-vat.js @@ -0,0 +1,42 @@ +import { E } from '@endo/eventual-send'; +import { makeDefaultExo } from '@metamask/kernel-utils/exo'; + +/** + * Build function for Alice's vat. + * Alice orchestrates the third-party handoff between Bob and Carol. + * + * @param {object} vatPowers - Special powers granted to this vat. + * @param {object} vatPowers.logger - The logger object. + * @returns {object} The root object for the new vat. + */ +export function buildRootObject({ logger }) { + return makeDefaultExo('aliceRoot', { + bootstrap() { + logger.log('Alice vat bootstrap'); + }, + + /** + * Orchestrates a third-party handoff by getting an exo from Bob, + * passing it to Carol, and having Carol use it. + * + * @param {object} bob - Reference to Bob's vat root. + * @param {object} carol - Reference to Carol's vat root. + * @param {string} greeting - The greeting for Bob to use. + * @param {string} name - The name for Carol to greet. + * @returns {Promise} The greeting result. + */ + async performHandoff(bob, carol, greeting, name) { + logger.log('Alice starting handoff'); + + // Get exo from Bob + const greeter = await E(bob).makeGreeter(greeting); + logger.log('Alice received greeter from Bob'); + + // Pass to Carol and have her use it + const result = await E(carol).receiveAndGreet(greeter, name); + logger.log(`Alice got result: ${result}`); + + return result; + }, + }); +} diff --git a/packages/nodejs/test/vats/bob-vat.js b/packages/nodejs/test/vats/bob-vat.js new file mode 100644 index 000000000..25dc085c0 --- /dev/null +++ b/packages/nodejs/test/vats/bob-vat.js @@ -0,0 +1,34 @@ +import { makeDefaultExo } from '@metamask/kernel-utils/exo'; + +/** + * Build function for Bob's vat. + * Bob can create greeter exos that can be passed to other vats. + * + * @param {object} vatPowers - Special powers granted to this vat. + * @param {object} vatPowers.logger - The logger object. + * @returns {object} The root object for the new vat. + */ +export function buildRootObject({ logger }) { + return makeDefaultExo('bobRoot', { + bootstrap() { + logger.log('Bob vat bootstrap'); + }, + + /** + * Create a greeter exo that can greet with a custom message. + * This exo can be passed to other vats (third-party handoff). + * + * @param {string} greeting - The greeting prefix to use. + * @returns {object} A greeter exo with a greet method. + */ + makeGreeter(greeting) { + return makeDefaultExo('greeter', { + greet(name) { + const message = `${greeting}, ${name}!`; + logger.log(`Greeter says: ${message}`); + return message; + }, + }); + }, + }); +} diff --git a/packages/nodejs/test/vats/carol-vat.js b/packages/nodejs/test/vats/carol-vat.js new file mode 100644 index 000000000..b97244be7 --- /dev/null +++ b/packages/nodejs/test/vats/carol-vat.js @@ -0,0 +1,60 @@ +import { E } from '@endo/eventual-send'; +import { makeDefaultExo } from '@metamask/kernel-utils/exo'; + +/** + * Build function for Carol's vat. + * Carol can receive exos from other vats and call methods on them. + * + * @param {object} vatPowers - Special powers granted to this vat. + * @param {object} vatPowers.logger - The logger object. + * @returns {object} The root object for the new vat. + */ +export function buildRootObject({ logger }) { + /** @type {object | null} */ + let storedExo = null; + + return makeDefaultExo('carolRoot', { + bootstrap() { + logger.log('Carol vat bootstrap'); + }, + + /** + * Receive an exo and immediately call a method on it. + * This proves the third-party handoff worked. + * + * @param {object} exo - An exo received from another vat. + * @param {string} name - The name to greet. + * @returns {Promise} The greeting from the exo. + */ + receiveAndGreet(exo, name) { + logger.log(`Carol received exo and will greet "${name}"`); + return E(exo).greet(name); + }, + + /** + * Store an exo for later use. + * + * @param {object} exo - An exo to store. + * @returns {string} Confirmation message. + */ + storeExo(exo) { + storedExo = exo; + logger.log('Carol stored exo'); + return 'stored'; + }, + + /** + * Use a previously stored exo to greet. + * + * @param {string} name - The name to greet. + * @returns {Promise} The greeting from the stored exo. + */ + useStoredExo(name) { + if (!storedExo) { + throw new Error('No exo stored'); + } + logger.log(`Carol using stored exo to greet "${name}"`); + return E(storedExo).greet(name); + }, + }); +} diff --git a/packages/nodejs/test/vats/promise-vat.js b/packages/nodejs/test/vats/promise-vat.js new file mode 100644 index 000000000..223f376a4 --- /dev/null +++ b/packages/nodejs/test/vats/promise-vat.js @@ -0,0 +1,124 @@ +import { E } from '@endo/eventual-send'; +import { makeDefaultExo } from '@metamask/kernel-utils/exo'; + +/** + * Build function for a vat that tests promise behaviors. + * This vat provides methods to test kernel promise (kp kref) handling. + * + * @param {object} vatPowers - Special powers granted to this vat. + * @param {object} vatPowers.logger - The logger object. + * @returns {object} The root object for the new vat. + */ +export function buildRootObject({ logger }) { + let deferredResolver = null; + let deferredRejecter = null; + + return makeDefaultExo('promiseRoot', { + bootstrap() { + logger.log('Promise vat bootstrap'); + }, + + /** + * Returns a promise that resolves to a greeter exo. + * + * @param {string} greeting - The greeting prefix to use. + * @returns {Promise} A promise resolving to a greeter exo. + */ + makeGreeter(greeting) { + logger.log(`makeGreeter called with greeting: ${greeting}`); + return makeDefaultExo('greeter', { + greet(name) { + const message = `${greeting}, ${name}!`; + logger.log(`Greeter says: ${message}`); + return message; + }, + }); + }, + + /** + * Makes a deferred promise that can be resolved or rejected + * via separate method calls. + * + * @returns {Promise} An unresolved promise. + */ + makeDeferredPromise() { + logger.log('makeDeferredPromise called'); + return new Promise((resolve, reject) => { + deferredResolver = resolve; + deferredRejecter = reject; + }); + }, + + /** + * Resolves the deferred promise created by makeDeferredPromise. + * + * @param {unknown} value - The value to resolve with. + */ + resolveDeferredPromise(value) { + logger.log(`resolveDeferredPromise called with: ${value}`); + if (deferredResolver) { + deferredResolver(value); + deferredResolver = null; + deferredRejecter = null; + } else { + logger.log('No deferred promise to resolve'); + } + }, + + /** + * Rejects the deferred promise created by makeDeferredPromise. + * + * @param {string} reason - The rejection reason. + */ + rejectDeferredPromise(reason) { + logger.log(`rejectDeferredPromise called with reason: ${reason}`); + if (deferredRejecter) { + deferredRejecter(new Error(reason)); + deferredResolver = null; + deferredRejecter = null; + } else { + logger.log('No deferred promise to reject'); + } + }, + + /** + * Returns a promise that immediately rejects with the given reason. + * + * @param {string} reason - The rejection reason. + * @returns {Promise} A rejecting promise. + */ + getRejectingPromise(reason) { + logger.log(`getRejectingPromise called with reason: ${reason}`); + return Promise.reject(new Error(reason)); + }, + + /** + * Accepts a promise argument and awaits it before returning. + * + * @param {Promise} promiseArg - A promise to await. + * @returns {Promise} A message containing the resolved value. + */ + async awaitPromiseArg(promiseArg) { + logger.log('awaitPromiseArg called, awaiting promise...'); + const result = await promiseArg; + logger.log(`awaitPromiseArg resolved to: ${result}`); + return `received: ${result}`; + }, + + /** + * Gets a deferred promise from another vat and awaits it. + * This tests cross-vat kernel promise handling. + * + * @param {object} promiserVat - A reference to another promise-vat. + * @returns {Promise} A message containing the resolved value. + */ + async awaitDeferredFromVat(promiserVat) { + logger.log('awaitDeferredFromVat called, getting deferred promise...'); + const deferredPromise = E(promiserVat).makeDeferredPromise(); + logger.log('Got deferred promise, awaiting...'); + const result = await deferredPromise; + logger.log(`Deferred promise resolved to: ${result}`); + return `received: ${result}`; + }, + }); +} From b78bc0daeaef5b47914a031668382836872e85c2 Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Wed, 28 Jan 2026 17:54:32 -0800 Subject: [PATCH 16/41] refactor(ocap-kernel): Use transport-based system vat architecture Replace direct supervisor instantiation with pluggable transport pattern: - Add SystemVatTransport interface with deliver/setSyscallHandler - Runtime creates supervisors, kernel connects via transports - Pass systemSubclusters config to Kernel.make() - Singleton kernel facet shared across system subclusters - Remove KernelHostRoot wrapper, expose KernelFacet directly This enables cross-process system vats (e.g., extension background) where kernel and supervisor run in different processes. Co-Authored-By: Claude --- .../nodejs/src/host-subcluster/index.test.ts | 204 ++++++------- packages/nodejs/src/host-subcluster/index.ts | 273 ++++++++--------- packages/nodejs/src/index.ts | 10 +- .../nodejs/test/e2e/system-subcluster.test.ts | 79 +++-- packages/ocap-kernel/package.json | 10 + packages/ocap-kernel/src/Kernel.ts | 53 ++-- packages/ocap-kernel/src/index.ts | 10 +- packages/ocap-kernel/src/types.ts | 72 +++++ .../src/vats/SystemSubclusterManager.test.ts | 274 ++++++++++-------- .../src/vats/SystemSubclusterManager.ts | 180 ++++-------- .../src/vats/SystemVatHandle.test.ts | 54 +--- .../ocap-kernel/src/vats/SystemVatHandle.ts | 46 +-- .../src/vats/SystemVatSupervisor.test.ts | 19 +- .../src/vats/SystemVatSupervisor.ts | 66 ++++- packages/ocap-kernel/src/vats/index.ts | 11 + 15 files changed, 668 insertions(+), 693 deletions(-) create mode 100644 packages/ocap-kernel/src/vats/index.ts diff --git a/packages/nodejs/src/host-subcluster/index.test.ts b/packages/nodejs/src/host-subcluster/index.test.ts index 27c103e1c..2fbb539f4 100644 --- a/packages/nodejs/src/host-subcluster/index.test.ts +++ b/packages/nodejs/src/host-subcluster/index.test.ts @@ -1,152 +1,114 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; -import type { KernelHostRoot } from './index.ts'; -import { makeKernelHostSubclusterConfig, makeHostSubcluster } from './index.ts'; - -describe('makeKernelHostSubclusterConfig', () => { - const mockKernelFacet = { - launchSubcluster: vi.fn(), - terminateSubcluster: vi.fn(), - getStatus: vi.fn(), - reloadSubcluster: vi.fn(), - getSubcluster: vi.fn(), - getSubclusters: vi.fn(), +import { makeHostSubcluster } from './index.ts'; + +// Mock SystemVatSupervisor +const mockStart = vi.fn(); +const mockDeliver = vi.fn(); + +vi.mock('@metamask/ocap-kernel/vats', () => { + return { + SystemVatSupervisor: class MockSystemVatSupervisor { + start = mockStart; + + deliver = mockDeliver; + }, + makeSyscallHandlerHolder: vi.fn(() => ({ handler: null })), }; +}); +describe('makeHostSubcluster', () => { beforeEach(() => { vi.clearAllMocks(); + mockStart.mockResolvedValue(null); + mockDeliver.mockResolvedValue(null); }); - it('returns a valid system subcluster config', () => { - const onRootCreated = vi.fn(); - const config = makeKernelHostSubclusterConfig(onRootCreated); + it('returns config, start, and getKernelFacet', () => { + const result = makeHostSubcluster(); - expect(config.bootstrap).toBe('kernelHost'); - expect(config.vats.kernelHost).toBeDefined(); - expect(config.vats?.kernelHost?.buildRootObject).toBeTypeOf('function'); + expect(result.config).toBeDefined(); + expect(result.start).toBeTypeOf('function'); + expect(result.getKernelFacet).toBeTypeOf('function'); }); - it('invokes onRootCreated callback when buildRootObject is called', () => { - const onRootCreated = vi.fn(); - const config = makeKernelHostSubclusterConfig(onRootCreated); + describe('config', () => { + it('has kernelHost as bootstrap vat', () => { + const { config } = makeHostSubcluster(); - const root = config.vats?.kernelHost?.buildRootObject( - { - kernelFacet: mockKernelFacet, - }, - {}, - ); + expect(config.bootstrap).toBe('kernelHost'); + }); - expect(onRootCreated).toHaveBeenCalledWith(root); - }); + it('has vatTransports with kernelHost transport', () => { + const { config } = makeHostSubcluster(); - describe('kernel host root', () => { - let root: KernelHostRoot; - - beforeEach(() => { - const onRootCreated = vi.fn(); - const config = makeKernelHostSubclusterConfig(onRootCreated); - root = config.vats?.kernelHost?.buildRootObject( - { - kernelFacet: mockKernelFacet, - }, - {}, - ) as KernelHostRoot; + expect(config.vatTransports).toHaveLength(1); + expect(config.vatTransports[0]?.name).toBe('kernelHost'); + expect(config.vatTransports[0]?.transport).toBeDefined(); + expect(config.vatTransports[0]?.transport.deliver).toBeTypeOf('function'); + expect(config.vatTransports[0]?.transport.setSyscallHandler).toBeTypeOf( + 'function', + ); }); + }); - it('creates root with expected methods', () => { - expect(root.ping).toBeTypeOf('function'); - expect(root.launchSubcluster).toBeTypeOf('function'); - expect(root.terminateSubcluster).toBeTypeOf('function'); - expect(root.getStatus).toBeTypeOf('function'); - expect(root.reloadSubcluster).toBeTypeOf('function'); - expect(root.getSubcluster).toBeTypeOf('function'); - expect(root.getSubclusters).toBeTypeOf('function'); - }); + describe('start', () => { + it('creates and starts the supervisor', async () => { + const { start } = makeHostSubcluster(); - it('ping returns pong', async () => { - const result = await root.ping(); - expect(result).toBe('pong'); - }); + await start(); - // Note: launchSubcluster, terminateSubcluster, getStatus, reloadSubcluster - // use E() which requires endo initialization. These are integration tested - // via the full system tests rather than unit tests. + expect(mockStart).toHaveBeenCalled(); + }); - it('getSubcluster calls kernel facet synchronously', () => { - mockKernelFacet.getSubcluster.mockReturnValue({ - id: 's1', - config: { bootstrap: 'test', vats: {} }, - vats: {}, - }); + it('throws if supervisor start returns error', async () => { + mockStart.mockResolvedValueOnce('Some error'); - const result = root.getSubcluster('s1'); + const { start } = makeHostSubcluster(); - expect(mockKernelFacet.getSubcluster).toHaveBeenCalledWith('s1'); - expect(result?.id).toBe('s1'); + await expect(start()).rejects.toThrow( + 'Failed to start host subcluster supervisor: Some error', + ); }); + }); - it('getSubclusters calls kernel facet synchronously', () => { - mockKernelFacet.getSubclusters.mockReturnValue([ - { id: 's1', config: { bootstrap: 'test', vats: {} }, vats: {} }, - ]); - - const result = root.getSubclusters(); + describe('getKernelFacet', () => { + it('throws if called before kernel facet is available', () => { + const { getKernelFacet } = makeHostSubcluster(); - expect(mockKernelFacet.getSubclusters).toHaveBeenCalled(); - expect(result).toHaveLength(1); + expect(() => getKernelFacet()).toThrow( + 'Kernel facet not available. Ensure start() was called and kernel has bootstrapped.', + ); }); }); -}); -describe('makeHostSubcluster', () => { - it('launches system subcluster and returns result', async () => { - const mockKernel = { - launchSystemSubcluster: vi.fn(async (config) => { - // Simulate the kernel calling buildRootObject - const mockKernelFacet = { - launchSubcluster: vi.fn(), - terminateSubcluster: vi.fn(), - getStatus: vi.fn(), - reloadSubcluster: vi.fn(), - getSubcluster: vi.fn(), - getSubclusters: vi.fn(), - }; - config.vats?.kernelHost?.buildRootObject( - { kernelFacet: mockKernelFacet }, - {}, - ); - return { systemSubclusterId: 'ss0', vatIds: { kernelHost: 'sv0' } }; - }), - }; - - const result = await makeHostSubcluster(mockKernel as never); - - expect(mockKernel.launchSystemSubcluster).toHaveBeenCalledWith( - expect.objectContaining({ - bootstrap: 'kernelHost', - vats: expect.objectContaining({ - kernelHost: expect.objectContaining({ - buildRootObject: expect.any(Function), - }), + describe('transport', () => { + it('deliver throws if supervisor not initialized', async () => { + const { config } = makeHostSubcluster(); + + await expect( + config.vatTransports[0]?.transport.deliver({ + type: 'message', + methargs: { body: '[]', slots: [] }, + result: 'p-1', + target: 'o+0', }), - }), - ); - expect(result.systemSubclusterId).toBe('ss0'); - expect(result.kernelHostRoot).toBeDefined(); - expect(result.kernelHostRoot.ping).toBeTypeOf('function'); - }); + ).rejects.toThrow('Supervisor not initialized'); + }); + + it('deliver calls supervisor after start', async () => { + const { config, start } = makeHostSubcluster(); + await start(); - it('throws if root object not captured', async () => { - const mockKernel = { - launchSystemSubcluster: vi.fn(async () => ({ - systemSubclusterId: 'ss0', - vatIds: { kernelHost: 'sv0' }, - })), - }; - - await expect(makeHostSubcluster(mockKernel as never)).rejects.toThrow( - 'Failed to capture kernel host root object', - ); + const delivery = { + type: 'message' as const, + methargs: { body: '[]', slots: [] }, + result: 'p-1', + target: 'o+0', + }; + await config.vatTransports[0]?.transport.deliver(delivery); + + expect(mockDeliver).toHaveBeenCalledWith(delivery); + }); }); }); diff --git a/packages/nodejs/src/host-subcluster/index.ts b/packages/nodejs/src/host-subcluster/index.ts index c5a3b0e5d..eaa2b8807 100644 --- a/packages/nodejs/src/host-subcluster/index.ts +++ b/packages/nodejs/src/host-subcluster/index.ts @@ -1,191 +1,154 @@ -import { E } from '@endo/eventual-send'; import { makeDefaultExo } from '@metamask/kernel-utils/exo'; +import { Logger } from '@metamask/logger'; import type { SystemVatBuildRootObject, - SystemSubclusterConfig, - ClusterConfig, - KernelStatus, - Subcluster, KernelFacet, - KernelFacetLaunchResult, - Kernel, + KernelSystemSubclusterConfig, + SystemVatTransport, + SystemVatSyscallHandler, + SystemVatDeliverFn, } from '@metamask/ocap-kernel'; +import { + SystemVatSupervisor, + makeSyscallHandlerHolder, +} from '@metamask/ocap-kernel/vats'; /** - * The kernel host vat's root object interface. - * - * This is the interface exposed by the kernel host vat to the host application. + * Result of creating a host subcluster. */ -export type KernelHostRoot = { - /** - * Ping the kernel host. - * - * @returns 'pong' to confirm the host is responsive. - */ - ping: () => Promise<'pong'>; - - /** - * Launch a dynamic subcluster. - * - * @param config - Configuration for the subcluster. - * @returns The launch result with subcluster ID and root presence. - */ - launchSubcluster: (config: ClusterConfig) => Promise; - - /** - * Terminate a subcluster. - * - * @param subclusterId - The ID of the subcluster to terminate. - */ - terminateSubcluster: (subclusterId: string) => Promise; - - /** - * Get kernel status. - * - * @returns The current kernel status. - */ - getStatus: () => Promise; - - /** - * Reload a subcluster. - * - * @param subclusterId - The ID of the subcluster to reload. - * @returns The reloaded subcluster. - */ - reloadSubcluster: (subclusterId: string) => Promise; - +export type HostSubclusterResult = { /** - * Get a subcluster by ID. - * - * @param subclusterId - The ID of the subcluster. - * @returns The subcluster or undefined if not found. + * Configuration to pass to Kernel.make() systemSubclusters option. */ - getSubcluster: (subclusterId: string) => Subcluster | undefined; + config: KernelSystemSubclusterConfig; /** - * Get all subclusters. + * Start the supervisor. Call after Kernel.make() returns. * - * @returns Array of all subclusters. + * @returns A promise that resolves when the supervisor is started. */ - getSubclusters: () => Subcluster[]; + start: () => Promise; /** - * Convert a kref string to a presence. + * Get the kernel facet (available after bootstrap is called by kernel). * - * Use this to restore a presence from a stored kref string after restart. - * - * @param kref - The kref string to convert. - * @returns The presence for the given kref. + * @returns The kernel facet presence for making E() calls. */ - getVatRoot: (kref: string) => unknown; + getKernelFacet: () => KernelFacet; }; /** - * Create the configuration for launching the kernel host subcluster. + * Create a host subcluster for use with Kernel.make(). + * + * This creates the supervisor and transport configuration needed to connect + * a host subcluster to the kernel. The supervisor is created in this process, + * and the transport allows the kernel to communicate with it. * - * @param onRootCreated - Callback invoked when the root object is created. - * @returns The system subcluster configuration. + * Usage: + * ```typescript + * const hostSubcluster = makeHostSubcluster({ logger }); + * const kernel = await Kernel.make(platformServices, db, { + * systemSubclusters: { subclusters: [hostSubcluster.config] }, + * }); + * await hostSubcluster.start(); + * const kernelFacet = hostSubcluster.getKernelFacet(); + * const result = await E(kernelFacet).launchSubcluster(config); + * ``` + * + * @param options - Options for creating the host subcluster. + * @param options.logger - Optional logger for the supervisor. + * @returns The host subcluster result with config and initialization functions. */ -export function makeKernelHostSubclusterConfig( - onRootCreated: (root: KernelHostRoot) => void, -): SystemSubclusterConfig { - const buildRootObject: SystemVatBuildRootObject = (vatPowers) => { - const kernelFacet = vatPowers.kernelFacet as KernelFacet; - - const root = makeDefaultExo('KernelHostRoot', { - ping: async () => 'pong' as const, - - launchSubcluster: async (config: ClusterConfig) => { - // Use E() to call kernel facet - this gives us proper reference handling - return E(kernelFacet).launchSubcluster(config); - }, - - terminateSubcluster: async (subclusterId: string) => { - return E(kernelFacet).terminateSubcluster(subclusterId); - }, - - getStatus: async () => { - return E(kernelFacet).getStatus(); +export function makeHostSubcluster( + options: { + logger?: Logger; + } = {}, +): HostSubclusterResult { + const logger = options.logger ?? new Logger('host-subcluster'); + const vatName = 'kernelHost'; + + // Captured kernel facet from bootstrap message + let capturedKernelFacet: KernelFacet | null = null; + + // Create syscall handler holder for deferred wiring + const syscallHandlerHolder = makeSyscallHandlerHolder(); + + // Build root object that receives kernelFacet via bootstrap message + const buildRootObject: SystemVatBuildRootObject = () => { + return makeDefaultExo('KernelHostRoot', { + // Bootstrap is called by the kernel with kernelFacet as a presence + bootstrap: ( + _roots: Record, + _services: Record, + kernelFacet: KernelFacet, + ) => { + capturedKernelFacet = kernelFacet; }, + }); + }; - reloadSubcluster: async (subclusterId: string) => { - return E(kernelFacet).reloadSubcluster(subclusterId); - }, - - getSubcluster: (subclusterId: string) => { - // Synchronous method - call directly - return kernelFacet.getSubcluster(subclusterId); - }, - - getSubclusters: () => { - // Synchronous method - call directly - return kernelFacet.getSubclusters(); - }, - - getVatRoot: async (kref: string) => { - // Convert kref to slot value, which becomes a presence via liveslots - return kernelFacet.getVatRoot(kref); - }, - }) as KernelHostRoot; - - // Capture the root object for external use - onRootCreated(root); + // Create the supervisor + let supervisor: SystemVatSupervisor | null = null; - return root; + // Create the transport + const deliver: SystemVatDeliverFn = async (delivery) => { + if (!supervisor) { + throw new Error('Supervisor not initialized'); + } + return supervisor.deliver(delivery); }; - return { - bootstrap: 'kernelHost', - vats: { - kernelHost: { buildRootObject }, + const transport: SystemVatTransport = { + deliver, + setSyscallHandler: (handler: SystemVatSyscallHandler) => { + syscallHandlerHolder.handler = handler; }, }; -} -harden(makeKernelHostSubclusterConfig); - -/** - * Result of launching the host subcluster. - */ -export type HostSubclusterResult = { - /** - * The system subcluster ID. - */ - systemSubclusterId: string; - - /** - * The kernel host root object for interacting with the kernel. - */ - kernelHostRoot: KernelHostRoot; -}; - -/** - * Launch the host subcluster on a kernel. - * - * This creates a system subcluster with a kernel host vat that provides - * privileged kernel operations. The returned root object can be used directly - * to interact with the kernel (e.g., launch subclusters, get status). - * - * @param kernel - The kernel instance to launch the host subcluster on. - * @returns The host subcluster result with the system subcluster ID and root object. - */ -export async function makeHostSubcluster( - kernel: Kernel, -): Promise { - let kernelHostRoot: KernelHostRoot | undefined; - const hostSubclusterConfig = makeKernelHostSubclusterConfig((root) => { - kernelHostRoot = root; - }); - - const result = await kernel.launchSystemSubcluster(hostSubclusterConfig); - - if (!kernelHostRoot) { - throw new Error('Failed to capture kernel host root object'); - } + // Config for Kernel.make() + const config: KernelSystemSubclusterConfig = { + bootstrap: vatName, + vatTransports: [ + { + name: vatName, + transport, + }, + ], + }; return harden({ - systemSubclusterId: result.systemSubclusterId, - kernelHostRoot, + config, + + start: async () => { + // Create the supervisor + supervisor = new SystemVatSupervisor({ + // The kernel assigns the actual ID via the transport + // This placeholder is only used for logging + id: 'sv0' as `sv${number}`, + buildRootObject, + vatPowers: {}, + parameters: undefined, + syscallHandlerHolder, + logger: logger.subLogger({ tags: ['supervisor'] }), + }); + + // Start the supervisor (dispatches startVat) + const startError = await supervisor.start(); + if (startError) { + throw new Error( + `Failed to start host subcluster supervisor: ${startError}`, + ); + } + }, + + getKernelFacet: () => { + if (!capturedKernelFacet) { + throw new Error( + 'Kernel facet not available. Ensure start() was called and kernel has bootstrapped.', + ); + } + return capturedKernelFacet; + }, }); } harden(makeHostSubcluster); diff --git a/packages/nodejs/src/index.ts b/packages/nodejs/src/index.ts index fa7d8400c..9a500d763 100644 --- a/packages/nodejs/src/index.ts +++ b/packages/nodejs/src/index.ts @@ -1,11 +1,5 @@ export { NodejsPlatformServices } from './kernel/PlatformServices.ts'; export { makeKernel } from './kernel/make-kernel.ts'; export { makeNodeJsVatSupervisor } from './vat/make-supervisor.ts'; -export { - makeHostSubcluster, - makeKernelHostSubclusterConfig, -} from './host-subcluster/index.ts'; -export type { - KernelHostRoot, - HostSubclusterResult, -} from './host-subcluster/index.ts'; +export { makeHostSubcluster } from './host-subcluster/index.ts'; +export type { HostSubclusterResult } from './host-subcluster/index.ts'; diff --git a/packages/nodejs/test/e2e/system-subcluster.test.ts b/packages/nodejs/test/e2e/system-subcluster.test.ts index d43d874db..e0d6265bc 100644 --- a/packages/nodejs/test/e2e/system-subcluster.test.ts +++ b/packages/nodejs/test/e2e/system-subcluster.test.ts @@ -1,10 +1,11 @@ import { E } from '@endo/eventual-send'; -import type { Kernel, ClusterConfig } from '@metamask/ocap-kernel'; +import { makeSQLKernelDatabase } from '@metamask/kernel-store/sqlite/nodejs'; +import { Logger } from '@metamask/logger'; +import type { Kernel, ClusterConfig, KernelFacet } from '@metamask/ocap-kernel'; import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { makeHostSubcluster } from '../../src/host-subcluster/index.ts'; -import type { KernelHostRoot } from '../../src/host-subcluster/index.ts'; -import { makeKernel } from '../../src/kernel/make-kernel.ts'; +import { NodejsPlatformServices } from '../../src/kernel/PlatformServices.ts'; type Bob = { makeGreeter: (greeting: string) => Promise; @@ -27,12 +28,37 @@ type PromiseVat = { describe('system subcluster e2e tests', { timeout: 30_000 }, () => { let kernel: Kernel; - let kernelHostRoot: KernelHostRoot; + let kernelFacet: KernelFacet; + let hostSubcluster: ReturnType; beforeEach(async () => { - kernel = await makeKernel({}); - const hostResult = await makeHostSubcluster(kernel); - kernelHostRoot = hostResult.kernelHostRoot; + const logger = new Logger('test'); + + // Create host subcluster first + hostSubcluster = makeHostSubcluster({ logger }); + + // Create kernel with system subcluster config + const platformServices = new NodejsPlatformServices({ + logger: logger.subLogger({ tags: ['platform-services'] }), + }); + const kernelDatabase = await makeSQLKernelDatabase({}); + + // Import Kernel dynamically to avoid circular deps + const { Kernel: KernelClass } = await import('@metamask/ocap-kernel'); + kernel = await KernelClass.make(platformServices, kernelDatabase, { + resetStorage: true, + logger: logger.subLogger({ tags: ['kernel'] }), + systemSubclusters: { subclusters: [hostSubcluster.config] }, + }); + + // Start host subcluster supervisor after kernel is created + await hostSubcluster.start(); + + // Run the kernel to process bootstrap message + await kernel.run(); + + // Get the kernel facet + kernelFacet = hostSubcluster.getKernelFacet(); }); afterEach(async () => { @@ -40,13 +66,8 @@ describe('system subcluster e2e tests', { timeout: 30_000 }, () => { }); describe('basic operations', () => { - it('pings the kernel host', async () => { - const result = await kernelHostRoot.ping(); - expect(result).toBe('pong'); - }); - it('gets kernel status', async () => { - const status = await kernelHostRoot.getStatus(); + const status = await E(kernelFacet).getStatus(); expect(status).toBeDefined(); expect(status.vats).toBeDefined(); expect(status.subclusters).toBeDefined(); @@ -62,7 +83,7 @@ describe('system subcluster e2e tests', { timeout: 30_000 }, () => { }, }; - const result = await kernelHostRoot.launchSubcluster(config); + const result = await E(kernelFacet).launchSubcluster(config); expect(result.subclusterId).toBeDefined(); expect(result.root).toBeDefined(); @@ -84,13 +105,13 @@ describe('system subcluster e2e tests', { timeout: 30_000 }, () => { }, }; - const result = await kernelHostRoot.launchSubcluster(config); - const subcluster = kernelHostRoot.getSubcluster(result.subclusterId); + const result = await E(kernelFacet).launchSubcluster(config); + const subcluster = kernelFacet.getSubcluster(result.subclusterId); expect(subcluster).toBeDefined(); - await kernelHostRoot.terminateSubcluster(result.subclusterId); + await E(kernelFacet).terminateSubcluster(result.subclusterId); - const terminatedSubcluster = kernelHostRoot.getSubcluster( + const terminatedSubcluster = kernelFacet.getSubcluster( result.subclusterId, ); expect(terminatedSubcluster).toBeUndefined(); @@ -112,20 +133,18 @@ describe('system subcluster e2e tests', { timeout: 30_000 }, () => { }, }; - const launchResult = await kernelHostRoot.launchSubcluster(config); + const launchResult = await E(kernelFacet).launchSubcluster(config); const bob = launchResult.root as Bob; // Get Carol's root object - const subcluster = kernelHostRoot.getSubcluster( - launchResult.subclusterId, - ); + const subcluster = kernelFacet.getSubcluster(launchResult.subclusterId); expect(subcluster).toBeDefined(); const carolVatId = subcluster?.vats.find( (vatId) => vatId !== subcluster.vats[0], ); expect(carolVatId).toBeDefined(); const carolKref = kernel.pinVatRoot(carolVatId!); - const carol = kernelHostRoot.getVatRoot(carolKref) as Carol; + const carol = kernelFacet.getVatRoot(carolKref) as Carol; // Host orchestrates: get exo from Bob, pass to Carol const greeter = await E(bob).makeGreeter('Greetings'); @@ -144,7 +163,7 @@ describe('system subcluster e2e tests', { timeout: 30_000 }, () => { }, }, }; - const bobResult = await kernelHostRoot.launchSubcluster(bobConfig); + const bobResult = await E(kernelFacet).launchSubcluster(bobConfig); const bob = bobResult.root as Bob; // Launch Carol in another subcluster @@ -156,7 +175,7 @@ describe('system subcluster e2e tests', { timeout: 30_000 }, () => { }, }, }; - const carolResult = await kernelHostRoot.launchSubcluster(carolConfig); + const carolResult = await E(kernelFacet).launchSubcluster(carolConfig); const carol = carolResult.root as Carol; // Host orchestrates cross-subcluster handoff @@ -178,7 +197,7 @@ describe('system subcluster e2e tests', { timeout: 30_000 }, () => { }, }; - const result = await kernelHostRoot.launchSubcluster(config); + const result = await E(kernelFacet).launchSubcluster(config); const promiseVat = result.root as PromiseVat; // Get a promise for an exo (without awaiting) @@ -202,7 +221,7 @@ describe('system subcluster e2e tests', { timeout: 30_000 }, () => { }, }; - const result = await kernelHostRoot.launchSubcluster(config); + const result = await E(kernelFacet).launchSubcluster(config); const promiseVat = result.root as PromiseVat; // Get a deferred promise (unresolved) @@ -226,7 +245,7 @@ describe('system subcluster e2e tests', { timeout: 30_000 }, () => { }, }; - const result = await kernelHostRoot.launchSubcluster(config); + const result = await E(kernelFacet).launchSubcluster(config); const promiseVat = result.root as PromiseVat; // Get a deferred promise (unresolved) @@ -254,11 +273,11 @@ describe('system subcluster e2e tests', { timeout: 30_000 }, () => { }; // Launch and get rootKref for storage - const result = await kernelHostRoot.launchSubcluster(config); + const result = await E(kernelFacet).launchSubcluster(config); const storedKref = result.rootKref; // Later: restore presence from kref - const restoredBob = kernelHostRoot.getVatRoot(storedKref) as Bob; + const restoredBob = kernelFacet.getVatRoot(storedKref) as Bob; // The restored presence should be E()-callable const greeter = await E(restoredBob).makeGreeter('Restored'); diff --git a/packages/ocap-kernel/package.json b/packages/ocap-kernel/package.json index 0e12041ae..dbcb8ea6a 100644 --- a/packages/ocap-kernel/package.json +++ b/packages/ocap-kernel/package.json @@ -39,6 +39,16 @@ "default": "./dist/rpc/index.cjs" } }, + "./vats": { + "import": { + "types": "./dist/vats/index.d.mts", + "default": "./dist/vats/index.mjs" + }, + "require": { + "types": "./dist/vats/index.d.cts", + "default": "./dist/vats/index.cjs" + } + }, "./package.json": "./package.json" }, "main": "./dist/index.cjs", diff --git a/packages/ocap-kernel/src/Kernel.ts b/packages/ocap-kernel/src/Kernel.ts index 6bcef262a..daa24602c 100644 --- a/packages/ocap-kernel/src/Kernel.ts +++ b/packages/ocap-kernel/src/Kernel.ts @@ -19,12 +19,11 @@ import type { KRef, PlatformServices, ClusterConfig, - SystemSubclusterConfig, + KernelSystemSubclustersConfig, VatConfig, KernelStatus, Subcluster, SubclusterLaunchResult, - SystemSubclusterLaunchResult, EndpointHandle, } from './types.ts'; import { isVatId, isRemoteId, isSystemVatId } from './types.ts'; @@ -83,6 +82,12 @@ export class Kernel { /** The kernel's router */ readonly #kernelRouter: KernelRouter; + /** + * System subclusters configuration passed to Kernel.make(). + * Stored for connection after initialization. + */ + readonly #systemSubclustersConfig: KernelSystemSubclustersConfig | undefined; + /** * Construct a new kernel instance. * @@ -93,6 +98,7 @@ export class Kernel { * @param options.logger - Optional logger for error and diagnostic output. * @param options.keySeed - Optional seed for libp2p key generation. * @param options.mnemonic - Optional BIP39 mnemonic for deriving the kernel identity. + * @param options.systemSubclusters - Optional system subclusters to connect at kernel creation. */ // eslint-disable-next-line no-restricted-syntax private constructor( @@ -103,11 +109,13 @@ export class Kernel { logger?: Logger; keySeed?: string | undefined; mnemonic?: string | undefined; + systemSubclusters?: KernelSystemSubclustersConfig; } = {}, ) { this.#platformServices = platformServices; this.#logger = options.logger ?? new Logger('ocap-kernel'); this.#kernelStore = makeKernelStore(kernelDatabase, this.#logger); + this.#systemSubclustersConfig = options.systemSubclusters; if (!this.#kernelStore.kv.get('initialized')) { this.#kernelStore.kv.set('initialized', 'true'); } @@ -212,6 +220,7 @@ export class Kernel { * @param options.logger - Optional logger for error and diagnostic output. * @param options.keySeed - Optional seed for libp2p key generation. * @param options.mnemonic - Optional BIP39 mnemonic for deriving the kernel identity. + * @param options.systemSubclusters - Optional system subclusters to connect at kernel creation. * @returns A promise for the new kernel instance. */ static async make( @@ -222,6 +231,7 @@ export class Kernel { logger?: Logger; keySeed?: string | undefined; mnemonic?: string | undefined; + systemSubclusters?: KernelSystemSubclustersConfig; } = {}, ): Promise { const kernel = new Kernel(platformServices, kernelDatabase, options); @@ -243,6 +253,16 @@ export class Kernel { // This ensures that any messages in the queue have their target vats ready await this.#vatManager.initializeAllVats(); + // Connect system subclusters if configured + if (this.#systemSubclustersConfig) { + const { subclusters } = this.#systemSubclustersConfig; + for (const subclusterConfig of subclusters) { + await this.#systemSubclusterManager.connectSystemSubcluster( + subclusterConfig, + ); + } + } + // Start the kernel queue processing (non-blocking) // This runs for the entire lifetime of the kernel this.#kernelQueue @@ -327,35 +347,6 @@ export class Kernel { return this.#subclusterManager.launchSubcluster(config); } - /** - * Launches a system subcluster. - * - * System subclusters contain vats that run without compartment isolation - * directly in the host process. The bootstrap vat receives a kernel facet - * as a vatpower, enabling it to launch dynamic subclusters and receive - * E()-callable presences. - * - * @param config - Configuration for the system subcluster. - * @returns A promise for the launch result containing system subcluster ID and vat IDs. - */ - async launchSystemSubcluster( - config: SystemSubclusterConfig, - ): Promise { - return this.#systemSubclusterManager.launchSystemSubcluster(config); - } - - /** - * Terminates a system subcluster. - * - * @param systemSubclusterId - The ID of the system subcluster to terminate. - * @returns A promise that resolves when termination is complete. - */ - async terminateSystemSubcluster(systemSubclusterId: string): Promise { - return this.#systemSubclusterManager.terminateSystemSubcluster( - systemSubclusterId, - ); - } - /** * Terminates a named sub-cluster of vats. * diff --git a/packages/ocap-kernel/src/index.ts b/packages/ocap-kernel/src/index.ts index 689e64455..eda1fcc8f 100644 --- a/packages/ocap-kernel/src/index.ts +++ b/packages/ocap-kernel/src/index.ts @@ -16,10 +16,14 @@ export type { // System vat types SystemVatId, SystemSubclusterId, - SystemSubclusterConfig, - SystemVatConfig, SystemVatBuildRootObject, - SystemSubclusterLaunchResult, + // System vat transport types (for Kernel.make()) + SystemVatTransport, + SystemVatSyscallHandler, + SystemVatDeliverFn, + SystemVatConnectionConfig, + KernelSystemSubclusterConfig, + KernelSystemSubclustersConfig, } from './types.ts'; export type { RemoteMessageHandler, diff --git a/packages/ocap-kernel/src/types.ts b/packages/ocap-kernel/src/types.ts index 910b605f9..bdfbe6224 100644 --- a/packages/ocap-kernel/src/types.ts +++ b/packages/ocap-kernel/src/types.ts @@ -2,8 +2,10 @@ import type { SwingSetCapData, Message as SwingsetMessage, VatSyscallObject, + VatSyscallResult, VatSyscallSend, VatOneResolution, + VatDeliveryObject, } from '@agoric/swingset-liveslots'; import type { CapData } from '@endo/marshal'; import type { VatCheckpoint } from '@metamask/kernel-store'; @@ -442,6 +444,7 @@ export type SystemVatBuildRootObject = ( /** * Configuration for a single system vat within a system subcluster. + * Used when launching system subclusters via Kernel.launchSystemSubcluster(). */ export type SystemVatConfig = { buildRootObject: SystemVatBuildRootObject; @@ -452,6 +455,8 @@ export type SystemVatConfig = { * Configuration for launching a system subcluster. * System subclusters contain vats that run without compartment isolation * directly in the host process. + * + * Used when launching system subclusters via Kernel.launchSystemSubcluster(). */ export type SystemSubclusterConfig = { /** The name of the bootstrap vat within the subcluster. */ @@ -472,6 +477,73 @@ export type SystemSubclusterLaunchResult = { vatIds: Record; }; +// ============================================================================ +// System Vat Transport Types (for Kernel.make() static configuration) +// ============================================================================ + +/** + * Syscall handler from system vat to kernel. + * The kernel provides this to the transport so syscalls can be routed correctly. + */ +export type SystemVatSyscallHandler = ( + syscall: VatSyscallObject, +) => VatSyscallResult; + +/** + * Deliver function from kernel to system vat. + * The runtime provides this to the kernel so deliveries can be routed correctly. + */ +export type SystemVatDeliverFn = ( + delivery: VatDeliveryObject, +) => Promise; + +/** + * Transport interface bridging kernel and system vat processes. + * + * The transport abstracts the communication channel between the kernel (which + * creates SystemVatHandle) and the system vat supervisor (which runs in the + * runtime's process). This allows: + * - Node.js: direct function calls (same process) + * - Extension: MessagePort IPC (cross-process) + */ +export type SystemVatTransport = { + /** Send deliveries from kernel to system vat. */ + deliver: SystemVatDeliverFn; + /** Register syscall handler (kernel calls this to wire up). */ + setSyscallHandler: (handler: SystemVatSyscallHandler) => void; +}; + +/** + * Configuration for a single system vat with transport (for Kernel.make). + * The runtime creates the supervisor and provides the transport. + */ +export type SystemVatConnectionConfig = { + /** Vat name (matches key in subcluster config). */ + name: string; + /** Transport callbacks for communication. */ + transport: SystemVatTransport; +}; + +/** + * System subcluster configuration for Kernel.make(). + * Used to connect to pre-existing system vat supervisors at kernel creation time. + */ +export type KernelSystemSubclusterConfig = { + /** Name of the bootstrap vat. */ + bootstrap: string; + /** Transport connections for each vat. */ + vatTransports: SystemVatConnectionConfig[]; + /** Kernel services to provide to bootstrap vat. */ + services?: string[]; +}; + +/** + * System subclusters configuration for Kernel.make(). + */ +export type KernelSystemSubclustersConfig = { + subclusters: KernelSystemSubclusterConfig[]; +}; + export const SubclusterStruct = object({ id: SubclusterIdStruct, config: ClusterConfigStruct, diff --git a/packages/ocap-kernel/src/vats/SystemSubclusterManager.test.ts b/packages/ocap-kernel/src/vats/SystemSubclusterManager.test.ts index fdf4dd671..a0ef9c81b 100644 --- a/packages/ocap-kernel/src/vats/SystemSubclusterManager.test.ts +++ b/packages/ocap-kernel/src/vats/SystemSubclusterManager.test.ts @@ -6,20 +6,12 @@ import type { KernelFacetDependencies } from '../kernel-facet.ts'; import type { KernelQueue } from '../KernelQueue.ts'; import type { KernelStore } from '../store/index.ts'; import type { - SystemSubclusterConfig, - SystemVatBuildRootObject, + KernelSystemSubclusterConfig, SystemVatId, + SystemVatTransport, } from '../types.ts'; import { SystemSubclusterManager } from './SystemSubclusterManager.ts'; -// Mock liveslots -const mockDispatch = vi.fn(); -vi.mock('@agoric/swingset-liveslots', () => ({ - makeLiveSlots: vi.fn(() => ({ - dispatch: mockDispatch, - })), -})); - describe('SystemSubclusterManager', () => { let kernelStore: KernelStore; let kernelQueue: KernelQueue; @@ -27,14 +19,20 @@ describe('SystemSubclusterManager', () => { let logger: Logger; let manager: SystemSubclusterManager; - const buildRootObject: SystemVatBuildRootObject = vi.fn(() => ({ - bootstrap: vi.fn(), - test: () => 'test', - })); + /** + * Creates a mock transport for testing. + * + * @returns A mock transport with vi.fn() implementations. + */ + function makeMockTransport(): SystemVatTransport { + return { + deliver: vi.fn().mockResolvedValue(null), + setSyscallHandler: vi.fn(), + }; + } beforeEach(() => { vi.clearAllMocks(); - mockDispatch.mockResolvedValue(undefined); kernelStore = { initEndpoint: vi.fn(), @@ -90,67 +88,97 @@ describe('SystemSubclusterManager', () => { }); }); - describe('launchSystemSubcluster', () => { - const config: SystemSubclusterConfig = { - bootstrap: 'testVat', - vats: { - testVat: { buildRootObject }, - }, - }; + describe('connectSystemSubcluster', () => { + it('waits for crank before connecting', async () => { + const config: KernelSystemSubclusterConfig = { + bootstrap: 'testVat', + vatTransports: [{ name: 'testVat', transport: makeMockTransport() }], + }; - it('waits for crank before launching', async () => { - await manager.launchSystemSubcluster(config); + await manager.connectSystemSubcluster(config); expect(kernelQueue.waitForCrank).toHaveBeenCalled(); }); - it('throws if bootstrap vat is not in vats config', async () => { - const badConfig: SystemSubclusterConfig = { + it('throws if bootstrap vat is not in vatTransports', async () => { + const config: KernelSystemSubclusterConfig = { bootstrap: 'missing', - vats: { - testVat: { buildRootObject }, - }, + vatTransports: [{ name: 'testVat', transport: makeMockTransport() }], }; - await expect(manager.launchSystemSubcluster(badConfig)).rejects.toThrow( + await expect(manager.connectSystemSubcluster(config)).rejects.toThrow( 'invalid bootstrap vat name missing', ); }); it('allocates system vat IDs starting from sv0', async () => { - const result = await manager.launchSystemSubcluster(config); + const config: KernelSystemSubclusterConfig = { + bootstrap: 'testVat', + vatTransports: [{ name: 'testVat', transport: makeMockTransport() }], + }; + + const result = await manager.connectSystemSubcluster(config); expect(result.vatIds.testVat).toBe('sv0'); }); it('allocates incrementing system vat IDs', async () => { - const result1 = await manager.launchSystemSubcluster(config); - const result2 = await manager.launchSystemSubcluster(config); + const config1: KernelSystemSubclusterConfig = { + bootstrap: 'testVat', + vatTransports: [{ name: 'testVat', transport: makeMockTransport() }], + }; + const config2: KernelSystemSubclusterConfig = { + bootstrap: 'testVat', + vatTransports: [{ name: 'testVat', transport: makeMockTransport() }], + }; + + const result1 = await manager.connectSystemSubcluster(config1); + const result2 = await manager.connectSystemSubcluster(config2); expect(result1.vatIds.testVat).toBe('sv0'); expect(result2.vatIds.testVat).toBe('sv1'); }); it('allocates system subcluster IDs starting from ss0', async () => { - const result = await manager.launchSystemSubcluster(config); + const config: KernelSystemSubclusterConfig = { + bootstrap: 'testVat', + vatTransports: [{ name: 'testVat', transport: makeMockTransport() }], + }; + + const result = await manager.connectSystemSubcluster(config); expect(result.systemSubclusterId).toBe('ss0'); }); it('initializes endpoints for each vat', async () => { - await manager.launchSystemSubcluster(config); + const config: KernelSystemSubclusterConfig = { + bootstrap: 'testVat', + vatTransports: [{ name: 'testVat', transport: makeMockTransport() }], + }; + + await manager.connectSystemSubcluster(config); expect(kernelStore.initEndpoint).toHaveBeenCalledWith('sv0'); }); it('initializes kernel objects for vat roots', async () => { - await manager.launchSystemSubcluster(config); + const config: KernelSystemSubclusterConfig = { + bootstrap: 'testVat', + vatTransports: [{ name: 'testVat', transport: makeMockTransport() }], + }; + + await manager.connectSystemSubcluster(config); expect(kernelStore.initKernelObject).toHaveBeenCalledWith('sv0'); }); it('adds clist entries for root objects', async () => { - await manager.launchSystemSubcluster(config); + const config: KernelSystemSubclusterConfig = { + bootstrap: 'testVat', + vatTransports: [{ name: 'testVat', transport: makeMockTransport() }], + }; + + await manager.connectSystemSubcluster(config); expect(kernelStore.addCListEntry).toHaveBeenCalledWith( 'sv0', @@ -160,7 +188,12 @@ describe('SystemSubclusterManager', () => { }); it('enqueues bootstrap message to root object', async () => { - await manager.launchSystemSubcluster(config); + const config: KernelSystemSubclusterConfig = { + bootstrap: 'testVat', + vatTransports: [{ name: 'testVat', transport: makeMockTransport() }], + }; + + await manager.connectSystemSubcluster(config); expect(kernelQueue.enqueueMessage).toHaveBeenCalledWith( 'ko1', @@ -169,16 +202,30 @@ describe('SystemSubclusterManager', () => { ); }); - it('launches multiple vats in a subcluster', async () => { - const multiVatConfig: SystemSubclusterConfig = { + it('wires syscall handler to transport', async () => { + const transport = makeMockTransport(); + const config: KernelSystemSubclusterConfig = { + bootstrap: 'testVat', + vatTransports: [{ name: 'testVat', transport }], + }; + + await manager.connectSystemSubcluster(config); + + expect(transport.setSyscallHandler).toHaveBeenCalledWith( + expect.any(Function), + ); + }); + + it('connects multiple vats in a subcluster', async () => { + const config: KernelSystemSubclusterConfig = { bootstrap: 'bootstrap', - vats: { - bootstrap: { buildRootObject }, - worker: { buildRootObject }, - }, + vatTransports: [ + { name: 'bootstrap', transport: makeMockTransport() }, + { name: 'worker', transport: makeMockTransport() }, + ], }; - const result = await manager.launchSystemSubcluster(multiVatConfig); + const result = await manager.connectSystemSubcluster(config); expect(result.vatIds.bootstrap).toBe('sv0'); expect(result.vatIds.worker).toBe('sv1'); @@ -190,9 +237,16 @@ describe('SystemSubclusterManager', () => { 'ko-existing', ); - await manager.launchSystemSubcluster(config); + const config: KernelSystemSubclusterConfig = { + bootstrap: 'testVat', + vatTransports: [{ name: 'testVat', transport: makeMockTransport() }], + }; - expect(kernelStore.initKernelObject).not.toHaveBeenCalled(); + await manager.connectSystemSubcluster(config); + + // Should not create new kernel object for root (only for kernelFacet) + expect(kernelStore.initKernelObject).toHaveBeenCalledTimes(1); + expect(kernelStore.initKernelObject).toHaveBeenCalledWith('kernel'); expect(kernelQueue.enqueueMessage).toHaveBeenCalledWith( 'ko-existing', 'bootstrap', @@ -201,13 +255,13 @@ describe('SystemSubclusterManager', () => { }); it('warns if requested service is not found', async () => { - const configWithServices: SystemSubclusterConfig = { + const config: KernelSystemSubclusterConfig = { bootstrap: 'testVat', - vats: { testVat: { buildRootObject } }, + vatTransports: [{ name: 'testVat', transport: makeMockTransport() }], services: ['unknownService'], }; - await manager.launchSystemSubcluster(configWithServices); + await manager.connectSystemSubcluster(config); expect(logger.warn).toHaveBeenCalledWith( "Kernel service 'unknownService' not found", @@ -219,13 +273,13 @@ describe('SystemSubclusterManager', () => { 'ko-service', ); - const configWithServices: SystemSubclusterConfig = { + const config: KernelSystemSubclusterConfig = { bootstrap: 'testVat', - vats: { testVat: { buildRootObject } }, + vatTransports: [{ name: 'testVat', transport: makeMockTransport() }], services: ['myService'], }; - await manager.launchSystemSubcluster(configWithServices); + await manager.connectSystemSubcluster(config); expect(kernelQueue.enqueueMessage).toHaveBeenCalledWith( 'ko1', @@ -233,52 +287,61 @@ describe('SystemSubclusterManager', () => { [ expect.anything(), expect.objectContaining({ myService: expect.anything() }), + expect.anything(), ], ); }); - }); - describe('terminateSystemSubcluster', () => { - it('waits for crank before terminating', async () => { - const config: SystemSubclusterConfig = { - bootstrap: 'testVat', - vats: { testVat: { buildRootObject } }, + it('creates singleton kernel facet across multiple subclusters', async () => { + const config1: KernelSystemSubclusterConfig = { + bootstrap: 'vat1', + vatTransports: [{ name: 'vat1', transport: makeMockTransport() }], + }; + const config2: KernelSystemSubclusterConfig = { + bootstrap: 'vat2', + vatTransports: [{ name: 'vat2', transport: makeMockTransport() }], }; - const result = await manager.launchSystemSubcluster(config); - - await manager.terminateSystemSubcluster(result.systemSubclusterId); - expect(kernelQueue.waitForCrank).toHaveBeenCalled(); - }); + await manager.connectSystemSubcluster(config1); + await manager.connectSystemSubcluster(config2); - it('throws if subcluster is not found', async () => { - await expect( - manager.terminateSystemSubcluster('ss-nonexistent'), - ).rejects.toThrow('System subcluster ss-nonexistent not found'); + // initKernelObject for 'kernel' should only be called once (singleton) + const kernelCalls = ( + kernelStore.initKernelObject as unknown as MockInstance + ).mock.calls.filter((call) => call[0] === 'kernel'); + expect(kernelCalls).toHaveLength(1); }); - it('removes subcluster from active subclusters', async () => { - const config: SystemSubclusterConfig = { + it('includes kernelFacet in bootstrap message', async () => { + const config: KernelSystemSubclusterConfig = { bootstrap: 'testVat', - vats: { testVat: { buildRootObject } }, + vatTransports: [{ name: 'testVat', transport: makeMockTransport() }], }; - const result = await manager.launchSystemSubcluster(config); - expect(manager.isSystemVatActive('sv0' as SystemVatId)).toBe(true); + await manager.connectSystemSubcluster(config); - await manager.terminateSystemSubcluster(result.systemSubclusterId); + // Bootstrap message should have 3 arguments: roots, services, kernelFacet + const callArgs = (kernelQueue.enqueueMessage as unknown as MockInstance) + .mock.calls[0]; + expect(callArgs).toHaveLength(3); + const [kref, method, args] = callArgs as [string, string, unknown[]]; + expect(kref).toBe('ko1'); + expect(method).toBe('bootstrap'); + expect(args).toHaveLength(3); - expect(manager.isSystemVatActive('sv0' as SystemVatId)).toBe(false); + // Third arg is kernelFacet slot (an exo with getKref method) + const kernelFacetSlot = args[2] as { getKref?: () => string }; + expect(typeof kernelFacetSlot.getKref).toBe('function'); }); }); describe('getSystemVatHandle', () => { - it('returns handle for active system vat', async () => { - const config: SystemSubclusterConfig = { + it('returns handle for connected system vat', async () => { + const config: KernelSystemSubclusterConfig = { bootstrap: 'testVat', - vats: { testVat: { buildRootObject } }, + vatTransports: [{ name: 'testVat', transport: makeMockTransport() }], }; - await manager.launchSystemSubcluster(config); + await manager.connectSystemSubcluster(config); const handle = manager.getSystemVatHandle('sv0' as SystemVatId); @@ -291,50 +354,25 @@ describe('SystemSubclusterManager', () => { expect(handle).toBeUndefined(); }); - }); - - describe('getSystemVatIds', () => { - it('returns empty array when no subclusters exist', () => { - const ids = manager.getSystemVatIds(); - - expect(ids).toStrictEqual([]); - }); - it('returns all system vat IDs', async () => { - const config: SystemSubclusterConfig = { - bootstrap: 'v1', - vats: { - v1: { buildRootObject }, - v2: { buildRootObject }, - }, + it('finds handle across multiple subclusters', async () => { + const config1: KernelSystemSubclusterConfig = { + bootstrap: 'vat1', + vatTransports: [{ name: 'vat1', transport: makeMockTransport() }], }; - await manager.launchSystemSubcluster(config); - - const ids = manager.getSystemVatIds(); - - expect(ids).toContain('sv0'); - expect(ids).toContain('sv1'); - expect(ids).toHaveLength(2); - }); - }); - - describe('isSystemVatActive', () => { - it('returns false for unknown system vat', () => { - const isActive = manager.isSystemVatActive('sv-unknown' as SystemVatId); - - expect(isActive).toBe(false); - }); - - it('returns true for active system vat', async () => { - const config: SystemSubclusterConfig = { - bootstrap: 'testVat', - vats: { testVat: { buildRootObject } }, + const config2: KernelSystemSubclusterConfig = { + bootstrap: 'vat2', + vatTransports: [{ name: 'vat2', transport: makeMockTransport() }], }; - await manager.launchSystemSubcluster(config); - const isActive = manager.isSystemVatActive('sv0' as SystemVatId); + await manager.connectSystemSubcluster(config1); + await manager.connectSystemSubcluster(config2); + + const handle1 = manager.getSystemVatHandle('sv0' as SystemVatId); + const handle2 = manager.getSystemVatHandle('sv1' as SystemVatId); - expect(isActive).toBe(true); + expect(handle1?.systemVatId).toBe('sv0'); + expect(handle2?.systemVatId).toBe('sv1'); }); }); }); diff --git a/packages/ocap-kernel/src/vats/SystemSubclusterManager.ts b/packages/ocap-kernel/src/vats/SystemSubclusterManager.ts index 20a3f875a..806f7d8be 100644 --- a/packages/ocap-kernel/src/vats/SystemSubclusterManager.ts +++ b/packages/ocap-kernel/src/vats/SystemSubclusterManager.ts @@ -3,7 +3,6 @@ import type { Logger } from '@metamask/logger'; import { makeKernelFacet } from '../kernel-facet.ts'; import type { KernelFacetDependencies } from '../kernel-facet.ts'; import type { KernelQueue } from '../KernelQueue.ts'; -import type { SystemVatDeliverFn } from './SystemVatHandle.ts'; import { SystemVatHandle } from './SystemVatHandle.ts'; import { kslot } from '../liveslots/kernel-marshal.ts'; import type { SlotValue } from '../liveslots/kernel-marshal.ts'; @@ -11,46 +10,47 @@ import type { KernelStore } from '../store/index.ts'; import type { SystemVatId, SystemSubclusterId, - SystemSubclusterConfig, - SystemSubclusterLaunchResult, + KernelSystemSubclusterConfig, KRef, } from '../types.ts'; import { ROOT_OBJECT_VREF } from '../types.ts'; -import { SystemVatSupervisor } from './SystemVatSupervisor.ts'; /** - * Callback type for connecting a system vat supervisor to the kernel. - * Called when a system vat is launched. + * Result of connecting a system subcluster. */ -export type SystemVatConnectFn = ( - systemVatId: SystemVatId, - deliver: SystemVatDeliverFn, -) => SystemVatHandle; +export type SystemSubclusterConnectResult = { + /** The ID of the connected system subcluster. */ + systemSubclusterId: SystemSubclusterId; + /** Map of vat names to their system vat IDs. */ + vatIds: Record; +}; type SystemSubclusterManagerOptions = { kernelStore: KernelStore; kernelQueue: KernelQueue; kernelFacetDeps: KernelFacetDependencies; - /** Logger is required for system subclusters since supervisors need it for liveslots. */ logger: Logger; }; +/** + * Internal record for a connected system subcluster. + */ type SystemSubclusterRecord = { id: SystemSubclusterId; - config: SystemSubclusterConfig; + config: KernelSystemSubclusterConfig; vatIds: Record; handles: Map; - supervisors: Map; }; /** * Manages system subclusters - subclusters whose vats run without compartment - * isolation directly in the host process. + * isolation directly in the runtime process. * * System vats: - * - Run without compartment isolation + * - Are created by the runtime (not the kernel) + * - Connect to the kernel via transports + * - Receive a kernel facet in the bootstrap message * - Don't participate in kernel persistence machinery - * - The bootstrap vat receives a kernel facet as a vatpower */ export class SystemSubclusterManager { /** Storage holding the kernel's persistent state */ @@ -75,6 +75,13 @@ export class SystemSubclusterManager { readonly #subclusters: Map = new Map(); + /** Singleton kernel facet (created lazily, kept alive for GC purposes) */ + // eslint-disable-next-line no-unused-private-class-members + #kernelFacet: object | null = null; + + /** Kref of the singleton kernel facet */ + #kernelFacetKref: KRef | null = null; + /** * Creates a new SystemSubclusterManager instance. * @@ -120,85 +127,68 @@ export class SystemSubclusterManager { } /** - * Launch a system subcluster. + * Get the singleton kernel facet kref, creating it if necessary. * - * @param config - Configuration for the system subcluster. - * @returns A promise for the launch result. + * @returns The kref for the kernel facet. */ - async launchSystemSubcluster( - config: SystemSubclusterConfig, - ): Promise { + #getKernelFacetKref(): KRef { + if (!this.#kernelFacetKref) { + this.#kernelFacet = makeKernelFacet(this.#kernelFacetDeps); + this.#kernelFacetKref = this.#kernelStore.initKernelObject('kernel'); + } + return this.#kernelFacetKref; + } + + /** + * Connect to a system subcluster using provided transports. + * + * The runtime creates supervisors externally and provides transports for + * communication. The kernel creates a kernel facet and delivers it in the + * bootstrap message as a presence. + * + * @param config - Configuration for the system subcluster with transports. + * @returns A promise for the connect result. + */ + async connectSystemSubcluster( + config: KernelSystemSubclusterConfig, + ): Promise { await this.#kernelQueue.waitForCrank(); - if (!config.vats[config.bootstrap]) { + const bootstrapTransport = config.vatTransports.find( + (vt) => vt.name === config.bootstrap, + ); + if (!bootstrapTransport) { throw Error(`invalid bootstrap vat name ${config.bootstrap}`); } const systemSubclusterId = this.#allocateSystemSubclusterId(); const vatIds: Record = {}; const handles = new Map(); - const supervisors = new Map(); const rootKrefs: Record = {}; - // Create kernel facet for the bootstrap vat - const kernelFacet = makeKernelFacet(this.#kernelFacetDeps); - - // Launch all system vats - for (const [vatName, vatConfig] of Object.entries(config.vats)) { + // Connect all system vats via their transports + for (const vatTransport of config.vatTransports) { + const { name: vatName, transport } = vatTransport; const systemVatId = this.#allocateSystemVatId(); vatIds[vatName] = systemVatId; // Initialize the endpoint in the kernel store this.#kernelStore.initEndpoint(systemVatId); - // Determine vatpowers - bootstrap vat gets the kernel facet - const isBootstrap = vatName === config.bootstrap; - const vatPowers: Record = isBootstrap - ? { kernelFacet } - : {}; - - // Create the system vat handle (kernel-side) - // We need the deliver function from the supervisor, so we create - // a deferred connection - let supervisorDeliver: SystemVatDeliverFn | null = null; - const deliver: SystemVatDeliverFn = async (delivery) => { - if (!supervisorDeliver) { - throw new Error('System vat supervisor not connected'); - } - return supervisorDeliver(delivery); - }; - + // Create the system vat handle (kernel-side) with the transport's deliver function const handle = new SystemVatHandle({ systemVatId, kernelStore: this.#kernelStore, kernelQueue: this.#kernelQueue, - deliver, + deliver: transport.deliver, logger: this.#logger.subLogger({ tags: [systemVatId] }), }); handles.set(systemVatId, handle); - // Create the supervisor (which runs liveslots) - const supervisor = new SystemVatSupervisor({ - id: systemVatId, - buildRootObject: vatConfig.buildRootObject, - vatPowers, - parameters: vatConfig.parameters, - executeSyscall: (vso) => - handle.getSyscallHandler()(vso) ?? harden(['ok', null]), - logger: this.#logger.subLogger({ tags: [systemVatId, 'supervisor'] }), - }); - supervisors.set(systemVatId, supervisor); + // Wire the syscall handler to the transport + transport.setSyscallHandler(handle.getSyscallHandler()); - // Connect the supervisor's deliver function to the handle - supervisorDeliver = supervisor.deliver.bind(supervisor); - - // Start the vat - const startError = await supervisor.start(); - if (startError) { - throw new Error(`Failed to start system vat ${vatName}: ${startError}`); - } - - // Get the root kref (the root object is exported at o+0) + // Get or create the root kref (the root object is exported at o+0) const existingRootKref = this.#kernelStore.erefToKref( systemVatId, ROOT_OBJECT_VREF, @@ -217,13 +207,15 @@ export class SystemSubclusterManager { } } + // Get the singleton kernel facet kref + const kernelFacetKref = this.#getKernelFacetKref(); + // Store the subcluster record const record: SystemSubclusterRecord = { id: systemSubclusterId, config, vatIds, handles, - supervisors, }; this.#subclusters.set(systemSubclusterId, record); @@ -248,7 +240,7 @@ export class SystemSubclusterManager { } } - // Call bootstrap on the bootstrap vat's root object + // Call bootstrap on the bootstrap vat's root object with kernelFacet as a presence const bootstrapVatId = vatIds[config.bootstrap]; if (!bootstrapVatId) { throw new Error(`Bootstrap vat ID not found for ${config.bootstrap}`); @@ -257,7 +249,7 @@ export class SystemSubclusterManager { await this.#kernelQueue.enqueueMessage( rootKrefs[config.bootstrap] as KRef, 'bootstrap', - [roots, services], + [roots, services, kslot(kernelFacetKref, 'KernelFacet')], ); return { @@ -266,29 +258,6 @@ export class SystemSubclusterManager { }; } - /** - * Terminate a system subcluster. - * - * @param systemSubclusterId - ID of the system subcluster to terminate. - */ - async terminateSystemSubcluster( - systemSubclusterId: SystemSubclusterId, - ): Promise { - await this.#kernelQueue.waitForCrank(); - - const record = this.#subclusters.get(systemSubclusterId); - if (!record) { - throw Error(`System subcluster ${systemSubclusterId} not found`); - } - - // Terminate all handles - for (const handle of record.handles.values()) { - await handle.terminate(true); - } - - this.#subclusters.delete(systemSubclusterId); - } - /** * Get a system vat handle by ID. * @@ -304,28 +273,5 @@ export class SystemSubclusterManager { } return undefined; } - - /** - * Get all system vat IDs. - * - * @returns Array of all system vat IDs. - */ - getSystemVatIds(): SystemVatId[] { - const ids: SystemVatId[] = []; - for (const record of this.#subclusters.values()) { - ids.push(...record.handles.keys()); - } - return ids; - } - - /** - * Check if a system vat is active. - * - * @param systemVatId - The system vat ID to check. - * @returns True if the system vat is active. - */ - isSystemVatActive(systemVatId: SystemVatId): boolean { - return this.getSystemVatHandle(systemVatId) !== undefined; - } } harden(SystemSubclusterManager); diff --git a/packages/ocap-kernel/src/vats/SystemVatHandle.test.ts b/packages/ocap-kernel/src/vats/SystemVatHandle.test.ts index d13e911d2..05af26abd 100644 --- a/packages/ocap-kernel/src/vats/SystemVatHandle.test.ts +++ b/packages/ocap-kernel/src/vats/SystemVatHandle.test.ts @@ -65,7 +65,11 @@ describe('SystemVatHandle', () => { expect(typeof handler).toBe('function'); // Test that it can handle a syscall - handler(['send', 'o+1', { methargs: { body: '[]', slots: [] } }]); + handler([ + 'send', + 'o+1', + { methargs: { body: '[]', slots: [] }, result: 'p-1' }, + ]); expect(kernelQueue.enqueueSend).toHaveBeenCalled(); }); }); @@ -174,52 +178,6 @@ describe('SystemVatHandle', () => { }); }); - describe('terminate', () => { - it('marks handle as inactive when terminating=true', async () => { - await systemVatHandle.terminate(true); - - // Subsequent syscalls should fail because handle is inactive - const handler = systemVatHandle.getSyscallHandler(); - handler(['send', 'o+1', { methargs: { body: '[]', slots: [] } }]); - - // The syscall should have been rejected due to inactive status - expect(kernelQueue.enqueueSend).not.toHaveBeenCalled(); - }); - - it('rejects promises for which this vat is decider when terminating', async () => { - ( - kernelStore.getPromisesByDecider as unknown as MockInstance - ).mockReturnValueOnce(['kp1', 'kp2']); - - await systemVatHandle.terminate(true); - - expect(kernelQueue.resolvePromises).toHaveBeenCalledWith( - systemVatId, - expect.arrayContaining([ - expect.arrayContaining(['kp1', true, expect.anything()]), - ]), - ); - expect(kernelQueue.resolvePromises).toHaveBeenCalledWith( - systemVatId, - expect.arrayContaining([ - expect.arrayContaining(['kp2', true, expect.anything()]), - ]), - ); - }); - - it('deletes endpoint when terminating', async () => { - await systemVatHandle.terminate(true); - - expect(kernelStore.deleteEndpoint).toHaveBeenCalledWith(systemVatId); - }); - - it('does not delete endpoint when not terminating', async () => { - await systemVatHandle.terminate(false); - - expect(kernelStore.deleteEndpoint).not.toHaveBeenCalled(); - }); - }); - describe('crank results', () => { it('returns abort and terminate on delivery error', async () => { (deliver as unknown as MockInstance).mockResolvedValueOnce( @@ -259,7 +217,7 @@ describe('SystemVatHandle', () => { handle.getSyscallHandler()([ 'send', 'o+1', - { methargs: { body: '[]', slots: [] } }, + { methargs: { body: '[]', slots: [] }, result: 'p-1' }, ]); } return null; diff --git a/packages/ocap-kernel/src/vats/SystemVatHandle.ts b/packages/ocap-kernel/src/vats/SystemVatHandle.ts index 9eb80329f..5a01be905 100644 --- a/packages/ocap-kernel/src/vats/SystemVatHandle.ts +++ b/packages/ocap-kernel/src/vats/SystemVatHandle.ts @@ -2,12 +2,13 @@ import type { VatDeliveryObject, VatOneResolution, VatSyscallObject, + VatSyscallResult, Message as SwingSetMessage, } from '@agoric/swingset-liveslots'; import type { Logger } from '@metamask/logger'; import type { KernelQueue } from '../KernelQueue.ts'; -import { kser, makeError } from '../liveslots/kernel-marshal.ts'; +import { makeError } from '../liveslots/kernel-marshal.ts'; import type { KernelStore } from '../store/index.ts'; import type { Message, @@ -28,7 +29,9 @@ export type SystemVatDeliverFn = ( /** * Syscall callback type - called by system vat to send syscalls to kernel. */ -export type SystemVatSyscallFn = (syscall: VatSyscallObject) => void; +export type SystemVatSyscallFn = ( + syscall: VatSyscallObject, +) => VatSyscallResult; type SystemVatHandleProps = { systemVatId: SystemVatId; @@ -51,12 +54,6 @@ export class SystemVatHandle implements EndpointHandle { /** Logger for outputting messages (such as errors) to the console */ readonly #logger: Logger | undefined; - /** Storage holding the kernel's persistent state */ - readonly #kernelStore: KernelStore; - - /** The kernel's queue */ - readonly #kernelQueue: KernelQueue; - /** The system vat's syscall handler */ readonly #vatSyscall: VatSyscall; @@ -64,7 +61,7 @@ export class SystemVatHandle implements EndpointHandle { readonly #deliver: SystemVatDeliverFn; /** Flag indicating if this handle is active */ - #isActive: boolean = true; + readonly #isActive: boolean = true; /** * Construct a new SystemVatHandle instance. @@ -85,8 +82,6 @@ export class SystemVatHandle implements EndpointHandle { }: SystemVatHandleProps) { this.systemVatId = systemVatId; this.#logger = logger; - this.#kernelStore = kernelStore; - this.#kernelQueue = kernelQueue; this.#deliver = deliver; this.#vatSyscall = new VatSyscall({ vatId: systemVatId, @@ -103,11 +98,11 @@ export class SystemVatHandle implements EndpointHandle { /** * Get a syscall handler function to pass to the system vat supervisor. * - * @returns A function that handles syscalls from the system vat. + * @returns A function that handles syscalls from the system vat and returns the result. */ - getSyscallHandler(): (syscall: VatSyscallObject) => void { + getSyscallHandler(): (syscall: VatSyscallObject) => VatSyscallResult { return (syscall: VatSyscallObject) => { - this.#vatSyscall.handleSyscall(syscall); + return this.#vatSyscall.handleSyscall(syscall); }; } @@ -184,29 +179,6 @@ export class SystemVatHandle implements EndpointHandle { return this.#getDeliveryCrankResults(deliveryError); } - /** - * Terminates the system vat handle. - * - * @param terminating - If true, the vat is being killed permanently. - * @param _error - The error to terminate with (unused for system vats). - */ - async terminate(terminating: boolean, _error?: Error): Promise { - this.#isActive = false; - if (terminating) { - // Reject promises exported to other vats for which this vat is the decider - const failure = kser(new Error('system vat terminated')); - for (const kpid of this.#kernelStore.getPromisesByDecider( - this.systemVatId, - )) { - this.#kernelQueue.resolvePromises(this.systemVatId, [ - [kpid, true, failure], - ]); - } - // Note: System vats don't have a vatStore to delete - this.#kernelStore.deleteEndpoint(this.systemVatId); - } - } - /** * Get the crank outcome for a delivery. * diff --git a/packages/ocap-kernel/src/vats/SystemVatSupervisor.test.ts b/packages/ocap-kernel/src/vats/SystemVatSupervisor.test.ts index 7466ca477..333835c22 100644 --- a/packages/ocap-kernel/src/vats/SystemVatSupervisor.test.ts +++ b/packages/ocap-kernel/src/vats/SystemVatSupervisor.test.ts @@ -1,7 +1,6 @@ import type { VatDeliveryObject } from '@agoric/swingset-liveslots'; import { Logger } from '@metamask/logger'; import { describe, it, expect, vi, beforeEach } from 'vitest'; -import type { MockInstance } from 'vitest'; import type { SystemVatBuildRootObject, SystemVatId } from '../types.ts'; import type { SystemVatExecuteSyscall } from './SystemVatSupervisor.ts'; @@ -252,8 +251,7 @@ describe('SystemVatSupervisor', () => { expect(supervisor.id).toBe(systemVatId); // Get the syscall object passed to makeLiveSlots - const syscall = (makeLiveSlots as unknown as MockInstance).mock - .calls[0][0]; + const syscall = vi.mocked(makeLiveSlots).mock.calls[0]?.[0]; // Test the send syscall syscall.send('o+1', { body: '[]', slots: [] }, 'p-1'); @@ -283,8 +281,7 @@ describe('SystemVatSupervisor', () => { expect(supervisor.id).toBe(systemVatId); // Get the syscall object passed to makeLiveSlots - const syscall = (makeLiveSlots as unknown as MockInstance).mock - .calls[0][0]; + const syscall = vi.mocked(makeLiveSlots).mock.calls[0]?.[0]; expect(() => syscall.send('o+1', { body: '[]', slots: [] }, 'p-1'), @@ -305,8 +302,7 @@ describe('SystemVatSupervisor', () => { expect(supervisor.id).toBe(systemVatId); // Get the syscall object passed to makeLiveSlots - const syscall = (makeLiveSlots as unknown as MockInstance).mock - .calls[0][0]; + const syscall = vi.mocked(makeLiveSlots).mock.calls[0]?.[0]; expect(() => syscall.callNow()).toThrow( 'callNow not supported for system vats', @@ -329,8 +325,7 @@ describe('SystemVatSupervisor', () => { }); // Get the syscall object passed to makeLiveSlots - const syscall = (makeLiveSlots as unknown as MockInstance).mock - .calls[0][0]; + const syscall = vi.mocked(makeLiveSlots).mock.calls[0]?.[0]; // Test vatstore operations expect(syscall.vatstoreGet('key')).toBeUndefined(); @@ -356,8 +351,7 @@ describe('SystemVatSupervisor', () => { }); // Get the syscall object passed to makeLiveSlots - const syscall = (makeLiveSlots as unknown as MockInstance).mock - .calls[0][0]; + const syscall = vi.mocked(makeLiveSlots).mock.calls[0]?.[0]; syscall.vatstoreSet('a', '1'); syscall.vatstoreSet('b', '2'); @@ -384,8 +378,7 @@ describe('SystemVatSupervisor', () => { }); // Get the buildVatNamespace function passed to makeLiveSlots - const buildVatNamespace = (makeLiveSlots as unknown as MockInstance).mock - .calls[0][6]; + const buildVatNamespace = vi.mocked(makeLiveSlots).mock.calls[0]?.[6]; // Call buildVatNamespace to get the namespace const namespace = await buildVatNamespace({}, {}); diff --git a/packages/ocap-kernel/src/vats/SystemVatSupervisor.ts b/packages/ocap-kernel/src/vats/SystemVatSupervisor.ts index 5a883d1a2..fe857b6ee 100644 --- a/packages/ocap-kernel/src/vats/SystemVatSupervisor.ts +++ b/packages/ocap-kernel/src/vats/SystemVatSupervisor.ts @@ -35,14 +35,41 @@ export type SystemVatExecuteSyscall = ( vso: VatSyscallObject, ) => VatSyscallResult; +/** + * A holder for a syscall handler that can be set after construction. + * This allows the supervisor to be created before the kernel wires up + * the transport. + */ +export type SyscallHandlerHolder = { + /** The syscall handler, set by the kernel when wiring up the transport. */ + handler: SystemVatExecuteSyscall | null; +}; + +/** + * Create a syscall handler holder for deferred wiring. + * + * @returns A syscall handler holder. + */ +export function makeSyscallHandlerHolder(): SyscallHandlerHolder { + return { handler: null }; +} + type SystemVatSupervisorProps = { id: SystemVatId; buildRootObject: SystemVatBuildRootObject; vatPowers: Record; parameters: Record | undefined; - executeSyscall: SystemVatExecuteSyscall; logger: Logger; -}; +} & ( + | { + /** Direct syscall executor (legacy - for same-process use). */ + executeSyscall: SystemVatExecuteSyscall; + } + | { + /** Syscall handler holder for deferred wiring (transport-based). */ + syscallHandlerHolder: SyscallHandlerHolder; + } +); /** * A non-persistent KV store for system vats. @@ -117,6 +144,10 @@ function makeEphemeralVatKVStore(): KVStore { * They don't load bundles via importBundle; instead, they receive a * buildRootObject function directly. They use an ephemeral vatstore since * they don't participate in kernel persistence machinery. + * + * The supervisor can be wired to the kernel in two ways: + * 1. Direct: Pass `executeSyscall` in constructor (same-process) + * 2. Deferred: Pass `syscallHandlerHolder` and set handler later (transport-based) */ export class SystemVatSupervisor { /** The ID of the system vat being supervised */ @@ -139,20 +170,31 @@ export class SystemVatSupervisor { * @param props.buildRootObject - Function to build the vat's root object. * @param props.vatPowers - External capabilities for this system vat. * @param props.parameters - Parameters to pass to buildRootObject. - * @param props.executeSyscall - Function to execute syscalls synchronously. + * @param props.executeSyscall - Function to execute syscalls (direct wiring). + * @param props.syscallHandlerHolder - Holder for deferred syscall handler wiring. * @param props.logger - The logger for this system vat. */ - constructor({ - id, - buildRootObject, - vatPowers, - parameters, - executeSyscall, - logger, - }: SystemVatSupervisorProps) { + constructor(props: SystemVatSupervisorProps) { + const { id, buildRootObject, vatPowers, parameters, logger } = props; this.id = id; this.#logger = logger; + // Determine the syscall executor + let executeSyscall: SystemVatExecuteSyscall; + if ('executeSyscall' in props) { + // Direct wiring (legacy) + executeSyscall = props.executeSyscall; + } else { + // Deferred wiring via holder + const { syscallHandlerHolder } = props; + executeSyscall = (vso: VatSyscallObject): VatSyscallResult => { + if (!syscallHandlerHolder.handler) { + throw new Error('Syscall handler not yet wired'); + } + return syscallHandlerHolder.handler(vso); + }; + } + // Initialize the system vat synchronously during construction this.#initializeVat(buildRootObject, vatPowers, parameters, executeSyscall); } @@ -293,7 +335,7 @@ export class SystemVatSupervisor { let deliveryError: string | null = null; try { - const serParam = marshal.toCapData(harden(undefined)) as CapData; + const serParam = marshal.toCapData(harden({})) as CapData; await this.#dispatch(harden(['startVat', serParam])); } catch (error) { deliveryError = error instanceof Error ? error.message : String(error); diff --git a/packages/ocap-kernel/src/vats/index.ts b/packages/ocap-kernel/src/vats/index.ts new file mode 100644 index 000000000..c20fe5022 --- /dev/null +++ b/packages/ocap-kernel/src/vats/index.ts @@ -0,0 +1,11 @@ +export { SystemVatSupervisor } from './SystemVatSupervisor.ts'; +export { + makeSyscallHandlerHolder, + type SyscallHandlerHolder, + type SystemVatExecuteSyscall, +} from './SystemVatSupervisor.ts'; +export { SystemVatHandle } from './SystemVatHandle.ts'; +export type { + SystemVatDeliverFn, + SystemVatSyscallFn, +} from './SystemVatHandle.ts'; From eccc2095c6b58f5495844e4aa16e2915e9d880e0 Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Wed, 28 Jan 2026 18:00:19 -0800 Subject: [PATCH 17/41] chore: Delete browser-runtime integration test leftovers --- packages/kernel-browser-runtime/package.json | 1 - .../vitest.integration.config.ts | 34 ------------------- 2 files changed, 35 deletions(-) delete mode 100644 packages/kernel-browser-runtime/vitest.integration.config.ts diff --git a/packages/kernel-browser-runtime/package.json b/packages/kernel-browser-runtime/package.json index 89597fa7b..a1bc8231e 100644 --- a/packages/kernel-browser-runtime/package.json +++ b/packages/kernel-browser-runtime/package.json @@ -59,7 +59,6 @@ "test:build": "tsx ./test/build-tests.ts", "test:clean": "yarn test --no-cache --coverage.clean", "test:dev": "yarn test --mode development", - "test:integration": "vitest run --config vitest.integration.config.ts", "test:verbose": "yarn test --reporter verbose", "test:watch": "vitest --config vitest.config.ts", "test:dev:quiet": "yarn test:dev --reporter @ocap/repo-tools/vitest-reporters/silent" diff --git a/packages/kernel-browser-runtime/vitest.integration.config.ts b/packages/kernel-browser-runtime/vitest.integration.config.ts deleted file mode 100644 index 6c20f76c6..000000000 --- a/packages/kernel-browser-runtime/vitest.integration.config.ts +++ /dev/null @@ -1,34 +0,0 @@ -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 endoify-node which imports @libp2p/webrtc before lockdown - // (webrtc imports reflect-metadata which modifies globalThis.Reflect) - fileURLToPath( - import.meta.resolve('@metamask/kernel-shims/endoify-node'), - ), - ], - }, - }), - ); - - delete config.test?.coverage; - - return config; -}); From 516be8a61bd8d74c04b04485e38acd8eb55ddf8e Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Wed, 28 Jan 2026 22:10:00 -0800 Subject: [PATCH 18/41] fix(ocap-kernel): Fix system vat integration and add integration tests - Fix queue length cache initialization (was showing 0 when item enqueued) - Fix vref format for system vats (sv0 now uses same format as v0) - Register kernel facet as kernel service for message delivery - Change bootstrap to fire-and-forget to avoid deadlock during Kernel.make() - Pass kernel facet via services parameter instead of separate argument - Add integration tests for system vat with kernel facet E() calls - Change SystemVatSupervisor.start() to throw on error instead of returning string Co-Authored-By: Claude --- packages/ocap-kernel/package.json | 6 +- packages/ocap-kernel/src/Kernel.ts | 2 + .../ocap-kernel/src/store/methods/clist.ts | 5 +- .../ocap-kernel/src/store/methods/queue.ts | 8 + .../src/vats/SystemSubclusterManager.test.ts | 94 +++++----- .../src/vats/SystemSubclusterManager.ts | 47 +++-- .../src/vats/SystemVatSupervisor.test.ts | 25 +-- .../src/vats/SystemVatSupervisor.ts | 18 +- .../test/integration/system-vat.test.ts | 177 ++++++++++++++++++ packages/ocap-kernel/tsconfig.json | 3 +- .../ocap-kernel/vitest.integration.config.ts | 29 +++ yarn.lock | 1 + 12 files changed, 313 insertions(+), 102 deletions(-) create mode 100644 packages/ocap-kernel/test/integration/system-vat.test.ts create mode 100644 packages/ocap-kernel/vitest.integration.config.ts diff --git a/packages/ocap-kernel/package.json b/packages/ocap-kernel/package.json index dbcb8ea6a..e00a40631 100644 --- a/packages/ocap-kernel/package.json +++ b/packages/ocap-kernel/package.json @@ -73,9 +73,10 @@ "test": "vitest run --config vitest.config.ts", "test:clean": "yarn test --no-cache --coverage.clean", "test:dev": "yarn test --mode development", + "test:dev:quiet": "yarn test:dev --reporter @ocap/repo-tools/vitest-reporters/silent", + "test:integration": "vitest run --config vitest.integration.config.ts", "test:verbose": "yarn test --reporter verbose", - "test:watch": "vitest --config vitest.config.ts", - "test:dev:quiet": "yarn test:dev --reporter @ocap/repo-tools/vitest-reporters/silent" + "test:watch": "vitest --config vitest.config.ts" }, "dependencies": { "@agoric/swingset-liveslots": "0.10.3-u21.0.1", @@ -115,6 +116,7 @@ "uint8arrays": "^5.1.0" }, "devDependencies": { + "@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/ocap-kernel/src/Kernel.ts b/packages/ocap-kernel/src/Kernel.ts index daa24602c..3c7def1aa 100644 --- a/packages/ocap-kernel/src/Kernel.ts +++ b/packages/ocap-kernel/src/Kernel.ts @@ -182,6 +182,8 @@ export class Kernel { getStatus: this.getStatus.bind(this), logger: this.#logger.subLogger({ tags: ['KernelFacet'] }), }, + registerKernelService: (name, service) => + this.#kernelServiceManager.registerKernelServiceObject(name, service), logger: this.#logger.subLogger({ tags: ['SystemSubclusterManager'] }), }); diff --git a/packages/ocap-kernel/src/store/methods/clist.ts b/packages/ocap-kernel/src/store/methods/clist.ts index 95661562b..aa15aa7f2 100644 --- a/packages/ocap-kernel/src/store/methods/clist.ts +++ b/packages/ocap-kernel/src/store/methods/clist.ts @@ -86,7 +86,10 @@ export function getCListMethods(ctx: StoreContext) { */ function allocateErefForKref(endpointId: EndpointId, kref: KRef): ERef { let id; - const refTag = endpointId.startsWith('v') ? '' : endpointId[0]; + // System vats (sv0, sv1) use the same vref format as regular vats (v0, v1) + const isVatOrSystemVat = + endpointId.startsWith('v') || endpointId.startsWith('sv'); + const refTag = isVatOrSystemVat ? '' : endpointId[0]; let refType; if (isPromiseRef(kref)) { id = ctx.kv.get(`e.nextPromiseId.${endpointId}`); diff --git a/packages/ocap-kernel/src/store/methods/queue.ts b/packages/ocap-kernel/src/store/methods/queue.ts index 54693580e..2b7839164 100644 --- a/packages/ocap-kernel/src/store/methods/queue.ts +++ b/packages/ocap-kernel/src/store/methods/queue.ts @@ -33,6 +33,10 @@ export function getQueueMethods(ctx: StoreContext) { * @param message - The message to enqueue. */ function enqueueRun(message: RunQueueItem): void { + // Ensure cache is initialized before incrementing + if (ctx.runQueueLengthCache < 0) { + ctx.runQueueLengthCache = getQueueLength('run'); + } ctx.runQueueLengthCache += 1; ctx.runQueue.enqueue(message); } @@ -44,6 +48,10 @@ export function getQueueMethods(ctx: StoreContext) { * empty. */ function dequeueRun(): RunQueueItem | undefined { + // Ensure cache is initialized before decrementing + if (ctx.runQueueLengthCache < 0) { + ctx.runQueueLengthCache = getQueueLength('run'); + } ctx.runQueueLengthCache -= 1; return ctx.runQueue.dequeue() as RunQueueItem | undefined; } diff --git a/packages/ocap-kernel/src/vats/SystemSubclusterManager.test.ts b/packages/ocap-kernel/src/vats/SystemSubclusterManager.test.ts index a0ef9c81b..2f3114d38 100644 --- a/packages/ocap-kernel/src/vats/SystemSubclusterManager.test.ts +++ b/packages/ocap-kernel/src/vats/SystemSubclusterManager.test.ts @@ -84,22 +84,14 @@ describe('SystemSubclusterManager', () => { kernelStore, kernelQueue, kernelFacetDeps, + registerKernelService: vi + .fn() + .mockReturnValue({ kref: 'ko-kernelFacet' }), logger, }); }); describe('connectSystemSubcluster', () => { - it('waits for crank before connecting', async () => { - const config: KernelSystemSubclusterConfig = { - bootstrap: 'testVat', - vatTransports: [{ name: 'testVat', transport: makeMockTransport() }], - }; - - await manager.connectSystemSubcluster(config); - - expect(kernelQueue.waitForCrank).toHaveBeenCalled(); - }); - it('throws if bootstrap vat is not in vatTransports', async () => { const config: KernelSystemSubclusterConfig = { bootstrap: 'missing', @@ -195,10 +187,11 @@ describe('SystemSubclusterManager', () => { await manager.connectSystemSubcluster(config); - expect(kernelQueue.enqueueMessage).toHaveBeenCalledWith( + expect(kernelQueue.enqueueSend).toHaveBeenCalledWith( 'ko1', - 'bootstrap', - expect.any(Array), + expect.objectContaining({ + methargs: expect.any(Object), + }), ); }); @@ -244,13 +237,14 @@ describe('SystemSubclusterManager', () => { await manager.connectSystemSubcluster(config); - // Should not create new kernel object for root (only for kernelFacet) - expect(kernelStore.initKernelObject).toHaveBeenCalledTimes(1); - expect(kernelStore.initKernelObject).toHaveBeenCalledWith('kernel'); - expect(kernelQueue.enqueueMessage).toHaveBeenCalledWith( + // Should not create new kernel object for root + // (only kernel facet is created via registerKernelService which is mocked) + expect(kernelStore.initKernelObject).not.toHaveBeenCalled(); + expect(kernelQueue.enqueueSend).toHaveBeenCalledWith( 'ko-existing', - 'bootstrap', - expect.any(Array), + expect.objectContaining({ + methargs: expect.any(Object), + }), ); }); @@ -281,18 +275,29 @@ describe('SystemSubclusterManager', () => { await manager.connectSystemSubcluster(config); - expect(kernelQueue.enqueueMessage).toHaveBeenCalledWith( + // Check that enqueueSend was called (service is embedded in the serialized methargs) + expect(kernelQueue.enqueueSend).toHaveBeenCalledWith( 'ko1', - 'bootstrap', - [ - expect.anything(), - expect.objectContaining({ myService: expect.anything() }), - expect.anything(), - ], + expect.objectContaining({ + methargs: expect.any(Object), + }), ); }); it('creates singleton kernel facet across multiple subclusters', async () => { + // Create a manager with a register function that tracks calls + const registerCalls: string[] = []; + const managerWithTracking = new SystemSubclusterManager({ + kernelStore, + kernelQueue, + kernelFacetDeps, + registerKernelService: vi.fn().mockImplementation((name: string) => { + registerCalls.push(name); + return { kref: 'ko-kernelFacet' }; + }), + logger, + }); + const config1: KernelSystemSubclusterConfig = { bootstrap: 'vat1', vatTransports: [{ name: 'vat1', transport: makeMockTransport() }], @@ -302,14 +307,13 @@ describe('SystemSubclusterManager', () => { vatTransports: [{ name: 'vat2', transport: makeMockTransport() }], }; - await manager.connectSystemSubcluster(config1); - await manager.connectSystemSubcluster(config2); + await managerWithTracking.connectSystemSubcluster(config1); + await managerWithTracking.connectSystemSubcluster(config2); - // initKernelObject for 'kernel' should only be called once (singleton) - const kernelCalls = ( - kernelStore.initKernelObject as unknown as MockInstance - ).mock.calls.filter((call) => call[0] === 'kernel'); - expect(kernelCalls).toHaveLength(1); + // registerKernelService should only be called once (singleton) + expect( + registerCalls.filter((name) => name === 'kernelFacet'), + ).toHaveLength(1); }); it('includes kernelFacet in bootstrap message', async () => { @@ -320,18 +324,16 @@ describe('SystemSubclusterManager', () => { await manager.connectSystemSubcluster(config); - // Bootstrap message should have 3 arguments: roots, services, kernelFacet - const callArgs = (kernelQueue.enqueueMessage as unknown as MockInstance) - .mock.calls[0]; - expect(callArgs).toHaveLength(3); - const [kref, method, args] = callArgs as [string, string, unknown[]]; - expect(kref).toBe('ko1'); - expect(method).toBe('bootstrap'); - expect(args).toHaveLength(3); - - // Third arg is kernelFacet slot (an exo with getKref method) - const kernelFacetSlot = args[2] as { getKref?: () => string }; - expect(typeof kernelFacetSlot.getKref).toBe('function'); + // Verify enqueueSend was called with bootstrap message + expect(kernelQueue.enqueueSend).toHaveBeenCalledWith( + 'ko1', + expect.objectContaining({ + methargs: expect.objectContaining({ + // The methargs should contain the kernelFacet kref in slots + slots: expect.arrayContaining(['ko-kernelFacet']), + }), + }), + ); }); }); diff --git a/packages/ocap-kernel/src/vats/SystemSubclusterManager.ts b/packages/ocap-kernel/src/vats/SystemSubclusterManager.ts index 806f7d8be..79eb9dbb7 100644 --- a/packages/ocap-kernel/src/vats/SystemSubclusterManager.ts +++ b/packages/ocap-kernel/src/vats/SystemSubclusterManager.ts @@ -4,7 +4,7 @@ import { makeKernelFacet } from '../kernel-facet.ts'; import type { KernelFacetDependencies } from '../kernel-facet.ts'; import type { KernelQueue } from '../KernelQueue.ts'; import { SystemVatHandle } from './SystemVatHandle.ts'; -import { kslot } from '../liveslots/kernel-marshal.ts'; +import { kser, kslot } from '../liveslots/kernel-marshal.ts'; import type { SlotValue } from '../liveslots/kernel-marshal.ts'; import type { KernelStore } from '../store/index.ts'; import type { @@ -29,6 +29,7 @@ type SystemSubclusterManagerOptions = { kernelStore: KernelStore; kernelQueue: KernelQueue; kernelFacetDeps: KernelFacetDependencies; + registerKernelService: (name: string, service: object) => { kref: string }; logger: Logger; }; @@ -76,12 +77,18 @@ export class SystemSubclusterManager { new Map(); /** Singleton kernel facet (created lazily, kept alive for GC purposes) */ - // eslint-disable-next-line no-unused-private-class-members + #kernelFacet: object | null = null; /** Kref of the singleton kernel facet */ #kernelFacetKref: KRef | null = null; + /** Function to register a kernel service */ + readonly #registerKernelService: ( + name: string, + service: object, + ) => { kref: string }; + /** * Creates a new SystemSubclusterManager instance. * @@ -89,17 +96,20 @@ export class SystemSubclusterManager { * @param options.kernelStore - The kernel's persistent state store. * @param options.kernelQueue - The kernel's message queue. * @param options.kernelFacetDeps - Dependencies for the kernel facet service. + * @param options.registerKernelService - Function to register kernel services. * @param options.logger - Logger instance for debugging and diagnostics. */ constructor({ kernelStore, kernelQueue, kernelFacetDeps, + registerKernelService, logger, }: SystemSubclusterManagerOptions) { this.#kernelStore = kernelStore; this.#kernelQueue = kernelQueue; this.#kernelFacetDeps = kernelFacetDeps; + this.#registerKernelService = registerKernelService; this.#logger = logger; harden(this); } @@ -127,14 +137,19 @@ export class SystemSubclusterManager { } /** - * Get the singleton kernel facet kref, creating it if necessary. + * Get the singleton kernel facet kref, creating and registering it if necessary. * * @returns The kref for the kernel facet. */ #getKernelFacetKref(): KRef { if (!this.#kernelFacetKref) { this.#kernelFacet = makeKernelFacet(this.#kernelFacetDeps); - this.#kernelFacetKref = this.#kernelStore.initKernelObject('kernel'); + // Register the kernel facet as a kernel service so it can receive messages + const { kref } = this.#registerKernelService( + 'kernelFacet', + this.#kernelFacet, + ); + this.#kernelFacetKref = kref; } return this.#kernelFacetKref; } @@ -152,8 +167,6 @@ export class SystemSubclusterManager { async connectSystemSubcluster( config: KernelSystemSubclusterConfig, ): Promise { - await this.#kernelQueue.waitForCrank(); - const bootstrapTransport = config.vatTransports.find( (vt) => vt.name === config.bootstrap, ); @@ -225,8 +238,10 @@ export class SystemSubclusterManager { roots[vatName] = kslot(kref, 'vatRoot'); } - // Build services object - const services: Record = {}; + // Build services object - always include kernelFacet + const services: Record = { + kernelFacet: kslot(kernelFacetKref, 'KernelFacet'), + }; if (config.services) { for (const serviceName of config.services) { const serviceKref = this.#kernelStore.kv.get( @@ -240,17 +255,21 @@ export class SystemSubclusterManager { } } - // Call bootstrap on the bootstrap vat's root object with kernelFacet as a presence + // Call bootstrap on the bootstrap vat's root object const bootstrapVatId = vatIds[config.bootstrap]; if (!bootstrapVatId) { throw new Error(`Bootstrap vat ID not found for ${config.bootstrap}`); } - await this.#kernelQueue.enqueueMessage( - rootKrefs[config.bootstrap] as KRef, - 'bootstrap', - [roots, services, kslot(kernelFacetKref, 'KernelFacet')], - ); + // Enqueue the bootstrap message without waiting for its result. + // We use enqueueSend (fire-and-forget) instead of enqueueMessage (which awaits result) + // because this is called during Kernel.make() before the run loop starts. + // The system vat will receive the bootstrap message once the run loop begins. + const bootstrapTarget = rootKrefs[config.bootstrap] as KRef; + const bootstrapArgs = [roots, services]; + this.#kernelQueue.enqueueSend(bootstrapTarget, { + methargs: kser(['bootstrap', bootstrapArgs]), + }); return { systemSubclusterId, diff --git a/packages/ocap-kernel/src/vats/SystemVatSupervisor.test.ts b/packages/ocap-kernel/src/vats/SystemVatSupervisor.test.ts index 333835c22..006315cbb 100644 --- a/packages/ocap-kernel/src/vats/SystemVatSupervisor.test.ts +++ b/packages/ocap-kernel/src/vats/SystemVatSupervisor.test.ts @@ -112,22 +112,7 @@ describe('SystemVatSupervisor', () => { ); }); - it('returns null on successful start', async () => { - const supervisor = new SystemVatSupervisor({ - id: systemVatId, - buildRootObject, - vatPowers, - parameters: undefined, - executeSyscall, - logger, - }); - - const result = await supervisor.start(); - - expect(result).toBeNull(); - }); - - it('returns error message on failed start', async () => { + it('throws on failed start', async () => { mockDispatch.mockRejectedValueOnce(new Error('start failed')); const supervisor = new SystemVatSupervisor({ @@ -139,13 +124,7 @@ describe('SystemVatSupervisor', () => { logger, }); - const result = await supervisor.start(); - - expect(result).toBe('start failed'); - expect(logger.error).toHaveBeenCalledWith( - expect.stringContaining('Start error'), - 'start failed', - ); + await expect(supervisor.start()).rejects.toThrow('start failed'); }); }); diff --git a/packages/ocap-kernel/src/vats/SystemVatSupervisor.ts b/packages/ocap-kernel/src/vats/SystemVatSupervisor.ts index fe857b6ee..1a694f925 100644 --- a/packages/ocap-kernel/src/vats/SystemVatSupervisor.ts +++ b/packages/ocap-kernel/src/vats/SystemVatSupervisor.ts @@ -325,26 +325,14 @@ export class SystemVatSupervisor { /** * Start the system vat by dispatching the startVat delivery. - * - * @returns A promise that resolves when the vat has started, or null on error. */ - async start(): Promise { + async start(): Promise { if (!this.#dispatch) { throw new Error('SystemVatSupervisor not initialized'); } - let deliveryError: string | null = null; - try { - const serParam = marshal.toCapData(harden({})) as CapData; - await this.#dispatch(harden(['startVat', serParam])); - } catch (error) { - deliveryError = error instanceof Error ? error.message : String(error); - this.#logger.error( - `Start error in system vat ${this.id}:`, - deliveryError, - ); - } - return deliveryError; + const serParam = marshal.toCapData(harden({})) as CapData; + await this.#dispatch(harden(['startVat', serParam])); } /** diff --git a/packages/ocap-kernel/test/integration/system-vat.test.ts b/packages/ocap-kernel/test/integration/system-vat.test.ts new file mode 100644 index 000000000..c50d76095 --- /dev/null +++ b/packages/ocap-kernel/test/integration/system-vat.test.ts @@ -0,0 +1,177 @@ +import { E } from '@endo/eventual-send'; +import { makePromiseKit } from '@endo/promise-kit'; +import type { JsonRpcMessage } from '@metamask/kernel-utils'; +import { makeDefaultExo } from '@metamask/kernel-utils/exo'; +import { Logger } from '@metamask/logger'; +import type { DuplexStream } from '@metamask/streams'; +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; + +import type { KernelFacet } from '../../src/kernel-facet.ts'; +import { Kernel } from '../../src/Kernel.ts'; +import type { + PlatformServices, + SystemVatBuildRootObject, + SystemVatTransport, + SystemVatSyscallHandler, + SystemVatDeliverFn, +} from '../../src/types.ts'; +import { + SystemVatSupervisor, + makeSyscallHandlerHolder, +} from '../../src/vats/SystemVatSupervisor.ts'; +import { makeMapKernelDatabase } from '../storage.ts'; + +/** + * Create a system vat transport and supervisor pair for testing. + * Uses a deferred pattern to handle the timing between kernel creation + * and supervisor startup. + * + * @param options - Options for creating the transport. + * @param options.buildRootObject - Function to build the root object. + * @param options.logger - Logger instance. + * @returns The transport config and start function. + */ +function makeTestSystemVat(options: { + buildRootObject: SystemVatBuildRootObject; + logger: Logger; +}): { + transport: SystemVatTransport; + start: () => Promise; +} { + const { buildRootObject, logger } = options; + + // Create syscall handler holder for deferred wiring + const syscallHandlerHolder = makeSyscallHandlerHolder(); + + // Promise kit to signal when supervisor is ready + const supervisorReady = makePromiseKit(); + + // Create the transport with a deliver function that waits for the supervisor + const deliver: SystemVatDeliverFn = async (delivery) => { + const supervisor = await supervisorReady.promise; + return supervisor.deliver(delivery); + }; + + const transport: SystemVatTransport = { + deliver, + setSyscallHandler: (handler: SystemVatSyscallHandler) => { + syscallHandlerHolder.handler = handler; + }, + }; + + const start = async () => { + const supervisor = new SystemVatSupervisor({ + id: 'sv-test' as `sv${number}`, + buildRootObject, + vatPowers: {}, + parameters: undefined, + syscallHandlerHolder, + logger: logger.subLogger({ tags: ['supervisor'] }), + }); + + await supervisor.start(); + + // Signal that the supervisor is ready + supervisorReady.resolve(supervisor); + }; + + return { transport, start }; +} + +describe('system vat integration', { timeout: 30_000 }, () => { + let kernel: Kernel; + let kernelFacet: KernelFacet; + + beforeEach(async () => { + const logger = new Logger('test'); + + // Captured kernel facet from bootstrap + let capturedKernelFacet: KernelFacet | null = null; + + // Build root object that captures the kernel facet from services + const buildRootObject: SystemVatBuildRootObject = () => { + return makeDefaultExo('TestRoot', { + bootstrap: ( + _roots: Record, + services: { kernelFacet: KernelFacet }, + ) => { + capturedKernelFacet = services.kernelFacet; + }, + }); + }; + + // Create the system vat transport and supervisor + const systemVat = makeTestSystemVat({ + buildRootObject, + logger: logger.subLogger({ tags: ['system-vat'] }), + }); + + // Create mock platform services + const mockPlatformServices: PlatformServices = { + launch: vi.fn().mockResolvedValue({ + end: vi.fn(), + } as unknown as DuplexStream), + terminate: vi.fn().mockResolvedValue(undefined), + terminateAll: vi.fn().mockResolvedValue(undefined), + stopRemoteComms: vi.fn().mockResolvedValue(undefined), + } as unknown as PlatformServices; + + // Create kernel with system subcluster config + const kernelDatabase = makeMapKernelDatabase(); + kernel = await Kernel.make(mockPlatformServices, kernelDatabase, { + resetStorage: true, + logger: logger.subLogger({ tags: ['kernel'] }), + systemSubclusters: { + subclusters: [ + { + bootstrap: 'testVat', + vatTransports: [ + { + name: 'testVat', + transport: systemVat.transport, + }, + ], + }, + ], + }, + }); + + // Start the supervisor - this unblocks the deliver function + await systemVat.start(); + + // Wait for the bootstrap message to be delivered and processed + await vi.waitFor( + () => { + if (!capturedKernelFacet) { + throw new Error('Waiting for kernel facet...'); + } + }, + { timeout: 5000, interval: 50 }, + ); + + kernelFacet = capturedKernelFacet!; + }); + + afterEach(async () => { + await kernel.clearStorage(); + }); + + describe('kernel facet', () => { + it.todo('gets kernel status via E()', async () => { + // TODO: Need to make getStatus stop waiting for crank + const status = await E(kernelFacet).getStatus(); + + expect(status).toBeDefined(); + expect(status.vats).toBeDefined(); + expect(status.subclusters).toBeDefined(); + expect(status.remoteComms).toBeDefined(); + }); + + it('gets subclusters via E()', async () => { + const subclusters = await E(kernelFacet).getSubclusters(); + + expect(subclusters).toBeDefined(); + expect(Array.isArray(subclusters)).toBe(true); + }); + }); +}); diff --git a/packages/ocap-kernel/tsconfig.json b/packages/ocap-kernel/tsconfig.json index d1a3ce3c9..994dded08 100644 --- a/packages/ocap-kernel/tsconfig.json +++ b/packages/ocap-kernel/tsconfig.json @@ -18,6 +18,7 @@ "./src", "./test", "./vite.config.ts", - "./vitest.config.ts" + "./vitest.config.ts", + "./vitest.integration.config.ts" ] } diff --git a/packages/ocap-kernel/vitest.integration.config.ts b/packages/ocap-kernel/vitest.integration.config.ts new file mode 100644 index 000000000..22520bf64 --- /dev/null +++ b/packages/ocap-kernel/vitest.integration.config.ts @@ -0,0 +1,29 @@ +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-integration', + include: ['**/test/integration/**'], + setupFiles: [ + fileURLToPath( + import.meta.resolve('@metamask/kernel-shims/endoify-node'), + ), + ], + }, + }), + ); + + delete config.test?.coverage; + + return config; +}); diff --git a/yarn.lock b/yarn.lock index 084a341ef..88755a787 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2704,6 +2704,7 @@ __metadata: "@chainsafe/libp2p-noise": "npm:^16.1.3" "@chainsafe/libp2p-yamux": "patch:@chainsafe/libp2p-yamux@npm%3A7.0.4#~/.yarn/patches/@chainsafe-libp2p-yamux-npm-7.0.4-284c2f6812.patch" "@endo/errors": "npm:^1.2.13" + "@endo/eventual-send": "npm:^1.3.4" "@endo/marshal": "npm:^1.8.0" "@endo/pass-style": "npm:^1.6.3" "@endo/promise-kit": "npm:^1.1.13" From 7ec9fd938928423cdb6d215ff28bac6abbbe35c8 Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Wed, 28 Jan 2026 22:34:22 -0800 Subject: [PATCH 19/41] fix(ocap-kernel): Avoid deadlock in E(kernelFacet) calls from within cranks Changed invokeKernelService to not await the service method result. Instead, it uses Promise chaining to resolve the kernel promise when the method eventually completes. This allows service methods to internally use waitForCrank() without causing deadlock - the crank can complete, and the resolution happens in a future turn of the event loop. Key changes: - KernelServiceManager.invokeKernelService() now returns void instead of Promise and uses Promise.resolve().then().catch() for async handling - KernelRouter.#deliverKernelServiceMessage() is now synchronous - Updated tests to use delay() for microtask flushing - Enabled the integration test for E(kernelFacet).getStatus() Co-Authored-By: Claude --- packages/nodejs/src/host-subcluster/index.ts | 9 +-- packages/ocap-kernel/src/Kernel.ts | 3 + packages/ocap-kernel/src/KernelRouter.ts | 16 ++--- .../src/KernelServiceManager.test.ts | 59 ++++++++++++++----- .../ocap-kernel/src/KernelServiceManager.ts | 46 +++++++++++---- .../test/integration/system-vat.test.ts | 3 +- 6 files changed, 92 insertions(+), 44 deletions(-) diff --git a/packages/nodejs/src/host-subcluster/index.ts b/packages/nodejs/src/host-subcluster/index.ts index eaa2b8807..daa5a34a2 100644 --- a/packages/nodejs/src/host-subcluster/index.ts +++ b/packages/nodejs/src/host-subcluster/index.ts @@ -132,13 +132,8 @@ export function makeHostSubcluster( logger: logger.subLogger({ tags: ['supervisor'] }), }); - // Start the supervisor (dispatches startVat) - const startError = await supervisor.start(); - if (startError) { - throw new Error( - `Failed to start host subcluster supervisor: ${startError}`, - ); - } + // Start the supervisor (dispatches startVat) - throws on failure + await supervisor.start(); }, getKernelFacet: () => { diff --git a/packages/ocap-kernel/src/Kernel.ts b/packages/ocap-kernel/src/Kernel.ts index 3c7def1aa..956bbbe93 100644 --- a/packages/ocap-kernel/src/Kernel.ts +++ b/packages/ocap-kernel/src/Kernel.ts @@ -513,6 +513,9 @@ export class Kernel { * Get the current kernel status, defined as the current cluster configuration * and a list of all running vats. * + * Returns a promise that resolves in a future crank to avoid deadlock when + * called from within a crank (e.g., via E(kernelFacet).getStatus()). + * * @returns A promise for the current kernel status containing vats, subclusters, and remote comms information. */ async getStatus(): Promise { diff --git a/packages/ocap-kernel/src/KernelRouter.ts b/packages/ocap-kernel/src/KernelRouter.ts index d26b82ea1..2e7c2a8ad 100644 --- a/packages/ocap-kernel/src/KernelRouter.ts +++ b/packages/ocap-kernel/src/KernelRouter.ts @@ -45,10 +45,7 @@ export class KernelRouter { readonly #getEndpoint: (endpointId: EndpointId) => EndpointHandle; /** A function that invokes a method on a kernel service. */ - readonly #invokeKernelService: ( - target: KRef, - message: Message, - ) => Promise; + readonly #invokeKernelService: (target: KRef, message: Message) => void; /** The logger, if any. */ readonly #logger: Logger | undefined; @@ -66,7 +63,7 @@ export class KernelRouter { kernelStore: KernelStore, kernelQueue: KernelQueue, getEndpoint: (endpointId: EndpointId) => EndpointHandle, - invokeKernelService: (target: KRef, message: Message) => Promise, + invokeKernelService: (target: KRef, message: Message) => void, logger?: Logger, ) { this.#kernelStore = kernelStore; @@ -266,7 +263,7 @@ export class KernelRouter { // Continue processing other messages - don't let one failure crash the queue } } else if (isKernelServiceMessage) { - crankResults = await this.#deliverKernelServiceMessage(target, message); + crankResults = this.#deliverKernelServiceMessage(target, message); } else { Fail`no owner for kernel object ${target}`; } @@ -288,11 +285,8 @@ export class KernelRouter { * @param message - The message to deliver to the service. * @returns A promise that resolves to the crank results indicating the delivery was to the kernel. */ - async #deliverKernelServiceMessage( - target: KRef, - message: Message, - ): Promise { - await this.#invokeKernelService(target, message); + #deliverKernelServiceMessage(target: KRef, message: Message): CrankResults { + this.#invokeKernelService(target, message); return { didDelivery: 'kernel' }; } diff --git a/packages/ocap-kernel/src/KernelServiceManager.test.ts b/packages/ocap-kernel/src/KernelServiceManager.test.ts index 0334437b2..f24cc1bd4 100644 --- a/packages/ocap-kernel/src/KernelServiceManager.test.ts +++ b/packages/ocap-kernel/src/KernelServiceManager.test.ts @@ -1,3 +1,4 @@ +import { delay } from '@metamask/kernel-utils'; import { Logger } from '@metamask/logger'; import { describe, it, expect, beforeEach, vi } from 'vitest'; @@ -173,7 +174,8 @@ describe('KernelServiceManager', () => { methargs: kser(['testMethod', ['arg1', 'arg2']]), }; - await serviceManager.invokeKernelService(registered.kref, message); + serviceManager.invokeKernelService(registered.kref, message); + await delay(); expect(testMethod).toHaveBeenCalledWith('arg1', 'arg2'); expect(mockKernelQueue.resolvePromises).not.toHaveBeenCalled(); @@ -195,7 +197,8 @@ describe('KernelServiceManager', () => { result: 'kp123', }; - await serviceManager.invokeKernelService(registered.kref, message); + serviceManager.invokeKernelService(registered.kref, message); + await delay(); expect(testMethod).toHaveBeenCalledWith('arg1'); expect(mockKernelQueue.resolvePromises).toHaveBeenCalledWith('kernel', [ @@ -220,7 +223,8 @@ describe('KernelServiceManager', () => { result: 'kp123', }; - await serviceManager.invokeKernelService(registered.kref, message); + serviceManager.invokeKernelService(registered.kref, message); + await delay(); expect(mockKernelQueue.resolvePromises).toHaveBeenCalledWith('kernel', [ ['kp123', true, kser(testError)], @@ -244,7 +248,8 @@ describe('KernelServiceManager', () => { methargs: kser(['testMethod', []]), }; - await serviceManager.invokeKernelService(registered.kref, message); + serviceManager.invokeKernelService(registered.kref, message); + await delay(); expect(loggerErrorSpy).toHaveBeenCalledWith( 'Error in kernel service method:', @@ -253,17 +258,17 @@ describe('KernelServiceManager', () => { expect(mockKernelQueue.resolvePromises).not.toHaveBeenCalled(); }); - it('throws error for non-existent service', async () => { + it('throws error for non-existent service', () => { const message: Message = { methargs: kser(['testMethod', []]), }; - await expect( + expect(() => serviceManager.invokeKernelService('ko999', message), - ).rejects.toThrow('No registered service for ko999'); + ).toThrow('No registered service for ko999'); }); - it('handles unknown method with result', async () => { + it('handles unknown method with result', () => { const testService = { existingMethod: () => 'test', }; @@ -278,14 +283,14 @@ describe('KernelServiceManager', () => { result: 'kp123', }; - await serviceManager.invokeKernelService(registered.kref, message); + serviceManager.invokeKernelService(registered.kref, message); expect(mockKernelQueue.resolvePromises).toHaveBeenCalledWith('kernel', [ ['kp123', true, kser(Error("unknown service method 'unknownMethod'"))], ]); }); - it('handles unknown method without result', async () => { + it('handles unknown method without result', () => { const loggerErrorSpy = vi.spyOn(logger, 'error'); const testService = { existingMethod: () => 'test', @@ -300,7 +305,7 @@ describe('KernelServiceManager', () => { methargs: kser(['unknownMethod', []]), }; - await serviceManager.invokeKernelService(registered.kref, message); + serviceManager.invokeKernelService(registered.kref, message); expect(loggerErrorSpy).toHaveBeenCalledWith( "unknown service method 'unknownMethod'", @@ -308,7 +313,7 @@ describe('KernelServiceManager', () => { expect(mockKernelQueue.resolvePromises).not.toHaveBeenCalled(); }); - it('handles service with no methods', async () => { + it('handles service with no methods', () => { const emptyService = {}; const registered = serviceManager.registerKernelServiceObject( @@ -321,7 +326,7 @@ describe('KernelServiceManager', () => { result: 'kp123', }; - await serviceManager.invokeKernelService(registered.kref, message); + serviceManager.invokeKernelService(registered.kref, message); expect(mockKernelQueue.resolvePromises).toHaveBeenCalledWith('kernel', [ ['kp123', true, kser(Error("unknown service method 'anyMethod'"))], @@ -343,11 +348,37 @@ describe('KernelServiceManager', () => { result: 'kp123', }; - await serviceManager.invokeKernelService(registered.kref, message); + serviceManager.invokeKernelService(registered.kref, message); + await delay(); expect(mockKernelQueue.resolvePromises).toHaveBeenCalledWith('kernel', [ ['kp123', false, kser(undefined)], ]); }); + + it('handles synchronous errors thrown by service method', () => { + const testError = new Error('Sync error'); + const testService = { + throwingMethod: () => { + throw testError; + }, + }; + + const registered = serviceManager.registerKernelServiceObject( + 'testService', + testService, + ); + + const message: Message = { + methargs: kser(['throwingMethod', []]), + result: 'kp123', + }; + + serviceManager.invokeKernelService(registered.kref, message); + + expect(mockKernelQueue.resolvePromises).toHaveBeenCalledWith('kernel', [ + ['kp123', true, kser(testError)], + ]); + }); }); }); diff --git a/packages/ocap-kernel/src/KernelServiceManager.ts b/packages/ocap-kernel/src/KernelServiceManager.ts index 5fcb74fb8..ccfb1f904 100644 --- a/packages/ocap-kernel/src/KernelServiceManager.ts +++ b/packages/ocap-kernel/src/KernelServiceManager.ts @@ -109,10 +109,16 @@ export class KernelServiceManager { /** * Invoke a kernel service. * + * This method does NOT await the service method result. Instead, it uses + * promise chaining to resolve the kernel promise when the method eventually + * completes. This allows service methods to use `waitForCrank()` without + * causing deadlock - the crank can complete, and the resolution happens + * in a future turn of the event loop. + * * @param target - The target kref of the service. * @param message - The message to invoke the service with. */ - async invokeKernelService(target: KRef, message: Message): Promise { + invokeKernelService(target: KRef, message: Message): void { const kernelService = this.#kernelServicesByObject.get(target); if (!kernelService) { throw Error(`No registered service for ${target}`); @@ -138,20 +144,40 @@ export class KernelServiceManager { } assert.typeof(methodFunction, 'function'); assert(Array.isArray(args)); + + // Call the method without awaiting. This allows the crank to complete + // even if the method internally waits for the crank to end. try { - const resultValue = await methodFunction.apply(service, args); - if (result) { - this.#kernelQueue.resolvePromises('kernel', [ - [result, false, kser(resultValue)], - ]); - } - } catch (problem) { + const maybePromise = methodFunction.apply(service, args); + // Use Promise.resolve to normalize: if maybePromise is a Promise, it + // returns that Promise; if it's a value, it returns an immediately- + // resolved Promise. + Promise.resolve(maybePromise) + .then((resultValue) => { + if (result) { + this.#kernelQueue.resolvePromises('kernel', [ + [result, false, kser(resultValue)], + ]); + } + return undefined; + }) + .catch((problem: unknown) => { + if (result) { + this.#kernelQueue.resolvePromises('kernel', [ + [result, true, kser(problem)], + ]); + } else { + this.#logger?.error('Error in kernel service method:', problem); + } + }); + } catch (syncError) { + // Handle synchronous errors thrown before returning a Promise if (result) { this.#kernelQueue.resolvePromises('kernel', [ - [result, true, kser(problem)], + [result, true, kser(syncError)], ]); } else { - this.#logger?.error('Error in kernel service method:', problem); + this.#logger?.error('Error in kernel service method:', syncError); } } } diff --git a/packages/ocap-kernel/test/integration/system-vat.test.ts b/packages/ocap-kernel/test/integration/system-vat.test.ts index c50d76095..dc28f0ccb 100644 --- a/packages/ocap-kernel/test/integration/system-vat.test.ts +++ b/packages/ocap-kernel/test/integration/system-vat.test.ts @@ -157,8 +157,7 @@ describe('system vat integration', { timeout: 30_000 }, () => { }); describe('kernel facet', () => { - it.todo('gets kernel status via E()', async () => { - // TODO: Need to make getStatus stop waiting for crank + it('gets kernel status via E()', async () => { const status = await E(kernelFacet).getStatus(); expect(status).toBeDefined(); From 1bc692c3108a89488636cd3dea3f27b2ebfa9c98 Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Wed, 28 Jan 2026 23:07:33 -0800 Subject: [PATCH 20/41] fix: Use ES2022 target to fix privateMap.get error under SES/lockdown TypeScript's WeakMap-based private field polyfills (used with target ES2020) caused "privateMap.get is not a function" errors when running under SES/lockdown in vitest fork mode. Changing target to ES2022 uses native private fields which work correctly with lockdown. Also fixes: - Bootstrap signature in host-subcluster (kernelFacet is in services) - Test for supervisor.start() throwing instead of returning error - Removes obsolete system-subcluster-simple.test.ts using old API Co-Authored-By: Claude --- .../nodejs/src/host-subcluster/index.test.ts | 8 +-- packages/nodejs/src/host-subcluster/index.ts | 8 +-- .../test/e2e/system-subcluster-simple.test.ts | 65 ------------------- tsconfig.base.json | 2 +- 4 files changed, 8 insertions(+), 75 deletions(-) delete mode 100644 packages/nodejs/test/e2e/system-subcluster-simple.test.ts diff --git a/packages/nodejs/src/host-subcluster/index.test.ts b/packages/nodejs/src/host-subcluster/index.test.ts index 2fbb539f4..a4ab132db 100644 --- a/packages/nodejs/src/host-subcluster/index.test.ts +++ b/packages/nodejs/src/host-subcluster/index.test.ts @@ -61,14 +61,12 @@ describe('makeHostSubcluster', () => { expect(mockStart).toHaveBeenCalled(); }); - it('throws if supervisor start returns error', async () => { - mockStart.mockResolvedValueOnce('Some error'); + it('throws if supervisor start throws', async () => { + mockStart.mockRejectedValueOnce(new Error('Start failed')); const { start } = makeHostSubcluster(); - await expect(start()).rejects.toThrow( - 'Failed to start host subcluster supervisor: Some error', - ); + await expect(start()).rejects.toThrow('Start failed'); }); }); diff --git a/packages/nodejs/src/host-subcluster/index.ts b/packages/nodejs/src/host-subcluster/index.ts index daa5a34a2..f6e7d89ab 100644 --- a/packages/nodejs/src/host-subcluster/index.ts +++ b/packages/nodejs/src/host-subcluster/index.ts @@ -76,13 +76,13 @@ export function makeHostSubcluster( // Build root object that receives kernelFacet via bootstrap message const buildRootObject: SystemVatBuildRootObject = () => { return makeDefaultExo('KernelHostRoot', { - // Bootstrap is called by the kernel with kernelFacet as a presence + // Bootstrap is called by the kernel with roots and services. + // kernelFacet is always included in services. bootstrap: ( _roots: Record, - _services: Record, - kernelFacet: KernelFacet, + services: { kernelFacet: KernelFacet }, ) => { - capturedKernelFacet = kernelFacet; + capturedKernelFacet = services.kernelFacet; }, }); }; diff --git a/packages/nodejs/test/e2e/system-subcluster-simple.test.ts b/packages/nodejs/test/e2e/system-subcluster-simple.test.ts deleted file mode 100644 index bb93768dc..000000000 --- a/packages/nodejs/test/e2e/system-subcluster-simple.test.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { makeDefaultExo } from '@metamask/kernel-utils/exo'; -import type { - Kernel, - ClusterConfig, - SystemSubclusterConfig, -} from '@metamask/ocap-kernel'; -import { describe, it, expect, beforeEach, afterEach } from 'vitest'; - -import { makeKernel } from '../../src/kernel/make-kernel.ts'; - -describe('system subcluster simple tests', { timeout: 60_000 }, () => { - let kernel: Kernel; - - beforeEach(async () => { - kernel = await makeKernel({}); - }); - - afterEach(async () => { - await kernel.clearStorage(); - }); - - // First test: verify regular dynamic subcluster works - it('launches a regular dynamic subcluster', async () => { - const config: ClusterConfig = { - bootstrap: 'bob', - vats: { - bob: { - bundleSpec: 'http://localhost:3000/bob-vat.bundle', - }, - }, - }; - - const result = await kernel.launchSubcluster(config); - expect(result.subclusterId).toBeDefined(); - expect(result.bootstrapRootKref).toBeDefined(); - }); - - // Second test: verify we can get kernel status - it('gets kernel status', async () => { - const status = await kernel.getStatus(); - expect(status).toBeDefined(); - expect(status.subclusters).toBeDefined(); - }); - - // Third test: launch a system subcluster - it('launches a system subcluster', async () => { - const config: SystemSubclusterConfig = { - bootstrap: 'testVat', - vats: { - testVat: { - buildRootObject: (_vatPowers, _params) => { - return makeDefaultExo('testRoot', { - bootstrap: () => undefined, - ping: () => 'pong', - }); - }, - }, - }, - }; - - const result = await kernel.launchSystemSubcluster(config); - expect(result.systemSubclusterId).toBeDefined(); - expect(result.vatIds.testVat).toBe('sv0'); - }); -}); diff --git a/tsconfig.base.json b/tsconfig.base.json index 397922e88..c3edaf4be 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -13,7 +13,7 @@ "resolveJsonModule": true, "rewriteRelativeImportExtensions": true, "strict": true, - "target": "ES2020", + "target": "ES2022", "verbatimModuleSyntax": true, "useUnknownInCatchVariables": true } From 3a94d2a46e7ddfed2a0b15b0aaf5bd6864775517 Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Wed, 28 Jan 2026 23:19:12 -0800 Subject: [PATCH 21/41] test(nodejs): Fix e2e tests for system subclusters - Use vi.waitFor() to poll for kernel facet availability instead of non-existent kernel.run() method - Fix all kernelFacet method calls to use E() since it's a presence - Fix promise rejection test to use rejects.toThrow() - Update host-subcluster deliver function to wait for supervisor using promise kit pattern instead of throwing Co-Authored-By: Claude --- .../nodejs/src/host-subcluster/index.test.ts | 27 ++++++--------- packages/nodejs/src/host-subcluster/index.ts | 16 +++++---- .../nodejs/test/e2e/system-subcluster.test.ts | 34 +++++++++++-------- 3 files changed, 39 insertions(+), 38 deletions(-) diff --git a/packages/nodejs/src/host-subcluster/index.test.ts b/packages/nodejs/src/host-subcluster/index.test.ts index a4ab132db..1472644a0 100644 --- a/packages/nodejs/src/host-subcluster/index.test.ts +++ b/packages/nodejs/src/host-subcluster/index.test.ts @@ -81,22 +81,8 @@ describe('makeHostSubcluster', () => { }); describe('transport', () => { - it('deliver throws if supervisor not initialized', async () => { - const { config } = makeHostSubcluster(); - - await expect( - config.vatTransports[0]?.transport.deliver({ - type: 'message', - methargs: { body: '[]', slots: [] }, - result: 'p-1', - target: 'o+0', - }), - ).rejects.toThrow('Supervisor not initialized'); - }); - - it('deliver calls supervisor after start', async () => { + it('deliver waits for supervisor then calls it', async () => { const { config, start } = makeHostSubcluster(); - await start(); const delivery = { type: 'message' as const, @@ -104,7 +90,16 @@ describe('makeHostSubcluster', () => { result: 'p-1', target: 'o+0', }; - await config.vatTransports[0]?.transport.deliver(delivery); + + // Start the delivery (it will wait for supervisor) + const deliverPromise = + config.vatTransports[0]?.transport.deliver(delivery); + + // Start the supervisor - this should unblock the delivery + await start(); + + // Now the delivery should complete + await deliverPromise; expect(mockDeliver).toHaveBeenCalledWith(delivery); }); diff --git a/packages/nodejs/src/host-subcluster/index.ts b/packages/nodejs/src/host-subcluster/index.ts index f6e7d89ab..57cd7faf4 100644 --- a/packages/nodejs/src/host-subcluster/index.ts +++ b/packages/nodejs/src/host-subcluster/index.ts @@ -1,3 +1,4 @@ +import { makePromiseKit } from '@endo/promise-kit'; import { makeDefaultExo } from '@metamask/kernel-utils/exo'; import { Logger } from '@metamask/logger'; import type { @@ -87,14 +88,12 @@ export function makeHostSubcluster( }); }; - // Create the supervisor - let supervisor: SystemVatSupervisor | null = null; + // Promise kit to signal when supervisor is ready + const supervisorReady = makePromiseKit(); - // Create the transport + // Create the transport with a deliver function that waits for the supervisor const deliver: SystemVatDeliverFn = async (delivery) => { - if (!supervisor) { - throw new Error('Supervisor not initialized'); - } + const supervisor = await supervisorReady.promise; return supervisor.deliver(delivery); }; @@ -121,7 +120,7 @@ export function makeHostSubcluster( start: async () => { // Create the supervisor - supervisor = new SystemVatSupervisor({ + const supervisor = new SystemVatSupervisor({ // The kernel assigns the actual ID via the transport // This placeholder is only used for logging id: 'sv0' as `sv${number}`, @@ -134,6 +133,9 @@ export function makeHostSubcluster( // Start the supervisor (dispatches startVat) - throws on failure await supervisor.start(); + + // Signal that the supervisor is ready - this unblocks any pending deliveries + supervisorReady.resolve(supervisor); }, getKernelFacet: () => { diff --git a/packages/nodejs/test/e2e/system-subcluster.test.ts b/packages/nodejs/test/e2e/system-subcluster.test.ts index e0d6265bc..d7a52dffb 100644 --- a/packages/nodejs/test/e2e/system-subcluster.test.ts +++ b/packages/nodejs/test/e2e/system-subcluster.test.ts @@ -2,7 +2,7 @@ import { E } from '@endo/eventual-send'; import { makeSQLKernelDatabase } from '@metamask/kernel-store/sqlite/nodejs'; import { Logger } from '@metamask/logger'; import type { Kernel, ClusterConfig, KernelFacet } from '@metamask/ocap-kernel'; -import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import { makeHostSubcluster } from '../../src/host-subcluster/index.ts'; import { NodejsPlatformServices } from '../../src/kernel/PlatformServices.ts'; @@ -54,11 +54,13 @@ describe('system subcluster e2e tests', { timeout: 30_000 }, () => { // Start host subcluster supervisor after kernel is created await hostSubcluster.start(); - // Run the kernel to process bootstrap message - await kernel.run(); - - // Get the kernel facet - kernelFacet = hostSubcluster.getKernelFacet(); + // Wait for the bootstrap message to be delivered and kernel facet to be available + await vi.waitFor( + () => { + kernelFacet = hostSubcluster.getKernelFacet(); + }, + { timeout: 5000, interval: 50 }, + ); }); afterEach(async () => { @@ -106,12 +108,14 @@ describe('system subcluster e2e tests', { timeout: 30_000 }, () => { }; const result = await E(kernelFacet).launchSubcluster(config); - const subcluster = kernelFacet.getSubcluster(result.subclusterId); + const subcluster = await E(kernelFacet).getSubcluster( + result.subclusterId, + ); expect(subcluster).toBeDefined(); await E(kernelFacet).terminateSubcluster(result.subclusterId); - const terminatedSubcluster = kernelFacet.getSubcluster( + const terminatedSubcluster = await E(kernelFacet).getSubcluster( result.subclusterId, ); expect(terminatedSubcluster).toBeUndefined(); @@ -137,14 +141,16 @@ describe('system subcluster e2e tests', { timeout: 30_000 }, () => { const bob = launchResult.root as Bob; // Get Carol's root object - const subcluster = kernelFacet.getSubcluster(launchResult.subclusterId); + const subcluster = await E(kernelFacet).getSubcluster( + launchResult.subclusterId, + ); expect(subcluster).toBeDefined(); const carolVatId = subcluster?.vats.find( (vatId) => vatId !== subcluster.vats[0], ); expect(carolVatId).toBeDefined(); const carolKref = kernel.pinVatRoot(carolVatId!); - const carol = kernelFacet.getVatRoot(carolKref) as Carol; + const carol = (await E(kernelFacet).getVatRoot(carolKref)) as Carol; // Host orchestrates: get exo from Bob, pass to Carol const greeter = await E(bob).makeGreeter('Greetings'); @@ -254,10 +260,8 @@ describe('system subcluster e2e tests', { timeout: 30_000 }, () => { // Reject it await E(promiseVat).rejectDeferredPromise('error reason'); - // Rejections from vats are delivered as Error objects - const rejection = await deferredPromise; - expect(rejection).toBeInstanceOf(Error); - expect((rejection as Error).message).toBe('error reason'); + // Rejections from vats throw errors + await expect(deferredPromise).rejects.toThrow('error reason'); }); }); @@ -277,7 +281,7 @@ describe('system subcluster e2e tests', { timeout: 30_000 }, () => { const storedKref = result.rootKref; // Later: restore presence from kref - const restoredBob = kernelFacet.getVatRoot(storedKref) as Bob; + const restoredBob = (await E(kernelFacet).getVatRoot(storedKref)) as Bob; // The restored presence should be E()-callable const greeter = await E(restoredBob).makeGreeter('Restored'); From 97e4642b083e07450b8e38a88fd430ea42db30c5 Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Thu, 29 Jan 2026 13:40:54 -0800 Subject: [PATCH 22/41] refactor(ocap-kernel): Use push-based system vat connections Change system vat connection model from pull-based (kernel reaches out) to push-based (supervisor initiates connection). This architectural change ensures the kernel is passive and only accepts pre-declared connections. Key changes: - Rename connectSystemSubcluster() to prepareSystemSubcluster() - Make prepareSystemSubcluster() synchronous (returns immediately) - Replace transport.onReady with transport.awaitConnection() - Add connect() method to HostSubclusterResult - Bootstrap message sent after all transports' awaitConnection() resolve New flow: 1. makeHostSubcluster() returns { config, connect, kernelFacetPromise } 2. Kernel.make() calls prepareSystemSubcluster() (non-blocking) 3. After Kernel.make() returns, call hostSubcluster.connect() 4. Supervisor starts and resolves awaitConnection() 5. Kernel sends bootstrap message, kernelFacetPromise resolves Co-Authored-By: Claude --- packages/nodejs/src/host-subcluster/index.ts | 94 ++++++------ .../nodejs/test/e2e/system-subcluster.test.ts | 40 +++-- packages/ocap-kernel/src/Kernel.ts | 8 +- packages/ocap-kernel/src/types.ts | 13 ++ .../src/vats/SystemSubclusterManager.test.ts | 141 ++++++++++-------- .../src/vats/SystemSubclusterManager.ts | 60 +++++--- .../src/vats/SystemVatSupervisor.ts | 55 ++++++- packages/ocap-kernel/src/vats/index.ts | 1 + .../test/integration/system-vat.test.ts | 111 +++++++------- 9 files changed, 306 insertions(+), 217 deletions(-) diff --git a/packages/nodejs/src/host-subcluster/index.ts b/packages/nodejs/src/host-subcluster/index.ts index 57cd7faf4..841c79eda 100644 --- a/packages/nodejs/src/host-subcluster/index.ts +++ b/packages/nodejs/src/host-subcluster/index.ts @@ -24,26 +24,25 @@ export type HostSubclusterResult = { config: KernelSystemSubclusterConfig; /** - * Start the supervisor. Call after Kernel.make() returns. - * - * @returns A promise that resolves when the supervisor is started. + * Call after Kernel.make() returns to initiate connection from supervisor side. + * This creates and starts the supervisor, then signals the kernel that + * the connection is ready. The kernel will then send the bootstrap message. */ - start: () => Promise; + connect: () => void; /** - * Get the kernel facet (available after bootstrap is called by kernel). - * - * @returns The kernel facet presence for making E() calls. + * Promise that resolves to kernelFacet when bootstrap completes. + * No polling needed - just await this promise after calling connect(). */ - getKernelFacet: () => KernelFacet; + kernelFacetPromise: Promise; }; /** * Create a host subcluster for use with Kernel.make(). * * This creates the supervisor and transport configuration needed to connect - * a host subcluster to the kernel. The supervisor is created in this process, - * and the transport allows the kernel to communicate with it. + * a host subcluster to the kernel. The supervisor is created when `connect()` + * is called (after Kernel.make() returns). * * Usage: * ```typescript @@ -51,14 +50,14 @@ export type HostSubclusterResult = { * const kernel = await Kernel.make(platformServices, db, { * systemSubclusters: { subclusters: [hostSubcluster.config] }, * }); - * await hostSubcluster.start(); - * const kernelFacet = hostSubcluster.getKernelFacet(); + * hostSubcluster.connect(); // Supervisor pushes connection to kernel + * const kernelFacet = await hostSubcluster.kernelFacetPromise; * const result = await E(kernelFacet).launchSubcluster(config); * ``` * * @param options - Options for creating the host subcluster. * @param options.logger - Optional logger for the supervisor. - * @returns The host subcluster result with config and initialization functions. + * @returns The host subcluster result with config, connect, and kernelFacetPromise. */ export function makeHostSubcluster( options: { @@ -68,13 +67,13 @@ export function makeHostSubcluster( const logger = options.logger ?? new Logger('host-subcluster'); const vatName = 'kernelHost'; - // Captured kernel facet from bootstrap message - let capturedKernelFacet: KernelFacet | null = null; + // Promise kit for kernel facet - resolves when bootstrap is called + const kernelFacetKit = makePromiseKit(); // Create syscall handler holder for deferred wiring const syscallHandlerHolder = makeSyscallHandlerHolder(); - // Build root object that receives kernelFacet via bootstrap message + // Build root object that captures kernelFacet from bootstrap const buildRootObject: SystemVatBuildRootObject = () => { return makeDefaultExo('KernelHostRoot', { // Bootstrap is called by the kernel with roots and services. @@ -83,14 +82,17 @@ export function makeHostSubcluster( _roots: Record, services: { kernelFacet: KernelFacet }, ) => { - capturedKernelFacet = services.kernelFacet; + kernelFacetKit.resolve(services.kernelFacet); }, }); }; - // Promise kit to signal when supervisor is ready + // Promise kit to signal when supervisor is ready to receive deliveries const supervisorReady = makePromiseKit(); + // Promise kit for connection - resolved when connect() is called and supervisor is ready + const connectionKit = makePromiseKit(); + // Create the transport with a deliver function that waits for the supervisor const deliver: SystemVatDeliverFn = async (delivery) => { const supervisor = await supervisorReady.promise; @@ -102,6 +104,31 @@ export function makeHostSubcluster( setSyscallHandler: (handler: SystemVatSyscallHandler) => { syscallHandlerHolder.handler = handler; }, + // Kernel calls this to wait for connection from supervisor side + awaitConnection: async () => connectionKit.promise, + }; + + /** + * Called after Kernel.make() returns to initiate connection from supervisor side. + * Creates and starts the supervisor, then resolves the connection promise. + */ + const connect = (): void => { + // Create and start the supervisor + SystemVatSupervisor.make({ + buildRootObject, + syscallHandlerHolder, + logger: logger.subLogger({ tags: ['supervisor'] }), + }) + .then((supervisor) => { + supervisorReady.resolve(supervisor); + // Signal connection ready - kernel will now send bootstrap message + connectionKit.resolve(); + return undefined; + }) + .catch((error) => { + connectionKit.reject(error as Error); + kernelFacetKit.reject(error as Error); + }); }; // Config for Kernel.make() @@ -117,35 +144,8 @@ export function makeHostSubcluster( return harden({ config, - - start: async () => { - // Create the supervisor - const supervisor = new SystemVatSupervisor({ - // The kernel assigns the actual ID via the transport - // This placeholder is only used for logging - id: 'sv0' as `sv${number}`, - buildRootObject, - vatPowers: {}, - parameters: undefined, - syscallHandlerHolder, - logger: logger.subLogger({ tags: ['supervisor'] }), - }); - - // Start the supervisor (dispatches startVat) - throws on failure - await supervisor.start(); - - // Signal that the supervisor is ready - this unblocks any pending deliveries - supervisorReady.resolve(supervisor); - }, - - getKernelFacet: () => { - if (!capturedKernelFacet) { - throw new Error( - 'Kernel facet not available. Ensure start() was called and kernel has bootstrapped.', - ); - } - return capturedKernelFacet; - }, + connect, + kernelFacetPromise: kernelFacetKit.promise, }); } harden(makeHostSubcluster); diff --git a/packages/nodejs/test/e2e/system-subcluster.test.ts b/packages/nodejs/test/e2e/system-subcluster.test.ts index d7a52dffb..ba5d5b913 100644 --- a/packages/nodejs/test/e2e/system-subcluster.test.ts +++ b/packages/nodejs/test/e2e/system-subcluster.test.ts @@ -1,8 +1,9 @@ import { E } from '@endo/eventual-send'; import { makeSQLKernelDatabase } from '@metamask/kernel-store/sqlite/nodejs'; import { Logger } from '@metamask/logger'; -import type { Kernel, ClusterConfig, KernelFacet } from '@metamask/ocap-kernel'; -import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import type { ClusterConfig, KernelFacet } from '@metamask/ocap-kernel'; +import { Kernel } from '@metamask/ocap-kernel'; +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { makeHostSubcluster } from '../../src/host-subcluster/index.ts'; import { NodejsPlatformServices } from '../../src/kernel/PlatformServices.ts'; @@ -28,14 +29,13 @@ type PromiseVat = { describe('system subcluster e2e tests', { timeout: 30_000 }, () => { let kernel: Kernel; - let kernelFacet: KernelFacet; - let hostSubcluster: ReturnType; + let kernelFacet: KernelFacet | Promise; beforeEach(async () => { const logger = new Logger('test'); // Create host subcluster first - hostSubcluster = makeHostSubcluster({ logger }); + const hostSubcluster = makeHostSubcluster({ logger }); // Create kernel with system subcluster config const platformServices = new NodejsPlatformServices({ @@ -44,23 +44,17 @@ describe('system subcluster e2e tests', { timeout: 30_000 }, () => { const kernelDatabase = await makeSQLKernelDatabase({}); // Import Kernel dynamically to avoid circular deps - const { Kernel: KernelClass } = await import('@metamask/ocap-kernel'); - kernel = await KernelClass.make(platformServices, kernelDatabase, { + kernel = await Kernel.make(platformServices, kernelDatabase, { resetStorage: true, logger: logger.subLogger({ tags: ['kernel'] }), systemSubclusters: { subclusters: [hostSubcluster.config] }, }); - // Start host subcluster supervisor after kernel is created - await hostSubcluster.start(); + // Supervisor-side initiates connection AFTER kernel exists + hostSubcluster.connect(); - // Wait for the bootstrap message to be delivered and kernel facet to be available - await vi.waitFor( - () => { - kernelFacet = hostSubcluster.getKernelFacet(); - }, - { timeout: 5000, interval: 50 }, - ); + // Wait for kernel facet - resolves after bootstrap message is delivered + kernelFacet = await hostSubcluster.kernelFacetPromise; }); afterEach(async () => { @@ -92,7 +86,7 @@ describe('system subcluster e2e tests', { timeout: 30_000 }, () => { expect(result.rootKref).toBeDefined(); // The root should be E()-callable - const bob = result.root as Bob; + const bob = result.root as unknown as Bob; const greeter = await E(bob).makeGreeter('Hello'); expect(greeter).toBeDefined(); }); @@ -138,7 +132,7 @@ describe('system subcluster e2e tests', { timeout: 30_000 }, () => { }; const launchResult = await E(kernelFacet).launchSubcluster(config); - const bob = launchResult.root as Bob; + const bob = launchResult.root as unknown as Bob; // Get Carol's root object const subcluster = await E(kernelFacet).getSubcluster( @@ -170,7 +164,7 @@ describe('system subcluster e2e tests', { timeout: 30_000 }, () => { }, }; const bobResult = await E(kernelFacet).launchSubcluster(bobConfig); - const bob = bobResult.root as Bob; + const bob = bobResult.root as unknown as Bob; // Launch Carol in another subcluster const carolConfig: ClusterConfig = { @@ -182,7 +176,7 @@ describe('system subcluster e2e tests', { timeout: 30_000 }, () => { }, }; const carolResult = await E(kernelFacet).launchSubcluster(carolConfig); - const carol = carolResult.root as Carol; + const carol = carolResult.root as unknown as Carol; // Host orchestrates cross-subcluster handoff const greeter = await E(bob).makeGreeter('Cross-cluster'); @@ -204,7 +198,7 @@ describe('system subcluster e2e tests', { timeout: 30_000 }, () => { }; const result = await E(kernelFacet).launchSubcluster(config); - const promiseVat = result.root as PromiseVat; + const promiseVat = result.root as unknown as PromiseVat; // Get a promise for an exo (without awaiting) const exoPromise = E(promiseVat).makeGreeter('Hi'); @@ -228,7 +222,7 @@ describe('system subcluster e2e tests', { timeout: 30_000 }, () => { }; const result = await E(kernelFacet).launchSubcluster(config); - const promiseVat = result.root as PromiseVat; + const promiseVat = result.root as unknown as PromiseVat; // Get a deferred promise (unresolved) const deferredPromise = E(promiseVat).makeDeferredPromise(); @@ -252,7 +246,7 @@ describe('system subcluster e2e tests', { timeout: 30_000 }, () => { }; const result = await E(kernelFacet).launchSubcluster(config); - const promiseVat = result.root as PromiseVat; + const promiseVat = result.root as unknown as PromiseVat; // Get a deferred promise (unresolved) const deferredPromise = E(promiseVat).makeDeferredPromise(); diff --git a/packages/ocap-kernel/src/Kernel.ts b/packages/ocap-kernel/src/Kernel.ts index 956bbbe93..a54b33109 100644 --- a/packages/ocap-kernel/src/Kernel.ts +++ b/packages/ocap-kernel/src/Kernel.ts @@ -255,13 +255,13 @@ export class Kernel { // This ensures that any messages in the queue have their target vats ready await this.#vatManager.initializeAllVats(); - // Connect system subclusters if configured + // Prepare system subclusters if configured (kernel side setup only). + // The kernel sets up to receive connections - it does NOT reach out. + // Actual connection happens when supervisor-side calls connect(). if (this.#systemSubclustersConfig) { const { subclusters } = this.#systemSubclustersConfig; for (const subclusterConfig of subclusters) { - await this.#systemSubclusterManager.connectSystemSubcluster( - subclusterConfig, - ); + this.#systemSubclusterManager.prepareSystemSubcluster(subclusterConfig); } } diff --git a/packages/ocap-kernel/src/types.ts b/packages/ocap-kernel/src/types.ts index bdfbe6224..c0167f657 100644 --- a/packages/ocap-kernel/src/types.ts +++ b/packages/ocap-kernel/src/types.ts @@ -505,12 +505,25 @@ export type SystemVatDeliverFn = ( * runtime's process). This allows: * - Node.js: direct function calls (same process) * - Extension: MessagePort IPC (cross-process) + * + * The kernel is passive - it sets up to receive connections via the transport. + * The supervisor side initiates the connection by resolving `awaitConnection()`. + * This push-based model allows: + * - Same-process: supervisor calls `connect()` which resolves the promise + * - Cross-process: supervisor sends "connect" message over IPC */ export type SystemVatTransport = { /** Send deliveries from kernel to system vat. */ deliver: SystemVatDeliverFn; /** Register syscall handler (kernel calls this to wire up). */ setSyscallHandler: (handler: SystemVatSyscallHandler) => void; + /** + * Returns a promise that resolves when the supervisor-side initiates + * connection. The kernel waits for this before sending bootstrap messages. + * For same-process transports, this resolves when `connect()` is called. + * For cross-process transports, this resolves when "connect" IPC message arrives. + */ + awaitConnection: () => Promise; }; /** diff --git a/packages/ocap-kernel/src/vats/SystemSubclusterManager.test.ts b/packages/ocap-kernel/src/vats/SystemSubclusterManager.test.ts index 2f3114d38..8a5a94cf6 100644 --- a/packages/ocap-kernel/src/vats/SystemSubclusterManager.test.ts +++ b/packages/ocap-kernel/src/vats/SystemSubclusterManager.test.ts @@ -21,6 +21,7 @@ describe('SystemSubclusterManager', () => { /** * Creates a mock transport for testing. + * The transport's awaitConnection() resolves immediately by default. * * @returns A mock transport with vi.fn() implementations. */ @@ -28,6 +29,7 @@ describe('SystemSubclusterManager', () => { return { deliver: vi.fn().mockResolvedValue(null), setSyscallHandler: vi.fn(), + awaitConnection: vi.fn().mockResolvedValue(undefined), }; } @@ -91,30 +93,30 @@ describe('SystemSubclusterManager', () => { }); }); - describe('connectSystemSubcluster', () => { - it('throws if bootstrap vat is not in vatTransports', async () => { + describe('prepareSystemSubcluster', () => { + it('throws if bootstrap vat is not in vatTransports', () => { const config: KernelSystemSubclusterConfig = { bootstrap: 'missing', vatTransports: [{ name: 'testVat', transport: makeMockTransport() }], }; - await expect(manager.connectSystemSubcluster(config)).rejects.toThrow( + expect(() => manager.prepareSystemSubcluster(config)).toThrow( 'invalid bootstrap vat name missing', ); }); - it('allocates system vat IDs starting from sv0', async () => { + it('allocates system vat IDs starting from sv0', () => { const config: KernelSystemSubclusterConfig = { bootstrap: 'testVat', vatTransports: [{ name: 'testVat', transport: makeMockTransport() }], }; - const result = await manager.connectSystemSubcluster(config); + const result = manager.prepareSystemSubcluster(config); expect(result.vatIds.testVat).toBe('sv0'); }); - it('allocates incrementing system vat IDs', async () => { + it('allocates incrementing system vat IDs', () => { const config1: KernelSystemSubclusterConfig = { bootstrap: 'testVat', vatTransports: [{ name: 'testVat', transport: makeMockTransport() }], @@ -124,53 +126,53 @@ describe('SystemSubclusterManager', () => { vatTransports: [{ name: 'testVat', transport: makeMockTransport() }], }; - const result1 = await manager.connectSystemSubcluster(config1); - const result2 = await manager.connectSystemSubcluster(config2); + const result1 = manager.prepareSystemSubcluster(config1); + const result2 = manager.prepareSystemSubcluster(config2); expect(result1.vatIds.testVat).toBe('sv0'); expect(result2.vatIds.testVat).toBe('sv1'); }); - it('allocates system subcluster IDs starting from ss0', async () => { + it('allocates system subcluster IDs starting from ss0', () => { const config: KernelSystemSubclusterConfig = { bootstrap: 'testVat', vatTransports: [{ name: 'testVat', transport: makeMockTransport() }], }; - const result = await manager.connectSystemSubcluster(config); + const result = manager.prepareSystemSubcluster(config); expect(result.systemSubclusterId).toBe('ss0'); }); - it('initializes endpoints for each vat', async () => { + it('initializes endpoints for each vat', () => { const config: KernelSystemSubclusterConfig = { bootstrap: 'testVat', vatTransports: [{ name: 'testVat', transport: makeMockTransport() }], }; - await manager.connectSystemSubcluster(config); + manager.prepareSystemSubcluster(config); expect(kernelStore.initEndpoint).toHaveBeenCalledWith('sv0'); }); - it('initializes kernel objects for vat roots', async () => { + it('initializes kernel objects for vat roots', () => { const config: KernelSystemSubclusterConfig = { bootstrap: 'testVat', vatTransports: [{ name: 'testVat', transport: makeMockTransport() }], }; - await manager.connectSystemSubcluster(config); + manager.prepareSystemSubcluster(config); expect(kernelStore.initKernelObject).toHaveBeenCalledWith('sv0'); }); - it('adds clist entries for root objects', async () => { + it('adds clist entries for root objects', () => { const config: KernelSystemSubclusterConfig = { bootstrap: 'testVat', vatTransports: [{ name: 'testVat', transport: makeMockTransport() }], }; - await manager.connectSystemSubcluster(config); + manager.prepareSystemSubcluster(config); expect(kernelStore.addCListEntry).toHaveBeenCalledWith( 'sv0', @@ -179,37 +181,40 @@ describe('SystemSubclusterManager', () => { ); }); - it('enqueues bootstrap message to root object', async () => { + it('enqueues bootstrap message after connection', async () => { const config: KernelSystemSubclusterConfig = { bootstrap: 'testVat', vatTransports: [{ name: 'testVat', transport: makeMockTransport() }], }; - await manager.connectSystemSubcluster(config); + manager.prepareSystemSubcluster(config); - expect(kernelQueue.enqueueSend).toHaveBeenCalledWith( - 'ko1', - expect.objectContaining({ - methargs: expect.any(Object), - }), - ); + // Wait for the async connection callback to fire + await vi.waitFor(() => { + expect(kernelQueue.enqueueSend).toHaveBeenCalledWith( + 'ko1', + expect.objectContaining({ + methargs: expect.any(Object), + }), + ); + }); }); - it('wires syscall handler to transport', async () => { + it('wires syscall handler to transport', () => { const transport = makeMockTransport(); const config: KernelSystemSubclusterConfig = { bootstrap: 'testVat', vatTransports: [{ name: 'testVat', transport }], }; - await manager.connectSystemSubcluster(config); + manager.prepareSystemSubcluster(config); expect(transport.setSyscallHandler).toHaveBeenCalledWith( expect.any(Function), ); }); - it('connects multiple vats in a subcluster', async () => { + it('prepares multiple vats in a subcluster', () => { const config: KernelSystemSubclusterConfig = { bootstrap: 'bootstrap', vatTransports: [ @@ -218,7 +223,7 @@ describe('SystemSubclusterManager', () => { ], }; - const result = await manager.connectSystemSubcluster(config); + const result = manager.prepareSystemSubcluster(config); expect(result.vatIds.bootstrap).toBe('sv0'); expect(result.vatIds.worker).toBe('sv1'); @@ -235,27 +240,31 @@ describe('SystemSubclusterManager', () => { vatTransports: [{ name: 'testVat', transport: makeMockTransport() }], }; - await manager.connectSystemSubcluster(config); + manager.prepareSystemSubcluster(config); // Should not create new kernel object for root // (only kernel facet is created via registerKernelService which is mocked) expect(kernelStore.initKernelObject).not.toHaveBeenCalled(); - expect(kernelQueue.enqueueSend).toHaveBeenCalledWith( - 'ko-existing', - expect.objectContaining({ - methargs: expect.any(Object), - }), - ); + + // Wait for the async connection callback to fire + await vi.waitFor(() => { + expect(kernelQueue.enqueueSend).toHaveBeenCalledWith( + 'ko-existing', + expect.objectContaining({ + methargs: expect.any(Object), + }), + ); + }); }); - it('warns if requested service is not found', async () => { + it('warns if requested service is not found', () => { const config: KernelSystemSubclusterConfig = { bootstrap: 'testVat', vatTransports: [{ name: 'testVat', transport: makeMockTransport() }], services: ['unknownService'], }; - await manager.connectSystemSubcluster(config); + manager.prepareSystemSubcluster(config); expect(logger.warn).toHaveBeenCalledWith( "Kernel service 'unknownService' not found", @@ -273,18 +282,20 @@ describe('SystemSubclusterManager', () => { services: ['myService'], }; - await manager.connectSystemSubcluster(config); + manager.prepareSystemSubcluster(config); - // Check that enqueueSend was called (service is embedded in the serialized methargs) - expect(kernelQueue.enqueueSend).toHaveBeenCalledWith( - 'ko1', - expect.objectContaining({ - methargs: expect.any(Object), - }), - ); + // Wait for the async connection callback to fire + await vi.waitFor(() => { + expect(kernelQueue.enqueueSend).toHaveBeenCalledWith( + 'ko1', + expect.objectContaining({ + methargs: expect.any(Object), + }), + ); + }); }); - it('creates singleton kernel facet across multiple subclusters', async () => { + it('creates singleton kernel facet across multiple subclusters', () => { // Create a manager with a register function that tracks calls const registerCalls: string[] = []; const managerWithTracking = new SystemSubclusterManager({ @@ -307,8 +318,8 @@ describe('SystemSubclusterManager', () => { vatTransports: [{ name: 'vat2', transport: makeMockTransport() }], }; - await managerWithTracking.connectSystemSubcluster(config1); - await managerWithTracking.connectSystemSubcluster(config2); + managerWithTracking.prepareSystemSubcluster(config1); + managerWithTracking.prepareSystemSubcluster(config2); // registerKernelService should only be called once (singleton) expect( @@ -322,28 +333,30 @@ describe('SystemSubclusterManager', () => { vatTransports: [{ name: 'testVat', transport: makeMockTransport() }], }; - await manager.connectSystemSubcluster(config); - - // Verify enqueueSend was called with bootstrap message - expect(kernelQueue.enqueueSend).toHaveBeenCalledWith( - 'ko1', - expect.objectContaining({ - methargs: expect.objectContaining({ - // The methargs should contain the kernelFacet kref in slots - slots: expect.arrayContaining(['ko-kernelFacet']), + manager.prepareSystemSubcluster(config); + + // Wait for the async connection callback to fire + await vi.waitFor(() => { + expect(kernelQueue.enqueueSend).toHaveBeenCalledWith( + 'ko1', + expect.objectContaining({ + methargs: expect.objectContaining({ + // The methargs should contain the kernelFacet kref in slots + slots: expect.arrayContaining(['ko-kernelFacet']), + }), }), - }), - ); + ); + }); }); }); describe('getSystemVatHandle', () => { - it('returns handle for connected system vat', async () => { + it('returns handle for prepared system vat', () => { const config: KernelSystemSubclusterConfig = { bootstrap: 'testVat', vatTransports: [{ name: 'testVat', transport: makeMockTransport() }], }; - await manager.connectSystemSubcluster(config); + manager.prepareSystemSubcluster(config); const handle = manager.getSystemVatHandle('sv0' as SystemVatId); @@ -357,7 +370,7 @@ describe('SystemSubclusterManager', () => { expect(handle).toBeUndefined(); }); - it('finds handle across multiple subclusters', async () => { + it('finds handle across multiple subclusters', () => { const config1: KernelSystemSubclusterConfig = { bootstrap: 'vat1', vatTransports: [{ name: 'vat1', transport: makeMockTransport() }], @@ -367,8 +380,8 @@ describe('SystemSubclusterManager', () => { vatTransports: [{ name: 'vat2', transport: makeMockTransport() }], }; - await manager.connectSystemSubcluster(config1); - await manager.connectSystemSubcluster(config2); + manager.prepareSystemSubcluster(config1); + manager.prepareSystemSubcluster(config2); const handle1 = manager.getSystemVatHandle('sv0' as SystemVatId); const handle2 = manager.getSystemVatHandle('sv1' as SystemVatId); diff --git a/packages/ocap-kernel/src/vats/SystemSubclusterManager.ts b/packages/ocap-kernel/src/vats/SystemSubclusterManager.ts index 79eb9dbb7..ed52b65f5 100644 --- a/packages/ocap-kernel/src/vats/SystemSubclusterManager.ts +++ b/packages/ocap-kernel/src/vats/SystemSubclusterManager.ts @@ -16,10 +16,10 @@ import type { import { ROOT_OBJECT_VREF } from '../types.ts'; /** - * Result of connecting a system subcluster. + * Result of preparing a system subcluster. */ -export type SystemSubclusterConnectResult = { - /** The ID of the connected system subcluster. */ +export type SystemSubclusterPrepareResult = { + /** The ID of the prepared system subcluster. */ systemSubclusterId: SystemSubclusterId; /** Map of vat names to their system vat IDs. */ vatIds: Record; @@ -155,18 +155,23 @@ export class SystemSubclusterManager { } /** - * Connect to a system subcluster using provided transports. + * Prepare a system subcluster using provided transports. * * The runtime creates supervisors externally and provides transports for - * communication. The kernel creates a kernel facet and delivers it in the - * bootstrap message as a presence. + * communication. This method sets up the kernel side and returns immediately. + * The actual connection and bootstrap happen asynchronously when the + * supervisor-side initiates connection via the transport's `awaitConnection()`. + * + * The kernel is passive - it sets up to receive connections and waits for + * the supervisor to push the connection. This push-based model supports + * both same-process and cross-process transports. * * @param config - Configuration for the system subcluster with transports. - * @returns A promise for the connect result. + * @returns The prepare result with IDs allocated for the subcluster. */ - async connectSystemSubcluster( + prepareSystemSubcluster( config: KernelSystemSubclusterConfig, - ): Promise { + ): SystemSubclusterPrepareResult { const bootstrapTransport = config.vatTransports.find( (vt) => vt.name === config.bootstrap, ); @@ -179,7 +184,7 @@ export class SystemSubclusterManager { const handles = new Map(); const rootKrefs: Record = {}; - // Connect all system vats via their transports + // Set up all system vats via their transports (kernel side only) for (const vatTransport of config.vatTransports) { const { name: vatName, transport } = vatTransport; const systemVatId = this.#allocateSystemVatId(); @@ -255,22 +260,31 @@ export class SystemSubclusterManager { } } - // Call bootstrap on the bootstrap vat's root object - const bootstrapVatId = vatIds[config.bootstrap]; - if (!bootstrapVatId) { - throw new Error(`Bootstrap vat ID not found for ${config.bootstrap}`); - } - - // Enqueue the bootstrap message without waiting for its result. - // We use enqueueSend (fire-and-forget) instead of enqueueMessage (which awaits result) - // because this is called during Kernel.make() before the run loop starts. - // The system vat will receive the bootstrap message once the run loop begins. + // Get bootstrap target for the bootstrap message const bootstrapTarget = rootKrefs[config.bootstrap] as KRef; const bootstrapArgs = [roots, services]; - this.#kernelQueue.enqueueSend(bootstrapTarget, { - methargs: kser(['bootstrap', bootstrapArgs]), - }); + // Set up to send bootstrap after ALL vats in the subcluster are connected. + // We wait for all transports' awaitConnection() to resolve before sending + // the bootstrap message to ensure all vats are ready. + const connectionPromises = config.vatTransports.map(async (vt) => + vt.transport.awaitConnection(), + ); + Promise.all(connectionPromises) + .then(() => { + // All supervisors have connected. Now send the bootstrap message. + // We use enqueueSend (fire-and-forget) because this runs asynchronously + // after the kernel queue has started. + this.#kernelQueue.enqueueSend(bootstrapTarget, { + methargs: kser(['bootstrap', bootstrapArgs]), + }); + return undefined; + }) + .catch((error) => { + this.#logger.error(`Failed to connect system subcluster:`, error); + }); + + // Return immediately - connection happens later when supervisor calls connect() return { systemSubclusterId, vatIds, diff --git a/packages/ocap-kernel/src/vats/SystemVatSupervisor.ts b/packages/ocap-kernel/src/vats/SystemVatSupervisor.ts index 1a694f925..2bc9ec68a 100644 --- a/packages/ocap-kernel/src/vats/SystemVatSupervisor.ts +++ b/packages/ocap-kernel/src/vats/SystemVatSupervisor.ts @@ -8,7 +8,7 @@ import { makeMarshal } from '@endo/marshal'; import type { CapData } from '@endo/marshal'; import type { KVStore } from '@metamask/kernel-store'; import { waitUntilQuiescent } from '@metamask/kernel-utils'; -import type { Logger } from '@metamask/logger'; +import { Logger } from '@metamask/logger'; import type { Json } from '@metamask/utils'; import { makeGCAndFinalize } from '../garbage-collection/gc-finalize.ts'; @@ -137,6 +137,17 @@ function makeEphemeralVatKVStore(): KVStore { }); } +/** + * Options for creating a system vat supervisor via the static factory method. + */ +export type SystemVatSupervisorMakeOptions = { + buildRootObject: SystemVatBuildRootObject; + syscallHandlerHolder: SyscallHandlerHolder; + vatPowers?: Record; + parameters?: Record; + logger?: Logger; +}; + /** * Supervises a system vat's execution. * @@ -148,6 +159,9 @@ function makeEphemeralVatKVStore(): KVStore { * The supervisor can be wired to the kernel in two ways: * 1. Direct: Pass `executeSyscall` in constructor (same-process) * 2. Deferred: Pass `syscallHandlerHolder` and set handler later (transport-based) + * + * For simplified usage, use the static `make()` factory method which handles + * waiting for the kernel to signal readiness before starting. */ export class SystemVatSupervisor { /** The ID of the system vat being supervised */ @@ -162,6 +176,45 @@ export class SystemVatSupervisor { /** Flag indicating if the system vat has been initialized */ #initialized: boolean = false; + /** + * Create a system vat supervisor that waits for kernel readiness before starting. + * + * This is the recommended way to create a supervisor when using transports. + * It handles all the timing coordination automatically: + * 1. Creates the supervisor (but doesn't start yet - syscalls not wired) + * 2. Waits for the kernel to signal ready (syscalls wired via onReady callback) + * 3. Starts the supervisor (dispatches startVat) + * + * @param options - Options for creating the supervisor. + * @returns A promise that resolves to the created supervisor. + */ + static async make( + options: SystemVatSupervisorMakeOptions, + ): Promise { + const { + buildRootObject, + syscallHandlerHolder, + vatPowers, + parameters, + logger, + } = options; + + // Create supervisor (but don't start yet - syscalls not wired) + const supervisor = new SystemVatSupervisor({ + id: 'sv-pending' as SystemVatId, + buildRootObject, + vatPowers: vatPowers ?? {}, + parameters, + syscallHandlerHolder, + logger: logger ?? new Logger('system-vat'), + }); + + // Now safe to start (dispatches startVat) + await supervisor.start(); + + return supervisor; + } + /** * Construct a new SystemVatSupervisor instance. * diff --git a/packages/ocap-kernel/src/vats/index.ts b/packages/ocap-kernel/src/vats/index.ts index c20fe5022..54f04acd9 100644 --- a/packages/ocap-kernel/src/vats/index.ts +++ b/packages/ocap-kernel/src/vats/index.ts @@ -3,6 +3,7 @@ export { makeSyscallHandlerHolder, type SyscallHandlerHolder, type SystemVatExecuteSyscall, + type SystemVatSupervisorMakeOptions, } from './SystemVatSupervisor.ts'; export { SystemVatHandle } from './SystemVatHandle.ts'; export type { diff --git a/packages/ocap-kernel/test/integration/system-vat.test.ts b/packages/ocap-kernel/test/integration/system-vat.test.ts index dc28f0ccb..c47b2c626 100644 --- a/packages/ocap-kernel/test/integration/system-vat.test.ts +++ b/packages/ocap-kernel/test/integration/system-vat.test.ts @@ -21,31 +21,54 @@ import { } from '../../src/vats/SystemVatSupervisor.ts'; import { makeMapKernelDatabase } from '../storage.ts'; +/** + * Result of creating a test system vat. + */ +type TestSystemVatResult = { + /** Transport config for kernel. */ + transport: SystemVatTransport; + /** Call after Kernel.make() to initiate connection from supervisor side. */ + connect: () => void; + /** Promise that resolves to kernelFacet when bootstrap completes. */ + kernelFacetPromise: Promise; +}; + /** * Create a system vat transport and supervisor pair for testing. - * Uses a deferred pattern to handle the timing between kernel creation - * and supervisor startup. + * Uses the push-based connection pattern where the supervisor initiates + * connection after the kernel is created. * * @param options - Options for creating the transport. - * @param options.buildRootObject - Function to build the root object. * @param options.logger - Logger instance. - * @returns The transport config and start function. + * @returns The transport config, connect function, and kernelFacetPromise. */ -function makeTestSystemVat(options: { - buildRootObject: SystemVatBuildRootObject; - logger: Logger; -}): { - transport: SystemVatTransport; - start: () => Promise; -} { - const { buildRootObject, logger } = options; +function makeTestSystemVat(options: { logger: Logger }): TestSystemVatResult { + const { logger } = options; + + // Promise kit for kernel facet - resolves when bootstrap is called + const kernelFacetKit = makePromiseKit(); // Create syscall handler holder for deferred wiring const syscallHandlerHolder = makeSyscallHandlerHolder(); + // Build root object that captures kernelFacet from bootstrap + const buildRootObject: SystemVatBuildRootObject = () => { + return makeDefaultExo('TestRoot', { + bootstrap: ( + _roots: Record, + services: { kernelFacet: KernelFacet }, + ) => { + kernelFacetKit.resolve(services.kernelFacet); + }, + }); + }; + // Promise kit to signal when supervisor is ready const supervisorReady = makePromiseKit(); + // Promise kit for connection - resolved when connect() is called + const connectionKit = makePromiseKit(); + // Create the transport with a deliver function that waits for the supervisor const deliver: SystemVatDeliverFn = async (delivery) => { const supervisor = await supervisorReady.promise; @@ -57,52 +80,39 @@ function makeTestSystemVat(options: { setSyscallHandler: (handler: SystemVatSyscallHandler) => { syscallHandlerHolder.handler = handler; }, + awaitConnection: async () => connectionKit.promise, }; - const start = async () => { - const supervisor = new SystemVatSupervisor({ - id: 'sv-test' as `sv${number}`, + // Called after Kernel.make() to initiate connection from supervisor side + const connect = (): void => { + SystemVatSupervisor.make({ buildRootObject, - vatPowers: {}, - parameters: undefined, syscallHandlerHolder, logger: logger.subLogger({ tags: ['supervisor'] }), - }); - - await supervisor.start(); - - // Signal that the supervisor is ready - supervisorReady.resolve(supervisor); + }) + .then((supervisor) => { + supervisorReady.resolve(supervisor); + connectionKit.resolve(); + return undefined; + }) + .catch((error) => { + connectionKit.reject(error as Error); + kernelFacetKit.reject(error as Error); + }); }; - return { transport, start }; + return { transport, connect, kernelFacetPromise: kernelFacetKit.promise }; } describe('system vat integration', { timeout: 30_000 }, () => { let kernel: Kernel; - let kernelFacet: KernelFacet; + let kernelFacet: KernelFacet | Promise; beforeEach(async () => { const logger = new Logger('test'); - // Captured kernel facet from bootstrap - let capturedKernelFacet: KernelFacet | null = null; - - // Build root object that captures the kernel facet from services - const buildRootObject: SystemVatBuildRootObject = () => { - return makeDefaultExo('TestRoot', { - bootstrap: ( - _roots: Record, - services: { kernelFacet: KernelFacet }, - ) => { - capturedKernelFacet = services.kernelFacet; - }, - }); - }; - - // Create the system vat transport and supervisor + // Create the system vat transport const systemVat = makeTestSystemVat({ - buildRootObject, logger: logger.subLogger({ tags: ['system-vat'] }), }); @@ -136,20 +146,11 @@ describe('system vat integration', { timeout: 30_000 }, () => { }, }); - // Start the supervisor - this unblocks the deliver function - await systemVat.start(); - - // Wait for the bootstrap message to be delivered and processed - await vi.waitFor( - () => { - if (!capturedKernelFacet) { - throw new Error('Waiting for kernel facet...'); - } - }, - { timeout: 5000, interval: 50 }, - ); + // Supervisor-side initiates connection AFTER kernel exists + systemVat.connect(); - kernelFacet = capturedKernelFacet!; + // Wait for kernel facet + kernelFacet = await systemVat.kernelFacetPromise; }); afterEach(async () => { From 1250ddb77fd22bba5414e5f5340d1d4d4ac05418 Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Thu, 29 Jan 2026 13:57:18 -0800 Subject: [PATCH 23/41] refactor(ocap-kernel): Remove syscallHandlerHolder abstraction The push-based connection model makes the holder unnecessary. Since setSyscallHandler() is called before connect(), we can store the handler directly and pass it to SystemVatSupervisor.make(). Changes: - Remove SyscallHandlerHolder type and makeSyscallHandlerHolder() - SystemVatSupervisor now takes executeSyscall directly - Transport stores syscall handler and passes it on connect() Co-Authored-By: Claude --- packages/nodejs/src/host-subcluster/index.ts | 18 +-- .../src/vats/SystemVatSupervisor.ts | 110 ++++-------------- packages/ocap-kernel/src/vats/index.ts | 8 +- .../test/integration/system-vat.test.ts | 16 +-- 4 files changed, 42 insertions(+), 110 deletions(-) diff --git a/packages/nodejs/src/host-subcluster/index.ts b/packages/nodejs/src/host-subcluster/index.ts index 841c79eda..5a7434125 100644 --- a/packages/nodejs/src/host-subcluster/index.ts +++ b/packages/nodejs/src/host-subcluster/index.ts @@ -9,10 +9,7 @@ import type { SystemVatSyscallHandler, SystemVatDeliverFn, } from '@metamask/ocap-kernel'; -import { - SystemVatSupervisor, - makeSyscallHandlerHolder, -} from '@metamask/ocap-kernel/vats'; +import { SystemVatSupervisor } from '@metamask/ocap-kernel/vats'; /** * Result of creating a host subcluster. @@ -70,8 +67,8 @@ export function makeHostSubcluster( // Promise kit for kernel facet - resolves when bootstrap is called const kernelFacetKit = makePromiseKit(); - // Create syscall handler holder for deferred wiring - const syscallHandlerHolder = makeSyscallHandlerHolder(); + // Syscall handler - set by kernel during prepareSystemSubcluster() + let syscallHandler: SystemVatSyscallHandler | null = null; // Build root object that captures kernelFacet from bootstrap const buildRootObject: SystemVatBuildRootObject = () => { @@ -102,7 +99,7 @@ export function makeHostSubcluster( const transport: SystemVatTransport = { deliver, setSyscallHandler: (handler: SystemVatSyscallHandler) => { - syscallHandlerHolder.handler = handler; + syscallHandler = handler; }, // Kernel calls this to wait for connection from supervisor side awaitConnection: async () => connectionKit.promise, @@ -113,10 +110,15 @@ export function makeHostSubcluster( * Creates and starts the supervisor, then resolves the connection promise. */ const connect = (): void => { + if (!syscallHandler) { + throw new Error( + 'Cannot connect: syscall handler not set. Was Kernel.make() called with this config?', + ); + } // Create and start the supervisor SystemVatSupervisor.make({ buildRootObject, - syscallHandlerHolder, + executeSyscall: syscallHandler, logger: logger.subLogger({ tags: ['supervisor'] }), }) .then((supervisor) => { diff --git a/packages/ocap-kernel/src/vats/SystemVatSupervisor.ts b/packages/ocap-kernel/src/vats/SystemVatSupervisor.ts index 2bc9ec68a..427cef0a7 100644 --- a/packages/ocap-kernel/src/vats/SystemVatSupervisor.ts +++ b/packages/ocap-kernel/src/vats/SystemVatSupervisor.ts @@ -35,41 +35,15 @@ export type SystemVatExecuteSyscall = ( vso: VatSyscallObject, ) => VatSyscallResult; -/** - * A holder for a syscall handler that can be set after construction. - * This allows the supervisor to be created before the kernel wires up - * the transport. - */ -export type SyscallHandlerHolder = { - /** The syscall handler, set by the kernel when wiring up the transport. */ - handler: SystemVatExecuteSyscall | null; -}; - -/** - * Create a syscall handler holder for deferred wiring. - * - * @returns A syscall handler holder. - */ -export function makeSyscallHandlerHolder(): SyscallHandlerHolder { - return { handler: null }; -} - type SystemVatSupervisorProps = { - id: SystemVatId; buildRootObject: SystemVatBuildRootObject; - vatPowers: Record; - parameters: Record | undefined; - logger: Logger; -} & ( - | { - /** Direct syscall executor (legacy - for same-process use). */ - executeSyscall: SystemVatExecuteSyscall; - } - | { - /** Syscall handler holder for deferred wiring (transport-based). */ - syscallHandlerHolder: SyscallHandlerHolder; - } -); + executeSyscall: SystemVatExecuteSyscall; + /** ID for the system vat. Defaults to 'sv-pending'. */ + id?: SystemVatId; + vatPowers?: Record; + parameters?: Record; + logger?: Logger; +}; /** * A non-persistent KV store for system vats. @@ -142,7 +116,7 @@ function makeEphemeralVatKVStore(): KVStore { */ export type SystemVatSupervisorMakeOptions = { buildRootObject: SystemVatBuildRootObject; - syscallHandlerHolder: SyscallHandlerHolder; + executeSyscall: SystemVatExecuteSyscall; vatPowers?: Record; parameters?: Record; logger?: Logger; @@ -155,13 +129,6 @@ export type SystemVatSupervisorMakeOptions = { * They don't load bundles via importBundle; instead, they receive a * buildRootObject function directly. They use an ephemeral vatstore since * they don't participate in kernel persistence machinery. - * - * The supervisor can be wired to the kernel in two ways: - * 1. Direct: Pass `executeSyscall` in constructor (same-process) - * 2. Deferred: Pass `syscallHandlerHolder` and set handler later (transport-based) - * - * For simplified usage, use the static `make()` factory method which handles - * waiting for the kernel to signal readiness before starting. */ export class SystemVatSupervisor { /** The ID of the system vat being supervised */ @@ -177,41 +144,16 @@ export class SystemVatSupervisor { #initialized: boolean = false; /** - * Create a system vat supervisor that waits for kernel readiness before starting. - * - * This is the recommended way to create a supervisor when using transports. - * It handles all the timing coordination automatically: - * 1. Creates the supervisor (but doesn't start yet - syscalls not wired) - * 2. Waits for the kernel to signal ready (syscalls wired via onReady callback) - * 3. Starts the supervisor (dispatches startVat) + * Create and start a system vat supervisor. * * @param options - Options for creating the supervisor. - * @returns A promise that resolves to the created supervisor. + * @returns A promise that resolves to the started supervisor. */ static async make( options: SystemVatSupervisorMakeOptions, ): Promise { - const { - buildRootObject, - syscallHandlerHolder, - vatPowers, - parameters, - logger, - } = options; - - // Create supervisor (but don't start yet - syscalls not wired) - const supervisor = new SystemVatSupervisor({ - id: 'sv-pending' as SystemVatId, - buildRootObject, - vatPowers: vatPowers ?? {}, - parameters, - syscallHandlerHolder, - logger: logger ?? new Logger('system-vat'), - }); - - // Now safe to start (dispatches startVat) + const supervisor = new SystemVatSupervisor(options); await supervisor.start(); - return supervisor; } @@ -219,35 +161,25 @@ export class SystemVatSupervisor { * Construct a new SystemVatSupervisor instance. * * @param props - Named constructor parameters. - * @param props.id - The ID of the system vat being supervised. * @param props.buildRootObject - Function to build the vat's root object. + * @param props.executeSyscall - Function to execute syscalls. + * @param props.id - ID for the system vat. Defaults to 'sv-pending'. * @param props.vatPowers - External capabilities for this system vat. * @param props.parameters - Parameters to pass to buildRootObject. - * @param props.executeSyscall - Function to execute syscalls (direct wiring). - * @param props.syscallHandlerHolder - Holder for deferred syscall handler wiring. * @param props.logger - The logger for this system vat. */ constructor(props: SystemVatSupervisorProps) { - const { id, buildRootObject, vatPowers, parameters, logger } = props; + const { + buildRootObject, + executeSyscall, + id = 'sv-pending' as SystemVatId, + vatPowers = {}, + parameters, + logger = new Logger('system-vat'), + } = props; this.id = id; this.#logger = logger; - // Determine the syscall executor - let executeSyscall: SystemVatExecuteSyscall; - if ('executeSyscall' in props) { - // Direct wiring (legacy) - executeSyscall = props.executeSyscall; - } else { - // Deferred wiring via holder - const { syscallHandlerHolder } = props; - executeSyscall = (vso: VatSyscallObject): VatSyscallResult => { - if (!syscallHandlerHolder.handler) { - throw new Error('Syscall handler not yet wired'); - } - return syscallHandlerHolder.handler(vso); - }; - } - // Initialize the system vat synchronously during construction this.#initializeVat(buildRootObject, vatPowers, parameters, executeSyscall); } diff --git a/packages/ocap-kernel/src/vats/index.ts b/packages/ocap-kernel/src/vats/index.ts index 54f04acd9..c0d7486c1 100644 --- a/packages/ocap-kernel/src/vats/index.ts +++ b/packages/ocap-kernel/src/vats/index.ts @@ -1,9 +1,7 @@ export { SystemVatSupervisor } from './SystemVatSupervisor.ts'; -export { - makeSyscallHandlerHolder, - type SyscallHandlerHolder, - type SystemVatExecuteSyscall, - type SystemVatSupervisorMakeOptions, +export type { + SystemVatExecuteSyscall, + SystemVatSupervisorMakeOptions, } from './SystemVatSupervisor.ts'; export { SystemVatHandle } from './SystemVatHandle.ts'; export type { diff --git a/packages/ocap-kernel/test/integration/system-vat.test.ts b/packages/ocap-kernel/test/integration/system-vat.test.ts index c47b2c626..a95f831ca 100644 --- a/packages/ocap-kernel/test/integration/system-vat.test.ts +++ b/packages/ocap-kernel/test/integration/system-vat.test.ts @@ -15,10 +15,7 @@ import type { SystemVatSyscallHandler, SystemVatDeliverFn, } from '../../src/types.ts'; -import { - SystemVatSupervisor, - makeSyscallHandlerHolder, -} from '../../src/vats/SystemVatSupervisor.ts'; +import { SystemVatSupervisor } from '../../src/vats/SystemVatSupervisor.ts'; import { makeMapKernelDatabase } from '../storage.ts'; /** @@ -48,8 +45,8 @@ function makeTestSystemVat(options: { logger: Logger }): TestSystemVatResult { // Promise kit for kernel facet - resolves when bootstrap is called const kernelFacetKit = makePromiseKit(); - // Create syscall handler holder for deferred wiring - const syscallHandlerHolder = makeSyscallHandlerHolder(); + // Syscall handler - set by kernel during prepareSystemSubcluster() + let syscallHandler: SystemVatSyscallHandler | null = null; // Build root object that captures kernelFacet from bootstrap const buildRootObject: SystemVatBuildRootObject = () => { @@ -78,16 +75,19 @@ function makeTestSystemVat(options: { logger: Logger }): TestSystemVatResult { const transport: SystemVatTransport = { deliver, setSyscallHandler: (handler: SystemVatSyscallHandler) => { - syscallHandlerHolder.handler = handler; + syscallHandler = handler; }, awaitConnection: async () => connectionKit.promise, }; // Called after Kernel.make() to initiate connection from supervisor side const connect = (): void => { + if (!syscallHandler) { + throw new Error('Syscall handler not set'); + } SystemVatSupervisor.make({ buildRootObject, - syscallHandlerHolder, + executeSyscall: syscallHandler, logger: logger.subLogger({ tags: ['supervisor'] }), }) .then((supervisor) => { From 985e45462b1426a2d6e0bc24ee40d536738030de Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Thu, 29 Jan 2026 15:09:50 -0800 Subject: [PATCH 24/41] refactor(ocap-kernel): Simplify system vat abstraction - Rename "system subclusters" to "system vats" throughout - Rename SystemSubclusterManager to SystemVatManager - Rename host-subcluster to host-vat with simplified API - Add dynamic system vat registration via kernel facet - Remove DeliveryHelper (didn't achieve DRY savings) - Update Kernel to use systemVats config instead of systemSubclusters Co-Authored-By: Claude --- .../nodejs/src/host-subcluster/index.test.ts | 107 ----- packages/nodejs/src/host-vat/index.test.ts | 89 ++++ .../{host-subcluster => host-vat}/index.ts | 51 ++- packages/nodejs/src/index.ts | 4 +- ...-subcluster.test.ts => system-vat.test.ts} | 16 +- packages/ocap-kernel/src/Kernel.ts | 38 +- packages/ocap-kernel/src/index.ts | 14 +- packages/ocap-kernel/src/kernel-facet.ts | 79 +++- packages/ocap-kernel/src/types.ts | 56 +-- .../src/vats/SystemSubclusterManager.test.ts | 393 ------------------ .../src/vats/SystemSubclusterManager.ts | 310 -------------- .../ocap-kernel/src/vats/SystemVatHandle.ts | 21 +- .../src/vats/SystemVatManager.test.ts | 313 ++++++++++++++ .../ocap-kernel/src/vats/SystemVatManager.ts | 359 ++++++++++++++++ .../src/vats/SystemVatSupervisor.test.ts | 28 +- packages/ocap-kernel/src/vats/VatHandle.ts | 23 +- packages/ocap-kernel/src/vats/index.ts | 2 + .../test/integration/system-vat.test.ts | 40 +- 18 files changed, 982 insertions(+), 961 deletions(-) delete mode 100644 packages/nodejs/src/host-subcluster/index.test.ts create mode 100644 packages/nodejs/src/host-vat/index.test.ts rename packages/nodejs/src/{host-subcluster => host-vat}/index.ts (76%) rename packages/nodejs/test/e2e/{system-subcluster.test.ts => system-vat.test.ts} (95%) delete mode 100644 packages/ocap-kernel/src/vats/SystemSubclusterManager.test.ts delete mode 100644 packages/ocap-kernel/src/vats/SystemSubclusterManager.ts create mode 100644 packages/ocap-kernel/src/vats/SystemVatManager.test.ts create mode 100644 packages/ocap-kernel/src/vats/SystemVatManager.ts diff --git a/packages/nodejs/src/host-subcluster/index.test.ts b/packages/nodejs/src/host-subcluster/index.test.ts deleted file mode 100644 index 1472644a0..000000000 --- a/packages/nodejs/src/host-subcluster/index.test.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest'; - -import { makeHostSubcluster } from './index.ts'; - -// Mock SystemVatSupervisor -const mockStart = vi.fn(); -const mockDeliver = vi.fn(); - -vi.mock('@metamask/ocap-kernel/vats', () => { - return { - SystemVatSupervisor: class MockSystemVatSupervisor { - start = mockStart; - - deliver = mockDeliver; - }, - makeSyscallHandlerHolder: vi.fn(() => ({ handler: null })), - }; -}); - -describe('makeHostSubcluster', () => { - beforeEach(() => { - vi.clearAllMocks(); - mockStart.mockResolvedValue(null); - mockDeliver.mockResolvedValue(null); - }); - - it('returns config, start, and getKernelFacet', () => { - const result = makeHostSubcluster(); - - expect(result.config).toBeDefined(); - expect(result.start).toBeTypeOf('function'); - expect(result.getKernelFacet).toBeTypeOf('function'); - }); - - describe('config', () => { - it('has kernelHost as bootstrap vat', () => { - const { config } = makeHostSubcluster(); - - expect(config.bootstrap).toBe('kernelHost'); - }); - - it('has vatTransports with kernelHost transport', () => { - const { config } = makeHostSubcluster(); - - expect(config.vatTransports).toHaveLength(1); - expect(config.vatTransports[0]?.name).toBe('kernelHost'); - expect(config.vatTransports[0]?.transport).toBeDefined(); - expect(config.vatTransports[0]?.transport.deliver).toBeTypeOf('function'); - expect(config.vatTransports[0]?.transport.setSyscallHandler).toBeTypeOf( - 'function', - ); - }); - }); - - describe('start', () => { - it('creates and starts the supervisor', async () => { - const { start } = makeHostSubcluster(); - - await start(); - - expect(mockStart).toHaveBeenCalled(); - }); - - it('throws if supervisor start throws', async () => { - mockStart.mockRejectedValueOnce(new Error('Start failed')); - - const { start } = makeHostSubcluster(); - - await expect(start()).rejects.toThrow('Start failed'); - }); - }); - - describe('getKernelFacet', () => { - it('throws if called before kernel facet is available', () => { - const { getKernelFacet } = makeHostSubcluster(); - - expect(() => getKernelFacet()).toThrow( - 'Kernel facet not available. Ensure start() was called and kernel has bootstrapped.', - ); - }); - }); - - describe('transport', () => { - it('deliver waits for supervisor then calls it', async () => { - const { config, start } = makeHostSubcluster(); - - const delivery = { - type: 'message' as const, - methargs: { body: '[]', slots: [] }, - result: 'p-1', - target: 'o+0', - }; - - // Start the delivery (it will wait for supervisor) - const deliverPromise = - config.vatTransports[0]?.transport.deliver(delivery); - - // Start the supervisor - this should unblock the delivery - await start(); - - // Now the delivery should complete - await deliverPromise; - - expect(mockDeliver).toHaveBeenCalledWith(delivery); - }); - }); -}); diff --git a/packages/nodejs/src/host-vat/index.test.ts b/packages/nodejs/src/host-vat/index.test.ts new file mode 100644 index 000000000..d9918dd7a --- /dev/null +++ b/packages/nodejs/src/host-vat/index.test.ts @@ -0,0 +1,89 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; + +import { makeHostVat } from './index.ts'; + +// Mock SystemVatSupervisor +const mockDeliverFn = vi.fn(); +const mockMakeFn = vi.fn(); + +vi.mock('@metamask/ocap-kernel/vats', () => ({ + SystemVatSupervisor: { + make: (...args: unknown[]) => mockMakeFn(...args), + }, +})); + +describe('makeHostVat', () => { + beforeEach(() => { + vi.clearAllMocks(); + mockDeliverFn.mockResolvedValue(null); + mockMakeFn.mockResolvedValue({ deliver: mockDeliverFn }); + }); + + it('returns config, connect, and kernelFacetPromise', () => { + const result = makeHostVat(); + + expect(result.config).toBeDefined(); + expect(result.connect).toBeTypeOf('function'); + expect(result.kernelFacetPromise).toBeInstanceOf(Promise); + }); + + describe('config', () => { + it('has kernelHost as default name', () => { + const { config } = makeHostVat(); + + expect(config.name).toBe('kernelHost'); + }); + + it('uses custom name when provided', () => { + const { config } = makeHostVat({ name: 'customHost' }); + + expect(config.name).toBe('customHost'); + }); + + it('has transport with deliver and setSyscallHandler', () => { + const { config } = makeHostVat(); + + expect(config.transport).toBeDefined(); + expect(config.transport.deliver).toBeTypeOf('function'); + expect(config.transport.setSyscallHandler).toBeTypeOf('function'); + expect(config.transport.awaitConnection).toBeTypeOf('function'); + }); + }); + + describe('connect', () => { + it('throws if syscall handler not set', () => { + const { connect } = makeHostVat(); + + expect(() => connect()).toThrow( + 'Cannot connect: syscall handler not set. Was Kernel.make() called with this config?', + ); + }); + }); + + describe('transport', () => { + it('deliver waits for supervisor then calls it', async () => { + const { config, connect } = makeHostVat(); + + // Set the syscall handler (simulating kernel setup) + config.transport.setSyscallHandler(vi.fn()); + + const delivery = { + type: 'message' as const, + methargs: { body: '[]', slots: [] }, + result: 'p-1', + target: 'o+0', + }; + + // Start the delivery (it will wait for supervisor) + const deliverPromise = config.transport.deliver(delivery); + + // Start the supervisor - this should unblock the delivery + connect(); + + // Now the delivery should complete + await deliverPromise; + + expect(mockDeliverFn).toHaveBeenCalledWith(delivery); + }); + }); +}); diff --git a/packages/nodejs/src/host-subcluster/index.ts b/packages/nodejs/src/host-vat/index.ts similarity index 76% rename from packages/nodejs/src/host-subcluster/index.ts rename to packages/nodejs/src/host-vat/index.ts index 5a7434125..7872c1cf4 100644 --- a/packages/nodejs/src/host-subcluster/index.ts +++ b/packages/nodejs/src/host-vat/index.ts @@ -4,7 +4,7 @@ import { Logger } from '@metamask/logger'; import type { SystemVatBuildRootObject, KernelFacet, - KernelSystemSubclusterConfig, + StaticSystemVatConfig, SystemVatTransport, SystemVatSyscallHandler, SystemVatDeliverFn, @@ -12,13 +12,13 @@ import type { import { SystemVatSupervisor } from '@metamask/ocap-kernel/vats'; /** - * Result of creating a host subcluster. + * Result of creating a host vat. */ -export type HostSubclusterResult = { +export type HostVatResult = { /** - * Configuration to pass to Kernel.make() systemSubclusters option. + * Configuration to pass to Kernel.make() systemVats option. */ - config: KernelSystemSubclusterConfig; + config: StaticSystemVatConfig; /** * Call after Kernel.make() returns to initiate connection from supervisor side. @@ -35,39 +35,41 @@ export type HostSubclusterResult = { }; /** - * Create a host subcluster for use with Kernel.make(). + * Create a host vat for use with Kernel.make(). * * This creates the supervisor and transport configuration needed to connect - * a host subcluster to the kernel. The supervisor is created when `connect()` + * a host vat to the kernel. The supervisor is created when `connect()` * is called (after Kernel.make() returns). * * Usage: * ```typescript - * const hostSubcluster = makeHostSubcluster({ logger }); + * const hostVat = makeHostVat({ logger }); * const kernel = await Kernel.make(platformServices, db, { - * systemSubclusters: { subclusters: [hostSubcluster.config] }, + * systemVats: { vats: [hostVat.config] }, * }); - * hostSubcluster.connect(); // Supervisor pushes connection to kernel - * const kernelFacet = await hostSubcluster.kernelFacetPromise; + * hostVat.connect(); // Supervisor pushes connection to kernel + * const kernelFacet = await hostVat.kernelFacetPromise; * const result = await E(kernelFacet).launchSubcluster(config); * ``` * - * @param options - Options for creating the host subcluster. + * @param options - Options for creating the host vat. + * @param options.name - Optional name for the host vat (default: 'kernelHost'). * @param options.logger - Optional logger for the supervisor. - * @returns The host subcluster result with config, connect, and kernelFacetPromise. + * @returns The host vat result with config, connect, and kernelFacetPromise. */ -export function makeHostSubcluster( +export function makeHostVat( options: { + name?: string; logger?: Logger; } = {}, -): HostSubclusterResult { - const logger = options.logger ?? new Logger('host-subcluster'); - const vatName = 'kernelHost'; +): HostVatResult { + const logger = options.logger ?? new Logger('host-vat'); + const vatName = options.name ?? 'kernelHost'; // Promise kit for kernel facet - resolves when bootstrap is called const kernelFacetKit = makePromiseKit(); - // Syscall handler - set by kernel during prepareSystemSubcluster() + // Syscall handler - set by kernel during prepareStaticSystemVat() let syscallHandler: SystemVatSyscallHandler | null = null; // Build root object that captures kernelFacet from bootstrap @@ -134,14 +136,9 @@ export function makeHostSubcluster( }; // Config for Kernel.make() - const config: KernelSystemSubclusterConfig = { - bootstrap: vatName, - vatTransports: [ - { - name: vatName, - transport, - }, - ], + const config: StaticSystemVatConfig = { + name: vatName, + transport, }; return harden({ @@ -150,4 +147,4 @@ export function makeHostSubcluster( kernelFacetPromise: kernelFacetKit.promise, }); } -harden(makeHostSubcluster); +harden(makeHostVat); diff --git a/packages/nodejs/src/index.ts b/packages/nodejs/src/index.ts index 9a500d763..5a6c4041e 100644 --- a/packages/nodejs/src/index.ts +++ b/packages/nodejs/src/index.ts @@ -1,5 +1,5 @@ export { NodejsPlatformServices } from './kernel/PlatformServices.ts'; export { makeKernel } from './kernel/make-kernel.ts'; export { makeNodeJsVatSupervisor } from './vat/make-supervisor.ts'; -export { makeHostSubcluster } from './host-subcluster/index.ts'; -export type { HostSubclusterResult } from './host-subcluster/index.ts'; +export { makeHostVat } from './host-vat/index.ts'; +export type { HostVatResult } from './host-vat/index.ts'; diff --git a/packages/nodejs/test/e2e/system-subcluster.test.ts b/packages/nodejs/test/e2e/system-vat.test.ts similarity index 95% rename from packages/nodejs/test/e2e/system-subcluster.test.ts rename to packages/nodejs/test/e2e/system-vat.test.ts index ba5d5b913..1da7a3597 100644 --- a/packages/nodejs/test/e2e/system-subcluster.test.ts +++ b/packages/nodejs/test/e2e/system-vat.test.ts @@ -5,7 +5,7 @@ import type { ClusterConfig, KernelFacet } from '@metamask/ocap-kernel'; import { Kernel } from '@metamask/ocap-kernel'; import { describe, it, expect, beforeEach, afterEach } from 'vitest'; -import { makeHostSubcluster } from '../../src/host-subcluster/index.ts'; +import { makeHostVat } from '../../src/host-vat/index.ts'; import { NodejsPlatformServices } from '../../src/kernel/PlatformServices.ts'; type Bob = { @@ -27,17 +27,17 @@ type PromiseVat = { awaitPromiseArg: (promiseArg: Promise) => Promise; }; -describe('system subcluster e2e tests', { timeout: 30_000 }, () => { +describe('system vat e2e tests', { timeout: 30_000 }, () => { let kernel: Kernel; let kernelFacet: KernelFacet | Promise; beforeEach(async () => { const logger = new Logger('test'); - // Create host subcluster first - const hostSubcluster = makeHostSubcluster({ logger }); + // Create host vat first + const hostVat = makeHostVat({ logger }); - // Create kernel with system subcluster config + // Create kernel with system vat config const platformServices = new NodejsPlatformServices({ logger: logger.subLogger({ tags: ['platform-services'] }), }); @@ -47,14 +47,14 @@ describe('system subcluster e2e tests', { timeout: 30_000 }, () => { kernel = await Kernel.make(platformServices, kernelDatabase, { resetStorage: true, logger: logger.subLogger({ tags: ['kernel'] }), - systemSubclusters: { subclusters: [hostSubcluster.config] }, + systemVats: { vats: [hostVat.config] }, }); // Supervisor-side initiates connection AFTER kernel exists - hostSubcluster.connect(); + hostVat.connect(); // Wait for kernel facet - resolves after bootstrap message is delivered - kernelFacet = await hostSubcluster.kernelFacetPromise; + kernelFacet = await hostVat.kernelFacetPromise; }); afterEach(async () => { diff --git a/packages/ocap-kernel/src/Kernel.ts b/packages/ocap-kernel/src/Kernel.ts index a54b33109..0f7ee4d9a 100644 --- a/packages/ocap-kernel/src/Kernel.ts +++ b/packages/ocap-kernel/src/Kernel.ts @@ -19,7 +19,7 @@ import type { KRef, PlatformServices, ClusterConfig, - KernelSystemSubclustersConfig, + KernelSystemVatsConfig, VatConfig, KernelStatus, Subcluster, @@ -28,7 +28,7 @@ import type { } from './types.ts'; import { isVatId, isRemoteId, isSystemVatId } from './types.ts'; import { SubclusterManager } from './vats/SubclusterManager.ts'; -import { SystemSubclusterManager } from './vats/SystemSubclusterManager.ts'; +import { SystemVatManager } from './vats/SystemVatManager.ts'; import type { VatHandle } from './vats/VatHandle.ts'; import { VatManager } from './vats/VatManager.ts'; @@ -52,8 +52,8 @@ export class Kernel { /** Manages subcluster operations */ readonly #subclusterManager: SubclusterManager; - /** Manages system subcluster operations */ - readonly #systemSubclusterManager: SystemSubclusterManager; + /** Manages system vat operations */ + readonly #systemVatManager: SystemVatManager; /** Manages remote kernel connections */ readonly #remoteManager: RemoteManager; @@ -83,10 +83,10 @@ export class Kernel { readonly #kernelRouter: KernelRouter; /** - * System subclusters configuration passed to Kernel.make(). + * System vats configuration passed to Kernel.make(). * Stored for connection after initialization. */ - readonly #systemSubclustersConfig: KernelSystemSubclustersConfig | undefined; + readonly #systemVatsConfig: KernelSystemVatsConfig | undefined; /** * Construct a new kernel instance. @@ -98,7 +98,7 @@ export class Kernel { * @param options.logger - Optional logger for error and diagnostic output. * @param options.keySeed - Optional seed for libp2p key generation. * @param options.mnemonic - Optional BIP39 mnemonic for deriving the kernel identity. - * @param options.systemSubclusters - Optional system subclusters to connect at kernel creation. + * @param options.systemVats - Optional system vats to connect at kernel creation. */ // eslint-disable-next-line no-restricted-syntax private constructor( @@ -109,13 +109,13 @@ export class Kernel { logger?: Logger; keySeed?: string | undefined; mnemonic?: string | undefined; - systemSubclusters?: KernelSystemSubclustersConfig; + systemVats?: KernelSystemVatsConfig; } = {}, ) { this.#platformServices = platformServices; this.#logger = options.logger ?? new Logger('ocap-kernel'); this.#kernelStore = makeKernelStore(kernelDatabase, this.#logger); - this.#systemSubclustersConfig = options.systemSubclusters; + this.#systemVatsConfig = options.systemVats; if (!this.#kernelStore.kv.get('initialized')) { this.#kernelStore.kv.set('initialized', 'true'); } @@ -170,7 +170,7 @@ export class Kernel { queueMessage: this.queueMessage.bind(this), }); - this.#systemSubclusterManager = new SystemSubclusterManager({ + this.#systemVatManager = new SystemVatManager({ kernelStore: this.#kernelStore, kernelQueue: this.#kernelQueue, kernelFacetDeps: { @@ -184,7 +184,7 @@ export class Kernel { }, registerKernelService: (name, service) => this.#kernelServiceManager.registerKernelServiceObject(name, service), - logger: this.#logger.subLogger({ tags: ['SystemSubclusterManager'] }), + logger: this.#logger.subLogger({ tags: ['SystemVatManager'] }), }); this.#kernelRouter = new KernelRouter( @@ -222,7 +222,7 @@ export class Kernel { * @param options.logger - Optional logger for error and diagnostic output. * @param options.keySeed - Optional seed for libp2p key generation. * @param options.mnemonic - Optional BIP39 mnemonic for deriving the kernel identity. - * @param options.systemSubclusters - Optional system subclusters to connect at kernel creation. + * @param options.systemVats - Optional system vats to connect at kernel creation. * @returns A promise for the new kernel instance. */ static async make( @@ -233,7 +233,7 @@ export class Kernel { logger?: Logger; keySeed?: string | undefined; mnemonic?: string | undefined; - systemSubclusters?: KernelSystemSubclustersConfig; + systemVats?: KernelSystemVatsConfig; } = {}, ): Promise { const kernel = new Kernel(platformServices, kernelDatabase, options); @@ -255,13 +255,12 @@ export class Kernel { // This ensures that any messages in the queue have their target vats ready await this.#vatManager.initializeAllVats(); - // Prepare system subclusters if configured (kernel side setup only). + // Prepare static system vats if configured (kernel side setup only). // The kernel sets up to receive connections - it does NOT reach out. // Actual connection happens when supervisor-side calls connect(). - if (this.#systemSubclustersConfig) { - const { subclusters } = this.#systemSubclustersConfig; - for (const subclusterConfig of subclusters) { - this.#systemSubclusterManager.prepareSystemSubcluster(subclusterConfig); + if (this.#systemVatsConfig) { + for (const vatConfig of this.#systemVatsConfig.vats) { + this.#systemVatManager.prepareStaticSystemVat(vatConfig); } } @@ -456,8 +455,7 @@ export class Kernel { } if (isSystemVatId(endpointId)) { const systemVatId = endpointId as SystemVatId; - const handle = - this.#systemSubclusterManager.getSystemVatHandle(systemVatId); + const handle = this.#systemVatManager.getSystemVatHandle(systemVatId); if (!handle) { throw Error(`system vat ${systemVatId} not found`); } diff --git a/packages/ocap-kernel/src/index.ts b/packages/ocap-kernel/src/index.ts index eda1fcc8f..5ac71de38 100644 --- a/packages/ocap-kernel/src/index.ts +++ b/packages/ocap-kernel/src/index.ts @@ -15,15 +15,15 @@ export type { SubclusterLaunchResult, // System vat types SystemVatId, - SystemSubclusterId, SystemVatBuildRootObject, // System vat transport types (for Kernel.make()) SystemVatTransport, SystemVatSyscallHandler, SystemVatDeliverFn, - SystemVatConnectionConfig, - KernelSystemSubclusterConfig, - KernelSystemSubclustersConfig, + StaticSystemVatConfig, + DynamicSystemVatConfig, + SystemVatRegistrationResult, + KernelSystemVatsConfig, } from './types.ts'; export type { RemoteMessageHandler, @@ -47,7 +47,11 @@ export { } from './types.ts'; export { kunser, kser, kslot, krefOf } from './liveslots/kernel-marshal.ts'; export type { SlotValue } from './liveslots/kernel-marshal.ts'; -export type { KernelFacet, KernelFacetLaunchResult } from './kernel-facet.ts'; +export type { + KernelFacet, + KernelFacetLaunchResult, + KernelFacetRegisterSystemVatResult, +} from './kernel-facet.ts'; export { makeKernelStore } from './store/index.ts'; export type { KernelStore } from './store/index.ts'; export { parseRef } from './store/utils/parse-ref.ts'; diff --git a/packages/ocap-kernel/src/kernel-facet.ts b/packages/ocap-kernel/src/kernel-facet.ts index e58995032..24d2987ce 100644 --- a/packages/ocap-kernel/src/kernel-facet.ts +++ b/packages/ocap-kernel/src/kernel-facet.ts @@ -4,7 +4,14 @@ import type { Logger } from '@metamask/logger'; import type { Kernel } from './Kernel.ts'; import { kslot } from './liveslots/kernel-marshal.ts'; import type { SlotValue } from './liveslots/kernel-marshal.ts'; -import type { ClusterConfig, Subcluster, KernelStatus } from './types.ts'; +import type { + ClusterConfig, + Subcluster, + KernelStatus, + DynamicSystemVatConfig, + SystemVatId, +} from './types.ts'; +import type { SystemVatManager } from './vats/SystemVatManager.ts'; /** * Dependencies required to create a kernel facet. @@ -19,6 +26,8 @@ export type KernelFacetDependencies = Pick< | 'getStatus' > & { logger?: Logger; + /** Optional system vat manager for dynamic registration. */ + systemVatManager?: Pick; }; /** @@ -41,11 +50,31 @@ export type KernelFacetLaunchResult = { rootKref: string; }; +/** + * Result of registering a dynamic system vat via the kernel facet. + */ +export type KernelFacetRegisterSystemVatResult = { + /** The allocated system vat ID. */ + systemVatId: SystemVatId; + /** + * The root object as a slot value (becomes a presence when marshalled). + */ + root: SlotValue; + /** + * The root kref string for storage purposes. + */ + rootKref: string; + /** + * Function to disconnect and clean up the vat. + */ + disconnect: () => Promise; +}; + /** * The kernel facet interface. * * This is the interface provided as a vatpower to the bootstrap vat of a - * system subcluster. It enables privileged kernel operations. + * system vat. It enables privileged kernel operations. * * Derived from KernelFacetDependencies but with launchSubcluster overridden * to return KernelFacetLaunchResult (root as SlotValue) instead of @@ -53,7 +82,7 @@ export type KernelFacetLaunchResult = { */ export type KernelFacet = Omit< KernelFacetDependencies, - 'logger' | 'launchSubcluster' + 'logger' | 'launchSubcluster' | 'systemVatManager' > & { /** * Launch a dynamic subcluster. @@ -64,6 +93,17 @@ export type KernelFacet = Omit< */ launchSubcluster: (config: ClusterConfig) => Promise; + /** + * Register a dynamic system vat at runtime. + * Used by UIs and other components that connect after kernel initialization. + * + * @param config - Configuration for the dynamic system vat. + * @returns A promise for the registration result. + */ + registerSystemVat: ( + config: DynamicSystemVatConfig, + ) => Promise; + /** * Convert a kref string to a slot value (presence). * @@ -79,8 +119,9 @@ export type KernelFacet = Omit< * Creates a kernel facet object that provides privileged kernel operations. * * The kernel facet is provided as a vatpower to the bootstrap vat of a - * system subcluster. It enables the bootstrap vat to: + * system vat. It enables the bootstrap vat to: * - Launch dynamic subclusters (and receive E()-callable presences) + * - Register dynamic system vats at runtime * - Terminate subclusters * - Reload subclusters * - Query kernel status @@ -97,6 +138,7 @@ export function makeKernelFacet(deps: KernelFacetDependencies): KernelFacet { getSubclusters, getStatus, logger, + systemVatManager, } = deps; const kernelFacet = makeDefaultExo('kernelFacet', { @@ -176,6 +218,35 @@ export function makeKernelFacet(deps: KernelFacetDependencies): KernelFacet { return getStatus(); }, + /** + * Register a dynamic system vat at runtime. + * Used by UIs and other components that connect after kernel initialization. + * + * @param config - Configuration for the dynamic system vat. + * @returns A promise for the registration result. + */ + async registerSystemVat( + config: DynamicSystemVatConfig, + ): Promise { + if (!systemVatManager) { + throw new Error( + 'Cannot register system vat: systemVatManager not provided to kernel facet', + ); + } + logger?.log(`kernelFacet: registering dynamic system vat ${config.name}`); + const result = await systemVatManager.registerDynamicSystemVat(config); + logger?.log( + `kernelFacet: registered system vat ${result.systemVatId} with root ${result.rootKref}`, + ); + + return { + systemVatId: result.systemVatId, + root: kslot(result.rootKref, 'vatRoot'), + rootKref: result.rootKref, + disconnect: result.disconnect, + }; + }, + /** * Convert a kref string to a slot value (presence). * diff --git a/packages/ocap-kernel/src/types.ts b/packages/ocap-kernel/src/types.ts index c0167f657..b36fb4024 100644 --- a/packages/ocap-kernel/src/types.ts +++ b/packages/ocap-kernel/src/types.ts @@ -45,7 +45,6 @@ export type RemoteId = string; export type SystemVatId = `sv${number}`; export type EndpointId = VatId | RemoteId | SystemVatId; export type SubclusterId = string; -export type SystemSubclusterId = string; export type KRef = string; export type VRef = string; @@ -467,18 +466,8 @@ export type SystemSubclusterConfig = { services?: string[]; }; -/** - * Result of launching a system subcluster. - */ -export type SystemSubclusterLaunchResult = { - /** The ID of the launched system subcluster. */ - systemSubclusterId: SystemSubclusterId; - /** Map of vat names to their system vat IDs. */ - vatIds: Record; -}; - // ============================================================================ -// System Vat Transport Types (for Kernel.make() static configuration) +// System Vat Transport Types // ============================================================================ /** @@ -527,34 +516,49 @@ export type SystemVatTransport = { }; /** - * Configuration for a single system vat with transport (for Kernel.make). + * Configuration for a static system vat (declared at kernel construction). * The runtime creates the supervisor and provides the transport. */ -export type SystemVatConnectionConfig = { - /** Vat name (matches key in subcluster config). */ +export type StaticSystemVatConfig = { + /** Vat name (used in bootstrap message). */ name: string; /** Transport callbacks for communication. */ transport: SystemVatTransport; + /** Optional kernel services to provide to the vat. */ + services?: string[]; }; /** - * System subcluster configuration for Kernel.make(). - * Used to connect to pre-existing system vat supervisors at kernel creation time. + * Configuration for a dynamic system vat (registered at runtime via kernel facet). + * Used by UIs and other components that connect after kernel initialization. */ -export type KernelSystemSubclusterConfig = { - /** Name of the bootstrap vat. */ - bootstrap: string; - /** Transport connections for each vat. */ - vatTransports: SystemVatConnectionConfig[]; - /** Kernel services to provide to bootstrap vat. */ +export type DynamicSystemVatConfig = { + /** Vat name (used in bootstrap message). */ + name: string; + /** Transport callbacks for communication. */ + transport: SystemVatTransport; + /** Optional kernel services to provide to the vat. */ services?: string[]; }; /** - * System subclusters configuration for Kernel.make(). + * Result of registering a dynamic system vat. + */ +export type SystemVatRegistrationResult = { + /** The allocated system vat ID. */ + systemVatId: SystemVatId; + /** The kref of the vat's root object. */ + rootKref: KRef; + /** Function to disconnect and clean up the vat. */ + disconnect: () => Promise; +}; + +/** + * System vats configuration for Kernel.make(). + * List of static system vats to connect at kernel creation time. */ -export type KernelSystemSubclustersConfig = { - subclusters: KernelSystemSubclusterConfig[]; +export type KernelSystemVatsConfig = { + vats: StaticSystemVatConfig[]; }; export const SubclusterStruct = object({ diff --git a/packages/ocap-kernel/src/vats/SystemSubclusterManager.test.ts b/packages/ocap-kernel/src/vats/SystemSubclusterManager.test.ts deleted file mode 100644 index 8a5a94cf6..000000000 --- a/packages/ocap-kernel/src/vats/SystemSubclusterManager.test.ts +++ /dev/null @@ -1,393 +0,0 @@ -import type { Logger } from '@metamask/logger'; -import { describe, it, expect, vi, beforeEach } from 'vitest'; -import type { MockInstance } from 'vitest'; - -import type { KernelFacetDependencies } from '../kernel-facet.ts'; -import type { KernelQueue } from '../KernelQueue.ts'; -import type { KernelStore } from '../store/index.ts'; -import type { - KernelSystemSubclusterConfig, - SystemVatId, - SystemVatTransport, -} from '../types.ts'; -import { SystemSubclusterManager } from './SystemSubclusterManager.ts'; - -describe('SystemSubclusterManager', () => { - let kernelStore: KernelStore; - let kernelQueue: KernelQueue; - let kernelFacetDeps: KernelFacetDependencies; - let logger: Logger; - let manager: SystemSubclusterManager; - - /** - * Creates a mock transport for testing. - * The transport's awaitConnection() resolves immediately by default. - * - * @returns A mock transport with vi.fn() implementations. - */ - function makeMockTransport(): SystemVatTransport { - return { - deliver: vi.fn().mockResolvedValue(null), - setSyscallHandler: vi.fn(), - awaitConnection: vi.fn().mockResolvedValue(undefined), - }; - } - - beforeEach(() => { - vi.clearAllMocks(); - - kernelStore = { - initEndpoint: vi.fn(), - erefToKref: vi.fn().mockReturnValue(null), - initKernelObject: vi.fn().mockReturnValue('ko1'), - addCListEntry: vi.fn(), - translateSyscallVtoK: vi.fn((_, vso) => vso), - getKernelPromise: vi.fn(() => ({ state: 'unresolved' })), - addPromiseSubscriber: vi.fn(), - clearReachableFlag: vi.fn(), - getReachableFlag: vi.fn(), - forgetKref: vi.fn(), - getPromisesByDecider: vi.fn(() => []), - deleteEndpoint: vi.fn(), - kv: { - get: vi.fn().mockReturnValue(undefined), - }, - } as unknown as KernelStore; - - kernelQueue = { - waitForCrank: vi.fn().mockResolvedValue(undefined), - enqueueSend: vi.fn(), - resolvePromises: vi.fn(), - enqueueNotify: vi.fn(), - enqueueMessage: vi.fn(), - } as unknown as KernelQueue; - - kernelFacetDeps = { - launchSubcluster: vi.fn().mockResolvedValue({ - subclusterId: 's1', - bootstrapRootKref: 'ko2', - }), - terminateSubcluster: vi.fn().mockResolvedValue(undefined), - reloadSubcluster: vi.fn().mockResolvedValue({ id: 's2' }), - getSubcluster: vi.fn().mockReturnValue(undefined), - getSubclusters: vi.fn().mockReturnValue([]), - getStatus: vi.fn().mockResolvedValue({ initialized: true }), - }; - - logger = { - debug: vi.fn(), - error: vi.fn(), - log: vi.fn(), - warn: vi.fn(), - subLogger: vi.fn(() => logger), - } as unknown as Logger; - - manager = new SystemSubclusterManager({ - kernelStore, - kernelQueue, - kernelFacetDeps, - registerKernelService: vi - .fn() - .mockReturnValue({ kref: 'ko-kernelFacet' }), - logger, - }); - }); - - describe('prepareSystemSubcluster', () => { - it('throws if bootstrap vat is not in vatTransports', () => { - const config: KernelSystemSubclusterConfig = { - bootstrap: 'missing', - vatTransports: [{ name: 'testVat', transport: makeMockTransport() }], - }; - - expect(() => manager.prepareSystemSubcluster(config)).toThrow( - 'invalid bootstrap vat name missing', - ); - }); - - it('allocates system vat IDs starting from sv0', () => { - const config: KernelSystemSubclusterConfig = { - bootstrap: 'testVat', - vatTransports: [{ name: 'testVat', transport: makeMockTransport() }], - }; - - const result = manager.prepareSystemSubcluster(config); - - expect(result.vatIds.testVat).toBe('sv0'); - }); - - it('allocates incrementing system vat IDs', () => { - const config1: KernelSystemSubclusterConfig = { - bootstrap: 'testVat', - vatTransports: [{ name: 'testVat', transport: makeMockTransport() }], - }; - const config2: KernelSystemSubclusterConfig = { - bootstrap: 'testVat', - vatTransports: [{ name: 'testVat', transport: makeMockTransport() }], - }; - - const result1 = manager.prepareSystemSubcluster(config1); - const result2 = manager.prepareSystemSubcluster(config2); - - expect(result1.vatIds.testVat).toBe('sv0'); - expect(result2.vatIds.testVat).toBe('sv1'); - }); - - it('allocates system subcluster IDs starting from ss0', () => { - const config: KernelSystemSubclusterConfig = { - bootstrap: 'testVat', - vatTransports: [{ name: 'testVat', transport: makeMockTransport() }], - }; - - const result = manager.prepareSystemSubcluster(config); - - expect(result.systemSubclusterId).toBe('ss0'); - }); - - it('initializes endpoints for each vat', () => { - const config: KernelSystemSubclusterConfig = { - bootstrap: 'testVat', - vatTransports: [{ name: 'testVat', transport: makeMockTransport() }], - }; - - manager.prepareSystemSubcluster(config); - - expect(kernelStore.initEndpoint).toHaveBeenCalledWith('sv0'); - }); - - it('initializes kernel objects for vat roots', () => { - const config: KernelSystemSubclusterConfig = { - bootstrap: 'testVat', - vatTransports: [{ name: 'testVat', transport: makeMockTransport() }], - }; - - manager.prepareSystemSubcluster(config); - - expect(kernelStore.initKernelObject).toHaveBeenCalledWith('sv0'); - }); - - it('adds clist entries for root objects', () => { - const config: KernelSystemSubclusterConfig = { - bootstrap: 'testVat', - vatTransports: [{ name: 'testVat', transport: makeMockTransport() }], - }; - - manager.prepareSystemSubcluster(config); - - expect(kernelStore.addCListEntry).toHaveBeenCalledWith( - 'sv0', - 'ko1', - 'o+0', - ); - }); - - it('enqueues bootstrap message after connection', async () => { - const config: KernelSystemSubclusterConfig = { - bootstrap: 'testVat', - vatTransports: [{ name: 'testVat', transport: makeMockTransport() }], - }; - - manager.prepareSystemSubcluster(config); - - // Wait for the async connection callback to fire - await vi.waitFor(() => { - expect(kernelQueue.enqueueSend).toHaveBeenCalledWith( - 'ko1', - expect.objectContaining({ - methargs: expect.any(Object), - }), - ); - }); - }); - - it('wires syscall handler to transport', () => { - const transport = makeMockTransport(); - const config: KernelSystemSubclusterConfig = { - bootstrap: 'testVat', - vatTransports: [{ name: 'testVat', transport }], - }; - - manager.prepareSystemSubcluster(config); - - expect(transport.setSyscallHandler).toHaveBeenCalledWith( - expect.any(Function), - ); - }); - - it('prepares multiple vats in a subcluster', () => { - const config: KernelSystemSubclusterConfig = { - bootstrap: 'bootstrap', - vatTransports: [ - { name: 'bootstrap', transport: makeMockTransport() }, - { name: 'worker', transport: makeMockTransport() }, - ], - }; - - const result = manager.prepareSystemSubcluster(config); - - expect(result.vatIds.bootstrap).toBe('sv0'); - expect(result.vatIds.worker).toBe('sv1'); - expect(kernelStore.initEndpoint).toHaveBeenCalledTimes(2); - }); - - it('uses existing root kref if available', async () => { - (kernelStore.erefToKref as unknown as MockInstance).mockReturnValueOnce( - 'ko-existing', - ); - - const config: KernelSystemSubclusterConfig = { - bootstrap: 'testVat', - vatTransports: [{ name: 'testVat', transport: makeMockTransport() }], - }; - - manager.prepareSystemSubcluster(config); - - // Should not create new kernel object for root - // (only kernel facet is created via registerKernelService which is mocked) - expect(kernelStore.initKernelObject).not.toHaveBeenCalled(); - - // Wait for the async connection callback to fire - await vi.waitFor(() => { - expect(kernelQueue.enqueueSend).toHaveBeenCalledWith( - 'ko-existing', - expect.objectContaining({ - methargs: expect.any(Object), - }), - ); - }); - }); - - it('warns if requested service is not found', () => { - const config: KernelSystemSubclusterConfig = { - bootstrap: 'testVat', - vatTransports: [{ name: 'testVat', transport: makeMockTransport() }], - services: ['unknownService'], - }; - - manager.prepareSystemSubcluster(config); - - expect(logger.warn).toHaveBeenCalledWith( - "Kernel service 'unknownService' not found", - ); - }); - - it('includes services in bootstrap message when available', async () => { - (kernelStore.kv.get as unknown as MockInstance).mockReturnValueOnce( - 'ko-service', - ); - - const config: KernelSystemSubclusterConfig = { - bootstrap: 'testVat', - vatTransports: [{ name: 'testVat', transport: makeMockTransport() }], - services: ['myService'], - }; - - manager.prepareSystemSubcluster(config); - - // Wait for the async connection callback to fire - await vi.waitFor(() => { - expect(kernelQueue.enqueueSend).toHaveBeenCalledWith( - 'ko1', - expect.objectContaining({ - methargs: expect.any(Object), - }), - ); - }); - }); - - it('creates singleton kernel facet across multiple subclusters', () => { - // Create a manager with a register function that tracks calls - const registerCalls: string[] = []; - const managerWithTracking = new SystemSubclusterManager({ - kernelStore, - kernelQueue, - kernelFacetDeps, - registerKernelService: vi.fn().mockImplementation((name: string) => { - registerCalls.push(name); - return { kref: 'ko-kernelFacet' }; - }), - logger, - }); - - const config1: KernelSystemSubclusterConfig = { - bootstrap: 'vat1', - vatTransports: [{ name: 'vat1', transport: makeMockTransport() }], - }; - const config2: KernelSystemSubclusterConfig = { - bootstrap: 'vat2', - vatTransports: [{ name: 'vat2', transport: makeMockTransport() }], - }; - - managerWithTracking.prepareSystemSubcluster(config1); - managerWithTracking.prepareSystemSubcluster(config2); - - // registerKernelService should only be called once (singleton) - expect( - registerCalls.filter((name) => name === 'kernelFacet'), - ).toHaveLength(1); - }); - - it('includes kernelFacet in bootstrap message', async () => { - const config: KernelSystemSubclusterConfig = { - bootstrap: 'testVat', - vatTransports: [{ name: 'testVat', transport: makeMockTransport() }], - }; - - manager.prepareSystemSubcluster(config); - - // Wait for the async connection callback to fire - await vi.waitFor(() => { - expect(kernelQueue.enqueueSend).toHaveBeenCalledWith( - 'ko1', - expect.objectContaining({ - methargs: expect.objectContaining({ - // The methargs should contain the kernelFacet kref in slots - slots: expect.arrayContaining(['ko-kernelFacet']), - }), - }), - ); - }); - }); - }); - - describe('getSystemVatHandle', () => { - it('returns handle for prepared system vat', () => { - const config: KernelSystemSubclusterConfig = { - bootstrap: 'testVat', - vatTransports: [{ name: 'testVat', transport: makeMockTransport() }], - }; - manager.prepareSystemSubcluster(config); - - const handle = manager.getSystemVatHandle('sv0' as SystemVatId); - - expect(handle).toBeDefined(); - expect(handle?.systemVatId).toBe('sv0'); - }); - - it('returns undefined for unknown system vat', () => { - const handle = manager.getSystemVatHandle('sv-unknown' as SystemVatId); - - expect(handle).toBeUndefined(); - }); - - it('finds handle across multiple subclusters', () => { - const config1: KernelSystemSubclusterConfig = { - bootstrap: 'vat1', - vatTransports: [{ name: 'vat1', transport: makeMockTransport() }], - }; - const config2: KernelSystemSubclusterConfig = { - bootstrap: 'vat2', - vatTransports: [{ name: 'vat2', transport: makeMockTransport() }], - }; - - manager.prepareSystemSubcluster(config1); - manager.prepareSystemSubcluster(config2); - - const handle1 = manager.getSystemVatHandle('sv0' as SystemVatId); - const handle2 = manager.getSystemVatHandle('sv1' as SystemVatId); - - expect(handle1?.systemVatId).toBe('sv0'); - expect(handle2?.systemVatId).toBe('sv1'); - }); - }); -}); diff --git a/packages/ocap-kernel/src/vats/SystemSubclusterManager.ts b/packages/ocap-kernel/src/vats/SystemSubclusterManager.ts deleted file mode 100644 index ed52b65f5..000000000 --- a/packages/ocap-kernel/src/vats/SystemSubclusterManager.ts +++ /dev/null @@ -1,310 +0,0 @@ -import type { Logger } from '@metamask/logger'; - -import { makeKernelFacet } from '../kernel-facet.ts'; -import type { KernelFacetDependencies } from '../kernel-facet.ts'; -import type { KernelQueue } from '../KernelQueue.ts'; -import { SystemVatHandle } from './SystemVatHandle.ts'; -import { kser, kslot } from '../liveslots/kernel-marshal.ts'; -import type { SlotValue } from '../liveslots/kernel-marshal.ts'; -import type { KernelStore } from '../store/index.ts'; -import type { - SystemVatId, - SystemSubclusterId, - KernelSystemSubclusterConfig, - KRef, -} from '../types.ts'; -import { ROOT_OBJECT_VREF } from '../types.ts'; - -/** - * Result of preparing a system subcluster. - */ -export type SystemSubclusterPrepareResult = { - /** The ID of the prepared system subcluster. */ - systemSubclusterId: SystemSubclusterId; - /** Map of vat names to their system vat IDs. */ - vatIds: Record; -}; - -type SystemSubclusterManagerOptions = { - kernelStore: KernelStore; - kernelQueue: KernelQueue; - kernelFacetDeps: KernelFacetDependencies; - registerKernelService: (name: string, service: object) => { kref: string }; - logger: Logger; -}; - -/** - * Internal record for a connected system subcluster. - */ -type SystemSubclusterRecord = { - id: SystemSubclusterId; - config: KernelSystemSubclusterConfig; - vatIds: Record; - handles: Map; -}; - -/** - * Manages system subclusters - subclusters whose vats run without compartment - * isolation directly in the runtime process. - * - * System vats: - * - Are created by the runtime (not the kernel) - * - Connect to the kernel via transports - * - Receive a kernel facet in the bootstrap message - * - Don't participate in kernel persistence machinery - */ -export class SystemSubclusterManager { - /** Storage holding the kernel's persistent state */ - readonly #kernelStore: KernelStore; - - /** The kernel's run queue */ - readonly #kernelQueue: KernelQueue; - - /** Dependencies for creating kernel facet services */ - readonly #kernelFacetDeps: KernelFacetDependencies; - - /** Logger for outputting messages to the console */ - readonly #logger: Logger; - - /** Counter for allocating system vat IDs */ - #nextSystemVatId: number = 0; - - /** Counter for allocating system subcluster IDs */ - #nextSystemSubclusterId: number = 0; - - /** Active system subclusters */ - readonly #subclusters: Map = - new Map(); - - /** Singleton kernel facet (created lazily, kept alive for GC purposes) */ - - #kernelFacet: object | null = null; - - /** Kref of the singleton kernel facet */ - #kernelFacetKref: KRef | null = null; - - /** Function to register a kernel service */ - readonly #registerKernelService: ( - name: string, - service: object, - ) => { kref: string }; - - /** - * Creates a new SystemSubclusterManager instance. - * - * @param options - Constructor options. - * @param options.kernelStore - The kernel's persistent state store. - * @param options.kernelQueue - The kernel's message queue. - * @param options.kernelFacetDeps - Dependencies for the kernel facet service. - * @param options.registerKernelService - Function to register kernel services. - * @param options.logger - Logger instance for debugging and diagnostics. - */ - constructor({ - kernelStore, - kernelQueue, - kernelFacetDeps, - registerKernelService, - logger, - }: SystemSubclusterManagerOptions) { - this.#kernelStore = kernelStore; - this.#kernelQueue = kernelQueue; - this.#kernelFacetDeps = kernelFacetDeps; - this.#registerKernelService = registerKernelService; - this.#logger = logger; - harden(this); - } - - /** - * Allocate a new system vat ID. - * - * @returns A new system vat ID. - */ - #allocateSystemVatId(): SystemVatId { - const id: SystemVatId = `sv${this.#nextSystemVatId}`; - this.#nextSystemVatId += 1; - return id; - } - - /** - * Allocate a new system subcluster ID. - * - * @returns A new system subcluster ID. - */ - #allocateSystemSubclusterId(): SystemSubclusterId { - const id: SystemSubclusterId = `ss${this.#nextSystemSubclusterId}`; - this.#nextSystemSubclusterId += 1; - return id; - } - - /** - * Get the singleton kernel facet kref, creating and registering it if necessary. - * - * @returns The kref for the kernel facet. - */ - #getKernelFacetKref(): KRef { - if (!this.#kernelFacetKref) { - this.#kernelFacet = makeKernelFacet(this.#kernelFacetDeps); - // Register the kernel facet as a kernel service so it can receive messages - const { kref } = this.#registerKernelService( - 'kernelFacet', - this.#kernelFacet, - ); - this.#kernelFacetKref = kref; - } - return this.#kernelFacetKref; - } - - /** - * Prepare a system subcluster using provided transports. - * - * The runtime creates supervisors externally and provides transports for - * communication. This method sets up the kernel side and returns immediately. - * The actual connection and bootstrap happen asynchronously when the - * supervisor-side initiates connection via the transport's `awaitConnection()`. - * - * The kernel is passive - it sets up to receive connections and waits for - * the supervisor to push the connection. This push-based model supports - * both same-process and cross-process transports. - * - * @param config - Configuration for the system subcluster with transports. - * @returns The prepare result with IDs allocated for the subcluster. - */ - prepareSystemSubcluster( - config: KernelSystemSubclusterConfig, - ): SystemSubclusterPrepareResult { - const bootstrapTransport = config.vatTransports.find( - (vt) => vt.name === config.bootstrap, - ); - if (!bootstrapTransport) { - throw Error(`invalid bootstrap vat name ${config.bootstrap}`); - } - - const systemSubclusterId = this.#allocateSystemSubclusterId(); - const vatIds: Record = {}; - const handles = new Map(); - const rootKrefs: Record = {}; - - // Set up all system vats via their transports (kernel side only) - for (const vatTransport of config.vatTransports) { - const { name: vatName, transport } = vatTransport; - const systemVatId = this.#allocateSystemVatId(); - vatIds[vatName] = systemVatId; - - // Initialize the endpoint in the kernel store - this.#kernelStore.initEndpoint(systemVatId); - - // Create the system vat handle (kernel-side) with the transport's deliver function - const handle = new SystemVatHandle({ - systemVatId, - kernelStore: this.#kernelStore, - kernelQueue: this.#kernelQueue, - deliver: transport.deliver, - logger: this.#logger.subLogger({ tags: [systemVatId] }), - }); - handles.set(systemVatId, handle); - - // Wire the syscall handler to the transport - transport.setSyscallHandler(handle.getSyscallHandler()); - - // Get or create the root kref (the root object is exported at o+0) - const existingRootKref = this.#kernelStore.erefToKref( - systemVatId, - ROOT_OBJECT_VREF, - ); - if (existingRootKref) { - rootKrefs[vatName] = existingRootKref; - } else { - // Initialize the root object in the clist - const newRootKref = this.#kernelStore.initKernelObject(systemVatId); - this.#kernelStore.addCListEntry( - systemVatId, - newRootKref, - ROOT_OBJECT_VREF, - ); - rootKrefs[vatName] = newRootKref; - } - } - - // Get the singleton kernel facet kref - const kernelFacetKref = this.#getKernelFacetKref(); - - // Store the subcluster record - const record: SystemSubclusterRecord = { - id: systemSubclusterId, - config, - vatIds, - handles, - }; - this.#subclusters.set(systemSubclusterId, record); - - // Build roots object for bootstrap - const roots: Record = {}; - for (const [vatName, kref] of Object.entries(rootKrefs)) { - roots[vatName] = kslot(kref, 'vatRoot'); - } - - // Build services object - always include kernelFacet - const services: Record = { - kernelFacet: kslot(kernelFacetKref, 'KernelFacet'), - }; - if (config.services) { - for (const serviceName of config.services) { - const serviceKref = this.#kernelStore.kv.get( - `kernelService.${serviceName}`, - ); - if (serviceKref) { - services[serviceName] = kslot(serviceKref); - } else { - this.#logger.warn(`Kernel service '${serviceName}' not found`); - } - } - } - - // Get bootstrap target for the bootstrap message - const bootstrapTarget = rootKrefs[config.bootstrap] as KRef; - const bootstrapArgs = [roots, services]; - - // Set up to send bootstrap after ALL vats in the subcluster are connected. - // We wait for all transports' awaitConnection() to resolve before sending - // the bootstrap message to ensure all vats are ready. - const connectionPromises = config.vatTransports.map(async (vt) => - vt.transport.awaitConnection(), - ); - Promise.all(connectionPromises) - .then(() => { - // All supervisors have connected. Now send the bootstrap message. - // We use enqueueSend (fire-and-forget) because this runs asynchronously - // after the kernel queue has started. - this.#kernelQueue.enqueueSend(bootstrapTarget, { - methargs: kser(['bootstrap', bootstrapArgs]), - }); - return undefined; - }) - .catch((error) => { - this.#logger.error(`Failed to connect system subcluster:`, error); - }); - - // Return immediately - connection happens later when supervisor calls connect() - return { - systemSubclusterId, - vatIds, - }; - } - - /** - * Get a system vat handle by ID. - * - * @param systemVatId - The system vat ID. - * @returns The system vat handle or undefined if not found. - */ - getSystemVatHandle(systemVatId: SystemVatId): SystemVatHandle | undefined { - for (const record of this.#subclusters.values()) { - const handle = record.handles.get(systemVatId); - if (handle) { - return handle; - } - } - return undefined; - } -} -harden(SystemSubclusterManager); diff --git a/packages/ocap-kernel/src/vats/SystemVatHandle.ts b/packages/ocap-kernel/src/vats/SystemVatHandle.ts index 5a01be905..d593f4c89 100644 --- a/packages/ocap-kernel/src/vats/SystemVatHandle.ts +++ b/packages/ocap-kernel/src/vats/SystemVatHandle.ts @@ -57,7 +57,7 @@ export class SystemVatHandle implements EndpointHandle { /** The system vat's syscall handler */ readonly #vatSyscall: VatSyscall; - /** Callback to deliver messages to the system vat supervisor */ + /** Callback to deliver messages to the system vat */ readonly #deliver: SystemVatDeliverFn; /** Flag indicating if this handle is active */ @@ -114,7 +114,6 @@ export class SystemVatHandle implements EndpointHandle { * @returns The crank results. */ async deliverMessage(target: VRef, message: Message): Promise { - // Convert our Message type to SwingSet's Message type for delivery const swingSetMessage: SwingSetMessage = { methargs: message.methargs, result: message.result ?? null, @@ -122,7 +121,7 @@ export class SystemVatHandle implements EndpointHandle { const deliveryError = await this.#deliver( harden(['message', target, swingSetMessage]), ); - return this.#getDeliveryCrankResults(deliveryError); + return this.#getCrankResults(deliveryError); } /** @@ -133,7 +132,7 @@ export class SystemVatHandle implements EndpointHandle { */ async deliverNotify(resolutions: VatOneResolution[]): Promise { const deliveryError = await this.#deliver(harden(['notify', resolutions])); - return this.#getDeliveryCrankResults(deliveryError); + return this.#getCrankResults(deliveryError); } /** @@ -144,7 +143,7 @@ export class SystemVatHandle implements EndpointHandle { */ async deliverDropExports(vrefs: VRef[]): Promise { const deliveryError = await this.#deliver(harden(['dropExports', vrefs])); - return this.#getDeliveryCrankResults(deliveryError); + return this.#getCrankResults(deliveryError); } /** @@ -155,7 +154,7 @@ export class SystemVatHandle implements EndpointHandle { */ async deliverRetireExports(vrefs: VRef[]): Promise { const deliveryError = await this.#deliver(harden(['retireExports', vrefs])); - return this.#getDeliveryCrankResults(deliveryError); + return this.#getCrankResults(deliveryError); } /** @@ -166,7 +165,7 @@ export class SystemVatHandle implements EndpointHandle { */ async deliverRetireImports(vrefs: VRef[]): Promise { const deliveryError = await this.#deliver(harden(['retireImports', vrefs])); - return this.#getDeliveryCrankResults(deliveryError); + return this.#getCrankResults(deliveryError); } /** @@ -176,16 +175,16 @@ export class SystemVatHandle implements EndpointHandle { */ async deliverBringOutYourDead(): Promise { const deliveryError = await this.#deliver(harden(['bringOutYourDead'])); - return this.#getDeliveryCrankResults(deliveryError); + return this.#getCrankResults(deliveryError); } /** - * Get the crank outcome for a delivery. + * Get the crank results after a delivery. * * @param deliveryError - The error from delivery, if any. - * @returns The crank outcome. + * @returns The crank results. */ - #getDeliveryCrankResults(deliveryError: string | null): CrankResults { + #getCrankResults(deliveryError: string | null): CrankResults { const results: CrankResults = { didDelivery: this.systemVatId, }; diff --git a/packages/ocap-kernel/src/vats/SystemVatManager.test.ts b/packages/ocap-kernel/src/vats/SystemVatManager.test.ts new file mode 100644 index 000000000..89cc8fb38 --- /dev/null +++ b/packages/ocap-kernel/src/vats/SystemVatManager.test.ts @@ -0,0 +1,313 @@ +import { Logger } from '@metamask/logger'; +import { describe, it, expect, beforeEach, vi } from 'vitest'; + +import type { KernelFacetDependencies } from '../kernel-facet.ts'; +import type { KernelQueue } from '../KernelQueue.ts'; +import type { KernelStore } from '../store/index.ts'; +import type { StaticSystemVatConfig, SystemVatTransport } from '../types.ts'; +import { SystemVatManager } from './SystemVatManager.ts'; + +describe('SystemVatManager', () => { + let mockKernelStore: KernelStore; + let mockKernelQueue: KernelQueue; + let mockKernelFacetDeps: KernelFacetDependencies; + let manager: SystemVatManager; + let mockTransport: SystemVatTransport; + + const makeTransport = (): SystemVatTransport => { + const connectionPromise = { + resolve: vi.fn(), + promise: Promise.resolve(), + }; + return { + deliver: vi.fn().mockResolvedValue(null), + setSyscallHandler: vi.fn(), + awaitConnection: vi.fn().mockReturnValue(connectionPromise.promise), + }; + }; + + beforeEach(() => { + vi.clearAllMocks(); + + mockKernelStore = { + initEndpoint: vi.fn(), + erefToKref: vi.fn().mockReturnValue(null), + initKernelObject: vi.fn().mockReturnValue('ko1'), + addCListEntry: vi.fn(), + kv: { + get: vi.fn().mockReturnValue(undefined), + set: vi.fn(), + }, + } as unknown as KernelStore; + + mockKernelQueue = { + enqueueSend: vi.fn(), + } as unknown as KernelQueue; + + mockKernelFacetDeps = { + launchSubcluster: vi.fn(), + terminateSubcluster: vi.fn(), + reloadSubcluster: vi.fn(), + getSubcluster: vi.fn(), + getSubclusters: vi.fn(), + getStatus: vi.fn(), + logger: new Logger('test'), + }; + + mockTransport = makeTransport(); + + manager = new SystemVatManager({ + kernelStore: mockKernelStore, + kernelQueue: mockKernelQueue, + kernelFacetDeps: mockKernelFacetDeps, + registerKernelService: vi.fn().mockReturnValue({ kref: 'ko0' }), + logger: new Logger('test'), + }); + }); + + describe('prepareStaticSystemVat', () => { + it('allocates system vat ID starting from sv0', () => { + const config: StaticSystemVatConfig = { + name: 'testVat', + transport: mockTransport, + }; + + const result = manager.prepareStaticSystemVat(config); + + expect(result.systemVatId).toBe('sv0'); + }); + + it('allocates sequential system vat IDs', () => { + const config1: StaticSystemVatConfig = { + name: 'vat1', + transport: makeTransport(), + }; + const config2: StaticSystemVatConfig = { + name: 'vat2', + transport: makeTransport(), + }; + + const result1 = manager.prepareStaticSystemVat(config1); + const result2 = manager.prepareStaticSystemVat(config2); + + expect(result1.systemVatId).toBe('sv0'); + expect(result2.systemVatId).toBe('sv1'); + }); + + it('initializes endpoint in kernel store', () => { + const config: StaticSystemVatConfig = { + name: 'testVat', + transport: mockTransport, + }; + + manager.prepareStaticSystemVat(config); + + expect(mockKernelStore.initEndpoint).toHaveBeenCalledWith('sv0'); + }); + + it('creates root kernel object if not exists', () => { + const config: StaticSystemVatConfig = { + name: 'testVat', + transport: mockTransport, + }; + + manager.prepareStaticSystemVat(config); + + expect(mockKernelStore.initKernelObject).toHaveBeenCalledWith('sv0'); + expect(mockKernelStore.addCListEntry).toHaveBeenCalledWith( + 'sv0', + 'ko1', + 'o+0', + ); + }); + + it('uses existing root kref if already exists', () => { + (mockKernelStore.erefToKref as ReturnType).mockReturnValue( + 'ko99', + ); + const config: StaticSystemVatConfig = { + name: 'testVat', + transport: mockTransport, + }; + + manager.prepareStaticSystemVat(config); + + expect(mockKernelStore.initKernelObject).not.toHaveBeenCalled(); + expect(mockKernelStore.addCListEntry).not.toHaveBeenCalled(); + }); + + it('sets syscall handler on transport', () => { + const config: StaticSystemVatConfig = { + name: 'testVat', + transport: mockTransport, + }; + + manager.prepareStaticSystemVat(config); + + expect(mockTransport.setSyscallHandler).toHaveBeenCalled(); + }); + + it('waits for connection before sending bootstrap', async () => { + let resolveConnection: () => void; + const connectionPromise = new Promise((resolve) => { + resolveConnection = resolve; + }); + const transport: SystemVatTransport = { + deliver: vi.fn().mockResolvedValue(null), + setSyscallHandler: vi.fn(), + awaitConnection: vi.fn().mockReturnValue(connectionPromise), + }; + + const config: StaticSystemVatConfig = { + name: 'testVat', + transport, + }; + + manager.prepareStaticSystemVat(config); + + // Bootstrap should not be sent yet + expect(mockKernelQueue.enqueueSend).not.toHaveBeenCalled(); + + // Resolve connection + resolveConnection!(); + await connectionPromise; + + // Give time for async handler to run + await new Promise((resolve) => setTimeout(resolve, 10)); + + // Now bootstrap should be sent + expect(mockKernelQueue.enqueueSend).toHaveBeenCalled(); + }); + }); + + describe('getSystemVatHandle', () => { + it('returns handle for prepared system vat', () => { + const config: StaticSystemVatConfig = { + name: 'testVat', + transport: mockTransport, + }; + + manager.prepareStaticSystemVat(config); + const handle = manager.getSystemVatHandle('sv0'); + + expect(handle).toBeDefined(); + }); + + it('returns undefined for non-existent system vat', () => { + const handle = manager.getSystemVatHandle('sv999'); + + expect(handle).toBeUndefined(); + }); + + it('returns correct handle for multiple system vats', () => { + const config1: StaticSystemVatConfig = { + name: 'vat1', + transport: makeTransport(), + }; + const config2: StaticSystemVatConfig = { + name: 'vat2', + transport: makeTransport(), + }; + + manager.prepareStaticSystemVat(config1); + manager.prepareStaticSystemVat(config2); + + const handle1 = manager.getSystemVatHandle('sv0'); + const handle2 = manager.getSystemVatHandle('sv1'); + + expect(handle1).toBeDefined(); + expect(handle2).toBeDefined(); + expect(handle1).not.toBe(handle2); + }); + }); + + describe('disconnectSystemVat', () => { + it('removes system vat from tracking', async () => { + const config: StaticSystemVatConfig = { + name: 'testVat', + transport: mockTransport, + }; + + manager.prepareStaticSystemVat(config); + expect(manager.getSystemVatHandle('sv0')).toBeDefined(); + + await manager.disconnectSystemVat('sv0'); + expect(manager.getSystemVatHandle('sv0')).toBeUndefined(); + }); + + it('handles disconnect of non-existent vat gracefully', async () => { + // Should not throw + const result = await manager.disconnectSystemVat('sv999'); + expect(result).toBeUndefined(); + }); + }); + + describe('registerDynamicSystemVat', () => { + it('allocates system vat ID', async () => { + const connectionKit = { + resolve: vi.fn(), + promise: Promise.resolve(), + }; + const transport: SystemVatTransport = { + deliver: vi.fn().mockResolvedValue(null), + setSyscallHandler: vi.fn(), + awaitConnection: vi.fn().mockReturnValue(connectionKit.promise), + }; + + const result = await manager.registerDynamicSystemVat({ + name: 'dynamicVat', + transport, + }); + + expect(result.systemVatId).toBe('sv0'); + }); + + it('returns root kref and disconnect function', async () => { + const transport: SystemVatTransport = { + deliver: vi.fn().mockResolvedValue(null), + setSyscallHandler: vi.fn(), + awaitConnection: vi.fn().mockResolvedValue(undefined), + }; + + const result = await manager.registerDynamicSystemVat({ + name: 'dynamicVat', + transport, + }); + + expect(result.rootKref).toBe('ko1'); + expect(typeof result.disconnect).toBe('function'); + }); + + it('sends bootstrap after awaiting connection', async () => { + const transport: SystemVatTransport = { + deliver: vi.fn().mockResolvedValue(null), + setSyscallHandler: vi.fn(), + awaitConnection: vi.fn().mockResolvedValue(undefined), + }; + + await manager.registerDynamicSystemVat({ + name: 'dynamicVat', + transport, + }); + + expect(mockKernelQueue.enqueueSend).toHaveBeenCalled(); + }); + + it('disconnect function removes vat', async () => { + const transport: SystemVatTransport = { + deliver: vi.fn().mockResolvedValue(null), + setSyscallHandler: vi.fn(), + awaitConnection: vi.fn().mockResolvedValue(undefined), + }; + + const result = await manager.registerDynamicSystemVat({ + name: 'dynamicVat', + transport, + }); + + expect(manager.getSystemVatHandle(result.systemVatId)).toBeDefined(); + await result.disconnect(); + expect(manager.getSystemVatHandle(result.systemVatId)).toBeUndefined(); + }); + }); +}); diff --git a/packages/ocap-kernel/src/vats/SystemVatManager.ts b/packages/ocap-kernel/src/vats/SystemVatManager.ts new file mode 100644 index 000000000..3877c43d7 --- /dev/null +++ b/packages/ocap-kernel/src/vats/SystemVatManager.ts @@ -0,0 +1,359 @@ +import type { Logger } from '@metamask/logger'; + +import { makeKernelFacet } from '../kernel-facet.ts'; +import type { KernelFacetDependencies } from '../kernel-facet.ts'; +import type { KernelQueue } from '../KernelQueue.ts'; +import { kser, kslot } from '../liveslots/kernel-marshal.ts'; +import type { SlotValue } from '../liveslots/kernel-marshal.ts'; +import type { KernelStore } from '../store/index.ts'; +import type { + SystemVatId, + StaticSystemVatConfig, + DynamicSystemVatConfig, + SystemVatRegistrationResult, + KRef, +} from '../types.ts'; +import { ROOT_OBJECT_VREF } from '../types.ts'; +import { SystemVatHandle } from './SystemVatHandle.ts'; + +/** + * Result of preparing a static system vat. + */ +export type StaticSystemVatPrepareResult = { + /** The system vat ID. */ + systemVatId: SystemVatId; +}; + +type SystemVatManagerOptions = { + kernelStore: KernelStore; + kernelQueue: KernelQueue; + kernelFacetDeps: KernelFacetDependencies; + registerKernelService: (name: string, service: object) => { kref: string }; + logger: Logger; +}; + +/** + * Internal record for a system vat. + */ +type SystemVatRecord = { + id: SystemVatId; + name: string; + handle: SystemVatHandle; + rootKref: KRef; + isStatic: boolean; +}; + +/** + * Manages system vats - vats that run without compartment isolation + * directly in the runtime process. + * + * System vats: + * - Are created by the runtime (not the kernel) + * - Connect to the kernel via transports + * - Receive a kernel facet in the bootstrap message + * - Don't participate in kernel persistence machinery + * + * Supports both: + * - Static system vats: Declared at kernel construction time + * - Dynamic system vats: Registered at runtime via kernel facet + */ +export class SystemVatManager { + /** Storage holding the kernel's persistent state */ + readonly #kernelStore: KernelStore; + + /** The kernel's run queue */ + readonly #kernelQueue: KernelQueue; + + /** Dependencies for creating kernel facet services */ + readonly #kernelFacetDeps: KernelFacetDependencies; + + /** Logger for outputting messages to the console */ + readonly #logger: Logger; + + /** Counter for allocating system vat IDs */ + #nextSystemVatId: number = 0; + + /** Active system vats indexed by ID */ + readonly #systemVats: Map = new Map(); + + /** Singleton kernel facet (created lazily, kept alive for GC purposes) */ + #kernelFacet: object | null = null; + + /** Kref of the singleton kernel facet */ + #kernelFacetKref: KRef | null = null; + + /** Function to register a kernel service */ + readonly #registerKernelService: ( + name: string, + service: object, + ) => { kref: string }; + + /** + * Creates a new SystemVatManager instance. + * + * @param options - Constructor options. + * @param options.kernelStore - The kernel's persistent state store. + * @param options.kernelQueue - The kernel's message queue. + * @param options.kernelFacetDeps - Dependencies for the kernel facet service. + * @param options.registerKernelService - Function to register kernel services. + * @param options.logger - Logger instance for debugging and diagnostics. + */ + constructor({ + kernelStore, + kernelQueue, + kernelFacetDeps, + registerKernelService, + logger, + }: SystemVatManagerOptions) { + this.#kernelStore = kernelStore; + this.#kernelQueue = kernelQueue; + this.#kernelFacetDeps = kernelFacetDeps; + this.#registerKernelService = registerKernelService; + this.#logger = logger; + harden(this); + } + + /** + * Allocate a new system vat ID. + * + * @returns A new system vat ID. + */ + #allocateSystemVatId(): SystemVatId { + const id: SystemVatId = `sv${this.#nextSystemVatId}`; + this.#nextSystemVatId += 1; + return id; + } + + /** + * Get the singleton kernel facet kref, creating and registering it if necessary. + * + * @returns The kref for the kernel facet. + */ + #getKernelFacetKref(): KRef { + if (!this.#kernelFacetKref) { + // Pass `this` as systemVatManager for dynamic registration + const depsWithManager = { + ...this.#kernelFacetDeps, + systemVatManager: this, + }; + this.#kernelFacet = makeKernelFacet(depsWithManager); + // Register the kernel facet as a kernel service so it can receive messages + const { kref } = this.#registerKernelService( + 'kernelFacet', + this.#kernelFacet, + ); + this.#kernelFacetKref = kref; + } + return this.#kernelFacetKref; + } + + /** + * Set up a system vat from a transport config. + * + * @param config - The static system vat config with transport. + * @param isStatic - Whether this is a static (at kernel init) or dynamic vat. + * @returns The system vat ID and root kref. + */ + #setupSystemVat( + config: StaticSystemVatConfig, + isStatic: boolean, + ): { systemVatId: SystemVatId; rootKref: KRef } { + const { name, transport } = config; + const systemVatId = this.#allocateSystemVatId(); + + // Initialize the endpoint in the kernel store + this.#kernelStore.initEndpoint(systemVatId); + + // Create the system vat handle (kernel-side) with the transport's deliver function + const handle = new SystemVatHandle({ + systemVatId, + kernelStore: this.#kernelStore, + kernelQueue: this.#kernelQueue, + deliver: transport.deliver, + logger: this.#logger.subLogger({ tags: [systemVatId] }), + }); + + // Wire the syscall handler to the transport + transport.setSyscallHandler(handle.getSyscallHandler()); + + // Get or create the root kref (the root object is exported at o+0) + let rootKref = this.#kernelStore.erefToKref(systemVatId, ROOT_OBJECT_VREF); + if (!rootKref) { + // Initialize the root object in the clist + rootKref = this.#kernelStore.initKernelObject(systemVatId); + this.#kernelStore.addCListEntry(systemVatId, rootKref, ROOT_OBJECT_VREF); + } + + // Store the vat record + const record: SystemVatRecord = { + id: systemVatId, + name, + handle, + rootKref, + isStatic, + }; + this.#systemVats.set(systemVatId, record); + + return { systemVatId, rootKref }; + } + + /** + * Prepare a static system vat using a provided transport. + * + * The runtime creates the supervisor externally and provides the transport for + * communication. This method sets up the kernel side and returns immediately. + * The actual connection and bootstrap happen asynchronously when the + * supervisor-side initiates connection via the transport's `awaitConnection()`. + * + * @param config - Configuration for the static system vat with transport. + * @returns The prepare result with the system vat ID. + */ + prepareStaticSystemVat( + config: StaticSystemVatConfig, + ): StaticSystemVatPrepareResult { + const { systemVatId, rootKref } = this.#setupSystemVat(config, true); + + // Get the singleton kernel facet kref + const kernelFacetKref = this.#getKernelFacetKref(); + + // Build roots object for bootstrap (just this vat's root) + const roots: Record = { + [config.name]: kslot(rootKref, 'vatRoot'), + }; + + // Build services object - always include kernelFacet + const services: Record = { + kernelFacet: kslot(kernelFacetKref, 'KernelFacet'), + }; + if (config.services) { + for (const serviceName of config.services) { + const serviceKref = this.#kernelStore.kv.get( + `kernelService.${serviceName}`, + ); + if (serviceKref) { + services[serviceName] = kslot(serviceKref); + } else { + this.#logger.warn(`Kernel service '${serviceName}' not found`); + } + } + } + + // Set up to send bootstrap after the vat is connected. + config.transport + .awaitConnection() + .then(() => { + // Supervisor has connected. Now send the bootstrap message. + this.#kernelQueue.enqueueSend(rootKref, { + methargs: kser(['bootstrap', [roots, services]]), + }); + return undefined; + }) + .catch((error) => { + this.#logger.error( + `Failed to connect system vat ${config.name}:`, + error, + ); + }); + + return { systemVatId }; + } + + /** + * Register a dynamic system vat at runtime. + * + * Called via the kernel facet to register new system vats after the kernel + * is already running (e.g., UI instances in an extension). + * + * @param config - Configuration for the dynamic system vat. + * @returns A promise for the registration result with system vat ID and disconnect function. + */ + async registerDynamicSystemVat( + config: DynamicSystemVatConfig, + ): Promise { + const staticConfig: StaticSystemVatConfig = { + name: config.name, + transport: config.transport, + }; + if (config.services !== undefined) { + staticConfig.services = config.services; + } + const { systemVatId, rootKref } = this.#setupSystemVat(staticConfig, false); + + // Get the singleton kernel facet kref + const kernelFacetKref = this.#getKernelFacetKref(); + + // Build roots object for bootstrap + const roots: Record = { + [config.name]: kslot(rootKref, 'vatRoot'), + }; + + // Build services object - always include kernelFacet + const services: Record = { + kernelFacet: kslot(kernelFacetKref, 'KernelFacet'), + }; + if (config.services) { + for (const serviceName of config.services) { + const serviceKref = this.#kernelStore.kv.get( + `kernelService.${serviceName}`, + ); + if (serviceKref) { + services[serviceName] = kslot(serviceKref); + } else { + this.#logger.warn(`Kernel service '${serviceName}' not found`); + } + } + } + + // Wait for connection then send bootstrap + await config.transport.awaitConnection(); + this.#kernelQueue.enqueueSend(rootKref, { + methargs: kser(['bootstrap', [roots, services]]), + }); + + // Return disconnect function for cleanup + const disconnect = async (): Promise => { + await this.disconnectSystemVat(systemVatId); + }; + + return { + systemVatId, + rootKref, + disconnect, + }; + } + + /** + * Disconnect and clean up a system vat. + * + * @param systemVatId - The system vat ID to disconnect. + */ + async disconnectSystemVat(systemVatId: SystemVatId): Promise { + const record = this.#systemVats.get(systemVatId); + if (!record) { + this.#logger.warn(`System vat ${systemVatId} not found for disconnect`); + return; + } + + // TODO: Proper cleanup: + // - Reject pending promises where this vat is the decider + // - Retire imports held by this vat + // - Notify other vats that references to this vat are broken + + // Remove the vat record + this.#systemVats.delete(systemVatId); + + this.#logger.log(`Disconnected system vat ${systemVatId} (${record.name})`); + } + + /** + * Get a system vat handle by ID. + * + * @param systemVatId - The system vat ID. + * @returns The system vat handle or undefined if not found. + */ + getSystemVatHandle(systemVatId: SystemVatId): SystemVatHandle | undefined { + const record = this.#systemVats.get(systemVatId); + return record?.handle; + } +} +harden(SystemVatManager); diff --git a/packages/ocap-kernel/src/vats/SystemVatSupervisor.test.ts b/packages/ocap-kernel/src/vats/SystemVatSupervisor.test.ts index 006315cbb..47ad6f042 100644 --- a/packages/ocap-kernel/src/vats/SystemVatSupervisor.test.ts +++ b/packages/ocap-kernel/src/vats/SystemVatSupervisor.test.ts @@ -45,7 +45,7 @@ describe('SystemVatSupervisor', () => { id: systemVatId, buildRootObject, vatPowers, - parameters: undefined, + parameters: {}, executeSyscall, logger, }); @@ -60,7 +60,7 @@ describe('SystemVatSupervisor', () => { id: systemVatId, buildRootObject, vatPowers, - parameters: undefined, + parameters: {}, executeSyscall, logger, }); @@ -76,7 +76,7 @@ describe('SystemVatSupervisor', () => { id: systemVatId, buildRootObject, vatPowers: { customPower: 'custom' }, - parameters: undefined, + parameters: {}, executeSyscall, logger, }); @@ -100,7 +100,7 @@ describe('SystemVatSupervisor', () => { id: systemVatId, buildRootObject, vatPowers, - parameters: undefined, + parameters: {}, executeSyscall, logger, }); @@ -119,7 +119,7 @@ describe('SystemVatSupervisor', () => { id: systemVatId, buildRootObject, vatPowers, - parameters: undefined, + parameters: {}, executeSyscall, logger, }); @@ -134,7 +134,7 @@ describe('SystemVatSupervisor', () => { id: systemVatId, buildRootObject, vatPowers, - parameters: undefined, + parameters: {}, executeSyscall, logger, }); @@ -154,7 +154,7 @@ describe('SystemVatSupervisor', () => { id: systemVatId, buildRootObject, vatPowers, - parameters: undefined, + parameters: {}, executeSyscall, logger, }); @@ -173,7 +173,7 @@ describe('SystemVatSupervisor', () => { id: systemVatId, buildRootObject, vatPowers, - parameters: undefined, + parameters: {}, executeSyscall, logger, }); @@ -195,7 +195,7 @@ describe('SystemVatSupervisor', () => { id: systemVatId, buildRootObject, vatPowers, - parameters: undefined, + parameters: {}, executeSyscall, logger, }); @@ -223,7 +223,7 @@ describe('SystemVatSupervisor', () => { id: systemVatId, buildRootObject, vatPowers, - parameters: undefined, + parameters: {}, executeSyscall, logger, }); @@ -253,7 +253,7 @@ describe('SystemVatSupervisor', () => { id: systemVatId, buildRootObject, vatPowers, - parameters: undefined, + parameters: {}, executeSyscall: failingExecuteSyscall, logger, }); @@ -274,7 +274,7 @@ describe('SystemVatSupervisor', () => { id: systemVatId, buildRootObject, vatPowers, - parameters: undefined, + parameters: {}, executeSyscall, logger, }); @@ -298,7 +298,7 @@ describe('SystemVatSupervisor', () => { id: systemVatId, buildRootObject, vatPowers, - parameters: undefined, + parameters: {}, executeSyscall, logger, }); @@ -324,7 +324,7 @@ describe('SystemVatSupervisor', () => { id: systemVatId, buildRootObject, vatPowers, - parameters: undefined, + parameters: {}, executeSyscall, logger, }); diff --git a/packages/ocap-kernel/src/vats/VatHandle.ts b/packages/ocap-kernel/src/vats/VatHandle.ts index a95e03a3a..127ad317b 100644 --- a/packages/ocap-kernel/src/vats/VatHandle.ts +++ b/packages/ocap-kernel/src/vats/VatHandle.ts @@ -217,7 +217,7 @@ export class VatHandle implements EndpointHandle { method: 'deliver', params: ['message', target, message], }); - return this.#getDeliveryCrankResults(); + return this.#getCrankResults(); } /** @@ -231,7 +231,7 @@ export class VatHandle implements EndpointHandle { method: 'deliver', params: ['notify', resolutions], }); - return this.#getDeliveryCrankResults(); + return this.#getCrankResults(); } /** @@ -245,7 +245,7 @@ export class VatHandle implements EndpointHandle { method: 'deliver', params: ['dropExports', vrefs], }); - return this.#getDeliveryCrankResults(); + return this.#getCrankResults(); } /** @@ -259,7 +259,7 @@ export class VatHandle implements EndpointHandle { method: 'deliver', params: ['retireExports', vrefs], }); - return this.#getDeliveryCrankResults(); + return this.#getCrankResults(); } /** @@ -273,7 +273,7 @@ export class VatHandle implements EndpointHandle { method: 'deliver', params: ['retireImports', vrefs], }); - return this.#getDeliveryCrankResults(); + return this.#getCrankResults(); } /** @@ -286,7 +286,7 @@ export class VatHandle implements EndpointHandle { method: 'deliver', params: ['bringOutYourDead'], }); - return this.#getDeliveryCrankResults(); + return this.#getCrankResults(); } /** @@ -342,11 +342,11 @@ export class VatHandle implements EndpointHandle { } /** - * Get the crank outcome for a given checkpoint result. + * Get the crank results after a delivery. * - * @returns The crank outcome. + * @returns The crank results. */ - async #getDeliveryCrankResults(): Promise { + #getCrankResults(): CrankResults { const results: CrankResults = { didDelivery: this.vatId, }; @@ -356,9 +356,6 @@ export class VatHandle implements EndpointHandle { if (this.#vatSyscall.illegalSyscall) { results.abort = true; const { info } = this.#vatSyscall.illegalSyscall; - // TODO: For now, vat errors both rewind changes and terminate the vat. - // Some day, they might rewind changes and retry the syscall. - // We should terminate the vat only after a certain # of failed retries. results.terminate = { vatId: this.vatId, reject: true, info }; } else if (this.#vatSyscall.deliveryError) { results.abort = true; @@ -366,7 +363,7 @@ export class VatHandle implements EndpointHandle { results.terminate = { vatId: this.vatId, reject: true, info }; } else if (this.#vatSyscall.vatRequestedTermination) { if (this.#vatSyscall.vatRequestedTermination.reject) { - results.abort = true; // vatPowers.exitWithFailure wants rewind + results.abort = true; } results.terminate = { vatId: this.vatId, diff --git a/packages/ocap-kernel/src/vats/index.ts b/packages/ocap-kernel/src/vats/index.ts index c0d7486c1..f374a9556 100644 --- a/packages/ocap-kernel/src/vats/index.ts +++ b/packages/ocap-kernel/src/vats/index.ts @@ -8,3 +8,5 @@ export type { SystemVatDeliverFn, SystemVatSyscallFn, } from './SystemVatHandle.ts'; +export { SystemVatManager } from './SystemVatManager.ts'; +export type { StaticSystemVatPrepareResult } from './SystemVatManager.ts'; diff --git a/packages/ocap-kernel/test/integration/system-vat.test.ts b/packages/ocap-kernel/test/integration/system-vat.test.ts index a95f831ca..bf58947b5 100644 --- a/packages/ocap-kernel/test/integration/system-vat.test.ts +++ b/packages/ocap-kernel/test/integration/system-vat.test.ts @@ -14,6 +14,7 @@ import type { SystemVatTransport, SystemVatSyscallHandler, SystemVatDeliverFn, + StaticSystemVatConfig, } from '../../src/types.ts'; import { SystemVatSupervisor } from '../../src/vats/SystemVatSupervisor.ts'; import { makeMapKernelDatabase } from '../storage.ts'; @@ -22,8 +23,8 @@ import { makeMapKernelDatabase } from '../storage.ts'; * Result of creating a test system vat. */ type TestSystemVatResult = { - /** Transport config for kernel. */ - transport: SystemVatTransport; + /** Config for kernel. */ + config: StaticSystemVatConfig; /** Call after Kernel.make() to initiate connection from supervisor side. */ connect: () => void; /** Promise that resolves to kernelFacet when bootstrap completes. */ @@ -37,15 +38,19 @@ type TestSystemVatResult = { * * @param options - Options for creating the transport. * @param options.logger - Logger instance. - * @returns The transport config, connect function, and kernelFacetPromise. + * @param options.name - Name for the system vat. + * @returns The config, connect function, and kernelFacetPromise. */ -function makeTestSystemVat(options: { logger: Logger }): TestSystemVatResult { - const { logger } = options; +function makeTestSystemVat(options: { + logger: Logger; + name?: string; +}): TestSystemVatResult { + const { logger, name = 'testVat' } = options; // Promise kit for kernel facet - resolves when bootstrap is called const kernelFacetKit = makePromiseKit(); - // Syscall handler - set by kernel during prepareSystemSubcluster() + // Syscall handler - set by kernel during prepareStaticSystemVat() let syscallHandler: SystemVatSyscallHandler | null = null; // Build root object that captures kernelFacet from bootstrap @@ -101,7 +106,12 @@ function makeTestSystemVat(options: { logger: Logger }): TestSystemVatResult { }); }; - return { transport, connect, kernelFacetPromise: kernelFacetKit.promise }; + const config: StaticSystemVatConfig = { + name, + transport, + }; + + return { config, connect, kernelFacetPromise: kernelFacetKit.promise }; } describe('system vat integration', { timeout: 30_000 }, () => { @@ -126,24 +136,12 @@ describe('system vat integration', { timeout: 30_000 }, () => { stopRemoteComms: vi.fn().mockResolvedValue(undefined), } as unknown as PlatformServices; - // Create kernel with system subcluster config + // Create kernel with system vat config const kernelDatabase = makeMapKernelDatabase(); kernel = await Kernel.make(mockPlatformServices, kernelDatabase, { resetStorage: true, logger: logger.subLogger({ tags: ['kernel'] }), - systemSubclusters: { - subclusters: [ - { - bootstrap: 'testVat', - vatTransports: [ - { - name: 'testVat', - transport: systemVat.transport, - }, - ], - }, - ], - }, + systemVats: { vats: [systemVat.config] }, }); // Supervisor-side initiates connection AFTER kernel exists From 974eb28bdbdd58acef38ad2361ebfb6370c01caf Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Thu, 29 Jan 2026 16:10:34 -0800 Subject: [PATCH 25/41] refactor(ocap-kernel): Unify system vat registration model - Replace prepareStaticSystemVat() and registerDynamicSystemVat() with single registerSystemVat() method - Change Kernel.make() option from systemVats to hostVat for single host vat - Remove StaticSystemVatConfig, DynamicSystemVatConfig, KernelSystemVatsConfig - Rename SystemVatConfig to SystemSubclusterVatConfig (for subclusters) - Implement proper disconnect cleanup: reject pending promises and delete endpoint state - Update all tests for unified API Co-Authored-By: Claude --- packages/nodejs/src/host-vat/index.ts | 12 +- packages/nodejs/test/e2e/system-vat.test.ts | 2 +- packages/ocap-kernel/src/Kernel.ts | 35 ++-- packages/ocap-kernel/src/index.ts | 4 +- packages/ocap-kernel/src/kernel-facet.ts | 20 +- packages/ocap-kernel/src/types.ts | 34 +--- .../src/vats/SystemVatManager.test.ts | 186 ++++++++---------- .../ocap-kernel/src/vats/SystemVatManager.ts | 130 +++--------- packages/ocap-kernel/src/vats/index.ts | 1 - .../test/integration/system-vat.test.ts | 10 +- 10 files changed, 165 insertions(+), 269 deletions(-) diff --git a/packages/nodejs/src/host-vat/index.ts b/packages/nodejs/src/host-vat/index.ts index 7872c1cf4..692fcedeb 100644 --- a/packages/nodejs/src/host-vat/index.ts +++ b/packages/nodejs/src/host-vat/index.ts @@ -4,7 +4,7 @@ import { Logger } from '@metamask/logger'; import type { SystemVatBuildRootObject, KernelFacet, - StaticSystemVatConfig, + SystemVatConfig, SystemVatTransport, SystemVatSyscallHandler, SystemVatDeliverFn, @@ -16,9 +16,9 @@ import { SystemVatSupervisor } from '@metamask/ocap-kernel/vats'; */ export type HostVatResult = { /** - * Configuration to pass to Kernel.make() systemVats option. + * Configuration to pass to Kernel.make() hostVat option. */ - config: StaticSystemVatConfig; + config: SystemVatConfig; /** * Call after Kernel.make() returns to initiate connection from supervisor side. @@ -45,7 +45,7 @@ export type HostVatResult = { * ```typescript * const hostVat = makeHostVat({ logger }); * const kernel = await Kernel.make(platformServices, db, { - * systemVats: { vats: [hostVat.config] }, + * hostVat: hostVat.config, * }); * hostVat.connect(); // Supervisor pushes connection to kernel * const kernelFacet = await hostVat.kernelFacetPromise; @@ -69,7 +69,7 @@ export function makeHostVat( // Promise kit for kernel facet - resolves when bootstrap is called const kernelFacetKit = makePromiseKit(); - // Syscall handler - set by kernel during prepareStaticSystemVat() + // Syscall handler - set by kernel during registerSystemVat() let syscallHandler: SystemVatSyscallHandler | null = null; // Build root object that captures kernelFacet from bootstrap @@ -136,7 +136,7 @@ export function makeHostVat( }; // Config for Kernel.make() - const config: StaticSystemVatConfig = { + const config: SystemVatConfig = { name: vatName, transport, }; diff --git a/packages/nodejs/test/e2e/system-vat.test.ts b/packages/nodejs/test/e2e/system-vat.test.ts index 1da7a3597..4066c9d98 100644 --- a/packages/nodejs/test/e2e/system-vat.test.ts +++ b/packages/nodejs/test/e2e/system-vat.test.ts @@ -47,7 +47,7 @@ describe('system vat e2e tests', { timeout: 30_000 }, () => { kernel = await Kernel.make(platformServices, kernelDatabase, { resetStorage: true, logger: logger.subLogger({ tags: ['kernel'] }), - systemVats: { vats: [hostVat.config] }, + hostVat: hostVat.config, }); // Supervisor-side initiates connection AFTER kernel exists diff --git a/packages/ocap-kernel/src/Kernel.ts b/packages/ocap-kernel/src/Kernel.ts index 0f7ee4d9a..ca6472476 100644 --- a/packages/ocap-kernel/src/Kernel.ts +++ b/packages/ocap-kernel/src/Kernel.ts @@ -19,7 +19,7 @@ import type { KRef, PlatformServices, ClusterConfig, - KernelSystemVatsConfig, + SystemVatConfig, VatConfig, KernelStatus, Subcluster, @@ -83,10 +83,10 @@ export class Kernel { readonly #kernelRouter: KernelRouter; /** - * System vats configuration passed to Kernel.make(). + * Host vat configuration passed to Kernel.make(). * Stored for connection after initialization. */ - readonly #systemVatsConfig: KernelSystemVatsConfig | undefined; + readonly #hostVatConfig: SystemVatConfig | undefined; /** * Construct a new kernel instance. @@ -98,7 +98,7 @@ export class Kernel { * @param options.logger - Optional logger for error and diagnostic output. * @param options.keySeed - Optional seed for libp2p key generation. * @param options.mnemonic - Optional BIP39 mnemonic for deriving the kernel identity. - * @param options.systemVats - Optional system vats to connect at kernel creation. + * @param options.hostVat - Optional host vat configuration to connect at kernel creation. */ // eslint-disable-next-line no-restricted-syntax private constructor( @@ -109,13 +109,13 @@ export class Kernel { logger?: Logger; keySeed?: string | undefined; mnemonic?: string | undefined; - systemVats?: KernelSystemVatsConfig; + hostVat?: SystemVatConfig; } = {}, ) { this.#platformServices = platformServices; this.#logger = options.logger ?? new Logger('ocap-kernel'); this.#kernelStore = makeKernelStore(kernelDatabase, this.#logger); - this.#systemVatsConfig = options.systemVats; + this.#hostVatConfig = options.hostVat; if (!this.#kernelStore.kv.get('initialized')) { this.#kernelStore.kv.set('initialized', 'true'); } @@ -222,7 +222,7 @@ export class Kernel { * @param options.logger - Optional logger for error and diagnostic output. * @param options.keySeed - Optional seed for libp2p key generation. * @param options.mnemonic - Optional BIP39 mnemonic for deriving the kernel identity. - * @param options.systemVats - Optional system vats to connect at kernel creation. + * @param options.hostVat - Optional host vat configuration to connect at kernel creation. * @returns A promise for the new kernel instance. */ static async make( @@ -233,7 +233,7 @@ export class Kernel { logger?: Logger; keySeed?: string | undefined; mnemonic?: string | undefined; - systemVats?: KernelSystemVatsConfig; + hostVat?: SystemVatConfig; } = {}, ): Promise { const kernel = new Kernel(platformServices, kernelDatabase, options); @@ -255,13 +255,18 @@ export class Kernel { // This ensures that any messages in the queue have their target vats ready await this.#vatManager.initializeAllVats(); - // Prepare static system vats if configured (kernel side setup only). - // The kernel sets up to receive connections - it does NOT reach out. - // Actual connection happens when supervisor-side calls connect(). - if (this.#systemVatsConfig) { - for (const vatConfig of this.#systemVatsConfig.vats) { - this.#systemVatManager.prepareStaticSystemVat(vatConfig); - } + // Register host vat if configured. + // This runs asynchronously - the registration completes when the supervisor + // side calls connect() via the transport's awaitConnection(). + if (this.#hostVatConfig) { + this.#systemVatManager + .registerSystemVat(this.#hostVatConfig) + .catch((error) => { + this.#logger.error( + `Failed to register host vat ${this.#hostVatConfig?.name}:`, + error, + ); + }); } // Start the kernel queue processing (non-blocking) diff --git a/packages/ocap-kernel/src/index.ts b/packages/ocap-kernel/src/index.ts index 5ac71de38..f9cb9f36b 100644 --- a/packages/ocap-kernel/src/index.ts +++ b/packages/ocap-kernel/src/index.ts @@ -20,10 +20,8 @@ export type { SystemVatTransport, SystemVatSyscallHandler, SystemVatDeliverFn, - StaticSystemVatConfig, - DynamicSystemVatConfig, + SystemVatConfig, SystemVatRegistrationResult, - KernelSystemVatsConfig, } from './types.ts'; export type { RemoteMessageHandler, diff --git a/packages/ocap-kernel/src/kernel-facet.ts b/packages/ocap-kernel/src/kernel-facet.ts index 24d2987ce..d1f808182 100644 --- a/packages/ocap-kernel/src/kernel-facet.ts +++ b/packages/ocap-kernel/src/kernel-facet.ts @@ -8,7 +8,7 @@ import type { ClusterConfig, Subcluster, KernelStatus, - DynamicSystemVatConfig, + SystemVatConfig, SystemVatId, } from './types.ts'; import type { SystemVatManager } from './vats/SystemVatManager.ts'; @@ -27,7 +27,7 @@ export type KernelFacetDependencies = Pick< > & { logger?: Logger; /** Optional system vat manager for dynamic registration. */ - systemVatManager?: Pick; + systemVatManager?: Pick; }; /** @@ -94,14 +94,14 @@ export type KernelFacet = Omit< launchSubcluster: (config: ClusterConfig) => Promise; /** - * Register a dynamic system vat at runtime. + * Register a system vat at runtime. * Used by UIs and other components that connect after kernel initialization. * - * @param config - Configuration for the dynamic system vat. + * @param config - Configuration for the system vat. * @returns A promise for the registration result. */ registerSystemVat: ( - config: DynamicSystemVatConfig, + config: SystemVatConfig, ) => Promise; /** @@ -219,22 +219,22 @@ export function makeKernelFacet(deps: KernelFacetDependencies): KernelFacet { }, /** - * Register a dynamic system vat at runtime. + * Register a system vat at runtime. * Used by UIs and other components that connect after kernel initialization. * - * @param config - Configuration for the dynamic system vat. + * @param config - Configuration for the system vat. * @returns A promise for the registration result. */ async registerSystemVat( - config: DynamicSystemVatConfig, + config: SystemVatConfig, ): Promise { if (!systemVatManager) { throw new Error( 'Cannot register system vat: systemVatManager not provided to kernel facet', ); } - logger?.log(`kernelFacet: registering dynamic system vat ${config.name}`); - const result = await systemVatManager.registerDynamicSystemVat(config); + logger?.log(`kernelFacet: registering system vat ${config.name}`); + const result = await systemVatManager.registerSystemVat(config); logger?.log( `kernelFacet: registered system vat ${result.systemVatId} with root ${result.rootKref}`, ); diff --git a/packages/ocap-kernel/src/types.ts b/packages/ocap-kernel/src/types.ts index b36fb4024..62640f7b2 100644 --- a/packages/ocap-kernel/src/types.ts +++ b/packages/ocap-kernel/src/types.ts @@ -445,7 +445,7 @@ export type SystemVatBuildRootObject = ( * Configuration for a single system vat within a system subcluster. * Used when launching system subclusters via Kernel.launchSystemSubcluster(). */ -export type SystemVatConfig = { +export type SystemSubclusterVatConfig = { buildRootObject: SystemVatBuildRootObject; parameters?: Record; }; @@ -461,7 +461,7 @@ export type SystemSubclusterConfig = { /** The name of the bootstrap vat within the subcluster. */ bootstrap: string; /** Map of vat names to their configurations. */ - vats: Record; + vats: Record; /** Optional list of kernel service names to provide to the bootstrap vat. */ services?: string[]; }; @@ -516,23 +516,11 @@ export type SystemVatTransport = { }; /** - * Configuration for a static system vat (declared at kernel construction). - * The runtime creates the supervisor and provides the transport. - */ -export type StaticSystemVatConfig = { - /** Vat name (used in bootstrap message). */ - name: string; - /** Transport callbacks for communication. */ - transport: SystemVatTransport; - /** Optional kernel services to provide to the vat. */ - services?: string[]; -}; - -/** - * Configuration for a dynamic system vat (registered at runtime via kernel facet). - * Used by UIs and other components that connect after kernel initialization. + * Configuration for a system vat using transport-based communication. + * Used for the host vat (configured at kernel construction) and dynamic + * system vats (registered at runtime via kernel facet). */ -export type DynamicSystemVatConfig = { +export type SystemVatConfig = { /** Vat name (used in bootstrap message). */ name: string; /** Transport callbacks for communication. */ @@ -542,7 +530,7 @@ export type DynamicSystemVatConfig = { }; /** - * Result of registering a dynamic system vat. + * Result of registering a system vat. */ export type SystemVatRegistrationResult = { /** The allocated system vat ID. */ @@ -553,14 +541,6 @@ export type SystemVatRegistrationResult = { disconnect: () => Promise; }; -/** - * System vats configuration for Kernel.make(). - * List of static system vats to connect at kernel creation time. - */ -export type KernelSystemVatsConfig = { - vats: StaticSystemVatConfig[]; -}; - export const SubclusterStruct = object({ id: SubclusterIdStruct, config: ClusterConfigStruct, diff --git a/packages/ocap-kernel/src/vats/SystemVatManager.test.ts b/packages/ocap-kernel/src/vats/SystemVatManager.test.ts index 89cc8fb38..6779ba1e7 100644 --- a/packages/ocap-kernel/src/vats/SystemVatManager.test.ts +++ b/packages/ocap-kernel/src/vats/SystemVatManager.test.ts @@ -4,7 +4,7 @@ import { describe, it, expect, beforeEach, vi } from 'vitest'; import type { KernelFacetDependencies } from '../kernel-facet.ts'; import type { KernelQueue } from '../KernelQueue.ts'; import type { KernelStore } from '../store/index.ts'; -import type { StaticSystemVatConfig, SystemVatTransport } from '../types.ts'; +import type { SystemVatConfig, SystemVatTransport } from '../types.ts'; import { SystemVatManager } from './SystemVatManager.ts'; describe('SystemVatManager', () => { @@ -15,14 +15,10 @@ describe('SystemVatManager', () => { let mockTransport: SystemVatTransport; const makeTransport = (): SystemVatTransport => { - const connectionPromise = { - resolve: vi.fn(), - promise: Promise.resolve(), - }; return { deliver: vi.fn().mockResolvedValue(null), setSyscallHandler: vi.fn(), - awaitConnection: vi.fn().mockReturnValue(connectionPromise.promise), + awaitConnection: vi.fn().mockResolvedValue(undefined), }; }; @@ -34,6 +30,8 @@ describe('SystemVatManager', () => { erefToKref: vi.fn().mockReturnValue(null), initKernelObject: vi.fn().mockReturnValue('ko1'), addCListEntry: vi.fn(), + getPromisesByDecider: vi.fn().mockReturnValue([]), + deleteEndpoint: vi.fn(), kv: { get: vi.fn().mockReturnValue(undefined), set: vi.fn(), @@ -42,6 +40,7 @@ describe('SystemVatManager', () => { mockKernelQueue = { enqueueSend: vi.fn(), + resolvePromises: vi.fn(), } as unknown as KernelQueue; mockKernelFacetDeps = { @@ -65,53 +64,53 @@ describe('SystemVatManager', () => { }); }); - describe('prepareStaticSystemVat', () => { - it('allocates system vat ID starting from sv0', () => { - const config: StaticSystemVatConfig = { + describe('registerSystemVat', () => { + it('allocates system vat ID starting from sv0', async () => { + const config: SystemVatConfig = { name: 'testVat', transport: mockTransport, }; - const result = manager.prepareStaticSystemVat(config); + const result = await manager.registerSystemVat(config); expect(result.systemVatId).toBe('sv0'); }); - it('allocates sequential system vat IDs', () => { - const config1: StaticSystemVatConfig = { + it('allocates sequential system vat IDs', async () => { + const config1: SystemVatConfig = { name: 'vat1', transport: makeTransport(), }; - const config2: StaticSystemVatConfig = { + const config2: SystemVatConfig = { name: 'vat2', transport: makeTransport(), }; - const result1 = manager.prepareStaticSystemVat(config1); - const result2 = manager.prepareStaticSystemVat(config2); + const result1 = await manager.registerSystemVat(config1); + const result2 = await manager.registerSystemVat(config2); expect(result1.systemVatId).toBe('sv0'); expect(result2.systemVatId).toBe('sv1'); }); - it('initializes endpoint in kernel store', () => { - const config: StaticSystemVatConfig = { + it('initializes endpoint in kernel store', async () => { + const config: SystemVatConfig = { name: 'testVat', transport: mockTransport, }; - manager.prepareStaticSystemVat(config); + await manager.registerSystemVat(config); expect(mockKernelStore.initEndpoint).toHaveBeenCalledWith('sv0'); }); - it('creates root kernel object if not exists', () => { - const config: StaticSystemVatConfig = { + it('creates root kernel object if not exists', async () => { + const config: SystemVatConfig = { name: 'testVat', transport: mockTransport, }; - manager.prepareStaticSystemVat(config); + await manager.registerSystemVat(config); expect(mockKernelStore.initKernelObject).toHaveBeenCalledWith('sv0'); expect(mockKernelStore.addCListEntry).toHaveBeenCalledWith( @@ -121,33 +120,33 @@ describe('SystemVatManager', () => { ); }); - it('uses existing root kref if already exists', () => { + it('uses existing root kref if already exists', async () => { (mockKernelStore.erefToKref as ReturnType).mockReturnValue( 'ko99', ); - const config: StaticSystemVatConfig = { + const config: SystemVatConfig = { name: 'testVat', transport: mockTransport, }; - manager.prepareStaticSystemVat(config); + await manager.registerSystemVat(config); expect(mockKernelStore.initKernelObject).not.toHaveBeenCalled(); expect(mockKernelStore.addCListEntry).not.toHaveBeenCalled(); }); - it('sets syscall handler on transport', () => { - const config: StaticSystemVatConfig = { + it('sets syscall handler on transport', async () => { + const config: SystemVatConfig = { name: 'testVat', transport: mockTransport, }; - manager.prepareStaticSystemVat(config); + await manager.registerSystemVat(config); expect(mockTransport.setSyscallHandler).toHaveBeenCalled(); }); - it('waits for connection before sending bootstrap', async () => { + it('awaits connection before sending bootstrap', async () => { let resolveConnection: () => void; const connectionPromise = new Promise((resolve) => { resolveConnection = resolve; @@ -158,36 +157,61 @@ describe('SystemVatManager', () => { awaitConnection: vi.fn().mockReturnValue(connectionPromise), }; - const config: StaticSystemVatConfig = { + const config: SystemVatConfig = { name: 'testVat', transport, }; - manager.prepareStaticSystemVat(config); + // Start registration (will await connection) + const registrationPromise = manager.registerSystemVat(config); // Bootstrap should not be sent yet expect(mockKernelQueue.enqueueSend).not.toHaveBeenCalled(); // Resolve connection resolveConnection!(); - await connectionPromise; - // Give time for async handler to run - await new Promise((resolve) => setTimeout(resolve, 10)); + // Wait for registration to complete + await registrationPromise; // Now bootstrap should be sent expect(mockKernelQueue.enqueueSend).toHaveBeenCalled(); }); + + it('returns root kref and disconnect function', async () => { + const config: SystemVatConfig = { + name: 'testVat', + transport: mockTransport, + }; + + const result = await manager.registerSystemVat(config); + + expect(result.rootKref).toBe('ko1'); + expect(typeof result.disconnect).toBe('function'); + }); + + it('disconnect function removes vat', async () => { + const config: SystemVatConfig = { + name: 'testVat', + transport: mockTransport, + }; + + const result = await manager.registerSystemVat(config); + + expect(manager.getSystemVatHandle(result.systemVatId)).toBeDefined(); + await result.disconnect(); + expect(manager.getSystemVatHandle(result.systemVatId)).toBeUndefined(); + }); }); describe('getSystemVatHandle', () => { - it('returns handle for prepared system vat', () => { - const config: StaticSystemVatConfig = { + it('returns handle for registered system vat', async () => { + const config: SystemVatConfig = { name: 'testVat', transport: mockTransport, }; - manager.prepareStaticSystemVat(config); + await manager.registerSystemVat(config); const handle = manager.getSystemVatHandle('sv0'); expect(handle).toBeDefined(); @@ -199,18 +223,18 @@ describe('SystemVatManager', () => { expect(handle).toBeUndefined(); }); - it('returns correct handle for multiple system vats', () => { - const config1: StaticSystemVatConfig = { + it('returns correct handle for multiple system vats', async () => { + const config1: SystemVatConfig = { name: 'vat1', transport: makeTransport(), }; - const config2: StaticSystemVatConfig = { + const config2: SystemVatConfig = { name: 'vat2', transport: makeTransport(), }; - manager.prepareStaticSystemVat(config1); - manager.prepareStaticSystemVat(config2); + await manager.registerSystemVat(config1); + await manager.registerSystemVat(config2); const handle1 = manager.getSystemVatHandle('sv0'); const handle2 = manager.getSystemVatHandle('sv1'); @@ -223,12 +247,12 @@ describe('SystemVatManager', () => { describe('disconnectSystemVat', () => { it('removes system vat from tracking', async () => { - const config: StaticSystemVatConfig = { + const config: SystemVatConfig = { name: 'testVat', transport: mockTransport, }; - manager.prepareStaticSystemVat(config); + await manager.registerSystemVat(config); expect(manager.getSystemVatHandle('sv0')).toBeDefined(); await manager.disconnectSystemVat('sv0'); @@ -240,74 +264,34 @@ describe('SystemVatManager', () => { const result = await manager.disconnectSystemVat('sv999'); expect(result).toBeUndefined(); }); - }); - - describe('registerDynamicSystemVat', () => { - it('allocates system vat ID', async () => { - const connectionKit = { - resolve: vi.fn(), - promise: Promise.resolve(), - }; - const transport: SystemVatTransport = { - deliver: vi.fn().mockResolvedValue(null), - setSyscallHandler: vi.fn(), - awaitConnection: vi.fn().mockReturnValue(connectionKit.promise), - }; - - const result = await manager.registerDynamicSystemVat({ - name: 'dynamicVat', - transport, - }); - - expect(result.systemVatId).toBe('sv0'); - }); - it('returns root kref and disconnect function', async () => { - const transport: SystemVatTransport = { - deliver: vi.fn().mockResolvedValue(null), - setSyscallHandler: vi.fn(), - awaitConnection: vi.fn().mockResolvedValue(undefined), - }; - - const result = await manager.registerDynamicSystemVat({ - name: 'dynamicVat', - transport, - }); + it('rejects pending promises where vat is decider', async () => { + ( + mockKernelStore.getPromisesByDecider as ReturnType + ).mockReturnValue(['kp1', 'kp2']); - expect(result.rootKref).toBe('ko1'); - expect(typeof result.disconnect).toBe('function'); - }); - - it('sends bootstrap after awaiting connection', async () => { - const transport: SystemVatTransport = { - deliver: vi.fn().mockResolvedValue(null), - setSyscallHandler: vi.fn(), - awaitConnection: vi.fn().mockResolvedValue(undefined), + const config: SystemVatConfig = { + name: 'testVat', + transport: mockTransport, }; - await manager.registerDynamicSystemVat({ - name: 'dynamicVat', - transport, - }); + await manager.registerSystemVat(config); + await manager.disconnectSystemVat('sv0'); - expect(mockKernelQueue.enqueueSend).toHaveBeenCalled(); + expect(mockKernelStore.getPromisesByDecider).toHaveBeenCalledWith('sv0'); + expect(mockKernelQueue.resolvePromises).toHaveBeenCalledTimes(2); }); - it('disconnect function removes vat', async () => { - const transport: SystemVatTransport = { - deliver: vi.fn().mockResolvedValue(null), - setSyscallHandler: vi.fn(), - awaitConnection: vi.fn().mockResolvedValue(undefined), + it('cleans up endpoint in kernel store', async () => { + const config: SystemVatConfig = { + name: 'testVat', + transport: mockTransport, }; - const result = await manager.registerDynamicSystemVat({ - name: 'dynamicVat', - transport, - }); + await manager.registerSystemVat(config); + await manager.disconnectSystemVat('sv0'); - expect(manager.getSystemVatHandle(result.systemVatId)).toBeDefined(); - await result.disconnect(); - expect(manager.getSystemVatHandle(result.systemVatId)).toBeUndefined(); + expect(mockKernelStore.deleteEndpoint).toHaveBeenCalledWith('sv0'); }); }); }); diff --git a/packages/ocap-kernel/src/vats/SystemVatManager.ts b/packages/ocap-kernel/src/vats/SystemVatManager.ts index 3877c43d7..361f60969 100644 --- a/packages/ocap-kernel/src/vats/SystemVatManager.ts +++ b/packages/ocap-kernel/src/vats/SystemVatManager.ts @@ -8,22 +8,13 @@ import type { SlotValue } from '../liveslots/kernel-marshal.ts'; import type { KernelStore } from '../store/index.ts'; import type { SystemVatId, - StaticSystemVatConfig, - DynamicSystemVatConfig, + SystemVatConfig, SystemVatRegistrationResult, KRef, } from '../types.ts'; import { ROOT_OBJECT_VREF } from '../types.ts'; import { SystemVatHandle } from './SystemVatHandle.ts'; -/** - * Result of preparing a static system vat. - */ -export type StaticSystemVatPrepareResult = { - /** The system vat ID. */ - systemVatId: SystemVatId; -}; - type SystemVatManagerOptions = { kernelStore: KernelStore; kernelQueue: KernelQueue; @@ -40,7 +31,6 @@ type SystemVatRecord = { name: string; handle: SystemVatHandle; rootKref: KRef; - isStatic: boolean; }; /** @@ -53,9 +43,8 @@ type SystemVatRecord = { * - Receive a kernel facet in the bootstrap message * - Don't participate in kernel persistence machinery * - * Supports both: - * - Static system vats: Declared at kernel construction time - * - Dynamic system vats: Registered at runtime via kernel facet + * The host vat (background/main vat) is configured at kernel construction time. + * Additional system vats can be registered dynamically via the kernel facet. */ export class SystemVatManager { /** Storage holding the kernel's persistent state */ @@ -150,14 +139,13 @@ export class SystemVatManager { /** * Set up a system vat from a transport config. * - * @param config - The static system vat config with transport. - * @param isStatic - Whether this is a static (at kernel init) or dynamic vat. + * @param config - The system vat config with transport. * @returns The system vat ID and root kref. */ - #setupSystemVat( - config: StaticSystemVatConfig, - isStatic: boolean, - ): { systemVatId: SystemVatId; rootKref: KRef } { + #setupSystemVat(config: SystemVatConfig): { + systemVatId: SystemVatId; + rootKref: KRef; + } { const { name, transport } = config; const systemVatId = this.#allocateSystemVatId(); @@ -190,7 +178,6 @@ export class SystemVatManager { name, handle, rootKref, - isStatic, }; this.#systemVats.set(systemVatId, record); @@ -198,91 +185,27 @@ export class SystemVatManager { } /** - * Prepare a static system vat using a provided transport. + * Register a system vat using a provided transport. * * The runtime creates the supervisor externally and provides the transport for - * communication. This method sets up the kernel side and returns immediately. - * The actual connection and bootstrap happen asynchronously when the - * supervisor-side initiates connection via the transport's `awaitConnection()`. + * communication. This method sets up the kernel side and awaits connection + * before sending the bootstrap message. * - * @param config - Configuration for the static system vat with transport. - * @returns The prepare result with the system vat ID. - */ - prepareStaticSystemVat( - config: StaticSystemVatConfig, - ): StaticSystemVatPrepareResult { - const { systemVatId, rootKref } = this.#setupSystemVat(config, true); - - // Get the singleton kernel facet kref - const kernelFacetKref = this.#getKernelFacetKref(); - - // Build roots object for bootstrap (just this vat's root) - const roots: Record = { - [config.name]: kslot(rootKref, 'vatRoot'), - }; - - // Build services object - always include kernelFacet - const services: Record = { - kernelFacet: kslot(kernelFacetKref, 'KernelFacet'), - }; - if (config.services) { - for (const serviceName of config.services) { - const serviceKref = this.#kernelStore.kv.get( - `kernelService.${serviceName}`, - ); - if (serviceKref) { - services[serviceName] = kslot(serviceKref); - } else { - this.#logger.warn(`Kernel service '${serviceName}' not found`); - } - } - } - - // Set up to send bootstrap after the vat is connected. - config.transport - .awaitConnection() - .then(() => { - // Supervisor has connected. Now send the bootstrap message. - this.#kernelQueue.enqueueSend(rootKref, { - methargs: kser(['bootstrap', [roots, services]]), - }); - return undefined; - }) - .catch((error) => { - this.#logger.error( - `Failed to connect system vat ${config.name}:`, - error, - ); - }); - - return { systemVatId }; - } - - /** - * Register a dynamic system vat at runtime. + * For the host vat (configured at kernel construction), call this during kernel + * init. For dynamic vats (UI instances, etc.), call via the kernel facet. * - * Called via the kernel facet to register new system vats after the kernel - * is already running (e.g., UI instances in an extension). - * - * @param config - Configuration for the dynamic system vat. + * @param config - Configuration for the system vat with transport. * @returns A promise for the registration result with system vat ID and disconnect function. */ - async registerDynamicSystemVat( - config: DynamicSystemVatConfig, + async registerSystemVat( + config: SystemVatConfig, ): Promise { - const staticConfig: StaticSystemVatConfig = { - name: config.name, - transport: config.transport, - }; - if (config.services !== undefined) { - staticConfig.services = config.services; - } - const { systemVatId, rootKref } = this.#setupSystemVat(staticConfig, false); + const { systemVatId, rootKref } = this.#setupSystemVat(config); // Get the singleton kernel facet kref const kernelFacetKref = this.#getKernelFacetKref(); - // Build roots object for bootstrap + // Build roots object for bootstrap (just this vat's root) const roots: Record = { [config.name]: kslot(rootKref, 'vatRoot'), }; @@ -325,6 +248,9 @@ export class SystemVatManager { /** * Disconnect and clean up a system vat. * + * This rejects any pending promises where this vat is the decider and + * cleans up all clist entries and endpoint state. + * * @param systemVatId - The system vat ID to disconnect. */ async disconnectSystemVat(systemVatId: SystemVatId): Promise { @@ -334,12 +260,16 @@ export class SystemVatManager { return; } - // TODO: Proper cleanup: - // - Reject pending promises where this vat is the decider - // - Retire imports held by this vat - // - Notify other vats that references to this vat are broken + // Reject pending promises where this vat is the decider + const failure = kser(`System vat ${systemVatId} disconnected`); + for (const kpid of this.#kernelStore.getPromisesByDecider(systemVatId)) { + this.#kernelQueue.resolvePromises(systemVatId, [[kpid, true, failure]]); + } + + // Clean up clist entries and endpoint state + this.#kernelStore.deleteEndpoint(systemVatId); - // Remove the vat record + // Remove the vat record from in-memory tracking this.#systemVats.delete(systemVatId); this.#logger.log(`Disconnected system vat ${systemVatId} (${record.name})`); diff --git a/packages/ocap-kernel/src/vats/index.ts b/packages/ocap-kernel/src/vats/index.ts index f374a9556..1e786da36 100644 --- a/packages/ocap-kernel/src/vats/index.ts +++ b/packages/ocap-kernel/src/vats/index.ts @@ -9,4 +9,3 @@ export type { SystemVatSyscallFn, } from './SystemVatHandle.ts'; export { SystemVatManager } from './SystemVatManager.ts'; -export type { StaticSystemVatPrepareResult } from './SystemVatManager.ts'; diff --git a/packages/ocap-kernel/test/integration/system-vat.test.ts b/packages/ocap-kernel/test/integration/system-vat.test.ts index bf58947b5..8822cfd99 100644 --- a/packages/ocap-kernel/test/integration/system-vat.test.ts +++ b/packages/ocap-kernel/test/integration/system-vat.test.ts @@ -14,7 +14,7 @@ import type { SystemVatTransport, SystemVatSyscallHandler, SystemVatDeliverFn, - StaticSystemVatConfig, + SystemVatConfig, } from '../../src/types.ts'; import { SystemVatSupervisor } from '../../src/vats/SystemVatSupervisor.ts'; import { makeMapKernelDatabase } from '../storage.ts'; @@ -24,7 +24,7 @@ import { makeMapKernelDatabase } from '../storage.ts'; */ type TestSystemVatResult = { /** Config for kernel. */ - config: StaticSystemVatConfig; + config: SystemVatConfig; /** Call after Kernel.make() to initiate connection from supervisor side. */ connect: () => void; /** Promise that resolves to kernelFacet when bootstrap completes. */ @@ -50,7 +50,7 @@ function makeTestSystemVat(options: { // Promise kit for kernel facet - resolves when bootstrap is called const kernelFacetKit = makePromiseKit(); - // Syscall handler - set by kernel during prepareStaticSystemVat() + // Syscall handler - set by kernel during registerSystemVat() let syscallHandler: SystemVatSyscallHandler | null = null; // Build root object that captures kernelFacet from bootstrap @@ -106,7 +106,7 @@ function makeTestSystemVat(options: { }); }; - const config: StaticSystemVatConfig = { + const config: SystemVatConfig = { name, transport, }; @@ -141,7 +141,7 @@ describe('system vat integration', { timeout: 30_000 }, () => { kernel = await Kernel.make(mockPlatformServices, kernelDatabase, { resetStorage: true, logger: logger.subLogger({ tags: ['kernel'] }), - systemVats: { vats: [systemVat.config] }, + hostVat: systemVat.config, }); // Supervisor-side initiates connection AFTER kernel exists From 5b383a512cae9d05adc9b9cd29e724146897a60a Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Thu, 29 Jan 2026 16:27:25 -0800 Subject: [PATCH 26/41] refactor(ocap-kernel): Move getCrankResults to VatSyscall Extract the shared #getCrankResults() logic from VatHandle and SystemVatHandle into VatSyscall, eliminating duplicate code. The method accepts an optional deliveryError parameter to support both usage patterns. Co-Authored-By: Claude --- .../ocap-kernel/src/vats/SystemVatHandle.ts | 47 +----- packages/ocap-kernel/src/vats/VatHandle.ts | 47 +----- .../ocap-kernel/src/vats/VatSyscall.test.ts | 156 +++++++++++++++++- packages/ocap-kernel/src/vats/VatSyscall.ts | 37 ++++- 4 files changed, 203 insertions(+), 84 deletions(-) diff --git a/packages/ocap-kernel/src/vats/SystemVatHandle.ts b/packages/ocap-kernel/src/vats/SystemVatHandle.ts index d593f4c89..38a966691 100644 --- a/packages/ocap-kernel/src/vats/SystemVatHandle.ts +++ b/packages/ocap-kernel/src/vats/SystemVatHandle.ts @@ -8,7 +8,6 @@ import type { import type { Logger } from '@metamask/logger'; import type { KernelQueue } from '../KernelQueue.ts'; -import { makeError } from '../liveslots/kernel-marshal.ts'; import type { KernelStore } from '../store/index.ts'; import type { Message, @@ -121,7 +120,7 @@ export class SystemVatHandle implements EndpointHandle { const deliveryError = await this.#deliver( harden(['message', target, swingSetMessage]), ); - return this.#getCrankResults(deliveryError); + return this.#vatSyscall.getCrankResults(deliveryError); } /** @@ -132,7 +131,7 @@ export class SystemVatHandle implements EndpointHandle { */ async deliverNotify(resolutions: VatOneResolution[]): Promise { const deliveryError = await this.#deliver(harden(['notify', resolutions])); - return this.#getCrankResults(deliveryError); + return this.#vatSyscall.getCrankResults(deliveryError); } /** @@ -143,7 +142,7 @@ export class SystemVatHandle implements EndpointHandle { */ async deliverDropExports(vrefs: VRef[]): Promise { const deliveryError = await this.#deliver(harden(['dropExports', vrefs])); - return this.#getCrankResults(deliveryError); + return this.#vatSyscall.getCrankResults(deliveryError); } /** @@ -154,7 +153,7 @@ export class SystemVatHandle implements EndpointHandle { */ async deliverRetireExports(vrefs: VRef[]): Promise { const deliveryError = await this.#deliver(harden(['retireExports', vrefs])); - return this.#getCrankResults(deliveryError); + return this.#vatSyscall.getCrankResults(deliveryError); } /** @@ -165,7 +164,7 @@ export class SystemVatHandle implements EndpointHandle { */ async deliverRetireImports(vrefs: VRef[]): Promise { const deliveryError = await this.#deliver(harden(['retireImports', vrefs])); - return this.#getCrankResults(deliveryError); + return this.#vatSyscall.getCrankResults(deliveryError); } /** @@ -175,40 +174,6 @@ export class SystemVatHandle implements EndpointHandle { */ async deliverBringOutYourDead(): Promise { const deliveryError = await this.#deliver(harden(['bringOutYourDead'])); - return this.#getCrankResults(deliveryError); - } - - /** - * Get the crank results after a delivery. - * - * @param deliveryError - The error from delivery, if any. - * @returns The crank results. - */ - #getCrankResults(deliveryError: string | null): CrankResults { - const results: CrankResults = { - didDelivery: this.systemVatId, - }; - - // These conditionals express a priority order: the consequences of an - // illegal syscall take precedence over a vat requesting termination, etc. - if (this.#vatSyscall.illegalSyscall) { - results.abort = true; - const { info } = this.#vatSyscall.illegalSyscall; - results.terminate = { vatId: this.systemVatId, reject: true, info }; - } else if (deliveryError) { - results.abort = true; - const info = makeError(deliveryError); - results.terminate = { vatId: this.systemVatId, reject: true, info }; - } else if (this.#vatSyscall.vatRequestedTermination) { - if (this.#vatSyscall.vatRequestedTermination.reject) { - results.abort = true; - } - results.terminate = { - vatId: this.systemVatId, - ...this.#vatSyscall.vatRequestedTermination, - }; - } - - return harden(results); + return this.#vatSyscall.getCrankResults(deliveryError); } } diff --git a/packages/ocap-kernel/src/vats/VatHandle.ts b/packages/ocap-kernel/src/vats/VatHandle.ts index 127ad317b..bb401872e 100644 --- a/packages/ocap-kernel/src/vats/VatHandle.ts +++ b/packages/ocap-kernel/src/vats/VatHandle.ts @@ -16,7 +16,7 @@ import { isJsonRpcNotification, isJsonRpcResponse } from '@metamask/utils'; import type { JsonRpcNotification, JsonRpcResponse } from '@metamask/utils'; import type { KernelQueue } from '../KernelQueue.ts'; -import { kser, makeError } from '../liveslots/kernel-marshal.ts'; +import { kser } from '../liveslots/kernel-marshal.ts'; import { vatMethodSpecs, vatSyscallHandlers } from '../rpc/index.ts'; import type { PingVatResult, VatMethod } from '../rpc/index.ts'; import type { KernelStore } from '../store/index.ts'; @@ -217,7 +217,7 @@ export class VatHandle implements EndpointHandle { method: 'deliver', params: ['message', target, message], }); - return this.#getCrankResults(); + return this.#vatSyscall.getCrankResults(); } /** @@ -231,7 +231,7 @@ export class VatHandle implements EndpointHandle { method: 'deliver', params: ['notify', resolutions], }); - return this.#getCrankResults(); + return this.#vatSyscall.getCrankResults(); } /** @@ -245,7 +245,7 @@ export class VatHandle implements EndpointHandle { method: 'deliver', params: ['dropExports', vrefs], }); - return this.#getCrankResults(); + return this.#vatSyscall.getCrankResults(); } /** @@ -259,7 +259,7 @@ export class VatHandle implements EndpointHandle { method: 'deliver', params: ['retireExports', vrefs], }); - return this.#getCrankResults(); + return this.#vatSyscall.getCrankResults(); } /** @@ -273,7 +273,7 @@ export class VatHandle implements EndpointHandle { method: 'deliver', params: ['retireImports', vrefs], }); - return this.#getCrankResults(); + return this.#vatSyscall.getCrankResults(); } /** @@ -286,7 +286,7 @@ export class VatHandle implements EndpointHandle { method: 'deliver', params: ['bringOutYourDead'], }); - return this.#getCrankResults(); + return this.#vatSyscall.getCrankResults(); } /** @@ -340,37 +340,4 @@ export class VatHandle implements EndpointHandle { } return result; } - - /** - * Get the crank results after a delivery. - * - * @returns The crank results. - */ - #getCrankResults(): CrankResults { - const results: CrankResults = { - didDelivery: this.vatId, - }; - - // These conditionals express a priority order: the consequences of an - // illegal syscall take precedence over a vat requesting termination, etc. - if (this.#vatSyscall.illegalSyscall) { - results.abort = true; - const { info } = this.#vatSyscall.illegalSyscall; - results.terminate = { vatId: this.vatId, reject: true, info }; - } else if (this.#vatSyscall.deliveryError) { - results.abort = true; - const info = makeError(this.#vatSyscall.deliveryError); - results.terminate = { vatId: this.vatId, reject: true, info }; - } else if (this.#vatSyscall.vatRequestedTermination) { - if (this.#vatSyscall.vatRequestedTermination.reject) { - results.abort = true; - } - results.terminate = { - vatId: this.vatId, - ...this.#vatSyscall.vatRequestedTermination, - }; - } - - return harden(results); - } } diff --git a/packages/ocap-kernel/src/vats/VatSyscall.test.ts b/packages/ocap-kernel/src/vats/VatSyscall.test.ts index 706ae0680..b235dbdae 100644 --- a/packages/ocap-kernel/src/vats/VatSyscall.test.ts +++ b/packages/ocap-kernel/src/vats/VatSyscall.test.ts @@ -15,7 +15,7 @@ describe('VatSyscall', () => { let kernelQueue: KernelQueue; let kernelStore: KernelStore; let logger: Logger; - let isActive: ReturnType>; + let isActive: () => boolean; let vatSys: VatSyscall; beforeEach(() => { @@ -219,7 +219,7 @@ describe('VatSyscall', () => { describe('error handling', () => { it('handles vat not found error', () => { - isActive.mockReturnValueOnce(false); + vi.mocked(isActive).mockReturnValueOnce(false); const vso = ['send', 'o+1', {}] as unknown as VatSyscallObject; const result = vatSys.handleSyscall(vso); @@ -294,4 +294,156 @@ describe('VatSyscall', () => { expect(result).toStrictEqual(['error', 'system vat not found']); }); }); + + describe('getCrankResults', () => { + it('returns basic result when no errors or termination', () => { + const results = vatSys.getCrankResults(); + expect(results).toStrictEqual({ + didDelivery: 'v1', + }); + }); + + it('returns termination result for illegalSyscall', () => { + vi.mocked(isActive).mockReturnValueOnce(false); + vatSys.handleSyscall(['send', 'o+1', {}] as unknown as VatSyscallObject); + + const results = vatSys.getCrankResults(); + expect(results).toStrictEqual({ + didDelivery: 'v1', + abort: true, + terminate: { + vatId: 'v1', + reject: true, + info: expect.objectContaining({ + body: expect.stringContaining('vat not found'), + }), + }, + }); + }); + + it('returns termination result for deliveryError stored in instance', () => { + vatSys.deliveryError = 'delivery failed'; + + const results = vatSys.getCrankResults(); + expect(results).toStrictEqual({ + didDelivery: 'v1', + abort: true, + terminate: { + vatId: 'v1', + reject: true, + info: expect.objectContaining({ + body: expect.stringContaining('delivery failed'), + }), + }, + }); + }); + + it('returns termination result for deliveryError passed as parameter', () => { + const results = vatSys.getCrankResults('param delivery error'); + expect(results).toStrictEqual({ + didDelivery: 'v1', + abort: true, + terminate: { + vatId: 'v1', + reject: true, + info: expect.objectContaining({ + body: expect.stringContaining('param delivery error'), + }), + }, + }); + }); + + it('prefers parameter deliveryError over instance deliveryError', () => { + vatSys.deliveryError = 'instance error'; + + const results = vatSys.getCrankResults('param error'); + expect(results).toStrictEqual({ + didDelivery: 'v1', + abort: true, + terminate: { + vatId: 'v1', + reject: true, + info: expect.objectContaining({ + body: expect.stringContaining('param error'), + }), + }, + }); + }); + + it('returns termination result for vatRequestedTermination with reject=true', () => { + vatSys.handleSyscall([ + 'exit', + true, + { body: '"error message"', slots: [] }, + ] as unknown as VatSyscallObject); + + const results = vatSys.getCrankResults(); + expect(results).toStrictEqual({ + didDelivery: 'v1', + abort: true, + terminate: { + vatId: 'v1', + reject: true, + info: { body: '"error message"', slots: [] }, + }, + }); + }); + + it('returns termination result for vatRequestedTermination with reject=false', () => { + vatSys.handleSyscall([ + 'exit', + false, + { body: '"graceful exit"', slots: [] }, + ] as unknown as VatSyscallObject); + + const results = vatSys.getCrankResults(); + expect(results).toStrictEqual({ + didDelivery: 'v1', + terminate: { + vatId: 'v1', + reject: false, + info: { body: '"graceful exit"', slots: [] }, + }, + }); + }); + + it('prioritizes illegalSyscall over deliveryError', () => { + vi.mocked(isActive).mockReturnValueOnce(false); + vatSys.handleSyscall(['send', 'o+1', {}] as unknown as VatSyscallObject); + vatSys.deliveryError = 'delivery error'; + + const results = vatSys.getCrankResults(); + expect(results.terminate?.info.body).toContain('vat not found'); + }); + + it('prioritizes illegalSyscall over vatRequestedTermination', () => { + vi.mocked(isActive).mockReturnValueOnce(false); + vatSys.handleSyscall(['send', 'o+1', {}] as unknown as VatSyscallObject); + vatSys.vatRequestedTermination = { + reject: false, + info: { body: '"graceful"', slots: [] }, + }; + + const results = vatSys.getCrankResults(); + expect(results.terminate?.info.body).toContain('vat not found'); + }); + + it('prioritizes deliveryError over vatRequestedTermination', () => { + vatSys.deliveryError = 'delivery error'; + vatSys.vatRequestedTermination = { + reject: false, + info: { body: '"graceful"', slots: [] }, + }; + + const results = vatSys.getCrankResults(); + expect(results.terminate?.info.body).toContain('delivery error'); + }); + + it('returns null parameter as no error', () => { + const results = vatSys.getCrankResults(null); + expect(results).toStrictEqual({ + didDelivery: 'v1', + }); + }); + }); }); diff --git a/packages/ocap-kernel/src/vats/VatSyscall.ts b/packages/ocap-kernel/src/vats/VatSyscall.ts index 2749d7899..cd8e2bf2d 100644 --- a/packages/ocap-kernel/src/vats/VatSyscall.ts +++ b/packages/ocap-kernel/src/vats/VatSyscall.ts @@ -15,7 +15,7 @@ import type { KernelQueue } from '../KernelQueue.ts'; import { makeError } from '../liveslots/kernel-marshal.ts'; import type { KernelStore } from '../store/index.ts'; import { coerceMessage } from '../types.ts'; -import type { Message, EndpointId, KRef } from '../types.ts'; +import type { Message, EndpointId, KRef, CrankResults } from '../types.ts'; type VatSyscallProps = { vatId: EndpointId; @@ -294,4 +294,39 @@ export class VatSyscall { #recordVatFatalSyscall(error: string): void { this.illegalSyscall = { vatId: this.vatId, info: makeError(error) }; } + + /** + * Build crank results after a delivery. + * + * @param deliveryError - Error from delivery, if any (for SystemVatHandle). + * @returns The crank results. + */ + getCrankResults(deliveryError?: string | null): CrankResults { + const results: CrankResults = { + didDelivery: this.vatId, + }; + + const errorMessage = deliveryError ?? this.deliveryError; + + // Priority order: illegalSyscall > deliveryError > vatRequestedTermination + if (this.illegalSyscall) { + results.abort = true; + const { info } = this.illegalSyscall; + results.terminate = { vatId: this.vatId, reject: true, info }; + } else if (errorMessage) { + results.abort = true; + const info = makeError(errorMessage); + results.terminate = { vatId: this.vatId, reject: true, info }; + } else if (this.vatRequestedTermination) { + if (this.vatRequestedTermination.reject) { + results.abort = true; + } + results.terminate = { + vatId: this.vatId, + ...this.vatRequestedTermination, + }; + } + + return harden(results); + } } From e7281959f7d75e4c93e1abc06662990f9f93ff75 Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Thu, 29 Jan 2026 16:44:47 -0800 Subject: [PATCH 27/41] refactor: Simplify host-vat and SystemVatSupervisor --- packages/nodejs/src/host-vat/index.ts | 22 ++++----------- .../src/vats/SystemVatSupervisor.ts | 28 +------------------ 2 files changed, 7 insertions(+), 43 deletions(-) diff --git a/packages/nodejs/src/host-vat/index.ts b/packages/nodejs/src/host-vat/index.ts index 692fcedeb..bf5446feb 100644 --- a/packages/nodejs/src/host-vat/index.ts +++ b/packages/nodejs/src/host-vat/index.ts @@ -72,6 +72,8 @@ export function makeHostVat( // Syscall handler - set by kernel during registerSystemVat() let syscallHandler: SystemVatSyscallHandler | null = null; + const supervisor: SystemVatSupervisor | null = null; + // Build root object that captures kernelFacet from bootstrap const buildRootObject: SystemVatBuildRootObject = () => { return makeDefaultExo('KernelHostRoot', { @@ -89,13 +91,9 @@ export function makeHostVat( // Promise kit to signal when supervisor is ready to receive deliveries const supervisorReady = makePromiseKit(); - // Promise kit for connection - resolved when connect() is called and supervisor is ready - const connectionKit = makePromiseKit(); - // Create the transport with a deliver function that waits for the supervisor const deliver: SystemVatDeliverFn = async (delivery) => { - const supervisor = await supervisorReady.promise; - return supervisor.deliver(delivery); + return (supervisor ?? (await supervisorReady.promise)).deliver(delivery); }; const transport: SystemVatTransport = { @@ -104,7 +102,7 @@ export function makeHostVat( syscallHandler = handler; }, // Kernel calls this to wait for connection from supervisor side - awaitConnection: async () => connectionKit.promise, + awaitConnection: async () => supervisorReady.promise.then(() => undefined), }; /** @@ -123,16 +121,8 @@ export function makeHostVat( executeSyscall: syscallHandler, logger: logger.subLogger({ tags: ['supervisor'] }), }) - .then((supervisor) => { - supervisorReady.resolve(supervisor); - // Signal connection ready - kernel will now send bootstrap message - connectionKit.resolve(); - return undefined; - }) - .catch((error) => { - connectionKit.reject(error as Error); - kernelFacetKit.reject(error as Error); - }); + .then((result) => supervisorReady.resolve(result)) + .catch((error) => kernelFacetKit.reject(error as Error)); }; // Config for Kernel.make() diff --git a/packages/ocap-kernel/src/vats/SystemVatSupervisor.ts b/packages/ocap-kernel/src/vats/SystemVatSupervisor.ts index 427cef0a7..aedffc1ee 100644 --- a/packages/ocap-kernel/src/vats/SystemVatSupervisor.ts +++ b/packages/ocap-kernel/src/vats/SystemVatSupervisor.ts @@ -138,10 +138,7 @@ export class SystemVatSupervisor { readonly #logger: Logger; /** Function to dispatch deliveries into liveslots */ - #dispatch: DispatchFn | null = null; - - /** Flag indicating if the system vat has been initialized */ - #initialized: boolean = false; + readonly #dispatch: DispatchFn | null = null; /** * Create and start a system vat supervisor. @@ -180,29 +177,6 @@ export class SystemVatSupervisor { this.id = id; this.#logger = logger; - // Initialize the system vat synchronously during construction - this.#initializeVat(buildRootObject, vatPowers, parameters, executeSyscall); - } - - /** - * Initialize the system vat by creating liveslots with the provided buildRootObject. - * - * @param buildRootObject - Function to build the vat's root object. - * @param vatPowers - External capabilities for this system vat. - * @param parameters - Parameters to pass to buildRootObject. - * @param executeSyscall - Function to execute syscalls synchronously. - */ - #initializeVat( - buildRootObject: SystemVatBuildRootObject, - vatPowers: Record, - parameters: Record | undefined, - executeSyscall: SystemVatExecuteSyscall, - ): void { - if (this.#initialized) { - throw Error('SystemVatSupervisor already initialized'); - } - this.#initialized = true; - const kvStore = makeEphemeralVatKVStore(); const syscall = this.#makeSyscall(executeSyscall, kvStore); const liveSlotsOptions = {}; From d793c7c2205a4c2ae469d1e4d5ba136fc6ba6926 Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Thu, 29 Jan 2026 17:09:48 -0800 Subject: [PATCH 28/41] refactor(ocap-kernel): Unify VatHandle and SystemVatHandle error patterns Make VatHandle explicitly pass deliveryError to getCrankResults() like SystemVatHandle does. Remove the deliveryError instance property from VatSyscall since it's no longer needed. Co-Authored-By: Claude --- packages/ocap-kernel/src/vats/VatHandle.ts | 25 ++++---- .../ocap-kernel/src/vats/VatSyscall.test.ts | 63 +++---------------- packages/ocap-kernel/src/vats/VatSyscall.ts | 13 ++-- 3 files changed, 26 insertions(+), 75 deletions(-) diff --git a/packages/ocap-kernel/src/vats/VatHandle.ts b/packages/ocap-kernel/src/vats/VatHandle.ts index bb401872e..3f126344f 100644 --- a/packages/ocap-kernel/src/vats/VatHandle.ts +++ b/packages/ocap-kernel/src/vats/VatHandle.ts @@ -213,11 +213,11 @@ export class VatHandle implements EndpointHandle { * @returns The crank results. */ async deliverMessage(target: VRef, message: Message): Promise { - await this.sendVatCommand({ + const [, deliveryError] = await this.sendVatCommand({ method: 'deliver', params: ['message', target, message], }); - return this.#vatSyscall.getCrankResults(); + return this.#vatSyscall.getCrankResults(deliveryError); } /** @@ -227,11 +227,11 @@ export class VatHandle implements EndpointHandle { * @returns The crank results. */ async deliverNotify(resolutions: VatOneResolution[]): Promise { - await this.sendVatCommand({ + const [, deliveryError] = await this.sendVatCommand({ method: 'deliver', params: ['notify', resolutions], }); - return this.#vatSyscall.getCrankResults(); + return this.#vatSyscall.getCrankResults(deliveryError); } /** @@ -241,11 +241,11 @@ export class VatHandle implements EndpointHandle { * @returns The crank results. */ async deliverDropExports(vrefs: VRef[]): Promise { - await this.sendVatCommand({ + const [, deliveryError] = await this.sendVatCommand({ method: 'deliver', params: ['dropExports', vrefs], }); - return this.#vatSyscall.getCrankResults(); + return this.#vatSyscall.getCrankResults(deliveryError); } /** @@ -255,11 +255,11 @@ export class VatHandle implements EndpointHandle { * @returns The crank results. */ async deliverRetireExports(vrefs: VRef[]): Promise { - await this.sendVatCommand({ + const [, deliveryError] = await this.sendVatCommand({ method: 'deliver', params: ['retireExports', vrefs], }); - return this.#vatSyscall.getCrankResults(); + return this.#vatSyscall.getCrankResults(deliveryError); } /** @@ -269,11 +269,11 @@ export class VatHandle implements EndpointHandle { * @returns The crank results. */ async deliverRetireImports(vrefs: VRef[]): Promise { - await this.sendVatCommand({ + const [, deliveryError] = await this.sendVatCommand({ method: 'deliver', params: ['retireImports', vrefs], }); - return this.#vatSyscall.getCrankResults(); + return this.#vatSyscall.getCrankResults(deliveryError); } /** @@ -282,11 +282,11 @@ export class VatHandle implements EndpointHandle { * @returns The crank results. */ async deliverBringOutYourDead(): Promise { - await this.sendVatCommand({ + const [, deliveryError] = await this.sendVatCommand({ method: 'deliver', params: ['bringOutYourDead'], }); - return this.#vatSyscall.getCrankResults(); + return this.#vatSyscall.getCrankResults(deliveryError); } /** @@ -328,7 +328,6 @@ export class VatHandle implements EndpointHandle { const result = await this.#rpcClient.call(method, params); if (method === 'initVat' || method === 'deliver') { const [[sets, deletes], deliveryError] = result as VatDeliveryResult; - this.#vatSyscall.deliveryError = deliveryError ?? undefined; const noErrors = !deliveryError && !this.#vatSyscall.illegalSyscall; // On errors, we neither update this vat's KV data nor rollback previous changes. // This is safe because vats are always terminated when errors occur diff --git a/packages/ocap-kernel/src/vats/VatSyscall.test.ts b/packages/ocap-kernel/src/vats/VatSyscall.test.ts index b235dbdae..442aa5ac5 100644 --- a/packages/ocap-kernel/src/vats/VatSyscall.test.ts +++ b/packages/ocap-kernel/src/vats/VatSyscall.test.ts @@ -297,7 +297,7 @@ describe('VatSyscall', () => { describe('getCrankResults', () => { it('returns basic result when no errors or termination', () => { - const results = vatSys.getCrankResults(); + const results = vatSys.getCrankResults(null); expect(results).toStrictEqual({ didDelivery: 'v1', }); @@ -307,7 +307,7 @@ describe('VatSyscall', () => { vi.mocked(isActive).mockReturnValueOnce(false); vatSys.handleSyscall(['send', 'o+1', {}] as unknown as VatSyscallObject); - const results = vatSys.getCrankResults(); + const results = vatSys.getCrankResults(null); expect(results).toStrictEqual({ didDelivery: 'v1', abort: true, @@ -321,10 +321,8 @@ describe('VatSyscall', () => { }); }); - it('returns termination result for deliveryError stored in instance', () => { - vatSys.deliveryError = 'delivery failed'; - - const results = vatSys.getCrankResults(); + it('returns termination result for deliveryError', () => { + const results = vatSys.getCrankResults('delivery error'); expect(results).toStrictEqual({ didDelivery: 'v1', abort: true, @@ -332,39 +330,7 @@ describe('VatSyscall', () => { vatId: 'v1', reject: true, info: expect.objectContaining({ - body: expect.stringContaining('delivery failed'), - }), - }, - }); - }); - - it('returns termination result for deliveryError passed as parameter', () => { - const results = vatSys.getCrankResults('param delivery error'); - expect(results).toStrictEqual({ - didDelivery: 'v1', - abort: true, - terminate: { - vatId: 'v1', - reject: true, - info: expect.objectContaining({ - body: expect.stringContaining('param delivery error'), - }), - }, - }); - }); - - it('prefers parameter deliveryError over instance deliveryError', () => { - vatSys.deliveryError = 'instance error'; - - const results = vatSys.getCrankResults('param error'); - expect(results).toStrictEqual({ - didDelivery: 'v1', - abort: true, - terminate: { - vatId: 'v1', - reject: true, - info: expect.objectContaining({ - body: expect.stringContaining('param error'), + body: expect.stringContaining('delivery error'), }), }, }); @@ -377,7 +343,7 @@ describe('VatSyscall', () => { { body: '"error message"', slots: [] }, ] as unknown as VatSyscallObject); - const results = vatSys.getCrankResults(); + const results = vatSys.getCrankResults(null); expect(results).toStrictEqual({ didDelivery: 'v1', abort: true, @@ -396,7 +362,7 @@ describe('VatSyscall', () => { { body: '"graceful exit"', slots: [] }, ] as unknown as VatSyscallObject); - const results = vatSys.getCrankResults(); + const results = vatSys.getCrankResults(null); expect(results).toStrictEqual({ didDelivery: 'v1', terminate: { @@ -410,9 +376,8 @@ describe('VatSyscall', () => { it('prioritizes illegalSyscall over deliveryError', () => { vi.mocked(isActive).mockReturnValueOnce(false); vatSys.handleSyscall(['send', 'o+1', {}] as unknown as VatSyscallObject); - vatSys.deliveryError = 'delivery error'; - const results = vatSys.getCrankResults(); + const results = vatSys.getCrankResults('delivery error'); expect(results.terminate?.info.body).toContain('vat not found'); }); @@ -424,26 +389,18 @@ describe('VatSyscall', () => { info: { body: '"graceful"', slots: [] }, }; - const results = vatSys.getCrankResults(); + const results = vatSys.getCrankResults(null); expect(results.terminate?.info.body).toContain('vat not found'); }); it('prioritizes deliveryError over vatRequestedTermination', () => { - vatSys.deliveryError = 'delivery error'; vatSys.vatRequestedTermination = { reject: false, info: { body: '"graceful"', slots: [] }, }; - const results = vatSys.getCrankResults(); + const results = vatSys.getCrankResults('delivery error'); expect(results.terminate?.info.body).toContain('delivery error'); }); - - it('returns null parameter as no error', () => { - const results = vatSys.getCrankResults(null); - expect(results).toStrictEqual({ - didDelivery: 'v1', - }); - }); }); }); diff --git a/packages/ocap-kernel/src/vats/VatSyscall.ts b/packages/ocap-kernel/src/vats/VatSyscall.ts index cd8e2bf2d..5091c0c58 100644 --- a/packages/ocap-kernel/src/vats/VatSyscall.ts +++ b/packages/ocap-kernel/src/vats/VatSyscall.ts @@ -54,9 +54,6 @@ export class VatSyscall { /** The illegal syscall that was received */ illegalSyscall: { vatId: EndpointId; info: SwingSetCapData } | undefined; - /** The error when delivery failed */ - deliveryError: string | undefined; - /** The termination request that was received from the vat with syscall.exit() */ vatRequestedTermination: | { reject: boolean; info: SwingSetCapData } @@ -298,24 +295,22 @@ export class VatSyscall { /** * Build crank results after a delivery. * - * @param deliveryError - Error from delivery, if any (for SystemVatHandle). + * @param deliveryError - Error from delivery, if any. * @returns The crank results. */ - getCrankResults(deliveryError?: string | null): CrankResults { + getCrankResults(deliveryError: string | null): CrankResults { const results: CrankResults = { didDelivery: this.vatId, }; - const errorMessage = deliveryError ?? this.deliveryError; - // Priority order: illegalSyscall > deliveryError > vatRequestedTermination if (this.illegalSyscall) { results.abort = true; const { info } = this.illegalSyscall; results.terminate = { vatId: this.vatId, reject: true, info }; - } else if (errorMessage) { + } else if (deliveryError) { results.abort = true; - const info = makeError(errorMessage); + const info = makeError(deliveryError); results.terminate = { vatId: this.vatId, reject: true, info }; } else if (this.vatRequestedTermination) { if (this.vatRequestedTermination.reject) { From 83ac8f1491c7606248cbd8e92e7061861617e17f Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Thu, 29 Jan 2026 18:21:00 -0800 Subject: [PATCH 29/41] refactor(ocap-kernel): Unify VatHandle and SystemVatHandle via BaseVatHandle Create abstract base class BaseVatHandle that implements the 6 delivery methods shared between VatHandle and SystemVatHandle. Both classes now extend BaseVatHandle and provide a deliver function via setDeliver(). - Add BaseVatHandle with deliverMessage, deliverNotify, deliverDropExports, deliverRetireExports, deliverRetireImports, deliverBringOutYourDead - Update VatHandle to extend BaseVatHandle - Update SystemVatHandle to extend BaseVatHandle - Export BaseVatHandle, DeliverFn, and DeliveryObject types Co-Authored-By: Claude Opus 4.5 --- .../ocap-kernel/src/vats/BaseVatHandle.ts | 129 ++++++++++++++++ .../ocap-kernel/src/vats/SystemVatHandle.ts | 123 ++++----------- .../ocap-kernel/src/vats/VatHandle.test.ts | 31 +--- packages/ocap-kernel/src/vats/VatHandle.ts | 145 ++++-------------- packages/ocap-kernel/src/vats/index.ts | 5 + 5 files changed, 192 insertions(+), 241 deletions(-) create mode 100644 packages/ocap-kernel/src/vats/BaseVatHandle.ts diff --git a/packages/ocap-kernel/src/vats/BaseVatHandle.ts b/packages/ocap-kernel/src/vats/BaseVatHandle.ts new file mode 100644 index 000000000..f6530a6b6 --- /dev/null +++ b/packages/ocap-kernel/src/vats/BaseVatHandle.ts @@ -0,0 +1,129 @@ +import type { VatOneResolution } from '@agoric/swingset-liveslots'; + +import type { CrankResults, Message, VRef, EndpointHandle } from '../types.ts'; +import type { VatSyscall } from './VatSyscall.ts'; + +/** + * Delivery object type using our Message type (with optional result). + */ +export type DeliveryObject = + | ['message', VRef, Message] + | ['notify', VatOneResolution[]] + | ['dropExports', VRef[]] + | ['retireExports', VRef[]] + | ['retireImports', VRef[]] + | ['bringOutYourDead']; + +/** + * Function type for delivering messages to a vat. + * + * @param delivery - The delivery object to send to the vat. + * @returns A promise that resolves to the delivery error (or null if no error). + */ +export type DeliverFn = (delivery: DeliveryObject) => Promise; + +/** + * Abstract base class for vat handles. + * + * Implements the delivery methods shared between VatHandle and SystemVatHandle. + * Subclasses provide a deliver function that handles transport-specific logic. + */ +export abstract class BaseVatHandle implements EndpointHandle { + readonly #vatSyscall: VatSyscall; + + protected deliver: DeliverFn | undefined; + + /** + * Construct a new BaseVatHandle instance. + * + * @param vatSyscall - The vat's syscall handler. + */ + protected constructor(vatSyscall: VatSyscall) { + this.#vatSyscall = vatSyscall; + } + + /** + * Get the vat syscall handler. + * + * @returns The vat syscall handler. + */ + protected get vatSyscall(): VatSyscall { + return this.#vatSyscall; + } + + /** + * Perform a delivery and get crank results. + * + * @param delivery - The delivery object. + * @returns The crank results. + */ + async #doDeliver(delivery: DeliveryObject): Promise { + if (!this.deliver) { + throw new Error( + 'deliver function not set - subclass must call setDeliver()', + ); + } + const deliveryError = await this.deliver(delivery); + return this.#vatSyscall.getCrankResults(deliveryError); + } + + /** + * Make a 'message' delivery to the vat. + * + * @param target - The VRef of the object to which the message is addressed. + * @param message - The message to deliver. + * @returns The crank results. + */ + async deliverMessage(target: VRef, message: Message): Promise { + return await this.#doDeliver(['message', target, message]); + } + + /** + * Make a 'notify' delivery to the vat. + * + * @param resolutions - One or more promise resolutions to deliver. + * @returns The crank results. + */ + async deliverNotify(resolutions: VatOneResolution[]): Promise { + return await this.#doDeliver(['notify', resolutions]); + } + + /** + * Make a 'dropExports' delivery to the vat. + * + * @param vrefs - The VRefs of the exports to be dropped. + * @returns The crank results. + */ + async deliverDropExports(vrefs: VRef[]): Promise { + return await this.#doDeliver(['dropExports', vrefs]); + } + + /** + * Make a 'retireExports' delivery to the vat. + * + * @param vrefs - The VRefs of the exports to be retired. + * @returns The crank results. + */ + async deliverRetireExports(vrefs: VRef[]): Promise { + return await this.#doDeliver(['retireExports', vrefs]); + } + + /** + * Make a 'retireImports' delivery to the vat. + * + * @param vrefs - The VRefs of the imports to be retired. + * @returns The crank results. + */ + async deliverRetireImports(vrefs: VRef[]): Promise { + return await this.#doDeliver(['retireImports', vrefs]); + } + + /** + * Make a 'bringOutYourDead' delivery to the vat. + * + * @returns The crank results. + */ + async deliverBringOutYourDead(): Promise { + return await this.#doDeliver(['bringOutYourDead']); + } +} diff --git a/packages/ocap-kernel/src/vats/SystemVatHandle.ts b/packages/ocap-kernel/src/vats/SystemVatHandle.ts index 38a966691..22da17098 100644 --- a/packages/ocap-kernel/src/vats/SystemVatHandle.ts +++ b/packages/ocap-kernel/src/vats/SystemVatHandle.ts @@ -1,6 +1,5 @@ import type { VatDeliveryObject, - VatOneResolution, VatSyscallObject, VatSyscallResult, Message as SwingSetMessage, @@ -9,13 +8,9 @@ import type { Logger } from '@metamask/logger'; import type { KernelQueue } from '../KernelQueue.ts'; import type { KernelStore } from '../store/index.ts'; -import type { - Message, - SystemVatId, - VRef, - CrankResults, - EndpointHandle, -} from '../types.ts'; +import type { SystemVatId } from '../types.ts'; +import { BaseVatHandle } from './BaseVatHandle.ts'; +import type { DeliveryObject } from './BaseVatHandle.ts'; import { VatSyscall } from './VatSyscall.ts'; /** @@ -46,19 +41,10 @@ type SystemVatHandleProps = { * System vats run without compartment isolation directly in the host process. * They don't participate in kernel persistence machinery (no vatstore). */ -export class SystemVatHandle implements EndpointHandle { +export class SystemVatHandle extends BaseVatHandle { /** The ID of the system vat this handles */ readonly systemVatId: SystemVatId; - /** Logger for outputting messages (such as errors) to the console */ - readonly #logger: Logger | undefined; - - /** The system vat's syscall handler */ - readonly #vatSyscall: VatSyscall; - - /** Callback to deliver messages to the system vat */ - readonly #deliver: SystemVatDeliverFn; - /** Flag indicating if this handle is active */ readonly #isActive: boolean = true; @@ -79,18 +65,35 @@ export class SystemVatHandle implements EndpointHandle { deliver, logger, }: SystemVatHandleProps) { - this.systemVatId = systemVatId; - this.#logger = logger; - this.#deliver = deliver; - this.#vatSyscall = new VatSyscall({ + const vatSyscall = new VatSyscall({ vatId: systemVatId, kernelQueue, kernelStore, isActive: () => this.#isActive, vatLabel: 'system vat', - logger: this.#logger?.subLogger({ tags: ['syscall'] }), + logger: logger?.subLogger({ tags: ['syscall'] }), }); + super(vatSyscall); + + this.systemVatId = systemVatId; + + // Set up deliver function that coerces Message to SwingSetMessage and hardens + this.deliver = async (delivery: DeliveryObject): Promise => { + let coercedDelivery: VatDeliveryObject; + if (delivery[0] === 'message') { + const [, target, message] = delivery; + const swingSetMessage: SwingSetMessage = { + methargs: message.methargs, + result: message.result ?? null, + }; + coercedDelivery = ['message', target, swingSetMessage]; + } else { + coercedDelivery = delivery; + } + return deliver(harden(coercedDelivery)); + }; + harden(this); } @@ -101,79 +104,7 @@ export class SystemVatHandle implements EndpointHandle { */ getSyscallHandler(): (syscall: VatSyscallObject) => VatSyscallResult { return (syscall: VatSyscallObject) => { - return this.#vatSyscall.handleSyscall(syscall); + return this.vatSyscall.handleSyscall(syscall); }; } - - /** - * Make a 'message' delivery to the system vat. - * - * @param target - The VRef of the object to which the message is addressed. - * @param message - The message to deliver. - * @returns The crank results. - */ - async deliverMessage(target: VRef, message: Message): Promise { - const swingSetMessage: SwingSetMessage = { - methargs: message.methargs, - result: message.result ?? null, - }; - const deliveryError = await this.#deliver( - harden(['message', target, swingSetMessage]), - ); - return this.#vatSyscall.getCrankResults(deliveryError); - } - - /** - * Make a 'notify' delivery to the system vat. - * - * @param resolutions - One or more promise resolutions to deliver. - * @returns The crank results. - */ - async deliverNotify(resolutions: VatOneResolution[]): Promise { - const deliveryError = await this.#deliver(harden(['notify', resolutions])); - return this.#vatSyscall.getCrankResults(deliveryError); - } - - /** - * Make a 'dropExports' delivery to the system vat. - * - * @param vrefs - The VRefs of the exports to be dropped. - * @returns The crank results. - */ - async deliverDropExports(vrefs: VRef[]): Promise { - const deliveryError = await this.#deliver(harden(['dropExports', vrefs])); - return this.#vatSyscall.getCrankResults(deliveryError); - } - - /** - * Make a 'retireExports' delivery to the system vat. - * - * @param vrefs - The VRefs of the exports to be retired. - * @returns The crank results. - */ - async deliverRetireExports(vrefs: VRef[]): Promise { - const deliveryError = await this.#deliver(harden(['retireExports', vrefs])); - return this.#vatSyscall.getCrankResults(deliveryError); - } - - /** - * Make a 'retireImports' delivery to the system vat. - * - * @param vrefs - The VRefs of the imports to be retired. - * @returns The crank results. - */ - async deliverRetireImports(vrefs: VRef[]): Promise { - const deliveryError = await this.#deliver(harden(['retireImports', vrefs])); - return this.#vatSyscall.getCrankResults(deliveryError); - } - - /** - * Make a 'bringOutYourDead' delivery to the system vat. - * - * @returns The crank results. - */ - async deliverBringOutYourDead(): Promise { - const deliveryError = await this.#deliver(harden(['bringOutYourDead'])); - return this.#vatSyscall.getCrankResults(deliveryError); - } } diff --git a/packages/ocap-kernel/src/vats/VatHandle.test.ts b/packages/ocap-kernel/src/vats/VatHandle.test.ts index 62dacc143..ccba469f7 100644 --- a/packages/ocap-kernel/src/vats/VatHandle.test.ts +++ b/packages/ocap-kernel/src/vats/VatHandle.test.ts @@ -2,7 +2,6 @@ import type { VatOneResolution } from '@agoric/swingset-liveslots'; import type { JsonRpcMessage } from '@metamask/kernel-utils'; import { isJsonRpcMessage } from '@metamask/kernel-utils'; import type { Logger } from '@metamask/logger'; -import type { Json } from '@metamask/utils'; import { delay } from '@ocap/repo-tools/test-utils'; import { TestDuplexStream } from '@ocap/repo-tools/test-utils/streams'; import { describe, it, expect, vi, beforeEach } from 'vitest'; @@ -242,31 +241,6 @@ describe('VatHandle', () => { }); }); - describe('sendVatCommand', () => { - it('sends a message and resolves the promise', async () => { - const dispatch = vi.fn(); - const { vat, stream } = await makeVat({ dispatch }); - const mockMessage = { - method: 'ping' as const, - params: [] as Json[], - }; - - const sendVatCommandPromise = vat.sendVatCommand(mockMessage); - await delay(10); - expect(dispatch).toHaveBeenCalledWith( - expect.objectContaining(mockMessage), - ); - - await stream.receiveInput({ - id: 'v0:1', - result: 'test-response', - jsonrpc: '2.0', - }); - - expect(await sendVatCommandPromise).toBe('test-response'); - }); - }); - describe('terminate', () => { it('terminates the vat and rejects unresolved messages', async () => { const { vat, stream } = await makeVat(); @@ -276,10 +250,7 @@ describe('VatHandle', () => { mockKernelStore.addSubclusterVat('s1', 'v0'); // Create a pending message that should be rejected on terminate - const messagePromise = vat.sendVatCommand({ - method: 'ping' as const, - params: [], - }); + const messagePromise = vat.ping(); await vat.terminate(true); diff --git a/packages/ocap-kernel/src/vats/VatHandle.ts b/packages/ocap-kernel/src/vats/VatHandle.ts index 3f126344f..289c2c4a4 100644 --- a/packages/ocap-kernel/src/vats/VatHandle.ts +++ b/packages/ocap-kernel/src/vats/VatHandle.ts @@ -1,7 +1,4 @@ -import type { - VatOneResolution, - VatSyscallObject, -} from '@agoric/swingset-liveslots'; +import type { VatSyscallObject } from '@agoric/swingset-liveslots'; import { VatDeletedError, StreamReadError } from '@metamask/kernel-errors'; import { RpcClient, RpcService } from '@metamask/kernel-rpc-methods'; import type { @@ -20,15 +17,9 @@ import { kser } from '../liveslots/kernel-marshal.ts'; import { vatMethodSpecs, vatSyscallHandlers } from '../rpc/index.ts'; import type { PingVatResult, VatMethod } from '../rpc/index.ts'; import type { KernelStore } from '../store/index.ts'; -import type { - Message, - VatId, - VatConfig, - VRef, - CrankResults, - VatDeliveryResult, - EndpointHandle, -} from '../types.ts'; +import type { VatId, VatConfig, VatDeliveryResult } from '../types.ts'; +import { BaseVatHandle } from './BaseVatHandle.ts'; +import type { DeliveryObject } from './BaseVatHandle.ts'; import { VatSyscall } from './VatSyscall.ts'; type MessageFromVat = JsonRpcResponse | JsonRpcNotification; @@ -47,7 +38,7 @@ type VatConstructorProps = { /** * Handles communication with and lifecycle management of a vat. */ -export class VatHandle implements EndpointHandle { +export class VatHandle extends BaseVatHandle { /** The ID of the vat this is the VatHandle for */ readonly vatId: VatId; @@ -66,9 +57,6 @@ export class VatHandle implements EndpointHandle { /** Storage holding this vat's persistent state */ readonly #vatStore: VatStore; - /** The vat's syscall */ - readonly #vatSyscall: VatSyscall; - /** The kernel's queue */ readonly #kernelQueue: KernelQueue; @@ -96,33 +84,44 @@ export class VatHandle implements EndpointHandle { kernelQueue, logger, }: VatConstructorProps) { - this.vatId = vatId; - this.config = vatConfig; - this.#logger = logger; - this.#vatStream = vatStream; - this.#kernelStore = kernelStore; - this.#vatStore = kernelStore.makeVatStore(vatId); - this.#kernelQueue = kernelQueue; - this.#vatSyscall = new VatSyscall({ + const vatSyscall = new VatSyscall({ vatId, kernelQueue, kernelStore, isActive: () => kernelStore.isVatActive(vatId), - logger: this.#logger?.subLogger({ tags: ['syscall'] }), + logger: logger?.subLogger({ tags: ['syscall'] }), }); + super(vatSyscall); + + this.vatId = vatId; + this.config = vatConfig; + this.#logger = logger; + this.#vatStream = vatStream; + this.#kernelStore = kernelStore; + this.#kernelQueue = kernelQueue; + this.#vatStore = kernelStore.makeVatStore(vatId); this.#rpcClient = new RpcClient( vatMethodSpecs, async (request) => { - await this.#vatStream.write(request); + await vatStream.write(request); }, - `${this.vatId}:`, + `${vatId}:`, ); this.#rpcService = new RpcService(vatSyscallHandlers, { handleSyscall: (params) => { - this.#vatSyscall.handleSyscall(params as VatSyscallObject); + this.vatSyscall.handleSyscall(params as VatSyscallObject); }, }); + + // Set up deliver function for BaseVatHandle + this.deliver = async (delivery: DeliveryObject): Promise => { + const [, deliveryError] = await this.sendVatCommand({ + method: 'deliver', + params: delivery, + }); + return deliveryError; + }; } /** @@ -205,90 +204,6 @@ export class VatHandle implements EndpointHandle { } } - /** - * Make a 'message' delivery to the vat. - * - * @param target - The VRef of the object to which the message is addressed. - * @param message - The message to deliver. - * @returns The crank results. - */ - async deliverMessage(target: VRef, message: Message): Promise { - const [, deliveryError] = await this.sendVatCommand({ - method: 'deliver', - params: ['message', target, message], - }); - return this.#vatSyscall.getCrankResults(deliveryError); - } - - /** - * Make a 'notify' delivery to the vat. - * - * @param resolutions - One or more promise resolutions to deliver. - * @returns The crank results. - */ - async deliverNotify(resolutions: VatOneResolution[]): Promise { - const [, deliveryError] = await this.sendVatCommand({ - method: 'deliver', - params: ['notify', resolutions], - }); - return this.#vatSyscall.getCrankResults(deliveryError); - } - - /** - * Make a 'dropExports' delivery to the vat. - * - * @param vrefs - The VRefs of the exports to be dropped. - * @returns The crank results. - */ - async deliverDropExports(vrefs: VRef[]): Promise { - const [, deliveryError] = await this.sendVatCommand({ - method: 'deliver', - params: ['dropExports', vrefs], - }); - return this.#vatSyscall.getCrankResults(deliveryError); - } - - /** - * Make a 'retireExports' delivery to the vat. - * - * @param vrefs - The VRefs of the exports to be retired. - * @returns The crank results. - */ - async deliverRetireExports(vrefs: VRef[]): Promise { - const [, deliveryError] = await this.sendVatCommand({ - method: 'deliver', - params: ['retireExports', vrefs], - }); - return this.#vatSyscall.getCrankResults(deliveryError); - } - - /** - * Make a 'retireImports' delivery to the vat. - * - * @param vrefs - The VRefs of the imports to be retired. - * @returns The crank results. - */ - async deliverRetireImports(vrefs: VRef[]): Promise { - const [, deliveryError] = await this.sendVatCommand({ - method: 'deliver', - params: ['retireImports', vrefs], - }); - return this.#vatSyscall.getCrankResults(deliveryError); - } - - /** - * Make a 'bringOutYourDead' delivery to the vat. - * - * @returns The crank results. - */ - async deliverBringOutYourDead(): Promise { - const [, deliveryError] = await this.sendVatCommand({ - method: 'deliver', - params: ['bringOutYourDead'], - }); - return this.#vatSyscall.getCrankResults(deliveryError); - } - /** * Terminates the vat. * @@ -326,9 +241,9 @@ export class VatHandle implements EndpointHandle { params: ExtractParams; }): Promise> { const result = await this.#rpcClient.call(method, params); - if (method === 'initVat' || method === 'deliver') { + if (method === 'deliver' || method === 'initVat') { const [[sets, deletes], deliveryError] = result as VatDeliveryResult; - const noErrors = !deliveryError && !this.#vatSyscall.illegalSyscall; + const noErrors = !deliveryError && !this.vatSyscall.illegalSyscall; // On errors, we neither update this vat's KV data nor rollback previous changes. // This is safe because vats are always terminated when errors occur // and they have their own databases, which are deleted when the vat is terminated. diff --git a/packages/ocap-kernel/src/vats/index.ts b/packages/ocap-kernel/src/vats/index.ts index 1e786da36..7c0ec1ba0 100644 --- a/packages/ocap-kernel/src/vats/index.ts +++ b/packages/ocap-kernel/src/vats/index.ts @@ -1,3 +1,8 @@ +export { + BaseVatHandle, + type DeliverFn, + type DeliveryObject, +} from './BaseVatHandle.ts'; export { SystemVatSupervisor } from './SystemVatSupervisor.ts'; export type { SystemVatExecuteSyscall, From 3f1aba79db34429fd4aadb6e4d6fd2cae7c3e7bd Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Thu, 29 Jan 2026 21:22:05 -0800 Subject: [PATCH 30/41] refactor(ocap-kernel): Move DeliveryObject to types.ts and simplify coercion Move DeliveryObject type from BaseVatHandle.ts to types.ts for better discoverability and export it from the package. Remove unnecessary message coercion in SystemVatHandle since our Message type (with optional result) is equivalent to SwingSet's for JSON serialization. Co-Authored-By: Claude --- packages/ocap-kernel/src/index.ts | 1 + packages/ocap-kernel/src/types.ts | 18 +++++++++++++-- .../ocap-kernel/src/vats/BaseVatHandle.ts | 19 ++++++--------- .../src/vats/SystemVatHandle.test.ts | 13 ++++------- .../ocap-kernel/src/vats/SystemVatHandle.ts | 23 ++++--------------- packages/ocap-kernel/src/vats/VatHandle.ts | 8 +++++-- packages/ocap-kernel/src/vats/index.ts | 6 +---- 7 files changed, 40 insertions(+), 48 deletions(-) diff --git a/packages/ocap-kernel/src/index.ts b/packages/ocap-kernel/src/index.ts index f9cb9f36b..29b20aba3 100644 --- a/packages/ocap-kernel/src/index.ts +++ b/packages/ocap-kernel/src/index.ts @@ -4,6 +4,7 @@ export { VatSupervisor } from './vats/VatSupervisor.ts'; export { initTransport } from './remotes/platform/transport.ts'; export type { ClusterConfig, + DeliveryObject, KRef, Message, VatId, diff --git a/packages/ocap-kernel/src/types.ts b/packages/ocap-kernel/src/types.ts index 62640f7b2..fbb040565 100644 --- a/packages/ocap-kernel/src/types.ts +++ b/packages/ocap-kernel/src/types.ts @@ -5,7 +5,6 @@ import type { VatSyscallResult, VatSyscallSend, VatOneResolution, - VatDeliveryObject, } from '@agoric/swingset-liveslots'; import type { CapData } from '@endo/marshal'; import type { VatCheckpoint } from '@metamask/kernel-store'; @@ -88,6 +87,21 @@ export function coerceMessage(message: SwingsetMessage): Message { return message as Message; } +/** + * Delivery object using our Message type. + * + * This is equivalent to VatDeliveryObject from swingset-liveslots for JSON + * serialization purposes. The only difference is Message.result typing: + * ours is `result?: string | null`, theirs is `result: string | null | undefined`. + */ +export type DeliveryObject = + | ['message', VRef, Message] + | ['notify', VatOneResolution[]] + | ['dropExports', VRef[]] + | ['retireExports', VRef[]] + | ['retireImports', VRef[]] + | ['bringOutYourDead']; + type JsonVatSyscallObject = | Exclude | ['send', string, Message]; @@ -483,7 +497,7 @@ export type SystemVatSyscallHandler = ( * The runtime provides this to the kernel so deliveries can be routed correctly. */ export type SystemVatDeliverFn = ( - delivery: VatDeliveryObject, + delivery: DeliveryObject, ) => Promise; /** diff --git a/packages/ocap-kernel/src/vats/BaseVatHandle.ts b/packages/ocap-kernel/src/vats/BaseVatHandle.ts index f6530a6b6..7dc208a3f 100644 --- a/packages/ocap-kernel/src/vats/BaseVatHandle.ts +++ b/packages/ocap-kernel/src/vats/BaseVatHandle.ts @@ -1,19 +1,14 @@ import type { VatOneResolution } from '@agoric/swingset-liveslots'; -import type { CrankResults, Message, VRef, EndpointHandle } from '../types.ts'; +import type { + CrankResults, + DeliveryObject, + EndpointHandle, + Message, + VRef, +} from '../types.ts'; import type { VatSyscall } from './VatSyscall.ts'; -/** - * Delivery object type using our Message type (with optional result). - */ -export type DeliveryObject = - | ['message', VRef, Message] - | ['notify', VatOneResolution[]] - | ['dropExports', VRef[]] - | ['retireExports', VRef[]] - | ['retireImports', VRef[]] - | ['bringOutYourDead']; - /** * Function type for delivering messages to a vat. * diff --git a/packages/ocap-kernel/src/vats/SystemVatHandle.test.ts b/packages/ocap-kernel/src/vats/SystemVatHandle.test.ts index 05af26abd..7c14c2948 100644 --- a/packages/ocap-kernel/src/vats/SystemVatHandle.test.ts +++ b/packages/ocap-kernel/src/vats/SystemVatHandle.test.ts @@ -1,14 +1,11 @@ -import type { - VatDeliveryObject, - VatOneResolution, -} from '@agoric/swingset-liveslots'; +import type { VatOneResolution } from '@agoric/swingset-liveslots'; import type { Logger } from '@metamask/logger'; import type { MockInstance } from 'vitest'; import { describe, it, expect, vi, beforeEach } from 'vitest'; import type { KernelQueue } from '../KernelQueue.ts'; import type { KernelStore } from '../store/index.ts'; -import type { Message, SystemVatId, VRef } from '../types.ts'; +import type { DeliveryObject, Message, SystemVatId, VRef } from '../types.ts'; import type { SystemVatDeliverFn } from './SystemVatHandle.ts'; import { SystemVatHandle } from './SystemVatHandle.ts'; @@ -91,7 +88,7 @@ describe('SystemVatHandle', () => { ]); }); - it('converts undefined result to null', async () => { + it('passes message without result property as-is', async () => { const target: VRef = 'o+0'; const message: Message = { methargs: { body: '["test"]', slots: [] }, @@ -102,7 +99,7 @@ describe('SystemVatHandle', () => { expect(deliver).toHaveBeenCalledWith([ 'message', target, - { methargs: message.methargs, result: null }, + { methargs: message.methargs }, ]); }); @@ -211,7 +208,7 @@ describe('SystemVatHandle', () => { systemVatId, kernelStore: illegalSyscallKernelStore, kernelQueue, - deliver: vi.fn().mockImplementation(async (del: VatDeliveryObject) => { + deliver: vi.fn().mockImplementation(async (del: DeliveryObject) => { // Simulate the vat making a syscall during delivery if (del[0] === 'message') { handle.getSyscallHandler()([ diff --git a/packages/ocap-kernel/src/vats/SystemVatHandle.ts b/packages/ocap-kernel/src/vats/SystemVatHandle.ts index 22da17098..a4cfdc1c0 100644 --- a/packages/ocap-kernel/src/vats/SystemVatHandle.ts +++ b/packages/ocap-kernel/src/vats/SystemVatHandle.ts @@ -1,23 +1,20 @@ import type { - VatDeliveryObject, VatSyscallObject, VatSyscallResult, - Message as SwingSetMessage, } from '@agoric/swingset-liveslots'; import type { Logger } from '@metamask/logger'; import type { KernelQueue } from '../KernelQueue.ts'; import type { KernelStore } from '../store/index.ts'; -import type { SystemVatId } from '../types.ts'; +import type { DeliveryObject, SystemVatId } from '../types.ts'; import { BaseVatHandle } from './BaseVatHandle.ts'; -import type { DeliveryObject } from './BaseVatHandle.ts'; import { VatSyscall } from './VatSyscall.ts'; /** * Delivery callback type - called by kernel to deliver messages to the system vat. */ export type SystemVatDeliverFn = ( - delivery: VatDeliveryObject, + delivery: DeliveryObject, ) => Promise; /** @@ -78,20 +75,8 @@ export class SystemVatHandle extends BaseVatHandle { this.systemVatId = systemVatId; - // Set up deliver function that coerces Message to SwingSetMessage and hardens - this.deliver = async (delivery: DeliveryObject): Promise => { - let coercedDelivery: VatDeliveryObject; - if (delivery[0] === 'message') { - const [, target, message] = delivery; - const swingSetMessage: SwingSetMessage = { - methargs: message.methargs, - result: message.result ?? null, - }; - coercedDelivery = ['message', target, swingSetMessage]; - } else { - coercedDelivery = delivery; - } - return deliver(harden(coercedDelivery)); + this.deliver = async (delivery): Promise => { + return deliver(harden(delivery)); }; harden(this); diff --git a/packages/ocap-kernel/src/vats/VatHandle.ts b/packages/ocap-kernel/src/vats/VatHandle.ts index 289c2c4a4..2dad89c6d 100644 --- a/packages/ocap-kernel/src/vats/VatHandle.ts +++ b/packages/ocap-kernel/src/vats/VatHandle.ts @@ -17,9 +17,13 @@ import { kser } from '../liveslots/kernel-marshal.ts'; import { vatMethodSpecs, vatSyscallHandlers } from '../rpc/index.ts'; import type { PingVatResult, VatMethod } from '../rpc/index.ts'; import type { KernelStore } from '../store/index.ts'; -import type { VatId, VatConfig, VatDeliveryResult } from '../types.ts'; +import type { + DeliveryObject, + VatConfig, + VatDeliveryResult, + VatId, +} from '../types.ts'; import { BaseVatHandle } from './BaseVatHandle.ts'; -import type { DeliveryObject } from './BaseVatHandle.ts'; import { VatSyscall } from './VatSyscall.ts'; type MessageFromVat = JsonRpcResponse | JsonRpcNotification; diff --git a/packages/ocap-kernel/src/vats/index.ts b/packages/ocap-kernel/src/vats/index.ts index 7c0ec1ba0..f34386dc5 100644 --- a/packages/ocap-kernel/src/vats/index.ts +++ b/packages/ocap-kernel/src/vats/index.ts @@ -1,8 +1,4 @@ -export { - BaseVatHandle, - type DeliverFn, - type DeliveryObject, -} from './BaseVatHandle.ts'; +export { BaseVatHandle, type DeliverFn } from './BaseVatHandle.ts'; export { SystemVatSupervisor } from './SystemVatSupervisor.ts'; export type { SystemVatExecuteSyscall, From a5b96f10a6a504f41f3d6817de1c7636fce6a70e Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Fri, 30 Jan 2026 11:35:18 -0800 Subject: [PATCH 31/41] feat(kernel-browser-runtime): Add cross-process system vat supervisor Replace host-subcluster with host-vat pattern that supports system vat supervisors running in a different process than the kernel. The kernel runs in a Worker, the host vat supervisor runs in the background script, and they communicate over a stream using the optimistic syscall model. Key changes: - Add makeKernelHostVat() for kernel Worker side - Add makeBackgroundHostVat() for background script side - Define typed messages for kernel-supervisor communication - Update kernel-worker to use the new transport pattern - Delete obsolete host-subcluster and kernel-host-vat modules Co-Authored-By: Claude Opus 4.5 --- packages/kernel-browser-runtime/package.json | 3 +- .../src/host-subcluster/index.ts | 15 - .../make-host-subcluster.test.ts | 166 -------- .../host-subcluster/make-host-subcluster.ts | 49 --- .../src/host-subcluster/types.ts | 44 --- .../src/host-vat/index.ts | 19 + .../src/host-vat/kernel-side.test.ts | 340 +++++++++++++++++ .../src/host-vat/kernel-side.ts | 194 ++++++++++ .../src/host-vat/supervisor-side.test.ts | 361 ++++++++++++++++++ .../src/host-vat/supervisor-side.ts | 210 ++++++++++ .../src/host-vat/transport.ts | 17 + .../kernel-browser-runtime/src/index.test.ts | 3 +- packages/kernel-browser-runtime/src/index.ts | 2 +- .../src/kernel-worker/kernel-host-vat.test.ts | 100 ----- .../src/kernel-worker/kernel-host-vat.ts | 144 ------- .../src/kernel-worker/kernel-worker.ts | 100 ++--- yarn.lock | 3 +- 17 files changed, 1180 insertions(+), 590 deletions(-) delete mode 100644 packages/kernel-browser-runtime/src/host-subcluster/index.ts delete mode 100644 packages/kernel-browser-runtime/src/host-subcluster/make-host-subcluster.test.ts delete mode 100644 packages/kernel-browser-runtime/src/host-subcluster/make-host-subcluster.ts delete mode 100644 packages/kernel-browser-runtime/src/host-subcluster/types.ts create mode 100644 packages/kernel-browser-runtime/src/host-vat/index.ts create mode 100644 packages/kernel-browser-runtime/src/host-vat/kernel-side.test.ts create mode 100644 packages/kernel-browser-runtime/src/host-vat/kernel-side.ts create mode 100644 packages/kernel-browser-runtime/src/host-vat/supervisor-side.test.ts create mode 100644 packages/kernel-browser-runtime/src/host-vat/supervisor-side.ts create mode 100644 packages/kernel-browser-runtime/src/host-vat/transport.ts delete mode 100644 packages/kernel-browser-runtime/src/kernel-worker/kernel-host-vat.test.ts delete mode 100644 packages/kernel-browser-runtime/src/kernel-worker/kernel-host-vat.ts diff --git a/packages/kernel-browser-runtime/package.json b/packages/kernel-browser-runtime/package.json index a1bc8231e..ecb7d4c22 100644 --- a/packages/kernel-browser-runtime/package.json +++ b/packages/kernel-browser-runtime/package.json @@ -64,8 +64,10 @@ "test:dev:quiet": "yarn test:dev --reporter @ocap/repo-tools/vitest-reporters/silent" }, "dependencies": { + "@agoric/swingset-liveslots": "0.10.3-u21.0.1", "@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:^", @@ -84,7 +86,6 @@ }, "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/host-subcluster/index.ts b/packages/kernel-browser-runtime/src/host-subcluster/index.ts deleted file mode 100644 index 2d8c14fc2..000000000 --- a/packages/kernel-browser-runtime/src/host-subcluster/index.ts +++ /dev/null @@ -1,15 +0,0 @@ -/** - * Host subcluster utilities for browser runtime. - * - * The host subcluster enables the background script to use E() on vat object - * presences directly, replacing CapTP. The background becomes the bootstrap - * vat of a system subcluster and receives a kernel facet as a vatpower. - */ - -export type { - HostSubclusterConfig, - HostSubclusterResult, - HostSubclusterVat, -} from './types.ts'; - -export { makeHostSubcluster } from './make-host-subcluster.ts'; diff --git a/packages/kernel-browser-runtime/src/host-subcluster/make-host-subcluster.test.ts b/packages/kernel-browser-runtime/src/host-subcluster/make-host-subcluster.test.ts deleted file mode 100644 index 3aa07da56..000000000 --- a/packages/kernel-browser-runtime/src/host-subcluster/make-host-subcluster.test.ts +++ /dev/null @@ -1,166 +0,0 @@ -import type { Kernel, SystemSubclusterConfig } from '@metamask/ocap-kernel'; -import { describe, it, expect, vi, beforeEach } from 'vitest'; - -import { makeHostSubcluster } from './make-host-subcluster.ts'; -import type { HostSubclusterConfig } from './types.ts'; - -describe('makeHostSubcluster', () => { - let kernel: Kernel; - const buildRootObject = vi.fn(() => ({ test: () => 'test' })); - - beforeEach(() => { - vi.clearAllMocks(); - - kernel = { - launchSystemSubcluster: vi.fn().mockResolvedValue({ - systemSubclusterId: 'ss0', - vatIds: { testVat: 'sv0' }, - }), - } as unknown as Kernel; - }); - - it('calls kernel.launchSystemSubcluster with converted config', async () => { - const config: HostSubclusterConfig = { - bootstrap: 'testVat', - vats: { - testVat: { buildRootObject }, - }, - }; - - await makeHostSubcluster({ kernel, config }); - - expect(kernel.launchSystemSubcluster).toHaveBeenCalledWith({ - bootstrap: 'testVat', - vats: { - testVat: { buildRootObject }, - }, - }); - }); - - it('returns systemSubclusterId and vatIds', async () => { - const config: HostSubclusterConfig = { - bootstrap: 'testVat', - vats: { - testVat: { buildRootObject }, - }, - }; - - const result = await makeHostSubcluster({ kernel, config }); - - expect(result.systemSubclusterId).toBe('ss0'); - expect(result.vatIds).toStrictEqual({ testVat: 'sv0' }); - }); - - it('converts multiple vats', async () => { - const config: HostSubclusterConfig = { - bootstrap: 'bootstrap', - vats: { - bootstrap: { buildRootObject }, - worker: { buildRootObject }, - }, - }; - - await makeHostSubcluster({ kernel, config }); - - const calledConfig = ( - kernel.launchSystemSubcluster as ReturnType - ).mock.calls[0][0] as SystemSubclusterConfig; - expect(calledConfig.vats.bootstrap).toBeDefined(); - expect(calledConfig.vats.worker).toBeDefined(); - }); - - it('includes parameters when provided', async () => { - const config: HostSubclusterConfig = { - bootstrap: 'testVat', - vats: { - testVat: { - buildRootObject, - parameters: { key: 'value' }, - }, - }, - }; - - await makeHostSubcluster({ kernel, config }); - - const calledConfig = ( - kernel.launchSystemSubcluster as ReturnType - ).mock.calls[0][0] as SystemSubclusterConfig; - expect(calledConfig.vats.testVat?.parameters).toStrictEqual({ - key: 'value', - }); - }); - - it('omits parameters when undefined', async () => { - const config: HostSubclusterConfig = { - bootstrap: 'testVat', - vats: { - testVat: { buildRootObject }, - }, - }; - - await makeHostSubcluster({ kernel, config }); - - const calledConfig = ( - kernel.launchSystemSubcluster as ReturnType - ).mock.calls[0][0] as SystemSubclusterConfig; - expect( - Object.prototype.hasOwnProperty.call( - calledConfig.vats.testVat, - 'parameters', - ), - ).toBe(false); - }); - - it('includes services when provided', async () => { - const config: HostSubclusterConfig = { - bootstrap: 'testVat', - vats: { - testVat: { buildRootObject }, - }, - services: ['platformService'], - }; - - await makeHostSubcluster({ kernel, config }); - - const calledConfig = ( - kernel.launchSystemSubcluster as ReturnType - ).mock.calls[0][0] as SystemSubclusterConfig; - expect(calledConfig.services).toStrictEqual(['platformService']); - }); - - it('omits services when undefined', async () => { - const config: HostSubclusterConfig = { - bootstrap: 'testVat', - vats: { - testVat: { buildRootObject }, - }, - }; - - await makeHostSubcluster({ kernel, config }); - - const calledConfig = ( - kernel.launchSystemSubcluster as ReturnType - ).mock.calls[0][0] as SystemSubclusterConfig; - expect(Object.prototype.hasOwnProperty.call(calledConfig, 'services')).toBe( - false, - ); - }); - - it('propagates errors from kernel.launchSystemSubcluster', async () => { - const error = new Error('Launch failed'); - ( - kernel.launchSystemSubcluster as ReturnType - ).mockRejectedValueOnce(error); - - const config: HostSubclusterConfig = { - bootstrap: 'testVat', - vats: { - testVat: { buildRootObject }, - }, - }; - - await expect(makeHostSubcluster({ kernel, config })).rejects.toThrow( - 'Launch failed', - ); - }); -}); diff --git a/packages/kernel-browser-runtime/src/host-subcluster/make-host-subcluster.ts b/packages/kernel-browser-runtime/src/host-subcluster/make-host-subcluster.ts deleted file mode 100644 index 1470f4067..000000000 --- a/packages/kernel-browser-runtime/src/host-subcluster/make-host-subcluster.ts +++ /dev/null @@ -1,49 +0,0 @@ -import type { SystemSubclusterConfig } from '@metamask/ocap-kernel'; - -import type { - HostSubclusterResult, - MakeHostSubclusterOptions, -} from './types.ts'; - -/** - * Create and launch the host subcluster. - * - * The host subcluster is a system subcluster that runs in the host process - * (e.g., the browser extension background script). The bootstrap vat receives - * a kernel facet as a vatpower, enabling it to launch dynamic subclusters and - * receive E()-callable presences. - * - * @param options - Configuration options. - * @param options.kernel - The kernel instance. - * @param options.config - Configuration for the host subcluster. - * @returns A promise for the launch result. - */ -export async function makeHostSubcluster( - options: MakeHostSubclusterOptions, -): Promise { - const { kernel, config } = options; - - // Convert HostSubclusterConfig to SystemSubclusterConfig - const systemConfig: SystemSubclusterConfig = { - bootstrap: config.bootstrap, - vats: {}, - ...(config.services !== undefined && { services: config.services }), - }; - - for (const [vatName, vatConfig] of Object.entries(config.vats)) { - systemConfig.vats[vatName] = { - buildRootObject: vatConfig.buildRootObject, - ...(vatConfig.parameters !== undefined && { - parameters: vatConfig.parameters, - }), - }; - } - - // Launch the system subcluster - const result = await kernel.launchSystemSubcluster(systemConfig); - - return { - systemSubclusterId: result.systemSubclusterId, - vatIds: result.vatIds as Record, - }; -} diff --git a/packages/kernel-browser-runtime/src/host-subcluster/types.ts b/packages/kernel-browser-runtime/src/host-subcluster/types.ts deleted file mode 100644 index 7386d1c60..000000000 --- a/packages/kernel-browser-runtime/src/host-subcluster/types.ts +++ /dev/null @@ -1,44 +0,0 @@ -import type { Kernel, SystemVatBuildRootObject } from '@metamask/ocap-kernel'; -import type { Json } from '@metamask/utils'; - -/** - * Configuration for a single vat within the host subcluster. - */ -export type HostSubclusterVat = { - /** Function to build the vat's root object. */ - buildRootObject: SystemVatBuildRootObject; - /** Optional parameters to pass to buildRootObject. */ - parameters?: Record; -}; - -/** - * Configuration for the host subcluster. - */ -export type HostSubclusterConfig = { - /** The name of the bootstrap vat (must exist in vats). */ - bootstrap: string; - /** Map of vat names to their configurations. */ - vats: Record; - /** Optional list of kernel service names to provide to the bootstrap vat. */ - services?: string[]; -}; - -/** - * Result of launching the host subcluster. - */ -export type HostSubclusterResult = { - /** The system subcluster ID. */ - systemSubclusterId: string; - /** Map of vat names to their system vat IDs. */ - vatIds: Record; -}; - -/** - * Options for creating the host subcluster. - */ -export type MakeHostSubclusterOptions = { - /** The kernel instance. */ - kernel: Kernel; - /** Configuration for the host subcluster. */ - config: HostSubclusterConfig; -}; diff --git a/packages/kernel-browser-runtime/src/host-vat/index.ts b/packages/kernel-browser-runtime/src/host-vat/index.ts new file mode 100644 index 000000000..9af6b81b4 --- /dev/null +++ b/packages/kernel-browser-runtime/src/host-vat/index.ts @@ -0,0 +1,19 @@ +/** + * Host vat utilities for cross-process system vat communication. + * + * The host vat enables a system vat supervisor to run in a different process + * than the kernel. The kernel runs in a Worker, and the supervisor runs in + * the background script. They communicate over a stream using the optimistic + * syscall model (fire-and-forget with ['ok', null]). + */ + +export { makeKernelHostVat } from './kernel-side.ts'; +export type { KernelHostVatResult } from './kernel-side.ts'; + +export { makeBackgroundHostVat } from './supervisor-side.ts'; +export type { BackgroundHostVatResult } from './supervisor-side.ts'; + +export type { + KernelToSupervisorMessage, + SupervisorToKernelMessage, +} from './transport.ts'; diff --git a/packages/kernel-browser-runtime/src/host-vat/kernel-side.test.ts b/packages/kernel-browser-runtime/src/host-vat/kernel-side.test.ts new file mode 100644 index 000000000..08f061a86 --- /dev/null +++ b/packages/kernel-browser-runtime/src/host-vat/kernel-side.test.ts @@ -0,0 +1,340 @@ +import type { DuplexStream } from '@metamask/streams'; +import { describe, it, expect, vi, beforeEach } from 'vitest'; + +import { makeKernelHostVat } from './kernel-side.ts'; +import type { + KernelToSupervisorMessage, + SupervisorToKernelMessage, +} from './transport.ts'; + +type TestStream = DuplexStream< + SupervisorToKernelMessage, + KernelToSupervisorMessage +>; + +const makeMockStream = () => { + const written: KernelToSupervisorMessage[] = []; + const messageHandlers: ((message: SupervisorToKernelMessage) => void)[] = []; + let drainResolver: (() => void) | null = null; + + const stream: TestStream = { + write: vi.fn(async (message: KernelToSupervisorMessage) => { + written.push(message); + return { done: false, value: undefined }; + }), + drain: vi.fn( + async (handler: (message: SupervisorToKernelMessage) => void) => { + messageHandlers.push(handler); + // Return a promise that resolves when test calls closeDrain() + return new Promise((resolve) => { + drainResolver = resolve; + }); + }, + ), + next: vi.fn(), + pipe: vi.fn(), + return: vi.fn(), + throw: vi.fn(), + end: vi.fn(), + [Symbol.asyncIterator]: vi.fn(() => stream), + }; + + return { + stream, + written, + // Simulate receiving a message from supervisor + receiveMessage: (message: SupervisorToKernelMessage) => { + for (const handler of messageHandlers) { + handler(message); + } + }, + closeDrain: () => { + drainResolver?.(); + }, + }; +}; + +describe('makeKernelHostVat', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + describe('config', () => { + it('returns config with default name', () => { + const result = makeKernelHostVat(); + expect(result.config.name).toBe('kernelHost'); + }); + + it('returns config with custom name', () => { + const result = makeKernelHostVat({ name: 'customHost' }); + expect(result.config.name).toBe('customHost'); + }); + + it('returns config with transport functions', () => { + const result = makeKernelHostVat(); + + expect(result.config.transport.deliver).toBeTypeOf('function'); + expect(result.config.transport.setSyscallHandler).toBeTypeOf('function'); + expect(result.config.transport.awaitConnection).toBeTypeOf('function'); + }); + }); + + describe('connect', () => { + it('sends connected message when stream is connected', async () => { + const result = makeKernelHostVat(); + const { stream, written } = makeMockStream(); + + result.connect(stream); + + // Allow async operations to complete + await vi.waitFor(() => { + expect(written).toContainEqual({ type: 'connected' }); + }); + }); + + it('starts draining the stream for messages', async () => { + const result = makeKernelHostVat(); + const { stream } = makeMockStream(); + + result.connect(stream); + + await vi.waitFor(() => { + expect(stream.drain).toHaveBeenCalled(); + }); + }); + }); + + describe('awaitConnection', () => { + it('resolves when ready message is received', async () => { + const result = makeKernelHostVat(); + const { stream, receiveMessage } = makeMockStream(); + + result.connect(stream); + + const connectionPromise = result.config.transport.awaitConnection(); + + // Simulate supervisor sending ready message + receiveMessage({ type: 'ready' }); + + expect(await connectionPromise).toBeUndefined(); + }); + + it('does not resolve before ready message', async () => { + const result = makeKernelHostVat(); + const { stream } = makeMockStream(); + + result.connect(stream); + + const connectionPromise = result.config.transport.awaitConnection(); + + // Check that promise is still pending using Promise.race + const PENDING = Symbol('pending'); + const status = await Promise.race([ + connectionPromise.then(() => 'resolved'), + new Promise((resolve) => setTimeout(() => resolve(PENDING), 10)), + ]); + + expect(status).toBe(PENDING); + }); + }); + + describe('deliver', () => { + it('sends delivery message over stream', async () => { + const result = makeKernelHostVat(); + const { stream, written, receiveMessage } = makeMockStream(); + + result.connect(stream); + + const delivery = [ + 'message', + 'o+0', + { methargs: { body: '', slots: [] } }, + ]; + const deliverPromise = result.config.transport.deliver( + delivery as unknown as Parameters< + typeof result.config.transport.deliver + >[0], + ); + + await vi.waitFor(() => { + const deliveryMsg = written.find((item) => item.type === 'delivery'); + expect(deliveryMsg).toBeDefined(); + }); + + // Verify the delivery message format + const deliveryMsg = written.find( + ( + item, + ): item is Extract => + item.type === 'delivery', + ); + expect(deliveryMsg?.delivery).toStrictEqual(delivery); + expect(deliveryMsg?.id).toBe('0'); + + // Simulate supervisor responding + receiveMessage({ type: 'delivery-result', id: '0', error: null }); + + expect(await deliverPromise).toBeNull(); + }); + + it('returns delivery error when supervisor reports error', async () => { + const result = makeKernelHostVat(); + const { stream, receiveMessage } = makeMockStream(); + + result.connect(stream); + + const delivery = [ + 'message', + 'o+0', + { methargs: { body: '', slots: [] } }, + ]; + const deliverPromise = result.config.transport.deliver( + delivery as unknown as Parameters< + typeof result.config.transport.deliver + >[0], + ); + + // Wait for delivery to be sent + await vi.waitFor(() => { + expect(stream.write).toHaveBeenCalled(); + }); + + // Simulate supervisor responding with error + receiveMessage({ + type: 'delivery-result', + id: '0', + error: 'Delivery failed', + }); + + expect(await deliverPromise).toBe('Delivery failed'); + }); + + it('increments delivery IDs', async () => { + const result = makeKernelHostVat(); + const { stream, written, receiveMessage } = makeMockStream(); + + result.connect(stream); + + const delivery = [ + 'message', + 'o+0', + { methargs: { body: '', slots: [] } }, + ]; + + // Send first delivery + const deliver1 = result.config.transport.deliver( + delivery as unknown as Parameters< + typeof result.config.transport.deliver + >[0], + ); + + await vi.waitFor(() => { + expect(written.filter((item) => item.type === 'delivery')).toHaveLength( + 1, + ); + }); + + // Send second delivery + const deliver2 = result.config.transport.deliver( + delivery as unknown as Parameters< + typeof result.config.transport.deliver + >[0], + ); + + await vi.waitFor(() => { + expect(written.filter((item) => item.type === 'delivery')).toHaveLength( + 2, + ); + }); + + // Check IDs + const deliveryMsgs = written.filter( + ( + item, + ): item is Extract => + item.type === 'delivery', + ); + expect(deliveryMsgs[0]?.id).toBe('0'); + expect(deliveryMsgs[1]?.id).toBe('1'); + + // Resolve both + receiveMessage({ type: 'delivery-result', id: '0', error: null }); + receiveMessage({ type: 'delivery-result', id: '1', error: null }); + + await Promise.all([deliver1, deliver2]); + }); + + it('throws if stream is not connected', async () => { + const result = makeKernelHostVat(); + + const delivery = [ + 'message', + 'o+0', + { methargs: { body: '', slots: [] } }, + ]; + + await expect( + result.config.transport.deliver( + delivery as unknown as Parameters< + typeof result.config.transport.deliver + >[0], + ), + ).rejects.toThrow('Stream not connected'); + }); + }); + + describe('syscall handling', () => { + it('calls syscall handler when syscall message is received', async () => { + const result = makeKernelHostVat(); + const { stream, receiveMessage } = makeMockStream(); + const syscallHandler = vi.fn().mockReturnValue(['ok', null]); + + result.config.transport.setSyscallHandler(syscallHandler); + result.connect(stream); + + const syscall = ['send', 'ko1', { methargs: { body: '', slots: [] } }]; + receiveMessage({ type: 'syscall', syscall: syscall as never }); + + expect(syscallHandler).toHaveBeenCalledWith(syscall); + }); + + it('ignores syscall if handler is not set', async () => { + const mockLogger = { warn: vi.fn() }; + const result = makeKernelHostVat({ logger: mockLogger as never }); + const { stream, receiveMessage } = makeMockStream(); + + // Don't set syscall handler + result.connect(stream); + + const syscall = ['send', 'ko1', { methargs: { body: '', slots: [] } }]; + + // Should not throw + expect(() => { + receiveMessage({ type: 'syscall', syscall: syscall as never }); + }).not.toThrow(); + + expect(mockLogger.warn).toHaveBeenCalledWith( + 'Received syscall before handler was set', + ); + }); + + it('logs error if syscall handler throws', async () => { + const mockLogger = { error: vi.fn() }; + const result = makeKernelHostVat({ logger: mockLogger as never }); + const { stream, receiveMessage } = makeMockStream(); + + const error = new Error('Syscall failed'); + const syscallHandler = vi.fn().mockImplementation(() => { + throw error; + }); + + result.config.transport.setSyscallHandler(syscallHandler); + result.connect(stream); + + const syscall = ['send', 'ko1', { methargs: { body: '', slots: [] } }]; + receiveMessage({ type: 'syscall', syscall: syscall as never }); + + expect(mockLogger.error).toHaveBeenCalledWith('Syscall error:', error); + }); + }); +}); diff --git a/packages/kernel-browser-runtime/src/host-vat/kernel-side.ts b/packages/kernel-browser-runtime/src/host-vat/kernel-side.ts new file mode 100644 index 000000000..7df6e5b96 --- /dev/null +++ b/packages/kernel-browser-runtime/src/host-vat/kernel-side.ts @@ -0,0 +1,194 @@ +import type { PromiseKit } from '@endo/promise-kit'; +import { makePromiseKit } from '@endo/promise-kit'; +import type { Logger } from '@metamask/logger'; +import type { + DeliveryObject, + SystemVatConfig, + SystemVatSyscallHandler, + SystemVatTransport, +} from '@metamask/ocap-kernel'; +import type { DuplexStream } from '@metamask/streams'; + +import type { + KernelToSupervisorMessage, + SupervisorToKernelMessage, +} from './transport.ts'; + +/** + * Result of creating a kernel-side host vat. + */ +export type KernelHostVatResult = { + /** + * Configuration to pass to Kernel.make() hostVat option. + */ + config: SystemVatConfig; + + /** + * Connect the stream after kernel is created. + * Call this to wire up communication with the supervisor. + * + * @param stream - The duplex stream to communicate with the supervisor. + */ + connect: ( + stream: DuplexStream, + ) => void; +}; + +/** + * Create a kernel-side host vat for use with Kernel.make(). + * + * This creates the transport configuration needed for the kernel to communicate + * with a system vat supervisor running in a different process (e.g., browser + * background script). + * + * The transport uses an optimistic syscall model where syscalls are fire-and-forget, + * returning ['ok', null] immediately. The kernel handles failures by terminating + * the vat and rolling back the crank. + * + * Usage in kernel Worker: + * ```typescript + * const hostVat = makeKernelHostVat({ logger }); + * const kernel = await Kernel.make(platformServices, db, { + * hostVat: hostVat.config, + * }); + * const stream = await createHostVatStream(); // e.g., BroadcastChannel + * hostVat.connect(stream); + * ``` + * + * @param options - Options for creating the host vat. + * @param options.name - Optional name for the host vat (default: 'kernelHost'). + * @param options.logger - Optional logger for debugging. + * @returns The host vat result with config and connect function. + */ +export function makeKernelHostVat(options?: { + name?: string; + logger?: Logger; +}): KernelHostVatResult { + const vatName = options?.name ?? 'kernelHost'; + const logger = options?.logger; + + // Syscall handler - set by kernel during registerSystemVat() + let syscallHandler: SystemVatSyscallHandler | null = null; + + // Promise kit to signal when supervisor is ready + const supervisorReady = makePromiseKit(); + + // Pending deliveries waiting for results + const pendingDeliveries = new Map>(); + let deliveryCounter = 0; + + // Stream for communication - set when connect() is called + let stream: DuplexStream< + SupervisorToKernelMessage, + KernelToSupervisorMessage + > | null = null; + + /** + * Deliver a message to the supervisor over the stream. + * + * @param delivery - The delivery object to send. + * @returns A promise that resolves to the delivery error (null if success). + */ + const deliver = async (delivery: DeliveryObject): Promise => { + if (!stream) { + throw new Error('Stream not connected'); + } + + const id = String(deliveryCounter); + deliveryCounter += 1; + + const resultKit = makePromiseKit(); + pendingDeliveries.set(id, resultKit); + + await stream.write({ type: 'delivery', delivery, id }); + + return resultKit.promise; + }; + + /** + * Handle incoming messages from the supervisor. + * + * @param message - The message from the supervisor. + */ + const handleMessage = (message: SupervisorToKernelMessage): void => { + switch (message.type) { + case 'ready': + supervisorReady.resolve(); + break; + + case 'syscall': + if (!syscallHandler) { + logger?.warn('Received syscall before handler was set'); + return; + } + // Process syscall synchronously - the result is ignored because + // the supervisor uses optimistic execution + try { + syscallHandler(message.syscall); + } catch (error) { + // Syscall errors are handled by the kernel (vat termination) + logger?.error('Syscall error:', error); + } + break; + + case 'delivery-result': { + const pending = pendingDeliveries.get(message.id); + if (pending) { + pendingDeliveries.delete(message.id); + pending.resolve(message.error); + } else { + logger?.warn(`Received result for unknown delivery: ${message.id}`); + } + break; + } + + default: + logger?.warn( + `Unknown message type: ${(message as { type: string }).type}`, + ); + } + }; + + const transport: SystemVatTransport = { + deliver, + setSyscallHandler: (handler: SystemVatSyscallHandler) => { + syscallHandler = handler; + }, + awaitConnection: async () => supervisorReady.promise, + }; + + const config: SystemVatConfig = { + name: vatName, + transport, + }; + + const connect = ( + connectedStream: DuplexStream< + SupervisorToKernelMessage, + KernelToSupervisorMessage + >, + ): void => { + stream = connectedStream; + + // Start draining the stream for incoming messages + stream.drain(handleMessage).catch((error) => { + logger?.error('Stream error:', error); + // Reject any pending deliveries + for (const pending of pendingDeliveries.values()) { + pending.reject(error as Error); + } + pendingDeliveries.clear(); + }); + + // Send connected message to supervisor + stream.write({ type: 'connected' }).catch((error) => { + logger?.error('Failed to send connected message:', error); + }); + }; + + return harden({ + config, + connect, + }); +} +harden(makeKernelHostVat); diff --git a/packages/kernel-browser-runtime/src/host-vat/supervisor-side.test.ts b/packages/kernel-browser-runtime/src/host-vat/supervisor-side.test.ts new file mode 100644 index 000000000..1ffdc9e4c --- /dev/null +++ b/packages/kernel-browser-runtime/src/host-vat/supervisor-side.test.ts @@ -0,0 +1,361 @@ +import { SystemVatSupervisor } from '@metamask/ocap-kernel/vats'; +import type { DuplexStream } from '@metamask/streams'; +import { describe, it, expect, vi, beforeEach } from 'vitest'; + +import { makeBackgroundHostVat } from './supervisor-side.ts'; +import type { + KernelToSupervisorMessage, + SupervisorToKernelMessage, +} from './transport.ts'; + +// Import after mock + +// Mock SystemVatSupervisor +vi.mock('@metamask/ocap-kernel/vats', () => ({ + SystemVatSupervisor: { + make: vi.fn(), + }, +})); + +type TestStream = DuplexStream< + KernelToSupervisorMessage, + SupervisorToKernelMessage +>; + +const makeMockStream = () => { + const written: SupervisorToKernelMessage[] = []; + const messageHandlers: (( + message: KernelToSupervisorMessage, + ) => void | Promise)[] = []; + let drainResolver: (() => void) | null = null; + + const stream: TestStream = { + write: vi.fn(async (message: SupervisorToKernelMessage) => { + written.push(message); + return { done: false, value: undefined }; + }), + drain: vi.fn( + async ( + handler: (message: KernelToSupervisorMessage) => void | Promise, + ) => { + messageHandlers.push(handler); + // Return a promise that resolves when test calls closeDrain() + return new Promise((resolve) => { + drainResolver = resolve; + }); + }, + ), + next: vi.fn(), + pipe: vi.fn(), + return: vi.fn(), + throw: vi.fn(), + end: vi.fn(), + [Symbol.asyncIterator]: vi.fn(() => stream), + }; + + return { + stream, + written, + // Simulate receiving a message from kernel + receiveMessage: async (message: KernelToSupervisorMessage) => { + for (const handler of messageHandlers) { + await handler(message); + } + }, + closeDrain: () => { + drainResolver?.(); + }, + }; +}; + +const makeMockSupervisor = () => ({ + deliver: vi.fn().mockResolvedValue(null), + id: 'sv0' as const, +}); + +describe('makeBackgroundHostVat', () => { + let mockSupervisor: ReturnType; + + beforeEach(() => { + vi.clearAllMocks(); + mockSupervisor = makeMockSupervisor(); + vi.mocked(SystemVatSupervisor.make).mockResolvedValue( + mockSupervisor as never, + ); + }); + + describe('connect', () => { + it('creates supervisor with provided buildRootObject', async () => { + const buildRootObject = vi.fn().mockReturnValue({}); + const result = makeBackgroundHostVat({ buildRootObject }); + const { stream } = makeMockStream(); + + result.connect(stream); + + await vi.waitFor(() => { + expect(SystemVatSupervisor.make).toHaveBeenCalled(); + }); + + const makeCall = vi.mocked(SystemVatSupervisor.make).mock.calls[0]; + expect(makeCall?.[0]).toHaveProperty('buildRootObject'); + expect(makeCall?.[0]).toHaveProperty('executeSyscall'); + }); + + it('sends ready message after supervisor is created', async () => { + const buildRootObject = vi.fn().mockReturnValue({}); + const result = makeBackgroundHostVat({ buildRootObject }); + const { stream, written } = makeMockStream(); + + result.connect(stream); + + await vi.waitFor(() => { + expect(written).toContainEqual({ type: 'ready' }); + }); + }); + + it('starts draining stream after sending ready', async () => { + const buildRootObject = vi.fn().mockReturnValue({}); + const result = makeBackgroundHostVat({ buildRootObject }); + const { stream } = makeMockStream(); + + result.connect(stream); + + await vi.waitFor(() => { + expect(stream.drain).toHaveBeenCalled(); + }); + }); + }); + + describe('kernelFacetPromise', () => { + it('resolves when buildRootObject receives kernelFacet in vatPowers', async () => { + const mockKernelFacet = { launchSubcluster: vi.fn() }; + let capturedBuildRootObject: + | ((vatPowers: Record) => object) + | null = null; + + vi.mocked(SystemVatSupervisor.make).mockImplementation( + async (options) => { + capturedBuildRootObject = + options.buildRootObject as typeof capturedBuildRootObject; + return mockSupervisor as never; + }, + ); + + const buildRootObject = vi.fn().mockReturnValue({}); + const result = makeBackgroundHostVat({ buildRootObject }); + const { stream } = makeMockStream(); + + result.connect(stream); + + await vi.waitFor(() => { + expect(capturedBuildRootObject).not.toBeNull(); + }); + + // Simulate liveslots calling buildRootObject with kernelFacet + capturedBuildRootObject?.({ kernelFacet: mockKernelFacet }); + + expect(await result.kernelFacetPromise).toBe(mockKernelFacet); + }); + + it('calls user buildRootObject with vatPowers', async () => { + let capturedBuildRootObject: + | (( + vatPowers: Record, + parameters: Record | undefined, + ) => object) + | null = null; + + vi.mocked(SystemVatSupervisor.make).mockImplementation( + async (options) => { + capturedBuildRootObject = + options.buildRootObject as typeof capturedBuildRootObject; + return mockSupervisor as never; + }, + ); + + const userBuildRootObject = vi + .fn() + .mockReturnValue({ myMethod: vi.fn() }); + const result = makeBackgroundHostVat({ + buildRootObject: userBuildRootObject, + }); + const { stream } = makeMockStream(); + + result.connect(stream); + + await vi.waitFor(() => { + expect(capturedBuildRootObject).not.toBeNull(); + }); + + const vatPowers = { kernelFacet: {}, otherPower: 'test' }; + const rootObject = capturedBuildRootObject?.(vatPowers, { + param: 'value', + }); + + expect(userBuildRootObject).toHaveBeenCalledWith(vatPowers, { + param: 'value', + }); + expect(rootObject).toHaveProperty('myMethod'); + }); + }); + + describe('delivery handling', () => { + it('delivers to supervisor and sends result back', async () => { + const buildRootObject = vi.fn().mockReturnValue({}); + const result = makeBackgroundHostVat({ buildRootObject }); + const { stream, written, receiveMessage } = makeMockStream(); + + result.connect(stream); + + // Wait for supervisor to be ready + await vi.waitFor(() => { + expect(written).toContainEqual({ type: 'ready' }); + }); + + const delivery = [ + 'message', + 'o+0', + { methargs: { body: '', slots: [] } }, + ]; + + await receiveMessage({ + type: 'delivery', + delivery: delivery as never, + id: '123', + }); + + expect(mockSupervisor.deliver).toHaveBeenCalledWith(delivery); + expect(written).toContainEqual({ + type: 'delivery-result', + id: '123', + error: null, + }); + }); + + it('sends delivery error when supervisor.deliver returns error', async () => { + mockSupervisor.deliver.mockResolvedValue('Something went wrong'); + + const buildRootObject = vi.fn().mockReturnValue({}); + const result = makeBackgroundHostVat({ buildRootObject }); + const { stream, written, receiveMessage } = makeMockStream(); + + result.connect(stream); + + await vi.waitFor(() => { + expect(written).toContainEqual({ type: 'ready' }); + }); + + const delivery = [ + 'message', + 'o+0', + { methargs: { body: '', slots: [] } }, + ]; + + await receiveMessage({ + type: 'delivery', + delivery: delivery as never, + id: '456', + }); + + expect(written).toContainEqual({ + type: 'delivery-result', + id: '456', + error: 'Something went wrong', + }); + }); + + it('handles connected message from kernel', async () => { + const mockLogger = { + debug: vi.fn(), + subLogger: vi.fn(() => mockLogger), + }; + const buildRootObject = vi.fn().mockReturnValue({}); + const result = makeBackgroundHostVat({ + buildRootObject, + logger: mockLogger as never, + }); + const { stream, written, receiveMessage } = makeMockStream(); + + result.connect(stream); + + await vi.waitFor(() => { + expect(written).toContainEqual({ type: 'ready' }); + }); + + await receiveMessage({ type: 'connected' }); + + expect(mockLogger.debug).toHaveBeenCalledWith( + 'Received connected message from kernel', + ); + }); + }); + + describe('syscall execution', () => { + it('sends syscall over stream with coerced object', async () => { + let capturedExecuteSyscall: ((vso: unknown) => unknown) | null = null; + + vi.mocked(SystemVatSupervisor.make).mockImplementation( + async (options) => { + capturedExecuteSyscall = + options.executeSyscall as typeof capturedExecuteSyscall; + return mockSupervisor as never; + }, + ); + + const buildRootObject = vi.fn().mockReturnValue({}); + const result = makeBackgroundHostVat({ buildRootObject }); + const { stream, written } = makeMockStream(); + + result.connect(stream); + + await vi.waitFor(() => { + expect(capturedExecuteSyscall).not.toBeNull(); + }); + + const syscall = [ + 'send', + 'ko1', + { methargs: { body: '#{}', slots: [] }, result: 'kp1' }, + ]; + const syscallResult = capturedExecuteSyscall?.(syscall); + + // Should return success immediately (optimistic) + expect(syscallResult).toStrictEqual(['ok', null]); + + await vi.waitFor(() => { + expect(written).toContainEqual({ + type: 'syscall', + syscall, + }); + }); + }); + + it('returns ok immediately without waiting for response', async () => { + let capturedExecuteSyscall: ((vso: unknown) => unknown) | null = null; + + vi.mocked(SystemVatSupervisor.make).mockImplementation( + async (options) => { + capturedExecuteSyscall = + options.executeSyscall as typeof capturedExecuteSyscall; + return mockSupervisor as never; + }, + ); + + const buildRootObject = vi.fn().mockReturnValue({}); + const result = makeBackgroundHostVat({ buildRootObject }); + const { stream } = makeMockStream(); + + result.connect(stream); + + await vi.waitFor(() => { + expect(capturedExecuteSyscall).not.toBeNull(); + }); + + const syscall = ['subscribe', 'kp1']; + const syscallResult = capturedExecuteSyscall?.(syscall); + + // Result is synchronous, not a promise + expect(syscallResult).toStrictEqual(['ok', null]); + }); + }); +}); diff --git a/packages/kernel-browser-runtime/src/host-vat/supervisor-side.ts b/packages/kernel-browser-runtime/src/host-vat/supervisor-side.ts new file mode 100644 index 000000000..cefc9119f --- /dev/null +++ b/packages/kernel-browser-runtime/src/host-vat/supervisor-side.ts @@ -0,0 +1,210 @@ +import type { + VatDeliveryObject, + VatSyscallObject, + VatSyscallResult, +} from '@agoric/swingset-liveslots'; +import { makePromiseKit } from '@endo/promise-kit'; +import type { Logger } from '@metamask/logger'; +import type { + KernelFacet, + SystemVatBuildRootObject, +} from '@metamask/ocap-kernel'; +import { SystemVatSupervisor } from '@metamask/ocap-kernel/vats'; +import type { DuplexStream } from '@metamask/streams'; + +import type { + KernelToSupervisorMessage, + SupervisorToKernelMessage, +} from './transport.ts'; + +/** + * Result of creating a background-side host vat. + */ +export type BackgroundHostVatResult = { + /** + * Connect and start the supervisor. + * Call this with the stream to the kernel Worker. + * + * @param stream - The duplex stream to communicate with the kernel. + */ + connect: ( + stream: DuplexStream, + ) => void; + + /** + * Promise that resolves to kernelFacet when bootstrap completes. + * No polling needed - just await this promise after calling connect(). + */ + kernelFacetPromise: Promise; +}; + +/** + * Create a background-side host vat for use in the browser background script. + * + * This creates a supervisor that communicates with the kernel over a stream. + * The supervisor uses an optimistic syscall model where syscalls are fire-and-forget, + * returning ['ok', null] immediately. + * + * Usage in background script: + * ```typescript + * const hostVat = makeBackgroundHostVat({ + * buildRootObject: (vatPowers) => { + * const kernelFacet = vatPowers.kernelFacet as KernelFacet; + * return makeDefaultExo('BackgroundRoot', { + * // ... methods that use E(kernelFacet) + * }); + * }, + * logger, + * }); + * const stream = await connectToKernelHostVat(); + * hostVat.connect(stream); + * const kernelFacet = await hostVat.kernelFacetPromise; + * const result = await E(kernelFacet).launchSubcluster(config); + * ``` + * + * @param options - Options for creating the host vat. + * @param options.buildRootObject - Function to build the vat's root object. + * @param options.logger - Optional logger for debugging. + * @returns The host vat result with connect and kernelFacetPromise. + */ +export function makeBackgroundHostVat(options: { + buildRootObject: SystemVatBuildRootObject; + logger?: Logger; +}): BackgroundHostVatResult { + const { buildRootObject, logger } = options; + + // Promise kit for kernel facet - resolves when bootstrap is called + const kernelFacetKit = makePromiseKit(); + + // Stream for communication - set when connect() is called + let stream: DuplexStream< + KernelToSupervisorMessage, + SupervisorToKernelMessage + > | null = null; + + // Supervisor instance - created when connect() is called + let supervisor: SystemVatSupervisor | null = null; + + /** + * Execute a syscall by sending it to the kernel. + * Uses optimistic execution - returns success immediately. + * + * @param vso - The syscall object to execute. + * @returns A syscall success result. + */ + const executeSyscall = (vso: VatSyscallObject): VatSyscallResult => { + if (!stream) { + throw new Error('Stream not connected'); + } + + // Send syscall notification (fire-and-forget) + // The syscall is sent as-is; structured clone handles serialization + stream.write({ type: 'syscall', syscall: vso }).catch((error) => { + logger?.error('Failed to send syscall:', error); + }); + + // Return success immediately (optimistic execution) + return ['ok', null]; + }; + + /** + * Wrap buildRootObject to capture the kernelFacet from bootstrap. + * + * @param vatPowers - The vat powers provided by liveslots. + * @param parameters - Optional parameters for the vat. + * @returns The root object for this vat. + */ + const wrappedBuildRootObject: SystemVatBuildRootObject = ( + vatPowers, + parameters, + ) => { + // Capture kernelFacet from vatPowers before passing to user's buildRootObject + if (vatPowers.kernelFacet) { + kernelFacetKit.resolve(vatPowers.kernelFacet as KernelFacet); + } + return buildRootObject(vatPowers, parameters); + }; + + /** + * Handle incoming messages from the kernel. + * + * @param message - The message from the kernel. + */ + const handleMessage = async ( + message: KernelToSupervisorMessage, + ): Promise => { + switch (message.type) { + case 'connected': + // Kernel acknowledges connection - nothing to do + logger?.debug('Received connected message from kernel'); + break; + + case 'delivery': { + if (!supervisor) { + logger?.error('Received delivery before supervisor was created'); + await stream?.write({ + type: 'delivery-result', + id: message.id, + error: 'Supervisor not ready', + }); + return; + } + + // Deliver to supervisor and send result back + // Cast from DeliveryObject (our JSON-safe type) to VatDeliveryObject + const deliveryError = await supervisor.deliver( + message.delivery as unknown as VatDeliveryObject, + ); + + await stream?.write({ + type: 'delivery-result', + id: message.id, + error: deliveryError, + }); + break; + } + + default: + logger?.warn( + `Unknown message type: ${(message as { type: string }).type}`, + ); + } + }; + + const connect = ( + connectedStream: DuplexStream< + KernelToSupervisorMessage, + SupervisorToKernelMessage + >, + ): void => { + stream = connectedStream; + + // Create and start the supervisor + const supervisorOptions = { + buildRootObject: wrappedBuildRootObject, + executeSyscall, + ...(logger && { logger: logger.subLogger({ tags: ['supervisor'] }) }), + }; + SystemVatSupervisor.make(supervisorOptions) + .then(async (createdSupervisor) => { + supervisor = createdSupervisor; + + // Signal to kernel that we're ready + return stream?.write({ type: 'ready' }); + }) + .then(async () => { + // Start draining the stream for incoming messages + return stream?.drain(handleMessage); + }) + .catch((error) => { + logger?.error('Supervisor initialization error:', error); + kernelFacetKit.reject(error as Error); + }); + }; + + return harden({ + connect, + kernelFacetPromise: kernelFacetKit.promise, + }); +} +harden(makeBackgroundHostVat); diff --git a/packages/kernel-browser-runtime/src/host-vat/transport.ts b/packages/kernel-browser-runtime/src/host-vat/transport.ts new file mode 100644 index 000000000..72a803493 --- /dev/null +++ b/packages/kernel-browser-runtime/src/host-vat/transport.ts @@ -0,0 +1,17 @@ +import type { VatSyscallObject } from '@agoric/swingset-liveslots'; +import type { DeliveryObject } from '@metamask/ocap-kernel'; + +/** + * Messages sent from kernel (Worker) to supervisor (background). + */ +export type KernelToSupervisorMessage = + | { type: 'delivery'; delivery: DeliveryObject; id: string } + | { type: 'connected' }; + +/** + * Messages sent from supervisor (background) to kernel (Worker). + */ +export type SupervisorToKernelMessage = + | { type: 'syscall'; syscall: VatSyscallObject } + | { type: 'delivery-result'; id: string; error: string | null } + | { type: 'ready' }; diff --git a/packages/kernel-browser-runtime/src/index.test.ts b/packages/kernel-browser-runtime/src/index.test.ts index e8496b2ce..aa3e0b786 100644 --- a/packages/kernel-browser-runtime/src/index.test.ts +++ b/packages/kernel-browser-runtime/src/index.test.ts @@ -13,9 +13,10 @@ describe('index', () => { 'getRelaysFromCurrentLocation', 'isCapTPNotification', 'makeBackgroundCapTP', + 'makeBackgroundHostVat', 'makeCapTPNotification', - 'makeHostSubcluster', 'makeIframeVatWorker', + 'makeKernelHostVat', 'parseRelayQueryString', 'receiveInternalConnections', 'rpcHandlers', diff --git a/packages/kernel-browser-runtime/src/index.ts b/packages/kernel-browser-runtime/src/index.ts index 24f1b0f56..7d2eca44d 100644 --- a/packages/kernel-browser-runtime/src/index.ts +++ b/packages/kernel-browser-runtime/src/index.ts @@ -1,5 +1,5 @@ export * from './rpc-handlers/index.ts'; -export * from './host-subcluster/index.ts'; +export * from './host-vat/index.ts'; export { connectToKernel, receiveInternalConnections, diff --git a/packages/kernel-browser-runtime/src/kernel-worker/kernel-host-vat.test.ts b/packages/kernel-browser-runtime/src/kernel-worker/kernel-host-vat.test.ts deleted file mode 100644 index d4ab415ec..000000000 --- a/packages/kernel-browser-runtime/src/kernel-worker/kernel-host-vat.test.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest'; - -import type { KernelHostRoot } from './kernel-host-vat.ts'; -import { makeKernelHostSubclusterConfig } from './kernel-host-vat.ts'; - -describe('makeKernelHostSubclusterConfig', () => { - const mockKernelFacet = { - launchSubcluster: vi.fn(), - terminateSubcluster: vi.fn(), - getStatus: vi.fn(), - reloadSubcluster: vi.fn(), - getSubcluster: vi.fn(), - getSubclusters: vi.fn(), - }; - - beforeEach(() => { - vi.clearAllMocks(); - }); - - it('returns a valid system subcluster config', () => { - const onRootCreated = vi.fn(); - const config = makeKernelHostSubclusterConfig(onRootCreated); - - expect(config.bootstrap).toBe('kernelHost'); - expect(config.vats.kernelHost).toBeDefined(); - expect(config.vats?.kernelHost?.buildRootObject).toBeTypeOf('function'); - }); - - it('invokes onRootCreated callback when buildRootObject is called', () => { - const onRootCreated = vi.fn(); - const config = makeKernelHostSubclusterConfig(onRootCreated); - - const root = config.vats?.kernelHost?.buildRootObject( - { - kernelFacet: mockKernelFacet, - }, - {}, - ); - - expect(onRootCreated).toHaveBeenCalledWith(root); - }); - - describe('kernel host root', () => { - let root: KernelHostRoot; - - beforeEach(() => { - const onRootCreated = vi.fn(); - const config = makeKernelHostSubclusterConfig(onRootCreated); - root = config.vats?.kernelHost?.buildRootObject( - { - kernelFacet: mockKernelFacet, - }, - {}, - ) as KernelHostRoot; - }); - - it('creates root with expected methods', () => { - expect(root.ping).toBeTypeOf('function'); - expect(root.launchSubcluster).toBeTypeOf('function'); - expect(root.terminateSubcluster).toBeTypeOf('function'); - expect(root.getStatus).toBeTypeOf('function'); - expect(root.reloadSubcluster).toBeTypeOf('function'); - expect(root.getSubcluster).toBeTypeOf('function'); - expect(root.getSubclusters).toBeTypeOf('function'); - }); - - it('ping returns pong', async () => { - const result = await root.ping(); - expect(result).toBe('pong'); - }); - - // Note: launchSubcluster, terminateSubcluster, getStatus, reloadSubcluster - // use E() which requires endo initialization. These are integration tested - // via the full system tests rather than unit tests. - - it('getSubcluster calls kernel facet synchronously', () => { - mockKernelFacet.getSubcluster.mockReturnValue({ - id: 's1', - config: { bootstrap: 'test', vats: {} }, - vats: {}, - }); - - const result = root.getSubcluster('s1'); - - expect(mockKernelFacet.getSubcluster).toHaveBeenCalledWith('s1'); - expect(result?.id).toBe('s1'); - }); - - it('getSubclusters calls kernel facet synchronously', () => { - mockKernelFacet.getSubclusters.mockReturnValue([ - { id: 's1', config: { bootstrap: 'test', vats: {} }, vats: {} }, - ]); - - const result = root.getSubclusters(); - - expect(mockKernelFacet.getSubclusters).toHaveBeenCalled(); - expect(result).toHaveLength(1); - }); - }); -}); diff --git a/packages/kernel-browser-runtime/src/kernel-worker/kernel-host-vat.ts b/packages/kernel-browser-runtime/src/kernel-worker/kernel-host-vat.ts deleted file mode 100644 index f1ecee673..000000000 --- a/packages/kernel-browser-runtime/src/kernel-worker/kernel-host-vat.ts +++ /dev/null @@ -1,144 +0,0 @@ -import { E } from '@endo/eventual-send'; -import { makeDefaultExo } from '@metamask/kernel-utils/exo'; -import type { - SystemVatBuildRootObject, - SystemSubclusterConfig, - ClusterConfig, - KernelStatus, - Subcluster, - KernelFacet, - KernelFacetLaunchResult, -} from '@metamask/ocap-kernel'; - -/** - * The kernel host vat's root object interface. - * - * This is the interface exposed by the kernel host vat to external clients - * (like the background service worker) via CapTP. - */ -export type KernelHostRoot = { - /** - * Ping the kernel host. - * - * @returns 'pong' to confirm the host is responsive. - */ - ping: () => Promise<'pong'>; - - /** - * Launch a dynamic subcluster. - * - * @param config - Configuration for the subcluster. - * @returns The launch result with subcluster ID and root presence. - */ - launchSubcluster: (config: ClusterConfig) => Promise; - - /** - * Terminate a subcluster. - * - * @param subclusterId - The ID of the subcluster to terminate. - */ - terminateSubcluster: (subclusterId: string) => Promise; - - /** - * Get kernel status. - * - * @returns The current kernel status. - */ - getStatus: () => Promise; - - /** - * Reload a subcluster. - * - * @param subclusterId - The ID of the subcluster to reload. - * @returns The reloaded subcluster. - */ - reloadSubcluster: (subclusterId: string) => Promise; - - /** - * Get a subcluster by ID. - * - * @param subclusterId - The ID of the subcluster. - * @returns The subcluster or undefined if not found. - */ - getSubcluster: (subclusterId: string) => Subcluster | undefined; - - /** - * Get all subclusters. - * - * @returns Array of all subclusters. - */ - getSubclusters: () => Subcluster[]; - - /** - * Convert a kref string to a presence. - * - * Use this to restore a presence from a stored kref string after restart. - * - * @param kref - The kref string to convert. - * @returns The presence for the given kref. - */ - getVatRoot: (kref: string) => unknown; -}; - -/** - * Create the configuration for launching the kernel host subcluster. - * - * @param onRootCreated - Callback invoked when the root object is created. - * @returns The system subcluster configuration. - */ -export function makeKernelHostSubclusterConfig( - onRootCreated: (root: KernelHostRoot) => void, -): SystemSubclusterConfig { - const buildRootObject: SystemVatBuildRootObject = (vatPowers) => { - const kernelFacet = vatPowers.kernelFacet as KernelFacet; - - const root = makeDefaultExo('KernelHostRoot', { - ping: async () => 'pong' as const, - - launchSubcluster: async (config: ClusterConfig) => { - // Use E() to call kernel facet - this gives us proper reference handling - return E(kernelFacet).launchSubcluster(config); - }, - - terminateSubcluster: async (subclusterId: string) => { - return E(kernelFacet).terminateSubcluster(subclusterId); - }, - - getStatus: async () => { - return E(kernelFacet).getStatus(); - }, - - reloadSubcluster: async (subclusterId: string) => { - return E(kernelFacet).reloadSubcluster(subclusterId); - }, - - getSubcluster: (subclusterId: string) => { - // Synchronous method - call directly - return kernelFacet.getSubcluster(subclusterId); - }, - - getSubclusters: () => { - // Synchronous method - call directly - return kernelFacet.getSubclusters(); - }, - - getVatRoot: async (kref: string) => { - // Convert kref to slot value, which becomes a presence via CapTP - return kernelFacet.getVatRoot(kref); - }, - }) as KernelHostRoot; - - // Capture the root object for external use (e.g., CapTP bootstrap) - onRootCreated(root); - - return root; - }; - - return { - bootstrap: 'kernelHost', - vats: { - kernelHost: { buildRootObject }, - }, - }; -} -harden(makeKernelHostSubclusterConfig); 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 fb1be1e11..af17f4709 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 { makeCapTP } from '@endo/captp'; +import { makePromiseKit } from '@endo/promise-kit'; import { JsonRpcServer } from '@metamask/json-rpc-engine/v2'; import { makeSQLKernelDatabase } from '@metamask/kernel-store/sqlite/wasm'; -import { isJsonRpcMessage, stringify } 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'; @@ -10,20 +10,19 @@ import { MessagePortDuplexStream, receiveMessagePort, } from '@metamask/streams/browser'; +import type { JsonRpcResponse } from '@metamask/utils'; -import { - isCapTPNotification, - makeCapTPNotification, -} from '../background-captp.ts'; -import type { CapTPMessage } from '../background-captp.ts'; +import { makeKernelHostVat } from '../host-vat/kernel-side.ts'; import { receiveInternalConnections } from '../internal-comms/internal-connections.ts'; import { PlatformServicesClient } from '../PlatformServicesClient.ts'; -import type { KernelHostRoot } from './kernel-host-vat.ts'; -import { makeKernelHostSubclusterConfig } from './kernel-host-vat.ts'; import { makeLoggingMiddleware } from './middleware/logging.ts'; import { makePanelMessageMiddleware } from './middleware/panel-message.ts'; import { getRelaysFromCurrentLocation } from '../utils/relay-query-string.ts'; +type HandleInternalMessage = ( + request: JsonRpcMessage, +) => Promise; + const logger = new Logger('kernel-worker'); const DB_FILENAME = 'store.db'; @@ -33,6 +32,13 @@ main().catch(logger.error); * Run the kernel. */ async function main(): Promise { + // Synchronously start listening for internal connections + const panelHandlerKit = makePromiseKit(); + receiveInternalConnections({ + handlerPromise: panelHandlerKit.promise, + logger, + }); + const port = await receiveMessagePort( (listener) => globalThis.addEventListener('message', listener), (listener) => globalThis.removeEventListener('message', listener), @@ -52,73 +58,31 @@ async function main(): Promise { new URLSearchParams(globalThis.location.search).get('reset-storage') === 'true'; - const kernelP = Kernel.make(platformServicesClient, kernelDatabase, { + const kernel = await Kernel.make(platformServicesClient, kernelDatabase, { resetStorage, }); - const handlerP = kernelP.then((kernel) => { - const server = new JsonRpcServer({ - middleware: [ - makeLoggingMiddleware(logger.subLogger('internal-rpc')), - makePanelMessageMiddleware(kernel, kernelDatabase), - ], - }); - return async (request: JsonRpcMessage) => server.handle(request); + const panelRpcServer = new JsonRpcServer({ + middleware: [ + makeLoggingMiddleware(logger.subLogger('internal-rpc')), + makePanelMessageMiddleware(kernel, kernelDatabase), + ], }); + panelHandlerKit.resolve(panelRpcServer.handle.bind(panelRpcServer)); - receiveInternalConnections({ - handlerPromise: handlerP, - logger, + const hostVat = makeKernelHostVat({ + name: 'kernelHost', + logger: logger.subLogger({ tags: ['host-vat'] }), }); - const kernel = await kernelP; - - // Launch the kernel host subcluster to get a proper system vat for CapTP - let kernelHostRoot: KernelHostRoot | undefined; - const hostSubclusterConfig = makeKernelHostSubclusterConfig((root) => { - kernelHostRoot = root; - }); - - try { - await kernel.launchSystemSubcluster(hostSubclusterConfig); - logger.log('Launched kernel host subcluster'); - } catch (error) { - logger.error('Failed to launch kernel host subcluster:', error); - throw error; - } - - if (!kernelHostRoot) { - throw new Error('Kernel host root was not captured during launch'); - } - - // Create CapTP with the kernel host vat root as the bootstrap - // This gives background proper presences for dynamic subcluster roots - const sendCapTPMessage = (captpMessage: CapTPMessage): void => { - const notification = makeCapTPNotification(captpMessage); - messageStream.write(notification).catch((error) => { - logger.error('Failed to send CapTP message:', error); - }); - }; - - const { dispatch: dispatchCapTP, abort: abortCapTP } = makeCapTP( - 'kernel', - sendCapTPMessage, - kernelHostRoot, - ); + // Connect host vat to the background via the message stream + // The background will use makeBackgroundHostVat to create the supervisor side + const hostVatStream = messageStream as unknown as Parameters< + typeof hostVat.connect + >[0]; + hostVat.connect(hostVatStream); - messageStream - .drain((message) => { - if (isCapTPNotification(message)) { - const captpMessage = message.params[0]; - dispatchCapTP(captpMessage); - } else { - throw new Error(`Unexpected message: ${stringify(message)}`); - } - }) - .catch((error) => { - abortCapTP(error); - logger.error('Message stream error:', error); - }); + logger.log('Kernel started with host vat transport'); const relays = getRelaysFromCurrentLocation(); await kernel.initRemoteComms({ relays }); diff --git a/yarn.lock b/yarn.lock index 88755a787..3e72eb6af 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2290,10 +2290,11 @@ __metadata: version: 0.0.0-use.local resolution: "@metamask/kernel-browser-runtime@workspace:packages/kernel-browser-runtime" dependencies: + "@agoric/swingset-liveslots": "npm:0.10.3-u21.0.1" "@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" + "@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" From 3b1e3de6120f692fc1a4a59dc8b12670c6eafd52 Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Fri, 30 Jan 2026 12:14:34 -0800 Subject: [PATCH 32/41] refactor(kernel-browser-runtime): Use RPC pattern for host-vat messaging Replace ad hoc message types with RpcClient/RpcService from @metamask/kernel-rpc-methods, using JSON-RPC 2.0 format for all communication between kernel and supervisor sides. Co-Authored-By: Claude Opus 4.5 --- .../src/host-vat/index.ts | 9 +- .../src/host-vat/kernel-side.test.ts | 203 +++++++++++------- .../src/host-vat/kernel-side.ts | 158 ++++++-------- .../src/host-vat/rpc/index.ts | 42 ++++ .../src/host-vat/supervisor-side.test.ts | 174 ++++++++------- .../src/host-vat/supervisor-side.ts | 150 +++++++------ .../src/host-vat/transport.ts | 17 -- 7 files changed, 411 insertions(+), 342 deletions(-) create mode 100644 packages/kernel-browser-runtime/src/host-vat/rpc/index.ts delete mode 100644 packages/kernel-browser-runtime/src/host-vat/transport.ts diff --git a/packages/kernel-browser-runtime/src/host-vat/index.ts b/packages/kernel-browser-runtime/src/host-vat/index.ts index 9af6b81b4..eea6203f4 100644 --- a/packages/kernel-browser-runtime/src/host-vat/index.ts +++ b/packages/kernel-browser-runtime/src/host-vat/index.ts @@ -3,8 +3,8 @@ * * The host vat enables a system vat supervisor to run in a different process * than the kernel. The kernel runs in a Worker, and the supervisor runs in - * the background script. They communicate over a stream using the optimistic - * syscall model (fire-and-forget with ['ok', null]). + * the background script. They communicate over a stream using JSON-RPC messages + * and the optimistic syscall model (fire-and-forget with ['ok', null]). */ export { makeKernelHostVat } from './kernel-side.ts'; @@ -12,8 +12,3 @@ export type { KernelHostVatResult } from './kernel-side.ts'; export { makeBackgroundHostVat } from './supervisor-side.ts'; export type { BackgroundHostVatResult } from './supervisor-side.ts'; - -export type { - KernelToSupervisorMessage, - SupervisorToKernelMessage, -} from './transport.ts'; diff --git a/packages/kernel-browser-runtime/src/host-vat/kernel-side.test.ts b/packages/kernel-browser-runtime/src/host-vat/kernel-side.test.ts index 08f061a86..e62266df6 100644 --- a/packages/kernel-browser-runtime/src/host-vat/kernel-side.test.ts +++ b/packages/kernel-browser-runtime/src/host-vat/kernel-side.test.ts @@ -1,29 +1,29 @@ import type { DuplexStream } from '@metamask/streams'; +import type { + JsonRpcMessage, + JsonRpcNotification, + JsonRpcRequest, + JsonRpcResponse, +} from '@metamask/utils'; import { describe, it, expect, vi, beforeEach } from 'vitest'; import { makeKernelHostVat } from './kernel-side.ts'; -import type { - KernelToSupervisorMessage, - SupervisorToKernelMessage, -} from './transport.ts'; -type TestStream = DuplexStream< - SupervisorToKernelMessage, - KernelToSupervisorMessage ->; +type TestStream = DuplexStream; const makeMockStream = () => { - const written: KernelToSupervisorMessage[] = []; - const messageHandlers: ((message: SupervisorToKernelMessage) => void)[] = []; + const written: JsonRpcMessage[] = []; + const messageHandlers: ((message: JsonRpcMessage) => void | Promise)[] = + []; let drainResolver: (() => void) | null = null; const stream: TestStream = { - write: vi.fn(async (message: KernelToSupervisorMessage) => { + write: vi.fn(async (message: JsonRpcMessage) => { written.push(message); return { done: false, value: undefined }; }), drain: vi.fn( - async (handler: (message: SupervisorToKernelMessage) => void) => { + async (handler: (message: JsonRpcMessage) => void | Promise) => { messageHandlers.push(handler); // Return a promise that resolves when test calls closeDrain() return new Promise((resolve) => { @@ -43,9 +43,9 @@ const makeMockStream = () => { stream, written, // Simulate receiving a message from supervisor - receiveMessage: (message: SupervisorToKernelMessage) => { + receiveMessage: async (message: JsonRpcMessage) => { for (const handler of messageHandlers) { - handler(message); + await handler(message); } }, closeDrain: () => { @@ -54,6 +54,20 @@ const makeMockStream = () => { }; }; +/** + * Helper to check if a message is a JSON-RPC request with the given method. + * + * @param message - The message to check. + * @param method - The expected method name. + * @returns True if the message is a request with the given method. + */ +const isRequestWithMethod = ( + message: JsonRpcMessage, + method: string, +): message is JsonRpcRequest => { + return 'method' in message && message.method === method && 'id' in message; +}; + describe('makeKernelHostVat', () => { beforeEach(() => { vi.clearAllMocks(); @@ -80,18 +94,6 @@ describe('makeKernelHostVat', () => { }); describe('connect', () => { - it('sends connected message when stream is connected', async () => { - const result = makeKernelHostVat(); - const { stream, written } = makeMockStream(); - - result.connect(stream); - - // Allow async operations to complete - await vi.waitFor(() => { - expect(written).toContainEqual({ type: 'connected' }); - }); - }); - it('starts draining the stream for messages', async () => { const result = makeKernelHostVat(); const { stream } = makeMockStream(); @@ -105,21 +107,29 @@ describe('makeKernelHostVat', () => { }); describe('awaitConnection', () => { - it('resolves when ready message is received', async () => { + it('resolves when ready notification is received', async () => { const result = makeKernelHostVat(); const { stream, receiveMessage } = makeMockStream(); result.connect(stream); + // Wait for drain to be called before sending messages + await vi.waitFor(() => { + expect(stream.drain).toHaveBeenCalled(); + }); + const connectionPromise = result.config.transport.awaitConnection(); - // Simulate supervisor sending ready message - receiveMessage({ type: 'ready' }); + // Simulate supervisor sending ready notification (JSON-RPC) + await receiveMessage({ + jsonrpc: '2.0', + method: 'ready', + }); expect(await connectionPromise).toBeUndefined(); }); - it('does not resolve before ready message', async () => { + it('does not resolve before ready notification', async () => { const result = makeKernelHostVat(); const { stream } = makeMockStream(); @@ -139,7 +149,7 @@ describe('makeKernelHostVat', () => { }); describe('deliver', () => { - it('sends delivery message over stream', async () => { + it('sends delivery request over stream as JSON-RPC', async () => { const result = makeKernelHostVat(); const { stream, written, receiveMessage } = makeMockStream(); @@ -157,29 +167,35 @@ describe('makeKernelHostVat', () => { ); await vi.waitFor(() => { - const deliveryMsg = written.find((item) => item.type === 'delivery'); + const deliveryMsg = written.find((item) => + isRequestWithMethod(item, 'deliver'), + ); expect(deliveryMsg).toBeDefined(); }); - // Verify the delivery message format - const deliveryMsg = written.find( - ( - item, - ): item is Extract => - item.type === 'delivery', - ); - expect(deliveryMsg?.delivery).toStrictEqual(delivery); - expect(deliveryMsg?.id).toBe('0'); - - // Simulate supervisor responding - receiveMessage({ type: 'delivery-result', id: '0', error: null }); + // Verify the delivery message format (JSON-RPC request) + const deliveryMsg = written.find((item) => + isRequestWithMethod(item, 'deliver'), + ) as JsonRpcRequest; + expect(deliveryMsg.jsonrpc).toBe('2.0'); + expect(deliveryMsg.method).toBe('deliver'); + expect(deliveryMsg.params).toStrictEqual(delivery); + expect(deliveryMsg.id).toMatch(/^kernel:\d+$/u); + + // Simulate supervisor responding with JSON-RPC response + // The deliver result is [checkpoint, deliveryError] + await receiveMessage({ + jsonrpc: '2.0', + id: deliveryMsg.id, + result: [[[], []], null], + } as JsonRpcResponse); expect(await deliverPromise).toBeNull(); }); it('returns delivery error when supervisor reports error', async () => { const result = makeKernelHostVat(); - const { stream, receiveMessage } = makeMockStream(); + const { stream, written, receiveMessage } = makeMockStream(); result.connect(stream); @@ -199,12 +215,17 @@ describe('makeKernelHostVat', () => { expect(stream.write).toHaveBeenCalled(); }); - // Simulate supervisor responding with error - receiveMessage({ - type: 'delivery-result', - id: '0', - error: 'Delivery failed', - }); + // Get the request ID + const deliveryMsg = written.find((item) => + isRequestWithMethod(item, 'deliver'), + ) as JsonRpcRequest; + + // Simulate supervisor responding with error in result + await receiveMessage({ + jsonrpc: '2.0', + id: deliveryMsg.id, + result: [[[], []], 'Delivery failed'], + } as JsonRpcResponse); expect(await deliverPromise).toBe('Delivery failed'); }); @@ -229,9 +250,9 @@ describe('makeKernelHostVat', () => { ); await vi.waitFor(() => { - expect(written.filter((item) => item.type === 'delivery')).toHaveLength( - 1, - ); + expect( + written.filter((item) => isRequestWithMethod(item, 'deliver')), + ).toHaveLength(1); }); // Send second delivery @@ -242,24 +263,33 @@ describe('makeKernelHostVat', () => { ); await vi.waitFor(() => { - expect(written.filter((item) => item.type === 'delivery')).toHaveLength( - 2, - ); + expect( + written.filter((item) => isRequestWithMethod(item, 'deliver')), + ).toHaveLength(2); }); - // Check IDs - const deliveryMsgs = written.filter( - ( - item, - ): item is Extract => - item.type === 'delivery', + // Check IDs increment within a single host vat instance + const deliveryMsgs = written.filter((item) => + isRequestWithMethod(item, 'deliver'), ); - expect(deliveryMsgs[0]?.id).toBe('0'); - expect(deliveryMsgs[1]?.id).toBe('1'); + const id1 = deliveryMsgs[0]?.id as string; + const id2 = deliveryMsgs[1]?.id as string; + // IDs should be different and follow the pattern + expect(id1).toMatch(/^kernel:\d+$/u); + expect(id2).toMatch(/^kernel:\d+$/u); + expect(id1).not.toBe(id2); // Resolve both - receiveMessage({ type: 'delivery-result', id: '0', error: null }); - receiveMessage({ type: 'delivery-result', id: '1', error: null }); + await receiveMessage({ + jsonrpc: '2.0', + id: id1, + result: [[[], []], null], + } as JsonRpcResponse); + await receiveMessage({ + jsonrpc: '2.0', + id: id2, + result: [[[], []], null], + } as JsonRpcResponse); await Promise.all([deliver1, deliver2]); }); @@ -284,7 +314,7 @@ describe('makeKernelHostVat', () => { }); describe('syscall handling', () => { - it('calls syscall handler when syscall message is received', async () => { + it('calls syscall handler when syscall notification is received', async () => { const result = makeKernelHostVat(); const { stream, receiveMessage } = makeMockStream(); const syscallHandler = vi.fn().mockReturnValue(['ok', null]); @@ -293,9 +323,16 @@ describe('makeKernelHostVat', () => { result.connect(stream); const syscall = ['send', 'ko1', { methargs: { body: '', slots: [] } }]; - receiveMessage({ type: 'syscall', syscall: syscall as never }); + // Send as JSON-RPC notification + await receiveMessage({ + jsonrpc: '2.0', + method: 'syscall', + params: syscall, + } as JsonRpcNotification); - expect(syscallHandler).toHaveBeenCalledWith(syscall); + await vi.waitFor(() => { + expect(syscallHandler).toHaveBeenCalledWith(syscall); + }); }); it('ignores syscall if handler is not set', async () => { @@ -309,13 +346,17 @@ describe('makeKernelHostVat', () => { const syscall = ['send', 'ko1', { methargs: { body: '', slots: [] } }]; // Should not throw - expect(() => { - receiveMessage({ type: 'syscall', syscall: syscall as never }); - }).not.toThrow(); + await receiveMessage({ + jsonrpc: '2.0', + method: 'syscall', + params: syscall, + } as JsonRpcNotification); - expect(mockLogger.warn).toHaveBeenCalledWith( - 'Received syscall before handler was set', - ); + await vi.waitFor(() => { + expect(mockLogger.warn).toHaveBeenCalledWith( + 'Received syscall before handler was set', + ); + }); }); it('logs error if syscall handler throws', async () => { @@ -332,9 +373,15 @@ describe('makeKernelHostVat', () => { result.connect(stream); const syscall = ['send', 'ko1', { methargs: { body: '', slots: [] } }]; - receiveMessage({ type: 'syscall', syscall: syscall as never }); + await receiveMessage({ + jsonrpc: '2.0', + method: 'syscall', + params: syscall, + } as JsonRpcNotification); - expect(mockLogger.error).toHaveBeenCalledWith('Syscall error:', error); + await vi.waitFor(() => { + expect(mockLogger.error).toHaveBeenCalledWith('Syscall error:', error); + }); }); }); }); diff --git a/packages/kernel-browser-runtime/src/host-vat/kernel-side.ts b/packages/kernel-browser-runtime/src/host-vat/kernel-side.ts index 7df6e5b96..2c6d3a23c 100644 --- a/packages/kernel-browser-runtime/src/host-vat/kernel-side.ts +++ b/packages/kernel-browser-runtime/src/host-vat/kernel-side.ts @@ -1,5 +1,8 @@ -import type { PromiseKit } from '@endo/promise-kit'; +import type { VatSyscallObject } from '@agoric/swingset-liveslots'; import { makePromiseKit } from '@endo/promise-kit'; +import { RpcClient, RpcService } from '@metamask/kernel-rpc-methods'; +import { stringify } from '@metamask/kernel-utils'; +import type { JsonRpcMessage } from '@metamask/kernel-utils'; import type { Logger } from '@metamask/logger'; import type { DeliveryObject, @@ -8,11 +11,9 @@ import type { SystemVatTransport, } from '@metamask/ocap-kernel'; import type { DuplexStream } from '@metamask/streams'; +import { isJsonRpcNotification, isJsonRpcResponse } from '@metamask/utils'; -import type { - KernelToSupervisorMessage, - SupervisorToKernelMessage, -} from './transport.ts'; +import { kernelToSupervisorSpecs, kernelHandlers } from './rpc/index.ts'; /** * Result of creating a kernel-side host vat. @@ -27,11 +28,9 @@ export type KernelHostVatResult = { * Connect the stream after kernel is created. * Call this to wire up communication with the supervisor. * - * @param stream - The duplex stream to communicate with the supervisor. + * @param stream - The duplex stream for JSON-RPC communication with the supervisor. */ - connect: ( - stream: DuplexStream, - ) => void; + connect: (stream: DuplexStream) => void; }; /** @@ -73,80 +72,42 @@ export function makeKernelHostVat(options?: { // Promise kit to signal when supervisor is ready const supervisorReady = makePromiseKit(); - // Pending deliveries waiting for results - const pendingDeliveries = new Map>(); - let deliveryCounter = 0; + // RpcClient for sending deliveries to supervisor - set when connect() is called + let rpcClient: RpcClient | null = null; - // Stream for communication - set when connect() is called - let stream: DuplexStream< - SupervisorToKernelMessage, - KernelToSupervisorMessage - > | null = null; + // RpcService for receiving syscalls from supervisor + const rpcService = new RpcService(kernelHandlers, { + handleSyscall: (params) => { + if (!syscallHandler) { + logger?.warn('Received syscall before handler was set'); + return; + } + // Process syscall synchronously - the result is ignored because + // the supervisor uses optimistic execution + try { + // Cast needed because the RPC spec uses slightly different types + syscallHandler(params as unknown as VatSyscallObject); + } catch (error) { + // Syscall errors are handled by the kernel (vat termination) + logger?.error('Syscall error:', error); + } + }, + }); /** - * Deliver a message to the supervisor over the stream. + * Deliver a message to the supervisor via RPC. * * @param delivery - The delivery object to send. * @returns A promise that resolves to the delivery error (null if success). */ const deliver = async (delivery: DeliveryObject): Promise => { - if (!stream) { + if (!rpcClient) { throw new Error('Stream not connected'); } - const id = String(deliveryCounter); - deliveryCounter += 1; - - const resultKit = makePromiseKit(); - pendingDeliveries.set(id, resultKit); - - await stream.write({ type: 'delivery', delivery, id }); - - return resultKit.promise; - }; - - /** - * Handle incoming messages from the supervisor. - * - * @param message - The message from the supervisor. - */ - const handleMessage = (message: SupervisorToKernelMessage): void => { - switch (message.type) { - case 'ready': - supervisorReady.resolve(); - break; - - case 'syscall': - if (!syscallHandler) { - logger?.warn('Received syscall before handler was set'); - return; - } - // Process syscall synchronously - the result is ignored because - // the supervisor uses optimistic execution - try { - syscallHandler(message.syscall); - } catch (error) { - // Syscall errors are handled by the kernel (vat termination) - logger?.error('Syscall error:', error); - } - break; - - case 'delivery-result': { - const pending = pendingDeliveries.get(message.id); - if (pending) { - pendingDeliveries.delete(message.id); - pending.resolve(message.error); - } else { - logger?.warn(`Received result for unknown delivery: ${message.id}`); - } - break; - } - - default: - logger?.warn( - `Unknown message type: ${(message as { type: string }).type}`, - ); - } + // The deliver spec returns [checkpoint, deliveryError], we want just the error + const result = await rpcClient.call('deliver', delivery); + return result[1]; }; const transport: SystemVatTransport = { @@ -163,27 +124,44 @@ export function makeKernelHostVat(options?: { }; const connect = ( - connectedStream: DuplexStream< - SupervisorToKernelMessage, - KernelToSupervisorMessage - >, + stream: DuplexStream, ): void => { - stream = connectedStream; + rpcClient = new RpcClient( + kernelToSupervisorSpecs, + async (message) => { + await stream.write(message); + }, + 'kernel:', + logger, + ); + + // Capture reference for use in drain callback + const client = rpcClient; // Start draining the stream for incoming messages - stream.drain(handleMessage).catch((error) => { - logger?.error('Stream error:', error); - // Reject any pending deliveries - for (const pending of pendingDeliveries.values()) { - pending.reject(error as Error); - } - pendingDeliveries.clear(); - }); - - // Send connected message to supervisor - stream.write({ type: 'connected' }).catch((error) => { - logger?.error('Failed to send connected message:', error); - }); + stream + .drain(async (message) => { + if (isJsonRpcResponse(message)) { + // Response to our deliver request + client.handleResponse(message.id as string, message); + } else if (isJsonRpcNotification(message)) { + if (message.method === 'ready') { + // Supervisor signals it's ready + supervisorReady.resolve(); + } else if (message.method === 'syscall') { + // Syscall notification from supervisor + await rpcService.execute('syscall', message.params); + } else { + throw new Error( + `Unexpected host vat message from supervisor: ${stringify(message)}`, + ); + } + } + }) + .catch((error) => { + logger?.error('Stream error:', error); + client.rejectAll(error as Error); + }); }; return harden({ diff --git a/packages/kernel-browser-runtime/src/host-vat/rpc/index.ts b/packages/kernel-browser-runtime/src/host-vat/rpc/index.ts new file mode 100644 index 000000000..200719a6b --- /dev/null +++ b/packages/kernel-browser-runtime/src/host-vat/rpc/index.ts @@ -0,0 +1,42 @@ +import type { + HandlerRecord, + MethodSpecRecord, +} from '@metamask/kernel-rpc-methods'; +import { + vatHandlers, + vatMethodSpecs, + vatSyscallHandlers, + vatSyscallMethodSpecs, +} from '@metamask/ocap-kernel/rpc'; + +// Extract types +type DeliverSpec = (typeof vatMethodSpecs)['deliver']; +type DeliverHandler = (typeof vatHandlers)['deliver']; +type VatSyscallSpec = (typeof vatSyscallMethodSpecs)['syscall']; +type VatSyscallHandler = (typeof vatSyscallHandlers)['syscall']; + +/** + * Method specs for messages from kernel to supervisor (requests). + */ +export const kernelToSupervisorSpecs = { + deliver: vatMethodSpecs.deliver, +} as MethodSpecRecord; + +/** + * Handlers for the kernel to process notifications from the supervisor. + */ +export const kernelHandlers: HandlerRecord = + vatSyscallHandlers; + +/** + * Method specs for messages from supervisor to kernel (notifications). + */ +export const supervisorToKernelSpecs: MethodSpecRecord = + vatSyscallMethodSpecs; + +/** + * Handlers for the supervisor to process requests from the kernel. + */ +export const supervisorHandlers = { + deliver: vatHandlers.deliver, +} as HandlerRecord; diff --git a/packages/kernel-browser-runtime/src/host-vat/supervisor-side.test.ts b/packages/kernel-browser-runtime/src/host-vat/supervisor-side.test.ts index 1ffdc9e4c..72431045e 100644 --- a/packages/kernel-browser-runtime/src/host-vat/supervisor-side.test.ts +++ b/packages/kernel-browser-runtime/src/host-vat/supervisor-side.test.ts @@ -1,14 +1,14 @@ import { SystemVatSupervisor } from '@metamask/ocap-kernel/vats'; import type { DuplexStream } from '@metamask/streams'; +import type { + JsonRpcMessage, + JsonRpcNotification, + JsonRpcRequest, + JsonRpcResponse, +} from '@metamask/utils'; import { describe, it, expect, vi, beforeEach } from 'vitest'; import { makeBackgroundHostVat } from './supervisor-side.ts'; -import type { - KernelToSupervisorMessage, - SupervisorToKernelMessage, -} from './transport.ts'; - -// Import after mock // Mock SystemVatSupervisor vi.mock('@metamask/ocap-kernel/vats', () => ({ @@ -17,27 +17,21 @@ vi.mock('@metamask/ocap-kernel/vats', () => ({ }, })); -type TestStream = DuplexStream< - KernelToSupervisorMessage, - SupervisorToKernelMessage ->; +type TestStream = DuplexStream; const makeMockStream = () => { - const written: SupervisorToKernelMessage[] = []; - const messageHandlers: (( - message: KernelToSupervisorMessage, - ) => void | Promise)[] = []; + const written: JsonRpcMessage[] = []; + const messageHandlers: ((message: JsonRpcMessage) => void | Promise)[] = + []; let drainResolver: (() => void) | null = null; const stream: TestStream = { - write: vi.fn(async (message: SupervisorToKernelMessage) => { + write: vi.fn(async (message: JsonRpcMessage) => { written.push(message); return { done: false, value: undefined }; }), drain: vi.fn( - async ( - handler: (message: KernelToSupervisorMessage) => void | Promise, - ) => { + async (handler: (message: JsonRpcMessage) => void | Promise) => { messageHandlers.push(handler); // Return a promise that resolves when test calls closeDrain() return new Promise((resolve) => { @@ -57,7 +51,7 @@ const makeMockStream = () => { stream, written, // Simulate receiving a message from kernel - receiveMessage: async (message: KernelToSupervisorMessage) => { + receiveMessage: async (message: JsonRpcMessage) => { for (const handler of messageHandlers) { await handler(message); } @@ -73,6 +67,30 @@ const makeMockSupervisor = () => ({ id: 'sv0' as const, }); +/** + * Helper to check if a message is a JSON-RPC notification with the given method. + * + * @param message - The message to check. + * @param method - The expected method name. + * @returns True if the message is a notification with the given method. + */ +const isNotificationWithMethod = ( + message: JsonRpcMessage, + method: string, +): message is JsonRpcNotification => { + return 'method' in message && message.method === method && !('id' in message); +}; + +/** + * Helper to check if a message is a JSON-RPC response. + * + * @param message - The message to check. + * @returns True if the message is a response. + */ +const isResponse = (message: JsonRpcMessage): message is JsonRpcResponse => { + return 'id' in message && ('result' in message || 'error' in message); +}; + describe('makeBackgroundHostVat', () => { let mockSupervisor: ReturnType; @@ -101,7 +119,7 @@ describe('makeBackgroundHostVat', () => { expect(makeCall?.[0]).toHaveProperty('executeSyscall'); }); - it('sends ready message after supervisor is created', async () => { + it('sends ready notification after supervisor is created', async () => { const buildRootObject = vi.fn().mockReturnValue({}); const result = makeBackgroundHostVat({ buildRootObject }); const { stream, written } = makeMockStream(); @@ -109,7 +127,19 @@ describe('makeBackgroundHostVat', () => { result.connect(stream); await vi.waitFor(() => { - expect(written).toContainEqual({ type: 'ready' }); + const readyMsg = written.find((item) => + isNotificationWithMethod(item, 'ready'), + ); + expect(readyMsg).toBeDefined(); + }); + + // Verify the ready message format (JSON-RPC notification) + const readyMsg = written.find((item) => + isNotificationWithMethod(item, 'ready'), + ); + expect(readyMsg).toStrictEqual({ + jsonrpc: '2.0', + method: 'ready', }); }); @@ -200,7 +230,7 @@ describe('makeBackgroundHostVat', () => { }); describe('delivery handling', () => { - it('delivers to supervisor and sends result back', async () => { + it('delivers to supervisor and sends JSON-RPC response back', async () => { const buildRootObject = vi.fn().mockReturnValue({}); const result = makeBackgroundHostVat({ buildRootObject }); const { stream, written, receiveMessage } = makeMockStream(); @@ -209,7 +239,10 @@ describe('makeBackgroundHostVat', () => { // Wait for supervisor to be ready await vi.waitFor(() => { - expect(written).toContainEqual({ type: 'ready' }); + const readyMsg = written.find((item) => + isNotificationWithMethod(item, 'ready'), + ); + expect(readyMsg).toBeDefined(); }); const delivery = [ @@ -218,21 +251,26 @@ describe('makeBackgroundHostVat', () => { { methargs: { body: '', slots: [] } }, ]; + // Send JSON-RPC request for delivery await receiveMessage({ - type: 'delivery', - delivery: delivery as never, - id: '123', - }); + jsonrpc: '2.0', + id: 'kernel:123', + method: 'deliver', + params: delivery, + } as JsonRpcRequest); expect(mockSupervisor.deliver).toHaveBeenCalledWith(delivery); - expect(written).toContainEqual({ - type: 'delivery-result', - id: '123', - error: null, + + // Check the response - VatDeliveryResult is [checkpoint, error] + const responseMsg = written.find((item) => isResponse(item)); + expect(responseMsg).toStrictEqual({ + jsonrpc: '2.0', + id: 'kernel:123', + result: [[[], []], null], }); }); - it('sends delivery error when supervisor.deliver returns error', async () => { + it('sends delivery error in response when supervisor.deliver returns error', async () => { mockSupervisor.deliver.mockResolvedValue('Something went wrong'); const buildRootObject = vi.fn().mockReturnValue({}); @@ -242,7 +280,10 @@ describe('makeBackgroundHostVat', () => { result.connect(stream); await vi.waitFor(() => { - expect(written).toContainEqual({ type: 'ready' }); + const readyMsg = written.find((item) => + isNotificationWithMethod(item, 'ready'), + ); + expect(readyMsg).toBeDefined(); }); const delivery = [ @@ -251,47 +292,26 @@ describe('makeBackgroundHostVat', () => { { methargs: { body: '', slots: [] } }, ]; + // Send JSON-RPC request for delivery await receiveMessage({ - type: 'delivery', - delivery: delivery as never, - id: '456', - }); - - expect(written).toContainEqual({ - type: 'delivery-result', - id: '456', - error: 'Something went wrong', - }); - }); - - it('handles connected message from kernel', async () => { - const mockLogger = { - debug: vi.fn(), - subLogger: vi.fn(() => mockLogger), - }; - const buildRootObject = vi.fn().mockReturnValue({}); - const result = makeBackgroundHostVat({ - buildRootObject, - logger: mockLogger as never, - }); - const { stream, written, receiveMessage } = makeMockStream(); - - result.connect(stream); - - await vi.waitFor(() => { - expect(written).toContainEqual({ type: 'ready' }); + jsonrpc: '2.0', + id: 'kernel:456', + method: 'deliver', + params: delivery, + } as JsonRpcRequest); + + // VatDeliveryResult is [checkpoint, error] + const responseMsg = written.find((item) => isResponse(item)); + expect(responseMsg).toStrictEqual({ + jsonrpc: '2.0', + id: 'kernel:456', + result: [[[], []], 'Something went wrong'], }); - - await receiveMessage({ type: 'connected' }); - - expect(mockLogger.debug).toHaveBeenCalledWith( - 'Received connected message from kernel', - ); }); }); describe('syscall execution', () => { - it('sends syscall over stream with coerced object', async () => { + it('sends syscall as JSON-RPC notification over stream', async () => { let capturedExecuteSyscall: ((vso: unknown) => unknown) | null = null; vi.mocked(SystemVatSupervisor.make).mockImplementation( @@ -323,10 +343,20 @@ describe('makeBackgroundHostVat', () => { expect(syscallResult).toStrictEqual(['ok', null]); await vi.waitFor(() => { - expect(written).toContainEqual({ - type: 'syscall', - syscall, - }); + const syscallMsg = written.find((item) => + isNotificationWithMethod(item, 'syscall'), + ); + expect(syscallMsg).toBeDefined(); + }); + + // Verify the syscall message format (JSON-RPC notification) + const syscallMsg = written.find((item) => + isNotificationWithMethod(item, 'syscall'), + ); + expect(syscallMsg).toStrictEqual({ + jsonrpc: '2.0', + method: 'syscall', + params: syscall, }); }); diff --git a/packages/kernel-browser-runtime/src/host-vat/supervisor-side.ts b/packages/kernel-browser-runtime/src/host-vat/supervisor-side.ts index cefc9119f..87caeb0e1 100644 --- a/packages/kernel-browser-runtime/src/host-vat/supervisor-side.ts +++ b/packages/kernel-browser-runtime/src/host-vat/supervisor-side.ts @@ -4,6 +4,9 @@ import type { VatSyscallResult, } from '@agoric/swingset-liveslots'; import { makePromiseKit } from '@endo/promise-kit'; +import { RpcClient, RpcService } from '@metamask/kernel-rpc-methods'; +import { stringify } from '@metamask/kernel-utils'; +import type { JsonRpcMessage } from '@metamask/kernel-utils'; import type { Logger } from '@metamask/logger'; import type { KernelFacet, @@ -11,11 +14,9 @@ import type { } from '@metamask/ocap-kernel'; import { SystemVatSupervisor } from '@metamask/ocap-kernel/vats'; import type { DuplexStream } from '@metamask/streams'; +import { isJsonRpcRequest } from '@metamask/utils'; -import type { - KernelToSupervisorMessage, - SupervisorToKernelMessage, -} from './transport.ts'; +import { supervisorToKernelSpecs, supervisorHandlers } from './rpc/index.ts'; /** * Result of creating a background-side host vat. @@ -25,11 +26,9 @@ export type BackgroundHostVatResult = { * Connect and start the supervisor. * Call this with the stream to the kernel Worker. * - * @param stream - The duplex stream to communicate with the kernel. + * @param stream - The duplex stream for JSON-RPC communication with the kernel. */ - connect: ( - stream: DuplexStream, - ) => void; + connect: (stream: DuplexStream) => void; /** * Promise that resolves to kernelFacet when bootstrap completes. @@ -76,30 +75,24 @@ export function makeBackgroundHostVat(options: { // Promise kit for kernel facet - resolves when bootstrap is called const kernelFacetKit = makePromiseKit(); - // Stream for communication - set when connect() is called - let stream: DuplexStream< - KernelToSupervisorMessage, - SupervisorToKernelMessage - > | null = null; - - // Supervisor instance - created when connect() is called - let supervisor: SystemVatSupervisor | null = null; + // RpcClient for sending syscalls to kernel - set when connect() is called + let rpcClient: RpcClient | null = null; /** - * Execute a syscall by sending it to the kernel. + * Execute a syscall by sending it to the kernel via RPC notification. * Uses optimistic execution - returns success immediately. * * @param vso - The syscall object to execute. * @returns A syscall success result. */ const executeSyscall = (vso: VatSyscallObject): VatSyscallResult => { - if (!stream) { + if (!rpcClient) { throw new Error('Stream not connected'); } - // Send syscall notification (fire-and-forget) - // The syscall is sent as-is; structured clone handles serialization - stream.write({ type: 'syscall', syscall: vso }).catch((error) => { + // Send syscall as notification (fire-and-forget) + // Cast needed because the RPC spec uses slightly different types + rpcClient.notify('syscall', vso as never).catch((error) => { logger?.error('Failed to send syscall:', error); }); @@ -125,59 +118,17 @@ export function makeBackgroundHostVat(options: { return buildRootObject(vatPowers, parameters); }; - /** - * Handle incoming messages from the kernel. - * - * @param message - The message from the kernel. - */ - const handleMessage = async ( - message: KernelToSupervisorMessage, - ): Promise => { - switch (message.type) { - case 'connected': - // Kernel acknowledges connection - nothing to do - logger?.debug('Received connected message from kernel'); - break; - - case 'delivery': { - if (!supervisor) { - logger?.error('Received delivery before supervisor was created'); - await stream?.write({ - type: 'delivery-result', - id: message.id, - error: 'Supervisor not ready', - }); - return; - } - - // Deliver to supervisor and send result back - // Cast from DeliveryObject (our JSON-safe type) to VatDeliveryObject - const deliveryError = await supervisor.deliver( - message.delivery as unknown as VatDeliveryObject, - ); - - await stream?.write({ - type: 'delivery-result', - id: message.id, - error: deliveryError, - }); - break; - } - - default: - logger?.warn( - `Unknown message type: ${(message as { type: string }).type}`, - ); - } - }; - const connect = ( - connectedStream: DuplexStream< - KernelToSupervisorMessage, - SupervisorToKernelMessage - >, + stream: DuplexStream, ): void => { - stream = connectedStream; + rpcClient = new RpcClient( + supervisorToKernelSpecs, + async (message) => { + await stream.write(message); + }, + 'supervisor:', + logger, + ); // Create and start the supervisor const supervisorOptions = { @@ -185,16 +136,59 @@ export function makeBackgroundHostVat(options: { executeSyscall, ...(logger && { logger: logger.subLogger({ tags: ['supervisor'] }) }), }; + SystemVatSupervisor.make(supervisorOptions) .then(async (createdSupervisor) => { - supervisor = createdSupervisor; + // Create RpcService for handling delivery requests from kernel + const rpcService = new RpcService(supervisorHandlers, { + handleDelivery: async (params) => { + const deliveryError = await createdSupervisor.deliver( + params as VatDeliveryObject, + ); + // SystemVatSupervisor returns just the error, but the spec expects + // VatDeliveryResult which is [VatCheckpoint, error]. System vats + // don't checkpoint, so we return an empty checkpoint. + const emptyCheckpoint: [[string, string][], string[]] = [[], []]; + return [emptyCheckpoint, deliveryError]; + }, + }); + + // Signal to kernel that we're ready via notification + await stream.write({ + jsonrpc: '2.0' as const, + method: 'ready', + }); - // Signal to kernel that we're ready - return stream?.write({ type: 'ready' }); - }) - .then(async () => { // Start draining the stream for incoming messages - return stream?.drain(handleMessage); + return stream.drain(async (message) => { + if (isJsonRpcRequest(message) && message.method === 'deliver') { + // Request from kernel (deliver) + try { + const result = await rpcService.execute( + 'deliver', + message.params, + ); + await stream.write({ + jsonrpc: '2.0', + id: message.id, + result, + }); + } catch (error) { + await stream.write({ + jsonrpc: '2.0', + id: message.id, + error: { + code: -32603, + message: (error as Error).message, + }, + }); + } + } else { + throw new Error( + `Unexpected host vat message from kernel: ${stringify(message)}`, + ); + } + }); }) .catch((error) => { logger?.error('Supervisor initialization error:', error); diff --git a/packages/kernel-browser-runtime/src/host-vat/transport.ts b/packages/kernel-browser-runtime/src/host-vat/transport.ts deleted file mode 100644 index 72a803493..000000000 --- a/packages/kernel-browser-runtime/src/host-vat/transport.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { VatSyscallObject } from '@agoric/swingset-liveslots'; -import type { DeliveryObject } from '@metamask/ocap-kernel'; - -/** - * Messages sent from kernel (Worker) to supervisor (background). - */ -export type KernelToSupervisorMessage = - | { type: 'delivery'; delivery: DeliveryObject; id: string } - | { type: 'connected' }; - -/** - * Messages sent from supervisor (background) to kernel (Worker). - */ -export type SupervisorToKernelMessage = - | { type: 'syscall'; syscall: VatSyscallObject } - | { type: 'delivery-result'; id: string; error: string | null } - | { type: 'ready' }; From aaf6d53abb73fa7f0a6a14ebe286eb9cc5a0a3cf Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Fri, 30 Jan 2026 12:26:31 -0800 Subject: [PATCH 33/41] fix(nodejs): Resolve delivery object type error --- .../src/host-vat/supervisor-side.ts | 4 ++-- packages/ocap-kernel/src/vats/SystemVatSupervisor.ts | 11 ++++++++--- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/packages/kernel-browser-runtime/src/host-vat/supervisor-side.ts b/packages/kernel-browser-runtime/src/host-vat/supervisor-side.ts index 87caeb0e1..a619dfc72 100644 --- a/packages/kernel-browser-runtime/src/host-vat/supervisor-side.ts +++ b/packages/kernel-browser-runtime/src/host-vat/supervisor-side.ts @@ -1,5 +1,4 @@ import type { - VatDeliveryObject, VatSyscallObject, VatSyscallResult, } from '@agoric/swingset-liveslots'; @@ -9,6 +8,7 @@ import { stringify } from '@metamask/kernel-utils'; import type { JsonRpcMessage } from '@metamask/kernel-utils'; import type { Logger } from '@metamask/logger'; import type { + DeliveryObject, KernelFacet, SystemVatBuildRootObject, } from '@metamask/ocap-kernel'; @@ -143,7 +143,7 @@ export function makeBackgroundHostVat(options: { const rpcService = new RpcService(supervisorHandlers, { handleDelivery: async (params) => { const deliveryError = await createdSupervisor.deliver( - params as VatDeliveryObject, + params as DeliveryObject, ); // SystemVatSupervisor returns just the error, but the spec expects // VatDeliveryResult which is [VatCheckpoint, error]. System vats diff --git a/packages/ocap-kernel/src/vats/SystemVatSupervisor.ts b/packages/ocap-kernel/src/vats/SystemVatSupervisor.ts index aedffc1ee..7453f6a50 100644 --- a/packages/ocap-kernel/src/vats/SystemVatSupervisor.ts +++ b/packages/ocap-kernel/src/vats/SystemVatSupervisor.ts @@ -20,7 +20,11 @@ import type { Syscall, SyscallResult, } from '../liveslots/types.ts'; -import type { SystemVatId, SystemVatBuildRootObject } from '../types.ts'; +import type { + SystemVatId, + SystemVatBuildRootObject, + DeliveryObject, +} from '../types.ts'; const makeLiveSlots: MakeLiveSlotsFn = localMakeLiveSlots; @@ -300,14 +304,15 @@ export class SystemVatSupervisor { * @param delivery - The delivery object to dispatch. * @returns A promise that resolves to the delivery error (null if success). */ - async deliver(delivery: VatDeliveryObject): Promise { + async deliver(delivery: DeliveryObject): Promise { if (!this.#dispatch) { throw new Error('SystemVatSupervisor not initialized'); } let deliveryError: string | null = null; try { - await this.#dispatch(harden(delivery)); + // Cast needed because DeliveryObject and VatDeliveryObject have minor type differences + await this.#dispatch(harden(delivery as VatDeliveryObject)); } catch (error) { deliveryError = error instanceof Error ? error.message : String(error); this.#logger.error( From 5cef59ef09807f9d79449207b60b68571615abbe Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Fri, 30 Jan 2026 12:58:38 -0800 Subject: [PATCH 34/41] feat(extension): Wire up host vat in background script Replace legacy CapTP connection with makeBackgroundHostVat API: - Fix kernel-worker.ts to pass hostVat.config to Kernel.make() - Simplify makeBackgroundHostVat to use internal buildRootObject - Update background.ts to use new host vat API - kernelFacet is now captured from bootstrap message services Co-Authored-By: Claude Opus 4.5 --- packages/extension/package.json | 1 + packages/extension/src/background.ts | 71 +++++----------- packages/extension/src/global.d.ts | 5 +- .../src/host-vat/supervisor-side.test.ts | 80 +++++++++---------- .../src/host-vat/supervisor-side.ts | 70 ++++++++-------- .../src/kernel-worker/kernel-worker.ts | 15 ++-- yarn.lock | 1 + 7 files changed, 104 insertions(+), 139 deletions(-) diff --git a/packages/extension/package.json b/packages/extension/package.json index bac0ca824..2a5e80917 100644 --- a/packages/extension/package.json +++ b/packages/extension/package.json @@ -49,6 +49,7 @@ "@metamask/kernel-ui": "workspace:^", "@metamask/kernel-utils": "workspace:^", "@metamask/logger": "workspace:^", + "@metamask/ocap-kernel": "workspace:^", "@metamask/streams": "workspace:^", "react": "^18.3.1", "react-dom": "^18.3.1", diff --git a/packages/extension/src/background.ts b/packages/extension/src/background.ts index b1a11267c..df1d144ca 100644 --- a/packages/extension/src/background.ts +++ b/packages/extension/src/background.ts @@ -1,13 +1,7 @@ import { E } from '@endo/eventual-send'; -import { - makeBackgroundCapTP, - makeCapTPNotification, - isCapTPNotification, - getCapTPMessage, -} from '@metamask/kernel-browser-runtime'; -import type { CapTPMessage } from '@metamask/kernel-browser-runtime'; +import { makeBackgroundHostVat } from '@metamask/kernel-browser-runtime'; import defaultSubcluster from '@metamask/kernel-browser-runtime/default-cluster'; -import { delay, isJsonRpcMessage, stringify } from '@metamask/kernel-utils'; +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'; @@ -21,7 +15,7 @@ let bootPromise: Promise | null = null; // With this we can click the extension action button to wake up the service worker. chrome.action.onClicked.addListener(() => { globalThis.kernel !== undefined && - E(globalThis.kernel).ping().catch(logger.error); + E(globalThis.kernel).getStatus().catch(logger.error); }); // Install/update @@ -87,65 +81,38 @@ async function main(): Promise { // Without this delay, sending messages via the chrome.runtime API can fail. await delay(50); - // Create stream for CapTP messages + // Create stream for JSON-RPC messages to kernel 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); - offscreenStream.write(notification).catch((error) => { - logger.error('Failed to send CapTP message:', error); - }); - }, - }); + // Create host vat - captures kernelFacet from bootstrap automatically + const hostVat = makeBackgroundHostVat({ logger }); - // Get the kernel remote presence - const kernelP = backgroundCapTP.getKernel(); - globalThis.kernel = kernelP; - - // Handle incoming CapTP messages from the kernel - const drainPromise = offscreenStream.drain((message) => { - if (isCapTPNotification(message)) { - const captpMessage = getCapTPMessage(message); - backgroundCapTP.dispatch(captpMessage); - } else { - throw new Error(`Unexpected message: ${stringify(message)}`); - } - }); - drainPromise.catch(logger.error); + // Connect to kernel via offscreen pipe + hostVat.connect(offscreenStream); - try { - await E(kernelP).ping(); - await startDefaultSubcluster(); - } catch (error) { - offscreenStream.throw(error as Error).catch(logger.error); - } + globalThis.kernel = hostVat.kernelFacetPromise; - try { - await drainPromise; - } catch (error) { - const finalError = new Error('Offscreen connection closed unexpectedly', { - cause: error, - }); - backgroundCapTP.abort(finalError); - throw finalError; - } + // Verify connectivity and start default subcluster + await E(kernel).getStatus(); + await startDefaultSubcluster(); } /** * Idempotently starts the default subcluster. + * Must be called after globalThis.kernel is set. */ async function startDefaultSubcluster(): Promise { - const status = await E(globalThis.kernel).getStatus(); + const { kernel } = globalThis; + if (kernel === undefined) { + throw new Error('Kernel not initialized'); + } + const status = await E(kernel).getStatus(); if (status.subclusters.length === 0) { - const result = await E(globalThis.kernel).launchSubcluster( - 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.'); diff --git a/packages/extension/src/global.d.ts b/packages/extension/src/global.d.ts index f63d2a3a6..5e62eac43 100644 --- a/packages/extension/src/global.d.ts +++ b/packages/extension/src/global.d.ts @@ -1,4 +1,4 @@ -import type { KernelFacade } from '@metamask/kernel-browser-runtime'; +import type { KernelFacet } from '@metamask/ocap-kernel'; // Type declarations for kernel dev console API. declare global { @@ -8,7 +8,6 @@ declare global { * * @example * ```typescript - * const kernel = await kernel.getKernel(); * const status = await E(kernel).getStatus(); * ``` */ @@ -16,7 +15,7 @@ declare global { var E: typeof import('@endo/eventual-send').E; // eslint-disable-next-line no-var - var kernel: KernelFacade | Promise; + var kernel: Promise | KernelFacet; } export {}; diff --git a/packages/kernel-browser-runtime/src/host-vat/supervisor-side.test.ts b/packages/kernel-browser-runtime/src/host-vat/supervisor-side.test.ts index 72431045e..02bf3b635 100644 --- a/packages/kernel-browser-runtime/src/host-vat/supervisor-side.test.ts +++ b/packages/kernel-browser-runtime/src/host-vat/supervisor-side.test.ts @@ -103,9 +103,8 @@ describe('makeBackgroundHostVat', () => { }); describe('connect', () => { - it('creates supervisor with provided buildRootObject', async () => { - const buildRootObject = vi.fn().mockReturnValue({}); - const result = makeBackgroundHostVat({ buildRootObject }); + it('creates supervisor with internal buildRootObject', async () => { + const result = makeBackgroundHostVat(); const { stream } = makeMockStream(); result.connect(stream); @@ -120,8 +119,7 @@ describe('makeBackgroundHostVat', () => { }); it('sends ready notification after supervisor is created', async () => { - const buildRootObject = vi.fn().mockReturnValue({}); - const result = makeBackgroundHostVat({ buildRootObject }); + const result = makeBackgroundHostVat(); const { stream, written } = makeMockStream(); result.connect(stream); @@ -144,8 +142,7 @@ describe('makeBackgroundHostVat', () => { }); it('starts draining stream after sending ready', async () => { - const buildRootObject = vi.fn().mockReturnValue({}); - const result = makeBackgroundHostVat({ buildRootObject }); + const result = makeBackgroundHostVat(); const { stream } = makeMockStream(); result.connect(stream); @@ -157,11 +154,9 @@ describe('makeBackgroundHostVat', () => { }); describe('kernelFacetPromise', () => { - it('resolves when buildRootObject receives kernelFacet in vatPowers', async () => { + it('resolves when bootstrap is called with kernelFacet in services', async () => { const mockKernelFacet = { launchSubcluster: vi.fn() }; - let capturedBuildRootObject: - | ((vatPowers: Record) => object) - | null = null; + let capturedBuildRootObject: (() => object) | null = null; vi.mocked(SystemVatSupervisor.make).mockImplementation( async (options) => { @@ -171,8 +166,7 @@ describe('makeBackgroundHostVat', () => { }, ); - const buildRootObject = vi.fn().mockReturnValue({}); - const result = makeBackgroundHostVat({ buildRootObject }); + const result = makeBackgroundHostVat(); const { stream } = makeMockStream(); result.connect(stream); @@ -181,19 +175,22 @@ describe('makeBackgroundHostVat', () => { expect(capturedBuildRootObject).not.toBeNull(); }); - // Simulate liveslots calling buildRootObject with kernelFacet - capturedBuildRootObject?.({ kernelFacet: mockKernelFacet }); + // Build root object (liveslots calls this) + const rootObject = capturedBuildRootObject?.() as { + bootstrap: ( + roots: Record, + services: Record, + ) => void; + }; + + // Simulate kernel sending bootstrap message with kernelFacet in services + rootObject.bootstrap({}, { kernelFacet: mockKernelFacet }); expect(await result.kernelFacetPromise).toBe(mockKernelFacet); }); - it('calls user buildRootObject with vatPowers', async () => { - let capturedBuildRootObject: - | (( - vatPowers: Record, - parameters: Record | undefined, - ) => object) - | null = null; + it('rejects if kernelFacet is not provided in services', async () => { + let capturedBuildRootObject: (() => object) | null = null; vi.mocked(SystemVatSupervisor.make).mockImplementation( async (options) => { @@ -203,12 +200,7 @@ describe('makeBackgroundHostVat', () => { }, ); - const userBuildRootObject = vi - .fn() - .mockReturnValue({ myMethod: vi.fn() }); - const result = makeBackgroundHostVat({ - buildRootObject: userBuildRootObject, - }); + const result = makeBackgroundHostVat(); const { stream } = makeMockStream(); result.connect(stream); @@ -217,22 +209,25 @@ describe('makeBackgroundHostVat', () => { expect(capturedBuildRootObject).not.toBeNull(); }); - const vatPowers = { kernelFacet: {}, otherPower: 'test' }; - const rootObject = capturedBuildRootObject?.(vatPowers, { - param: 'value', - }); + const rootObject = capturedBuildRootObject?.() as { + bootstrap: ( + roots: Record, + services: Record, + ) => void; + }; - expect(userBuildRootObject).toHaveBeenCalledWith(vatPowers, { - param: 'value', - }); - expect(rootObject).toHaveProperty('myMethod'); + // Bootstrap without kernelFacet + rootObject.bootstrap({}, {}); + + await expect(result.kernelFacetPromise).rejects.toThrow( + 'kernelFacet not provided in bootstrap services', + ); }); }); describe('delivery handling', () => { it('delivers to supervisor and sends JSON-RPC response back', async () => { - const buildRootObject = vi.fn().mockReturnValue({}); - const result = makeBackgroundHostVat({ buildRootObject }); + const result = makeBackgroundHostVat(); const { stream, written, receiveMessage } = makeMockStream(); result.connect(stream); @@ -273,8 +268,7 @@ describe('makeBackgroundHostVat', () => { it('sends delivery error in response when supervisor.deliver returns error', async () => { mockSupervisor.deliver.mockResolvedValue('Something went wrong'); - const buildRootObject = vi.fn().mockReturnValue({}); - const result = makeBackgroundHostVat({ buildRootObject }); + const result = makeBackgroundHostVat(); const { stream, written, receiveMessage } = makeMockStream(); result.connect(stream); @@ -322,8 +316,7 @@ describe('makeBackgroundHostVat', () => { }, ); - const buildRootObject = vi.fn().mockReturnValue({}); - const result = makeBackgroundHostVat({ buildRootObject }); + const result = makeBackgroundHostVat(); const { stream, written } = makeMockStream(); result.connect(stream); @@ -371,8 +364,7 @@ describe('makeBackgroundHostVat', () => { }, ); - const buildRootObject = vi.fn().mockReturnValue({}); - const result = makeBackgroundHostVat({ buildRootObject }); + const result = makeBackgroundHostVat(); const { stream } = makeMockStream(); result.connect(stream); diff --git a/packages/kernel-browser-runtime/src/host-vat/supervisor-side.ts b/packages/kernel-browser-runtime/src/host-vat/supervisor-side.ts index a619dfc72..5c8e813eb 100644 --- a/packages/kernel-browser-runtime/src/host-vat/supervisor-side.ts +++ b/packages/kernel-browser-runtime/src/host-vat/supervisor-side.ts @@ -4,14 +4,10 @@ import type { } from '@agoric/swingset-liveslots'; import { makePromiseKit } from '@endo/promise-kit'; import { RpcClient, RpcService } from '@metamask/kernel-rpc-methods'; -import { stringify } from '@metamask/kernel-utils'; +import { makeDefaultExo, stringify } from '@metamask/kernel-utils'; import type { JsonRpcMessage } from '@metamask/kernel-utils'; import type { Logger } from '@metamask/logger'; -import type { - DeliveryObject, - KernelFacet, - SystemVatBuildRootObject, -} from '@metamask/ocap-kernel'; +import type { DeliveryObject, KernelFacet } from '@metamask/ocap-kernel'; import { SystemVatSupervisor } from '@metamask/ocap-kernel/vats'; import type { DuplexStream } from '@metamask/streams'; import { isJsonRpcRequest } from '@metamask/utils'; @@ -44,33 +40,25 @@ export type BackgroundHostVatResult = { * The supervisor uses an optimistic syscall model where syscalls are fire-and-forget, * returning ['ok', null] immediately. * + * The kernelFacet is automatically captured from the bootstrap message sent by the + * kernel. You can await `kernelFacetPromise` to get it after calling `connect()`. + * * Usage in background script: * ```typescript - * const hostVat = makeBackgroundHostVat({ - * buildRootObject: (vatPowers) => { - * const kernelFacet = vatPowers.kernelFacet as KernelFacet; - * return makeDefaultExo('BackgroundRoot', { - * // ... methods that use E(kernelFacet) - * }); - * }, - * logger, - * }); - * const stream = await connectToKernelHostVat(); + * const hostVat = makeBackgroundHostVat({ logger }); * hostVat.connect(stream); * const kernelFacet = await hostVat.kernelFacetPromise; * const result = await E(kernelFacet).launchSubcluster(config); * ``` * * @param options - Options for creating the host vat. - * @param options.buildRootObject - Function to build the vat's root object. * @param options.logger - Optional logger for debugging. * @returns The host vat result with connect and kernelFacetPromise. */ -export function makeBackgroundHostVat(options: { - buildRootObject: SystemVatBuildRootObject; +export function makeBackgroundHostVat(options?: { logger?: Logger; }): BackgroundHostVatResult { - const { buildRootObject, logger } = options; + const logger = options?.logger; // Promise kit for kernel facet - resolves when bootstrap is called const kernelFacetKit = makePromiseKit(); @@ -101,21 +89,35 @@ export function makeBackgroundHostVat(options: { }; /** - * Wrap buildRootObject to capture the kernelFacet from bootstrap. + * Build the root object for this host vat. + * + * The root object only needs a bootstrap method to receive the kernelFacet + * from the kernel's bootstrap message. * - * @param vatPowers - The vat powers provided by liveslots. - * @param parameters - Optional parameters for the vat. - * @returns The root object for this vat. + * @returns The root object with a bootstrap method. */ - const wrappedBuildRootObject: SystemVatBuildRootObject = ( - vatPowers, - parameters, - ) => { - // Capture kernelFacet from vatPowers before passing to user's buildRootObject - if (vatPowers.kernelFacet) { - kernelFacetKit.resolve(vatPowers.kernelFacet as KernelFacet); - } - return buildRootObject(vatPowers, parameters); + const buildRootObject = (): object => { + return makeDefaultExo('BackgroundHostVat', { + /** + * Called by the kernel after connection with roots and services. + * Captures the kernelFacet from services. + * + * @param _roots - The roots object (unused). + * @param services - The services object containing kernelFacet. + */ + bootstrap: ( + _roots: Record, + services: Record, + ) => { + if (services.kernelFacet) { + kernelFacetKit.resolve(services.kernelFacet as KernelFacet); + } else { + kernelFacetKit.reject( + new Error('kernelFacet not provided in bootstrap services'), + ); + } + }, + }); }; const connect = ( @@ -132,7 +134,7 @@ export function makeBackgroundHostVat(options: { // Create and start the supervisor const supervisorOptions = { - buildRootObject: wrappedBuildRootObject, + buildRootObject, executeSyscall, ...(logger && { logger: logger.subLogger({ tags: ['supervisor'] }) }), }; 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 af17f4709..35a15e45e 100644 --- a/packages/kernel-browser-runtime/src/kernel-worker/kernel-worker.ts +++ b/packages/kernel-browser-runtime/src/kernel-worker/kernel-worker.ts @@ -58,8 +58,16 @@ async function main(): Promise { new URLSearchParams(globalThis.location.search).get('reset-storage') === 'true'; + // Create host vat first to get config + const hostVat = makeKernelHostVat({ + name: 'kernelHost', + logger: logger.subLogger({ tags: ['host-vat'] }), + }); + + // Pass host vat config to kernel const kernel = await Kernel.make(platformServicesClient, kernelDatabase, { resetStorage, + hostVat: hostVat.config, }); const panelRpcServer = new JsonRpcServer({ @@ -70,12 +78,7 @@ async function main(): Promise { }); panelHandlerKit.resolve(panelRpcServer.handle.bind(panelRpcServer)); - const hostVat = makeKernelHostVat({ - name: 'kernelHost', - logger: logger.subLogger({ tags: ['host-vat'] }), - }); - - // Connect host vat to the background via the message stream + // Connect host vat to the background via the message stream after kernel is created // The background will use makeBackgroundHostVat to create the supervisor side const hostVatStream = messageStream as unknown as Parameters< typeof hostVat.connect diff --git a/yarn.lock b/yarn.lock index 3e72eb6af..1c320f7de 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3475,6 +3475,7 @@ __metadata: "@metamask/kernel-ui": "workspace:^" "@metamask/kernel-utils": "workspace:^" "@metamask/logger": "workspace:^" + "@metamask/ocap-kernel": "workspace:^" "@metamask/streams": "workspace:^" "@ocap/cli": "workspace:^" "@ocap/kernel-test": "workspace:^" From 3f79b0433fd7e1417d8c6b5c47988ac94a6acac0 Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Fri, 30 Jan 2026 13:54:04 -0800 Subject: [PATCH 35/41] fix(extension): Update e2e test kref values for host vat offset The host vat introduction allocates 2 kernel objects (ko3, ko4) before user vats, shifting all user vat kref assignments by +2. Updates hardcoded kref values in e2e tests accordingly. Co-Authored-By: Claude Opus 4.5 --- .../extension/test/e2e/control-panel.test.ts | 28 +++++++++---------- .../test/e2e/object-registry.test.ts | 2 +- .../extension/test/e2e/remote-comms.test.ts | 4 +-- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/packages/extension/test/e2e/control-panel.test.ts b/packages/extension/test/e2e/control-panel.test.ts index 8677aeb5d..e7d18a586 100644 --- a/packages/extension/test/e2e/control-panel.test.ts +++ b/packages/extension/test/e2e/control-panel.test.ts @@ -191,19 +191,19 @@ test.describe('Control Panel', () => { const v3Values = [ '{"key":"e.nextPromiseId.v3","value":"2"}', '{"key":"e.nextObjectId.v3","value":"1"}', - '{"key":"ko5.owner","value":"v3"}', - '{"key":"v3.c.ko5","value":"R o+0"}', - '{"key":"v3.c.o+0","value":"ko5"}', - '{"key":"v3.c.kp4","value":"R p-1"}', - '{"key":"v3.c.p-1","value":"kp4"}', - '{"key":"ko5.refCount","value":"1,1"}', - '{"key":"kp4.refCount","value":"2"}', + '{"key":"ko7.owner","value":"v3"}', + '{"key":"v3.c.ko7","value":"R o+0"}', + '{"key":"v3.c.o+0","value":"ko7"}', + '{"key":"v3.c.kp7","value":"R p-1"}', + '{"key":"v3.c.p-1","value":"kp7"}', + '{"key":"ko7.refCount","value":"1,1"}', + '{"key":"kp7.refCount","value":"2"}', ]; const v1koValues = [ - '{"key":"v1.c.ko4","value":"R o-1"}', - '{"key":"v1.c.o-1","value":"ko4"}', - '{"key":"v1.c.ko5","value":"R o-2"}', - '{"key":"v1.c.o-2","value":"ko5"}', + '{"key":"v1.c.ko6","value":"R o-1"}', + '{"key":"v1.c.o-1","value":"ko6"}', + '{"key":"v1.c.ko7","value":"R o-2"}', + '{"key":"v1.c.o-2","value":"ko7"}', ]; await expect( popupPage.locator('[data-testid="message-output"]'), @@ -263,16 +263,16 @@ test.describe('Control Panel', () => { popupPage.locator('[data-testid="message-output"]'), ).not.toContainText(value); } - // ko3 (vat root) reference still exists for v1 + // ko6, ko7 (bob, carol vat roots) references still exist for v1 for (const value of v1koValues) { await expect( popupPage.locator('[data-testid="message-output"]'), ).toContainText(value); } - // kp4 reference dropped to 1 + // kp7 reference dropped to 1 await expect( popupPage.locator('[data-testid="message-output"]'), - ).toContainText('{"key":"kp4.refCount","value":"1"}'); + ).toContainText('{"key":"kp7.refCount","value":"1"}'); await popupPage.click('button:text("Control Panel")'); await popupPage.locator('[data-testid="accordion-header"]').first().click(); // delete v1 diff --git a/packages/extension/test/e2e/object-registry.test.ts b/packages/extension/test/e2e/object-registry.test.ts index f54038a4a..5a1cedfdc 100644 --- a/packages/extension/test/e2e/object-registry.test.ts +++ b/packages/extension/test/e2e/object-registry.test.ts @@ -108,7 +108,7 @@ test.describe('Object Registry', () => { test('should revoke an object', async () => { const owner = 'v1'; - const v1Root = 'ko3'; + const v1Root = 'ko5'; const [target, method, params] = [v1Root, 'hello', '["Bob"]']; // Before revoking, we should be able to send a message to the object diff --git a/packages/extension/test/e2e/remote-comms.test.ts b/packages/extension/test/e2e/remote-comms.test.ts index b07c3935c..5ed622b6b 100644 --- a/packages/extension/test/e2e/remote-comms.test.ts +++ b/packages/extension/test/e2e/remote-comms.test.ts @@ -118,8 +118,8 @@ test.describe('Remote Communications', () => { await expect(targetSelect).toBeVisible(); const options = await targetSelect.locator('option').all(); expect(options.length).toBeGreaterThan(1); - await targetSelect.selectOption({ value: 'ko3' }); - expect(await targetSelect.inputValue()).toBe('ko3'); + await targetSelect.selectOption({ value: 'ko5' }); + expect(await targetSelect.inputValue()).toBe('ko5'); // Set method to doRunRun (the remote communication method) const methodInput = popupPage1.locator('[data-testid="message-method"]'); From 83ac1426cf8d4ac5c2097f91b22f2b6b16aa2548 Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Fri, 30 Jan 2026 13:02:59 -0800 Subject: [PATCH 36/41] feat(omnium-gatherum): Wire up host vat in background script Replace legacy CapTP connection with makeBackgroundHostVat API: - Update background.ts to use new host vat API - Update controllers/index.ts to use KernelFacet type - kernelFacet is now captured from bootstrap message services Co-Authored-By: Claude Opus 4.5 --- packages/omnium-gatherum/src/background.ts | 61 ++++++------------- .../omnium-gatherum/src/controllers/index.ts | 5 +- packages/omnium-gatherum/src/global.d.ts | 5 +- 3 files changed, 21 insertions(+), 50 deletions(-) diff --git a/packages/omnium-gatherum/src/background.ts b/packages/omnium-gatherum/src/background.ts index 191160ec6..c0566c0e8 100644 --- a/packages/omnium-gatherum/src/background.ts +++ b/packages/omnium-gatherum/src/background.ts @@ -1,12 +1,6 @@ import { E } from '@endo/eventual-send'; -import { - makeBackgroundCapTP, - makeCapTPNotification, - isCapTPNotification, - getCapTPMessage, -} from '@metamask/kernel-browser-runtime'; -import type { CapTPMessage } from '@metamask/kernel-browser-runtime'; -import { delay, isJsonRpcMessage, stringify } from '@metamask/kernel-utils'; +import { makeBackgroundHostVat } 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'; @@ -25,7 +19,7 @@ let bootPromise: Promise | null = null; // With this we can click the extension action button to wake up the service worker. chrome.action.onClicked.addListener(() => { globalThis.kernel !== undefined && - E(globalThis.kernel).ping().catch(logger.error); + E(globalThis.kernel).getStatus().catch(logger.error); }); // Install/update @@ -91,49 +85,28 @@ async function main(): Promise { // Without this delay, sending messages via the chrome.runtime API can fail. await delay(50); + // Create stream for JSON-RPC messages to kernel const offscreenStream = await ChromeRuntimeDuplexStream.make< JsonRpcMessage, JsonRpcMessage >(chrome.runtime, 'background', 'offscreen', isJsonRpcMessage); - const backgroundCapTP = makeBackgroundCapTP({ - send: (captpMessage: CapTPMessage) => { - const notification = makeCapTPNotification(captpMessage); - offscreenStream.write(notification).catch((error) => { - logger.error('Failed to send CapTP message:', error); - }); - }, - }); + // Create host vat - captures kernelFacet from bootstrap automatically + const hostVat = makeBackgroundHostVat({ logger }); - const kernelP = backgroundCapTP.getKernel(); - globalThis.kernel = kernelP; + // Connect to kernel via offscreen pipe + hostVat.connect(offscreenStream); - try { - const controllers = await initializeControllers({ - logger, - kernel: kernelP, - }); - globals.setCapletController(controllers.caplet); - } catch (error) { - offscreenStream.throw(error as Error).catch(logger.error); - } + // Wait for kernel facet (resolves after bootstrap) + const kernelFacet = await hostVat.kernelFacetPromise; + globalThis.kernel = kernelFacet; - try { - await offscreenStream.drain((message) => { - if (isCapTPNotification(message)) { - const captpMessage = getCapTPMessage(message); - backgroundCapTP.dispatch(captpMessage); - } else { - throw new Error(`Unexpected message: ${stringify(message)}`); - } - }); - } catch (error) { - const finalError = new Error('Offscreen connection closed unexpectedly', { - cause: error, - }); - backgroundCapTP.abort(finalError); - throw finalError; - } + // Initialize controllers with kernel facet + const controllers = await initializeControllers({ + logger, + kernel: kernelFacet, + }); + globals.setCapletController(controllers.caplet); } type GlobalSetters = { diff --git a/packages/omnium-gatherum/src/controllers/index.ts b/packages/omnium-gatherum/src/controllers/index.ts index e31664d41..7f1b70164 100644 --- a/packages/omnium-gatherum/src/controllers/index.ts +++ b/packages/omnium-gatherum/src/controllers/index.ts @@ -1,7 +1,6 @@ import { E } from '@endo/eventual-send'; -import type { KernelFacade } from '@metamask/kernel-browser-runtime'; import type { Logger } from '@metamask/logger'; -import type { ClusterConfig } from '@metamask/ocap-kernel'; +import type { ClusterConfig, KernelFacet } from '@metamask/ocap-kernel'; import { CapletController } from './caplet/caplet-controller.ts'; import type { CapletControllerFacet, LaunchResult } from './caplet/index.ts'; @@ -46,7 +45,7 @@ export { type InitializeControllersOptions = { logger: Logger; - kernel: KernelFacade | Promise; + kernel: KernelFacet; }; /** diff --git a/packages/omnium-gatherum/src/global.d.ts b/packages/omnium-gatherum/src/global.d.ts index 545ed5d14..8a0b89ae7 100644 --- a/packages/omnium-gatherum/src/global.d.ts +++ b/packages/omnium-gatherum/src/global.d.ts @@ -1,5 +1,5 @@ -import type { KernelFacade } from '@metamask/kernel-browser-runtime'; import type { Promisified } from '@metamask/kernel-utils'; +import type { KernelFacet } from '@metamask/ocap-kernel'; import type { CapletControllerFacet, @@ -14,7 +14,6 @@ declare global { * * @example * ```typescript - * const kernel = await omnium.getKernel(); * const status = await E(kernel).getStatus(); * ``` */ @@ -22,7 +21,7 @@ declare global { var E: typeof import('@endo/eventual-send').E; // eslint-disable-next-line no-var - var kernel: KernelFacade | Promise; + var kernel: KernelFacet | Promise; // eslint-disable-next-line no-var var omnium: { From 78bead7faef8bdba8a1eafacc723367d6ee23686 Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Fri, 30 Jan 2026 14:34:33 -0800 Subject: [PATCH 37/41] feat(omnium-gatherum): Rename getCapletRoot to getRoot in public API Rename the public API method from `getCapletRoot` to `getRoot` for a cleaner, more idiomatic interface. Add usage documentation showing how to load, install, and interact with caplets using E(). Co-Authored-By: Claude --- packages/omnium-gatherum/README.md | 21 +++++++++++++++++++++ packages/omnium-gatherum/src/background.ts | 2 +- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/packages/omnium-gatherum/README.md b/packages/omnium-gatherum/README.md index 688955bae..9f0b62555 100644 --- a/packages/omnium-gatherum/README.md +++ b/packages/omnium-gatherum/README.md @@ -10,6 +10,27 @@ or `npm install @ocap/omnium-gatherum` +## Usage + +### Installing and using the `echo` caplet + +After loading the extension, open the background console (chrome://extensions → Omnium → "Inspect views: service worker") and run the following: + +```javascript +// 1. Load the echo caplet manifest +const { manifest } = await omnium.caplet.load('echo'); + +// 2. Install the caplet +const { capletId } = await omnium.caplet.install(manifest); + +// 3. Get the caplet's root as an E()-usable presence +const echoRoot = await omnium.caplet.getRoot(capletId); + +// 4. Call the echo method +const result = await E(echoRoot).echo('Hello, world!'); +console.log(result); // "echo: Hello, world!" +``` + ## Contributing This package is part of a monorepo. Instructions for contributing can be found in the [monorepo README](https://github.com/MetaMask/ocap-kernel#readme). diff --git a/packages/omnium-gatherum/src/background.ts b/packages/omnium-gatherum/src/background.ts index c0566c0e8..8b513d8c5 100644 --- a/packages/omnium-gatherum/src/background.ts +++ b/packages/omnium-gatherum/src/background.ts @@ -189,7 +189,7 @@ function defineGlobals(): GlobalSetters { list: async () => E(capletController).list(), load: loadCaplet, get: async (capletId: string) => E(capletController).get(capletId), - getCapletRoot: async (capletId: string) => + getRoot: async (capletId: string) => E(capletController).getCapletRoot(capletId), }), }, From e7aa623d5b6fee620cac87918d62b366057d3c4e Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Fri, 30 Jan 2026 14:43:46 -0800 Subject: [PATCH 38/41] test(nodejs): Add e2e test for host vat promise resolution Test that a vat can await a promise created in the host vat. Creates a deferred promise using makePromiseKit, passes it to the promise-vat, resolves it from the host, and verifies the vat receives the value. Co-Authored-By: Claude --- packages/nodejs/test/e2e/system-vat.test.ts | 28 +++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/packages/nodejs/test/e2e/system-vat.test.ts b/packages/nodejs/test/e2e/system-vat.test.ts index 4066c9d98..b0cbd299d 100644 --- a/packages/nodejs/test/e2e/system-vat.test.ts +++ b/packages/nodejs/test/e2e/system-vat.test.ts @@ -1,4 +1,5 @@ import { E } from '@endo/eventual-send'; +import { makePromiseKit } from '@endo/promise-kit'; import { makeSQLKernelDatabase } from '@metamask/kernel-store/sqlite/nodejs'; import { Logger } from '@metamask/logger'; import type { ClusterConfig, KernelFacet } from '@metamask/ocap-kernel'; @@ -257,6 +258,33 @@ describe('system vat e2e tests', { timeout: 30_000 }, () => { // Rejections from vats throw errors await expect(deferredPromise).rejects.toThrow('error reason'); }); + + it('vat awaits promise created in host vat', async () => { + const config: ClusterConfig = { + bootstrap: 'promiseVat', + vats: { + promiseVat: { + bundleSpec: 'http://localhost:3000/promise-vat.bundle', + }, + }, + }; + + const result = await E(kernelFacet).launchSubcluster(config); + const promiseVat = result.root as unknown as PromiseVat; + + // Create a deferred promise in the host vat (this test) + const { promise, resolve } = makePromiseKit(); + + // Pass the unresolved promise to the vat + const resultPromise = E(promiseVat).awaitPromiseArg(promise); + + // Resolve the deferred promise from the host vat + resolve('host-resolved-value'); + + // The vat should receive the resolved value + const vatResult = await resultPromise; + expect(vatResult).toBe('received: host-resolved-value'); + }); }); describe('kref to presence restoration', () => { From 8a8a63fc154109b796b63ab52277fd1c3871206a Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Fri, 30 Jan 2026 14:45:07 -0800 Subject: [PATCH 39/41] chore: Delete accomplished claude plan --- userspace-as-vat.md | 336 -------------------------------------------- 1 file changed, 336 deletions(-) delete mode 100644 userspace-as-vat.md diff --git a/userspace-as-vat.md b/userspace-as-vat.md deleted file mode 100644 index 5d034010a..000000000 --- a/userspace-as-vat.md +++ /dev/null @@ -1,336 +0,0 @@ -# Plan: System Vats/Subclusters for ocap-kernel - -## Overview - -Enable user space (omnium) to use `E()` on vat object presences directly by making the background (and optionally UI) part of a **system subcluster** called the "host subcluster". - -## Problem Statement - -Currently, the background communicates with the kernel via CapTP and receives kref strings. To call methods on vat objects, it must use `kernel.queueMessage(kref, method, args)` which returns more kref strings. The `rekm/kref-presence` branch attempted to solve this by creating "dummy" presences that forward to `queueMessage()`, but this approach is complex and doesn't integrate well with the kernel's reference management. - -## Solution - -Introduce **system subclusters** - subclusters whose vats run without compartment isolation in the host process. The first system subcluster is the **host subcluster**: - -- **System vats** run without compartments, directly in the host process (e.g., background service worker) -- **System subclusters** are configurable like dynamic subclusters, with a bootstrap vat and optional additional vats -- The **host subcluster** bootstrap vat receives a kernel facet as a vatpower -- The bootstrap vat controls how kernel access is shared with other vats in the subcluster - -This enables: -- E()-callable presences from krefs -- Third-party handoff between vats -- Promise pipelining -- Proper integration with kernel GC - -## Terminology - -- **System vat**: A vat that runs without compartment isolation in the host process -- **System subcluster**: A subcluster composed of system vats -- **Host subcluster**: The specific system subcluster for the host application (omnium) -- **Dynamic vat/subcluster**: Regular vats that run in compartments (existing behavior) - -## Key Constraints - -1. System vats do NOT execute in a compartment - run directly in host process -2. System vats do NOT participate in kernel persistence machinery -3. System subcluster bootstrap vat receives a kernel facet as a vatpower -4. System vats do NOT export durable capabilities -5. Both browser and Node.js runtimes must be supported - -## Architecture - -``` -Host Process (Background/Node.js) Kernel -+---------------------------------------+ +---------------------------+ -| Host Subcluster (System Subcluster) | | | -| +-----------------------------------+ | | | -| | Bootstrap Vat (e.g., background) | | | SystemVatHandle (per vat)| -| | - receives kernel facet vatpower |<--->| - EndpointHandle impl | -| | - uses E() on presences | | | - VRef<->KRef xlat | -| +-----------------------------------+ | | | -| +-----------------------------------+ | | | -| | Other vats (e.g., UI) |<--->| SystemVatHandle (per vat)| -| | - receives refs from bootstrap | | | | -| +-----------------------------------+ | | | -| | | | -| SystemVatSupervisor (per vat) | | KernelRouter | -| - liveslots (no compartment) | | - routes to system vats | -| - dispatch function | | | -| - syscall interface | | KernelFacet service | -+---------------------------------------+ +---------------------------+ -``` - -## Key Design Decisions - -### D1: Liveslots Without Compartment -Run liveslots in the host process WITHOUT compartment isolation. The `buildVatNamespace` callback returns the vat module directly (no `importBundle`). This provides: -- VRef allocation and clist management -- Presence creation for imported objects -- Syscall interface -- Promise tracking - -### D2: System Vat ID Format -Use `sv0`, `sv1`, etc. (prefix "sv" for "system vat") to distinguish from dynamic vats (`v0`, `v1`). - -### D3: Kernel Facet as Vatpower (Bootstrap Only) -The kernel facet is a vatpower passed ONLY to the system subcluster's bootstrap vat: -- `launchSubcluster(config)` - launch dynamic subclusters, returns presences -- `terminateSubcluster(id)` -- `getStatus()` -- Other privileged operations - -Other vats in the system subcluster receive access via normal vat-to-vat communication from bootstrap. - -### D4: Configurable System Subcluster -System subclusters are configurable like dynamic subclusters: -- Define bootstrap vat and additional vats -- Bootstrap vat receives kernel facet vatpower -- Bootstrap message passes roots to all vats in subcluster -- Bootstrap vat controls access distribution - -### D5: Both Runtimes Supported -Implementation must work for both: -- Browser: `packages/kernel-browser-runtime` -- Node.js: `packages/nodejs` - -Core system vat logic in `packages/ocap-kernel` is runtime-agnostic. - -## Implementation Phases - -### Phase 1: Core Infrastructure (packages/ocap-kernel) - -**1.1: Add system vat types to types.ts** -- Add `SystemVatId` type (`sv${number}`) -- Add `isSystemVatId()` type guard -- Update `EndpointId` to include `SystemVatId` -- Add `SystemSubclusterConfig` type (extends ClusterConfig with system vat specifics) - -**1.2: Create SystemVatHandle** -File: `packages/ocap-kernel/src/vats/SystemVatHandle.ts` - -Similar to `VatHandle` but: -- Does NOT manage vatstore persistence (system vats are non-durable) -- Simpler `#getDeliveryCrankResults()` - no vatstore checkpoints -- Communication via callback functions instead of streams (runtime provides transport) - -**1.3: Create SystemVatSyscall** -File: `packages/ocap-kernel/src/vats/SystemVatSyscall.ts` - -Reuse logic from `VatSyscall.ts` but: -- No persistence concerns -- Simpler state tracking - -**1.4: Create KernelFacetService** -File: `packages/ocap-kernel/src/services/KernelFacetService.ts` - -Provides privileged kernel operations as a remotable object: -- `launchSubcluster(config)` - launch dynamic subclusters -- `terminateSubcluster(id)` -- `getStatus()` -- `reloadSubcluster(id)` - -**1.5: Create SystemSubclusterManager** -File: `packages/ocap-kernel/src/vats/SystemSubclusterManager.ts` - -Manages system subclusters: -- Launches system vats with correct vatpowers -- Bootstrap vat receives kernel facet -- Coordinates with SubclusterManager for tracking - -**1.6: Update Kernel.ts** -- Add `launchSystemSubcluster()` method -- Update `#getEndpoint()` to handle system vat IDs -- Register KernelFacetService during initialization -- Accept system vat connection callbacks from runtime - -### Phase 2: Shared System Vat Supervisor (new package or in ocap-kernel) - -**2.1: Create SystemVatSupervisor** -File: `packages/ocap-kernel/src/vats/SystemVatSupervisor.ts` - -Runtime-agnostic supervisor for system vats: -- Uses liveslots without compartment -- `buildVatNamespace` returns provided module directly -- Non-persistent vatstore (Map-based) -- Accepts syscall callback for kernel communication -- Provides dispatch function for deliveries - -This is in ocap-kernel because it's runtime-agnostic - runtimes just provide the transport. - -### Phase 3: Browser Runtime (packages/kernel-browser-runtime) - -**3.1: Create host subcluster utilities** -File: `packages/kernel-browser-runtime/src/host-subcluster/index.ts` - -- `makeHostSubcluster()` factory function -- Sets up SystemVatSupervisor for each vat in host subcluster -- Provides transport (MessagePort) between supervisors and kernel - -**3.2: Update kernel-worker initialization** -File: `packages/kernel-browser-runtime/src/kernel-worker/kernel-worker.ts` - -- Remove CapTP setup -- Accept host subcluster vat connections -- Register SystemVatHandles with kernel - -**3.3: Remove CapTP code** -Files to remove: -- `packages/kernel-browser-runtime/src/background-captp.ts` -- `packages/kernel-browser-runtime/src/kernel-worker/captp/` directory - -### Phase 4: Node.js Runtime (packages/nodejs) - -**4.1: Create host subcluster utilities** -File: `packages/nodejs/src/host-subcluster/index.ts` - -- Similar to browser but using appropriate transport (direct calls or MessageChannel) -- `makeHostSubcluster()` factory function - -**4.2: Update Node.js kernel initialization** -- Support host subcluster configuration -- Register SystemVatHandles - -### Phase 5: Integration - -**5.1: Update omnium-gatherum** -- Use `makeHostSubcluster()` instead of `makeBackgroundCapTP()` -- Background code becomes bootstrap vat's `buildRootObject` -- UI (if in host subcluster) becomes another vat -- Bootstrap vat distributes kernel access as needed - -## Critical Files - -### Files to Create -| File | Purpose | -|------|---------| -| `packages/ocap-kernel/src/vats/SystemVatHandle.ts` | EndpointHandle for system vats (kernel-side) | -| `packages/ocap-kernel/src/vats/SystemVatSyscall.ts` | Syscall handler for system vats | -| `packages/ocap-kernel/src/vats/SystemVatSupervisor.ts` | Liveslots supervisor (runtime-agnostic) | -| `packages/ocap-kernel/src/vats/SystemSubclusterManager.ts` | Manages system subcluster lifecycle | -| `packages/ocap-kernel/src/services/KernelFacetService.ts` | Kernel facet exposed to bootstrap vat | -| `packages/kernel-browser-runtime/src/host-subcluster/index.ts` | Browser host subcluster setup | -| `packages/nodejs/src/host-subcluster/index.ts` | Node.js host subcluster setup | - -### Files to Modify -| File | Changes | -|------|---------| -| `packages/ocap-kernel/src/types.ts` | Add `SystemVatId`, `isSystemVatId()`, `SystemSubclusterConfig` | -| `packages/ocap-kernel/src/Kernel.ts` | Add `launchSystemSubcluster()`, update `#getEndpoint()` | -| `packages/kernel-browser-runtime/src/kernel-worker/kernel-worker.ts` | Remove CapTP, add system vat support | - -### Files to Remove -| File | Reason | -|------|--------| -| `packages/kernel-browser-runtime/src/background-captp.ts` | Replaced by host subcluster | -| `packages/kernel-browser-runtime/src/kernel-worker/captp/` | Replaced by host subcluster | - -## Key Implementation Details - -### buildVatNamespace Without Compartment - -```typescript -// In SystemVatSupervisor -const buildVatNamespace = async ( - lsEndowments: Record, - _inescapableGlobalProperties: object, -): Promise> => { - // NO importBundle - return the system vat module directly - return { - buildRootObject: this.#buildRootObject, - }; -}; -``` - -### System Subcluster Configuration - -```typescript -type SystemSubclusterConfig = { - bootstrap: string; // Name of bootstrap vat - vats: Record; -}; - -// Example usage -const hostSubclusterConfig = { - bootstrap: 'background', - vats: { - background: { buildRootObject: backgroundBuildRootObject }, - ui: { buildRootObject: uiBuildRootObject }, - }, -}; -``` - -### Syscall Flow - -``` -System Vat Code -> E(presence).method(args) - -> liveslots marshals to VRef - -> syscall.send(vref, methargs, resultVRef) - -> SystemVatSupervisor.executeSyscall() - -> [transport callback] -> SystemVatHandle - -> SystemVatSyscall.handleSyscall() (VRef->KRef) - -> KernelQueue.enqueueSend() -``` - -### Delivery Flow - -``` -KernelQueue -> KernelRouter.deliver() - -> SystemVatHandle.deliverMessage(vref, message) - -> [transport callback] -> SystemVatSupervisor - -> liveslots.dispatch(['message', ...]) - -> System vat code method invoked with presence args -``` - -### Host Subcluster Bootstrap - -```typescript -// Bootstrap vat receives kernel facet as vatpower -export function buildRootObject({ kernelFacet }, parameters) { - return makeDefaultExo('hostRoot', { - async bootstrap(roots, services) { - // roots contains presences to other vats in host subcluster - // e.g., roots.ui is the UI vat's root object - - // Launch a dynamic subcluster - const result = await E(kernelFacet).launchSubcluster(dynamicConfig); - // result.root is an E()-callable presence! - - // Pass reference to UI vat if needed - await E(roots.ui).setKernel(kernelFacet); - } - }); -} -``` - -### Obtaining Presences from Kernel Facet - -When `E(kernelFacet).launchSubcluster(config)` is called: -1. KernelFacetService's method calls kernel, gets result with root kref -2. Result serialized with kref in slots via kernel-marshal -3. Delivered to system vat via `deliverNotify` -4. Liveslots sees kref slot, creates presence via c-list -5. Bootstrap vat receives E()-callable presence directly - -## Verification - -### Unit Tests -- `SystemVatHandle` tests: Mock supervisor, test delivery/syscall handling -- `SystemVatSupervisor` tests: Mock kernel connection, test liveslots integration -- `SystemSubclusterManager` tests: Test subcluster lifecycle -- `KernelFacetService` tests: Test service methods - -### Integration Tests -- Full host subcluster lifecycle with real kernel -- Multiple vats in host subcluster communicating -- Bootstrap vat distributing kernel access to other vats -- Third-party handoff between dynamic and system vats -- Promise pipelining through system vats - -### E2E Tests -- Migrate/adapt tests from `rekm/kref-presence` branch -- Test behaviors from `kernel-to-host-captp.test.ts` -- Browser: background + UI as host subcluster -- Node.js: equivalent host subcluster tests From d101b170453fc2df75c063ecc509555d243a2785 Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Fri, 30 Jan 2026 16:13:09 -0800 Subject: [PATCH 40/41] refactor(ocap-kernel): Unify vat and system vat cleanup in cleanupTerminatedVat Extend cleanupTerminatedVat to handle both regular vats and system vats, eliminating the need for a separate cleanupEndpoint function. The function now automatically detects system vats (IDs starting with 'sv') and skips vat-specific operations: - Terminated vat check (system vats aren't in the terminated list) - Vat KV entries cleanup (system vats have no vatstore) - Terminated vats list removal Also simplify orphaned system vat cleanup in Kernel.ts to scan for system vat c-list keys directly instead of using a separate helper. Co-Authored-By: Claude Opus 4.5 --- packages/ocap-kernel/src/Kernel.ts | 67 +++++ .../ocap-kernel/src/store/methods/vat.test.ts | 248 ++++++++++++++++-- packages/ocap-kernel/src/store/methods/vat.ts | 87 +++--- .../src/vats/SystemVatManager.test.ts | 5 +- .../ocap-kernel/src/vats/SystemVatManager.ts | 14 +- 5 files changed, 365 insertions(+), 56 deletions(-) diff --git a/packages/ocap-kernel/src/Kernel.ts b/packages/ocap-kernel/src/Kernel.ts index ca6472476..d3eb58263 100644 --- a/packages/ocap-kernel/src/Kernel.ts +++ b/packages/ocap-kernel/src/Kernel.ts @@ -251,6 +251,10 @@ export class Kernel { this.#remoteManager.handleRemoteMessage(from, message), ); + // Clean up any orphaned system vat state from a previous session. + // This handles crash recovery where disconnect was never called. + this.#cleanupOrphanedSystemVats(); + // 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(); @@ -282,6 +286,69 @@ export class Kernel { }); } + /** + * Clean up orphaned system vat state from a previous session. + * + * System vats are ephemeral - they don't persist across restarts. However, + * their krefs (for owned objects) are persisted to the database. If the + * kernel restarts without properly disconnecting system vats (e.g., crash, + * browser refresh), orphaned state can remain. + * + * This method scans for system vat state and cleans it up before new + * system vats are registered, ensuring a clean slate. + */ + #cleanupOrphanedSystemVats(): void { + // Scan for system vat c-list keys (sv*.c.*) to find orphaned system vats + const orphanedSystemVatIds = new Set(); + const { kv } = this.#kernelStore; + + // Look for c-list entries with system vat prefixes (sv0, sv1, etc.) + // C-list keys use the format: {endpointId}.c.{slot} + let key: string | undefined = 'sv'; + while ((key = kv.getNextKey(key)) !== undefined) { + if (!key.startsWith('sv')) { + break; + } + // Extract the system vat ID from keys like "sv0.c.o+0" + const parts = key.split('.'); + if (parts.length >= 2 && parts[1] === 'c') { + const endpointId = parts[0]; + if (isSystemVatId(endpointId)) { + orphanedSystemVatIds.add(endpointId); + } + } + } + + for (const systemVatId of orphanedSystemVatIds) { + this.#logger.log( + `Cleaning up orphaned system vat state for ${systemVatId}`, + ); + + // Reject pending promises where this vat was the decider + const failure = { body: '"System vat disconnected (orphan cleanup)"' }; + for (const kpid of this.#kernelStore.getPromisesByDecider(systemVatId)) { + // Since there's no active vat, we directly resolve the promise + // in the kernel store rather than going through the queue + this.#kernelStore.resolveKernelPromise(kpid, true, { + ...failure, + slots: [], + }); + } + + // Clean up kernel state: exports, imports, promises, c-list entries + const work = this.#kernelStore.cleanupTerminatedVat(systemVatId); + this.#logger.debug( + `Orphaned system vat ${systemVatId} cleanup: ${work.exports} exports, ${work.imports} imports, ${work.promises} promises`, + ); + } + + if (orphanedSystemVatIds.size > 0) { + this.#logger.log( + `Cleaned up ${orphanedSystemVatIds.size} orphaned system vat(s)`, + ); + } + } + /** * Initialize the remote comms object. * diff --git a/packages/ocap-kernel/src/store/methods/vat.test.ts b/packages/ocap-kernel/src/store/methods/vat.test.ts index 527a0a0f1..50253b57b 100644 --- a/packages/ocap-kernel/src/store/methods/vat.test.ts +++ b/packages/ocap-kernel/src/store/methods/vat.test.ts @@ -261,35 +261,35 @@ describe('vat store methods', () => { describe('deleteEndpoint', () => { it('deletes all keys related to the endpoint', () => { - const endpointId = 'e1'; + const endpointId = 'v1'; - // Setup mock data - mockKV.set(`cle.${endpointId}.obj1`, 'data1'); - mockKV.set(`cle.${endpointId}.obj2`, 'data2'); - mockKV.set(`clk.${endpointId}.prom1`, 'data3'); + // Setup mock data - c-list keys use the format {endpointId}.c.{slot} + mockKV.set(`${endpointId}.c.o+0`, 'ko1'); + mockKV.set(`${endpointId}.c.ko1`, 'R o+0'); + mockKV.set(`${endpointId}.c.p+1`, 'kp1'); mockKV.set(`e.nextObjectId.${endpointId}`, '10'); mockKV.set(`e.nextPromiseId.${endpointId}`, '5'); mockGetPrefixedKeys.mockImplementation((prefix: string) => { - if (prefix === `cle.${endpointId}.`) { - return [`cle.${endpointId}.obj1`, `cle.${endpointId}.obj2`]; - } - if (prefix === `clk.${endpointId}.`) { - return [`clk.${endpointId}.prom1`]; + if (prefix === `${endpointId}.c.`) { + return [ + `${endpointId}.c.o+0`, + `${endpointId}.c.ko1`, + `${endpointId}.c.p+1`, + ]; } return []; }); vatMethods.deleteEndpoint(endpointId); - expect(mockKV.has(`cle.${endpointId}.obj1`)).toBe(false); - expect(mockKV.has(`cle.${endpointId}.obj2`)).toBe(false); - expect(mockKV.has(`clk.${endpointId}.prom1`)).toBe(false); + expect(mockKV.has(`${endpointId}.c.o+0`)).toBe(false); + expect(mockKV.has(`${endpointId}.c.ko1`)).toBe(false); + expect(mockKV.has(`${endpointId}.c.p+1`)).toBe(false); expect(mockKV.has(`e.nextObjectId.${endpointId}`)).toBe(false); expect(mockKV.has(`e.nextPromiseId.${endpointId}`)).toBe(false); - expect(mockGetPrefixedKeys).toHaveBeenCalledWith(`cle.${endpointId}.`); - expect(mockGetPrefixedKeys).toHaveBeenCalledWith(`clk.${endpointId}.`); + expect(mockGetPrefixedKeys).toHaveBeenCalledWith(`${endpointId}.c.`); }); it('does nothing if endpoint has no associated keys', () => { @@ -299,8 +299,7 @@ describe('vat store methods', () => { expect(() => vatMethods.deleteEndpoint(endpointId)).not.toThrow(); - expect(mockGetPrefixedKeys).toHaveBeenCalledWith(`cle.${endpointId}.`); - expect(mockGetPrefixedKeys).toHaveBeenCalledWith(`clk.${endpointId}.`); + expect(mockGetPrefixedKeys).toHaveBeenCalledWith(`${endpointId}.c.`); }); }); @@ -535,4 +534,219 @@ describe('vat store methods', () => { expect(result).toBe(false); }); }); + + describe('cleanupTerminatedVat', () => { + describe('regular vats', () => { + it('returns zero counts when vat is not terminated', () => { + mockTerminatedVats.get.mockReturnValue('[]'); + mockGetPrefixedKeys.mockReturnValue([]); + + const work = vatMethods.cleanupTerminatedVat(vatID1); + + expect(work).toStrictEqual({ + exports: 0, + imports: 0, + promises: 0, + kv: 0, + }); + }); + + it('cleans up vat KV entries for regular vats', () => { + mockTerminatedVats.get.mockReturnValue(JSON.stringify([vatID1])); + + // Set up some vat KV entries + mockKV.set(`${vatID1}.someKey`, 'value'); + + mockGetPrefixedKeys.mockImplementation((prefix: string) => { + if (prefix === `${vatID1}.`) { + return [`${vatID1}.someKey`]; + } + return []; + }); + + const work = vatMethods.cleanupTerminatedVat(vatID1); + + expect(work.kv).toBe(1); + expect(mockKV.has(`${vatID1}.someKey`)).toBe(false); + }); + + it('removes vat from terminated list for regular vats', () => { + mockTerminatedVats.get.mockReturnValue(JSON.stringify([vatID1])); + mockGetPrefixedKeys.mockReturnValue([]); + + vatMethods.cleanupTerminatedVat(vatID1); + + expect(mockTerminatedVats.set).toHaveBeenCalledWith('[]'); + }); + }); + + describe('system vats', () => { + it('cleans up exported objects and removes owner entries', () => { + const systemVatId = 'sv0'; + + // Set up exported object c-list entry (eref->kref) + mockKV.set(`${systemVatId}.c.o+0`, 'ko1'); + // Set up owner entry - use the mock owner key format + mockKV.set('owner.ko1', systemVatId); + + mockGetPrefixedKeys.mockImplementation((prefix: string) => { + if (prefix === `${systemVatId}.c.o+`) { + return [`${systemVatId}.c.o+0`]; + } + return []; + }); + + mockGetReachableAndVatSlot.mockReturnValue({ vatSlot: 'o+0' }); + + const work = vatMethods.cleanupTerminatedVat(systemVatId); + + expect(work.exports).toBe(1); + expect(mockKV.has('owner.ko1')).toBe(false); + expect(mockDecrementRefCount).toHaveBeenCalledWith( + 'ko1', + 'cleanup|export|baseline', + ); + expect(mockMaybeFreeKrefs.add).toHaveBeenCalledWith('ko1'); + }); + + it('cleans up imported objects', () => { + const systemVatId = 'sv0'; + + // Set up imported object c-list entry (eref->kref) + mockKV.set(`${systemVatId}.c.o-1`, 'ko2'); + + mockGetPrefixedKeys.mockImplementation((prefix: string) => { + if (prefix === `${systemVatId}.c.o-`) { + return [`${systemVatId}.c.o-1`]; + } + return []; + }); + + const work = vatMethods.cleanupTerminatedVat(systemVatId); + + expect(work.imports).toBe(1); + expect(mockDeleteCListEntry).toHaveBeenCalledWith( + systemVatId, + 'ko2', + 'o-1', + ); + }); + + it('cleans up promises and decrements decider refcount', () => { + const systemVatId = 'sv0'; + + // Set up promise c-list entry (eref->kref) + mockKV.set(`${systemVatId}.c.p+1`, 'kp1'); + + mockGetPrefixedKeys.mockImplementation((prefix: string) => { + if (prefix === `${systemVatId}.c.p`) { + return [`${systemVatId}.c.p+1`]; + } + return []; + }); + + mockGetKernelPromise.mockReturnValue({ decider: systemVatId }); + + const work = vatMethods.cleanupTerminatedVat(systemVatId); + + expect(work.promises).toBe(1); + expect(mockDeleteCListEntry).toHaveBeenCalledWith( + systemVatId, + 'kp1', + 'p+1', + ); + expect(mockDecrementRefCount).toHaveBeenCalledWith( + 'kp1', + 'cleanup|promise|decider', + ); + }); + + it('does not decrement promise refcount if endpoint is not the decider', () => { + const systemVatId = 'sv0'; + + mockKV.set(`${systemVatId}.c.p+1`, 'kp1'); + + mockGetPrefixedKeys.mockImplementation((prefix: string) => { + if (prefix === `${systemVatId}.c.p`) { + return [`${systemVatId}.c.p+1`]; + } + return []; + }); + + // Different decider + mockGetKernelPromise.mockReturnValue({ decider: 'v1' }); + + const work = vatMethods.cleanupTerminatedVat(systemVatId); + + expect(work.promises).toBe(1); + expect(mockDeleteCListEntry).toHaveBeenCalledWith( + systemVatId, + 'kp1', + 'p+1', + ); + // Should not decrement decider refcount since this endpoint is not the decider + expect(mockDecrementRefCount).not.toHaveBeenCalledWith( + 'kp1', + 'cleanup|promise|decider', + ); + }); + + it('returns zero counts when system vat has no state', () => { + const systemVatId = 'sv99'; + + mockGetPrefixedKeys.mockReturnValue([]); + + const work = vatMethods.cleanupTerminatedVat(systemVatId); + + expect(work).toStrictEqual({ + exports: 0, + imports: 0, + promises: 0, + kv: 0, + }); + }); + + it('skips vat KV cleanup for system vats', () => { + const systemVatId = 'sv0'; + + mockGetPrefixedKeys.mockReturnValue([]); + + const work = vatMethods.cleanupTerminatedVat(systemVatId); + + expect(work.kv).toBe(0); + }); + + it('does not remove from terminated list for system vats', () => { + const systemVatId = 'sv0'; + mockGetPrefixedKeys.mockReturnValue([]); + + vatMethods.cleanupTerminatedVat(systemVatId); + + expect(mockTerminatedVats.set).not.toHaveBeenCalled(); + }); + + it('skips terminated check for system vats', () => { + const systemVatId = 'sv0'; + // System vat is not in the terminated list, but cleanup should still proceed + mockTerminatedVats.get.mockReturnValue('[]'); + + mockKV.set(`${systemVatId}.c.o+0`, 'ko1'); + mockKV.set('owner.ko1', systemVatId); + + mockGetPrefixedKeys.mockImplementation((prefix: string) => { + if (prefix === `${systemVatId}.c.o+`) { + return [`${systemVatId}.c.o+0`]; + } + return []; + }); + + mockGetReachableAndVatSlot.mockReturnValue({ vatSlot: 'o+0' }); + + const work = vatMethods.cleanupTerminatedVat(systemVatId); + + // Should still clean up even though not in terminated list + expect(work.exports).toBe(1); + }); + }); + }); }); diff --git a/packages/ocap-kernel/src/store/methods/vat.ts b/packages/ocap-kernel/src/store/methods/vat.ts index 7e6482cd9..5e690f789 100644 --- a/packages/ocap-kernel/src/store/methods/vat.ts +++ b/packages/ocap-kernel/src/store/methods/vat.ts @@ -6,7 +6,7 @@ import { getObjectMethods } from './object.ts'; import { getPromiseMethods } from './promise.ts'; import { getReachableMethods } from './reachable.ts'; import { getRefCountMethods } from './refcount.ts'; -import { insistEndpointId } from '../../types.ts'; +import { insistEndpointId, isSystemVatId } from '../../types.ts'; import type { EndpointId, KRef, VatConfig, VatId, ERef } from '../../types.ts'; import type { StoreContext, VatCleanupWork } from '../types.ts'; import { parseRef } from '../utils/parse-ref.ts'; @@ -45,13 +45,15 @@ export function getVatMethods(ctx: StoreContext) { /** * Delete all persistent state associated with an endpoint. * + * This deletes c-list entries and endpoint counters. It does NOT handle + * cleaning up kernel objects, refcounts, or other state - use + * cleanupTerminatedVat for full cleanup. + * * @param endpointId - The endpoint whose state is to be deleted. */ function deleteEndpoint(endpointId: EndpointId): void { - for (const key of getPrefixedKeys(`cle.${endpointId}.`)) { - kv.delete(key); - } - for (const key of getPrefixedKeys(`clk.${endpointId}.`)) { + // C-list keys use the format: {endpointId}.c.{slot} + for (const key of getPrefixedKeys(`${endpointId}.c.`)) { kv.delete(key); } kv.delete(`e.nextObjectId.${endpointId}`); @@ -201,24 +203,37 @@ export function getVatMethods(ctx: StoreContext) { } /** - * Cleanup a terminated vat. + * Cleanup a terminated vat or system vat endpoint. + * + * This handles cleanup of: + * - Exported objects: deletes owner, decrements refcounts, marks for GC + * - Imported objects: decrements refcounts via deleteCListEntry + * - Promises: cleans up c-list entries and decrements decider refcounts + * - Vat KV entries (for regular vats only, system vats have no vatstore) + * - Terminated vats list (for regular vats only) * - * @param vatID - The ID of the vat to cleanup. + * For system vats (IDs starting with 'sv'), the terminated check, vat KV cleanup, + * and terminated list removal are automatically skipped. + * + * @param endpointId - The ID of the endpoint to cleanup. * @returns The work done during the cleanup. */ - function cleanupTerminatedVat(vatID: VatId): VatCleanupWork { - const work = { + function cleanupTerminatedVat(endpointId: EndpointId): VatCleanupWork { + const isSystemVat = isSystemVatId(endpointId); + + const work: VatCleanupWork = { exports: 0, imports: 0, promises: 0, kv: 0, }; - if (!isVatTerminated(vatID)) { + // Check if vat is terminated (skip for system vats which aren't in the list) + if (!isSystemVat && !isVatTerminated(endpointId)) { return work; } - const clistPrefix = `${vatID}.c.`; + const clistPrefix = `${endpointId}.c.`; const exportPrefix = `${clistPrefix}o+`; const importPrefix = `${clistPrefix}o-`; const promisePrefix = `${clistPrefix}p`; @@ -243,19 +258,19 @@ export function getVatMethods(ctx: StoreContext) { // must also delete the corresponding kernel owner entry for the object, // since the object will no longer be accessible. assert(key.startsWith(clistPrefix), key); - const vref = key.slice(clistPrefix.length); - assert(vref.startsWith('o+'), vref); + const eref = key.slice(clistPrefix.length); + assert(eref.startsWith('o+'), eref); const kref = ctx.kv.get(key); assert(kref, key); // deletes c-list and .owner, adds to maybeFreeKrefs const ownerKey = getOwnerKey(kref); - const ownerVat = ctx.kv.get(ownerKey); - ownerVat === vatID || Fail`export ${kref} not owned by old vat`; + const owner = ctx.kv.get(ownerKey); + owner === endpointId || Fail`export ${kref} not owned by ${endpointId}`; ctx.kv.delete(ownerKey); - const { vatSlot } = getReachableAndVatSlot(vatID, kref); - ctx.kv.delete(getSlotKey(vatID, kref)); - ctx.kv.delete(getSlotKey(vatID, vatSlot)); - // Decrease refcounts that belonged to the terminating vat + const { vatSlot } = getReachableAndVatSlot(endpointId, kref); + ctx.kv.delete(getSlotKey(endpointId, kref)); + ctx.kv.delete(getSlotKey(endpointId, vatSlot)); + // Decrease refcounts that belonged to the terminating endpoint decrementRefCount(kref, 'cleanup|export|baseline'); ctx.maybeFreeKrefs.add(kref); work.exports += 1; @@ -267,8 +282,8 @@ export function getVatMethods(ctx: StoreContext) { // drop+retire const kref = ctx.kv.get(key) ?? Fail`getNextKey ensures get`; assert(key.startsWith(clistPrefix), key); - const vref = key.slice(clistPrefix.length); - deleteCListEntry(vatID, kref, vref); + const eref = key.slice(clistPrefix.length); + deleteCListEntry(endpointId, kref, eref); // that will also delete both db keys work.imports += 1; } @@ -279,31 +294,35 @@ export function getVatMethods(ctx: StoreContext) { for (const key of getPrefixedKeys(promisePrefix)) { const kref = ctx.kv.get(key) ?? Fail`getNextKey ensures get`; assert(key.startsWith(clistPrefix), key); - const vref = key.slice(clistPrefix.length); + const eref = key.slice(clistPrefix.length); // the following will also delete both db keys - deleteCListEntry(vatID, kref, vref); - // If the dead vat was still the decider, drop the decider’s refcount, too. + deleteCListEntry(endpointId, kref, eref); + // If the dead endpoint was still the decider, drop the decider's refcount, too. const kp = getKernelPromise(kref); - if (kp.decider === vatID) { + if (kp.decider === endpointId) { decrementRefCount(kref, 'cleanup|promise|decider'); } work.promises += 1; } - // Finally, clean up any remaining KV entries for this vat - for (const key of getPrefixedKeys(`${vatID}.`)) { - ctx.kv.delete(key); - work.kv += 1; + // Clean up vat KV entries (skip for system vats which have no vatstore) + if (!isSystemVat) { + for (const key of getPrefixedKeys(`${endpointId}.`)) { + ctx.kv.delete(key); + work.kv += 1; + } } - // Clean up any remaining c-list entries and vat-specific counters - deleteEndpoint(vatID); + // Clean up any remaining c-list entries and endpoint-specific counters + deleteEndpoint(endpointId); - // Remove the vat from the terminated vats list - forgetTerminatedVat(vatID); + // Remove from terminated vats list (skip for system vats) + if (!isSystemVat) { + forgetTerminatedVat(endpointId); + } // Log the cleanup work done - ctx.logger?.debug(`Cleaned up terminated vat ${vatID}:`, work); + ctx.logger?.debug(`Cleaned up endpoint ${endpointId}:`, work); return work; } diff --git a/packages/ocap-kernel/src/vats/SystemVatManager.test.ts b/packages/ocap-kernel/src/vats/SystemVatManager.test.ts index 6779ba1e7..9bfd4f03a 100644 --- a/packages/ocap-kernel/src/vats/SystemVatManager.test.ts +++ b/packages/ocap-kernel/src/vats/SystemVatManager.test.ts @@ -32,6 +32,9 @@ describe('SystemVatManager', () => { addCListEntry: vi.fn(), getPromisesByDecider: vi.fn().mockReturnValue([]), deleteEndpoint: vi.fn(), + cleanupTerminatedVat: vi + .fn() + .mockReturnValue({ exports: 0, imports: 0, promises: 0, kv: 0 }), kv: { get: vi.fn().mockReturnValue(undefined), set: vi.fn(), @@ -291,7 +294,7 @@ describe('SystemVatManager', () => { await manager.registerSystemVat(config); await manager.disconnectSystemVat('sv0'); - expect(mockKernelStore.deleteEndpoint).toHaveBeenCalledWith('sv0'); + expect(mockKernelStore.cleanupTerminatedVat).toHaveBeenCalledWith('sv0'); }); }); }); diff --git a/packages/ocap-kernel/src/vats/SystemVatManager.ts b/packages/ocap-kernel/src/vats/SystemVatManager.ts index 361f60969..5b109aeb8 100644 --- a/packages/ocap-kernel/src/vats/SystemVatManager.ts +++ b/packages/ocap-kernel/src/vats/SystemVatManager.ts @@ -248,8 +248,11 @@ export class SystemVatManager { /** * Disconnect and clean up a system vat. * - * This rejects any pending promises where this vat is the decider and - * cleans up all clist entries and endpoint state. + * This performs full cleanup equivalent to vat termination: + * - Rejects pending promises where this vat is the decider + * - Deletes owned kernel objects (removes owner entries) + * - Decrements reference counts for imported objects + * - Cleans up c-list entries and adds orphaned krefs to GC * * @param systemVatId - The system vat ID to disconnect. */ @@ -266,8 +269,11 @@ export class SystemVatManager { this.#kernelQueue.resolvePromises(systemVatId, [[kpid, true, failure]]); } - // Clean up clist entries and endpoint state - this.#kernelStore.deleteEndpoint(systemVatId); + // Clean up kernel state: exports, imports, promises, c-list entries + const work = this.#kernelStore.cleanupTerminatedVat(systemVatId); + this.#logger.debug( + `System vat ${systemVatId} cleanup: ${work.exports} exports, ${work.imports} imports, ${work.promises} promises`, + ); // Remove the vat record from in-memory tracking this.#systemVats.delete(systemVatId); From 7be05e6c9eb08cae1d70f09610088794931560d2 Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Fri, 30 Jan 2026 16:48:30 -0800 Subject: [PATCH 41/41] refactor(ocap-kernel): Remove logging from kernel-facet Remove all logger?.log() calls from kernel-facet.ts and remove the logger dependency from KernelFacetDependencies. This simplifies the facet by removing verbose logging that adds noise without significant debugging value. - Remove Logger import and logger from dependencies type - Remove logger?.log() calls from all methods - Update tests to remove logging-related test cases - Update Kernel.ts to stop passing logger to kernelFacetDeps Co-Authored-By: Claude --- packages/ocap-kernel/src/Kernel.ts | 1 - packages/ocap-kernel/src/kernel-facet.test.ts | 97 ------------------- packages/ocap-kernel/src/kernel-facet.ts | 22 +---- 3 files changed, 1 insertion(+), 119 deletions(-) diff --git a/packages/ocap-kernel/src/Kernel.ts b/packages/ocap-kernel/src/Kernel.ts index d3eb58263..eec0ceb84 100644 --- a/packages/ocap-kernel/src/Kernel.ts +++ b/packages/ocap-kernel/src/Kernel.ts @@ -180,7 +180,6 @@ export class Kernel { getSubcluster: this.getSubcluster.bind(this), getSubclusters: this.getSubclusters.bind(this), getStatus: this.getStatus.bind(this), - logger: this.#logger.subLogger({ tags: ['KernelFacet'] }), }, registerKernelService: (name, service) => this.#kernelServiceManager.registerKernelServiceObject(name, service), diff --git a/packages/ocap-kernel/src/kernel-facet.test.ts b/packages/ocap-kernel/src/kernel-facet.test.ts index a2ac6b79c..45c8c685d 100644 --- a/packages/ocap-kernel/src/kernel-facet.test.ts +++ b/packages/ocap-kernel/src/kernel-facet.test.ts @@ -1,4 +1,3 @@ -import type { Logger } from '@metamask/logger'; import { describe, it, expect, vi, beforeEach } from 'vitest'; import type { KernelFacetDependencies } from './kernel-facet.ts'; @@ -9,17 +8,8 @@ import type { ClusterConfig, KernelStatus, Subcluster } from './types.ts'; describe('makeKernelFacet', () => { let deps: KernelFacetDependencies; - let logger: Logger; beforeEach(() => { - logger = { - debug: vi.fn(), - error: vi.fn(), - log: vi.fn(), - warn: vi.fn(), - subLogger: vi.fn(() => logger), - } as unknown as Logger; - deps = { launchSubcluster: vi.fn().mockResolvedValue({ subclusterId: 's1', @@ -48,7 +38,6 @@ describe('makeKernelFacet', () => { vatCount: 2, endpointCount: 3, }), - logger, }; }); @@ -90,26 +79,6 @@ describe('makeKernelFacet', () => { // The root is a slot value (remotable) that carries the kref expect(krefOf(result.root)).toBe('ko1'); }); - - it('logs launch events', async () => { - const facet = makeKernelFacet(deps) as { - launchSubcluster: (config: ClusterConfig) => Promise; - }; - const config: ClusterConfig = { - bootstrap: 'myVat', - vats: { myVat: { sourceSpec: 'test.js' } }, - }; - - await facet.launchSubcluster(config); - - expect(logger.log).toHaveBeenCalledWith( - expect.stringContaining('launching subcluster'), - 'myVat', - ); - expect(logger.log).toHaveBeenCalledWith( - expect.stringContaining('launched subcluster s1'), - ); - }); }); describe('terminateSubcluster', () => { @@ -122,21 +91,6 @@ describe('makeKernelFacet', () => { expect(deps.terminateSubcluster).toHaveBeenCalledWith('s1'); }); - - it('logs termination events', async () => { - const facet = makeKernelFacet(deps) as { - terminateSubcluster: (id: string) => Promise; - }; - - await facet.terminateSubcluster('s1'); - - expect(logger.log).toHaveBeenCalledWith( - expect.stringContaining('terminating subcluster s1'), - ); - expect(logger.log).toHaveBeenCalledWith( - expect.stringContaining('terminated subcluster s1'), - ); - }); }); describe('reloadSubcluster', () => { @@ -159,21 +113,6 @@ describe('makeKernelFacet', () => { expect(result.id).toBe('s2'); }); - - it('logs reload events', async () => { - const facet = makeKernelFacet(deps) as { - reloadSubcluster: (id: string) => Promise; - }; - - await facet.reloadSubcluster('s1'); - - expect(logger.log).toHaveBeenCalledWith( - expect.stringContaining('reloading subcluster s1'), - ); - expect(logger.log).toHaveBeenCalledWith( - expect.stringContaining('reloaded subcluster'), - ); - }); }); describe('getSubcluster', () => { @@ -259,40 +198,4 @@ describe('makeKernelFacet', () => { expect(result.endpointCount).toBe(3); }); }); - - describe('without logger', () => { - it('does not throw when logger is undefined', async () => { - const depsWithoutLogger: KernelFacetDependencies = { - launchSubcluster: vi.fn().mockResolvedValue({ - subclusterId: 's1', - bootstrapRootKref: 'ko1', - }), - terminateSubcluster: vi.fn().mockResolvedValue(undefined), - reloadSubcluster: vi.fn().mockResolvedValue({ - id: 's2', - config: { bootstrap: 'test', vats: {} }, - vats: {}, - }), - getSubcluster: vi.fn().mockReturnValue(undefined), - getSubclusters: vi.fn().mockReturnValue([]), - getStatus: vi.fn().mockResolvedValue({ - initialized: true, - cranksExecuted: 0, - cranksPending: 0, - vatCount: 0, - endpointCount: 0, - }), - }; - - const facet = makeKernelFacet(depsWithoutLogger) as { - launchSubcluster: (config: ClusterConfig) => Promise; - }; - - const result = await facet.launchSubcluster({ - bootstrap: 'test', - vats: { test: { sourceSpec: 'test.js' } }, - }); - expect(result).toBeDefined(); - }); - }); }); diff --git a/packages/ocap-kernel/src/kernel-facet.ts b/packages/ocap-kernel/src/kernel-facet.ts index d1f808182..eed9865d6 100644 --- a/packages/ocap-kernel/src/kernel-facet.ts +++ b/packages/ocap-kernel/src/kernel-facet.ts @@ -1,5 +1,4 @@ import { makeDefaultExo } from '@metamask/kernel-utils'; -import type { Logger } from '@metamask/logger'; import type { Kernel } from './Kernel.ts'; import { kslot } from './liveslots/kernel-marshal.ts'; @@ -25,7 +24,6 @@ export type KernelFacetDependencies = Pick< | 'getSubclusters' | 'getStatus' > & { - logger?: Logger; /** Optional system vat manager for dynamic registration. */ systemVatManager?: Pick; }; @@ -137,7 +135,6 @@ export function makeKernelFacet(deps: KernelFacetDependencies): KernelFacet { getSubcluster, getSubclusters, getStatus, - logger, systemVatManager, } = deps; @@ -151,14 +148,7 @@ export function makeKernelFacet(deps: KernelFacetDependencies): KernelFacet { async launchSubcluster( config: ClusterConfig, ): Promise { - logger?.log(`kernelFacet: launching subcluster`, config.bootstrap); const result = await launchSubcluster(config); - logger?.log( - `kernelFacet: launched subcluster ${result.subclusterId} with root ${result.bootstrapRootKref}`, - ); - - // Convert the kref to a slot value that will become a presence - // when marshalled/delivered to the system vat return { subclusterId: result.subclusterId, root: kslot(result.bootstrapRootKref, 'vatRoot'), @@ -172,9 +162,7 @@ export function makeKernelFacet(deps: KernelFacetDependencies): KernelFacet { * @param subclusterId - ID of the subcluster to terminate. */ async terminateSubcluster(subclusterId: string): Promise { - logger?.log(`kernelFacet: terminating subcluster ${subclusterId}`); await terminateSubcluster(subclusterId); - logger?.log(`kernelFacet: terminated subcluster ${subclusterId}`); }, /** @@ -184,10 +172,7 @@ export function makeKernelFacet(deps: KernelFacetDependencies): KernelFacet { * @returns The reloaded subcluster information. */ async reloadSubcluster(subclusterId: string): Promise { - logger?.log(`kernelFacet: reloading subcluster ${subclusterId}`); - const result = await reloadSubcluster(subclusterId); - logger?.log(`kernelFacet: reloaded subcluster, new id: ${result.id}`); - return result; + return reloadSubcluster(subclusterId); }, /** @@ -233,12 +218,7 @@ export function makeKernelFacet(deps: KernelFacetDependencies): KernelFacet { 'Cannot register system vat: systemVatManager not provided to kernel facet', ); } - logger?.log(`kernelFacet: registering system vat ${config.name}`); const result = await systemVatManager.registerSystemVat(config); - logger?.log( - `kernelFacet: registered system vat ${result.systemVatId} with root ${result.rootKref}`, - ); - return { systemVatId: result.systemVatId, root: kslot(result.rootKref, 'vatRoot'),