Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
f166b3d
feat(omnium): Add controller architecture and CapletController
rekmarks Jan 8, 2026
83c867d
refactor(omnium): Simplify CapletController state structure
rekmarks Jan 8, 2026
93705fc
refactor(omnium): Add abstract Controller base class
rekmarks Jan 9, 2026
3f498e6
refactor(omnium): Refactor ControllerStorage with debounced persistence
rekmarks Jan 9, 2026
c755a35
refactor(omnium): Simplify CapletId validation to allow any ASCII string
rekmarks Jan 10, 2026
e8bc97c
chore: Update coverage
rekmarks Jan 15, 2026
e359423
refactor: Fix omnium background globals
rekmarks Jan 15, 2026
1ab49c9
refactor: Always close the offscreen stream on startup failure
rekmarks Jan 20, 2026
0330172
feat(omnium): Add Phase 1a - Single echo caplet implementation
rekmarks Jan 10, 2026
13e8e6f
feat(omnium): Add Phase 1b - Store and retrieve caplet root krefs
rekmarks Jan 10, 2026
3d54e30
fix(omnium): Fix TypeScript type errors in Phase 1b implementation
rekmarks Jan 12, 2026
43fcfba
refactor(omnium): Simplify LaunchResult and remove KrefWrapper
rekmarks Jan 12, 2026
a2ffb8f
test(kernel-browser-runtime): Add error case tests for launchSubcluster
rekmarks Jan 12, 2026
e16c0f7
feat(omnium): Expose caplet manifests in background console
rekmarks Jan 12, 2026
7cb55c3
feat(omnium): Add loadCaplet method and fix vat bootstrap kref
rekmarks Jan 13, 2026
aebcdd1
fix: Fix launch-subcluster RPC result type for JSON compatibility
rekmarks Jan 14, 2026
4fdfbc0
test: Fix test failures
rekmarks Jan 15, 2026
a86908d
refactor(omnium): omnium.loadCaplet -> omnium.caplet.load
rekmarks Jan 15, 2026
b5dcd13
fix: Fix nodejs test helper
rekmarks Jan 16, 2026
408102a
fix: Fix another nodejs test helper
rekmarks Jan 16, 2026
84ed1b2
chore: Remove unused dependency from nodejs
rekmarks Jan 16, 2026
48bee6e
refactor: Rationalize globalThis.kernel
rekmarks Jan 13, 2026
0bd27d4
feat(kernel-browser-runtime): Add slot translation for E() on vat obj…
rekmarks Jan 13, 2026
cb17105
refactor(kernel-browser-runtime): Split vitest config into unit and i…
rekmarks Jan 13, 2026
13ecd12
refactor(nodejs): Migrate endoify setup to kernel-shims and fix test …
rekmarks Jan 13, 2026
9451aae
fix(kernel-shims): Use relative import in node-endoify.js
rekmarks Jan 13, 2026
a44a765
refactor(kernel-shims): Rename node-endoify to endoify-node and updat…
rekmarks Jan 14, 2026
2e90b61
fix: Build in CI before integration tests
rekmarks Jan 14, 2026
b836aa5
feat(extension): Add CapTP E() support for calling vat methods
rekmarks Jan 14, 2026
cb7e613
refactor: Rename background-kref to kref-presence for clarity
rekmarks Jan 14, 2026
f195073
test(kernel-browser-runtime): Add unit tests for kref-presence and co…
rekmarks Jan 14, 2026
5172b94
refactor: Post-rebase fixup
rekmarks Jan 15, 2026
f229707
test(extension): Fix object-registry e2e test
rekmarks Jan 19, 2026
cbae7f9
test(extension): Fix persistence e2e test
rekmarks Jan 20, 2026
b95fd28
chore: Remove unused dependency
rekmarks Jan 21, 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
479 changes: 479 additions & 0 deletions .claude/plans/phase-1-caplet-installation-with-consumer.md

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions .depcheckrc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ ignores:
# Used by @ocap/nodejs to build the sqlite3 bindings
- 'node-gyp'

# Used by @metamask/kernel-shims/endoify-node for tests
- '@libp2p/webrtc'

