OpenClaw 设计与实现

第10章 工具系统

作者 杨艺韬 · 14,355 字 · 发布于 · 更新于

第10章 工具系统

工具安全的最难问题不是'如何阻止恶意调用',而是'如何区分一次危险但正确的操作和一次安全但错误的操作'——前者应该放行,后者应该拦截,而 LLM 给你的信号往往不足以可靠区分两者。

本章要点

  • 理解 Agent 工具的核心悖论:能力越强,风险越大
  • 掌握三层工具抽象与七层策略防御管线
  • 深入浏览器自动化、进程执行、Web 搜索等核心工具实现
  • 理解工具系统与上下文引擎的协同关系

前九章,我们构建了 OpenClaw 的完整骨架:Gateway 调度全局、Provider 连接模型、Session 管理记忆、Agent 编排决策、通道对接用户、插件开放生态。骨架已成,但 Agent 仍然只是一个"能说会道的嘴"——它能生成文字,却无法触碰真实世界。

工具系统,就是给 Agent 装上的双手。

10.1 Agent 工具的核心悖论

什么区分了 AI Agent 和聊天机器人?答案看似简单:行动的能力。聊天机器人生成"我可以帮你搜索"的文本;Agent 真正打开浏览器、输入查询、返回结果。一个是纸上谈兵,一个是真刀真枪。

但这种简单性掩盖了一个困扰所有 Agent 框架的设计难题:如何让 AI 安全地、可扩展地、智能地调用系统能力?

考虑最朴素的方案:将每个操作系统能力暴露为 LLM 可调用的函数。这在演示中行得通,但在生产环境中必然崩溃。没有权限控制,Agent 可能因误解指令而执行 rm -rf /;没有路由智能,用户要搜索它却去生成图片;没有并发管理,并行工具调用耗尽系统资源。能力越大,责任越大——也越需要精密的管控。

好的工具系统不是给 Agent 更多的刀——而是一套完整的刀具管理体系:哪些刀放在台面上,哪些锁在柜子里,谁有钥匙,什么情况下可以用。

🔥 深度洞察:工具是 Agent 的"宪法修正案"

从政治学的视角看工具系统,你会获得一个全新的理解。LLM 的基础能力(文本生成)就像宪法原文——庄严但抽象。每一个工具的添加,就像一条宪法修正案——它赋予 Agent 新的权力(执行命令、浏览网页、操作设备),但同时必须有对应的约束条款。OpenClaw 的工具系统遵循同样的逻辑:exec 工具赋予 Agent 执行 Shell 命令的能力(强大的"武器"),但七层策略管线确保这一能力在严格约束下行使。没有约束的能力不是能力,是隐患。

10.1.1 如果没有工具系统会怎样?

让我们做一个思想实验。假设 OpenClaw 没有专门的工具系统,而是像早期的 LangChain 那样,让开发者自己将函数注册到 LLM 的工具列表中。会发生什么?

第一个问题是安全真空。 没有统一的策略层,每个工具自行决定"谁能调用我"。浏览器工具可能有 URL 白名单,但 exec 工具完全不设防。安全变成了一个个孤岛——有的工具很安全,有的工具裸奔,整体安全性取决于最弱的那一环。

第二个问题是配置爆炸。 运营者需要为每个工具单独配置权限、超时、资源限制。10 个工具需要 10 套配置。50 个工具需要 50 套。配置的复杂度与工具数量线性增长,维护成本很快超过工具本身的价值。

第三个问题是上下文浪费。 没有统一的输出格式控制,每个工具返回自己认为合理的格式——浏览器返回完整 HTML(50K token),搜索返回原始 JSON(30K token),一次工具调用就可能耗尽整个上下文窗口。Agent 变成了只能使用一次工具的"一次性工人"。

OpenClaw 的工具系统正是对这三个问题的系统性回应。它不是简单的函数注册表,而是一个完整的工具治理框架——在安全、配置和效率三个维度上提供统一的解决方案。

10.1.2 工具系统的设计目标

在深入实现之前,让我们明确工具系统要解决的核心需求:

  1. 安全隔离:不同利益相关者(平台运营者、Agent 开发者、模型提供商)能独立控制工具访问,且安全策略可组合。
  2. 上下文效率:工具的输出必须经过压缩和格式化,使之适合有限的上下文窗口。
  3. 配置简洁:运营者能用一行配置(如 tools.profile: coding)获得一组合理的工具默认值。
  4. 扩展透明:添加新工具不需要修改安全系统;修改安全策略不需要理解工具实现。
  5. 优雅降级:工具不可用时(如浏览器未安装),系统平滑降级而非崩溃。

这五个目标之间存在张力。例如,安全隔离要求细粒度控制,但配置简洁要求粗粒度抽象。工具系统的架构设计本质上是在这些张力之间寻找平衡。

关键概念:工具策略管线(Tool Policy Pipeline) 工具策略管线是 OpenClaw 工具安全的核心机制——在任何工具进入 LLM 的可用工具列表之前,它必须通过默认七个独立的过滤阶段(配置文件、提供商、全局、Agent 级、群组等;调用处还会追加沙箱级与子代理级两步,Owner-Only 等过滤则在进入管线之前完成)。每个阶段可以独立允许或拒绝工具,且修改安全策略不需要修改工具实现,反之亦然。

10.2 工具架构:三层抽象

工具系统不是一个扁平的函数列表。它在三个层次上运作,每层解决不同类别的问题。

第一层:工具目录(src/agents/tool-catalog.ts)。核心工具的主注册表(插件注册的工具不在其中)——文件操作(read、write、edit)、运行时操作(exec、process)、Web 操作(web_search、web_fetch)、界面操作(browser、canvas)、媒体操作(image、image_generate、tts)和会话操作(sessions_spawn、sessions_yield)。每个工具属于一个分区(section),并声明参与哪些配置文件(profile)。

第二层:工具策略管线(src/agents/tool-policy-pipeline.ts)。安全性的核心。在任何工具进入 LLM 的意识之前,它通过多达七个独立过滤阶段——配置文件策略、提供商策略、全局策略、Agent 级策略和群组策略(策略管线的示意见第 6 章图 6-3,安全模型详见第 13 章)。每个阶段要么不表态(policy 为空则整段跳过),要么对当前工具集做一次过滤。

第三层:工具执行(散落在 src/ 与 extensions/ 下)。每个工具的实际实现——浏览器自动化在 extensions/browser/(作为插件分发,不在核心 src/ 里),exec / process 工具在 src/agents/bash-tools.exec.ts / src/agents/bash-tools.process.ts(底层进程派生在 src/process/),网页搜索工具在 src/agents/tools/web-search.ts(提供商选择在 src/web-search/runtime.ts,各搜索提供商由插件注册)。

graph TB
    subgraph "图 10-1:工具系统三层架构"
        Catalog["第一层:工具目录<br/>有哪些工具?"]
        Pipeline["第二层:策略管线<br/>允许哪些工具?"]
        Execution["第三层:工具执行<br/>每个工具怎么工作?"]
    end
    Catalog --> Pipeline --> Execution
    style Catalog fill:#1f618d,color:#fff
    style Pipeline fill:#c0392b,color:#fff
    style Execution fill:#1e8449,color:#fff

这种分离至关重要。目录是静态且全面的。管线是动态且安全感知的。执行层是模块化且可独立测试的。改变安全策略永远不需要修改工具实现,添加新工具永远不需要理解策略系统。

