插件
ZeroClaw 的插件系统可让你在不触碰核心二进制的情况下为代理添加能力。本页解释了技术选型:插件由什么构成、为什么选择 WebAssembly,以及宿主如何将不受信任的组件限制在可控范围内。下面的指南将逐步讲解如何构建各类插件,并且随着向下阅读会变得更技术化。
- 编写工具插件:模型可以调用的可调用工具。从这里开始;这是从空 crate 到已安装工具的完整实践路径。
- 编写通道插件:具有完整 capability 标志表面的消息平台集成。
- 编写内存插件:一种实现 agent-attributed recall 的存储后端。
- 分发插件:签名、注册表和安装安全性。
Markdown-only skill bundles 不是插件,但它们会通过相同的清单、签名和安装机制;该页面与 Skills 文档放在一起。
有关发现、签名策略和配置的运维视图,请参见 插件如何工作。有关规范性契约参考,请参见 插件协议。
为什么选择 WebAssembly
插件会在一个进程内运行任意第三方代码,而该进程持有你的 API 密钥、对话历史和 shell 访问权限。隔离边界必须是真实的,而不是仅供建议。ZeroClaw 使用 wasmtime 上的 WASI Component Model,因为它同时提供了四个性质,而没有任何动态库或子进程方案能一次性匹配:
- 基于能力的沙箱。 WebAssembly 组件没有环境权限。除非宿主显式将该能力接入其链接器,否则它不能打开文件、套接字或环境变量。ZeroClaw 的宿主在构建每个插件存储时都会使用一个 WASI 上下文,该上下文没有文件系统预打开项,也没有网络(
crates/zeroclaw-plugins/src/component.rs中的PluginState)。插件能够访问的,正好是其 world 声明的宿主导入,以及其 manifest 权限所添加的内容,除此之外别无其他。 - 计量执行。 引擎启用了燃料计量,每次调用都会获得全新的燃料预算和一个包含等待宿主工作时间在内的墙上时钟截止时间。无限循环或无限等待的插件会失败,无法挂起代理。存储的内存、表和实例上限由存储限制器强制执行。所有五个限制都来自操作员配置(
plugins.limits.*),并经过非零校验;如果没有这些限制,就无法构造存储,因此任何加载路径都不会产生未受沙箱保护的插件。 - 一种带类型、语言无关的 ABI。 宿主与插件之间的契约是一组 WIT 接口文件(ZeroClaw 仓库中的
wit/v0/),而不是 Rust API。宿主使用 wasmtime 的bindgen!从这些文件生成其绑定;插件则使用 Rust 中的wit-bindgen,或任何编译为wasm32-wasip2组件的语言中的等效工具生成镜像的 guest 绑定。记录、变体、结果和可选类型会以其原本的类型跨越边界。 - 行为与内置项完全一致。 每种插件类型都会适配到第一方实现所使用的同一个 Rust trait:工具插件变成
Tool(wasm_tool.rs),通道插件变成Channel(wasm_channel.rs),内存插件变成Memory(wasm_memory.rs)。代理循环、归因、收据和安全策略看不出任何差异。
这些部分
磁盘上的插件是一个包含清单和已编译组件的目录:
~/.zeroclaw/plugins/
└── my-plugin/
├── manifest.toml # identity, capabilities, permissions, signature
└── my-plugin.wasm # wasm32-wasip2 component
清单声明了两个正交的内容:
- Capabilities:插件本身是什么。
tool、channel、memory、observer、skill中的一个或多个(crates/zeroclaw-plugins/src/lib.rs中的PluginCapability枚举)。每个 WASM capability 都会选择组件必须导出的 WIT world。skillcapability 是个例外:它标记的是借助安装机制分发的 markdown skill bundle,不是代码,也不需要 component。 - 权限:插件代码可以_访问_哪些主机服务。同一文件中的
PluginPermission枚举。当前,config_read(工具和通道适配器会接收各自根据架构生成并经过验证的公开配置,并可以在获授权的服务调用中解析架构指定的机密)和http_client会产生实际行为影响。HTTP 权限是实现出站wasi:http的适配器必须获得的授权:工具和通道启用该接口,而 memory 目前有意不启用。config_read必须与清单中的config_schema配对;缺少任一项都会被拒绝。文件系统和内存访问权限会被架构接受,但尚未由主机函数提供支持,因此声明它们不会授予任何权限。
这些世界
wit/v0/ 为每个 WASM 能力定义一个 world。每个 world 都导入宿主 logging 接口,其 log-record 事件会落入结构化日志,并携带宿主调用点的 span attribution,同时导出 plugin-info(自报名称和版本)以及其主接口:
| World | 导出 | 存储生命周期 |
|---|---|---|
tool-plugin | tool:name、description、parameters-schema、execute | 每次 execute 使用全新的存储;导入限定在 secrets 作用域内 |
channel-plugin | channel:configure、send、poll-message,以及 22 个受能力限制的方法 | 异步互斥锁后的预热存储,每次调用都会重新填充;导入作用域限定为 config、secrets 和由主机提供的 inbound |
memory-plugin | memory:存储、调用、获取、忘记,以及 11 个受能力门控的方法 | 由异步互斥锁保护的热存储,每次调用都会重新填充 |
channel 和 memory worlds 使用 capability flags:主机在加载时只读取一次的位掩码(get-channel-capabilities / get-memory-capabilities)。对于每个未设置的标志,主机使用 Rust trait 的默认实现,并且永远不会调用插件的导出。这就是 WIT 合约保持可扩展的方式:一个新的可选方法就是一个新的标志加上一个新的函数,绝不会破坏兼容性。
执行模型
主机(crates/zeroclaw-plugins/src/component.rs)为该进程持有一个异步 wasmtime::Engine。加载方式取决于后端:使用 Cranelift JIT 的构建会在加载时编译 .wasm;仅运行时构建则反序列化预编译的 .cwasm。每个插件实例化都会获得:
- 一个携带 sandboxed WASI 上下文、资源表、可选 HTTP 上下文以及 fuel 预算的
Store; - 一个
Linker,其导入项恰好由其 world、授权项和适配器支持决定:始终调用logging,工具和通道调用secrets,通道调用config和inbound,而工具和通道适配器仅在清单授予http_client时调用wasi:http。Memory 既不会创建 HTTP 上下文,也不会创建 HTTP 链接器。每个适配器都会在实例化时交叉检查其上下文和链接器(ensure_http_coherent)。
工具调用在设计上是无状态的:WasmTool::execute 会创建一个全新的存储,运行调用,然后将其丢弃。通道和内存后端本质上是有状态的,因此会在插件的整个生命周期内持有一个预热存储;宿主会在每次调用前为其重新补充资源,使长期运行的插件每次调用都获得完整配额,而不是随着时间推移逐渐耗尽。截止时间中断会丢弃预热存储,而不是恢复部分展开的来宾状态。通道会在下一次调用时重新创建实例;内存在其所有者重建之前会一直不可用。在一次获授权的通道调用期间,config.get 和 secrets.get 最多会具体化该获准实例规范配置的一个版本。调用结束时,宿主会丢弃该视图。符合要求的通道插件 必须 在每个使用点解析这两者,并且不得将返回的配置或明文机密保留在预热的来宾状态中。数据返回给受信任的来宾代码后,宿主无法强制其不予保留。
边界是 32 位:wasm32-wasip2 是 Rust 工具链随附的唯一 WASI Preview 2 目标,并且 component ABI 会将偏移量按 32 位处理,而不管宿主字长大小。大值(例如通道附件的字节)按值传递。有关这为何是上游约束,请参见 protocol page。
当前布线状态
请注意端到端已注册的内容与已完成主机端但尚未能从正在运行的守护进程访问的内容之间的区别:
| 能力 | 主机适配器 | 运行时连接 |
|---|---|---|
tool | WasmTool | 已注册端到端;发现工具插件出现在代理的工具集中 |
skill | markdown 加载器 | 已完成端到端注册;技能按命名空间 plugin:<plugin>/<skill> 加载 |
channel | WasmChannel,已完成并有单元测试覆盖 | 由 Alias 负责的构造和运行时配置解析已合入(#10146);按供应商划分的主机监听器将每个传输的数据排入频道的 inbound 队列,这是后续工作 |
memory | WasmMemory,实现完整的 Memory trait | 运行时尚未将其构造为可配置的后端 |
observer | 无 | PluginCapability::Observer 已保留;目前还没有 WIT world 或 adapter |
配置
静态插件主机设置使用与其他设置相同的架构镜像。每个实例的值目前通过通用 TOML 或 zeroclaw config set 设置;插件清单架构尚未渲染为 zerocode 或网关表单。手动编辑时请务必小心:某个区段中的语法错误(例如本应使用 [[plugins.entries]] 却使用了 [plugins.entries])目前会导致整个 [plugins] 区段反序列化失败,并静默回退到默认值,读取时会显示为 plugins.enabled = false,且不会发出警告(已在 issue #8636 中跟踪)。常见操作:
# 打开系统
zeroclaw config set plugins.enabled true
# 在运行时加载自动发现的工具和技能插件(默认值:false)
zeroclaw config set plugins.auto_discover true
# 插件发现位置(默认:~/.zeroclaw/plugins)
zeroclaw config set plugins.plugins_dir /srv/zeroclaw/plugins
# 签名策略: disabled | permissive | strict
zeroclaw config set plugins.security.signature_mode strict
# per-call 沙箱限制
zeroclaw config set plugins.limits.call_fuel 1000000000
zeroclaw config set plugins.limits.call_timeout_ms 30000
zeroclaw config set plugins.limits.max_memory_mb 256
plugins.enabled = true 会启用插件宿主,但只有在同时设置 plugins.auto_discover = true 时,自动发现的工具和技能功能才会加载。该标志默认值为 false(故障关闭),因此单独设置 enabled = true 只会提供你在 [channels.plugin.<alias>] 下声明的通道,不会提供任何插件工具或技能:工具或技能包可以正常列出并执行 info,但在运行时不会贡献任何功能。显式通道绑定由操作员命名,而不是自动发现,因此不需要 auto_discover;该标志只控制自动发现的工具和技能。
每个实例的设置位于 plugins.entries 下,键为一个版本化的 zpi1_… 字符串,该字符串根据主机拥有的包、能力和绑定标识派生而来。安装时会打印并预置该包默认工具绑定的键;zeroclaw plugin info <package> 会再次打印该工具键。这些自动化入口仅适用于工具。基于别名的通道构造会根据实际配置的别名派生键,而不是凭空创建基于包名的绑定。该运行时路径已在 #10146 中落地:守护进程现在会构造一个显式声明的 [channels.plugin.<alias>] 实例,并从该别名解析其类型化配置。通道实例的自动 plugin info 键显示和安装时预置在 #9584 中的授权流程完成之前仍需手动进行。完整身份键让不同的包和能力域能够安全地复用 main 等别名,而不会共享凭据。规范的操作员值是一个标记为机密的字符串映射,并在静态存储时保持加密(enc2:…)。请求 config_read 的插件会在 config_schema 中声明该映射的单一类型契约:一个封闭的 Draft 2020-12 对象,其顶层属性明确使用 string、boolean、integer、number、array 或 object。工具或通道使用方可以在顶层字符串属性上设置 x-secret = true;主机会使用完整对象进行验证,将该机密值从公开配置中移除,并仅通过获准实例的 secrets.get 导入提供该值。工具可以在 execute 期间读取机密;通道则在 configure 和各项操作调用期间通过 config.get 获取公开配置,并通过 secrets.get 获取机密。若没有有效的 config_read 授权,任一导入都会返回 access-denied;实例化、静态元数据发现、解析失败和主机调用预算耗尽都会返回 unavailable。字符串直接存储,布尔值和数字存储为 JSON 标量文本,数组和对象存储为 JSON 文本。主机会在使用工具或通道来宾代码之前生成并验证得到的类型化对象;未知、格式错误或超出范围的值会导致失败,而不会传递给插件。记忆插件目前还没有配置导入接口,在该 ABI 加入之前不得请求 config_read。
1.0 之前的插件作者必须显式迁移:请求 config_read 但未包含 config_schema 的清单将不再被发现。添加与当前值匹配的封闭式 schema,更新工具/频道 guest,使其反序列化类型化 JSON,而不是字符串映射,然后重新构建并重新签名,因为 schema 包含在签名覆盖范围内。宿主集成注入 PluginHostServices,该服务封装 PluginConfigResolver,而不是使用自有的配置映射。每个获授权的工具或频道帧最多实例化一个与作用域绑定的 ResolvedPluginConfig,在帧内的每次配置读取中都使用该视图,并在帧结束时将其丢弃。频道会在使用点调用 config.get 和 secrets.get,因此同一逻辑绑定中的公开配置和凭据轮换会在下一次操作中以同一个修订版本可见。静态身份和能力导出项在加载时只读取一次;更改机器人/账户身份或其他静态元数据需要重建频道生命周期。迁移到类型化配置是分步指南,其中包括在不提供兼容性垫片的情况下发布这项强制要求的决定。
这是严格的 pre-1.0 键格式:仅以包或绑定命名的旧条目不会被使用。对于现有工具包,运行 zeroclaw plugin info <package> 获取其完整实例键,将旧条目重命名为该键,然后保存配置。全新安装的工具会自动填充该键。
有效授权会独立于清单请求进行检查。如果拒绝了 config_read,主机会验证一个空对象。必需属性缺失会使启动安全失败。当空对象有效时,工具会省略空的 __config 键,而通道 config/secret 导入会返回 access-denied。规范的主机字段列表和默认值位于配置参考中;zeroclaw config list 会显示当前存储的值。
信任边界实际上在哪里
sandbox 限制已加载插件能做什么;签名策略限制哪些内容可以被加载。两者都是由操作者决定的,并且它们可以组合:
plugins.enabled为 false(默认值):不会运行任何插件代码,始终如此。plugins.auto_discoverfalse(默认值):不会加载自动发现的工具和技能功能。仅设置plugins.enabled = true会激活你在[channels.plugin.<alias>]下声明的通道;只有同时设置auto_discover = true时,工具和技能才会加载。- 签名
strict:只有其清单带有来自您受信任密钥集合中密钥的有效 Ed25519 签名的组件才会加载。 - 已加载插件:受 fuel、内存上限、无预打开的 WASI,以及受权限门控的导入集约束。
沙箱无法约束模型选择调用的工具的语义行为:拥有 http_client 授权且具备工具适配器 HTTP 接口的工具,可以将模型传给它的任何内容发送到其代码决定的任何位置。签名策略之所以存在,是因为“我加载哪段代码”是最重要的决策;要有意识地做出这一决策。