Agent Sandbox Registry

awesome-providers

opensandbox/tests/mock/mock-opensandbox.mjs

无中文版

opensandbox/tests/mock/mock-opensandbox.mjs

// A small in-memory OpenSandbox that reproduces the quirks the plugin has to handle
// (see docs/en/design/provider-opensandbox-mapping.md). Lifecycle API and per-sandbox "execd" live
// on one HTTP server; each sandbox's files are a real temp directory and commands run as local processes.
//
// Options: { apiKey, legacyExecd, gateway, onRequest }
//   legacyExecd  execd < the line-preserving build: no `argv`, one stdout event per line without terminator
//   gateway      resolve endpoints with required headers (the data plane rejects requests without them)

import { createServer } from "node:http";
import { spawn } from "node:child_process";
import { mkdtempSync, mkdirSync, rmSync, readFileSync, writeFileSync, statSync, readdirSync, renameSync, existsSync, chmodSync } from "node:fs";
import { tmpdir } from "node:os";
import { join, dirname, resolve, sep } from "node:path";
import { randomUUID, randomBytes } from "node:crypto";

const LABEL = /^[A-Za-z0-9]([-A-Za-z0-9_.]*[A-Za-z0-9])?$/;
const send = (res, status, body, headers = {}) => {
  const text = body === undefined ? "" : typeof body === "string" ? body : JSON.stringify(body);
  res.writeHead(status, { "Content-Type": typeof body === "string" ? "text/plain" : "application/json", ...headers });
  res.end(text);
};
const readBody = (req) => new Promise((resolveBody) => {
  const chunks = [];
  req.on("data", (c) => chunks.push(c));
  req.on("end", () => resolveBody(Buffer.concat(chunks)));
});

function parseMultipart(buf, contentType) {
  const boundary = /boundary=(.+)$/.exec(contentType ?? "")?.[1];
  if (!boundary) return [];
  const delim = Buffer.from(`--${boundary}`);
  const parts = [];
  let pos = buf.indexOf(delim);
  while (pos >= 0) {
    const next = buf.indexOf(delim, pos + delim.length);
    if (next < 0) break;
    const chunk = buf.subarray(pos + delim.length + 2, next - 2);
    const split = chunk.indexOf("\r\n\r\n");
    const head = chunk.subarray(0, split).toString();
    parts.push({
      name: /name="([^"]*)"/.exec(head)?.[1],
      filename: /filename="([^"]*)"/.exec(head)?.[1],
      data: chunk.subarray(split + 4),
    });
    pos = next;
  }
  return parts;
}