⚠️ 注意:tools.exec.security 的默认值有一处注释与实现打架,值得单独记一笔。src/config/types.tools.ts 上写的是 /** Exec security mode (default: deny). */,但真正解析它的 src/agents/bash-tools.exec.ts 是 defaults?.security ?? (host === "sandbox" ? "deny" : "allowlist")——未显式配置时按 exec 实际落地的主机分流:沙箱主机取 deny,网关/节点主机取 allowlist(src/node-host/invoke.ts 的 resolveExecSecurity 对无法识别的值也回落 allowlist)。也就是说开箱即用状态下并不是"一条命令都跑不了",而是"没有沙箱时按白名单(审批白名单条目与 SafeBins)放行"。要真正关死就显式写 "deny",要全放开才写 "full"——在面向公众的 Agent 上用 "full" 是严重的安全风险。与之配套的 tools.exec.ask 默认是 "on-miss"——未命中白名单时向用户征询。

10.2.1 为什么是三层而不是两层?

一个自然的疑问:为什么不把目录和策略合并为一层?答案在于关注点的独立演化速度。

工具目录的变更频率较低——当 OpenClaw 添加新的核心工具(如 canvas、image_generate)时才变化,这可能是几周一次。策略管线的变更频率中等——运营者调整安全配置时变化,可能是每天。工具执行的变更频率最高——Bug 修复、性能优化、API 适配,几乎每次提交都涉及。

如果目录和策略耦合,每次添加新工具都要重新审视安全策略的组合逻辑。如果策略和执行耦合,每次修改浏览器自动化的细节都可能意外改变安全行为。三层分离让每一层按自己的节奏演化,互不干扰。

10.2.2 工具目录的数据模型

让我们看看工具目录的核心数据结构(src/agents/tool-catalog.ts):

// 工具的核心定义——每个工具是一个静态声明
type CoreToolDefinition = {
  id: string;              // 工具唯一标识
  label: string;           // 显示名称
  description: string;     // 功能描述
  sectionId: string;       // 所属分区(fs/runtime/web/media/...)
  profiles: ToolProfileId[];  // 属于哪些配置文件
  includeInOpenClawGroup?: boolean;  // 是否归入 OpenClaw 工具组
};

这个定义有几个微妙的设计决策:

sectionId 的分区设计。11 个分区(fs、runtime、web、memory、sessions、ui、messaging、automation、nodes、agents、media)不是按技术实现分的,而是按用户心理模型分的。运营者不会想"我要允许使用 Playwright 的工具",而是"我要允许 Web 相关的工具"。分区对齐了运营者的思维方式,让配置更直觉。

profiles 的多重归属。字段类型是数组,一个工具原则上可以属于多个配置文件。不过在 v2026.3.31 上,实际登记的 29 个工具里多数只写了 ["coding"],少数同时属于多档(如 sessions_send 为 ["coding", "messaging"]、session_status 三档都有),还有 6 个写 [](如 browser、tts);没有任何工具写 "full"——因为 full 根本不需要登记:CORE_TOOL_PROFILES 里 full: {} 是个空策略,resolveCoreToolProfilePolicy() 遇到既无 allow 又无 deny 的条目直接返回 undefined,于是这一阶段被整个跳过,等价于"不设限"。换句话说,full 不是"包含所有工具的清单",而是"没有清单"——这是一个很值得学的建模技巧:用"缺席"表达"无限制",比维护一张必须随新工具同步更新的全量列表可靠得多。

includeInOpenClawGroup 的组简写成员标识。这个布尔值决定工具是否归入 group:openclaw 这个组简写(buildCoreToolGroupMap() 据此生成;另外每个分区自动生成 group:<sectionId>,如 group:fs)。v2026.3.31 上,除 read、write、edit、apply_patch、exec、process 这六个基础文件/运行时工具外,其余核心工具都标了 true。它与下文 10.3.4 的"仅含插件工具的 allowlist 会被剥离"无关——那段逻辑判断"核心工具"的依据是运行时工具有没有插件元数据,而不是这个字段。

10.3 策略管线:七层防御

如果你只能理解整个工具系统的一个机制,那应该是策略管线。这是 OpenClaw 安全态势的执行点——它的设计揭示了关于 Agent 安全的深刻洞察。

10.3.1 问题的本质

不同利益相关者需要不同级别的工具控制。平台运营者想全局禁止 sessions_spawn(防止远程代码执行)。Agent 配置想将某个 Agent 限制为只读工具(数据分析场景)。模型提供商策略想根据模型能力限制工具(某些模型不支持工具调用)。群组管理员想在特定 Discord 群里禁用 exec(安全考虑)。

传统的做法是把所有这些规则塞进一个巨大的 if-else 树。但这种方案有致命缺陷:规则之间会产生意外的交互效应。当运营者的全局禁止和 Agent 的局部允许冲突时,谁赢?当模型提供商策略和群组策略都对同一个工具有意见时,优先级怎么定?

10.3.2 管线方案

OpenClaw 的回答是:独立过滤阶段的有序管线,每个阶段有自己的作用域和真实来源:

// src/agents/tool-policy-pipeline.ts:37(简化)
const stages = [
  { policy: profilePolicy,         label: "tools.profile" },
  { policy: providerProfilePolicy, label: "tools.byProvider.profile" },
  { policy: globalPolicy,          label: "tools.allow" },
  { policy: globalProviderPolicy,  label: "tools.byProvider.allow" },
  { policy: agentPolicy,           label: "agents.X.tools.allow" },
  { policy: agentProviderPolicy,   label: "agents.X.tools.byProvider.allow" },
  { policy: groupPolicy,           label: "group tools.allow" },
];

管线的执行语义简洁但强大,核心是这一行:

// src/agents/tool-policy-pipeline.ts:109 — applyToolPolicyPipeline 的循环体
let filtered = params.tools;
for (const step of params.steps) {
  if (!step.policy) continue;              // 该阶段没配策略 → 整段跳过
  // ...(allowlist 归一化、插件组展开)
  filtered = expanded ? filterToolsByPolicy(filtered, expanded) : filtered;
}

注意 filtered 是被逐级收窄的——每个阶段过滤的是上一阶段的输出,而不是原始工具集。这带来一个关键性质:管线是单调递减的,后面的阶段永远无法"救回"前面被移除的工具。所以"某个阶段显式 allow 了 X"并不能让 X 复活,它只能表达"在我这一层不排除 X"。理解这一点,就能解释很多"我明明 allow 了为什么还是没有"的困惑——去往前找那个把它筛掉的阶段,而不是在当前层加配置。

补充一点口径:上面这七步来自 buildDefaultToolPolicyPipelineSteps(),是默认管线。真正的调用处 src/agents/pi-tools.ts:611-632 在其后又追加了两步——sandbox tools.allow 与 subagent tools.allow,所以一次完整调用实际会走 9 步(第 17 章按这个口径写作「7 级(调用处 9 级)」)。此外 applyMessageProviderToolPolicy()、applyModelProviderToolPolicy() 与 applyOwnerOnlyToolPolicy() 在进入管线之前就已经依次过滤过,它们不算在 steps 里。

通过全部阶段的工具才对 LLM 可用。在任何阶段被阻止的工具从 LLM 的工具目录中移除——模型甚至不知道它的存在。

这最后一点至关重要。被策略拒绝的工具不是"显示为不可用",而是从 LLM 的世界中彻底消失。LLM 不能请求使用它不知道存在的工具,这从根本上消除了模型试图绕过工具限制的可能性。

