1148 lines · 32.2 KB · TypeScript
RawDownload
import type { ProtocolFramePayloadCodec } from "@rivet-dev/agentos-runtime-core/protocol-frames";import type { CreateVmConfig } from "@rivet-dev/agentos-runtime-core/vm-config";import {	type BrowserChildProcessPollEvent,	decodeChildProcessInput,	encodeChildProcessBytes,	parseChildProcessSpawnRequest,} from "./child-process-bridge.js";import type { ConvergedServicer } from "./converged-driver-setup.js";import { getBrowserSystemDriverOptions } from "./driver.js";import { base64ToBytes, toUint8Array } from "./encoding.js";import type {	CommandExecutor,	ExecOptions,	ExecResult,	NetworkAdapter,	NodeRuntimeDriver,	NodeRuntimeDriverFactory,	Permissions,	PtyOpenResult,	RunResult,	RuntimeDriverOptions,	StdioHook,	TimingMitigation,} from "./runtime.js";import {	createNetworkStub,	filterEnv,	wrapCommandExecutor,} from "./runtime.js";import {	applyProcessSignalStateUpdate,	type BrowserSignalRegistration,	parseProcessSignalStateArgs,} from "./signals.js";import {	assertBrowserSyncBridgeSupport,	type BrowserSyncBridgePayload,	type BrowserWorkerSyncRequestMessage,	createBrowserSyncBridgePayload,	isBrowserWorkerSyncOperation,	SYNC_BRIDGE_KIND_BINARY,	SYNC_BRIDGE_KIND_JSON,	SYNC_BRIDGE_KIND_NONE,	SYNC_BRIDGE_KIND_TEXT,	SYNC_BRIDGE_PAYLOAD_LIMIT_ERROR_CODE,	SYNC_BRIDGE_SIGNAL_KIND_INDEX,	SYNC_BRIDGE_SIGNAL_LENGTH_INDEX,	SYNC_BRIDGE_SIGNAL_STATE_INDEX,	SYNC_BRIDGE_SIGNAL_STATE_READY,	SYNC_BRIDGE_SIGNAL_STATUS_INDEX,	SYNC_BRIDGE_STATUS_ERROR,	SYNC_BRIDGE_STATUS_OK,	toBrowserSyncBridgeError,} from "./sync-bridge.js";import type {	BrowserWorkerExecOptions,	BrowserWorkerExtensionResponse,	BrowserWorkerInitPayload,	BrowserWorkerOutboundMessage,	BrowserWorkerRequestMessage,	BrowserWorkerResponseMessage,	BrowserWorkerPtyOpenedMessage,	BrowserWorkerStdioMessage,} from "./worker-protocol.js"; export interface BrowserRuntimeDriverFactoryOptions {	workerUrl?: URL | string;	// When provided, guest sync-bridge syscalls are serviced by the converged	// wasm kernel (fs/net/dns/module) instead of the legacy in-process TS kernel,	// with the remaining families falling back to legacy. The setup module is	// imported dynamically so the unbundled legacy load never pulls in its	// bundled-only `@rivet-dev/agentos-runtime-core` imports.	convergedSidecar?: ConvergedSidecarFactoryOptions;} export interface ConvergedSidecarHandle {	pushFrame(frame: Uint8Array): Uint8Array;	// Sets the execution id the sidecar's execution host bridge echoes for the	// next `execute`; omitted when the sidecar has no execution host bridge	// (then guest net/dgram are unavailable but fs/module still converge).	setNextExecutionId?(executionId: string): void;} export interface ConvergedSidecarFactoryOptions {	loadSidecar(): Promise<ConvergedSidecarHandle>;	config: CreateVmConfig;	codec?: ProtocolFramePayloadCodec;	onFsReadDenied?: () => void;} type PendingRequest = {	resolve(value: unknown): void;	reject(reason: unknown): void;	hook?: StdioHook;	onPtyOpen?: (pty: PtyOpenResult) => void;	executionId?: string;}; type SyncBridgeResponse =	| { kind: typeof SYNC_BRIDGE_KIND_NONE }	| { kind: typeof SYNC_BRIDGE_KIND_TEXT; value: string }	| { kind: typeof SYNC_BRIDGE_KIND_BINARY; value: Uint8Array }	| { kind: typeof SYNC_BRIDGE_KIND_JSON; value: unknown }; type BrowserChildProcessSession = {	executionId: string;	process: ReturnType<CommandExecutor["spawn"]>;	events: BrowserChildProcessPollEvent[];	exited: boolean;}; function normalizeCommandWaitResult(	result: Awaited<ReturnType<BrowserChildProcessSession["process"]["wait"]>>,): Extract<BrowserChildProcessPollEvent, { type: "exit" }> {	if (typeof result === "number") {		return { type: "exit", exitCode: result, signal: null };	}	return {		type: "exit",		exitCode: result.exitCode,		signal: result.signal,	};} type BrowserNetworkPermission = NonNullable<	RuntimeDriverOptions["system"]["permissions"]>["network"]; type BrowserSyncBridgeHost = {	commandExecutor: CommandExecutor;	networkAdapter: NetworkAdapter;	childProcessSessions: Map<number, BrowserChildProcessSession>;	signalStates: Map<string, Map<number, BrowserSignalRegistration>>;	allocateChildProcessSessionId(): number;	networkPermission?: BrowserNetworkPermission;}; const DEFAULT_BROWSER_TIMING_MITIGATION: TimingMitigation = "freeze"; const BROWSER_OPTION_VALIDATORS = [	{		label: "memoryLimit",		hasValue: (options: RuntimeDriverOptions) =>			options.memoryLimit !== undefined,	},	{		label: "cpuTimeLimitMs",		hasValue: (options: RuntimeDriverOptions) =>			options.cpuTimeLimitMs !== undefined,	},]; // Permission predicates are consumed only on the trusted main thread (here and// in `wrapCommandExecutor`); they are never serialized to or evaluated in the// untrusted guest worker. The kernel is the sole enforcement point for fs/net.// The one thing the worker needs is an already-filtered env, so apply the env// predicate to a process config on this side before it crosses the boundary.function filterProcessConfigEnv<T extends { env?: Record<string, string> }>(	processConfig: T,	permissions?: Permissions,): T {	if (!permissions?.env || !processConfig.env) {		return processConfig;	}	return { ...processConfig, env: filterEnv(processConfig.env, permissions) };} function resolveWorkerUrl(workerUrl?: URL | string): URL {	if (workerUrl instanceof URL) {		return workerUrl;	}	if (workerUrl) {		return new URL(workerUrl, import.meta.url);	}	return new URL("./worker.js", import.meta.url);} function createWorkerControlToken(): string {	if (typeof globalThis.crypto?.randomUUID === "function") {		return globalThis.crypto.randomUUID();	}	return `browser-runtime-${Date.now()}-${Math.random().toString(16).slice(2)}`;} function toBrowserWorkerExecOptions(	options?: ExecOptions,	permissions?: Permissions,): BrowserWorkerExecOptions | undefined {	if (!options) {		return undefined;	}	return {		filePath: options.filePath,		// Filter per-exec env on the trusted main thread before it reaches the		// guest worker (the worker no longer re-applies the env permission).		env:			options.env && permissions?.env				? filterEnv(options.env, permissions)				: options.env,		cwd: options.cwd,		stdin: options.stdin,		stdioPty: options.stdioPty			? {					open: options.stdioPty.open,					slaveFd: options.stdioPty.slaveFd,					columns: options.stdioPty.columns,					rows: options.stdioPty.rows,				}			: undefined,		timingMitigation: options.timingMitigation,		persistent: options.persistent,		streamingStdin: options.streamingStdin,	};} function validateBrowserRuntimeOptions(options: RuntimeDriverOptions): void {	const unsupported = BROWSER_OPTION_VALIDATORS.filter((validator) =>		validator.hasValue(options),	).map((validator) => validator.label);	if (unsupported.length === 0) {		return;	}	throw new Error(		`Browser runtime does not support Node-only options: ${unsupported.join(", ")}`,	);} function validateBrowserExecOptions(options?: ExecOptions): void {	const unsupported: string[] = [];	if (options?.cpuTimeLimitMs !== undefined) {		unsupported.push("cpuTimeLimitMs");	}	if (unsupported.length === 0) {		return;	}	throw new Error(		`Browser runtime does not support Node-only exec options: ${unsupported.join(", ")}`,	);} function isStdioMessage(	message: BrowserWorkerOutboundMessage,): message is BrowserWorkerStdioMessage {	return message.type === "stdio";} function isPtyOpenedMessage(	message: BrowserWorkerOutboundMessage,): message is BrowserWorkerPtyOpenedMessage {	return message.type === "pty-opened";} function isResponseMessage(	message: BrowserWorkerOutboundMessage,): message is BrowserWorkerResponseMessage {	return message.type === "response";} function isSyncRequestMessage(	message: BrowserWorkerOutboundMessage,): message is BrowserWorkerSyncRequestMessage {	return message.type === "sync-request";} function throwBridgePayloadTooLarge(	label: string,	actualBytes: number,	maxBytes: number,): never {	const error = new Error(		`[${SYNC_BRIDGE_PAYLOAD_LIMIT_ERROR_CODE}] ${label}: payload is ${actualBytes} bytes, limit is ${maxBytes} bytes`,	);	(error as { code?: string }).code = SYNC_BRIDGE_PAYLOAD_LIMIT_ERROR_CODE;	throw error;} async function waitForChildProcessEvent(	session: BrowserChildProcessSession,	waitMs: number,): Promise<BrowserChildProcessPollEvent | null> {	const deadline = Date.now() + Math.max(0, waitMs);	while (		session.events.length === 0 &&		!session.exited &&		Date.now() < deadline	) {		await new Promise((resolve) => setTimeout(resolve, 5));	}	return session.events.shift() ?? null;} function createSyncBridgeResponseBytes(	response: SyncBridgeResponse,	encoder: TextEncoder,): Uint8Array {	switch (response.kind) {		case SYNC_BRIDGE_KIND_NONE:			return new Uint8Array(0);		case SYNC_BRIDGE_KIND_TEXT:			return encoder.encode(response.value);		case SYNC_BRIDGE_KIND_BINARY:			return response.value;		case SYNC_BRIDGE_KIND_JSON:			return encoder.encode(JSON.stringify(response.value));		default:			return new Uint8Array(0);	}} async function handleSyncBridgeOperation(	host: BrowserSyncBridgeHost,	message: BrowserWorkerSyncRequestMessage,): Promise<SyncBridgeResponse> {	switch (message.operation) {		case "child_process.spawn": {			const request = parseChildProcessSpawnRequest(				message.args[0],				"child_process.spawn request",			);			const { command, args } = request;			const options = request.options ?? {};			const sessionId = host.allocateChildProcessSessionId();			const events: BrowserChildProcessPollEvent[] = [];			const process = host.commandExecutor.spawn(command, args, {				argv0: options.argv0,				cwd: options.cwd,				env: options.env,				onStdout: (data) => {					events.push({						type: "stdout",						data: encodeChildProcessBytes(data),					});				},				onStderr: (data) => {					events.push({						type: "stderr",						data: encodeChildProcessBytes(data),					});				},			});			const session: BrowserChildProcessSession = {				executionId: message.executionId,				process,				events,				exited: false,			};			void process				.wait()				.then((result) => {					session.exited = true;					session.events.push(normalizeCommandWaitResult(result));				})				.catch((error) => {					session.exited = true;					const message =						error instanceof Error ? error.message : String(error);					session.events.push({						type: "stderr",						data: encodeChildProcessBytes(new TextEncoder().encode(message)),					});					session.events.push({ type: "exit", exitCode: 1, signal: null });				});			host.childProcessSessions.set(sessionId, session);			return { kind: SYNC_BRIDGE_KIND_JSON, value: sessionId };		}		case "child_process.poll": {			const sessionId = Number(message.args[0]);			const waitMs = Number(message.args[1] ?? 0);			const session = host.childProcessSessions.get(sessionId);			if (!session || session.executionId !== message.executionId) {				return { kind: SYNC_BRIDGE_KIND_JSON, value: null };			}			const event = await waitForChildProcessEvent(session, waitMs);			if (event?.type === "exit" && session.events.length === 0) {				host.childProcessSessions.delete(sessionId);			}			return { kind: SYNC_BRIDGE_KIND_JSON, value: event };		}		case "child_process.write_stdin": {			const sessionId = Number(message.args[0]);			const session = host.childProcessSessions.get(sessionId);			if (!session || session.executionId !== message.executionId) {				throw new Error(`unknown child_process session ${sessionId}`);			}			session?.process.writeStdin(toUint8Array(message.args[1]));			return { kind: SYNC_BRIDGE_KIND_NONE };		}		case "child_process.close_stdin": {			const sessionId = Number(message.args[0]);			const session = host.childProcessSessions.get(sessionId);			if (!session || session.executionId !== message.executionId) {				throw new Error(`unknown child_process session ${sessionId}`);			}			session.process.closeStdin();			return { kind: SYNC_BRIDGE_KIND_NONE };		}		case "child_process.kill": {			const sessionId = Number(message.args[0]);			const signal = Number(message.args[1]);			const session = host.childProcessSessions.get(sessionId);			if (!session || session.executionId !== message.executionId) {				throw new Error(`unknown child_process session ${sessionId}`);			}			const accepted = session.process.kill(signal);			return {				kind: SYNC_BRIDGE_KIND_JSON,				value: accepted !== false,			};		}		case "child_process.spawn_sync": {			const request = parseChildProcessSpawnRequest(				message.args[0],				"child_process.spawn_sync request",			);			const { command, args } = request;			const options = request.options ?? {};			const stdoutChunks: Uint8Array[] = [];			const stderrChunks: Uint8Array[] = [];			const proc = host.commandExecutor.spawn(command, args, {				argv0: options.argv0,				cwd: options.cwd,				env: options.env,				onStdout: (data) => stdoutChunks.push(data),				onStderr: (data) => stderrChunks.push(data),			});			const input = decodeChildProcessInput(options.input);			if (input !== undefined) {				proc.writeStdin(input);			}			proc.closeStdin();			const waitResult = normalizeCommandWaitResult(await proc.wait());			const decoder = new TextDecoder();			return {				kind: SYNC_BRIDGE_KIND_TEXT,				value: JSON.stringify({					stdout: stdoutChunks.map((chunk) => decoder.decode(chunk)).join(""),					stderr: stderrChunks.map((chunk) => decoder.decode(chunk)).join(""),					code: waitResult.exitCode,					signal: waitResult.signal,				}),			};		}		case "process.signal_state": {			const { signal, registration } = parseProcessSignalStateArgs(				message.args,			);			applyProcessSignalStateUpdate(				host.signalStates,				message.executionId,				signal,				registration,			);			return { kind: SYNC_BRIDGE_KIND_NONE };		}		case "network.fetch": {			const url = String(message.args[0] ?? "");			const options = (message.args[1] ?? {}) as Parameters<				NetworkAdapter["fetch"]			>[1];			const result = await host.networkAdapter.fetch(url, options);			return { kind: SYNC_BRIDGE_KIND_JSON, value: result };		}		default:			throw new Error(				`Unsupported browser sync bridge operation: ${String(message.operation)}`,			);	}} export class BrowserRuntimeDriver implements NodeRuntimeDriver {	private readonly worker: Worker;	private readonly pending = new Map<number, PendingRequest>();	private readonly controlToken = createWorkerControlToken();	private readonly defaultOnStdio?: StdioHook;	private readonly defaultTimingMitigation: TimingMitigation;	private readonly networkAdapter: NetworkAdapter;	private readonly commandExecutor: CommandExecutor;	private readonly syncBridge: BrowserSyncBridgePayload;	private readonly childProcessSessions = new Map<		number,		BrowserChildProcessSession	>();	private readonly signalStates = new Map<		string,		Map<number, BrowserSignalRegistration>	>();	private readonly ready: Promise<void>;	private readonly encoder = new TextEncoder();	private nextId = 1;	private nextExecutionId = 1;	private nextChildProcessSessionId = 1;	private disposed = false;	private readonly networkPermission?: BrowserNetworkPermission;	private readonly permissions?: Permissions;	private convergedServicer?: ConvergedServicer;	private readonly convergedReady?: Promise<void>; 	constructor(		options: RuntimeDriverOptions,

Lines 1–500 of 1148.