Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
3c78bee
feat(omnium): Add Phase 1a - Single echo caplet implementation
rekmarks Jan 10, 2026
47d276f
feat(omnium): Add Phase 1b - Store and retrieve caplet root krefs
rekmarks Jan 10, 2026
1dffc1a
fix(omnium): Fix TypeScript type errors in Phase 1b implementation
rekmarks Jan 12, 2026
3894833
refactor(omnium): Simplify LaunchResult and remove KrefWrapper
rekmarks Jan 12, 2026
3b7f375
test(kernel-browser-runtime): Add error case tests for launchSubcluster
rekmarks Jan 12, 2026
335ce4d
feat(omnium): Expose caplet manifests in background console
rekmarks Jan 12, 2026
c43d32c
feat(omnium): Add loadCaplet method and fix vat bootstrap kref
rekmarks Jan 13, 2026
0446603
fix: Fix launch-subcluster RPC result type for JSON compatibility
rekmarks Jan 14, 2026
66356ca
test: Fix test failures
rekmarks Jan 15, 2026
cd8014e
refactor(omnium): omnium.loadCaplet -> omnium.caplet.load
rekmarks Jan 15, 2026
2de71e8
fix: Fix nodejs test helper
rekmarks Jan 16, 2026
355e5fe
fix: Fix another nodejs test helper
rekmarks Jan 16, 2026
0e06cb4
chore: Remove unused dependency from nodejs
rekmarks Jan 16, 2026
33727fa
refactor: Post-rebase fixup
rekmarks Jan 22, 2026
4b58ada
test(caplet-controller): Add unit tests for getCapletRoot method
rekmarks Jan 22, 2026
51ef13a
refactor(omnium): Colocate caplet files and complete manifest
rekmarks Jan 23, 2026
b9089b5
refactor: Remove unused import / export tables
rekmarks Jan 23, 2026
1aeb855
refactor(echo-caplet): Use console.log instead of vatPowers
rekmarks Jan 23, 2026
bc6e395
refactor: Cleanup
rekmarks Jan 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,7 @@ const config = createConfig([
{
files: [
'packages/*/src/**/vats/**/*',
'packages/*/src/**/caplets/**/*.js',
'packages/*/test/**/vats/**/*',
'packages/nodejs/test/workers/**/*',
'packages/logger/test/workers/**/*',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,12 @@ describe('CapTP Integration', () => {
// Create mock kernel with method implementations
mockKernel = {
launchSubcluster: vi.fn().mockResolvedValue({
body: '#{"rootKref":"ko1"}',
slots: ['ko1'],
subclusterId: 'sc1',
bootstrapRootKref: 'ko1',
bootstrapResult: {
body: '#{"result":"ok"}',
slots: [],
},
}),
terminateSubcluster: vi.fn().mockResolvedValue(undefined),
queueMessage: vi.fn().mockResolvedValue({
Expand Down Expand Up @@ -113,9 +117,11 @@ describe('CapTP Integration', () => {

// Call launchSubcluster via E()
const result = await E(kernel).launchSubcluster(config);

// The kernel facade now returns LaunchResult instead of CapData
expect(result).toStrictEqual({
body: '#{"rootKref":"ko1"}',
slots: ['ko1'],
subclusterId: 'sc1',
rootKref: 'ko1',
});

expect(mockKernel.launchSubcluster).toHaveBeenCalledWith(config);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ describe('makeKernelFacade', () => {
beforeEach(() => {
mockKernel = {
launchSubcluster: vi.fn().mockResolvedValue({
body: '#{"status":"ok"}',
slots: [],
subclusterId: 'sc1',
bootstrapRootKref: 'ko1',
}),
terminateSubcluster: vi.fn().mockResolvedValue(undefined),
queueMessage: vi.fn().mockResolvedValue({
Expand Down Expand Up @@ -60,16 +60,24 @@ describe('makeKernelFacade', () => {
expect(mockKernel.launchSubcluster).toHaveBeenCalledTimes(1);
});

it('returns result from kernel', async () => {
const expectedResult = { body: '#{"rootObject":"ko1"}', slots: ['ko1'] };
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(
expectedResult,
kernelResult,
);

const config: ClusterConfig = makeClusterConfig();

const result = await facade.launchSubcluster(config);
expect(result).toStrictEqual(expectedResult);

expect(result).toStrictEqual({
subclusterId: 's1',
rootKref: 'ko1',
});
});

it('propagates errors from kernel', async () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { makeDefaultExo } from '@metamask/kernel-utils/exo';
import type { Kernel, ClusterConfig, KRef, VatId } from '@metamask/ocap-kernel';

import type { KernelFacade } from '../../types.ts';
import type { KernelFacade, LaunchResult } from '../../types.ts';

export type { KernelFacade } from '../../types.ts';

Expand All @@ -15,8 +15,10 @@ export function makeKernelFacade(kernel: Kernel): KernelFacade {
return makeDefaultExo('KernelFacade', {
ping: async () => 'pong' as const,

launchSubcluster: async (config: ClusterConfig) => {
return kernel.launchSubcluster(config);
launchSubcluster: async (config: ClusterConfig): Promise<LaunchResult> => {
const { subclusterId, bootstrapRootKref } =
await kernel.launchSubcluster(config);
return { subclusterId, rootKref: bootstrapRootKref };
},

terminateSubcluster: async (subclusterId: string) => {
Expand All @@ -34,6 +36,12 @@ export function makeKernelFacade(kernel: Kernel): KernelFacade {
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 };
},
Comment on lines +41 to +44
Copy link
Member Author

@rekmarks rekmarks Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As Bugbot so astutely points out, this doesn't work, and won't work until #754.

});
}
harden(makeKernelFacade);
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,14 @@ import { describe, it, expect, vi } from 'vitest';
import { launchSubclusterHandler } from './launch-subcluster.ts';

describe('launchSubclusterHandler', () => {
it('should call kernel.launchSubcluster with the provided config', async () => {
it('calls kernel.launchSubcluster with the provided config', async () => {
const mockResult = {
subclusterId: 's1',
bootstrapRootKref: 'ko1',
bootstrapResult: { body: '#null', slots: [] },
};
const mockKernel = {
launchSubcluster: vi.fn().mockResolvedValue(undefined),
launchSubcluster: vi.fn().mockResolvedValue(mockResult),
};
const params = {
config: {
Expand All @@ -20,9 +25,14 @@ describe('launchSubclusterHandler', () => {
expect(mockKernel.launchSubcluster).toHaveBeenCalledWith(params.config);
});

it('should return null when kernel.launchSubcluster returns undefined', async () => {
it('returns the result from kernel.launchSubcluster', async () => {
const mockResult = {
subclusterId: 's1',
bootstrapRootKref: 'ko1',
bootstrapResult: { body: '#{"result":"ok"}', slots: [] },
};
const mockKernel = {
launchSubcluster: vi.fn().mockResolvedValue(undefined),
launchSubcluster: vi.fn().mockResolvedValue(mockResult),
};
const params = {
config: {
Expand All @@ -34,11 +44,15 @@ describe('launchSubclusterHandler', () => {
{ kernel: mockKernel },
params,
);
expect(result).toBeNull();
expect(result).toStrictEqual(mockResult);
});

it('should return the result from kernel.launchSubcluster when not undefined', async () => {
const mockResult = { body: 'test', slots: [] };
it('converts undefined bootstrapResult to null for JSON compatibility', async () => {
const mockResult = {
subclusterId: 's1',
bootstrapRootKref: 'ko1',
bootstrapResult: undefined,
};
const mockKernel = {
launchSubcluster: vi.fn().mockResolvedValue(mockResult),
};
Expand All @@ -52,6 +66,10 @@ describe('launchSubclusterHandler', () => {
{ kernel: mockKernel },
params,
);
expect(result).toBe(mockResult);
expect(result).toStrictEqual({
subclusterId: 's1',
bootstrapRootKref: 'ko1',
bootstrapResult: null,
});
});
});
Original file line number Diff line number Diff line change
@@ -1,17 +1,38 @@
import type { CapData } from '@endo/marshal';
import type { MethodSpec, Handler } from '@metamask/kernel-rpc-methods';
import type { Kernel, ClusterConfig, KRef } from '@metamask/ocap-kernel';
import { CapDataStruct, ClusterConfigStruct } from '@metamask/ocap-kernel';
import { object, nullable } from '@metamask/superstruct';
import { ClusterConfigStruct, CapDataStruct } from '@metamask/ocap-kernel';
import {
object,
string,
nullable,
type as structType,
} from '@metamask/superstruct';

/**
* JSON-compatible version of SubclusterLaunchResult for RPC.
* Uses null instead of undefined for JSON serialization.
*/
type LaunchSubclusterRpcResult = {
subclusterId: string;
bootstrapRootKref: string;
bootstrapResult: CapData<KRef> | null;
};

const LaunchSubclusterRpcResultStruct = structType({
subclusterId: string(),
bootstrapRootKref: string(),
bootstrapResult: nullable(CapDataStruct),
});

export const launchSubclusterSpec: MethodSpec<
'launchSubcluster',
{ config: ClusterConfig },
Promise<CapData<KRef> | null>
Promise<LaunchSubclusterRpcResult>
> = {
method: 'launchSubcluster',
params: object({ config: ClusterConfigStruct }),
result: nullable(CapDataStruct),
result: LaunchSubclusterRpcResultStruct,
};

export type LaunchSubclusterHooks = {
Expand All @@ -21,16 +42,21 @@ export type LaunchSubclusterHooks = {
export const launchSubclusterHandler: Handler<
'launchSubcluster',
{ config: ClusterConfig },
Promise<CapData<KRef> | null>,
Promise<LaunchSubclusterRpcResult>,
LaunchSubclusterHooks
> = {
...launchSubclusterSpec,
hooks: { kernel: true },
implementation: async (
{ kernel }: LaunchSubclusterHooks,
params: { config: ClusterConfig },
): Promise<CapData<KRef> | null> => {
): Promise<LaunchSubclusterRpcResult> => {
const result = await kernel.launchSubcluster(params.config);
return result ?? null;
// Convert undefined to null for JSON compatibility
return {
subclusterId: result.subclusterId,
bootstrapRootKref: result.bootstrapRootKref,
bootstrapResult: result.bootstrapResult ?? null,
};
},
};
15 changes: 13 additions & 2 deletions packages/kernel-browser-runtime/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,32 @@
import type { Kernel } from '@metamask/ocap-kernel';
import type { Kernel, ClusterConfig } from '@metamask/ocap-kernel';
import type { Json } from '@metamask/utils';

/**
* A CapTP message that can be sent over the wire.
*/
export type CapTPMessage = Record<string, Json>;

/**
* Result of launching a subcluster.
*
* The rootKref contains the kref string for the bootstrap vat's root object.
*/
export type LaunchResult = {
subclusterId: string;
rootKref: string;
};

/**
* The kernel facade interface - methods exposed to userspace via CapTP.
*
* This is the remote presence type that the background receives from the kernel.
*/
export type KernelFacade = {
ping: () => Promise<'pong'>;
launchSubcluster: Kernel['launchSubcluster'];
launchSubcluster: (config: ClusterConfig) => Promise<LaunchResult>;
terminateSubcluster: Kernel['terminateSubcluster'];
queueMessage: Kernel['queueMessage'];
getStatus: Kernel['getStatus'];
pingVat: Kernel['pingVat'];
getVatRoot: (krefString: string) => Promise<unknown>;
};
6 changes: 3 additions & 3 deletions packages/kernel-test/src/liveslots.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,14 +67,14 @@ describe('liveslots promise handling', () => {
testName: string,
): Promise<unknown> {
const bundleSpec = getBundleSpec(bundleName);
const bootstrapResultRaw = await kernel.launchSubcluster(
const { bootstrapResult } = await kernel.launchSubcluster(
makeTestSubcluster(testName, bundleSpec),
);
await waitUntilQuiescent(1000);
if (bootstrapResultRaw === undefined) {
if (bootstrapResult === undefined) {
throw Error(`this can't happen but eslint is stupid`);
}
return kunser(bootstrapResultRaw);
return kunser(bootstrapResult);
}

it('promiseArg1: send promise parameter, resolve after send', async () => {
Expand Down
2 changes: 1 addition & 1 deletion packages/kernel-test/src/persistence.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ describe('persistent storage', { timeout: 20_000 }, () => {
false,
logger.logger.subLogger({ tags: ['test'] }),
);
const bootstrapResult = await kernel1.launchSubcluster(testSubcluster);
const { bootstrapResult } = await kernel1.launchSubcluster(testSubcluster);
expect(kunser(bootstrapResult as CapData<string>)).toBe(
'Counter initialized with count: 1',
);
Expand Down
6 changes: 3 additions & 3 deletions packages/kernel-test/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,12 @@ export async function runTestVats(
kernel: Kernel,
config: ClusterConfig,
): Promise<unknown> {
const bootstrapResultRaw = await kernel.launchSubcluster(config);
const { bootstrapResult } = await kernel.launchSubcluster(config);
await waitUntilQuiescent();
if (bootstrapResultRaw === undefined) {
if (bootstrapResult === undefined) {
throw Error(`this can't happen but eslint is stupid`);
}
return kunser(bootstrapResultRaw);
return kunser(bootstrapResult);
}

/**
Expand Down
1 change: 0 additions & 1 deletion packages/nodejs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@
},
"dependencies": {
"@endo/eventual-send": "^1.3.4",
"@endo/marshal": "^1.8.0",
"@endo/promise-kit": "^1.1.13",
"@libp2p/interface": "2.11.0",
"@libp2p/webrtc": "5.2.24",
Expand Down
7 changes: 2 additions & 5 deletions packages/nodejs/test/helpers/kernel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,7 @@ export async function runTestVats(
kernel: Kernel,
config: ClusterConfig,
): Promise<unknown> {
const bootstrapResultRaw = await kernel.launchSubcluster(config);
const { bootstrapResult } = await kernel.launchSubcluster(config);
await waitUntilQuiescent();
if (bootstrapResultRaw === undefined) {
throw Error(`this can't happen but eslint is stupid`);
}
return kunser(bootstrapResultRaw);
return bootstrapResult && kunser(bootstrapResult);
}
11 changes: 8 additions & 3 deletions packages/nodejs/test/helpers/remote-comms.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { CapData } from '@endo/marshal';
import type { KernelDatabase } from '@metamask/kernel-store';
import { stringify } from '@metamask/kernel-utils';
import { Kernel, kunser, makeKernelStore } from '@metamask/ocap-kernel';
import type { ClusterConfig, KRef } from '@metamask/ocap-kernel';

Expand Down Expand Up @@ -58,8 +58,13 @@ export async function launchVatAndGetURL(
kernel: Kernel,
config: ClusterConfig,
): Promise<string> {
const result = await kernel.launchSubcluster(config);
return kunser(result as CapData<KRef>) as string;
const { bootstrapResult } = await kernel.launchSubcluster(config);
if (!bootstrapResult) {
throw new Error(
`No bootstrap result for vat "${config.bootstrap}" with config ${stringify(config)}`,
);
}
return kunser(bootstrapResult) as string;
}

/**
Expand Down
6 changes: 5 additions & 1 deletion packages/ocap-kernel/src/Kernel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,11 @@ describe('Kernel', () => {
);
const config = makeMockClusterConfig();
const result = await kernel.launchSubcluster(config);
expect(result).toStrictEqual({ body: '{"result":"ok"}', slots: [] });
expect(result).toStrictEqual({
subclusterId: 's1',
bootstrapResult: { body: '{"result":"ok"}', slots: [] },
bootstrapRootKref: expect.stringMatching(/^ko\d+$/u),
});
});
});

Expand Down
Loading
Loading