10.3.3 为什么是七层而不是三层?

七层看起来过度设计了?让我们通过一个具体场景理解为什么每一层都不可或缺。

想象这个场景:运营者部署了一个 OpenClaw 实例,同时服务于三个 Agent——coder(编码助手)、researcher(研究助手)和 support(客户支持)。

  • 配置文件层:coder 使用 coding 配置文件(包含 exec、process、文件读写等),support 使用 messaging 配置文件(只有 message 与 sessions_list、sessions_history、sessions_send、session_status 几个会话工具)。这一层决定了每个 Agent 的"能力天花板"。
  • 提供商配置文件层:researcher 使用的某个模型工具调用能力较弱,用 tools.byProvider 给它换一个更窄的 profile,少发工具定义。
  • 全局策略层:运营者全局禁止 sessions_spawn,不让任何 Agent 自行拉起子 Agent。这一层实施"绝对不可逾越的红线"。
  • 全局提供商策略层:对所有使用某家提供商的 Agent,禁止 browser(例如出于合规考虑)。
  • Agent 策略层:coder 用不到 cron、message,就在 Agent 级把它们去掉。这一层实施每个 Agent 的最小权限原则(注意策略管线只按工具名过滤,限定"只能操作哪些目录"不归它管)。
  • Agent 提供商策略层:coder 在使用低端模型时禁止 process(防止长时间运行的命令浪费 token)。
  • 群组策略层:在 Discord 的 #general 频道中禁用 exec(公共频道安全考虑),但在 #dev 频道中允许。

七层,每一层回答一个独立的问题,没有任何两层的关注点完全重叠。这就是为什么不能简化到三层——那样必然会把不同关注点混在一起,导致配置变得混乱。

10.3.4 未知 Allowlist 条目的智能处理

管线中有一个容易被忽视但极其重要的细节:当 allowlist 包含未知的工具名时会怎样?

// src/agents/tool-policy-pipeline.ts:117 — 未知 allowlist 处理
const resolved = stripPluginOnlyAllowlist(policy, pluginGroups, coreToolNames);
if (resolved.unknownAllowlist.length > 0) {
  // 区分两种情况:
  // 1. 已知核心工具但当前不可用(受限于运行时/提供商/模型/配置)
  // 2. 完全未知的条目(可能是拼写错误或未启用的插件)
  const gatedCoreEntries = resolved.unknownAllowlist.filter(isKnownCoreToolId);
  const otherEntries = resolved.unknownAllowlist.filter((entry) => !isKnownCoreToolId(entry));
  // ...(据此拼接告警文案)
}

如果 allowlist 中只包含插件工具名(没有任何核心工具),系统会剥离整个 allowlist。为什么?因为这种情况通常意味着运营者是在为插件配置允许列表,而不是想禁止所有核心工具。如果不做这个处理,一个 tools.allow: [my_custom_tool] 的配置会意外地禁止 read、write、exec 等所有核心工具——这几乎肯定不是运营者的意图。

这种"猜测运营者意图"的启发式处理,是安全系统中罕见的"宁可放行一些、不要误杀关键功能"的设计取舍。它反映了一个务实的认知:安全系统如果太难用,运营者会绕过它,最终导致更差的安全态势。

10.3.5 HTTP 工具拒绝列表

src/security/dangerous-tools.ts 中的默认 HTTP 工具拒绝列表特别值得深入分析:

// src/security/dangerous-tools.ts:9 —— v2026.3.31 全表(注释为源码原注释的中译)
export const DEFAULT_GATEWAY_HTTP_TOOL_DENY = [
  "exec",            // 直接命令执行 —— 即时 RCE 面
  "spawn",           // 任意子进程创建 —— 即时 RCE 面
  "shell",           // Shell 命令执行 —— 即时 RCE 面
  "fs_write",        // 宿主机上的任意文件写入
  "fs_delete",       // 宿主机上的任意文件删除
  "fs_move",         // 宿主机上的任意文件移动/改名
  "apply_patch",     // 打补丁可改写任意文件
  "sessions_spawn",  // 会话编排 —— 远程生成 Agent 等于 RCE
  "sessions_send",   // 跨会话注入
  "cron",            // 持久化自动化控制面板
  "gateway",         // 网关控制面 —— 阻止经 HTTP 重配网关
  "nodes",           // 节点命令中继可触达配对主机的 system.run
  "whatsapp_login",  // 交互式安装 —— 需终端扫码,在 HTTP 上会挂住
] as const;

这份 13 项的名单不是按"危险程度"排的,而是按威胁类别聚成了四组:

  1. 即时 RCE 面(exec / spawn / shell)——一次调用就是一次任意代码执行。
  2. 宿主机文件系统改写(fs_write / fs_delete / fs_move / apply_patch)——不执行代码也能毁掉宿主。
  3. 超出当前会话的持久化与放大(sessions_spawn / sessions_send / cron / gateway / nodes)——效果活得比这次会话更久:sessions_spawn 远程拉起新 Agent、sessions_send 横向注入其他会话、cron 植入定时后门、gateway 改写安全策略本身、nodes 把命令中继到配对主机。
  4. 在 HTTP 语义下根本跑不通的交互式流程(whatsapp_login)——需要终端扫码,放在非交互的 HTTP 表面只会把请求挂死。

注意最后一类:它进入拒绝列表的理由不是安全,而是"这个动作在这个表面上没有意义"。一份好的拒绝列表往往混合了安全与可用性两种理由,读的时候别把它们混为一谈。

即使有有效认证,这些工具在 HTTP API 表面也被默认拒绝。当运营者通过 gateway.tools.allow 明确重新启用名单里的工具(例如 sessions_spawn)时,安全审计系统(src/security/audit.ts)根据网关暴露级别评估风险——网关只绑定回环地址时报 warn;绑定到回环以外或开启 Tailscale Funnel 时报 critical。

10.4 工具配置文件:能力包

10.4.1 配置文件的设计动机

不必逐个配置每个工具,OpenClaw 将工具分组为匹配常见用例的配置文件:

配置文件 包含的工具 典型用例
minimal 由 CORE_TOOL_DEFINITIONS 中 profiles 含 "minimal" 的工具组成(v2026.3.31 上只有 session_status) 受限环境
coding 文件系统、exec、process、web_search、web_fetch、会话、记忆、cron、image 等 22 个核心工具(不含 browser、canvas、gateway、nodes、message、tts 等) 开发工作流
messaging message 加 sessions_list、sessions_history、sessions_send、session_status 共 5 个 通信 Agent
full 空策略(full: {}),即不做任何过滤 不受限操作

(口径:各档成员由 listCoreToolIdsForProfile(profile) 在 src/agents/tool-catalog.ts 中从 CORE_TOOL_DEFINITIONS 动态筛出,v2026.3.31 共登记 29 个核心工具、11 个分区。)

配置文件既简化了配置(运营者设置 tools.profile: coding 而不是逐个列出 22 个工具),又作为文档(读取配置文件定义即可了解 Agent 的能力级别)。

💡 最佳实践:为不同场景的 Agent 选择合适的工具配置文件。面向公众的客服 Agent 使用 minimal;内部开发 Agent 使用 coding;个人助手使用 full。可以在配置文件基础上通过 tools.alsoAllow(追加)/ tools.deny(删减)进一步微调——tools.allow 只能在 profile 的结果上再收窄,加不回 profile 之外的工具。

