别把二阶 pin 留给人记
一个发布脚本正确刷新了公开源码 URL,却没有告诉 agent 同提交更新 CI 成本守卫里的 SHA-256 pin。真正的修复不是再写一条提醒,而是让产生变更的工具输出下游 pin,并把旧断言反转成拒绝危险回归的测试。
问题
agent 工作流最常见的失败方式很朴素:不是 agent 不听话,而是你给它的指令本来就少了一步。
这次是一个发布辅助脚本的问题。OSS sync 脚本会发布并验证公开源码 tag,然后改生产 compose 文件里的 POSTFLOP_SOLVER_SOURCE_URL,让它指向本次部署 solver 对应的不可变源码 commit。可这个 compose 文件本身又被 GitHub Actions 的成本守卫用 SHA-256 pin 住了。URL 一变,文件 hash 也必然变。
脚本结尾的 Next: 清单列了 sync 后要提交哪些文件,却没列成本守卫里的 pin。于是 agent 完全照着清单做,也能通过本地 gate,最后在 hosted CI 的 GitHub Actions cost policy 步骤失败。标准生产发布的 admission 一旦预留确实不会退还,但这次并没有走到那一步:run 31818321915 是 hotfix dispatch,而且在 Reserve production admission 之前就失败了,所以消耗的是 hosted runner 时间,不是 standard 每日名额。
可复用的教训很窄,但很关键:当一个工具会改动另一个守卫按内容 pin 住的文件时,这个工具必须自己输出下游 pin 的更新。不要把这个二阶改动留给记忆、reviewer 口口相传,或者另一份文档。
机制
内容 pin 好用,正是因为它故意很笨。regex 可以争辩;review 过的 digest 要么匹配,要么不匹配。我们的 hosted-cost policy 会 pin 住允许进入 hosted runner 的 workflow 和本地文件;只要出现没 review 过的 payload,就 fail closed。
HOSTED_LOCAL_FILE_SHA256=(
'scripts/tests/db-observability-guard.sh|...'
'deploy/docker-compose.prod.yml|...'
)
对于昂贵的 hosted runner 和生产发布路径,这种做法是合理的。它能防止一个小小的 YAML 写法变化、helper 改写,或者 build context 扩大,悄悄把新工作塞进 CI。
但内容 pin 会制造第二个必须同步移动的对象。这次 oss-sync.sh 明明知道自己何时改了 deploy/docker-compose.prod.yml,也能直接读到改完后的本地字节。让后面的 agent 再去记「还要 repin cost guard」,就是薄弱点。
改了什么
修复把指令挪回了生产者。源码 tag 验证完成、compose 文件重写之后,脚本现在会直接打印应该写进成本守卫的下一个 SHA-256:
2. REPIN in the same commit: set the deploy/docker-compose.prod.yml SHA-256 in
scripts/tests/github-actions-cost-guard.sh to $(shasum -a 256 deploy/docker-compose.prod.yml | awk '{print $1}')
改动很小,但工作流的形状变了。agent 不再需要推断「source URL 变了」和「hosted-cost manifest 也要变」之间的隐藏依赖。制造第一处改动的命令,现在把第二处改动作为自己的回执一起吐出来。
随后成本守卫里的 compose pin 被更新到 review 过的文件字节。同一个补丁还更新了 scripts/tests/db-observability-guard.sh 的 digest,因为那个 guard 自己也变了。
不要只删掉过时断言
这个补丁的后半段,才是我会复制到任何 agent-heavy 代码库里的做法。数据库可观测性 guard 以前要求 0065 down migration 里必须有 DROP EXTENSION IF EXISTS pg_stat_statements。这个反向操作是错的:生产环境通过 shared_preload_libraries 预加载 pg_stat_statements,而 up migration 写的是 CREATE EXTENSION IF NOT EXISTS。回滚时如果 drop,它可能删掉一个本 migration 根本没创建的扩展。
偷懒的修法,是把旧断言删掉。更稳的修法,是把它反转:
if grep -Fq 'DROP EXTENSION IF EXISTS pg_stat_statements' "$down"; then
echo "0065's down migration must NOT drop pg_stat_statements" >&2
exit 1
fi
这样,昨天的错误要求变成了明天的回归测试。这对 agent 尤其重要:未来某次修复可能会按「up 有 CREATE EXTENSION,down 就该对称 drop」去 grep,然后把危险步骤「补回去」。反转后的检查会让这个看起来很顺手的改动立刻失败。
可以照抄的模式
凡是生成或半生成的发布产物,都把三件事放近一点:
| 问题 | 应该放哪 | 缺了会怎样 |
|---|---|---|
| 工具改了哪个文件? | 生产者自己的输出 | agent 只提交了半套文件 |
| 谁按内容 pin 或 review 这个文件? | 同一段输出里,点名下游文件 | 本地证明绿,hosted 证明红 |
| 哪种旧形状绝不能回来? | 反转后的 guard,而不是删掉的 prose | 后续「清理」把 bug 带回来 |
重点不是让 agent 更聪明。重点是让工作流少依赖聪明。如果工具能算出下一个 hash,就把 hash 打出来。如果旧检查错了,而旧形状本身危险,就把它写成 forbidden shape。如果 hosted guard 会消耗时间或发布容量,就让它在平台允许的最早位置失败。
总结
- 内容 pin 是契约。只有每个会改变被 pin 内容的生产者都知道怎么移动 pin,它才真的可靠。
- 回执胜过提醒。字节变化的那一刻,就在脚本输出里给出 repin 命令。
- 反转坏断言。旧检查过时且危险时,不要直接删除;把那种旧形状明确列为禁止。
- 按字面 agent 设计。一个只有在「有人记得隐藏第二步」时才会成功的流程,还不能算自动化。