Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
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 @@ -122,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
75 changes: 43 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,11 @@ async function main(): Promise<void> {
});
drainPromise.catch(logger.error);

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

try {
await drainPromise;
Expand All @@ -143,19 +143,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 +179,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 {};
1 change: 1 addition & 0 deletions packages/kernel-browser-runtime/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@
"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",
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
Expand Up @@ -122,6 +122,37 @@ describe('makeKernelFacade', () => {
expect(mockKernel.queueMessage).toHaveBeenCalledTimes(1);
});

it('converts kref strings in args to standins', async () => {
const target: KRef = 'ko1';
const method = 'sendTo';
// Use ko refs only - kp refs become promise standins with different structure
const args = ['ko42', { target: 'ko99', data: 'hello' }];

await facade.queueMessage(target, method, args);

// Verify the call was made
expect(mockKernel.queueMessage).toHaveBeenCalledTimes(1);

// Get the actual args passed to kernel
const [, , processedArgs] = vi.mocked(mockKernel.queueMessage).mock
.calls[0]!;

// First arg should be a standin with getKref method
expect(processedArgs[0]).toHaveProperty('getKref');
expect((processedArgs[0] as { getKref: () => string }).getKref()).toBe(
'ko42',
);

// Second arg should be an object with converted kref
const secondArg = processedArgs[1] as {
target: { getKref: () => string };
data: string;
};
expect(secondArg.target).toHaveProperty('getKref');
expect(secondArg.target.getKref()).toBe('ko99');
expect(secondArg.data).toBe('hello');
});

it('returns result from kernel', async () => {
const expectedResult = { body: '#{"answer":42}', slots: [] };
vi.mocked(mockKernel.queueMessage).mockResolvedValueOnce(expectedResult);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { makeDefaultExo } from '@metamask/kernel-utils/exo';
import type { Kernel, ClusterConfig, KRef, VatId } from '@metamask/ocap-kernel';

import { convertKrefsToStandins } from '../../kref-presence.ts';
import type { KernelFacade, LaunchResult } from '../../types.ts';

export type { KernelFacade } from '../../types.ts';
Expand All @@ -26,7 +27,9 @@ export function makeKernelFacade(kernel: Kernel): KernelFacade {
},

queueMessage: async (target: KRef, method: string, args: unknown[]) => {
return kernel.queueMessage(target, method, args);
// Convert kref strings in args to standins for kernel-marshal
const processedArgs = convertKrefsToStandins(args) as unknown[];
return kernel.queueMessage(target, method, processedArgs);
},

getStatus: async () => {
Expand Down
Loading
Loading