文档 运行 沙箱

沙箱

每次聊天和应用阶段都运行在 沙箱 中:一台拥有独立文件系统、进程和网络的临时计算机,用完即扔,绝不触及你的机器或其他用户。

一句话讲清它为什么重要。 正是有了沙箱,我们才敢放心地说"是的,智能体可以随意 pip install、执行 shell 命令、写文件、试错",因为爆炸半径只有一个容器,运行结束就会销毁。

三种 Provider

Aitroop 把运行时抽象在统一的 ISandbox 接口背后,并提供三种实现。具体某次运行使用哪一种, 取决于工作区设置、套餐以及任务类型。智能体的代码感知不到差异,但运行特性差别很大。

Provider运行位置适用场景限制
host 本地直接运行在 Aitroop 服务器上,使用每个用户的临时目录。自托管的开发安装、冒烟测试,以及那种"启动一台远程虚拟机太重"的轻量阶段。用户之间没有真正的隔离,host 模式仅适用于单租户 / 开发环境。
e2b 默认云端E2B 按需配置的全新 Firecracker microVM。绝大多数聊天和应用运行。启动时间 <1 秒,最长存活 7 天,完整支持 claude-code 模板。每个沙箱的 RAM/CPU 由你的套餐决定。空闲 5 分钟后会被暂停。
daytona 重型托管在 Daytona 上的开发容器。长时间运行的任务、大型代码库,以及阶段之间需要保留缓存的运行。冷启动比 E2B 慢。更适合那种能把启动成本摊薄的阶段。

智能体在沙箱里能做什么

沙箱接口暴露两个面,命令文件:以及几个生命周期钩子:

命令

sandbox.commands.run("python script.py")
sandbox.commands.run({ cmd: "node", args: ["build.js"] })
// returns: { exitCode, stdout, stderr }
  • Shell 命令以沙箱用户身份(非 root)在 projectDir 下运行。
  • 你可以安装包,在 E2B 与 Daytona 上可以用 pip installnpm installapt-get;在 host 模式下则受限于你自己的权限。
  • 标准输出与标准错误会流式回传到运行日志;命令完成时智能体即可看到。
  • 单条命令的超时由阶段的 timeout_ms 限定。

文件

sandbox.files.write(path, contents)
sandbox.files.read(path) // utf-8 by default
sandbox.files.list(dir)
sandbox.files.stat(path)
sandbox.files.remove(path)
sandbox.files.rename(from, to)
sandbox.files.mkdir(path)
sandbox.files.copy(from, to)

文件位于 projectDir 下(E2B 上通常是 /home/user/project)。该目录在同一次运行的 多条命令之间会被保留,但在沙箱销毁时一并清空。

生命周期钩子

  • sandbox.pause():主动释放沙箱。下一次调用会启动一个全新的沙箱。
  • sandbox.keepAlive(timeoutMs):为长时间运行的阶段延长空闲超时。

你应该了解的几个超时

每次运行都由三个计时器掌控:

常量默认值控制的内容
SANDBOX_IDLE_TIMEOUT_MS5 分钟未使用的沙箱在被暂停前会保留多久。
AGENT_RUN_TIMEOUT_MS7 天单次运行的硬上限,不受阶段超时影响。
stage.timeout_ms3 分钟单阶段的墙钟预算。可在应用定义中覆盖。
你遇到的失败大多是阶段超时,而不是沙箱超时。 如果一个研究阶段超过 3 分钟,阶段就会失败,哪怕沙箱本身可以愉快地跑上几个小时。对于重度研究类任务,在应用定义中设置 timeout_ms: 600000(10 分钟)。

什么会保留,什么不会

单次运行内

单次应用执行的所有阶段共享同一个沙箱。所以:

  • 阶段 1 写入的文件,阶段 2 可以读取。
  • 阶段 1 通过 pip install 装的包,阶段 2 可以使用。
  • 阶段 1 设置的环境变量会延续到阶段 2。

这就是为什么 "阶段 1 写报告,阶段 2 转成 PDF" 这种用法天然可行,阶段 2 在阶段 1 放下报告的同一位置 就能找到它。

跨运行

运行之间什么都不会保留。 即便是同一个应用,每次执行都会得到一个全新的沙箱。 如果你的应用需要在两次运行之间记住一些东西,已处理项的列表、计数器、缓存,请存到外部:

  • 作为 产物,下次运行时再通过 Connect(Drive、S3、Notion)读回来。
  • 通过 webhook 写入你自己的状态存储。
  • 通过定时任务把一个应用的输出串联成另一个应用的输入。

跨聊天

同样的规则:每个聊天都有自己的沙箱。两个并排打开的聊天,文件系统完全独立。 一个聊天里的智能体看不到另一个聊天里的智能体在做什么。

网络与 Connects

沙箱拥有完整的对外网络访问权限。智能体可以抓取 URL、调用公开 API、从 npm / pypi 安装包等等。

