← 上一节:01 · 先看它出事 · 章目录 · 下一节:03 · 判断写成一个纯函数
02 · 检查该由谁来做
上一节看到:模型自己填了路径 /etc/hosts,代码直接就读了,中间没有一步在问"该不该"。
这一节把这道检查补上。先看它长什么样,再回头说为什么是这样。
解法长这样
现在执行工具的代码是这样(第 05 章的 lib/agent.ts):
const toolExecutions = functionToolCalls.map(
(toolCall) => executeAgentTool(toolCall),
);改成这样:
// 先问一句能不能执行
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 · 判断写成一个纯函数