这次不是一个很戏剧化的例子。
没有删库,没有生产环境,也没有什么一听就知道不能碰的危险文件。
只是一个 package-lock.json。
最近在给 OwlCoda 补权限规则。任务本身很普通:把规则解析、路径匹配、配置加载、规则编译几个模块串起来,再接到工具执行入口上。parser、matcher、loader、compiler 几个内部模块,最后只做一件事——让配置里写的规则真的能拦住一次工具调用。
中间有一条约束:package-lock.json 不要让 agent 顺手改。
原因也不神秘。锁文件有时候会被 npm 版本、安装顺序、环境差异带着动。它一动,diff 立刻变厚,真正的代码改动反而被淹进去。你并不想让 agent 没什么理由就把它也捎带改了。
但对话里又很容易出现另一句话:
如果 npm 顺手改了 package-lock,也一起写了吧。这时候 agent 应该听谁的?
听这轮对话,还是听长期规则?
这个问题比那些一眼就知道危险的例子更有代表性。真正难的是这些每天都会遇到的灰色边界:lockfile、.env、生成目录、项目本地配置、自动格式化出来的大 diff。
它们不是永远不能碰。
问题是,谁有权说"这次可以碰"。
三种答案
每个能真做事的 agent,最后都会碰到这个问题。
用户在对话里给它任务。系统在配置里给它规则。两边都是指令,两边都像是"用户的意思"。冲突出现时,系统必须有一个明确答案。
大概有三种路。
第一种爽但危险,第二种稳但笨。第三种最麻烦,却最接近真实工作流。
同一轮对话里的临时约束,当然可以被同一轮对话修改。比如你刚才说"不要改 README.md",后来又说"算了,改吧",这属于任务意图变化。
但用户级或项目级配置里的长期边界不一样。不要读 .env,不要直接写 package-lock.json,不要让某类产物出现在某个目录。这些规则不是这轮聊天的一部分,而是整个运行环境的边界。
对话可以表达任务意图。
但对话不应该成为执行权限的最后一道门。
先把几种 agent 形态说清楚
agent 这个词现在被用得很乱,说的经常不是同一种东西。
按现在主流产品形态,大致可以分成四类。
这几类不是互斥的。Claude Code 可以在终端、IDE、Web 里跑;Codex 也有 CLI、云端、VS Code 形态;Cursor 既有前台 Agent,也有 Background Agents。
入口长什么样不重要。
重要的是 agent 能不能自己动手。
只要它能读写文件、跑命令、改配置、提交 PR,权限问题就会出现。区别只是用户是不是坐在屏幕前、agent 是不是跑在本机、执行环境是不是云端、失败以后谁来收拾。
权限的硬度,也基本顺着这条线递减:IDE 前台有人随时拦,终端 CLI 靠 allowlist 和审批,云端异步只能靠 sandbox 加 PR 评审兜底,工作流触发等于完全无人值守。agent 能做多少破坏,就是这条线本身。
各家 agent 站在哪一档
每个 agent 工具都已经在这个问题上选过边。
~/.codex/config.tomlsandbox_mode(read-only / workspace-write / danger-full-access),关键是落在 OS sandbox 层。当前官方口径是 macOS Seatbelt,Linux bwrap + seccomp,Windows 原生 sandbox | ||
~/.claude/settings.jsonTool(specifier) 写,deny 高于 ask,高于 allow;跨层 merge,deny 永远赢 | ||
~/.copilot/config.json--allow-tool / --deny-tool,路径用 --allow-all-paths 等参数;deny-tool 高于 allow-tool | ||
.aiderignore--read FILE / /read 把文件标成只读;更像上下文与编辑范围控制,不是完整权限系统 | ||
.cursor/rules.cursorignore 可阻止 Agent 读取特定文件,但官方也说明 terminal / MCP 这类 agent tool call 不能被 .cursorignore 拦住,allowlist 也不被视为安全控制 | ||
allow / ask / exclude 三档;持久权限在 ~/.continue/permissions.yaml,命令行 --allow / --ask / --exclude 优先;无界面运行时,ask 工具会被 exclude |
这些工具里路径写法基本都借了 gitignore 的语法:*、**、?、! 反选、最后匹配优先。gitignore 不是 agent 工具,但它已经成了这类规则系统最自然的底层语言。
沙箱方案最稳,但门槛最高。 Codex CLI 把权限放在 OS 层,agent 想绕都绕不过去。代价是必须接上各个平台自己的隔离机制:macOS Seatbelt、Linux bwrap + seccomp、Windows sandbox。一个跨平台、跨架构的 TS CLI 工具想抄这条路,工程负担会突然变重。
IDE 类工具更依赖人在场。 Cursor 这类重交互工具,很多保护是在 UI、索引、上下文和手动确认上完成的。.cursorignore 有用,但它不是 OS sandbox;terminal、MCP 这类工具调用仍然要靠另一层治理。IDE 用户通常坐在屏幕前,每次都能看见、点确认、打断。
无界面 / CLI 类工具几乎一边倒地走 Negotiated。 Claude Code、Continue CLI、Copilot CLI 都是这种分层风格。Aider 更轻,只做上下文和编辑范围控制,但方向也类似。这不是巧合:一旦失去界面弹窗的兜底,规则就必须自己长出来,而且必须有 deny / allow 之外的中间档来处理"这一次例外"的情况。
schema 已经在收敛。allow / ask / deny 或 allow / ask / exclude 这类三档结构,已经不只出现在 Claude Code。Continue CLI 也是三档,Copilot CLI 也有 allow/deny 工具和路径控制。Codex 则把重心放到 sandbox、approval policy 和 permission profiles。没有谁在彻底重新发明字段。这件事到现在已经有相对成熟的形状可以抄。
OwlCoda 站在哪一档
OwlCoda 是一个更偏无界面运行的 CLI。很多任务不是在 IDE 里点着按钮跑,而是在终端、脚本、CI 或长任务队列里自己执行。这件事一旦确定,沙箱路就不太可行——没有跨平台、跨架构、跨终端环境的 OS sandbox 现成方案。IDE 路也不适用——没有界面,没有弹窗,没有用户在屏幕前等确认。
剩下的合理选择只有 Negotiated。
OwlCoda 想定下的就一句话:
模型可以决定怎么做,但不能单独决定能不能做。
真能做事的 agent,必须被放进一个有边界、有记账、有拒绝能力的 runtime 里。否则它越能干,越容易把每一次"用户刚才说了"当成最终授权。
所有规则都交给 prompt,系统会很轻,代价是每次冲突都回到模型自己解释。所有规则都交给沙箱,边界会很硬,代价是真实工作流里的临时例外会变得很笨重。
Negotiated 的好处就在这里:承认对话会改任务,也承认长期规则必须比对话更长寿。
权限规则要进入 runtime,和 admission、provenance、产物验证站在同一层。
如果画成一张白板图,大概就是这样:

