跳到正文

内存实现

本地沙箱在真实文件上运行真实命令。这很好,直到你想让Agent探索代码而不信任它不会破坏任何东西。

just-bash就是答案。这是一个语义写时复制虚拟文件系统:Agent从真实磁盘读取,但写入的内容存于内存中,沙箱停止时会消失。快速、便宜、安全。非常适合探索、测试,或者任何你不想让任何Agent对你实际文件泄露时使用。

学习成果

createJustBashSandbox(dir)退回了一份由just-bash支持的Sandbox。Harness可以通过环境变量在本地和内存后端之间切换,Agent对任一运行相同的提示词。

快速路径

  1. bun add just-bash安装just-bash
  2. src/sandbox-just-bash.ts中实现createJustBashSandbox(dir)
  3. readFileexecjust-bash API,注意虚拟骑乘点
  4. 在创业时根据process.env.SANDBOX选择后端

动手练习 4.3

just-bash后端,接好环境变频开关。

要求:

  1. createJustBashSandbox(dir: string): Promise<Sandbox>(注意Promise,因为创建是异步的)
  2. JustBashSandbox.create({ overlayRoot: dir })启动虚拟FS
  3. readFileexec中,通过虚拟挂载点进行路径转换,/home/user/project
  4. index.ts中,根据process.env.SANDBOX选择localjust-bash

实现提示:

  • JustBashSandbox.create回应了一个承诺。你的工厂也必须是异步的
  • 挂钩点就是陷阱。overlayRoot: "/Users/you/project"不是安装在/,而是安装在/home/user/project。沙箱内的每条路径都必须加上该常数
  • runCommand返回的是命令句柄,而不是结果。退出码调用 wait()output()综合 stdout/stderr

just-bash API

简单介绍一下你要包的部分:

ts
import { Sandbox as JustBashSandbox } from "just-bash";

const jb = await JustBashSandbox.create({ overlayRoot: "/path/to/project" });

const content = await jb.readFile("/home/user/project/package.json");

const cmd = await jb.runCommand("ls", { cwd: "/home/user/project" });
const finished = await cmd.wait();
console.log(await cmd.output());
console.log(finished.exitCode);

**Warning:安装点trap**

当你通过overlayRoot: "/path/to/project"时,just-bash会将该目录挂载在虚拟文件系统内的/home/user/project/没有。不是在原来的道路上。每个 readFilerunCommand 调用都必须使用虚拟挂载点。这会让你陷入困境。这会让大家都困惑。

实现

ts
import { Sandbox as JustBashSandbox } from "just-bash";
import type { Sandbox } from "./sandbox";

const MOUNT = "/home/user/project";

export async function createJustBashSandbox(dir: string): Promise<Sandbox> {
  const jb = await JustBashSandbox.create({ overlayRoot: dir });

  return {
    type: "just-bash",
    workingDirectory: dir,
    readFile: async (p) => {
      const virtualPath = `${MOUNT}/${p}`;
      return jb.readFile(virtualPath);
    },
    exec: async (command) => {
      const cmd = await jb.runCommand(command, { cwd: MOUNT });
      const finished = await cmd.wait();
      return {
        stdout: await cmd.output(),
        exitCode: finished.exitCode,
      };
    },
    stop: async () => {},
  };
}

MOUNT常数是just-bash后端唯一关心、本地后端不关心的。每一条进出的路都通过它被翻译。

接线环境变换开关

ts
import { createLocalSandbox } from "./src/sandbox-local";
import { createJustBashSandbox } from "./src/sandbox-just-bash";

const sandboxType = process.env.SANDBOX || "local";
const sandbox =
  sandboxType === "just-bash"
    ? await createJustBashSandbox(cwd)
    : createLocalSandbox(cwd);

console.error(`Sandbox: ${sandbox.type}`);

工厂的 local 是同步的,just-bash 的工厂是异步的。条件句柄了这一点。所有下游(工具、Agent、提示词构建器)都是一样的。

写时复制,一句话

读取来自真实磁盘。写入会进入内存。真实的文件系统从未被修改。当沙箱停止时,虚拟文件系统会被回收。Agent可以读取你的package.json,然后创建和删除test.txt一百次,而你的磁盘项目依然不受影响。

动手试试

同样提示词,两个后端:

bash
bun run index.ts . "Read the package.json"
bash
SANDBOX=just-bash bun run index.ts . "Read the package.json"

你应该会得到相同的答案,分别在各自的运行中印有Sandbox: localSandbox: just-bash。这就是界面在工作。

试试在内存后端做写形任务:

bash
SANDBOX=just-bash bun run index.ts . "Create a file called scratch.txt with the text 'hello'"

Agent会写入文件。现在检查真正的磁盘:scratch.txt不存在。写入发生在覆盖层,内存中。

bash
npx tsc --noEmit

**注意:不是所有工具第一次尝试就能携带**

有些工具在两个后端之间工作方式完全相同。有些人会默默失败,因为他们对宿主有某种假设。grep是常见的“问题”,因为just-bash下的壳体行为是模拟的,且不总是与系统grep字节完全相同。可移植性测试是真实存在的,不是理论上的。计划换完后修理一两件工具。

提交

bash
git add src/sandbox-just-bash.ts index.ts package.json
git commit -m "feat(sandbox): add just-bash backend with in-memory FS"

完成标准

  • [ ] just-bash已安装
  • [ ] src/sandbox-just-bash.ts导出的createJustBashSandbox(dir)会返回Promise<Sandbox>
  • [ ] 路径通过MOUNT常数
  • [ ] SANDBOX=just-bash bun run ... 在内存后端运行Agent
  • [ ] just-bash上的写入任务不涉及真实文件系统
  • [ ] npx tsc --noEmit

**注意:找到泄漏的工具**

选择一个对local后端有效但在just-bash下失败或表现不同的提示词。追踪哪个工具在做特定主机的假设。然后决定:你是修工具,还是让界面吸收差异(比如让just-bash为该命令提供垫片)?这两种方式都是设计上的选择。注意哪种方法让工具更简单。

参考实现

ts
import { Sandbox as JustBashSandbox } from "just-bash";
import type { Sandbox } from "./sandbox";

const MOUNT = "/home/user/project";

export async function createJustBashSandbox(dir: string): Promise<Sandbox> {
  const jb = await JustBashSandbox.create({ overlayRoot: dir });

  return {
    type: "just-bash",
    workingDirectory: dir,
    readFile: async (p) => {
      const virtualPath = `${MOUNT}/${p}`;
      return jb.readFile(virtualPath);
    },
    exec: async (command) => {
      const cmd = await jb.runCommand(command, { cwd: MOUNT });
      const finished = await cmd.wait();
      return {
        stdout: await cmd.output(),
        exitCode: finished.exitCode,
      };
    },
    stop: async () => {},
  };
}
ts
const sandboxType = process.env.SANDBOX || "local";
const sandbox =
  sandboxType === "just-bash"
    ? await createJustBashSandbox(cwd)
    : createLocalSandbox(cwd);

非官方简体中文翻译 · 原课程来自 Vercel Academy