# These are peer dependencies of various modules we actually do
# depend on, which have been elevated to full dependencies (even
# though we don't actually depend on them) in order to work around a
Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
"lint:misc": "prettier --no-error-on-unmatched-pattern '**/*.json' '**/*.md' '**/*.html' '**/*.yml' '!**/CHANGELOG.old.md' '!.yarnrc.yml' '!CLAUDE.md' '!merged-packages/**' --ignore-path .gitignore --log-level error",
"postinstall": "simple-git-hooks && yarn rebuild:native",
"prepack": "./scripts/prepack.sh",
"pretest": "bash scripts/reset-coverage-thresholds.sh",
"pretest": "./scripts/reset-coverage-thresholds.sh",
"rebuild:native": "./scripts/rebuild-native.sh",
"test": "yarn pretest && vitest run",
"test:ci": "vitest run --coverage false",
Expand Down Expand Up @@ -123,7 +123,8 @@
"vite>sass>@parcel/watcher": false,
"vitest>@vitest/browser>webdriverio>@wdio/utils>edgedriver": false,
"vitest>@vitest/browser>webdriverio>@wdio/utils>geckodriver": false,
"vitest>@vitest/mocker>msw": false
"vitest>@vitest/mocker>msw": false,
"@ocap/cli>@metamask/kernel-shims>@libp2p/webrtc>@ipshipyard/node-datachannel": false
}
},
"resolutions": {
Expand Down
79 changes: 47 additions & 32 deletions packages/extension/src/background.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
import { E } from '@endo/eventual-send';
import {
makeBackgroundCapTP,
makePresenceManager,
makeCapTPNotification,
isCapTPNotification,
getCapTPMessage,
} from '@metamask/kernel-browser-runtime';
import type {
KernelFacade,
CapTPMessage,
} from '@metamask/kernel-browser-runtime';
import type { CapTPMessage } from '@metamask/kernel-browser-runtime';
import defaultSubcluster from '@metamask/kernel-browser-runtime/default-cluster';
import { delay, isJsonRpcMessage, stringify } from '@metamask/kernel-utils';
import type { JsonRpcMessage } from '@metamask/kernel-utils';
Expand All @@ -20,12 +18,11 @@ defineGlobals();
const OFFSCREEN_DOCUMENT_PATH = '/offscreen.html';
const logger = new Logger('background');
let bootPromise: Promise<void> | null = null;
let kernelP: Promise<KernelFacade>;
let ping: () => Promise<void>;

// With this we can click the extension action button to wake up the service worker.
chrome.action.onClicked.addListener(() => {
ping?.().catch(logger.error);
globalThis.kernel !== undefined &&
E(globalThis.kernel).ping().catch(logger.error);
});

// Install/update
Expand Down Expand Up @@ -108,12 +105,12 @@ async function main(): Promise<void> {
});

// Get the kernel remote presence
kernelP = backgroundCapTP.getKernel();
const kernelP = backgroundCapTP.getKernel();
globalThis.kernel = kernelP;

ping = async () => {
const result = await E(kernelP).ping();
logger.info(result);
};
// Create presence manager for E() calls on vat objects
const presenceManager = makePresenceManager({ kernelFacade: kernelP });
Object.assign(globalThis.captp, presenceManager);