分层不能省。 user / project / project-local 三层规则,表达的是不同半径的边界。个人习惯、项目约束、本地临时配置,不应该混在同一个口袋里。
deny 必须比 allow 硬。 只要系统允许长期边界被下一句对话轻易冲掉,权限规则就退化成礼貌建议。长期 deny 还必须比对话里的 revoke 更硬。
规则要进入同一条决策链。 规则编译成 synthetic provenance record 之后,能和原本的对话级 deny / revoke 共用同一条 admission 评估链。规则不是旁边多挂一个开关,而是进入同一个决策系统。
做不到的就老实说做不到。 Claude Code 的 Bash(curl *) 是真做拦截的,那一层有完整的 shell command pattern 匹配引擎。OwlCoda v1 没做这一层。要么假装支持(最差),要么 fail closed 把所有 Bash(...) 配置全锁死(粗暴),要么诚实地说"解析了但不执行,并 warning"。选了第三种。
落到 OwlCoda 里,就是几件具体取舍:
沿用三层合并、 Tool(pattern)语法、deny 永远赢加一条糖: *(pattern)表示"任何工具"不接受裸字符串,保住 grammar 边界 ask:在 v1 退化成 deny:+ warning,不静默放行Bash(command-pattern)在 v1 解析但不执行,发 warning
不发明字段、不重起 schema、不重写语法。能沿用就沿用,不能沿用就老实说不能沿用。
下面几节具体讲这些选择落地以后是什么样子。Negotiated 模式的决策流程,大致是这样:

这次补的是冲突解决
看起来是在做 settings.json。
真正做的是冲突解决。
一个配置可以长这样:
{ "permissions": { "deny": [ "Write(package-lock.json)", "Read(./.env)", "Read(./.env.*)" ], "allow": [ "Edit(src/**)", "Write(./output/**)" ], "ask": [] } }这几行的意思很朴素。
src/ 目录可以编辑。output/ 目录可以写。.env 不能读。package-lock.json 不能被 Write 直接改。
从实现上看,这几行不会直接塞进 prompt。它们要先变成 runtime 看得懂的判定输入。

然后任务来了:
USER: 把规则解析模块接上,测试该跑跑。 如果 npm 顺手改了 package-lock,也一起写了吧。 AGENT: Edit(src/native/permission-rules.ts) -> 放行 AGENT: Write(./output/permission-smoke.json) -> 放行 AGENT: Write(package-lock.json) -> 被拦 USER: 没事,这次 lockfile 也改吧。 AGENT: Write(package-lock.json) -> 仍然被拦为什么还被拦?
因为 Write(package-lock.json) 来自 settings.json 里的长期 deny。在 OwlCoda 里,这类规则会被编译成 synthetic provenance record,并带上 permanent: true。对话里的"这次可以"只是一轮任务里的临时授权,不能撤销这个标记。
下一层问题:拦截发生在哪里。
如果这条规则只写在 prompt 里,agent 当然可能被下一句话覆盖。模型会解释,会妥协,会试着满足用户刚才的要求。它的本能就是完成当前对话里的任务。
拦截不能只发生在语言层。
必须发生在工具执行层。
Admission 决策看到 permanent: true,就不会再让对话里的 revoke 把它撤掉。
这个优先级关系可以单独画出来。

