从零编写 Sandcastle 沙箱提供商:Bind-Mount 与 Isolated 双模式完整指南
【免费下载链接】sandcastleOrchestrate sandboxed coding agents in TypeScript with sandcastle.run()项目地址: https://gitcode.com/gh_mirrors/sandcastl/sandcastle
🏖️Sandcastle是一个用 TypeScript 编排 AI 编码智能体的开源库:你只调用一次sandcastle.run(),它就把智能体关进隔离沙箱干活、再把分支上的提交合并回来。内置了 Docker、Podman、Vercel 三种沙箱提供商,但真正的亮点是——你可以编写自定义沙箱提供商。本文将带你从零开始,用两种方式接入任意隔离环境:Bind-Mount(绑定挂载)与Isolated(完全隔离),从接口契约到跑通第一次运行,全程只讲核心步骤。
为什么需要自定义沙箱提供商?
当你想在自己的环境里跑智能体时,会遇到这些场景:
- 🖥️ 公司内网有自研的容器运行时或轻量虚拟机
- ☁️ 想接 E2B、Daytona 这类云端沙箱服务
- 🧪 写测试时不想依赖 Docker,想用一个纯本地临时目录"模拟"沙箱
Sandcastle 的架构把"怎么执行命令"抽象成了SandboxProvider接口(定义在 src/SandboxProvider.ts),你只要实现一个小巧的 Promise 接口,Sandcastle 会替你处理 worktree 创建、git 挂载解析、提交提取这些脏活。
先选模式:Bind-Mount 与 Isolated 的 3 秒判断法
两种模式的本质区别只有一句话:沙箱能不能直接访问宿主机的文件系统?
| 对比项 | 📌 Bind-Mount(绑定挂载) | 🚀 Isolated(完全隔离) |
|---|---|---|
| 适用场景 | Docker、Podman 等本地容器运行时 | 云端 VM、微虚拟机等独立文件系统环境 |
| 代码同步 | 宿主机建好 worktree 后直接挂载进沙箱,零同步 | 由你实现copyIn/copyFileOut手动搬文件 |
| 分支策略 | 支持head、merge-to-head、branch三种 | 仅支持merge-to-head、branch(无法写宿主机) |
| 默认分支策略 | head(智能体直写宿主机工作目录) | merge-to-head(临时分支 + 合并回 HEAD) |
| 工厂函数 | createBindMountSandboxProvider() | createIsolatedSandboxProvider() |
一句话决策:
本地容器 → Bind-Mount;远端 VM / 云端沙箱 → Isolated。
更多模式的选型背景可以参考仓库里的调研文档 research/sandbox-provider-research.md。
核心契约:沙箱 Handle 只需要 3~4 个方法
两种提供商的create()函数都返回一个沙箱句柄(handle),契约非常小:
| 方法 | Bind-Mount | Isolated | 作用 |
|---|---|---|---|
exec(command, opts) | ✅ 必须 | ✅ 必须 | 在沙箱中执行命令,必须支持逐行流式输出 |
close() | ✅ 必须 | ✅ 必须 | 销毁沙箱 |
worktreePath | ✅ 必须 | ✅ 必须 | 仓库目录在沙箱内的绝对路径 |
copyFileIn/copyFileOut | ✅ 必须 | — / ✅ 必须 | 单文件在宿主机与沙箱间拷贝 |
copyIn | — | ✅ 必须 | 拷贝文件或整个目录进沙箱 |
两个关键细节,官方在 src/SandboxProvider.ts 的注释里写得非常明确:
exec必须支持onLine逐行流式回调——这是 Sandcastle 给用户实时反馈、执行空闲超时的唯一通道。"等进程结束再一次性吐出全部输出"的实现不满足契约,空闲超时和实时日志都会失效。- 每次
exec返回统一的ExecResult:{ stdout, stderr, exitCode }。
第一步:5 分钟写出你的 Bind-Mount 提供商
以"把本地进程当沙箱"为例(适合快速理解契约,也适合写测试),完整代码见 src/sandboxes/test-bind-mount.ts:
import { createBindMountSandboxProvider } from "@ai-hero/sandcastle"; const localProcess = () => createBindMountSandboxProvider({ name: "local-process", create: async (options) => { const worktreePath = options.worktreePath; return { worktreePath, // 逐行流式执行命令(spawn + readline,每行回调一次 onLine) exec: (command, opts) => { /* spawn("sh", ["-c", command]) … */ }, copyFileIn: async (hostPath, sandboxPath) => { /* 拷入单文件 */ }, copyFileOut: async (sandboxPath, hostPath) => { /* 拷出单文件 */ }, close: async () => { /* 本地进程无需清理 */ }, }; }, });create收到的BindMountCreateOptions包含worktreePath(宿主机 worktree 路径)、hostRepoPath、mounts(宿主:沙箱路径对)和env——写容器提供商时,把这些映射成你的-v挂载参数即可。真实实现可参考 src/sandboxes/docker.ts(含 SELinux 标签支持)和 src/sandboxes/podman.ts。
第二步:5 分钟写出你的 Isolated 提供商
Isolated 提供商多了两件事:目录级拷贝和独立文件系统的清理。最小示例(临时目录模拟远端 VM)见 src/sandboxes/test-isolated.ts:
import { createIsolatedSandboxProvider } from "@ai-hero/sandcastle"; const tempDir = () => createIsolatedSandboxProvider({ name: "temp-dir", create: async () => { const root = await mkdtemp(join(tmpdir(), "sandbox-")); const worktreePath = join(root, "workspace"); await mkdir(worktreePath, { recursive: true }); return { worktreePath, exec: (command, opts) => { /* 同上的流式执行实现 */ }, // 目录递归拷入,文件单拷 copyIn: async (hostPath, sandboxPath) => { const isDir = (await stat(hostPath)).isDirectory(); isDir ? await cp(hostPath, sandboxPath, { recursive: true }) : await copyFile(hostPath, sandboxPath); }, copyFileOut: async (sandboxPath, hostPath) => { await mkdir(dirname(hostPath), { recursive: true }); await copyFile(sandboxPath, hostPath); }, close: async () => { await rm(root, { recursive: true, force: true }); }, }; }, });接真实云服务时,把copyIn换成 SDK 的上传接口即可,例如 src/sandboxes/vercel.ts(Vercel Firecracker 微虚拟机)和 src/sandboxes/daytona.ts(Daytona 云沙箱,含输出尾部长度限制,防止长日志撑爆内存)。
第三步:接入 run(),第一次运行就跑通
提供商写好后,通过sandbox选项传给run()——用法和内置docker()完全一样:
import { run, claudeCode } from "@ai-hero/sandcastle"; const result = await run({ agent: claudeCode("claude-opus-4-8"), sandbox: localProcess(), // 👈 你的自定义提供商 prompt: "Fix issue #42 in this repo.", }); console.log(result.commits); // [{ sha: "abc123" }]⚠️别忘了分支策略的差异(详见 README.md 的Custom Sandbox Providers章节):
- Bind-Mount 提供商默认为
head——智能体直接写宿主机工作目录,快但无分支隔离; - Isolated 提供商默认
merge-to-head——提交先在临时分支产生,结束后自动合并回 HEAD,出问题时 HEAD 毫发无损,是 CI 与无人值守场景的安全默认值; - 想指定分支(比如给 PR 用)时显式传
branchStrategy: { type: "branch", branch: "agent/fix-42" },两种模式都支持。
避坑清单与参考实现
📋 写提供商时最容易踩的四个坑:
exec不做流式输出→ 实时日志消失、空闲超时失效(最常见的坑);- Isolated 的
copyIn只拷文件不拷目录→ worktree 整体搬不进去,智能体开工即报错; close()没有清理资源→ 云端沙箱泄漏,账单刺客;- Isolated 提供商配了
head分支策略→ 类型层面直接报错,因为它无法写宿主机。
📂 官方参考实现索引:
| 文件 | 模式 | 说明 |
|---|---|---|
| src/sandboxes/docker.ts | Bind-Mount | Docker 容器,含 SELinux 标签 |
| src/sandboxes/podman.ts | Bind-Mount | Podman 容器,无守护进程 |
| src/sandboxes/vercel.ts | Isolated | Vercel Firecracker 微虚拟机 |
| src/sandboxes/daytona.ts | Isolated | Daytona 云沙箱(SDK 动态导入) |
| src/sandboxes/test-bind-mount.ts | Bind-Mount | 临时目录版,适合写单测 |
| src/sandboxes/test-isolated.ts | Isolated | 临时目录版,适合写单测 |
接口类型(BindMountSandboxHandle、IsolatedSandboxHandle、ExecResult等)全部集中在 src/SandboxProvider.ts,完整使用文档见 README.md。
总结:三种提供商,一张表带走
| 提供商类型 | 工厂函数 | 文件同步 | 分支策略 | 典型代表 |
|---|---|---|---|---|
| Bind-Mount | createBindMountSandboxProvider | 挂载共享,零同步 | 全部三种 | Docker / Podman / 你的本地运行时 |
| Isolated | createIsolatedSandboxProvider | copyIn+copyFileOut | merge-to-head/branch | Vercel / Daytona / 你的云 VM |
| No-Sandbox | noSandbox() | 无(直接跑在宿主机) | 全部三种 | 本地交互式会话 |
掌握契约后你会发现:写一个 Sandcastle 沙箱提供商,核心代码其实只有exec、文件拷贝、close三块——剩下的一切,sandcastle.run()都替你编排好了。🎉
【免费下载链接】sandcastleOrchestrate sandboxed coding agents in TypeScript with sandcastle.run()项目地址: https://gitcode.com/gh_mirrors/sandcastl/sandcastle
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考