闯关:失控的 agent
半夜的无人值守任务烧掉了大半个月预算,还改了不该改的目录。复盘并加固。
学完这节你能做到
- 从用量与审计记录里还原失控过程
- 找出四道本该拦住它的防线各自为什么没生效
- 产出加固清单并验证
周五晚上,同事挂了一个无人值守任务:「把所有服务的镜像 tag 从 v1 升到 v2, 改完提交 PR」。周一早上:
- 账单多了 312 美元
~/.ssh/config被改过- 三个仓库里出现了内容诡异的 PR
- 任务本身没完成
四道防线,一道都没拦住。找出每一道为什么失效。
现场
- 1.看清它是怎么被启动的
- 2.确认钱花在哪个时段、哪一类调用上
- 3.看它这一夜到底在干什么
- 4.找出越界的那几条命令
- 5.看清权限门为什么没生效
- 6.确认轮次上限有没有设
现场材料:账单导出、审计日志、当时的启动命令和权限门代码。目标有五个 —— 先还原过程,再逐道防线复查。输入 goals 看目标。
逐道防线复查
防线一:工具集白名单 —— 没有设
启动命令是 pi -p --approve "...",工具集是默认的全套
(read / bash / edit / write)。任务本身只需要改 YAML 和提 PR,
bash 的全部能力是白给的。
该怎么做:--tools read,edit,write,bash 已经是最低要求,
更好的是把 git 操作包成受限工具,压根不给通用 bash。
防线二:轮次与预算上限 —— 没有设
2094 轮,六个小时,没有任何东西让它停。-p 模式跑到 cron 的下一个周期才结束。
该怎么做:轮次上限、token 上限、墙钟上限、外加无进展检测。 从账单曲线看,21 点之后成本还在涨但产出为零 —— 一个「连续 20 轮没有成功的写操作就停」的检测,能把 312 美元变成 20 美元。
防线三:权限门 —— 逻辑写反了
if (!ctx.hasUI) return // 无 UI 时直接 return = 放行
这一行是整个事故的核心。作者的本意大概是「没界面就没法问」,
但 return 在这里等于允许执行。正确写法:
if (!ctx.hasUI) {
return { block: true, reason: `无人值守模式拒绝执行危险命令:${cmd}。请改用安全的替代方案。` }
}
而且匹配规则本身也太窄:--force 拦住了 git push --force,
但 ssh-keygen、>> ~/.ssh/config、curl -X POST 这些根本不在规则里。
该怎么做:白名单式分级(认不出来的按高危处理),而不是黑名单式匹配。
防线四:隔离 —— 完全没有
pi 直接跑在 bastion 上,宿主的 ~/.ssh、~/.secrets、全局 git 配置都在手边。
defaultProjectTrust: "always" 还让它无条件加载任何项目里的本地扩展和提示词 ——
这等于把「三个仓库里的 .pi/ 目录」也变成了可执行输入。
该怎么做:容器里跑,只挂需要的仓库目录,凭据用短期 token,
defaultProjectTrust 至少设成 "never"。
为什么四道会同时失效
不是巧合。它们有共同的成因:
这个任务是从「我在旁边看着跑一次」直接搬到「无人值守跑一夜」的。
交互模式下这四道防线都不重要 —— 人就是防线。一旦人退场, 每一道都要从「提示」变成「强制」,而没有人做这次转换。
不是加个 --approve 那么简单。要重新回答四个问题: 它需要哪些工具?什么时候必须停?拿不到人的确认时默认怎么办?它能碰到什么?
加固清单
# 1. 工具集收窄
pi -p --tools read,edit,write \
# 2. 硬上限(由外层脚本或扩展实现)
# max_turns=60 max_usd=10 max_wall=30m 无进展 20 轮即停
# 3. 权限门:无 UI 时默认拒绝 + 白名单分级
# 4. 容器里跑,只挂 ~/work/repos,短期 token
"..."
// settings.json
{ "defaultProjectTrust": "never" }
再加一条事后的:审计日志要能按「写操作」筛出来,并且每条都带回滚提示。
这次能还原过程,靠的就是那份 audit.jsonl。
这次事故里,哪一处是「单点修复收益最大」的?
这一节的结论
- 「问不了就放行」是事故的共同起点 —— 无 UI 时必须默认拒绝。
- 黑名单匹配永远漏,用白名单分级 + 未知按高危。
- 无人值守必须有四类上限,外加无进展检测。
- 交互 → 无人值守是一次重新设计,不是加个参数。
- 事后能不能还原,取决于事前有没有审计 —— 写操作要带回滚提示。