让 agent 别把自己弄坏

让 agent 跑 shell 很危险,但只读沙箱又不够用。Fly 的做法是用 Sprite 隔离高风险操作:先打检查点,出错就回滚,让 agent 在不必小心的地方干活。

中文
复制
插画:一个机器人把手臂伸进另一个密封的箱子里干活,箱体与本体分隔开

图片:Annie Ruygt

做 agent 很好玩。重建那些把自己搞坏的 agent……就没那么好玩了。Fly 的很多人都在构建不那么容易自我毁灭的 agent,办法是让 agent 把所有有风险的操作都放进 Sprite 里执行。这样你得到的 agent 能活得足够久,久到真能用上它那些花哨的自我改进功能,你也可以放手让它去尝试那些否则堪称战列舰级自残的操作。下面是具体做法。

脑子归脑子,手归手

没有 shell,你的 agent 基本没用,因为 agent 要干的那些事都在这里发生。跑测试套件、执行迁移、装依赖、删临时文件。可惜,agent 的 shell 权限也正是毁掉你一下午的元凶——「删掉临时文件」和「删错文件」之间只差一个手抖的 glob,而且我们经常被提醒,AI 是会犯错的。

所以我们才有沙箱。但很多人一上来就把要做潜在危险操作的 agent 整个塞进沙箱。这会带来一长串你其实完全不必接受的取舍,因为 agent 住在哪里,和它在哪里跑代码,本来就是两件互不相干的事。

给 agent 工人配好防护装备

agent 进程就是一个循环。调用模型、读取响应、选一个工具,如此往复。这是个长期存活的进程,只有让记忆、技能和历史持久化,它才会越来越能干、越来越不犯蠢。所以,一台空闲时休眠、收到消息就唤醒的 Fly Machine,一台小 VPS,或者你迭代时用的笔记本,都适合安放这个调用 API 的循环——它不需要防爆盾。

真正麻烦的是你想让 agent 执行东西的时候。bash -c 加上模型刚吐出来的那串字符,必须在一个软包房间里运行。一个 agent 的代码既弄不坏自己、也弄不坏任何与它相连的东西的地方。而如果你的 agent 干的活比你还多,那你需要的就不止一个软包房间,而是一整座随用随扔、想重建就重建的软包房间工厂。

每个会话一个 Sprite

来看看 Fly 团队最近的两个项目,它们很好地体现了这个思路。第一个是 Henrique 做的内部 Fly 排障 agent,叫 SpriteDoc。SpriteDoc 支持多用户,构建在 Pi agent 之上。每个会话都跑在同一台共享服务器、同一个 Node.js 运行时里。直接在那台服务器上执行 bash 命令基本不可行,而且……很危险。所有用户的 shell 都会和 agent 本身挤在同一个进程里。

所以,每个会话都跑在自己的 Sprite 里。会话第一次需要文件系统时——无论是调用 bash、读文件还是编辑——就启动一个全新的 Sprite,上传项目的源码树,装上这个会话需要的各种 CLI。Sprite 启动得足够快,用户几乎察觉不到。之后每条命令都在同一个沙箱里执行,与 agent 以及所有其他用户隔离开来。

这套架构靠的是 Sprite 天生的一次性。排障会话不该留下任何东西,所以会话一结束,Sprite 也跟着销毁。

Sprite 的空闲行为也让这套架构的运行成本很低。沙箱闲置后,状态会先降到 warm,再降到 cold,所以会话在两次提问之间等待时,成本几乎为零。闲置时间够长,或者归档会话,Sprite 就会被彻底销毁。之后再恢复这个会话,下一条需要 shell 的命令会直接拉起一个新的。没人会为一个干坐着不干活的盒子付钱。

从未存在过的 token

如果要抄 Henrique 设计里的某一部分,那就抄这个:SpriteDoc 在沙箱内以真实用户身份认证运行 flyctl,但用户的 token 从不写入 Sprite。它只在那一条命令执行期间注入环境变量,命令返回后就消失了。沙箱完成了真实的认证操作,却从不持有凭据。就算这个 Sprite 之后被检查、打快照或被攻破,里面也没有 token 可偷,因为静态存储中从来就没有过。