对于 Connects(OAuth 集成),Aitroop 会在运行开始时注入凭证,但不是把 token 写到沙箱 文件系统里,而是通过内部代理调用,从不暴露原始 token。如果智能体执行 gh repo list 或调用 Gmail API,请求会经过一个代理,由代理用你的 OAuth token 签名。token 在沙箱内部完全不可见。

对安全意味着什么。 即使某个阶段的 script_code 是恶意的, 它也无法外泄你的 Connect token,这些 token 既不在文件系统里,也不在环境变量里。最坏的情形是脚本利用 Connect 做一些对外可见的动作(发送邮件、创建记录),这些动作会像任何其他操作一样出现在你的服务商审计日志里。

选择 Provider

如果你使用托管套餐

你不需要选择,由平台决定。99% 的场景平台会选 e2b。对于特定类型的阶段,比如需要跑完整 IDE 负载的阶段,可能会选 daytona

如果你是自托管

config.yaml 中或通过环境变量设置 provider:

# Use the local host machine (single-tenant only)
SANDBOX_PROVIDER=host

# Use E2B (recommended; needs E2B_API_KEY)
SANDBOX_PROVIDER=e2b
E2B_API_KEY=e2b_...
E2B_TEMPLATE=claude-code

# Use Daytona (needs DAYTONA_API_KEY)
SANDBOX_PROVIDER=daytona
DAYTONA_API_KEY=...
DAYTONA_TARGET=...

混合 provider 路由,"轻量用 host,重型用 E2B",在路线图上,但还不是一等的配置项。目前每个部署 只能选用一个 provider。

UI 里你能看到什么

沙箱基本是隐形的,这正是设计目的,但有两个界面让你可以一窥究竟:

运行日志

应用运行期间,右侧面板会显示智能体的每一次工具调用。每一条 shell 命令和文件操作都会连同参数、退出码 以及(截断后的)输出一起被记录下来:

$ bash -c "pip install pandas"
→ exit 0 · 8.2s
  Successfully installed pandas-2.3.1 numpy-2.1.0

$ python analyze.py
→ exit 0 · 4.1s
  Wrote 1,247 rows to /home/user/project/output.csv

文件树(适用于代码型运行)

当某个阶段产出 code 类型的产物时,预览面板会展示运行结束时沙箱内的完整项目树。 你可以点开任意文件查看,然后下载或分享。

生命周期:沙箱是如何获取与持有的

Aitroop 不会为每个请求都创建一个沙箱。每个用户最多只有一个热的沙箱,记录在 sb_container 表的一行里,以其内部用户名为键。连续的聊天与应用运行复用同一个沙箱,这正是把冷启动成本挪出热路径 的关键。生命周期状态全部存放在那一行:

sb_container含义
username主键。用户稳定的内部标识。
sandbox_idProvider 侧的标识(E2B sandbox ID、Daytona workspace ID、host PID 前缀)。创建中时为空。
status可用时为 ready;当 worker 正在配置新的沙箱时为 creating
pausedProvider 因空闲自动暂停沙箱后为 true;恢复并热起来后为 false
type归属的 provider(e2b / daytona / local)。
lock_version乐观锁计数器。每次写入都会自增;版本过期的并发写入会被拒绝。
total_usage_seconds沙箱累计活跃的墙钟时间,计费指标。
last_activity_atSDK 最近一次成功触达的时间。驱动 5 分钟空闲暂停计时器。

带乐观锁的并发获取

沙箱可能被同时触达的两种原因:聊天回复和定时应用同一时刻启动;或者两台服务器实例同时处理同一用户的流量。 平台通过对 lock_version 的 CAS 来串行化:

  1. 调用方读取当前行,包括 lock_version
  2. 用刚刚读到的版本作为 UPDATE 的条件来抢锁,并原子地把版本号 +1。
  3. 成功 → 该调用方拥有沙箱;其他人在 ~100 ms 后重读重试。
  4. 失败 → 别人先一步抢到。重新读取再试一次。

如果一个 worker 在获取中途崩溃,会让 status = 'creating' 一直挂着。为了避免被无限卡死, 任何在 creating 状态停留超过 ~25 秒的行都被视为过期:下一个获取者会重置它、推高版本号, 然后继续。这就是过期锁的恢复路径,正常时悄无声息,触发时会在日志里以一行警告出现。

暂停 / 恢复

每一次对底层 provider 的 SDK 调用都会重置 5 分钟空闲计时器。如果 5 分钟内没有任何触达,provider 会冻结 沙箱:行记录保留,paused 翻为 true,容器不再计费。下一次获取会把它唤醒,E2B 上的冷恢复大约 1 秒。为了在智能体长时间思考(LLM 轮次之间没有 SDK 调用)时保持沙箱温热, runner 每 30 秒发送一次 keepAlive ping。这就是为什么集群常量是这样对齐的:

SANDBOX_KEEPALIVE_INTERVAL_MS = 30 s
SANDBOX_IDLE_TIMEOUT_MS      = 5 min (12× the keepalive)
AGENT_RUN_TIMEOUT_MS         = 7 days (hard ceiling on a single turn)

已死亡的沙箱与 withSandbox

