--- summary: "门控、重写和批准工具调用,并在持久化之前重写工具结果" read_when: - "您需要阻止工具调用或需要插件的批准" - "您需要观察工具调用运行后做了什么" - "您正在独立的插件文件中编写发件人感知工具策略" - "您正在向 exec 工具贡献环境变量" - "您正在重写或阻止抄本写入" title: "工具调用策略挂钩" sidebarTitle: "工具政策" --- 工具端挂钩:参数重写、阻止、批准、执行环境贡献和转录持久性。 [ 插件挂钩 ](/plugins/hooks) 指南的一部分。 ## 工具调用策略 `before_tool_call` 接收: - `event.toolName` - `event.params` - 可选`event.toolKind`和`event.toolInputKind`,有意共享名称的工具的主机权威鉴别器;例如,外部代码模式 `exec` 调用使用 `toolKind: "code_mode_exec"` 并包含 `toolInputKind: "javascript"` 来接受代码模式输入 - 可选的 `event.derivedPaths`,由宿主尽力推导的目标路径提示, 适用于 `apply_patch` 等已知工具封装;这些路径可能不完整,或过度近似工具实际会修改的内容 (例如,对于格式错误或不完整的输入) - 可选的 `event.runId` - 可选的 `event.toolCallId` - 上下文字段,例如 `ctx.agentId`、`ctx.sessionKey`、`ctx.sessionId`、 `ctx.runId`、`ctx.toolKind`、`ctx.toolInputKind`,以及诊断用的 `ctx.trace` - 可选的 `ctx.abortSignal`,在所属工具调用被取消时触发;处理器应将其传递给可取消的 I/O,并移除其注册的任何监听器 - 可选 `ctx.requester`,发起当前消息运行的主机派生请求者。它可以包括 `channel`、`accountId`、`senderId`、`senderIsOwner` 和提供商本机 `roleIds`。缺失的字段是未经证实的,不是虚假的保证;如果策略需要这些字段,则拒绝该操作。 它可以返回: ```typescript type BeforeToolCallResult = { params?: Record; block?: boolean; blockReason?: string; requireApproval?: { title: string; description: string; scope?: ApprovalScope; severity?: "info" | "warning" | "critical"; timeoutMs?: number; allowedDecisions?: Array<"allow-once" | "allow-always" | "deny">; pluginId?: string; onResolution?: ( decision: "allow-once" | "allow-always" | "deny" | "timeout" | "cancelled", ) => Promise | void; }; }; ``` 类型化生命周期钩子的守卫行为: - `block: true` 是终结性的,并会跳过更低优先级的处理器。 - `block: false` 视为没有决策。 - 返回`params`重写主机拥有的工具参数。每个处理程序都会看到原始事件的独立副本,而不是先前返回的重写。最后返回的 `params` 获胜,直到请求批准。 - 第一个 `requireApproval` 获胜,其插件 id 由主机盖章。它冻结选定的参数快照:以后的处理程序可以阻止但不能更改已批准的参数。 - 原生工具继电器可以有更窄的合约。 Codex 原生工具支持阻塞和观察,但拒绝参数重写;请参阅 [Codex 挂钩边界](/plugins/codex-harness-runtime#hook-boundaries)。 - `requireApproval` 会暂停代理运行,并通过插件审批请求用户。`/approve` 可以同时批准 exec 和插件审批。在 Codex app-server report-mode 原生 `PreToolUse` 转发中,这会委托给匹配的 app-server 审批请求;参见 [Codex harness runtime](/plugins/codex-harness-runtime#hook-boundaries)。 - 更低优先级的 `block: true` 即使在更高优先级钩子请求了批准之后,仍然可以阻止执行。 - `onResolution` 接收已解析的决策:`allow-once`、`allow-always`、 `deny`、`timeout` 或 `cancelled`。 例如,将其添加到 `register(api)` 中以在主机拥有的 `exec` 调用之前进行询问。 `before_tool_call` 不需要选择加入对话访问: ```typescript api.on( "before_tool_call", () => ({ requireApproval: { title: "Run command", description: "Allow this exec tool call?", severity: "info", timeoutMs: 60_000, }, }), { matcher: ["exec"], priority: 50 }, ); ``` ### 单文件中的发送者感知策略 独立插件文件可以将部署特定的策略保存在代码中,而无需添加其他配置模式。此示例为所有者提供每个工具的访问权限,让已配置的维护者使用一组保守的工具和消息操作,并向已获得频道配置授权的发送者开放 `/fix`: ```typescript import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry"; const AGENT_ID = "maintenance-agent"; const MAINTAINER_SCOPES = [ { channel: "discord", accountId: "operations", senderIds: new Set(["maintainer-user-id"]), roleIds: new Set(["maintainer-role-id"]), }, ]; const MAINTAINER_TOOLS = new Set(["read", "web_fetch", "web_search", "session_status", "message"]); const MAINTAINER_MESSAGE_ACTIONS = new Set(["react", "reply", "thread-create", "thread-reply"]); export default definePluginEntry({ id: "maintenance-access", name: "Maintenance access", description: "Apply sender-aware tool policy to the maintenance agent.", register(api) { api.on("before_tool_call", (event, ctx) => { if (ctx.agentId !== AGENT_ID) { return; } const requester = ctx.requester; if (requester?.senderIsOwner === true) { return; } const maintainerScope = requester ? MAINTAINER_SCOPES.find( (scope) => scope.channel === requester.channel && scope.accountId === requester.accountId, ) : undefined; const isMaintainer = maintainerScope !== undefined && ((requester?.senderId !== undefined && maintainerScope.senderIds.has(requester.senderId)) || requester?.roleIds?.some((roleId) => maintainerScope.roleIds.has(roleId)) === true); if (!isMaintainer) { return { block: true, blockReason: "Maintainer access required." }; } if (event.toolName === "message") { const action = typeof event.params.action === "string" ? event.params.action : ""; if (MAINTAINER_MESSAGE_ACTIONS.has(action)) { return; } return { block: true, blockReason: `Owner required for message.${action || "unknown"}.` }; } if (MAINTAINER_TOOLS.has(event.toolName)) { return; } return { block: true, blockReason: `Owner required for ${event.toolName}.` }; }); api.registerCommand({ name: "fix", description: "Ask the maintenance agent to investigate and fix an issue.", acceptsArgs: true, requireAuth: true, handler: async (ctx) => ctx.agentId === AGENT_ID ? { continueAgent: true } : { text: "This command is only available in the maintenance conversation." }, }); }, }); ``` 将文件添加到`plugins.load.paths`并启用;默认混合重新加载模式应用更改。独立文件使用其文件名作为插件 ID(`maintenance-access.ts` 是 `maintenance-access`),因此请使其等于传递给 `definePluginEntry` 的 `id`: ```json5 { agents: { entries: { "maintenance-agent": { workspace: "~/.openclaw/workspace-maintenance", }, }, }, bindings: [ { agentId: "maintenance-agent", match: { channel: "discord", accountId: "operations", peer: { kind: "channel", id: "maintenance-channel-id" }, }, }, ], plugins: { load: { paths: ["~/.openclaw/policies/maintenance-access.ts"] }, entries: { "maintenance-access": { enabled: true } }, }, } ``` `AGENT_ID` 必须命名绑定到维护会话的代理。绑定选择正常消息和 `/fix` 的代理;独立文件仍然是所有者与维护者工具策略的单一所有者。编辑文件本身后,运行 `openclaw plugins reload maintenance-access`。 `requireAuth: true` 重用每个通道的现有发送者准入。对于 Discord,公会或频道 `users`/`roles` 白名单可以授权维护受众。其他渠道可以使用稳定的发件人 ID。然后,该挂钩将更精细的每个工具决策应用于运行中的每个工具调用,包括 Codex 本机 `PreToolUse` 调用。它可以否决模型看到的工具,但不能添加主机省略的工具。现有的沙箱、执行审批、仅限所有者的核心工具和渠道政策仍然适用;钩子无法通过它们。 将发送者和角色 ID 范围限定为精确的 channel/account 对,如图所示;两者都是提供者本地命名空间。保持许可名单保守。仅当部署的沙箱和批准策略确保安全时才添加写入或执行工具。对于自动化或系统运行,明确决定是否应通过不存在的 `ctx.requester`;该示例拒绝为作用域代理进行此操作。 参见[插件权限请求](/plugins/plugin-permission-requests),了解审批路由、决策行为,以及何时应使用 `requireApproval` 而不是可选工具或 exec 审批。 需要宿主级策略的插件可以通过 `api.registerTrustedToolPolicy(...)` 注册受信任的工具策略。这些策略会在普通的 `before_tool_call` 钩子之前以及正常钩子决策之前运行。捆绑的受信任策略最先运行;已安装插件的受信任策略随后按插件加载顺序运行;普通的 `before_tool_call` 钩子在它们之后运行。捆绑插件保留现有的受信任策略路径。已安装插件必须显式启用,并在 `contracts.trustedToolPolicies` 中声明每个策略 id;未声明的 id 会在注册前被拒绝。策略 id 仅在注册该策略的插件范围内有效,因此不同插件可以重用相同的本地 id。仅在工作区策略、预算执行或保留工作流安全等宿主信任的门控场景中使用这一层。 可信策略可以将 `matcher` 设置为 `before_tool_call` 接受的相同标准工具 ID 列表。省略匹配器以保留全部匹配行为。 当可信策略批准内联完成时,普通 `before_tool_call` 挂钩会收到已批准参数的独立副本。参数未更改的调用仍然被允许,但要受到普通否决和稍后的执行检查。转换这些参数的普通挂钩必须返回 `requireApproval` 及其最终的 `params`,以便替换得到单独的批准。如果没有该批准请求,更改后的呼叫将被 `Tool call parameters changed after trusted approval` 阻止。 当升级将可信批准与普通参数重写相结合的插件时,请更新转换钩子以请求单独的批准。第一次批准并不授权替换参数。此内联边界不会改变上述较窄的本机中继合约。 ### 工具调用观察 `after_tool_call` 观察到已运行的工具调用。它是 `before_tool_call` 的另一半,用于记录门控调用所做的事情。它接收与`before_tool_call`(`PluginHookToolContext`)相同的上下文类型;可用的上下文字段取决于发射harness。其事件包含: - `event.toolName`:工具名称。 - `event.params`:工具参数,包括 `before_tool_call` 的调整。 - 可选 `event.runId`:所属运行标识符。 - 可选 `event.toolCallId`:工具调用标识符。 - 可选 `event.result`:harness提供的工具结果。 - 可选 `event.error`:harness提供或提取的错误消息。 - 可选 `event.durationMs`:已知开始时间时经过的毫秒数。 ```typescript type AfterToolCallEvent = { toolName: string; params: Record; runId?: string; toolCallId?: string; result?: unknown; error?: string; durationMs?: number; }; ``` 它是一个观察钩子,所以它不能改变已经发生的事情: - Handlers 并发运行,并且它们的返回值被忽略。 - A 抛出或超时的处理程序被记录,并继续执行。 - `matcher` 接受与 `before_tool_call` 相同的标准工具 ID 列表。 `result` 和 `error` 都是可选的,因此两者都不包含的事件并不能证明该工具成功。通过缺少 `error` 来推断成功的处理程序可以将失败的调用记录为成功的调用;将不可读的结果视为未知,而不是任一结果。 `result` 为 exec 和 bash 系列工具携带的内容在 [#102961](https://github.com/openclaw/openclaw/issues/102961) 中单独跟踪。 不要使用此挂钩来执行策略。正如 [hook 参考文献 ](/plugins/hooks/reference) 所说,使用一个在错误时阻止操作的门,而不是假设观察或传递钩子会在失败时拒绝操作。 ### Exec 环境钩子 `resolve_exec_env` 允许插件在命令运行之前向 OpenClaw `exec` 工具调用提供环境变量。它并不是适用于所有harness-native shell 的钩子。它接收: - `event.sessionKey` - `event.toolName`,始终为 `"exec"` - `event.host`,取值为 `"gateway"`、`"sandbox"` 或 `"node"` - 上下文字段,例如`ctx.agentId`、`ctx.sessionKey`、`ctx.sessionId`、`ctx.messageProvider`和`ctx.channelId` 返回一个 `Record` 以合并到 exec 环境中。处理器按优先级顺序运行;后面的结果会覆盖较早结果中相同的键。 在合并之前,钩子输出通过主机执行环境密钥策略进行过滤。 `PATH` 始终被丢弃(命令解析和安全箱检查取决于它)。无效密钥和危险主机覆盖密钥,例如 `LD_*`、`DYLD_*`、`NODE_OPTIONS`、代理变量(`HTTP_PROXY`、`HTTPS_PROXY`、`ALL_PROXY`、`NO_PROXY`)和 TLS 覆盖变量(`NODE_TLS_REJECT_UNAUTHORIZED`、`SSL_CERT_FILE` 和类似)已被删除。过滤后的插件环境包含在网关 approval/audit 元数据中,并转发到节点主机执行请求。 ### 工具结果持久化 `tool_result_persist`和`before_message_write`是同步钩子。不要让他们的处理程序 `async`:返回的承诺将被忽略并带有警告。每个处理程序都会接收前一个处理程序返回的消息。 `tool_result_persist` 返回 `{ message }` 替换工具结果; `before_message_write` 可以返回 `{ message }` 或 `{ block: true }` 以防止转录写入。阻止持久性并不是工具执行否决权。 这些挂钩对 OpenClaw- 拥有的转录本写入进行操作。它们不会重写 Codex-native 工具记录;请参阅 [Codex 转录本边界 ](/plugins/codex-harness-runtime#compaction-and-transcript-mirror)。 工具结果可以包含用于 UI 渲染、诊断、媒体路由或插件自有元数据的结构化 `details`。请将 `details` 视为运行时元数据,而不是提示词内容: - OpenClaw 会在提供方回放和压缩输入之前剥离 `toolResult.details`,因此元数据不会成为模型上下文。 - 持久化的会话条目只保留有界的 `details`。过大的 details 会被替换为紧凑摘要和 `persistedDetailsTruncated: true`。 - `tool_result_persist` 和 `before_message_write` 在最终持久化上限之前运行。请保持返回的 `details` 较小,并避免只把与提示相关的文本放在 `details` 中;应将模型可见的工具输出放在 `content` 中。