export async function startMock(options = {}) {
  const { apiKey = "mock-key", legacyExecd = false, gateway = false, onRequest } = options;
  const sandboxes = new Map();
  const commands = new Map();
  let origin = "";

  const fsPath = (sb, p) => {
    if (p.startsWith("/tmp/.abx-stdin-")) return p; // the plugin's stdin spool is read through an absolute path
    const full = resolve(sb.root, `.${p.startsWith("/") ? p : `/${p}`}`);
    if (full !== sb.root && !full.startsWith(sb.root + sep)) throw new Error("path escapes sandbox");
    return full;
  };
  const view = (sb) => ({
    id: sb.id,
    image: { uri: sb.image },
    status: { state: sb.state, message: `mock ${sb.state}` },
    metadata: sb.metadata,
    entrypoint: sb.entrypoint,
    expiresAt: sb.expiresAt,
    createdAt: sb.createdAt,
  });
  const fileInfo = (sb, p) => {
    const st = statSync(fsPath(sb, p), { throwIfNoEntry: false });
    if (!st) return undefined;
    return {
      path: p,
      type: st.isDirectory() ? "directory" : "file",
      size: st.size,
      modified_at: st.mtime.toISOString(),
      created_at: st.ctime.toISOString(),
      owner: "root",
      group: "root",
      mode: Number.parseInt((st.mode & 0o777).toString(8), 10),
    };
  };
  const walk = (dir, depth, max, out = []) => {
    for (const name of readdirSync(dir).sort()) {
      const full = join(dir, name);
      out.push(full);
      if (statSync(full).isDirectory() && depth < max) walk(full, depth + 1, max, out);
    }
    return out;
  };

  // ------------------------------------------------------------------ execd
  async function execd(req, res, sb, path, query) {
    if (gateway && req.headers["x-gw-route"] !== "1") return send(res, 401, { error: "Unauthorized: invalid or missing header X-GW-ROUTE" });
    const method = req.method;
    if (path === "/ping") return send(res, 200);
    if (path === "/command" && method === "POST") {
      const body = JSON.parse((await readBody(req)).toString());
      if (legacyExecd && body.argv) {
        return send(res, 400, { code: "INVALID_REQUEST_BODY", message: "Key: 'RunCommandRequest.Command' Error:Field validation for 'Command' failed on the 'required' tag" });
      }
      const argv = body.argv ?? ["sh", "-c", body.command];
      const id = randomBytes(16).toString("hex");
      res.writeHead(200, { "Content-Type": "text/event-stream" });
      const emit = (ev) => res.write(`${JSON.stringify(ev)}\n\n`);
      emit({ type: "init", text: id, timestamp: Date.now() });
      const record = { log: "", running: true, exit: undefined };
      commands.set(id, record);
      const cwd = body.cwd ? fsPath(sb, body.cwd) : sb.root;
      mkdirSync(cwd, { recursive: true });
      let child;
      try {
        child = spawn(argv[0], argv.slice(1), { cwd, env: { PATH: process.env.PATH, HOME: sb.root, ...sb.env, ...(body.envs ?? {}) }, detached: true });
      } catch (err) {
        emit({ type: "error", error: { ename: "CommandExecError", evalue: String(err.message), traceback: [] } });
        return res.end();
      }
      let startFailed = false;
      child.on("error", (err) => {
        startFailed = true;
        emit({ type: "error", error: { ename: "CommandExecError", evalue: `exec: "${argv[0]}": executable file not found in $PATH (${err.code})`, traceback: [] } });
        record.running = false;
        res.end();
      });
      const pump = (stream, type) => {
        let carry = "";
        stream.on("data", (chunk) => {
          record.log += chunk;
          const text = carry + chunk.toString();
          if (!legacyExecd) return emit({ type, text, timestamp: Date.now() });
          const lines = text.split("\n");
          carry = lines.pop();
          for (const line of lines) emit({ type, text: line.replace(/\r$/, ""), timestamp: Date.now() });
        });
        stream.on("end", () => { if (legacyExecd && carry) emit({ type, text: carry }); });
      };
      pump(child.stdout, "stdout");
      pump(child.stderr, "stderr");
      let timedOut = false;
      const timer = body.timeout ? setTimeout(() => { timedOut = true; try { process.kill(-child.pid, "SIGKILL"); } catch { /* gone */ } }, body.timeout) : undefined;
      req.on("close", () => { if (!body.background && record.running) try { process.kill(-child.pid, "SIGKILL"); } catch { /* gone */ } });
      record.child = child;
      if (body.background) {
        emit({ type: "execution_complete", execution_time: 1 });
        res.end();
      }
      child.on("close", (code, signal) => {
        clearTimeout(timer);
        if (startFailed) return;
        record.running = false;
        record.exit = code ?? 137;
        if (body.background) return;
        if (timedOut || signal) emit({ type: "error", error: { ename: "CommandExecError", evalue: "-1", traceback: ["signal: killed"] } });
        else if (code !== 0) emit({ type: "error", error: { ename: "CommandExecError", evalue: String(code), traceback: [`exit status ${code}`] } });
        else emit({ type: "execution_complete", execution_time: 1 });
        res.end();
      });
      return undefined;
    }
    let m = /^\/command\/status\/(.+)$/.exec(path);
    if (m) {
      const rec = commands.get(m[1]);
      if (!rec) return send(res, 404, { code: "INVALID_REQUEST_BODY", message: `command not found: ${m[1]}` });
      return send(res, 200, { id: m[1], content: "", running: rec.running, exit_code: rec.exit });
    }
    m = /^\/command\/([^/]+)\/logs$/.exec(path);
    if (m) {
      const rec = commands.get(m[1]);
      if (!rec) return send(res, 404, { code: "INVALID_REQUEST_BODY", message: "command not found" });
      const cursor = Number(query.get("cursor") ?? 0);
      return send(res, 200, rec.log.slice(cursor), { "EXECD-COMMANDS-TAIL-CURSOR": String(rec.log.length) });
    }
    if (path === "/command" && method === "DELETE") {
      const rec = commands.get(query.get("id"));
      if (rec?.child) try { process.kill(-rec.child.pid, "SIGKILL"); } catch { /* gone */ }
      return send(res, 200);
    }
    if (path === "/files/info" && method === "GET") {
      const out = {};
      for (const p of query.getAll("path")) {
        const info = fileInfo(sb, p);
        if (!info) return send(res, 404, { code: "FILE_NOT_FOUND", message: `file not found: ${p}` });
        out[p] = info;
      }
      return send(res, 200, out);
    }
    if (path === "/files/download" && method === "GET") {
      const full = fsPath(sb, query.get("path"));
      if (!existsSync(full)) return send(res, 404, { code: "FILE_NOT_FOUND", message: "file not found" });
      if (statSync(full).isDirectory()) return send(res, 200, "");
      let data = readFileSync(full);
      const range = /bytes=(\d+)-(\d*)/.exec(req.headers.range ?? "");
      if (range) {
        data = data.subarray(Number(range[1]), range[2] ? Number(range[2]) + 1 : undefined);
        res.writeHead(206, { "Content-Type": "application/octet-stream" });
        return res.end(data);
      }
      res.writeHead(200, { "Content-Type": "application/octet-stream" });
      return res.end(data);
    }
    if (path === "/files/upload" && method === "POST") {
      const parts = parseMultipart(await readBody(req), req.headers["content-type"]);
      const meta = parts.find((p) => p.name === "metadata");
      const file = parts.find((p) => p.name === "file");
      if (!meta?.filename) return send(res, 400, { code: "INVALID_REQUEST_BODY", message: "metadata file is missing" });
      const info = JSON.parse(meta.data.toString());
      const full = fsPath(sb, info.path);
      mkdirSync(dirname(full), { recursive: true });
      writeFileSync(full, file.data);
      if (info.mode) chmodSync(full, Number.parseInt(String(info.mode), 8));
      return send(res, 200);
    }
    if (path === "/files" && method === "DELETE") {
      const full = fsPath(sb, query.get("path"));
      if (existsSync(full) && statSync(full).isDirectory()) return send(res, 500, { code: "RUNTIME_ERROR", message: "path is a directory" });
      rmSync(full, { force: true });
      return send(res, 200);
    }
    if (path === "/files/mv" && method === "POST") {
      for (const { src, dest } of JSON.parse((await readBody(req)).toString())) {
        if (!existsSync(fsPath(sb, src))) return send(res, 404, { code: "FILE_NOT_FOUND", message: "source not found" });
        if (existsSync(fsPath(sb, dest))) return send(res, 500, { code: "RUNTIME_ERROR", message: "destination path already exists" });
        mkdirSync(dirname(fsPath(sb, dest)), { recursive: true });
        renameSync(fsPath(sb, src), fsPath(sb, dest));
      }
      return send(res, 200);
    }
    if (path === "/files/permissions" && method === "POST") {
      for (const [p, spec] of Object.entries(JSON.parse((await readBody(req)).toString()))) chmodSync(fsPath(sb, p), Number.parseInt(String(spec.mode), 8));
      return send(res, 200);
    }
    if (path === "/files/search" && method === "GET") {
      const rootDir = fsPath(sb, query.get("path"));
      const pattern = query.get("pattern") ?? "**";
      const re = new RegExp(`^${pattern.replace(/\*\*\/|\*\*|\*|[.+^${}()|[\]\\]/g, (t) => ({ "**/": "(?:.*/)?", "**": ".*", "*": "[^/]*" })[t] ?? `\\${t}`)}$`);
      const found = walk(rootDir, 1, 99).filter((f) => re.test(f.slice(rootDir.length + 1)));
      return send(res, 200, found.map((f) => fileInfo(sb, f.slice(sb.root.length))));
    }
    if (path === "/directories/list" && method === "GET") {
      const full = fsPath(sb, query.get("path"));
      if (!existsSync(full)) return send(res, 404, { code: "FILE_NOT_FOUND", message: "directory not found" });
      if (!statSync(full).isDirectory()) return send(res, 400, { code: "INVALID_REQUEST", message: "not a directory" });
      return send(res, 200, walk(full, 1, Number(query.get("depth") ?? 1)).map((f) => fileInfo(sb, f.slice(sb.root.length))));
    }
    if (path === "/directories" && method === "POST") {
      for (const p of Object.keys(JSON.parse((await readBody(req)).toString()))) mkdirSync(fsPath(sb, p), { recursive: true });
      return send(res, 200);
    }
    if (path === "/directories" && method === "DELETE") {
      const full = fsPath(sb, query.get("path"));
      if (!existsSync(full)) return send(res, 404, { code: "FILE_NOT_FOUND", message: "directory not found" });
      rmSync(full, { recursive: true, force: true });
      return send(res, 200);
    }
    return send(res, 404, { code: "NOT_FOUND", message: `no route ${method} ${path}` });
  }

  // ------------------------------------------------------------------ lifecycle
  async function lifecycle(req, res, path, query) {
    if (req.headers["open-sandbox-api-key"] === undefined) return send(res, 401, { code: "MISSING_API_KEY", message: "Missing API key" });
    if (req.headers["open-sandbox-api-key"] !== apiKey) return send(res, 401, { code: "INVALID_API_KEY", message: "Invalid API key" });
    const method = req.method;
    if (path === "/sandboxes" && method === "POST") {
      const body = JSON.parse((await readBody(req)).toString());
      const errors = [];
      if (!body.entrypoint) errors.push({ type: "value_error", loc: ["body", "entrypoint"], msg: "Entrypoint is required when image is provided." });
      if (!body.resourceLimits) errors.push({ type: "missing", loc: ["body", "resourceLimits"], msg: "Field required" });
      if (body.timeout !== undefined && body.timeout < 60) errors.push({ type: "greater_than_equal", loc: ["body", "timeout"], msg: "Input should be greater than or equal to 60" });
      if (errors.length) return send(res, 422, { detail: errors });
      for (const [k, v] of Object.entries(body.metadata ?? {})) {
        if (!LABEL.test(k) || (v !== "" && !LABEL.test(v)) || v.length > 63) return send(res, 400, { code: "SANDBOX::INVALID_METADATA_LABEL", message: `Invalid metadata ${k}` });
      }
      if (body.image.uri.includes("missing-image")) return send(res, 500, { code: "DOCKER::SANDBOX_IMAGE_PULL_FAILED", message: "pull access denied" });
      if (body.secureAccess) return send(res, 400, { code: "SANDBOX::SECURE_ACCESS_UNSUPPORTED", message: "secureAccess needs the gateway" });
      const id = randomUUID();
      const root = mkdtempSync(join(tmpdir(), "mock-osb-"));
      const sb = {
        id, root, image: body.image.uri, state: "Running", metadata: body.metadata ?? {}, env: body.env ?? {}, entrypoint: body.entrypoint,
        createdAt: new Date().toISOString(), expiresAt: body.timeout ? new Date(Date.now() + body.timeout * 1000).toISOString() : undefined, request: body,
      };
      sandboxes.set(id, sb);
      return send(res, 202, { id, status: { state: "Running" }, metadata: sb.metadata, expiresAt: sb.expiresAt, createdAt: sb.createdAt, entrypoint: sb.entrypoint });
    }
    if (path === "/sandboxes" && method === "GET") {
      const page = Number(query.get("page") ?? 1);
      const size = Number(query.get("pageSize") ?? 20);
      const states = query.getAll("state");
      const meta = Object.fromEntries(new URLSearchParams(query.get("metadata") ?? ""));
      const all = [...sandboxes.values()].filter((sb) => (!states.length || states.includes(sb.state)) && Object.entries(meta).every(([k, v]) => sb.metadata[k] === v));
      const items = all.slice((page - 1) * size, page * size).map(view);
      return send(res, 200, { items, pagination: { page, pageSize: size, totalItems: all.length, totalPages: Math.ceil(all.length / size), hasNextPage: page * size < all.length } });
    }
    const m = /^\/sandboxes\/([^/]+)(?:\/(.+))?$/.exec(path);
    if (!m) return send(res, 404, { code: "NOT_FOUND", message: "no route" });
    const sb = sandboxes.get(m[1]);
    if (!sb) return send(res, 404, { code: "DOCKER::SANDBOX_NOT_FOUND", message: `Sandbox ${m[1]} not found` });
    const sub = m[2];
    if (!sub && method === "GET") return send(res, 200, view(sb));
    if (!sub && method === "DELETE") {
      sandboxes.delete(sb.id);
      rmSync(sb.root, { recursive: true, force: true });
      return send(res, 204);
    }
    if (sub === "renew-expiration" && method === "POST") {
      sb.expiresAt = JSON.parse((await readBody(req)).toString()).expiresAt;
      return send(res, 200, { expiresAt: sb.expiresAt });
    }
    if (sub === "pause" || sub === "resume") {
      if ((sub === "pause") === (sb.state === "Paused")) return send(res, 409, { code: "DOCKER::INVALID_STATE", message: `Sandbox is ${sb.state}` });
      sb.state = sub === "pause" ? "Paused" : "Running";
      return send(res, 202);
    }
    const ep = /^endpoints\/(\d+)$/.exec(sub ?? "");
    if (ep) {
      const proxied = query.get("use_server_proxy") === "true";
      const base = proxied ? `${origin.replace("http://", "")}/v1/sandboxes/${sb.id}/proxy/${ep[1]}` : ep[1] === "44772" ? `${origin.replace("http://", "")}/execd/${sb.id}` : `${origin.replace("http://", "")}/execd/${sb.id}/proxy/${ep[1]}`;
      return send(res, 200, { endpoint: base, ...(gateway ? { headers: { "X-GW-Route": "1" } } : {}) });
    }
    return send(res, 404, { code: "NOT_FOUND", message: "no route" });
  }

  const server = createServer(async (req, res) => {
    const url = new URL(req.url, "http://x");
    onRequest?.(req);
    try {
      const m = /^\/execd\/([^/]+)(\/.*)?$/.exec(url.pathname);
      if (m) {
        const sb = sandboxes.get(m[1]);
        if (!sb) return send(res, 502, "bad gateway: sandbox gone");
        if (sb.state === "Paused") return req.socket.destroy();
        return await execd(req, res, sb, m[2] ?? "/", url.searchParams);
      }
      const proxy = /^\/v1\/sandboxes\/([^/]+)\/proxy\/(\d+)(\/.*)?$/.exec(url.pathname);
      if (proxy) {
        const sb = sandboxes.get(proxy[1]);
        if (!sb) return send(res, 404, { code: "DOCKER::SANDBOX_NOT_FOUND", message: "gone" });
        return proxy[2] === "44772" ? await execd(req, res, sb, proxy[3] ?? "/", url.searchParams) : send(res, 200, "proxied");
      }
      const path = url.pathname.replace(/^\/v1(?=\/)/, "");
      return await lifecycle(req, res, path, url.searchParams);
    } catch (err) {
      return send(res, 500, { code: "MOCK_ERROR", message: String(err?.stack ?? err) });
    }
  });
  await new Promise((r) => server.listen(0, "127.0.0.1", r));
  origin = `http://127.0.0.1:${server.address().port}`;
  return {
    origin,
    apiKey,
    sandboxes,
    async close() {
      for (const sb of sandboxes.values()) rmSync(sb.root, { recursive: true, force: true });
      server.closeAllConnections?.();
      await new Promise((r) => server.close(r));
    },
  };
}