// Handle incoming CapTP messages from the kernel
const drainPromise = offscreenStream.drain((message) => {
Expand All @@ -126,8 +123,15 @@ async function main(): Promise<void> {
});
drainPromise.catch(logger.error);

await ping(); // Wait for the kernel to be ready
await startDefaultSubcluster(kernelP);
try {
await E(kernelP).ping();
const rootKref = await startDefaultSubcluster();
if (rootKref) {
await greetBootstrapVat(rootKref);
}
} catch (error) {
offscreenStream.throw(error as Error).catch(logger.error);
}

try {
await drainPromise;
Expand All @@ -143,19 +147,33 @@ async function main(): Promise<void> {
/**
* Idempotently starts the default subcluster.
*
* @param kernelPromise - Promise for the kernel facade.
* @returns The rootKref of the bootstrap vat if launched, undefined if subcluster already exists.
*/
async function startDefaultSubcluster(
kernelPromise: Promise<KernelFacade>,
): Promise<void> {
const status = await E(kernelPromise).getStatus();
async function startDefaultSubcluster(): Promise<string | undefined> {
const status = await E(globalThis.kernel).getStatus();

if (status.subclusters.length === 0) {
const result = await E(kernelPromise).launchSubcluster(defaultSubcluster);
const result = await E(globalThis.kernel).launchSubcluster(
defaultSubcluster,
);
logger.info(`Default subcluster launched: ${JSON.stringify(result)}`);
} else {
logger.info('Subclusters already exist. Not launching default subcluster.');
return result.rootKref;
}
logger.info('Subclusters already exist. Not launching default subcluster.');
return undefined;
}

/**
* Greets the bootstrap vat by calling its hello() method.
*
* @param rootKref - The kref of the bootstrap vat's root object.
*/
async function greetBootstrapVat(rootKref: string): Promise<void> {
const rootPresence = captp.resolveKref(rootKref) as {
hello: (from: string) => string;
};
const greeting = await E(rootPresence).hello('background');
logger.info(`Got greeting from bootstrap vat: ${greeting}`);
}

/**
Expand All @@ -165,19 +183,16 @@ function defineGlobals(): void {
Object.defineProperty(globalThis, 'kernel', {
configurable: false,
enumerable: true,
writable: false,
value: {},
writable: true,
value: undefined,
});

Object.defineProperties(globalThis.kernel, {
ping: {
get: () => ping,
},
getKernel: {
value: async () => kernelP,
},
Object.defineProperty(globalThis, 'captp', {
configurable: false,
enumerable: true,
writable: false,
value: {},
});
harden(globalThis.kernel);

Object.defineProperty(globalThis, 'E', {
value: E,
Expand Down
34 changes: 16 additions & 18 deletions packages/extension/src/global.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import type { KernelFacade } from '@metamask/kernel-browser-runtime';
import type {
PresenceManager,
KernelFacade,
} from '@metamask/kernel-browser-runtime';

// Type declarations for kernel dev console API.
declare global {
Expand All @@ -16,24 +19,19 @@ declare global {
var E: typeof import('@endo/eventual-send').E;

// eslint-disable-next-line no-var
var kernel: {
/**
* Ping the kernel to verify connectivity.
*/
ping: () => Promise<void>;
var kernel: KernelFacade | Promise<KernelFacade>;

/**
* Get the kernel remote presence for use with E().
*
* @returns A promise for the kernel facade remote presence.
* @example
* ```typescript
* const kernel = await kernel.getKernel();
* const status = await E(kernel).getStatus();
* ```
*/
getKernel: () => Promise<KernelFacade>;
};
/**
* CapTP utilities for resolving krefs to E()-callable presences.
*
* @example
* ```typescript
* const alice = captp.resolveKref('ko1');
* await E(alice).hello('console');
* ```
*/
// eslint-disable-next-line no-var
var captp: PresenceManager;
}

export {};
4 changes: 2 additions & 2 deletions packages/extension/test/e2e/object-registry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ test.describe('Object Registry', () => {
await clearLogsButton.click();
await popupPage.click('button:text("Object Registry")');
await expect(
popupPage.locator('text=Alice (v1) - 5 objects, 4 promises'),
popupPage.locator('text=Alice (v1) - 5 objects, 5 promises'),
).toBeVisible();
const targetSelect = popupPage.locator('[data-testid="message-target"]');
await expect(targetSelect).toBeVisible();
Expand Down Expand Up @@ -102,7 +102,7 @@ test.describe('Object Registry', () => {
await expect(messageResponse).toContainText('"body":"#\\"vat Alice got');
await expect(messageResponse).toContainText('"slots":[');
await expect(
popupPage.locator('text=Alice (v1) - 5 objects, 6 promises'),
popupPage.locator('text=Alice (v1) - 5 objects, 7 promises'),
).toBeVisible();
});

Expand Down
2 changes: 2 additions & 0 deletions packages/extension/test/e2e/persistence.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ test.describe('Kernel Persistence', () => {
await expect(
newPopupPage.locator('text=Subcluster s2 - 1 Vat'),
).toBeVisible();
// Wait for database to fully persist before reloading
await newPopupPage.waitForTimeout(1000);
// reload the extension
await newPopupPage.evaluate(() => chrome.runtime.reload());
await newPopupPage.close();
Expand Down
2 changes: 1 addition & 1 deletion packages/kernel-browser-runtime/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -86,11 +86,11 @@
"devDependencies": {
"@arethetypeswrong/cli": "^0.17.4",
"@endo/eventual-send": "^1.3.4",
"@libp2p/webrtc": "5.2.24",
"@metamask/auto-changelog": "^5.3.0",
"@metamask/eslint-config": "^15.0.0",
"@metamask/eslint-config-nodejs": "^15.0.0",
"@metamask/eslint-config-typescript": "^15.0.0",
"@ocap/nodejs": "workspace:^",
"@ocap/repo-tools": "workspace:^",
"@ts-bridge/cli": "^0.6.3",
"@ts-bridge/shims": "^0.1.1",
Expand Down
1 change: 1 addition & 0 deletions packages/kernel-browser-runtime/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ describe('index', () => {
'makeBackgroundCapTP',
'makeCapTPNotification',
'makeIframeVatWorker',
'makePresenceManager',
'parseRelayQueryString',
'receiveInternalConnections',
'rpcHandlers',
Expand Down
5 changes: 5 additions & 0 deletions packages/kernel-browser-runtime/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,8 @@ export {
type BackgroundCapTPOptions,
type CapTPMessage,
} from './background-captp.ts';
export {
makePresenceManager,
type PresenceManager,
type PresenceManagerOptions,
} from './kref-presence.ts';
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
// Real endoify needed for CapTP and E() to work properly
import '@ocap/nodejs/endoify-ts';

import { E } from '@endo/eventual-send';
import type { ClusterConfig, Kernel } from '@metamask/ocap-kernel';
import { describe, it, expect, vi, beforeEach } from 'vitest';
Expand All @@ -24,8 +21,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 +114,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 @@ -74,4 +74,21 @@ describe('makeKernelCapTP', () => {

expect(() => capTP.abort({ reason: 'test shutdown' })).not.toThrow();
});

describe('kref marshalling', () => {
it('creates kernel CapTP with custom import/export tables', () => {
// Verify that makeKernelCapTP with the custom tables doesn't throw
const capTP = makeKernelCapTP({
kernel: mockKernel,
send: sendMock,
});

expect(capTP).toBeDefined();
expect(capTP.dispatch).toBeDefined();
expect(capTP.abort).toBeDefined();

// The custom tables are internal to CapTP, so we can't test them directly
// Integration tests will verify the end-to-end kref marshalling functionality
});
});
});
Loading
Loading