Skip to content

Commit c6c235c

Browse files
rekmarksclaude
andcommitted
feat(ocap-kernel): add evaluate RPC method for vat REPL
Add an `evaluate` RPC method to VatSupervisor that evaluates code in a vat's isolated compartment, enabling REPL functionality while maintaining security isolation from the supervisor. Key changes: - New evaluate.ts RPC spec and handler - VatSupervisor creates a separate eval compartment with vat exports in scope - VatHandle.evaluate() method for kernel-side API - Result serialization for JSON-RPC transport (handles functions, symbols, bigints) - Errors return as { success: false, error } without crashing the vat Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 48ff5d1 commit c6c235c

6 files changed

Lines changed: 259 additions & 1 deletion

File tree

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { describe, it, expect, vi } from 'vitest';
2+
3+
import { evaluateHandler } from './evaluate.ts';
4+
import type { HandleEvaluate } from './evaluate.ts';
5+
6+
describe('evaluateHandler', () => {
7+
it('calls handleEvaluate with the code parameter', () => {
8+
const handleEvaluate = vi.fn(() => ({
9+
success: true as const,
10+
value: 42,
11+
}));
12+
const result = evaluateHandler.implementation(
13+
{ handleEvaluate },
14+
{ code: '1 + 1' },
15+
);
16+
expect(result).toStrictEqual({ success: true, value: 42 });
17+
expect(handleEvaluate).toHaveBeenCalledWith('1 + 1');
18+
});
19+
20+
it('returns success result with value', () => {
21+
const handleEvaluate: HandleEvaluate = () => ({
22+
success: true,
23+
value: { foo: 'bar' },
24+
});
25+
const result = evaluateHandler.implementation(
26+
{ handleEvaluate },
27+
{ code: 'test' },
28+
);
29+
expect(result).toStrictEqual({ success: true, value: { foo: 'bar' } });
30+
});
31+
32+
it('returns success result without value for undefined', () => {
33+
const handleEvaluate: HandleEvaluate = () => ({
34+
success: true,
35+
});
36+
const result = evaluateHandler.implementation(
37+
{ handleEvaluate },
38+
{ code: 'undefined' },
39+
);
40+
expect(result).toStrictEqual({ success: true });
41+
});
42+
43+
it('returns error result for failures', () => {
44+
const handleEvaluate: HandleEvaluate = () => ({
45+
success: false,
46+
error: 'SyntaxError: Unexpected token',
47+
});
48+
const result = evaluateHandler.implementation(
49+
{ handleEvaluate },
50+
{ code: 'invalid{code' },
51+
);
52+
expect(result).toStrictEqual({
53+
success: false,
54+
error: 'SyntaxError: Unexpected token',
55+
});
56+
});
57+
});
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import type { Handler, MethodSpec } from '@metamask/kernel-rpc-methods';
2+
import {
3+
object,
4+
string,
5+
literal,
6+
union,
7+
exactOptional,
8+
} from '@metamask/superstruct';
9+
import type { Infer } from '@metamask/superstruct';
10+
import { UnsafeJsonStruct } from '@metamask/utils';
11+
import type { Json } from '@metamask/utils';
12+
13+
const EvaluateParamsStruct = object({
14+
code: string(),
15+
});
16+
17+
type EvaluateParams = Infer<typeof EvaluateParamsStruct>;
18+
19+
const EvaluateSuccessResultStruct = object({
20+
success: literal(true),
21+
value: exactOptional(UnsafeJsonStruct),
22+
});
23+
24+
const EvaluateErrorResultStruct = object({
25+
success: literal(false),
26+
error: string(),
27+
});
28+
29+
const EvaluateResultStruct = union([
30+
EvaluateSuccessResultStruct,
31+
EvaluateErrorResultStruct,
32+
]);
33+
34+
export type EvaluateResult =
35+
| { success: true; value?: Json }
36+
| { success: false; error: string };
37+
38+
export type EvaluateSpec = MethodSpec<
39+
'evaluate',
40+
EvaluateParams,
41+
EvaluateResult
42+
>;
43+
44+
export const evaluateSpec = {
45+
method: 'evaluate',
46+
params: EvaluateParamsStruct,
47+
result: EvaluateResultStruct,
48+
} as const as EvaluateSpec;
49+
50+
export type HandleEvaluate = (code: string) => EvaluateResult;
51+
52+
type EvaluateHooks = {
53+
handleEvaluate: HandleEvaluate;
54+
};
55+
56+
export type EvaluateHandler = Handler<
57+
'evaluate',
58+
EvaluateParams,
59+
EvaluateResult,
60+
EvaluateHooks
61+
>;
62+
63+
export const evaluateHandler: EvaluateHandler = {
64+
...evaluateSpec,
65+
hooks: { handleEvaluate: true },
66+
implementation: ({ handleEvaluate }, params) => handleEvaluate(params.code),
67+
} as const;

