branch:
node-executor-server.ts
4638 bytesRaw
/**
 * Standalone Node.js HTTP server that executes LLM-generated code in a VM sandbox.
 *
 * Tool calls from the sandboxed code are routed back to the caller via HTTP POSTs
 * to the provided callbackUrl.
 *
 * Usage:
 *   npx tsx node-executor-server.ts
 *
 * Environment variables:
 *   PORT          — listen port (default 3001)
 *   TIMEOUT_MS    — execution timeout in milliseconds (default 30000)
 */

import {
  createServer,
  type IncomingMessage,
  type ServerResponse
} from "node:http";
import * as vm from "node:vm";

const PORT = Number(process.env.PORT) || 3001;
const TIMEOUT_MS = Number(process.env.TIMEOUT_MS) || 30_000;

function readBody(req: IncomingMessage): Promise<string> {
  return new Promise((resolve, reject) => {
    const chunks: Buffer[] = [];
    req.on("data", (chunk: Buffer) => chunks.push(chunk));
    req.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
    req.on("error", reject);
  });
}

interface ExecuteRequest {
  code: string;
  callbackUrl: string;
  tools: string[];
}

async function handleExecute(
  body: ExecuteRequest
): Promise<{ result: unknown; error?: string; logs: string[] }> {
  const { code, callbackUrl, tools } = body;
  const logs: string[] = [];

  // Build a codemode proxy that routes tool calls back via HTTP
  const codemode: Record<string, (args: unknown) => Promise<unknown>> = {};
  for (const toolName of tools) {
    codemode[toolName] = async (args: unknown) => {
      const res = await fetch(`${callbackUrl}/${toolName}`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(args ?? {})
      });
      const data = (await res.json()) as { result?: unknown; error?: string };
      if (data.error) throw new Error(data.error);
      return data.result;
    };
  }

  const sandbox = {
    codemode,
    console: {
      log: (...args: unknown[]) => logs.push(args.map(String).join(" ")),
      error: (...args: unknown[]) =>
        logs.push(`[error] ${args.map(String).join(" ")}`),
      warn: (...args: unknown[]) =>
        logs.push(`[warn] ${args.map(String).join(" ")}`)
    },
    fetch: globalThis.fetch,
    setTimeout: globalThis.setTimeout,
    clearTimeout: globalThis.clearTimeout,
    URL,
    Response,
    Request,
    Headers
  };

  const context = vm.createContext(sandbox);

  try {
    // The code is expected to be an async arrow function expression, e.g.:
    //   async () => { ... }
    // We evaluate it to get the function, then call it.
    const script = new vm.Script(`(${code})()`, {
      filename: "codemode-exec.js"
    });
    const result = await script.runInContext(context, { timeout: TIMEOUT_MS });
    return { result, logs };
  } catch (err) {
    return {
      result: undefined,
      error: err instanceof Error ? err.message : String(err),
      logs
    };
  }
}

const server = createServer(
  async (req: IncomingMessage, res: ServerResponse) => {
    // CORS headers for local dev
    res.setHeader("Access-Control-Allow-Origin", "*");
    res.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS");
    res.setHeader("Access-Control-Allow-Headers", "Content-Type");

    if (req.method === "OPTIONS") {
      res.writeHead(204);
      res.end();
      return;
    }

    if (req.method === "POST" && req.url === "/execute") {
      try {
        const raw = await readBody(req);
        const body = JSON.parse(raw) as ExecuteRequest;

        if (!body.code || !body.callbackUrl || !Array.isArray(body.tools)) {
          res.writeHead(400, { "Content-Type": "application/json" });
          res.end(
            JSON.stringify({
              error: "Missing required fields: code, callbackUrl, tools"
            })
          );
          return;
        }

        const result = await handleExecute(body);
        res.writeHead(200, { "Content-Type": "application/json" });
        res.end(JSON.stringify(result));
      } catch (err) {
        res.writeHead(500, { "Content-Type": "application/json" });
        res.end(
          JSON.stringify({
            error: err instanceof Error ? err.message : String(err)
          })
        );
      }
      return;
    }

    // Health check
    if (req.method === "GET" && req.url === "/health") {
      res.writeHead(200, { "Content-Type": "application/json" });
      res.end(JSON.stringify({ status: "ok" }));
      return;
    }

    res.writeHead(404, { "Content-Type": "application/json" });
    res.end(JSON.stringify({ error: "Not found" }));
  }
);

server.listen(PORT, () => {
  console.log(`Node executor server listening on http://localhost:${PORT}`);
});