乐于分享
好东西不私藏

AI Agent 的自由度,不能让大模型自己定

AI Agent 的自由度,不能让大模型自己定

这次不是一个很戏剧化的例子。

没有删库,没有生产环境,也没有什么一听就知道不能碰的危险文件。

只是一个 package-lock.json。

最近在给 OwlCoda 补权限规则。任务本身很普通:把规则解析、路径匹配、配置加载、规则编译几个模块串起来,再接到工具执行入口上。parser、matcher、loader、compiler 几个内部模块,最后只做一件事——让配置里写的规则真的能拦住一次工具调用。

中间有一条约束:package-lock.json 不要让 agent 顺手改。

原因也不神秘。锁文件有时候会被 npm 版本、安装顺序、环境差异带着动。它一动,diff 立刻变厚,真正的代码改动反而被淹进去。你并不想让 agent 没什么理由就把它也捎带改了。

但对话里又很容易出现另一句话:

TEXT
如果 npm 顺手改了 package-lock,也一起写了吧。

这时候 agent 应该听谁的?

听这轮对话,还是听长期规则?

这个问题比那些一眼就知道危险的例子更有代表性。真正难的是这些每天都会遇到的灰色边界:lockfile、.env、生成目录、项目本地配置、自动格式化出来的大 diff。

它们不是永远不能碰。

问题是,谁有权说"这次可以碰"。

三种答案

每个能真做事的 agent,最后都会碰到这个问题。

用户在对话里给它任务。系统在配置里给它规则。两边都是指令,两边都像是"用户的意思"。冲突出现时,系统必须有一个明确答案。

大概有三种路。

立场
谁说了算
好处
代价
Prompt wins
对话说了算
最灵活,用户一句话就能改方向
agent 也能被一句话说服去绕过长期保护
Rule wins
规则说了算
最稳,配置就是边界
临时例外很麻烦,容易把工具做死
Negotiated
分层决定
接近真实工作流
规则系统会变复杂,必须把冲突写清楚

第一种爽但危险,第二种稳但笨。第三种最麻烦,却最接近真实工作流。

同一轮对话里的临时约束,当然可以被同一轮对话修改。比如你刚才说"不要改 README.md",后来又说"算了,改吧",这属于任务意图变化。

但用户级或项目级配置里的长期边界不一样。不要读 .env,不要直接写 package-lock.json,不要让某类产物出现在某个目录。这些规则不是这轮聊天的一部分,而是整个运行环境的边界。

对话可以表达任务意图。

但对话不应该成为执行权限的最后一道门。

先把几种 agent 形态说清楚

agent 这个词现在被用得很乱,说的经常不是同一种东西。

按现在主流产品形态,大致可以分成四类。

形态
代表
它在干什么
权限问题在哪里
IDE 前台 agent
Cursor Agent、VS Code / Copilot Agent Mode
坐在编辑器里,读代码、改多文件、跑命令、展示 diff
用户通常在屏幕前,权限一部分靠 UI、确认、diff review
终端 / CLI agent
Claude Code、Codex CLI、Continue CLI、Aider、OwlCoda
在本地 shell 里读写文件、调用工具、跑测试
离文件系统和命令行最近,权限必须贴近工具入口
云端异步 agent
Codex Cloud、GitHub Copilot cloud agent、Jules、Devin、Cursor Background Agents
接一个 issue 或任务,在云端环境里跑完,最后给 PR / patch / diff
权限变成仓库授权、云端 VM、数据保留、自动运行命令和合并策略
工作流触发 agent
CI 任务、Slack 触发、Sentry / Snyk / GitHub issue 触发
不是人打开聊天框,而是事件把 agent 拉起来干活
权限跨系统,除了代码仓库,还涉及 token、secret、外部服务边界

这几类不是互斥的。Claude Code 可以在终端、IDE、Web 里跑;Codex 也有 CLI、云端、VS Code 形态;Cursor 既有前台 Agent,也有 Background Agents。

入口长什么样不重要。

重要的是 agent 能不能自己动手。