packages/ocap-kernel/src/rpc/vat/index.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@ import type { Infer } from '@metamask/superstruct';
22

33
import { deliverSpec, deliverHandler } from './deliver.ts';
44
import type { DeliverSpec, DeliverHandler } from './deliver.ts';
5+
import { evaluateSpec, evaluateHandler } from './evaluate.ts';
6+
import type {
7+
EvaluateSpec,
8+
EvaluateHandler,
9+
EvaluateResult,
10+
} from './evaluate.ts';
511
import { initVatSpec, initVatHandler } from './initVat.ts';
612
import type { InitVatSpec, InitVatHandler } from './initVat.ts';
713
import { pingSpec, pingHandler } from './ping.ts';
@@ -12,20 +18,24 @@ import type { PingSpec, PingHandler } from './ping.ts';
1218

1319
export const vatHandlers = {
1420
deliver: deliverHandler,
21+
evaluate: evaluateHandler,
1522
initVat: initVatHandler,
1623
ping: pingHandler,
1724
} as {
1825
deliver: DeliverHandler;
26+
evaluate: EvaluateHandler;
1927
initVat: InitVatHandler;
2028
ping: PingHandler;
2129
};
2230

2331
export const vatMethodSpecs = {
2432
deliver: deliverSpec,
33+
evaluate: evaluateSpec,
2534
initVat: initVatSpec,
2635
ping: pingSpec,
2736
} as {
2837
deliver: DeliverSpec;
38+
evaluate: EvaluateSpec;
2939
initVat: InitVatSpec;
3040
ping: PingSpec;
3141
};
@@ -35,3 +45,5 @@ type Handlers = (typeof vatHandlers)[keyof typeof vatHandlers];
3545
export type VatMethod = Handlers['method'];
3646

3747
export type PingVatResult = Infer<PingSpec['result']>;
48+
49+
export type { EvaluateResult };

packages/ocap-kernel/src/vats/VatHandle.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import type { JsonRpcNotification, JsonRpcResponse } from '@metamask/utils';
1818
import type { KernelQueue } from '../KernelQueue.ts';
1919
import { kser, makeError } from '../liveslots/kernel-marshal.ts';
2020
import { vatMethodSpecs, vatSyscallHandlers } from '../rpc/index.ts';
21-
import type { PingVatResult, VatMethod } from '../rpc/index.ts';
21+
import type { EvaluateResult, PingVatResult, VatMethod } from '../rpc/index.ts';
2222
import type { KernelStore } from '../store/index.ts';
2323
import type {
2424
Message,
@@ -184,6 +184,19 @@ export class VatHandle implements EndpointHandle {
184184
});
185185
}
186186

187+
/**
188+
* Evaluate code in the vat's REPL compartment.
189+
*
190+
* @param code - The code to evaluate.
191+
* @returns A promise that resolves to the evaluation result.
192+
*/
193+
async evaluate(code: string): Promise<EvaluateResult> {
194+
return await this.sendVatCommand({
195+
method: 'evaluate',
196+
params: { code },
197+
});
198+
}
199+
187200
/**
188201
* Handle a message from the vat.
189202
*

packages/ocap-kernel/src/vats/VatSupervisor.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,26 @@ describe('VatSupervisor', () => {
124124
}),
125125
);
126126
});
127+
128+
it('handles "evaluate" requests before vat initialization', async () => {
129+
const dispatch = vi.fn();
130+
const { stream } = await makeVatSupervisor({ dispatch });
131+
132+
await stream.receiveInput({
133+
id: 'v0:1',
134+
method: 'evaluate',
135+
params: { code: '1 + 1' },
136+
jsonrpc: '2.0',
137+
});
138+
await delay(10);
139+
140+
expect(dispatch).toHaveBeenCalledWith(
141+
expect.objectContaining({
142+
id: 'v0:1',
143+
result: { success: false, error: 'Vat not initialized' },
144+
}),
145+
);
146+
});
127147
});
128148

129149
describe('terminate', () => {

packages/ocap-kernel/src/vats/VatSupervisor.ts

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
isJsonRpcRequest,
2626
isJsonRpcResponse,
2727
} from '@metamask/utils';
28+
import type { Json } from '@metamask/utils';
2829
import type { PlatformFactory } from '@ocap/kernel-platforms';
2930

3031
import { loadBundle } from './bundle-loader.ts';
@@ -37,6 +38,7 @@ import type {
3738
GCTools,
3839
} from '../liveslots/types.ts';
3940
import { vatSyscallMethodSpecs, vatHandlers } from '../rpc/index.ts';
41+
import type { EvaluateResult } from '../rpc/index.ts';
4042
import { makeVatKVStore } from '../store/vat-kv-store.ts';
4143
import type { VatConfig, VatDeliveryResult, VatId } from '../types.ts';
4244
import { isVatConfig, coerceVatSyscallObject } from '../types.ts';
@@ -105,6 +107,9 @@ export class VatSupervisor {
105107
/** Options to pass to the makePlatform function. */
106108
readonly #platformOptions: Record<string, unknown>;
107109

