Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
aa29a9b
chore: Add PLAN.md
rekmarks Jan 6, 2026
c6aabf1
feat(omnium): Add dev console object to background
rekmarks Jan 6, 2026
38c7235
chore: Move PLAN.md to omnium package
rekmarks Jan 6, 2026
b40ebb7
feat(omnium): Add CapTP-based E() infrastructure for kernel communica…
rekmarks Jan 6, 2026
8b68905
refactor: Remove Kernel commandStream and consolidate CapTP infrastru…
rekmarks Jan 9, 2026
344a470
docs: Add plan for performing E() on vat objects
rekmarks Jan 7, 2026
548e3d8
test(kernel-browser-runtime): Add CapTP infrastructure tests
rekmarks Jan 7, 2026
0d626ee
feat(omnium): Add controller architecture and CapletController
rekmarks Jan 8, 2026
2f1d4dc
refactor(omnium): Simplify CapletController state structure
rekmarks Jan 8, 2026
9841d0a
refactor(omnium): Add abstract Controller base class
rekmarks Jan 9, 2026
3e55325
refactor(omnium): Refactor ControllerStorage with debounced persistence
rekmarks Jan 9, 2026
011d258
docs: Update PLAN.md
rekmarks Jan 9, 2026
cb7a392
refactor(omnium): Simplify CapletId validation to allow any ASCII string
rekmarks Jan 10, 2026
41bf5f3
feat(omnium): Add Phase 1a - Single echo caplet implementation
rekmarks Jan 10, 2026
ebfb84a
feat(omnium): Add Phase 1b - Store and retrieve caplet root krefs
rekmarks Jan 10, 2026
6f7c671
fix(omnium): Fix TypeScript type errors in Phase 1b implementation
rekmarks Jan 12, 2026
4063401
refactor(omnium): Simplify LaunchResult and remove KrefWrapper
rekmarks Jan 12, 2026
ec88cdf
test(kernel-browser-runtime): Add error case tests for launchSubcluster
rekmarks Jan 12, 2026
f0f6f44
feat(omnium): Expose caplet manifests in background console
rekmarks Jan 12, 2026
4c130fc
feat(omnium): Add loadCaplet method and fix vat bootstrap kref
rekmarks Jan 13, 2026
63c1867
refactor: Rationalize globalThis.kernel
rekmarks Jan 13, 2026
cdf152e
feat(kernel-browser-runtime): Add slot translation for E() on vat obj…
rekmarks Jan 13, 2026
bc92f82
refactor(kernel-browser-runtime): Split vitest config into unit and i…
rekmarks Jan 13, 2026
8dc4139
refactor(nodejs): Migrate endoify setup to kernel-shims and fix test …
rekmarks Jan 13, 2026
a7185f0
fix(kernel-shims): Use relative import in node-endoify.js
rekmarks Jan 13, 2026
9491df8
refactor(kernel-shims): Rename node-endoify to endoify-node and updat…
rekmarks Jan 14, 2026
e4bedcf
fix: Build in CI before integration tests
rekmarks Jan 14, 2026
f5c5af0
feat(extension): Add CapTP E() support for calling vat methods
rekmarks Jan 14, 2026
e552951
refactor: Rename background-kref to kref-presence for clarity
rekmarks Jan 14, 2026
029a8af
test(kernel-browser-runtime): Add unit tests for kref-presence and co…
rekmarks Jan 14, 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
1 change: 1 addition & 0 deletions .github/workflows/lint-build-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ jobs:
node-version: ${{ matrix.node-version }}
env:
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
- run: yarn build
- run: yarn test:integration
- name: Require clean working directory
shell: bash
Expand Down
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,15 @@
"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",
"test:dev": "yarn test --mode development --reporter dot",
"test:e2e": "yarn workspaces foreach --all run test:e2e",
"test:e2e:ci": "yarn workspaces foreach --all run test:e2e:ci",
"test:e2e:local": "yarn workspaces foreach --all run test:e2e:local",
"test:integration": "yarn workspaces foreach --all run test:integration",
"test:verbose": "yarn test --reporter verbose",
"test:watch": "vitest",
"why:batch": "./scripts/why-batch.sh"
Expand Down Expand Up @@ -121,7 +122,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
4 changes: 1 addition & 3 deletions packages/extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,13 @@
"test:e2e:debug": "playwright test --debug"
},
"dependencies": {
"@endo/eventual-send": "^1.3.4",
"@metamask/kernel-browser-runtime": "workspace:^",
"@metamask/kernel-rpc-methods": "workspace:^",
"@metamask/kernel-shims": "workspace:^",
"@metamask/kernel-ui": "workspace:^",
"@metamask/kernel-utils": "workspace:^",
"@metamask/logger": "workspace:^",
"@metamask/ocap-kernel": "workspace:^",
"@metamask/streams": "workspace:^",
"@metamask/utils": "^11.9.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"ses": "^1.14.0"
Expand Down
2 changes: 1 addition & 1 deletion packages/extension/scripts/build-constants.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export const kernelBrowserRuntimeSrcDir = path.resolve(
*/
export const trustedPreludes = {
background: {
path: path.resolve(sourceDir, 'env/background-trusted-prelude.js'),
content: "import './endoify.js';",
},
'kernel-worker': { content: "import './endoify.js';" },
};
158 changes: 97 additions & 61 deletions packages/extension/src/background.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
import { E } from '@endo/eventual-send';
import {
connectToKernel,
rpcMethodSpecs,
makeBackgroundCapTP,
makePresenceManager,
makeCapTPNotification,
isCapTPNotification,
getCapTPMessage,
} from '@metamask/kernel-browser-runtime';
import type {
KernelFacade,
CapTPMessage,
} from '@metamask/kernel-browser-runtime';
import defaultSubcluster from '@metamask/kernel-browser-runtime/default-cluster';
import { RpcClient } from '@metamask/kernel-rpc-methods';
import { delay } from '@metamask/kernel-utils';
import type { JsonRpcCall } from '@metamask/kernel-utils';
import { delay, isJsonRpcMessage } from '@metamask/kernel-utils';
import type { JsonRpcMessage } from '@metamask/kernel-utils';
import { Logger } from '@metamask/logger';
import { kernelMethodSpecs } from '@metamask/ocap-kernel/rpc';
import { ChromeRuntimeDuplexStream } from '@metamask/streams/browser';
import { isJsonRpcResponse } from '@metamask/utils';
import type { JsonRpcResponse } from '@metamask/utils';

defineGlobals();

const OFFSCREEN_DOCUMENT_PATH = '/offscreen.html';
const logger = new Logger('background');
Expand Down Expand Up @@ -79,49 +85,49 @@ async function main(): Promise<void> {
// Without this delay, sending messages via the chrome.runtime API can fail.
await delay(50);

// Create stream for CapTP messages
const offscreenStream = await ChromeRuntimeDuplexStream.make<
JsonRpcResponse,
JsonRpcCall
>(chrome.runtime, 'background', 'offscreen', isJsonRpcResponse);

const rpcClient = new RpcClient(
kernelMethodSpecs,
async (request) => {
await offscreenStream.write(request);
JsonRpcMessage,
JsonRpcMessage
>(chrome.runtime, 'background', 'offscreen', isJsonRpcMessage);

// Set up CapTP for E() based communication with the kernel
const backgroundCapTP = makeBackgroundCapTP({
send: (captpMessage: CapTPMessage) => {
const notification = makeCapTPNotification(captpMessage);
offscreenStream.write(notification).catch((error) => {
logger.error('Failed to send CapTP message:', error);
});
},
'background:',
);
});

const ping = async (): Promise<void> => {
const result = await rpcClient.call('ping', []);
logger.info(result);
};
// Get the kernel remote presence
const kernelP = backgroundCapTP.getKernel();
globalThis.kernel = kernelP;

// globalThis.kernel will exist due to dev-console.js in background-trusted-prelude.js
Object.defineProperties(globalThis.kernel, {
ping: {
value: ping,
},
sendMessage: {
value: async (message: JsonRpcCall) =>
await offscreenStream.write(message),
},
});
harden(globalThis.kernel);
// Create presence manager for E() calls on vat objects
const presenceManager = makePresenceManager({ kernelFacade: kernelP });
Object.assign(globalThis.captp, presenceManager);

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

// Pipe responses back to the RpcClient
const drainPromise = offscreenStream.drain(async (message) =>
rpcClient.handleResponse(message.id as string, message),
);
// Handle incoming CapTP messages from the kernel
const drainPromise = offscreenStream.drain((message) => {
if (isCapTPNotification(message)) {
const captpMessage = getCapTPMessage(message);
backgroundCapTP.dispatch(captpMessage);
}
});
drainPromise.catch(logger.error);

await ping(); // Wait for the kernel to be ready
await startDefaultSubcluster();
await E(kernelP).ping(); // Wait for the kernel to be ready
const rootKref = await startDefaultSubcluster(kernelP);
if (rootKref) {
await greetBootstrapVat(rootKref);
}

try {
await drainPromise;
Expand All @@ -134,30 +140,60 @@ 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(): Promise<void> {
const kernelStream = await connectToKernel({ label: 'background', logger });
const rpcClient = new RpcClient(
rpcMethodSpecs,
async (request) => {
await kernelStream.write(request);
},
'background',
);
async function startDefaultSubcluster(
kernelPromise: Promise<KernelFacade>,
): Promise<string | undefined> {
const kernel = await kernelPromise;
const status = await E(kernel).getStatus();

kernelStream
.drain(async (message) =>
rpcClient.handleResponse(message.id as string, message),
)
.catch(logger.error);

const status = await rpcClient.call('getStatus', []);
if (status.subclusters.length === 0) {
const result = await rpcClient.call('launchSubcluster', {
config: defaultSubcluster,
});
const result = await E(kernel).launchSubcluster(defaultSubcluster);
logger.info(`Default subcluster launched: ${JSON.stringify(result)}`);
} else {
logger.info('Subclusters already exist. Not launching default subcluster.');
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}`);
}

/**
* Define globals accessible via the background console.
*/
function defineGlobals(): void {
Object.defineProperty(globalThis, 'kernel', {
configurable: false,
enumerable: true,
writable: true,
value: {},
});

Object.defineProperty(globalThis, 'captp', {
configurable: false,
enumerable: true,
writable: false,
value: {},
});

Object.defineProperty(globalThis, 'E', {
value: E,
configurable: false,
enumerable: true,
writable: false,
});
}
3 changes: 0 additions & 3 deletions packages/extension/src/env/background-trusted-prelude.js

This file was deleted.

9 changes: 0 additions & 9 deletions packages/extension/src/env/dev-console.js

This file was deleted.

20 changes: 0 additions & 20 deletions packages/extension/src/env/dev-console.test.ts

This file was deleted.

37 changes: 37 additions & 0 deletions packages/extension/src/global.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import type {
PresenceManager,
KernelFacade,
} from '@metamask/kernel-browser-runtime';

// Type declarations for kernel dev console API.
declare global {
/**
* The E() function from @endo/eventual-send for making eventual sends.
* Set globally in the trusted prelude before lockdown.
*
* @example
* ```typescript
* const kernel = await kernel.getKernel();
* const status = await E(kernel).getStatus();
* ```
*/
// eslint-disable-next-line no-var,id-length
var E: typeof import('@endo/eventual-send').E;

// eslint-disable-next-line no-var
var kernel: KernelFacade | 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 {};
22 changes: 10 additions & 12 deletions packages/extension/src/offscreen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import {
PlatformServicesServer,
createRelayQueryString,
} from '@metamask/kernel-browser-runtime';
import { delay, isJsonRpcCall } from '@metamask/kernel-utils';
import type { JsonRpcCall } from '@metamask/kernel-utils';
import { delay, isJsonRpcMessage } from '@metamask/kernel-utils';
import type { JsonRpcMessage } from '@metamask/kernel-utils';
import { Logger } from '@metamask/logger';
import type { DuplexStream } from '@metamask/streams';
import {
Expand All @@ -13,8 +13,6 @@ import {
MessagePortDuplexStream,
} from '@metamask/streams/browser';
import type { PostMessageTarget } from '@metamask/streams/browser';
import type { JsonRpcResponse } from '@metamask/utils';
import { isJsonRpcResponse } from '@metamask/utils';

const logger = new Logger('offscreen');

Expand All @@ -27,11 +25,11 @@ async function main(): Promise<void> {
// Without this delay, sending messages via the chrome.runtime API can fail.
await delay(50);

// Create stream for messages from the background script
// Create stream for CapTP messages from the background script
const backgroundStream = await ChromeRuntimeDuplexStream.make<
JsonRpcCall,
JsonRpcResponse
>(chrome.runtime, 'offscreen', 'background', isJsonRpcCall);
JsonRpcMessage,
JsonRpcMessage
>(chrome.runtime, 'offscreen', 'background', isJsonRpcMessage);

const kernelStream = await makeKernelWorker();

Expand All @@ -48,7 +46,7 @@ async function main(): Promise<void> {
* @returns The message port stream for worker communication
*/
async function makeKernelWorker(): Promise<
DuplexStream<JsonRpcResponse, JsonRpcCall>
DuplexStream<JsonRpcMessage, JsonRpcMessage>
> {
// Assign local relay address generated from `yarn ocap relay`
const relayQueryString = createRelayQueryString([
Expand All @@ -72,9 +70,9 @@ async function makeKernelWorker(): Promise<
);

const kernelStream = await MessagePortDuplexStream.make<
JsonRpcResponse,
JsonRpcCall
>(port, isJsonRpcResponse);
JsonRpcMessage,
JsonRpcMessage
>(port, isJsonRpcMessage);

await PlatformServicesServer.make(worker as PostMessageTarget, (vatId) =>
makeIframeVatWorker({
Expand Down
Loading
Loading