10.4.2 替代方案的考量

设计这类能力分组时,至少有三种可选方案:

方案 A:纯 Allowlist/Denylist。运营者显式列出每个允许或拒绝的工具。优点:完全精确。缺点:随着工具数量增长,配置变得冗长且脆弱——新工具添加时,所有现有配置都需要更新。

方案 B:角色基础访问控制(RBAC)。定义"管理员"、"开发者"、"只读用户"等角色,每个角色有固定的工具集。优点:企业用户熟悉。缺点:角色是面向人的抽象,不适合面向Agent的场景——Agent 的能力需求由任务决定而非由身份决定。

方案 C:配置文件 + 叠加层(最终选择)。提供几个有名字的配置文件作为基线,再允许通过 allowlist/denylist 微调。优点:兼得便捷性(一行配置)和精确性(可微调)。缺点:增加了概念数量(配置文件 + 策略 + 管线)。

最终选择方案 C 的原因是它最符合 OpenClaw 的运营者画像:大多数情况下,coding 或 minimal 就够了;少数需要精细控制的场景,可以叠加策略层。大多数用户只需要一行配置,少数用户有完整的策略管线可用。

10.4.3 配置文件与策略的交互

配置文件不是策略管线之外的独立机制——它本身就是管线的第一阶段。这意味着:

  1. 配置文件设置的是最大可用工具集。生效的 profile 取 agents.<id>.tools.profile ?? tools.profile(Agent 级覆盖全局);alsoAllow 在这一阶段并入 profile 的允许列表;此外,只要显式写了 tools.exec 配置段就会隐式追加 exec、process,写了 tools.fs 就隐式追加 read、write、edit(src/agents/pi-tools.policy.ts 的 resolveEffectiveToolPolicy() / resolveImplicitProfileAlsoAllow())。
  2. 后续的策略阶段只能从中减少,不能增加。
  3. 如果配置文件(连同上述追加项)中没有 exec,即使 Agent 级策略显式 allow 了 exec 也无效。

这种"配置文件封顶、策略精简"的语义,确保了一个重要的安全属性:一旦某个 Agent 生效的 profile(连同 alsoAllow)确定,后续任何 allow 都不会让它的能力超出这个范围。这就像一个保险丝——它设定了该 Agent 的上限。但要注意,这个上限并非全局不可逾越:Agent 级 tools.profile 会覆盖全局设置,所以全局写 minimal 并不能约束一个自己声明了 coding 的 Agent。

10.5 浏览器自动化:最复杂的工具

浏览器工具(extensions/browser/,作为内建插件分发而非核心模块)基于 Playwright 构建。它是 OpenClaw 中最复杂的单一工具——不是因为"让 AI 控制浏览器"概念复杂,而是因为浏览器是通往整个互联网的门,安全、效率和可靠性的挑战在这里被极限放大。

10.5.1 浏览器自动化的设计哲学

浏览器自动化面临一个根本性的表示问题:如何将一个视觉化的、交互式的、状态丰富的网页,转化为 LLM 能够高效推理和操作的文本表示?

考虑三种方案:

方案 A:截图 + 视觉模型。将网页截图发送给视觉 LLM,让它基于图像理解进行操作。优点:最接近人类的浏览方式。缺点:每次截图都要消耗上千 token,且视觉模型对精确坐标的推理并不可靠,"点击搜索框"可能因为几像素的偏差而点到其他元素。

方案 B:完整 DOM/HTML。将网页的 HTML 源码发送给 LLM。优点:信息完整。缺点:一个典型网页的 HTML 有 50,000+ token,仅一次浏览就可能耗尽整个上下文窗口。而且 HTML 中充斥着样式、脚本、广告代码等 Agent 不需要的噪音。

方案 C:无障碍树(Accessibility Tree)。提取网页的无障碍语义结构——标题、链接、按钮、输入框——以紧凑的文本形式呈现。优点:相对完整 HTML 有一个数量级以上的体积压缩,且语义明确(每个元素的角色和用途都清晰标注)。缺点:丢失了视觉布局信息(元素的相对位置、颜色、大小)。

本节及下文出现的 token 数字("约 2K"、"50K+")是用于说明数量级的示意值,不是仓库里的实测基准——具体页面的压缩比差异极大,取决于页面结构与快照参数(maxDepth、是否启用紧凑模式)。仓库中没有提供官方的压缩比基准数据。

OpenClaw 选择了方案 C 作为主方案,方案 A 作为补充——需要视觉信息时可以请求截图。这个选择反映了一个核心洞察:Agent 浏览网页的目的是获取信息或执行操作,而不是欣赏设计。无障碍树恰好捕获了"信息"和"操作"两个维度,同时抛弃了"视觉"维度。

10.5.2 快照机制的工程实现

快照机制(extensions/browser/src/browser/pw-role-snapshot.ts)是浏览器工具最具创新性的部分。它将完整的 DOM 转化为紧凑的无障碍树表示:

- heading "Hacker News" [ref=e1]
- link "Show HN: My Project" [ref=e2]
- link "142 comments" [ref=e3]
- textbox "搜索..." [ref=e4]

系统为每个交互元素分配一个简短的 ref 标识符(如 e3)。Agent 基于这种紧凑表示推理并发出类似 click ref=e3 的动作。对于内容型页面,这通常能把量级从"整页 HTML 的数万 token"降到"无障碍树的数千 token",使浏览器自动化在上下文窗口约束内变得实用。

快照实现中有几个精妙的细节:

角色分类体系。源码(extensions/browser/src/browser/snapshot-roles.ts)将 ARIA 角色分为三类:STRUCTURAL_ROLES(list、group、table 等容器)、CONTENT_ROLES(heading、navigation、listitem、cell 等)和 INTERACTIVE_ROLES(button、link、textbox 等)。交互角色总会分到 ref,内容角色有名字时才分;紧凑模式(compact)会跳过没有名字的结构角色,只看交互元素时还可以用 interactive 选项,进一步压缩无关信息。

去重机制。当同一角色和名称出现多次时(如页面上有多个"Submit"按钮),RoleNameTracker 自动为它们分配 nth 索引以消除歧义。

深度限制。maxDepth 参数控制树的遍历深度——对于深层嵌套的 SPA 页面,限制深度可以在信息完整性和 token 开销之间取得平衡。

10.5.3 导航守卫

导航守卫(extensions/browser/src/browser/navigation-guard.ts)是浏览器安全的第一道防线。它在 Playwright 层面拦截每一次导航请求,检查:

  1. 协议验证:NETWORK_NAVIGATION_PROTOCOLS 只含 http: 与 https:;非网络协议里唯一放行的是 about:blank(SAFE_NON_NETWORK_URLS),file: / javascript: / data: 一律抛 InvalidBrowserNavigationUrlError。
  2. SSRF 防护:resolvePinnedHostnameWithPolicy()(src/infra/net/ssrf.ts)先按 SsrFPolicy.hostnameAllowlist 检查主机名,再解析主机名、判定目标是否落在私网/特殊用途地址段(127.0.0.1、192.168.x.x、10.x.x.x 等)。私网是否放行取决于 src/infra/net/ssrf.ts:67 的 isPrivateNetworkAllowedByPolicy()——策略里有 dangerouslyAllowPrivateNetwork 或 allowPrivateNetwork 为 true 时放行。要特别注意浏览器的默认值:extensions/browser/src/browser/config.ts 的 resolveBrowserSsrFPolicy() 在 browser.ssrfPolicy 没有显式写私网开关时,按"可信网络模式"补上 dangerouslyAllowPrivateNetwork: true(src/config/types.browser.ts 的注释同样写着 Default: true)。也就是说,浏览器导航开箱即可访问私网,想拦截内网目标必须显式把 browser.ssrfPolicy.dangerouslyAllowPrivateNetwork 设为 false。
  3. 代理环境下失败关闭:在严格模式(私网未放开)下,检测到 env 里配了代理变量就直接拒绝导航——因为浏览器的网络栈会在连接时走代理路由,导航前做的 DNS 绑定检查会被绕过。

