Skip to content
Closed
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
13 changes: 10 additions & 3 deletions packages/extension/src/background.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Json } from '@metamask/utils';
import './background-trusted-prelude.js';
import { CommandMethod } from '@ocap/utils';
import type { Command } from '@ocap/utils';
import { CommandMethod, isCommandReply } from '@ocap/utils';

import {
ExtensionMessageTarget,
Expand Down Expand Up @@ -44,7 +45,10 @@ chrome.action.onClicked.addListener(() => {
* @param params - The message data.
* @param params.name - The name to include in the message.
*/
async function sendCommand(method: string, params?: Json): Promise<void> {
async function sendCommand<Type extends Command>(
method: Type['method'],
params?: Type['params'],
): Promise<void> {
await provideOffScreenDocument();

await chrome.runtime.sendMessage({
Expand Down Expand Up @@ -72,7 +76,10 @@ async function provideOffScreenDocument(): Promise<void> {
// Handle replies from the offscreen document
chrome.runtime.onMessage.addListener(
makeHandledCallback(async (message: unknown) => {
if (!isExtensionRuntimeMessage(message)) {
if (
!isExtensionRuntimeMessage(message) ||
!isCommandReply(message.payload)
) {
console.error('Background received unexpected message', message);
return;
}
Expand Down
7 changes: 5 additions & 2 deletions packages/extension/src/iframe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { makeExo } from '@endo/exo';
import { M } from '@endo/patterns';
import { Supervisor } from '@ocap/kernel';
import { makeMessagePortStreamPair, receiveMessagePort } from '@ocap/streams';
import type { StreamEnvelope } from '@ocap/utils';
import type { StreamEnvelope, StreamEnvelopeReply } from '@ocap/utils';

main().catch(console.error);

Expand All @@ -11,7 +11,10 @@ main().catch(console.error);
*/
async function main(): Promise<void> {
const port = await receiveMessagePort();
const streams = makeMessagePortStreamPair<StreamEnvelope>(port);
const streams = makeMessagePortStreamPair<
StreamEnvelope,
StreamEnvelopeReply
>(port);

const bootstrap = makeExo(
'TheGreatFrangooly',
Expand Down
9 changes: 9 additions & 0 deletions packages/extension/src/kernel.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Ocap Kernel</title>
<script src="kernel.ts" type="module"></script>
</head>
<body></body>
</html>
78 changes: 78 additions & 0 deletions packages/extension/src/kernel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { Kernel } from '@ocap/kernel';
import { initializeMessageChannel, makeMessagePortStreamPair, receiveMessagePort } from '@ocap/streams';
import type { CommandReply } from '@ocap/utils';
import { Command, CommandMethod } from '@ocap/utils';

import { makeIframeVatWorker } from './makeIframeVatWorker.js';
main().catch(console.error);

/**
* The main function for the kernel script.
*/
async function main(): Promise<void> {
console.debug('starting kernel');
const port = await receiveMessagePort();
console.debug('kernel connected');
const streams = makeMessagePortStreamPair<Command, CommandReply>(port);
const kernel = new Kernel();
console.debug('launching vat');
const iframeReadyP = kernel.launchVat({
id: 'default',
worker: makeIframeVatWorker('default', initializeMessageChannel),
});
let vatLaunchNotified: boolean = false;

for await (const { method, params } of streams.reader) {
console.debug('kernel received message', { method, params });

const vat = await iframeReadyP;
if (!vatLaunchNotified) {
console.debug('vat connected');
vatLaunchNotified = true;
}

switch (method) {
case CommandMethod.Evaluate:
await streams.writer.next({ method: CommandMethod.Evaluate, params: await evaluate(vat.id, params) });
break;
case CommandMethod.CapTpCall:
const result = await vat.callCapTp(params);
await streams.writer.next({ method: CommandMethod.CapTpCall, params: JSON.stringify(result, null, 2) });
break;
case CommandMethod.CapTpInit:
await vat.makeCapTp();
await streams.writer.next({ method: CommandMethod.CapTpInit, params: '~~~ CapTP Initialized ~~~'});
break;
case CommandMethod.Ping:
await streams.writer.next({ method: CommandMethod.Ping, params: 'pong' });
break;
default:
console.error(
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
`Offscreen received unexpected command method: "${method}"`,
);
}
}

/**
* Evaluate a string in the default iframe.
*
* @param vatId - The ID of the vat to send the message to.
* @param source - The source string to evaluate.
* @returns The result of the evaluation, or an error message.
*/
async function evaluate(vatId: string, source: string): Promise<string> {
try {
const result = await kernel.sendMessage(vatId, {
method: CommandMethod.Evaluate,
params: source,
});
return String(result);
} catch (error) {
if (error instanceof Error) {
return `Error: ${error.message}`;
}
return `Error: Unknown error during evaluation.`;
}
}
}
10 changes: 8 additions & 2 deletions packages/extension/src/makeIframeVatWorker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { createWindow } from '@metamask/snaps-utils';
import type { VatId, VatWorker } from '@ocap/kernel';
import type { initializeMessageChannel } from '@ocap/streams';
import { makeMessagePortStreamPair } from '@ocap/streams';
import type { StreamEnvelope } from '@ocap/utils';
import type { StreamEnvelopeReply, StreamEnvelope } from '@ocap/utils';

const IFRAME_URI = 'iframe.html';

Expand All @@ -20,9 +20,15 @@ export const makeIframeVatWorker = (
): VatWorker => {
return {
init: async () => {
console.debug('creating new vat worker');
const newWindow = await createWindow(IFRAME_URI, getHtmlId(id));
console.debug('waiting for vat worker connection');
const port = await getPort(newWindow);
const streams = makeMessagePortStreamPair<StreamEnvelope>(port);
console.debug('vat worker connected');
const streams = makeMessagePortStreamPair<
StreamEnvelopeReply,
StreamEnvelope
>(port);

return [streams, newWindow];
},
Expand Down
2 changes: 1 addition & 1 deletion packages/extension/src/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"action": {},
"permissions": ["offscreen"],
"sandbox": {
"pages": ["iframe.html"]
"pages": ["iframe.html", "kernel.html"]
},
"content_security_policy": {
"extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'none'; frame-ancestors 'none';",
Expand Down
143 changes: 67 additions & 76 deletions packages/extension/src/offscreen.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,75 @@
import { Kernel } from '@ocap/kernel';
import { initializeMessageChannel } from '@ocap/streams';
import { CommandMethod } from '@ocap/utils';
import { createWindow } from '@metamask/snaps-utils';
import { initializeMessageChannel, makeMessagePortStreamPair, StreamPair } from '@ocap/streams';
import type { Command, CommandReply } from '@ocap/utils';
import { CommandMethod, isCommand } from '@ocap/utils';

import { makeIframeVatWorker } from './makeIframeVatWorker.js';
import {
ExtensionMessageTarget,
isExtensionRuntimeMessage,
makeHandledCallback,
} from './shared.js';

main().catch(console.error);
const kernelStreams = startKernel({
uri: 'kernel.html',
id: 'ocap-kernel'
});

Promise.race([
receiveMessagesFromKernel(),
receiveMessagesFromBackground(),
]).catch(console.error).finally();

type StartKernelArgs = {
uri: string;
id: string;
}

async function startKernel({ uri, id }: StartKernelArgs): Promise<StreamPair<CommandReply, Command>> {
console.debug('starting kernel');
const targetWindow = await createWindow(uri, id);
const port = await initializeMessageChannel(targetWindow);
console.debug('kernel connected');
return makeMessagePortStreamPair(port);
}

/**
* The main function for the offscreen script.
* Listen to messages from the kernel.
*/
async function main(): Promise<void> {
const kernel = new Kernel();
const iframeReadyP = kernel.launchVat({
id: 'default',
worker: makeIframeVatWorker('default', initializeMessageChannel),
});
async function receiveMessagesFromKernel(): Promise<void> {
const streams = await kernelStreams;

for await(const payload of streams.reader) {

switch (payload.method) {
case CommandMethod.Evaluate:
case CommandMethod.CapTpCall:
case CommandMethod.CapTpInit:
case CommandMethod.Ping:
// For now, we only receive command replies,
// and we simply forward them to the background service worker.
await chrome.runtime.sendMessage({
target: ExtensionMessageTarget.Background,
payload,
});
break;
default:
console.error(
// @ts-expect-error The type of `payload` is `never`, but this could happen at runtime.
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
`Offscreen received unexpected command reply method: "${payload.method}"`,
);
}
}
}

// Handle messages from the background service worker
/**
* Listen to messages from the background service worker.
*/
async function receiveMessagesFromBackground(): Promise<void> {
console.debug('starting background listener');
chrome.runtime.onMessage.addListener(
makeHandledCallback(async (message: unknown) => {
if (!isExtensionRuntimeMessage(message)) {
if (!isExtensionRuntimeMessage(message) || !isCommand(message.payload)) {
console.error('Offscreen received unexpected message', message);
return;
}
Expand All @@ -35,34 +80,21 @@ async function main(): Promise<void> {
return;
}

const vat = await iframeReadyP;
console.debug('offscreen received message', message);

const streams = await kernelStreams;

const { payload } = message;

switch (payload.method) {
case CommandMethod.Evaluate:
await replyToCommand(
CommandMethod.Evaluate,
await evaluate(vat.id, payload.params),
);
break;
case CommandMethod.CapTpCall: {
const result = await vat.callCapTp(payload.params);
await replyToCommand(
CommandMethod.CapTpCall,
JSON.stringify(result, null, 2),
);
break;
}
case CommandMethod.CapTpCall:
case CommandMethod.CapTpInit:
await vat.makeCapTp();
await replyToCommand(
CommandMethod.CapTpInit,
'~~~ CapTP Initialized ~~~',
);
break;
case CommandMethod.Ping:
await replyToCommand(CommandMethod.Ping, 'pong');
// For now, we only recieve kernel commands,
// and we simply forward them to the kernel.
console.debug('forwarding message to kernel');
await streams.writer.next(payload);
break;
default:
console.error(
Expand All @@ -73,45 +105,4 @@ async function main(): Promise<void> {
}
}),
);

/**
* Reply to a command from the background script.
*
* @param method - The command method.
* @param params - The command parameters.
*/
async function replyToCommand(
method: CommandMethod,
params?: string,
): Promise<void> {
await chrome.runtime.sendMessage({
target: ExtensionMessageTarget.Background,
payload: {
method,
params: params ?? null,
},
});
}

/**
* Evaluate a string in the default iframe.
*
* @param vatId - The ID of the vat to send the message to.
* @param source - The source string to evaluate.
* @returns The result of the evaluation, or an error message.
*/
async function evaluate(vatId: string, source: string): Promise<string> {
try {
const result = await kernel.sendMessage(vatId, {
method: CommandMethod.Evaluate,
params: source,
});
return String(result);
} catch (error) {
if (error instanceof Error) {
return `Error: ${error.message}`;
}
return `Error: Unknown error during evaluation.`;
}
}
}
10 changes: 6 additions & 4 deletions packages/extension/src/shared.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { isObject } from '@metamask/utils';
import type { Command } from '@ocap/utils';
import { isCommand } from '@ocap/utils';
import type { Command, CommandReply } from '@ocap/utils';
import { isCommand, isCommandReply } from '@ocap/utils';

export type VatId = string;

Expand All @@ -10,7 +10,9 @@ export enum ExtensionMessageTarget {
}

export type ExtensionRuntimeMessage = {
payload: Command;
// On some systems, including CI, ESLint complains of overlap between the union operands.
// eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents
payload: Command | CommandReply;
target: ExtensionMessageTarget;
};

Expand All @@ -22,7 +24,7 @@ export const isExtensionRuntimeMessage = (
Object.values(ExtensionMessageTarget).includes(
message.target as ExtensionMessageTarget,
) &&
isCommand(message.payload);
(isCommand(message.payload) || isCommandReply(message.payload));

/**
* Wrap an async callback to ensure any errors are at least logged.
Expand Down
1 change: 1 addition & 0 deletions packages/extension/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export default defineConfig(({ mode }) => ({
background: path.resolve(projectRoot, 'background.ts'),
offscreen: path.resolve(projectRoot, 'offscreen.html'),
iframe: path.resolve(projectRoot, 'iframe.html'),
kernel: path.resolve(projectRoot, 'kernel.html'),
},
output: {
entryFileNames: '[name].js',
Expand Down
Loading