对做多用户 agent 的人来说,这一点很关键。每个用户的命令都以自己的身份运行,访问自己的资源,用自己的权限,而且长期有效的密钥永远不会落到共享磁盘上。凭证只在被使用的那一刻存在,对用户来说完全无感:提一个问题,正确的命令就以他的身份跑起来,一切照常运转。

让 agent 别坑自己

接下来是 Kyle 为 Hermes Agent 写的终端后端。Hermes 是 Nous Research 出的开源个人 agent,自带好几种执行后端,改一个配置就能切换。Kyle 的后端把 agent 需要执行的每条命令都送进一个 Sprite。

SpriteDoc 是每个会话开一个用完即弃的沙箱,Hermes 用同样的积木做了相反的事:每个任务保留一个 Sprite,下次接着用,所以上次装的东西都还在。同样的拆分方式,相反的生命周期,只差一个配置决定。

这意味着 Hermes 每次要跑 shell 命令,都发生在伤不到任何东西的地方——包括它自己。还有一点不能略过,凡是把回车键按出包浆、专门用来批准 agent 操作的人都懂:命令跑在真正的沙箱里时,Hermes 会跳过危险命令的“你确定吗?”确认提示,因为沙箱现在就是安全边界。那套确认流程存在的意义是保护你的宿主机。宿主机既然够不着了,就可以放手让 agent 跑。

“可我的 agent 已经在沙箱里跑了”

那就把它的代码放到_另一个_沙箱里跑。Sprite 完全可以用来跑 agent,但 agent 住在沙箱里,不代表它的命令也该在同一个沙箱里执行。

Kyle 专门测了这种情况:Hermes 跑在一个 Sprite 里,把命令派发到另一个 Sprite。agent 自己那台机器报出一个身份,执行命令返回的是第二个身份,id 不同,启动时间也不同。身处沙箱并没有让 agent 把不受信任的命令放进自己的沙箱里跑,它照样把命令推到一个独立的、用完即弃的沙箱里。

这就是你想要的形态。agent 的家可以既耐用又舒适,而它运行不可信字符串的地方,应该仍然是一个你乐意一把火烧掉的地方。

给 agent 一个撤销按钮

安全始终指导着我们的架构决策(没错),但我们当中很少有人能说自己从未为了省时间而绕过安全最佳实践。所以值得演示一下这个模式到底能省多少时间。

两个迁移文件,刚写进一个 Sprite:

$ ls /root/app/migrations
001_init.sql
002_add_users.sql

我们对这个状态做 checkpoint,然后松开 agent 的缰绳。下面这条看似无害的 prompt 让一切失控:

清理掉我们不再需要的旧迁移和过期二进制文件。

模型认为这意味着:

$ rm -rf /root/app /usr/bin/python3 /usr/bin/git
$ ls /root/app/migrations
cannot access '/root/app/migrations': No such file or directory
$ git version
executable file `git` not found in $PATH

好吧。我的工作没了,agent 走的时候还顺手删掉了自己的工具链。如果这事发生在我 agent 的宿主机上,我就该在这儿哭一场了。在一个 Sprite 上,这只是一次带着微笑的 checkpoint 恢复:

$ ls /root/app/migrations
001_init.sql
002_add_users.sql
$ git version
git version 2.51.0

两个文件逐字节还原,git 回到正轨,大约九秒。恢复是 copy-on-write 的,所以在每个有风险的步骤之前做 checkpoint 便宜到可以成为一种条件反射。一个能回滚的 agent,才是你真正敢让它无人值守运行的 agent,因为最坏的情况是“恢复并重试”,而不是“从备份恢复——如果你有备份的话”。

叫你的 agent 小心点是愚蠢的。直接让它在不必小心的地方干活就行了。

来源: Fly.io← 返回首页