从零编写 Sandcastle 沙箱提供商:Bind-Mount 与 Isolated 双模式完整指南
2026/9/2 13:45:47 网站建设 项目流程

从零编写 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手动搬文件
分支策略支持headmerge-to-headbranch三种仅支持merge-to-headbranch(无法写宿主机)
默认分支策略head(智能体直写宿主机工作目录)merge-to-head(临时分支 + 合并回 HEAD)
工厂函数createBindMountSandboxProvider()createIsolatedSandboxProvider()

一句话决策:

本地容器 → Bind-Mount;远端 VM / 云端沙箱 → Isolated。

更多模式的选型背景可以参考仓库里的调研文档 research/sandbox-provider-research.md。

核心契约:沙箱 Handle 只需要 3~4 个方法

两种提供商的create()函数都返回一个沙箱句柄(handle),契约非常小:

方法Bind-MountIsolated作用
exec(command, opts)✅ 必须✅ 必须在沙箱中执行命令,必须支持逐行流式输出
close()✅ 必须✅ 必须销毁沙箱
worktreePath✅ 必须✅ 必须仓库目录在沙箱内的绝对路径
copyFileIn/copyFileOut✅ 必须— / ✅ 必须单文件在宿主机与沙箱间拷贝
copyIn✅ 必须拷贝文件或整个目录进沙箱

两个关键细节,官方在 src/SandboxProvider.ts 的注释里写得非常明确:

  1. exec必须支持onLine逐行流式回调——这是 Sandcastle 给用户实时反馈、执行空闲超时的唯一通道。"等进程结束再一次性吐出全部输出"的实现不满足契约,空闲超时和实时日志都会失效。
  2. 每次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 路径)、hostRepoPathmounts(宿主:沙箱路径对)和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" },两种模式都支持。

避坑清单与参考实现

📋 写提供商时最容易踩的四个坑:

  1. exec不做流式输出→ 实时日志消失、空闲超时失效(最常见的坑);
  2. Isolated 的copyIn只拷文件不拷目录→ worktree 整体搬不进去,智能体开工即报错;
  3. close()没有清理资源→ 云端沙箱泄漏,账单刺客;
  4. Isolated 提供商配了head分支策略→ 类型层面直接报错,因为它无法写宿主机。

📂 官方参考实现索引:

文件模式说明
src/sandboxes/docker.tsBind-MountDocker 容器,含 SELinux 标签
src/sandboxes/podman.tsBind-MountPodman 容器,无守护进程
src/sandboxes/vercel.tsIsolatedVercel Firecracker 微虚拟机
src/sandboxes/daytona.tsIsolatedDaytona 云沙箱(SDK 动态导入)
src/sandboxes/test-bind-mount.tsBind-Mount临时目录版,适合写单测
src/sandboxes/test-isolated.tsIsolated临时目录版,适合写单测

接口类型(BindMountSandboxHandleIsolatedSandboxHandleExecResult等)全部集中在 src/SandboxProvider.ts,完整使用文档见 README.md。

总结:三种提供商,一张表带走

提供商类型工厂函数文件同步分支策略典型代表
Bind-MountcreateBindMountSandboxProvider挂载共享,零同步全部三种Docker / Podman / 你的本地运行时
IsolatedcreateIsolatedSandboxProvidercopyIn+copyFileOutmerge-to-head/branchVercel / Daytona / 你的云 VM
No-SandboxnoSandbox()无(直接跑在宿主机)全部三种本地交互式会话

掌握契约后你会发现:写一个 Sandcastle 沙箱提供商,核心代码其实只有exec、文件拷贝、close三块——剩下的一切,sandcastle.run()都替你编排好了。🎉

【免费下载链接】sandcastleOrchestrate sandboxed coding agents in TypeScript with sandcastle.run()项目地址: https://gitcode.com/gh_mirrors/sandcastl/sandcastle

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询