内存实现
本地沙箱在真实文件上运行真实命令。这很好,直到你想让Agent探索代码而不信任它不会破坏任何东西。
just-bash就是答案。这是一个语义写时复制虚拟文件系统:Agent从真实磁盘读取,但写入的内容存于内存中,沙箱停止时会消失。快速、便宜、安全。非常适合探索、测试,或者任何你不想让任何Agent对你实际文件泄露时使用。
学习成果
createJustBashSandbox(dir)退回了一份由just-bash支持的Sandbox。Harness可以通过环境变量在本地和内存后端之间切换,Agent对任一运行相同的提示词。
快速路径
- 用
bun add just-bash安装just-bash - 在
src/sandbox-just-bash.ts中实现createJustBashSandbox(dir) readFile并exec到just-bashAPI,注意虚拟骑乘点- 在创业时根据
process.env.SANDBOX选择后端
动手练习 4.3
加just-bash后端,接好环境变频开关。
要求:
createJustBashSandbox(dir: string): Promise<Sandbox>(注意Promise,因为创建是异步的)- 用
JustBashSandbox.create({ overlayRoot: dir })启动虚拟FS - 在
readFile和exec中,通过虚拟挂载点进行路径转换,/home/user/project - 在
index.ts中,根据process.env.SANDBOX选择local或just-bash
实现提示:
JustBashSandbox.create回应了一个承诺。你的工厂也必须是异步的- 挂钩点就是陷阱。
overlayRoot: "/Users/you/project"不是安装在/,而是安装在/home/user/project。沙箱内的每条路径都必须加上该常数 runCommand返回的是命令句柄,而不是结果。退出码调用wait(),output()综合 stdout/stderr
just-bash API
简单介绍一下你要包的部分:
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。/没有。不是在原来的道路上。每个 readFile 和 runCommand 调用都必须使用虚拟挂载点。这会让你陷入困境。这会让大家都困惑。
实现
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后端唯一关心、本地后端不关心的。每一条进出的路都通过它被翻译。
接线环境变换开关
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一百次,而你的磁盘项目依然不受影响。
动手试试
同样提示词,两个后端:
bun run index.ts . "Read the package.json"SANDBOX=just-bash bun run index.ts . "Read the package.json"你应该会得到相同的答案,分别在各自的运行中印有Sandbox: local和Sandbox: just-bash。这就是界面在工作。
试试在内存后端做写形任务:
SANDBOX=just-bash bun run index.ts . "Create a file called scratch.txt with the text 'hello'"Agent会写入文件。现在检查真正的磁盘:scratch.txt不存在。写入发生在覆盖层,内存中。
npx tsc --noEmit**注意:不是所有工具第一次尝试就能携带**
有些工具在两个后端之间工作方式完全相同。有些人会默默失败,因为他们对宿主有某种假设。grep是常见的“问题”,因为just-bash下的壳体行为是模拟的,且不总是与系统grep字节完全相同。可移植性测试是真实存在的,不是理论上的。计划换完后修理一两件工具。
提交
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为该命令提供垫片)?这两种方式都是设计上的选择。注意哪种方法让工具更简单。
参考实现
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 () => {},
};
}const sandboxType = process.env.SANDBOX || "local";
const sandbox =
sandboxType === "just-bash"
? await createJustBashSandbox(cwd)
: createLocalSandbox(cwd);