跳转至

工具策略

工具端钩子:参数重写、阻止、审批、exec 环境贡献以及转录持久化。属于 插件钩子 指南的一部分。

工具调用策略

before_tool_call 接收:

  • event.toolName
  • event.params
  • 可选的 event.toolKind 和 event.toolInputKind,是宿主权威的判别器,用于有意共享名称的工具;例如,外层 code-mode exec 调用使用 toolKind: "code_mode_exec",并对接受的 Code Mode 输入包含 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。缺失字段表示未证明,而不是虚假保证;当策略要求这些字段时,应失败关闭。

它可以返回:

type BeforeToolCallResult = {
  params?: Record<string, unknown>;
  block?: boolean;
  blockReason?: string;
  requireApproval?: {
    title: string;
    description: string;
    scope?: ApprovalScope;
    severity?: "info" | "warning" | "critical";
    timeoutMs?: number;
    /** @deprecated Unresolved approvals always deny. */
    timeoutBehavior?: "allow" | "deny";
    allowedDecisions?: Array<"allow-once" | "allow-always" | "deny">;
    pluginId?: string;
    onResolution?: (
      decision: "allow-once" | "allow-always" | "deny" | "timeout" | "cancelled",
    ) => Promise<void> | void;
  };
};

类型化生命周期钩子的守卫行为:

  • block: true 是终止性的,并跳过较低优先级的处理器。
  • block: false 被视为无决定。
  • 返回 params 以重写宿主拥有的工具参数。每个处理器看到的是原始事件的隔离副本,而不是先前返回的重写。在请求审批之前,最后返回的 params 生效。
  • 第一个 requireApproval 生效,其插件 id 由宿主盖章。它会冻结所选参数快照:后续处理器可以阻止,但不能更改已批准的参数。
  • 原生工具中继可能有更窄的契约。Codex 原生工具支持阻止和观察,但参数重写会被拒绝;参见 Codex 钩子边界。
  • requireApproval 会暂停代理运行,并通过插件审批询问用户。/approve 可以同时批准 exec 和插件审批。在 Codex app-server report-mode 原生 PreToolUse 中继中,这会委托给匹配的 app-server 审批请求;参见 Codex harness 运行时。
  • 较低优先级的 block: true 仍可在较高优先级钩子请求审批后阻止。
  • onResolution 接收已解决的决策:allow-once、allow-always、deny、timeout 或 cancelled。

例如,在 register(api) 内添加以下内容,以在宿主拥有的 exec 调用前询问。before_tool_call 不需要会话访问选择加入:

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:

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;默认的混合重载模式会应用该更改:

{
  agents: {
    entries: {
      "maintenance-agent": {
        default: true,
        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"] },
  },
}

AGENT_ID 必须指定绑定到维护会话的代理。该绑定会为普通消息和 /fix 选择该代理;独立文件仍然是所有者与维护者工具策略的唯一持有者。编辑文件本身后,运行 openclaw plugins reload maintenance-access。

requireAuth: true 会复用各频道现有的发送者准入机制。对于 Discord,服务器或频道的 users/roles 白名单可以授权维护受众。其他频道可以使用稳定的发送者 ID。随后,钩子会在运行中的每次工具调用上应用更细粒度的按工具决策,包括 Codex 原生的 PreToolUse 调用。它可以否决模型可见的工具,但无法添加宿主未提供的工具。现有的沙箱、exec 审批、仅限所有者的核心工具和频道策略仍然适用;钩子不能超越它们授权。

如示例所示,将发送者和角色 ID 限定到精确的频道/账户对;两者都是提供者本地命名空间。请保持白名单保守。仅在部署的沙箱和审批策略确保安全时,才添加写入或执行工具。对于自动化或系统运行,请明确决定当 ctx.requester 缺失时是否应让其通过;示例中对限定的代理予以拒绝。

有关审批路由、决策行为以及何时使用 requireApproval 而非可选工具或 exec 审批,请参阅 插件权限请求。

需要宿主级策略的插件可以通过 api.registerTrustedToolPolicy(...) 注册受信任的工具策略。这些策略在普通的 before_tool_call 钩子之前以及正常的钩子决策之前运行。内置的受信任策略首先运行;已安装插件的受信任策略随后按插件加载顺序运行;普通的 before_tool_call 钩子在其之后运行。内置插件保留现有的受信任策略路径。已安装插件必须被显式启用,并在 contracts.trustedToolPolicies 中声明每个策略 ID;未声明的 ID 会在注册前被拒绝。策略 ID 限定在注册插件的作用域内,因此不同插件可以复用相同的本地 ID。仅将这一层用于宿主可信的门控,例如工作区策略、预算强制执行或预留工作流安全。

受信任策略可以将 matcher 设置为 before_tool_call 接受的同一规范化工具 ID 列表。省略 matcher 以保留全匹配行为。

Exec 环境钩子

resolve_exec_env 允许插件在命令运行前向 OpenClaw exec 工具调用贡献环境变量。它不是针对所有宿主原生 shell 的钩子。它接收:

  • event.sessionKey
  • event.toolName,始终为 "exec"
  • event.host,取值为 "gateway"、"sandbox" 或 "node"
  • 上下文字段,如 ctx.agentId、ctx.sessionKey、ctx.sessionId、ctx.messageProvider 和 ctx.channelId

返回一个 Record<string, string> 以合并到 exec 环境中。处理程序按优先级顺序运行;对于相同的键,后面的结果会覆盖前面的结果。

钩子输出在合并前会经过宿主 exec 环境键策略的过滤。PATH 始终被丢弃(命令解析和安全 bin 检查依赖于它)。无效键和危险的宿主覆盖键(如 LD_*、DYLD_*、NODE_OPTIONS、代理变量(HTTP_PROXY、HTTPS_PROXY、ALL_PROXY、NO_PROXY)以及 TLS 覆盖变量(NODE_TLS_REJECT_UNAUTHORIZED、SSL_CERT_FILE 等))会被丢弃。过滤后的插件环境变量会包含在 Gateway 审批/审计元数据中,并转发到节点宿主执行请求。

工具结果持久化

tool_result_persist 和 before_message_write 是同步钩子。不要将它们的处理程序设为 async:返回的 promise 会被忽略并发出警告。每个处理程序都会接收前一个处理程序返回的消息。tool_result_persist 返回 { message } 以替换工具结果;before_message_write 可以返回 { message } 或 { block: true } 来阻止该记录写入。阻止持久化不是对工具执行的否决。

这些钩子作用于 OpenClaw 拥有的记录写入。它们不会重写 Codex 原生的工具记录;请参阅 Codex 记录边界。

工具结果可以包含结构化的 details,用于 UI 渲染、诊断、媒体路由或插件拥有的元数据。请将 details 视为运行时元数据,而非提示词内容:

  • OpenClaw 会在提供者重放和压缩输入之前剥离 toolResult.details,从而使元数据不会成为模型上下文。
  • 持久化的会话条目仅保留有界 details。过大的 details 会被替换为紧凑摘要,并标记 persistedDetailsTruncated: true。
  • tool_result_persist 和 before_message_write 在最终持久化上限之前运行。保持返回的 details 较小,避免仅将提示词相关文本放在 details 中;将模型可见的工具输出放在 content 中。

本页原文 Markdown:在 AtomGit 查看·内容源自开源项目 cl/openclaw