Agent Sandbox Registry

awesome-providers

opensandbox/src/epp.mjs

无中文版

opensandbox/src/epp.mjs

// EPP/1: newline-delimited JSON-RPC 2.0 over stdin/stdout. Logs go to stderr only.

import { createInterface } from "node:readline";
import { CODES, RpcError } from "./errors.mjs";

const MAX_LINE_BYTES = 8 * 1024 * 1024 - 1024;

/**
 * Serve requests until stdin closes or `shutdown` is answered.
 *
 * `dispatch(method, params, call)` returns the result or throws; `call` carries:
 *   signal          aborted on `$/cancel` or shutdown
 *   notify(m, p)    send a notification
 *   afterResponse(fn)  run `fn` once this request's response has been written
 *   shutdown()      ask the loop to stop after this response
 */
export function serve({ input, output, dispatch, onClose = () => {}, log = () => {} }) {
  const inflight = new Map();
  let stopping = false;

  const write = (obj) => {
    let line = JSON.stringify(obj);
    if (Buffer.byteLength(line) > MAX_LINE_BYTES) {
      line = JSON.stringify({
        jsonrpc: "2.0",
        id: obj.id ?? null,
        error: {
          code: CODES.BAD_PARAMS,
          message: "Result is larger than the 8 MiB EPP line limit; request a smaller range.",
          data: { suggestion: "Read the file in chunks (offset/length)." },
        },
      });
    }
    return output.write(`${line}\n`);
  };
  const notify = (method, params) => write({ jsonrpc: "2.0", method, params });

  const finish = () => {
    if (stopping) return;
    stopping = true;
    for (const controller of inflight.values()) controller.abort();
    inflight.clear();
    onClose();
  };

  async function handle(message) {
    const { id, method, params } = message;
    if (id === undefined || id === null) {
      if (method === "$/cancel" && params && inflight.has(params.id)) inflight.get(params.id).abort();
      return;
    }
    if (typeof method !== "string") {
      write({ jsonrpc: "2.0", id, error: { code: -32600, message: "Invalid request" } });
      return;
    }
    const controller = new AbortController();
    inflight.set(id, controller);
    const after = [];
    let stopAfter = false;
    try {
      const result = await dispatch(method, params ?? {}, {
        signal: controller.signal,
        notify,
        afterResponse: (fn) => after.push(fn),
        shutdown: () => { stopAfter = true; },
      });
      write({ jsonrpc: "2.0", id, result: result ?? {} });
    } catch (err) {
      if (err instanceof RpcError) {
        write({ jsonrpc: "2.0", id, error: { code: err.code, message: err.message, data: err.data } });
      } else {
        log(`internal error in ${method}: ${err?.stack ?? err}`);
        write({
          jsonrpc: "2.0",
          id,
          error: { code: CODES.FAIL, message: `${err?.name ?? "Error"}: ${err?.message ?? err}`, data: {} },
        });
      }
    } finally {
      inflight.delete(id);
    }
    for (const fn of after) {
      try {
        fn();
      } catch (err) {
        log(`afterResponse hook failed: ${err}`);
      }
    }
    if (stopAfter) finish();
  }

  const rl = createInterface({ input, crlfDelay: Infinity });
  rl.on("line", (line) => {
    const text = line.trim();
    if (!text) return;
    let message;
    try {
      message = JSON.parse(text);
    } catch {
      write({ jsonrpc: "2.0", id: null, error: { code: -32700, message: "Parse error" } });
      return;
    }
    if (!message || typeof message !== "object") return;
    handle(message).catch((err) => log(`unhandled: ${err?.stack ?? err}`));
  });
  rl.on("close", finish);
  return { notify, close: finish };
}