110+
/** Compartment for REPL evaluation with vat exports in scope. */
111+
#evalCompartment: { evaluate: (code: string) => unknown } | null = null;
112+
108113
/**
109114
* Construct a new VatSupervisor instance.
110115
*
@@ -151,6 +156,7 @@ export class VatSupervisor {
151156
this.#rpcServer = new RpcService(vatHandlers, {
152157
initVat: this.#initVat.bind(this),
153158
handleDelivery: this.#deliver.bind(this),
159+
handleEvaluate: this.#evaluate.bind(this),
154160
});
155161

156162
Promise.all([
@@ -250,6 +256,81 @@ export class VatSupervisor {
250256
return [this.#vatKVStore!.checkpoint(), deliveryError];
251257
}
252258

259+
/**
260+
* Evaluate code in the vat's REPL compartment.
261+
*
262+
* @param code - The code to evaluate.
263+
* @returns The result of the evaluation.
264+
*/
265+
#evaluate(code: string): EvaluateResult {
266+
if (!this.#evalCompartment) {
267+
return { success: false, error: 'Vat not initialized' };
268+
}
269+
try {
270+
const result = this.#evalCompartment.evaluate(code);
271+
const serialized = this.#serializeResult(result);
272+
if (serialized === undefined) {
273+
return { success: true };
274+
}
275+
return { success: true, value: serialized };
276+
} catch (error) {
277+
return {
278+
success: false,
279+
error: error instanceof Error ? error.message : String(error),
280+
};
281+
}
282+
}
283+
284+
/**
285+
* Serialize a value for JSON-RPC transport.
286+
* Non-serializable values (functions, symbols, etc.) are converted to string representation.
287+
*
288+
* @param value - The value to serialize.
289+
* @returns A JSON-serializable value.
290+
*/
291+
#serializeResult(value: unknown): Json | undefined {
292+
if (value === undefined) {
293+
return undefined;
294+
}
295+
if (value === null) {
296+
return null;
297+
}
298+
const type = typeof value;
299+
if (type === 'string') {
300+
return value as string;
301+
}
302+
if (type === 'number') {
303+
return value as number;
304+
}
305+
if (type === 'boolean') {
306+
return value as boolean;
307+
}
308+
if (type === 'bigint') {
309+
return `${String(value as bigint)}n`;
310+
}
311+
if (type === 'symbol') {
312+
return (value as symbol).toString();
313+
}
314+
if (type === 'function') {
315+
return `[Function: ${(value as { name?: string }).name ?? 'anonymous'}]`;
316+
}
317+
if (Array.isArray(value)) {
318+
return value.map((item) => this.#serializeResult(item) ?? null) as Json[];
319+
}
320+
if (type === 'object') {
321+
// Handle objects - try to serialize their properties
322+
const result: Record<string, Json> = {};
323+
for (const key of Object.keys(value as object)) {
324+
result[key] =
325+
this.#serializeResult((value as Record<string, unknown>)[key]) ??
326+
null;
327+
}
328+
return result;
329+
}
330+
// Fallback for any other types - unlikely to reach here
331+
return `[${type}]`;
332+
}
333+
253334
/**
254335
* Initialize the vat by loading its user code bundle and creating a liveslots
255336
* instance to manage it.
@@ -357,6 +438,14 @@ export class VatSupervisor {
357438
endowments,
358439
inescapableGlobalProperties,
359440
});
441+
442+
// Create a separate compartment for REPL evaluation.
443+
// Vat exports become endowments, so they're directly in scope.
444+
this.#evalCompartment = new Compartment({
445+
harden: globalThis.harden,
446+
...vatNS,
447+
});
448+
360449
return vatNS;
361450
};
362451

0 commit comments

Comments
 (0)