Skip to content

Commit e69fbb1

Browse files
rekmarksclaude
andcommitted
feat(omnium): Add loadCaplet method and fix vat bootstrap kref
- Add omnium.loadCaplet(id) to dynamically fetch caplet manifest and bundle - Fix vatPowers.logger missing in browser vats (iframe.ts) - Fix SubclusterLaunchResult to return bootstrapRootKref directly instead of trying to extract it from bootstrap() return slots The bootstrapRootKref is the kref of the vat root object, which is already known when the vat launches. Previously we incorrectly tried to get it from the slots of the bootstrap() method return value. Next step: Wire up CapTP marshalling so E(root).echo() works with the caplet root presence. Co-Authored-By: Claude Opus 4.5 <[email protected]>
1 parent bd1d749 commit e69fbb1

File tree

19 files changed

+183
-148
lines changed

19 files changed

+183
-148
lines changed

packages/kernel-browser-runtime/src/kernel-worker/captp/captp.integration.test.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,12 @@ describe('CapTP Integration', () => {
2525
// Create mock kernel with method implementations
2626
mockKernel = {
2727
launchSubcluster: vi.fn().mockResolvedValue({
28-
body: '#{"subclusterId":"sc1"}',
29-
slots: ['ko1'],
28+
subclusterId: 'sc1',
29+
bootstrapRootKref: 'ko1',
30+
bootstrapResult: {
31+
body: '#{"result":"ok"}',
32+
slots: [],
33+
},
3034
}),
3135
terminateSubcluster: vi.fn().mockResolvedValue(undefined),
3236
queueMessage: vi.fn().mockResolvedValue({

packages/kernel-browser-runtime/src/kernel-worker/captp/kernel-facade.ts

Lines changed: 3 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -16,32 +16,9 @@ export function makeKernelFacade(kernel: Kernel): KernelFacade {
1616
ping: async () => 'pong' as const,
1717

1818
launchSubcluster: async (config: ClusterConfig): Promise<LaunchResult> => {
19-
const capData = await kernel.launchSubcluster(config);
20-
21-
// A subcluster always has a bootstrap vat with a root object
22-
if (!capData) {
23-
throw new Error('launchSubcluster: expected capData with root kref');
24-
}
25-
26-
// Parse the CapData body (format: "#..." where # prefix indicates JSON)
27-
const bodyJson = capData.body.startsWith('#')
28-
? capData.body.slice(1)
29-
: capData.body;
30-
const body = JSON.parse(bodyJson) as { subclusterId?: string };
31-
if (!body.subclusterId) {
32-
throw new Error('launchSubcluster: expected subclusterId in body');
33-
}
34-
35-
// Extract root kref from slots (first slot is bootstrap vat's root object)
36-
const rootKref = capData.slots[0];
37-
if (!rootKref) {
38-
throw new Error('launchSubcluster: expected root kref in slots');
39-
}
40-
41-
return {
42-
subclusterId: body.subclusterId,
43-
rootKref,
44-
};
19+
const { subclusterId, bootstrapRootKref } =
20+
await kernel.launchSubcluster(config);
21+
return { subclusterId, rootKref: bootstrapRootKref };
4522
},
4623

4724
terminateSubcluster: async (subclusterId: string) => {

packages/kernel-browser-runtime/src/rpc-handlers/launch-subcluster.test.ts

Lines changed: 12 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,14 @@ import { describe, it, expect, vi } from 'vitest';
33
import { launchSubclusterHandler } from './launch-subcluster.ts';
44

55
describe('launchSubclusterHandler', () => {
6-
it('should call kernel.launchSubcluster with the provided config', async () => {
6+
it('calls kernel.launchSubcluster with the provided config', async () => {
7+
const mockResult = {
8+
subclusterId: 's1',
9+
bootstrapRootKref: 'ko1',
10+
bootstrapResult: { body: '#null', slots: [] },
11+
};
712
const mockKernel = {
8-
launchSubcluster: vi.fn().mockResolvedValue(undefined),
13+
launchSubcluster: vi.fn().mockResolvedValue(mockResult),
914
};
1015
const params = {
1116
config: {
@@ -20,25 +25,12 @@ describe('launchSubclusterHandler', () => {
2025
expect(mockKernel.launchSubcluster).toHaveBeenCalledWith(params.config);
2126
});
2227

23-
it('should return null when kernel.launchSubcluster returns undefined', async () => {
24-
const mockKernel = {
25-
launchSubcluster: vi.fn().mockResolvedValue(undefined),
28+
it('returns the result from kernel.launchSubcluster', async () => {
29+
const mockResult = {
30+
subclusterId: 's1',
31+
bootstrapRootKref: 'ko1',
32+
bootstrapResult: { body: '#{"result":"ok"}', slots: [] },
2633
};
27-
const params = {
28-
config: {
29-
bootstrap: 'test-bootstrap',
30-
vats: {},
31-
},
32-
};
33-
const result = await launchSubclusterHandler.implementation(
34-
{ kernel: mockKernel },
35-
params,
36-
);
37-
expect(result).toBeNull();
38-
});
39-
40-
it('should return the result from kernel.launchSubcluster when not undefined', async () => {
41-
const mockResult = { body: 'test', slots: [] };
4234
const mockKernel = {
4335
launchSubcluster: vi.fn().mockResolvedValue(mockResult),
4436
};
Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,31 @@
1-
import type { CapData } from '@endo/marshal';
21
import type { MethodSpec, Handler } from '@metamask/kernel-rpc-methods';
3-
import type { Kernel, ClusterConfig, KRef } from '@metamask/ocap-kernel';
4-
import { CapDataStruct, ClusterConfigStruct } from '@metamask/ocap-kernel';
5-
import { object, nullable } from '@metamask/superstruct';
2+
import type {
3+
Kernel,
4+
ClusterConfig,
5+
SubclusterLaunchResult,
6+
} from '@metamask/ocap-kernel';
7+
import { ClusterConfigStruct, CapDataStruct } from '@metamask/ocap-kernel';
8+
import {
9+
object,
10+
string,
11+
optional,
12+
type as structType,
13+
} from '@metamask/superstruct';
14+
15+
const SubclusterLaunchResultStruct = structType({
16+
subclusterId: string(),
17+
bootstrapRootKref: string(),
18+
bootstrapResult: optional(CapDataStruct),
19+
});
620

721
export const launchSubclusterSpec: MethodSpec<
822
'launchSubcluster',
923
{ config: ClusterConfig },
10-
Promise<CapData<KRef> | null>
24+
Promise<SubclusterLaunchResult>
1125
> = {
1226
method: 'launchSubcluster',
1327
params: object({ config: ClusterConfigStruct }),
14-
result: nullable(CapDataStruct),
28+
result: SubclusterLaunchResultStruct,
1529
};
1630

1731
export type LaunchSubclusterHooks = {
@@ -21,16 +35,15 @@ export type LaunchSubclusterHooks = {
2135
export const launchSubclusterHandler: Handler<
2236
'launchSubcluster',
2337
{ config: ClusterConfig },
24-
Promise<CapData<KRef> | null>,
38+
Promise<SubclusterLaunchResult>,
2539
LaunchSubclusterHooks
2640
> = {
2741
...launchSubclusterSpec,
2842
hooks: { kernel: true },
2943
implementation: async (
3044
{ kernel }: LaunchSubclusterHooks,
3145
params: { config: ClusterConfig },
32-
): Promise<CapData<KRef> | null> => {
33-
const result = await kernel.launchSubcluster(params.config);
34-
return result ?? null;
46+
): Promise<SubclusterLaunchResult> => {
47+
return kernel.launchSubcluster(params.config);
3548
},
3649
};

packages/kernel-browser-runtime/src/vat/iframe.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,15 @@ async function main(): Promise<void> {
2828

2929
const urlParams = new URLSearchParams(window.location.search);
3030
const vatId = urlParams.get('vatId') ?? 'unknown';
31+
const vatLogger = logger.subLogger(vatId);
3132

3233
// eslint-disable-next-line no-new
3334
new VatSupervisor({
3435
id: vatId,
3536
kernelStream,
36-
logger: logger.subLogger(vatId),
37+
logger: vatLogger,
3738
makePlatform,
39+
vatPowers: { logger: vatLogger },
3840
});
3941

4042
logger.info('VatSupervisor initialized with vatId:', vatId);

packages/kernel-test/src/liveslots.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,14 +67,14 @@ describe('liveslots promise handling', () => {
6767
testName: string,
6868
): Promise<unknown> {
6969
const bundleSpec = getBundleSpec(bundleName);
70-
const bootstrapResultRaw = await kernel.launchSubcluster(
70+
const { bootstrapResult } = await kernel.launchSubcluster(
7171
makeTestSubcluster(testName, bundleSpec),
7272
);
7373
await waitUntilQuiescent(1000);
74-
if (bootstrapResultRaw === undefined) {
74+
if (bootstrapResult === undefined) {
7575
throw Error(`this can't happen but eslint is stupid`);
7676
}
77-
return kunser(bootstrapResultRaw);
77+
return kunser(bootstrapResult);
7878
}
7979

8080
it('promiseArg1: send promise parameter, resolve after send', async () => {

packages/kernel-test/src/persistence.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ describe('persistent storage', { timeout: 20_000 }, () => {
155155
false,
156156
logger.logger.subLogger({ tags: ['test'] }),
157157
);
158-
const bootstrapResult = await kernel1.launchSubcluster(testSubcluster);
158+
const { bootstrapResult } = await kernel1.launchSubcluster(testSubcluster);
159159
expect(kunser(bootstrapResult as CapData<string>)).toBe(
160160
'Counter initialized with count: 1',
161161
);

packages/kernel-test/src/utils.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,12 @@ export async function runTestVats(
3737
kernel: Kernel,
3838
config: ClusterConfig,
3939
): Promise<unknown> {
40-
const bootstrapResultRaw = await kernel.launchSubcluster(config);
40+
const { bootstrapResult } = await kernel.launchSubcluster(config);
4141
await waitUntilQuiescent();
42-
if (bootstrapResultRaw === undefined) {
42+
if (bootstrapResult === undefined) {
4343
throw Error(`this can't happen but eslint is stupid`);
4444
}
45-
return kunser(bootstrapResultRaw);
45+
return kunser(bootstrapResult);
4646
}
4747

4848
/**

packages/ocap-kernel/src/Kernel.test.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -288,7 +288,11 @@ describe('Kernel', () => {
288288
);
289289
const config = makeMockClusterConfig();
290290
const result = await kernel.launchSubcluster(config);
291-
expect(result).toStrictEqual({ body: '{"result":"ok"}', slots: [] });
291+
expect(result).toMatchObject({
292+
subclusterId: 's1',
293+
bootstrapResult: { body: '{"result":"ok"}', slots: [] },
294+
});
295+
expect(result.bootstrapRootKref).toMatch(/^ko\d+$/u);
292296
});
293297
});
294298

packages/ocap-kernel/src/Kernel.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import type {
2121
VatConfig,
2222
KernelStatus,
2323
Subcluster,
24+
SubclusterLaunchResult,
2425
EndpointHandle,
2526
} from './types.ts';
2627
import { isVatId, isRemoteId } from './types.ts';
@@ -293,11 +294,12 @@ export class Kernel {
293294
* Launches a sub-cluster of vats.
294295
*
295296
* @param config - Configuration object for sub-cluster.
296-
* @returns a promise for the (CapData encoded) result of the bootstrap message.
297+
* @returns A promise for the subcluster ID and the (CapData encoded) result
298+
* of the bootstrap message.
297299
*/
298300
async launchSubcluster(
299301
config: ClusterConfig,
300-
): Promise<CapData<KRef> | undefined> {
302+
): Promise<SubclusterLaunchResult> {
301303
return this.#subclusterManager.launchSubcluster(config);
302304
}
303305

0 commit comments

Comments
 (0)