沙箱可能在平台视线之外死亡,provider 把底层虚拟机删了,host 进程被 OOM 杀死。runner 通过把对应错误 归类为 sandbox_not_found 来识别这种情况(参见 runner 错误分类)。大多数服务端代码都使用一个包装辅助函数:

provider.withSandbox(userId, userEmail, async (sb) => {
  await sb.commands.run('pip install pandas');
  await sb.files.write('script.py', code);
});

如果内部函数抛错且错误匹配"沙箱已不存在",wrapper 会清掉内存缓存,调用 createOrResume 获取一个新的沙箱,并把函数再跑一次。仅重试一次,就这么多;第二次失败会向外抛出。其他临时性错误 (网络、繁忙)会立刻向外传播;它们不属于 wrapper 的职责范围。

Host 模式的密钥剥离

Host 模式是仅供开发者使用的 provider,所谓的"沙箱"就是 Aitroop 服务器上的一个进程。如果不做防护, 智能体运行的任何代码都会继承服务器的环境,包括数据库连接串、JWT 签名密钥和 S3 凭证。因此 host provider 会在 spawn 之前从子进程环境里剥除一组固定的变量:DATABASE_URLJWT_SECRETE2B_API_KEYDAYTONA_API_KEY、各种 OAuth client secret、S3 access/secret key, 以及任何 Composio 侧的密钥。智能体拿到的是一个干净的环境;服务器自己的密钥得以保全。

沙箱用量与费用

每一秒沙箱时间都会被计量。计费表(buy_balance)以美元记录累计充值与累计消费; 每次运行的明细出现在 设置 → 用量

  • 沙箱时间:只在沙箱实际运行时计入。被暂停(空闲 > 5 分钟)的沙箱不产生费用。
  • LLM token:按阶段单独计费,因模型而异。默认是 Sonnet-4.6。
  • 存储:S3/R2 中的产物按保留的 GB·月计费。在你套餐额度内免费。

sb_container 表通过 total_usage_seconds 跟踪每个沙箱实例, sb_usage_log 则按原因记录每段时长,智能体运行、文件操作等等。

FAQ 与故障排查

我的阶段以 "command timed out" 失败了。怎么回事?

可能的原因:

  • 阶段的 timeout_ms 对于实际工作量太小了(默认 3 分钟)。调大它。
  • 命令本身卡住了,一个等不到 stdin 的进程、对无响应主机的 curl,或者公司代理后的 npm install
  • 命令之间触发了沙箱空闲计时器。如果一个阶段在智能体两次决策之间停留 >5 分钟(罕见),沙箱会被暂停。 下一条命令会自动恢复但会增加延迟。

解决:打开失败的运行,点 Debug in chat,查阅运行日志, 找出哪条命令一直没有返回。多数情况下,把那个阶段的 timeout_ms 调大就行, 比如 "timeout_ms": 600000 给出 10 分钟预算。

为什么我的阶段 2 看不到阶段 1 写的文件?

可能原因:阶段 1 写到了 projectDir 之外的绝对路径(比如 /tmp/foo.csv),而阶段 2 在 projectDir 下查找。又或者这个应用被拆成了两次 执行(每次都会拿到自己的沙箱)。

解决:永远写到 projectDir(智能体的工作目录)下。运行开始时该路径会 作为 $AITROOP_PROJECT_DIR 暴露给智能体。

我能 SSH 到沙箱里面看看吗?

不能直接连。沙箱在设计上就是封闭的。如果你需要查看运行过程中的状况,对失败的运行使用 Debug in chat:聊天会带着同一份沙箱快照打开,你可以交互式地运行任何命令。

我怎么安装系统包(apt-get)?

e2bdaytona 上,智能体拥有 sudo 权限来安装包。 在阶段目标里告诉它:"先运行 apt-get install -y ffmpeg,然后……"。 在 host 模式下,智能体只拥有 host 进程的权限,通常没有 sudo,所以 apt-get 用不了。

为什么我的 E2B 沙箱今天比昨天慢?

冷启动会因地区与时段不同而波动。多数情况下 <1 秒。如果你看到了 10 秒的冷启动,那是智能体在等 provider 配置一台新虚拟机。这是正常现象,会自行恢复。如果系统性地发生,请查看 设置 → Workspace 里的状态横幅。

两个阶段能并行运行吗?

不能。阶段严格按顺序运行:阶段 n 必须等到阶段 n−1 完成、其产物已保存后才会启动。 如果你需要在阶段内部并行,智能体可以通过 commands.run 派生子进程,但那是它的选择,不是你能控制的。

沙箱销毁后我的文件会怎样?

它们没了。任何你想保留的东西都必须在运行结束之前保存为产物(存到 S3/R2 的 app_artifact.s3_key)。产物在沙箱销毁后仍然保留。

沙箱是真的虚拟机还是容器?

取决于 provider。E2B 用 Firecracker microVM,完整的内核隔离。Daytona 用开发容器。 Host 模式只是服务器上属于你账户的一个进程。从功能上看智能体的代码在三种实现里都一样; 从安全性上看 E2B 最强。