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. Load more
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,