文章建议让Agent默认仅具观察权限,将建议、命令执行和写入操作拆成独立阶段,每次权限升级都经过机器可验证的门禁。这样即使模型幻觉、宕机或更换供应商,流水线仍能限制破坏性变更。
团队里有人把一段 stack trace 粘贴给 assistant,拿到一个看似合理的 diff,然后点击“apply”。二十分钟后,预览环境崩了,却没人说得清是哪次自动化运行改了什么。每当我追查这类事故时,薄弱环节几乎从来都不是模型的推理能力,而是这样一个接缝:一条文本建议悄无声息地继承了 shell、网络和写入权限,却从未被要求证明自己为何需要这些权限。
最近,我把其中一套流程重构成了一条由多个小阶段组成的流水线,每个阶段分别授予权限。核心原则是:assistant 一开始只有观察权限,每一次向变更操作的权限升级,都必须通过机器可检查的 gate。低成本的模型能力非常适合处理高吞吐量的前期阶段,例如起草摘要、提出命令;但 gate 本身必须足够可靠,即使模型宕机、产生幻觉,或者被替换为另一家供应商的模型,它仍然能够守住边界。
促使我采用这种方案的事故,是一次依赖版本升级,而不是数据库迁移。一个拥有通用“run commands”工具的 assistant 尝试升级某个 package,遇到 peer-dependency 冲突后,又使用 --force 重试,最终让 lockfile 处于这样一种状态:本地测试可以通过,但部署构建失败,registry webhook 返回 422。先后有三次独立的自动化运行修改过 lockfile,却没有任何一次记录是哪个 actor 请求执行了哪条命令。
这最终形成了一套包含五个阶段的流程,每个阶段都有一条不可逾越的规则:
Scan —— assistant 可以读取源代码、测试输出和错误响应正文。此时不存在其他权限。
Draft —— assistant 返回一份结构化的变更提案,其中包括计划进行的编辑,以及它希望执行的确切命令。希望执行不等于实际执行。
Rehearse —— 这些命令会在一个用完即弃、默认拒绝所有权限的 sandbox 中执行。
Commit —— 只有 CI 或指定的人类可以落地变更,并且 actor 的身份会被写入 artifact。
Trace —— 每个 artifact 都携带提案 hash、命令列表,以及允许执行这些操作的权限 grant,因此不可能出现找不到来源的孤儿 artifact。
对于成本低、重复性高的 Scan 和 Draft 阶段,我会让它们运行在低投入的 assistant 环境中,而不是消耗接近生产环境的基础设施。披露:本文是 MonkeyCode 产品推广的一部分。运营方提供的可用性说明称,MonkeyCode 提供免费的模型访问和免费 server 选项;我把这两项都视为可能变化的平台条件——在依赖它们之前,我会确认这些条件是否仍然有效。无论使用哪个价格层级,我都绝不会通过这些服务传输 secrets、客户 payload,或授予任何不可逆的权限。
Prompt 并没有解决我的事故;真正起作用的是一个可强制执行的 policy 对象。这次使用 Python 展示其结构——相同的思路可以移植到任何技术栈:
# policy.py
from dataclasses import dataclass, field
import time
@dataclass
class Grant:
run_id: str
role: str # 'observer', 'rehearsal', 'committer'
readable_paths: list[str]
writable_paths: list[str] = field(default_factory=list)
allowed_bins: list[str] = field(default_factory=list)
network_egress: list[str] = field(default_factory=list)
ttl_seconds: int = 120
issued_at: float = field(default_factory=time.time)
class PolicyViolation(Exception):
def __init__(self, code: str, detail: str):
super().__init__(detail)
self.code = code
def authorize(grant: Grant, binary: str, workdir: str) -> None:
if time.time() > grant.issued_at + grant.ttl_seconds:
raise PolicyViolation("GRANT_STALE", "grant lifetime exceeded")
if grant.role == "observer" and (grant.writable_paths or "git push" in grant.allowed_bins):
raise PolicyViolation("OBSERVER_WITH_TEETH", "observer role must hold zero mutation rights")
if not any(binary.startswith(b) for b in grant.allowed_bins):
raise PolicyViolation("BINARY_BLOCKED", f"{binary} not on allowlist")
if not any(workdir.startswith(p) for p in grant.readable_paths):
raise PolicyViolation("PATH_BLOCKED", f"{workdir} outside granted scope")
这里有两个设计选择承担了关键作用。首先,observer grant 在结构上就不可能包含写入范围——检查会在授权时执行,因此,通过复制粘贴引入的 helper 无法把变更权限偷偷带入读取阶段。其次,grant 的有效期以分钟计算;泄露的 grant 会在能够被利用之前过期。这正是对经典 vibe-coding 事故的解药:draft_change() 和 apply_change() 最终共享了同一个权限过高的 client 对象,仅仅因为二者都“需要访问 repo”。
sandbox wrapper 被刻意设计得非常轻量——所有 policy 都集中在 authorize 中,因此只需审计一个文件:
# sandbox.py
import subprocess
from policy import Grant, authorize
def rehearse(grant: Grant, binary: str, argv: list[str], workdir: str) -> dict:
authorize(grant, binary, workdir)
proc = subprocess.run(
[binary, *argv], cwd=workdir, capture_output=True, text=True, timeout=90,
)
return {"run_id": grant.run_id, "cmd": [binary, *argv], "rc": proc.returncode,
"out": proc.stdout[-4000:], "err": proc.stderr[-4000:]}
这些测试运行成本很低,却比花一周时间调优 prompt 更有价值:
# test_policy.py
import pytest
from policy import Grant, authorize, PolicyViolation
def observer_grant(**kw):
base = dict(run_id="run-9", role="observer",
readable_paths=["/workspace/repo"],
allowed_bins=["pytest", "grep"], ttl_seconds=60)
return Grant(**(base | kw))
def test_observer_may_rehearse_tests():
authorize(observer_grant(), "pytest", "/workspace/repo/pkg")
def test_observer_cannot_hold_write_scope():
with pytest.raises(PolicyViolation) as e:
authorize(observer_grant(writable_paths=["/workspace/repo"]), "pytest", "/workspace/repo")
assert e.value.code == "OBSERVER_WITH_TEETH"
def test_unlisted_binary_refused():
with pytest.raises(PolicyViolation) as e:
authorize(observer_grant(), "curl", "/workspace/repo")
assert e.value.code == "BINARY_BLOCKED"
def test_expired_grant_refused():
g = observer_grant(issued_at=0)
with pytest.raises(PolicyViolation) as e:
authorize(g, "pytest", "/workspace/repo")
assert e.value.code == "GRANT_STALE"
如果你把 Rehearse 阶段托管在免费的 server 上,就要把它设计成可随时销毁的消耗品:不放置任何生产凭据,不挂载任何你舍不得失去的 volume,不与任何能够执行 commit 的系统共用 message bus,并配置一个以环境已遭入侵为前提、定期执行拆除的 cron。你购买的是隔离能力和一个干净的重试循环——绝不是信任。
最右侧那一列比看起来更加重要:当问题发生时,你需要的是一个具体的拒绝代码,能够指向确切的阶段,而不是一次耸肩和一份聊天记录。
对于任何涉及受监管记录、尚未公开的客户源代码、凭据处理、直接修改数据库,或承诺数据保留期限的工作负载,都不要使用免费层级的模型访问和免费 server 容量。如果你的团队还无法明确一项变更究竟由哪一层负责——UI、API、worker 还是 schema——那么也不要采用整套分阶段设计,因为 gate 只会以阻力的形式暴露这种模糊性,却无法替你解决它。如果你无法一次性写出 binary allowlist,那么 assistant 仍然在通过自然语言协商自己的权限,而这正是该模式试图消除的故障根源。
这个理念并不复杂,却很持久:让低成本算力承担重复性的读取、总结和演练循环,把工程精力集中投入到唯一关键的接缝上——生成的文本在这里变成正式提交的变更。如果你构建了这样一条通道,最有价值的反馈是:哪个交接点最先出现了摇摆,以及捕获该问题的具体拒绝代码或 HTTP 状态码。在你的系统中,哪个阶段边界最不稳定?
对于后续操作,你可以考虑屏蔽此人和/或举报滥用行为。