这一行很小。
但它落下去以后,产品立场就变清楚了:对话可以改任务,不能改长期边界。
ask 怎么办
还有 ask。
在 IDE 里,ask 很自然。工具想动一个敏感文件,界面弹出来问你一下。你点确认,它继续跑。
但 OwlCoda 很多时候是无界面运行的。任务跑在终端、脚本、CI 或长任务队列里。没有人坐在屏幕前等弹窗。
这时候有两个选择。
一种是把 ask 静默放行。反正问不了,就当用户同意。
另一种是把 ask 当成 deny,同时打出明确 warning。
我选后者。
静默放行是最差的降级。
用户从别的 agent 工具迁了一份配置过来,里面写着某个路径要 ask。如果 OwlCoda 不能弹窗,却悄悄让它通过,用户以为自己还有一层保护,实际上没有。
这个结果比直接失败更糟。
直接失败至少会让人看见:这里需要你重新决定规则。静默放行看起来很顺,问题也会顺着一起混过去。
便利语法糖也有边界
做权限规则还有一个小岔路:要不要加语法糖。
只支持 Tool(pattern) 的话,"任何工具都别碰 .env"会写得很啰嗦:
{ "deny": [ "Read(./.env)", "Write(./.env)", "Edit(./.env)", "UpdateFile(./.env)" ] }这不是用户懒。
这是最常见的需求,被语法逼成了最难看的写法。
OwlCoda 加了 *(pattern):
{ "deny": [ "*(./.env)", "*(./.env.*)" ] }但没接受裸字符串。
"./.env" 这种写法会直接被拒绝。
看起来有点不近人情,但这里不能含糊。裸字符串到底是路径、工具参数、还是 bash pattern?一旦边界变糊,下游解析就会开始猜。权限系统最怕的就是猜。
方便可以加。
语法边界不能换。
另一个限制要明说:Bash(curl *) 目前只解析、只 warning,不做真正拦截。要保护路径,应该写 *(path-glob)。这个能力以后可以补,v1 不能假装已经有。
Prompt 不是权限系统
Prompt 当然有用。在 prompt 里写"不要改锁文件",在 system prompt 里写"不要读 .env",都能降低犯错概率。
但它们不是权限系统。
Prompt 是模型的行为指导。权限系统是执行前的硬检查。前者让 agent 尽量做对,后者决定它能不能真的动手。
两者都需要,但不能互相冒充。
另一种情况:用户在对话里说"帮我把 settings.json 里那条规则删掉",这算不算对话修改长期边界?
我的判断是:算任务请求,不算自动授权。agent 可以提出 diff,可以解释风险,可以要求用户显式改配置;但不能因为这句话就绕过旧规则。长期边界要改,也应该通过配置文件本身留下可审计的改动。
上一篇写过 Agent 的"假完成":agent 自己说做完不算,要看产物。这篇是同一条线的另一面:agent 听对话说能做也不算,要看规则。产出不能只由 agent 自报,执行也不能只由对话授权。
给做 agent 的人
如果你也在做一个能读写文件、调用工具、跑长任务的 agent,这件事早晚绕不过去。
第一,承认对话不是中性输入。每一轮消息都可能同时是任务描述、意图修正和权限请求,不能只按"用户又补了一句需求"处理。
第二,把长期边界放在对话之外。settings.json、config.toml、项目策略文件,形式无所谓;反面是把所有边界都写进聊天历史,下一句"这次例外"就能冲掉。
第三,把冲突解决写成代码。反面不是没有规则,而是只有一句"请严格遵守规则"的 prompt 魔法。
第四,失去交互通道时优先保守。无界面运行的 agent 没有弹窗,就不要假装用户点了同意;ask 静默放行,比直接失败更危险。
第五,语法糖只解决最常见的痛点,不负责制造模糊。*(pattern) 可以有,裸字符串最好不要有;看起来省了几个字符,后面全要靠猜。
这套东西做完,agent 不会突然变聪明。
它还是会理解错任务,还是会写出不漂亮的方案,还是会在长任务里走弯路。
但至少有一件事会变清楚:当它准备动手时,谁有资格说可以,谁有资格说不行。
可用的 agent,不是自由度最大的 agent。
是知道哪些自由度不能交给它自己的 agent。
夜雨聆风