Skip to content

← 上一节:01 · 先看它出事 · 章目录 · 下一节:03 · 判断写成一个纯函数

02 · 检查该由谁来做

上一节看到:模型自己填了路径 /etc/hosts,代码直接就读了,中间没有一步在问"该不该"。

这一节把这道检查补上。先看它长什么样,再回头说为什么是这样。

解法长这样

现在执行工具的代码是这样(第 05 章的 lib/agent.ts):

ts
const toolExecutions = functionToolCalls.map(
  (toolCall) => executeAgentTool(toolCall),
);

改成这样:

ts
// 先问一句能不能执行
const decision = decideAgentToolPermission(工具的性质, 这次调用的参数, 这次运行的设置);

if (decision.type === 'deny') {
  // 不执行,把理由交回给模型
  return 拒绝的结果;
}

executeAgentTool(toolCall);   // 允许了才真的执行

就这么一件事:执行工具之前,先调用一个检查函数;它说行,才真的执行。

/etc/hosts 那次调用会死在 decision.type === 'deny' 这一行,工具函数根本不会被调用。

下面回头拆三个问题:这个检查函数要知道什么?为什么放在这个位置?这段代码写在哪个文件里?

问题一:这个检查函数要知道什么

看它的三个参数。它们不是随便凑的,少一个,判断就做不出来。

第一个参数:这个工具是什么性质的。

同样是碰一个路径,读和写完全是两回事:

  • 读错文件,内容泄露了,但没有破坏。
  • 写错文件,数据被覆盖,可能不可逆。
  • 执行命令,后果没有上限。

所以要知道四件事:是不是只读、会不会造成不可逆后果、会不会访问外部世界、同样的调用做两次结果是否和做一次一样。

这四件事只跟工具本身有关。read_file 是只读的,换到哪个项目、哪个用户、哪次运行都成立。

第二个参数:这次调用具体要碰什么。

对文件类工具就是那个路径。但光有模型填的字符串不够,还要知道它解析之后指向哪里。

举个例子:模型填的是 ../../../etc/hosts。看字符串像个项目内的相对路径,解析完在项目外面。只比对字符串的检查,一行就被绕过去了。

第三个参数:这次运行允许到什么程度。

同一个工具、同一个路径,答案可以不一样:

  • 这次运行设定成"只准读项目内" → 拒绝。
  • 用户说了"这台机器归你随便用" → 放行。
  • 用户希望每次越界都问一下自己 → 暂停去问。

这类信息跟工具无关,也跟这次调用无关,它属于这次运行。至少两个维度:能碰多少东西(只能读 / 可以改工作区 / 不设限),要不要问人(能问就问 / 有需要才问 / 从不问)。

三个凑齐,答案才出得来。 回到 /etc/hosts 那次:工具是只读的(第一个),路径解析后在项目外(第二个),这次运行只允许项目内(第三个)。三条合起来,结论是拒绝。

少任何一个都不行。只有前两个,你不知道这次运行允不允许越界;只有第三个,你不知道这次到底想碰哪儿。

问题二:为什么检查放在调用工具之前,而不是工具里面

看到问题的人,第一反应通常是在 read_file 里面加一句路径检查。五分钟能挡住 /etc/hosts

但工具函数被调用的时候,它拿不到第三个参数

它手里只有解析好的路径。这次运行是只读模式还是随便用,没人告诉它。想知道就得自己去读配置——于是每个工具都要写一遍同样的读取代码。以后运行规矩多一个维度,所有工具跟着改。

第一个参数也有问题。工具"是只读的"这件事,现在只存在于写代码的人脑子里,代码里没有任何地方写着这句话。检查函数要用它,就得先把它写下来(下一步就做这件事)。

反过来看调用工具之前那个位置:这次运行的设置就在手边,模型填的参数也在手边,工具是哪个也知道。三个参数在这里最容易凑齐。

问题三:那段代码写在哪个文件里

上面的示意代码写在 agent.ts 里。能跑,但不该留在那儿。

agent.ts 负责的是和模型对话:组装消息、发请求、处理流式返回、决定要不要再来一轮。第 10 章要在这里处理提交时机,第 21 章要在这里做上下文压缩。它已经够重了。

再让它兼管安全判断,结果是:以后每次调整权限,都要改这个最核心、最容易出事的文件。

所以新建一个文件 lib/agent-tool-runtime.ts,把"执行一次工具调用"这件事整个搬进去——查出是哪个工具、凑齐三个参数、做检查、执行、整理结果。

agent.ts 那边只剩一行:把工具调用交出去,拿回结果。它不再关心工具是怎么跑起来的。

这一节的位置

现在解法定了:执行之前先检查,检查需要三个参数,这段流程搬进一个新文件。

还有一个问题没答:decideAgentToolPermission 这个函数本身要写成什么样?它可以去读配置文件、可以直接弹窗问用户、可以记日志——这些都能工作。

下一节说为什么它什么都不做,只做判断。

本节小结

  • 解法:执行工具之前先调用一个检查函数,它说行才执行。
  • 检查函数要三样信息:工具是什么性质、这次调用要碰什么、这次运行允许什么。
  • 路径要看解析之后的结果,只比对字符串会被 ../../ 绕过。
  • 不能写在工具里面:工具函数拿不到这次运行的设置。
  • 不能留在 agent.ts:那个文件的职责是和模型对话,不该兼管安全。
  • 所以新建 lib/agent-tool-runtime.ts,把一次工具调用的完整流程搬进去。

← 上一节:01 · 先看它出事 · 章目录 · 下一节:03 · 判断写成一个纯函数