需要澄清一点:extensions/browser/src/browser/url-pattern.ts 的 matchBrowserUrlPattern() 常被误当成安全过滤器,它实际只用在 routes/agent.act.ts 里"轮询等待当前 URL 匹配某个模式"这类就绪判断(以及 pw-tools-core.responses.ts 里按 URL 匹配响应)上,与鉴权无关。真正的域名收口是 browser.ssrfPolicy.hostnameAllowlist(支持精确主机名与 *.example.com 通配),默认不配置——不配时导航守卫的立场是:协议收口 + SSRF 策略判定,公网 URL 一律放行。

10.5.4 配置文件隔离

浏览器配置文件(extensions/browser/src/browser/profiles-service.ts 负责创建、列出、删除)是具名的浏览器实例,各有自己的用户数据目录与 CDP 端口,因而 Cookie、本地存储和会话状态彼此独立。工具调用不指定 profile 时,用的是 OpenClaw 自管的隔离浏览器(openclaw profile),与用户日常使用的浏览器分开;只有显式指定 profile="user" 才会连到用户已登录的本机浏览器(工具描述要求仅在需要现有登录态且用户在场时使用)。注意隔离的单位是 profile 而不是 Agent——多个 Agent 若要互不共享登录态,需要各自使用不同的 profile。

10.5.5 与其他框架的浏览器方案对比

维度 OpenClaw Anthropic Computer Use AutoGPT Browser Use
表示方式 无障碍树(pw-role-snapshot.ts) 截图 + 坐标 Selenium DOM 混合(视觉+DOM)
操作寻址 语义 ref(如 ref=e3) 像素坐标 CSS 选择器 混合
上下文成本量级 低 中(每次截图固定开销) 高(整页 DOM) 中
安全隔离 浏览器 profile 隔离(profiles-service.ts) 沙箱 依部署而定 依部署而定
导航守卫 协议收口 + SSRF 策略(navigation-guard.ts) 依部署而定 依部署而定 依部署而定

(本表的"OpenClaw"一列有仓库源码为据;其余三列基于各方案的公开设计定性描述,用于说明取舍方向,不代表精确的实测对比。)

三种表示各有其失败模式:截图方案更"自然",但坐标推理会因为几像素的偏差点错元素;完整 DOM 方案信息最全,但一个页面就可能吃掉整个上下文窗口;无障碍树用语义引用换掉了视觉布局信息——遇到"把红色那个按钮点掉"这类依赖视觉的指令就会失灵,这也正是 OpenClaw 保留截图作为补充手段的原因。

10.6 进程执行:受控的力量

exec 工具(实现在 src/agents/bash-tools.exec.ts,底层进程派生在 src/process/,配置类型在 src/config/types.tools.ts 的 ExecToolConfig)提供 Shell 命令执行。它是 Agent 最强大也最危险的能力——一条命令可以创建文件、安装软件、启动服务,也可以删除数据、暴露密钥或攻击网络。

10.6.1 三模式安全模型

tools.exec.security 的取值是 "deny" | "allowlist" | "full"。未显式配置时按 exec 落地的主机分流:沙箱主机取 "deny",网关/节点主机取 "allowlist"(见 10.2 的提醒)。不过这个 "deny" 并不意味着沙箱内 exec 默认被禁:src/agents/bash-tools.exec.ts 只把 security 传给网关与节点两条执行路径(:712-770),host=sandbox 时命令直接在容器里执行,不走档位与审批判定——沙箱这一侧的约束来自容器本身。

deny — 完全禁止命令执行。适用于纯对话 Agent(客户支持、内容生成);想稳定停在这一档必须显式写出来。

allowlist — 仅放行命中白名单的命令:审批白名单(exec-approvals.json 里按可执行文件路径模式登记的条目,可用 openclaw approvals allowlist add 添加)或 SafeBins;未命中时按 tools.exec.ask(默认 on-miss)转人工审批,审批不通过即拒绝。这是推荐的生产配置。SafeBins 超越简单的命令名匹配:每个条目必须有对应的 profile 才生效(内置 profile 只覆盖 jq、grep、cut、sort、uniq、head、tail、tr、wc,其余要用 tools.exec.safeBinProfiles(Record<string, SafeBinProfileFixture>)自行声明,否则该条目被忽略),由 profile 做参数级别的约束,以及 tools.exec.safeBinTrustedDirs 限定可信目录(源码注释特意强调"never derived from PATH"——绝不从 PATH 推导,避免 PATH 污染绕过)。

full — 任何命令都执行。仅用于完全可信的本地开发环境。

10.6.2 SafeBins 的深层设计

SafeBins 的设计体现了"最小权限"原则的精细应用。考虑这个场景:运营者想让 Agent 能使用 Python 来分析数据。

天真的做法是把 python 加入 SafeBins。但 python 能做的事情几乎无限——它可以调用 os.system("rm -rf /")、可以开启网络监听、可以读取 /etc/passwd。把 python 加入 SafeBins 等于授予完整代码执行能力,SafeBins 名存实亡。

OpenClaw 的安全审计系统会标记这种情况:src/security/audit.ts 中的检查会将没有显式 profile 的"解释器类"(interpreter-like)SafeBin 条目标记为 warn(tools.exec.safe_bins_interpreter_unprofiled)。src/infra/exec-safe-bin-runtime-policy.ts 里的 INTERPRETER_LIKE_SAFE_BINS 是一张 30 余项的集合,覆盖面比想象中宽——除了显而易见的 python / node / ruby / perl / php / bash / zsh / sh,还包括 awk / gawk / mawk / nawk、sed / gsed、lua、bun / deno、busybox / toybox,以及 Windows 侧的 cmd.exe / powershell.exe / pwsh / cscript / wscript。另外还有一组正则兜住带版本号的变体:

// src/infra/exec-safe-bin-runtime-policy.ts:61
const INTERPRETER_LIKE_PATTERNS = [
  /^python\d+(?:\.\d+)?$/,
  /^ruby\d+(?:\.\d+)?$/,
  /^perl\d+(?:\.\d+)?$/,
  /^php\d+(?:\.\d+)?$/,
  /^node\d+(?:\.\d+)?$/,
];

把 sed 和 awk 也算作解释器,一开始会让人意外——但 GNU sed 的 e 命令能执行 shell,gawk 有 system(),它们确实是完整的代码执行入口。这份名单的宽度本身就是一条经验:判断"是不是解释器"要看它能不能触达 shell,而不是看它像不像编程语言。

配套还有一个 tools.exec.strictInlineEval 开关,专门要求 python -c / node -e 这类内联求值形式必须显式批准,源码注释写明理由是"防止静默复用白名单条目、以及 allow-always 被持久化"。

10.6.3 命令注入防御