只要它能读写文件、跑命令、改配置、提交 PR,权限问题就会出现。区别只是用户是不是坐在屏幕前、agent 是不是跑在本机、执行环境是不是云端、失败以后谁来收拾。

权限的硬度,也基本顺着这条线递减:IDE 前台有人随时拦,终端 CLI 靠 allowlist 和审批,云端异步只能靠 sandbox 加 PR 评审兜底,工作流触发等于完全无人值守。agent 能做多少破坏,就是这条线本身。

各家 agent 站在哪一档

每个 agent 工具都已经在这个问题上选过边。

工具
站位
机制要点
Codex CLI
Rule wins,沙箱优先
~/.codex/config.toml
 加 sandbox_mode(read-only / workspace-write / danger-full-access),关键是落在 OS sandbox 层。当前官方口径是 macOS Seatbelt,Linux bwrap + seccomp,Windows 原生 sandbox
Claude Code
Negotiated,最完整
~/.claude/settings.json
 三层合并(user / project / project-local),规则按 Tool(specifier) 写,deny 高于 ask,高于 allow;跨层 merge,deny 永远赢
GitHub Copilot CLI
Negotiated
~/.copilot/config.json
 存 trusted folders;工具用 --allow-tool / --deny-tool,路径用 --allow-all-paths 等参数;deny-tool 高于 allow-tool
Aider
Negotiated,极简
.aiderignore
(gitignore 语法)收缩仓库范围,--read FILE / /read 把文件标成只读;更像上下文与编辑范围控制,不是完整权限系统
Cursor
Prompt / context guard
.cursor/rules
 和 AGENTS.md 是 prompt-level 指令;.cursorignore 可阻止 Agent 读取特定文件,但官方也说明 terminal / MCP 这类 agent tool call 不能被 .cursorignore 拦住,allowlist 也不被视为安全控制
Continue CLI
Negotiated
CLI 工具有 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、产物验证站在同一层。

如果画成一张白板图,大概就是这样:

Agent Permission Runtime 白板图

分层不能省。 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 模式的决策流程,大致是这样:

Prompt 临时授权与长期规则的冲突决策

这次补的是冲突解决

看起来是在做 settings.json。

真正做的是冲突解决。

一个配置可以长这样:

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 看得懂的判定输入。

权限规则如何进入 Runtime

然后任务来了:

TEXT
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 把它撤掉。

这个优先级关系可以单独画出来。

长期 deny 如何压过临时授权

这一行很小。

但它落下去以后,产品立场就变清楚了:对话可以改任务,不能改长期边界。

ask 怎么办

还有 ask。

在 IDE 里,ask 很自然。工具想动一个敏感文件,界面弹出来问你一下。你点确认,它继续跑。

但 OwlCoda 很多时候是无界面运行的。任务跑在终端、脚本、CI 或长任务队列里。没有人坐在屏幕前等弹窗。

这时候有两个选择。

一种是把 ask 静默放行。反正问不了,就当用户同意。

另一种是把 ask 当成 deny,同时打出明确 warning。

我选后者。

静默放行是最差的降级。

用户从别的 agent 工具迁了一份配置过来,里面写着某个路径要 ask。如果 OwlCoda 不能弹窗,却悄悄让它通过,用户以为自己还有一层保护,实际上没有。

这个结果比直接失败更糟。

直接失败至少会让人看见:这里需要你重新决定规则。静默放行看起来很顺,问题也会顺着一起混过去。

便利语法糖也有边界

做权限规则还有一个小岔路:要不要加语法糖。

只支持 Tool(pattern) 的话,"任何工具都别碰 .env"会写得很啰嗦:

JSON
{   "deny": [     "Read(./.env)",     "Write(./.env)",     "Edit(./.env)",     "UpdateFile(./.env)"   ] }

这不是用户懒。

这是最常见的需求,被语法逼成了最难看的写法。

OwlCoda 加了 *(pattern):

JSON
{   "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。

#Agent#执行框架#权限边界
A
二月影
写真实工程压力里的 AI Agent、工具和交付系统。
技术文章 · Agent 执行框架 · 关注官网 owlcoda.com

相关学习资料