239 lines · 5.5 KB · TypeScript
RawDownload
import { fork, type ChildProcess } from "node:child_process";import { fileURLToPath } from "node:url"; import type {  CodeExecutor,  ExecuteResult,  ToolInvoker,} from "@executor/codemode-core";import * as Effect from "effect/Effect"; export type SesExecutorOptions = {  timeoutMs?: number;  allowFetch?: boolean;}; const DEFAULT_TIMEOUT_MS = 5 * 60_000;const DEFAULT_EVALUATION_ID = "evaluation";const WORKER_PATH = fileURLToPath(new URL("./sandbox-worker.mjs", import.meta.url)); type ReadyMessage = {  type: "ready";}; type ToolCallMessage = {  type: "tool-call";  callId: string;  path: string;  args: unknown;}; type ResultMessage = {  type: "result";  id: string;  value?: unknown;  error?: string;  logs?: string[];}; type WorkerMessage = ReadyMessage | ToolCallMessage | ResultMessage; type EvaluateMessage = {  type: "evaluate";  id: string;  code: string;  allowFetch: boolean;}; type ToolResponseMessage = {  type: "tool-response";  callId: string;  value?: unknown;  error?: string;}; const toError = (cause: unknown): Error =>  cause instanceof Error ? cause : new Error(String(cause)); const createSandboxWorker = (): ChildProcess => fork(WORKER_PATH, [], { silent: true }); const describeWorkerExit = (  code: number | null,  signal: NodeJS.Signals | null,  stderr: string,): Error => {  const stderrSuffix = stderr.trim().length > 0 ? `\n${stderr.trim()}` : "";   if (signal) {    return new Error(`SES worker exited from signal ${signal}${stderrSuffix}`);  }   if (typeof code === "number") {    return new Error(`SES worker exited with code ${code}${stderrSuffix}`);  }   return new Error(`SES worker exited before returning a result${stderrSuffix}`);}; const sendMessage = (child: ChildProcess, message: EvaluateMessage | ToolResponseMessage): void => {  if (typeof child.send !== "function") {    throw new Error("SES worker IPC channel is unavailable");  }   child.send(message);}; const evaluateInSandbox = async (  options: SesExecutorOptions,  code: string,  toolInvoker: ToolInvoker,): Promise<ExecuteResult> => {  const child = createSandboxWorker();  const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;  const stderrChunks: string[] = [];   if (child.stderr) {    child.stderr.on("data", (chunk) => {      stderrChunks.push(chunk.toString());    });  }   return new Promise<ExecuteResult>((resolve, reject) => {    let settled = false;    let timeoutHandle: ReturnType<typeof setTimeout> | undefined;     const cleanup = () => {      if (timeoutHandle) {        clearTimeout(timeoutHandle);        timeoutHandle = undefined;      }       child.off("error", onError);      child.off("exit", onExit);      child.off("message", onMessage);       if (!child.killed) {        child.kill();      }    };     const settle = (effect: () => void) => {      if (settled) {        return;      }       settled = true;      cleanup();      effect();    };     const resetTimeout = () => {      if (timeoutHandle) {        clearTimeout(timeoutHandle);      }       timeoutHandle = setTimeout(() => {        settle(() => {          reject(new Error(`Execution timed out after ${timeoutMs}ms`));        });      }, timeoutMs);    };     const onError = (error: Error) => {      settle(() => {        reject(error);      });    };     const onExit = (code: number | null, signal: NodeJS.Signals | null) => {      settle(() => {        reject(describeWorkerExit(code, signal, stderrChunks.join("")));      });    };     const onMessage = (message: WorkerMessage) => {      if (message.type === "ready") {        resetTimeout();        sendMessage(child, {          type: "evaluate",          id: DEFAULT_EVALUATION_ID,          code,          allowFetch: options.allowFetch === true,        });        return;      }       if (message.type === "tool-call") {        resetTimeout();         void Effect.runPromise(toolInvoker.invoke({ path: message.path, args: message.args }))          .then((value) => {            if (settled) {              return;            }             resetTimeout();            sendMessage(child, {              type: "tool-response",              callId: message.callId,              value,            });          })          .catch((cause) => {            if (settled) {              return;            }             resetTimeout();            const error = toError(cause);            sendMessage(child, {              type: "tool-response",              callId: message.callId,              error: error.stack ?? error.message,            });          });        return;      }       if (message.type === "result") {        settle(() => {          if (message.error) {            resolve({              result: null,              error: message.error,              logs: message.logs ?? [],            });            return;          }           resolve({            result: message.value,            logs: message.logs ?? [],          });        });      }    };     child.on("error", onError);    child.on("exit", onExit);    child.on("message", onMessage);     resetTimeout();  });}; const runInSes = (  options: SesExecutorOptions,  code: string,  toolInvoker: ToolInvoker,): Effect.Effect<ExecuteResult, Error> =>  Effect.tryPromise({    try: () => evaluateInSandbox(options, code, toolInvoker),    catch: toError,  }); export const makeSesExecutor = (  options: SesExecutorOptions = {},): CodeExecutor => ({  execute: (code: string, toolInvoker: ToolInvoker) => runInSes(options, code, toolInvoker),});