这里要先分清两处代码。Agent 发来的整条 exec 命令,在 allowlist 模式下由 src/infra/exec-approvals-analysis.ts 做受限的 Shell 分词:按 && / ; 与管道切成若干段,逐段解析出可执行文件再去匹配白名单;遇到它不打算理解的语法——重定向 > / <、反引号、$() 命令替换、子 shell 括号、换行、行尾续行、未闭合引号等——直接判定"无法分析",按未命中白名单处理(转审批或拒绝)。

另一处 src/infra/exec-safety.ts 的 isSafeExecutableValue() 则不管 Agent 的命令,而是校验配置里填写的可执行文件名/路径(如 src/config/zod-schema.core.ts 中 secrets.providers.*.command 的校验、src/infra/detect-binary.ts),同样遵循默认拒绝原则,检查顺序是一串短路否定:

const SHELL_METACHARS = /[;&|`$<>]/;
const CONTROL_CHARS = /[\r\n]/;
const QUOTE_CHARS = /["']/;
const BARE_NAME_PATTERN = /^[A-Za-z0-9._+-]+$/;

// NUL → 控制字符 → shell 元字符 → 引号,命中任意一条即判定不安全;
// 随后:像路径的直接放行;以 "-" 开头的拒绝(防止把参数当成可执行文件);
// 其余必须完全匹配 BARE_NAME_PATTERN。

注意 BARE_NAME_PATTERN 是白名单式的完全匹配——不是"排除坏字符",而是"只接受这几类字符"。这是黑白名单混用的典型正确姿势:先用黑名单快速否掉明显危险的输入并给出精确的失败原因,最后用白名单兜底,确保没想到的字符一律不放行。

为什么 exec 的分析器只解析 Shell 的一个子集,而不去"完整理解"命令的真实意图?因为 Shell 语法的复杂性远超想象——变量展开、命令替换、进程替换、Here Documents、转义序列......可靠地解析所有 Shell 方言几乎不可能。与其追求"完美理解",不如只接受自己能确定分析的子集,其余一律"遇到可疑就不放行"。宁可让合法命令多走一次人工审批,也不允许注入向量通过。

10.6.4 后台进程管理

后台进程管理(process 工具,src/agents/bash-tools.process.ts)允许 Agent 让 exec 启动的长时间运行进程转入后台并稍后检查,通过会话 ID 跟踪活动进程,提供 list、poll、log、write、send-keys、kill 等操作,超时方面,普通 exec 默认 1800 秒超时,而显式请求后台(background / yieldMs)又没给 timeout 的进程不设超时——正是为了让开发服务器这类进程长期运行。

这个能力解锁了一类重要的工作流:Agent 启动一个 npm run dev 开发服务器,然后继续编辑代码,定期检查服务器日志看是否有错误。在传统的"执行-等待-返回"模式下,Agent 必须等待进程结束才能做其他事——但开发服务器不会"结束"。后台进程管理让 Agent 能像人类开发者一样同时管理多个终端。

10.7 Web 搜索与内容理解

10.7.1 搜索引擎抽象

Web 搜索(工具在 src/agents/tools/web-search.ts,提供商解析在 src/web-search/runtime.ts)采用多提供商统一接口设计:v2026.3.31 随包的搜索提供商以插件形式注册,包括 brave、duckduckgo、exa、firecrawl、gemini、grok、kimi、perplexity、tavily。关键设计决策是搜索结果的结构化提取——10 个搜索结果只需 200-300 token,而原始 HTML 可能超过 50,000 token。

每次搜索只走一个提供商:显式配置了 tools.web.search.provider 就用它;否则按自动探测顺序选第一个有可用 API Key 的;一个 Key 都没有时回落到无需凭据的提供商(如 DuckDuckGo)。它不会把多家结果合并,也不会在某家调用失败时自动换下一家。为什么仍要支持多个提供商?两个原因:

  1. 部署条件不同:有的用户手头只有某一家的 Key,有的一个都没有——多提供商加无 Key 回落,让 web_search 在各种条件下都能开箱可用。
  2. 成本与效果取舍:不同提供商的 API 定价、免费额度和结果形态(纯链接列表还是带引用的综合回答)不同,运营者可以按预算和用途选择。

10.7.2 内容提取的权衡

web_fetch 使用可读性算法从 URL 提取内容,将 HTML 转为干净的 Markdown。这里的核心权衡是信息保真度 vs. Token 效率。

完整的 HTML 保留了所有信息,但对 LLM 来说大部分是噪音。纯文本提取丢失了结构(标题层次、列表、链接)。Markdown 是折中——保留语义结构,去除视觉样式,在信息密度和 token 效率之间取得平衡。

10.7.3 媒体理解

image 工具(src/agents/tools/image-tool.ts):发送图片到视觉模型分析。支持本地路径、file:// URL、HTTP(S) URL 和 data: URI。除 data: URI 外,图片经 loadWebMedia()(src/media/web-media.ts)加载时会按字节上限(可用 maxBytesMb 调整)重新编码压缩——带透明通道的 PNG 优先保留为 PNG,其余转 JPEG(GIF 不压缩,超限直接报错)。这是在"保留细节"与"控制请求体积"之间取了折中。

pdf 工具:处理 PDF 文档,支持页面范围选择(pages: "1-5,8")以避免处理不相关页面。这是 token 预算意识的又一体现——一个 100 页的 PDF 全部发送是灾难性的,页面选择让 Agent 能精确加载需要的部分。

10.8 语音合成

TTS(tts 工具在 src/agents/tools/tts-tool.ts;src/tts/ 是门面,主体实现在 extensions/speech-core/src/tts.ts)遵循多提供商模式。值得注意的设计是"自动朗读回复"路径(maybeApplyTtsToPayload())在合成前的文本预处理:

  1. 长文本摘要:文本超过长度上限(默认 1500 字符)且摘要开关开启(默认开启)时,先调用模型把它压缩到上限以内——与其截断导致句子不完整,不如压缩保持连贯;摘要关闭或失败时才退回截断加省略号。
  2. Markdown 剥离:stripMarkdown() 去除标题标记、粗体/斜体语法等——TTS 引擎不需要这些。
  3. 过短不读:处理后不足 10 个字符的文本直接跳过合成。

提供商选择没有复杂的评分:主提供商排在最前,其余提供商按各自声明的 autoSelectOrder(如 OpenAI 10、ElevenLabs 20、Microsoft 30)排成回退链(resolveTtsProviderOrder()),主提供商失败时依次尝试下一个。

10.9 工具系统与上下文引擎的关系

工具系统不是孤立存在的——它与会话/上下文管理(第 5 章)形成一个紧密的反馈回路。

每次工具调用产生的输出都进入会话的上下文历史。如果浏览器返回 5,000 token 的页面内容,这 5,000 token 就永久占据上下文窗口的一部分,直到被上下文压缩算法摘要。这意味着工具的输出格式直接影响 Agent 的后续推理能力。

这就是为什么无障碍树比完整 HTML 不仅仅是"省 token"——它实际上让 Agent 在同一次对话中能使用更多次工具。同一个上下文窗口,单次工具输出的量级每降一个数量级,可用的工具调用次数就大致抬高一个数量级。

这种乘数效应意味着工具输出压缩不是性能优化,而是功能使能——它决定了 Agent 能做多少事。

10.10 实战推演:Agent 自动化浏览器完成表单填写

让我们通过一个典型场景,展示浏览器工具的完整交互过程——Agent 帮用户在线填写一份多页表单。这个场景综合了快照机制、导航守卫、配置文件隔离和上下文效率的所有设计决策。

10.10.1 场景描述

用户对 Agent 说:"帮我到 https://forms.example.com 填写参会报名表,我的信息是:姓名张三,邮箱 zhang@example.com,参加第二天的工作坊。"

10.10.2 Agent 的执行序列

回合1: Agent 决策 → 调用 browser(action: "navigate", url: "https://forms.example.com")
  ↳ 导航守卫检查: 协议 https ✅, 主机名解析与 SSRF 策略 ✅ → 放行
  ↳ 浏览器配置文件: 未指定 profile → 使用 OpenClaw 自管的隔离浏览器(独立 Cookie)

回合2: Agent 调用 browser(action: "snapshot")
  ↳ 快照引擎生成无障碍树(约 1,800 token,而非 45,000 token 的完整 HTML):
    - heading "会议报名表" [ref=e1]
    - textbox "姓名" [ref=e2]
    - textbox "邮箱" [ref=e3]
    - radio "第一天" [ref=e4]
    - radio "第二天" [ref=e5]
    - checkbox "参加工作坊" [ref=e6]
    - button "下一页" [ref=e7]

回合3: Agent 推理 → 依次执行:
  browser(action: "act", kind: "type", ref: "e2", text: "张三")
  browser(action: "act", kind: "type", ref: "e3", text: "zhang@example.com")
  browser(action: "act", kind: "click", ref: "e5")  // 选择"第二天"
  browser(action: "act", kind: "click", ref: "e6")  // 勾选"工作坊"
  browser(action: "act", kind: "click", ref: "e7")  // 点击"下一页"

回合4: 新页面加载 → Agent 调用 snapshot → 确认页快照(每次快照的 ref 重新从 e1 编号):
    - heading "确认您的信息" [ref=e1]
    - text: 姓名: 张三
    - text: 邮箱: zhang@example.com
    - button "提交" [ref=e2]
    - button "返回修改" [ref=e3]

回合5: Agent 确认信息正确 → browser(action: "act", kind: "click", ref: "e2")

10.10.3 上下文效率的关键作用

整个 5 回合交互中,浏览器只产生了两次快照的开销。如果换成完整 HTML 方案,同样两次页面加载会高出一到两个数量级——在同一个上下文窗口里,前者还剩大半空间给 Agent 推理,后者几乎不剩什么。(本节的具体数字为示意,用于说明量级差异。)

浏览器自动化的本质不是"让 AI 看到网页"——而是"让 AI 以最小认知成本理解网页的交互结构"。无障碍树是为残障人士设计的语义抽象,恰好也是 AI 最高效的网页理解方式。技术的美在于:为一类用户设计的无障碍方案,意外地成为了另一类"用户"(AI)的最佳接口。

10.11 跨章关联

工具系统既消费其他系统的能力,也向其他系统提供服务:

消费 Agent 系统(第6章):Agent 决定何时调用哪个工具。工具系统提供工具目录供 Agent 发现和选择。

消费策略管线 → 安全系统(本专栏第 13 章):策略管线的过滤规则来自安全配置。安全审计扫描工具配置的合理性。

向 Node 系统(第11章)提供接口:Node 系统的设备能力(相机、GPS)以工具的形式暴露给 Agent。Node 系统可以看作"物理世界的工具扩展"。

Cron 系统(第12章)消费工具:定时任务通过调用工具执行实际操作——Agent 处理 Cron 作业的消息时,使用工具来完成任务。

技能系统(第16章)消费工具:技能通过 command-dispatch: tool 绕过 LLM 直接触发工具调用,为工具提供"使用方法"的知识。

graph TB
    subgraph "图 10-2:工具系统的跨模块关联"
        Agent["Agent 系统<br/>决策层"] --> Tools["工具系统<br/>执行层"]
        Security["安全系统<br/>策略层"] --> Tools
        Tools --> Context["上下文引擎<br/>输出进入上下文"]
        Node["Node 系统<br/>物理扩展"] --> Tools
        Skills["技能系统<br/>知识注入"] --> Tools
        Cron["Cron 系统<br/>触发工具调用"] --> Tools
    end
    style Tools fill:#c0392b,color:#fff
    style Agent fill:#1f618d,color:#fff
    style Security fill:#f39c12,color:#000

10.12 框架对比

维度 OpenClaw LangChain AutoGPT CrewAI Semantic Kernel
工具访问控制 7 层策略管线 无内建 基本权限模式 角色绑定 有限
浏览器自动化 Playwright + 无障碍树 第三方 Selenium 封装 无 无
命令执行 deny/allowlist/full + SafeBins 框架不内建限制 基于权限 无 无内建
上下文效率 快照压缩,token 感知 原始输出 原始输出 原始输出 原始输出
工具输出格式 结构化压缩 原始返回 原始返回 原始返回 原始返回

(与 10.5.5 的表一样,"OpenClaw"一列有仓库源码为据,其余各列是对各框架公开设计的定性概括,未做逐项实测。)

最显著的差异化因素是上下文效率。OpenClaw 的浏览器无障碍树、搜索结构化提取和 PDF 页面选择都反映了对工具输出与对话历史竞争相同有限上下文窗口的深刻认识。

graph LR
    subgraph "图 10-3:工具输出的上下文效率"
        Raw["原始 HTML<br/>~50K token"] --> Compressed["无障碍树<br/>~2K token"]
        RawSearch["原始搜索 HTML<br/>~30K token"] --> Structured["结构化结果<br/>~300 token"]
        RawPDF["完整 PDF<br/>~100K token"] --> Selected["页面选择<br/>~5K token"]
    end
    style Raw fill:#c0392b,color:#fff
    style RawSearch fill:#c0392b,color:#fff
    style RawPDF fill:#c0392b,color:#fff
    style Compressed fill:#1e8449,color:#fff
    style Structured fill:#1e8449,color:#fff
    style Selected fill:#1e8449,color:#fff

10.13 历史演进

工具系统的设计不是一蹴而就的。下面几个节点可以在仓库 CHANGELOG.md 里直接查到:

2026.1.12 — 配置文件与提供商维度同时落地。同一个版本里出现了两条相邻的条目:

Tools/Sandbox: add tool profiles + group shorthands; support tool-policy groups in tools.sandbox.tools; drop legacy memory shorthand… (#790)

Tools: add provider/model-specific tool policy overrides (tools.byProvider) to trim tool exposure per provider.

也就是说,tools.profile(从"列出每个工具"到"选择一种模式")和 tools.byProvider(按提供商/模型收窄工具暴露面)是同一批引入的——今天管线里那对 tools.profile / tools.byProvider.profile 阶段,源头在这里。同一条目还顺手删掉了 legacy 的 memory 简写,说明"组简写"这套机制当时就在做收敛。

2026.2.22 — SafeBin 收紧。

Security/Exec approvals: require explicit safe-bin profiles for tools.exec.safeBins entries in allowlist mode (remove generic safe-bin profile fallback), and add tools.exec.safeBinProfiles for safe custom binaries so unprofiled interpreter-style entries cannot be treated as stdin-safe.

这条把"通用 safe-bin 兜底"整个删掉了——正是 10.6.2 讲的"解释器类条目不能被当作 stdin-safe"这个问题的修复。

2026.3.2 → 2026.3.7 — 默认配置文件的一次来回。3.2 版把新手引导(onboarding)为新本地安装写入的默认 tools.profile 改成 messaging(标记为 BREAKING,理由是"新装机不应该一上来就带着宽泛的编码/系统工具");到 3.7 版又改回 coding("恢复全新本地安装的文件/运行时工具,同时保留用户显式设置的 profile")。

这次来回值得多看一眼:它是"安全默认值"与"开箱可用"之间张力的真实记录——把默认值收紧到 messaging 在安全上无可指摘,但让本地开发者一装上就少了一半工具,于是紧接着的下一个正式版就调了回去。默认值的选择是产品决策,不只是安全决策。

2026.3.13 — 诊断信息细化:

Agents/tool warnings: distinguish gated core tools like apply_patch from plugin-only unknown entries in tools.profile warnings…

这正是 10.3.4 讲的那段 gatedCoreEntries / otherEntries 区分逻辑的来历。

这条演进线索反映了一个更普遍的规律:系统的安全模型总是追赶系统的能力模型。每次添加新能力(多 Agent、多通道、多提供商),安全模型都要跟着扩展;而每一次收紧默认值,都要重新面对易用性的账。

10.14 关键源码文件

目录/文件 用途
src/agents/tool-catalog.ts 工具定义和配置文件分配
src/agents/tool-policy-pipeline.ts 多阶段访问控制
src/agents/tool-policy.ts 策略标准化和插件组扩展
src/security/dangerous-tools.ts 网关 HTTP /tools/invoke 默认拒绝列表
extensions/browser/ 基于 Playwright 的浏览器自动化插件(实测 241 个文件,其中 src/ 下 234 个 .ts;口径:find extensions/browser -type f | wc -l)
extensions/browser/src/browser/pw-role-snapshot.ts 无障碍树快照生成
extensions/browser/src/browser/navigation-guard.ts 导航协议与 SSRF 检查
src/agents/bash-tools.exec.ts / bash-tools.process.ts exec / process 工具(底层进程派生在 src/process/)
src/web-search/ Web 搜索提供商解析(工具本体在 src/agents/tools/web-search.ts)
src/tts/ 语音合成门面(主体在 extensions/speech-core/)
src/infra/exec-approvals-analysis.ts exec 命令的受限 Shell 分析(白名单匹配前置)
src/infra/exec-safety.ts 配置中可执行文件名/路径的安全校验

10.15 本章小结

OpenClaw 的工具系统解决了 Agent 工具的根本张力:最大能力与最小风险。三层架构(目录→策略→执行)清晰分离关注点,让每一层按自己的节奏演化。七阶段策略管线组合独立的安全关注而不纠缠它们,每个利益相关者获得独立的控制面。上下文高效的输出格式(无障碍树、结构化搜索结果、页面选择的 PDF)不只是节省 token——它们决定了 Agent 在一次对话中能使用多少次工具,是功能使能而非性能优化。

核心洞察:Agent 工具的最难问题不是实现工具——而是控制对它们的访问。OpenClaw 的策略管线证明,有效的工具安全需要组合式设计,而非单体访问控制列表。每个利益相关者(平台运营者、Agent 开发者、模型提供商)获得独立的策略阶段,它们的组合效果从管线的有序组合中自然涌现。而工具输出的上下文效率设计,揭示了一个 LLM 系统独有的架构约束:所有工具输出都在与对话历史争夺同一个有限的上下文窗口——工具的价值不只取决于它能做什么,更取决于它的输出占用多少 token。

工具系统的终极使命,不是让 Agent 能做更多事——而是让它在做正确的事时毫无阻碍,在做危险的事前悬崖勒马。 给一个人一把锤子,他看什么都像钉子。给一个 Agent 所有工具但不加管控,它做什么都像冒险。

工具让 Agent 拥有了在数字世界中行动的能力——执行命令、浏览网页、搜索信息。但 Agent 的活动空间不必局限于服务器边界之内。下一章,我们将看到 Node 系统如何将 Agent 的触角从虚拟世界延伸到物理世界——相机、屏幕、传感器,一切物理设备都将成为 Agent 可调用的"远程工具"。

🛠️ 试一试:编写一个自定义工具

通过 Agent 的工具系统,你可以让 Agent 调用自定义脚本。以下示例创建一个"磁盘用量检查"工具:

# 1. 创建工具脚本目录
mkdir -p ~/.openclaw/workspace/tools

# 2. 编写工具脚本
cat > ~/.openclaw/workspace/tools/disk-usage.sh << 'EOF'
#!/bin/bash
# 磁盘用量检查工具
# 用法: disk-usage.sh [path]
TARGET="${1:-/}"
echo "=== Disk Usage Report ==="
echo "Target: $TARGET"
df -h "$TARGET" | tail -1 | awk '{print "Total:", $2, "| Used:", $3, "| Available:", $4, "| Usage:", $5}'
echo ""
echo "Top 5 largest directories:"
du -sh "$TARGET"/*/ 2>/dev/null | sort -rh | head -5
EOF
chmod +x ~/.openclaw/workspace/tools/disk-usage.sh

# 3. 在配置中启用 exec 工具
# ~/.openclaw/openclaw.json(按 JSON5 解析):
# {
#   "tools": {
#     "profile": "coding",                  // ← 策略管线:coding 档包含 exec(tools.allow 只能收窄,加不进工具)
#     "exec": {
#       "security": "allowlist",            // ← 不配时按主机分流,显式写死才不随主机变
#       "ask": "on-miss"                    // ← 未命中白名单时向用户征询(默认值)
#     }
#   }
# }

# 4. 把脚本登记进 exec 审批白名单(按可执行文件路径匹配,写入 ~/.openclaw/exec-approvals.json)
openclaw approvals allowlist add "~/.openclaw/workspace/tools/disk-usage.sh"
# 不要图省事把 df、du、awk 塞进 tools.exec.safeBins:safeBins 条目没有对应 profile
# 会被直接忽略(内置 profile 只覆盖 jq/grep/cut/sort/uniq/head/tail/tr/wc),
# 而 awk 属于 INTERPRETER_LIKE_SAFE_BINS,安全审计还会对它告警。

# 5. 测试:让 Agent 检查磁盘用量
# 向 Agent 发送消息:"请用 ~/.openclaw/workspace/tools/disk-usage.sh 检查一下根目录的磁盘使用情况"
# Agent 调用 exec 执行脚本;命中白名单直接运行,未命中则按 ask=on-miss 请你审批

💡 观察要点:这里有两道互相独立的闸门,别混为一谈——tools.profile / tools.allow 属于 10.3 节的七阶段策略管线,决定 exec 这个工具能不能出现在 LLM 的工具列表里;tools.exec.security + 审批白名单(及 safeBins)则是 10.6 节的 exec 内部安全模型,决定拿到 exec 之后能跑哪些命令。试着分别改动这两处,观察被拦截时的报错信息有什么不同。


思考题

  1. 概念理解:工具系统的七层策略管线为什么采用"有序组合"而非"单一 ACL"?每个利益相关方(平台运营者、Agent 开发者、模型提供商)拥有独立的策略阶段有什么好处?
  2. 实践应用:如果要为 OpenClaw 添加一个"数据库查询"工具,允许 Agent 执行 SQL 查询,你会在策略管线中如何配置安全限制(防止 DROP TABLE 等危险操作)?
  3. 开放讨论:工具输出与对话历史竞争同一个有限的上下文窗口——这是 LLM 系统独有的架构约束。你认为未来有没有可能从根本上解决这个"上下文预算"问题?

📚 推荐阅读