Keyboard shortcuts

Press or to navigate between chapters

Press ? to show this help

Press Esc to hide this help


类型:参考 状态:已接受 最后审阅:2026-07-17 相关于:

  • FND-001
  • ADR-003
  • crates/zeroclaw-plugins

插件协议

本文档定义了 ZeroClaw 的插件宿主与 WASM 插件组件之间的协议。

什么是插件

插件是一个自包含的 WebAssembly 组件,ZeroClaw 会在运行时加载它,以添加核心二进制未随附的能力。它位于 ~/.zeroclaw/plugins/ 下自己的目录中,并带有一个清单,说明其名称并声明它提供什么。ZeroClaw 会在启动时发现它,验证它,并将其导出的函数接入正在运行的 agent,使其表现得像内置能力:工具插件在模型看来只是另一个可调用的工具(WasmTool 实现了与原生工具相同的 Tool trait),通道插件表现为消息通道,内存插件表现为存储后端。

插件可以提供 PluginCapabilitycrates/zeroclaw-plugins/src/lib.rs)中定义的一项或多项能力:可调用工具、消息通道、内存后端、可观测性后端,或一组 markdown 技能。技能这一情况比较特殊:它完全不包含 WASM,只提供一个包含 markdown 的 skills/ 目录,这也是它唯一省略编译组件的能力。

为什么要构建一个

  • 无需分叉即可扩展。 无需修改 ZeroClaw 源码树或等待发布即可添加工具或通道;插件归你所有,并从你的安装目录加载。
  • 原生行为。 已加载的插件不是二等附加组件。桥接实现了内置项使用的相同运行时特性,因此插件工具会像第一方工具一样被提供给模型、归属并被调用。
  • 语言选择。 合约是 WIT 和 WASI Component Model,而不是 Rust API。任何可编译为 wasm32-wasip2 component 的语言都可以实现一个 world。下面的实操指南以 Rust 为例,因为这是目前支持最完善的路径,但边界本身与语言无关。
  • 默认沙箱隔离。 宿主将每个插件加载到一个没有文件系统预开放权限、也没有环境网络的 WASI 上下文中。插件无法悄无声息地访问宿主;它只能获得连接到其 world 的宿主函数,仅此而已。出站 HTTP 是唯一可以开放的网络接口,且仅当清单授予 http_client 权限,并且相应的能力适配器显式启用其经过测试的 HTTP 边界时方可使用。工具适配器和通道适配器支持此功能;内存适配器目前尚不支持。
  • 可验证的来源。 清单可以使用 Ed25519 签名,并且运营者可以要求在任何插件加载之前,由受信任的发布者提供签名。

插件(目前)不能做什么

这些是当前主机的真实限制,不是样式偏好。在围绕一个并不存在的能力进行设计之前,请先了解它们。

  • logging、类型化配置、实例级机密、http_client 以及主机提供的入站能力均已接入。 在清单可以声明的权限中,config_read 会公开插件自身经模式验证的公开配置。工具或通道的模式可以指定不纳入公开配置的机密,并在获授权的服务调用中解析这些机密。出站 wasi:http 必须获得 http_client 授权,但能力适配器还必须选择启用该主机接口。工具和通道适配器会这样做;在其网络边界获得组件级覆盖之前,memory 则有意不使用 HTTP。文件系统和内存访问权限仍会被清单模式接受,但不起作用:其主机函数尚未在链接器中注册。请参阅下文的“权限和主机导入”。
  • 无环境宿主网络或文件系统。 WASI 上下文没有预打开项,也没有环境网络,因此插件无法通过环境 WASI 打开原始套接字或读取宿主文件。具有 http_client 授权的工具或通道插件可获得出站 wasi:http 访问权限,因为这些适配器已选择启用;但插件无法监听。必须接收入站流量的通道插件不会自行打开监听器:宿主运行监听器并通过 inbound 导入将消息传入,插件则从其 poll-message 导出中取出消息。
  • 32 位边界。 目标是 wasm32-wasip2。来宾内存是 32 位地址空间,组件 ABI 会将偏移量降级为 32 位,而不受宿主字长大小影响。大值(例如通道附件的原始字节)通过按值传递跨越该边界。有关这为何是上游工具链的约束,而不是本仓库可以切换的标志,请参见 32 位地址空间部分。
  • 每个工具插件只对应一个工具。 tool-plugin world 只导出一个名为 tool 的接口,并带有一个 schema。需要暴露多个工具的插件会提供多个组件,或者使用不同的 world。
  • 实验性,未冻结的契约。 wit/v0 目前还没有 .frozen 标记,因此这些接口在首次稳定版发布前仍可能变更。请固定到某个版本,并预期在 WIT 升级后重新编译。

架构

ZeroClaw 插件是由 wit/v0/ 下的 WIT 接口定义的 WebAssembly 组件,并通过直接使用 wasmtimecrates/zeroclaw-plugins)托管。插件会编译为 WASI Preview 2 组件(wasm32-wasip2),导出插件 world 中的一个(tool-pluginchannel-pluginmemory-plugin),并导入该 world 在 wit/v0/ 中声明的宿主接口。

宿主位于 crates/zeroclaw-plugins/src/component.rs。它持有一个启用异步的 wasmtime::Engine,使用 wasmtime::component::bindgen!wit/v0 生成 world 绑定,并将沙箱化的 WASI p2 接口接入每个 world 的链接器。每个 store 的宿主状态(PluginState)包含一个在无预打开目录且无网络的条件下构建的 WasiCtx、WASI 所需的 ResourceTable、宿主签发的作用域,以及带类型的活动服务句柄。每个 world 都导入 logging;tool 导入 secrets,而 channel 导入 configsecretsinbound。获得 http_client 权限后,还会附加并链接 wasi:http。world 声明和获准作用域仍是该接口的规范契约(参见“宿主导入”)。

这三个 world bridges 将每个 WIT world 映射到运行时的原生 traits:

World桥接模块运行时表面
tool-pluginruntime.rs, wasm_tool.rszeroclaw_api::tool::Tool
channel-pluginwasm_channel.rschannel trait
memory-pluginwasm_memory.rs内存后端 trait

工具插件在每次调用时使用全新的存储(无状态)。Channel 和 memory 插件在插件生命周期内持有一个由异步互斥锁保护的热存储。

工具插件已实现端到端的发现和注册:运行时遍历 channel_plugin_details() 的工具对应项,并为每个对应项构建一个 WasmTool。通道主机适配器(WasmChannel、其 wasi:http 门控、使用点配置服务以及由主机馈送的 inbound 队列)已完成并通过单元测试覆盖,同时 PluginHost::channel_plugin_details() 会暴露由 wasm 支持的通道插件以供注册。运行时现在可以解析显式声明的 [channels.plugin.<alias>] 绑定,根据配置的别名构建其 WasmChannel 并完成注册;这种支持别名的构建和运行时配置解析已在 #10146 中合入。剩余的后续工作是实现按供应商划分的主机监听器,将各个传输中的数据排入通道的 inbound 队列。内存桥接(WasmMemory)所处的位置比这早一步:适配器已针对 memory-plugin world 实现完整的 Memory trait,但主机尚未提供 channel_plugin_details() 的内存对应项,运行时也尚未将 WasmMemory 构建为可配置后端。

插件结构

插件是一个包含以下内容的目录:

my-plugin/
  manifest.toml    # Plugin metadata and permissions
  plugin.wasm      # Compiled WASM module (optional for skill-only plugins)

插件从 ~/.zeroclaw/plugins/ 目录中自动发现(可通过配置文件中的 plugins.plugins_dir 进行配置)。

注册表搜索和安装

本地插件安装路径仍然是已安装插件的事实来源。注册表只是一个在命令执行时用于发现并下载插件归档的 JSON 索引:

zeroclaw plugin search calendar
zeroclaw plugin install team-calendar
zeroclaw plugin install team-calendar@0.2.0
zeroclaw plugin search calendar --registry https://example.invalid/registry.json
zeroclaw plugin install team-calendar --registry https://example.invalid/registry.json

zeroclaw plugin search 会获取注册表元数据,并将查询与插件名称和描述进行匹配。它不会安装、启用或执行插件代码。

zeroclaw plugin install <name> 从注册表解析名称,下载所选的 zip 压缩包,校验可选的 SHA-256 摘要,安全解压归档,然后将解压后的插件目录交给现有的 PluginHost::install 路径。本地路径安装保持不变:

当没有固定版本时,ZeroClaw 会选择注册表索引中的最后一个匹配条目,因此注册表发布者应有意安排重复名称的顺序。

zeroclaw plugin install ./my-plugin
zeroclaw plugin install ./my-plugin/manifest.toml

默认的注册表 URL 是:

https://raw.githubusercontent.com/zeroclaw-labs/zeroclaw-plugins/main/registry.json

对于私有或分阶段的 registry,可为每个命令使用 --registry <url>,或设置 ZEROCLAW_PLUGIN_REGISTRY_URL

Registry 条目使用此格式:

{
  "plugins": [
    {
      "name": "team-calendar",
      version: "0.8.5",
      "描述": 在团队日历上安排会议,
      "author": "Example Team",
      "capabilities": [tool],
      "url": "https://example.invalid/team-calendar-0.2.0.zip",
      "sha256": "sha256:<zip 的十六进制摘要>"
    }
  ]
}

归档必须包含根级 manifest.toml,或包含一个嵌套插件目录且其中有 manifest.toml。带有遍历路径、绝对路径、Windows 驱动器前缀路径,或包含多个 manifest 的归档会在安装前被拒绝。下载在流式传输时也有上限,因此没有 Content-Length 的服务器无法强迫 ZeroClaw 缓冲一个过大的归档。解压同样有上限,因此压缩归档不能在临时安装区域内无限膨胀。

搜索是未认证的发现。安装是安全边界:注册表安装会使用已配置的插件签名策略和受信任的发布者密钥,与通过 PluginHost::install 进行的本地插件安装相同。

仅含技能的插件布局(markdown 包)

仅具备 skill 能力的插件会将技能以 agentskills.io 格式放置在 skills/ 目录下,并省略 wasm_path

my-toolkit/
  manifest.toml              # declares the skill capability, no wasm_path
  README.md                  # optional bundle-level overview
  skills/
    design-review/
      SKILL.md
      scripts/
      references/
    code-review/
      SKILL.md
    data-analysis/
      SKILL.md
      references/

每个 SKILL.md 都必须包含带有 namedescription 字段的 YAML frontmatter;如果 bundle 中的技能缺少其中任一字段,运行时会在发现阶段(而非首次调用时)将其拒绝。技能以插件命名空间 ID 的形式注册,格式为 plugin:<plugin-name>/<skill-name>(例如 plugin:my-toolkit/design-review),以避免与用户自定义技能以及不同 bundle 之间发生冲突。

清单格式

manifest 是插件目录中名为 manifest.toml 的文件。其字段是 crates/zeroclaw-plugins/src/lib.rsPluginManifest 的 serde 表面,这是唯一的事实来源:

字段必填含义
name唯一的规范包 slug,也是每个派生实例配置键中的包组件。它本身不是操作符配置键。使用 1–128 个小写 ASCII 字符;以 [a-z0-9] 开头和结尾,中间只能包含 [a-z0-9._-]。发现过程会拒绝无效或重复的名称。
version版本字符串,例如 0.1.0
descriptionzeroclaw plugin list 显示的人类可读描述。
author作者姓名或组织。
wasm_path用于 WASM 能力组件文件名,相对于插件目录。除非唯一的 capability 是 skill,否则为必填。如果指定的文件不存在,发现过程会跳过该插件。
capabilities是,非空插件类型:toolchannelmemoryobserverskill 中的任意一个(PluginCapability,序列化为 snake_case)。
permissions代码可能访问的主机服务:http_clientconfig_readfile_readfile_writememory_readmemory_writePluginPermission)。目前仅前两项会被强制执行;其余项虽会被接受,但不起作用。声明 config_read 需要 config_schema,目前只有工具/通道适配器会提供它。
config_schema恰好使用 config_read为此插件的私有配置起草 2020-12 JSON Schema;它会包含在规范清单字节中,因此在清单签名时也会受到保护。根必须是一个包含 properties 映射且 additionalProperties = false 的对象。每个顶层属性都必须具有一个明确的受支持类型,可直接指定,也可通过本地 JSON Pointer 指定:stringbooleanintegernumberarrayobject。工具和通道使用者可以直接在顶层字符串属性上设置 x-secret = true,以便将其从公共配置中移除,并通过带作用域的 secrets.get 主机导入来公开。工具会在 execute 期间通过 __config 接收公共配置,并可以读取机密。通道会在 configure 和操作调用期间通过 config.get 读取当前公共对象,并通过 secrets.get 读取机密;这两个导入在实例化和静态元数据发现期间均不可用。嵌套、值为 false 或非布尔值的机密标记,以及非字符串的机密属性都会被拒绝。没有 config_read 的 schema,或没有 schema 的 config_read,都会被拒绝。
signature对规范化清单字节进行的 Base64url Ed25519 签名。用于发布签名时设置。
publisher_key签名者的十六进制编码 Ed25519 公钥。

只声明代码实际使用的权限。未声明的权限是组件无法触达的宿主表面;不必要声明的权限则是你主动增加的攻击面,也是审查你的插件的人需要承担的审核负担。

操作方提供的值在 plugins.entries 中仍保持为字符串,并会在持久化时加密,以一个由宿主拥有的包、能力和绑定标识派生的版本化 zpi1_… 字符串作为键(安装过程会打印并初始化默认工具绑定的完整实例密钥):字符串按原样存储,布尔值和数字使用 JSON 标量文本,数组和对象使用 JSON 文本。在任何来宾代码运行之前,宿主会将这些字符串物化为包模式规定的类型,并针对工具和频道适配器验证完整对象。非机密工具属性构成 __config;频道通过 config.get 获取非机密对象。标记为 x-secret = true 的属性会从两个公共接口中省略,仅可在授权的服务帧中通过 secrets.get("property") 获取。频道在一次调用中的公共和机密读取共享同一个规范修订版本,调用结束时宿主会丢弃该物化视图。符合要求的频道插件 必须 在每个使用点都解析这两类值,并且不得在来宾的热状态中保留配置或凭据值;将明文返回给来宾意味着宿主无法针对恶意代码强制实施不保留策略。如果请求了 config_read 但未实际获授予,宿主会验证一个空对象;因此,包含必需属性的模式会拒绝启动,而不会在缺少必需配置的情况下启动。如果空对象有效,工具会省略空的 __config,而频道的配置/机密导入会返回 access-denied;在授权帧之外的调用、解析失败以及宿主调用预算耗尽会返回 unavailable

功能

capabilities 是一个非空的 PluginCapability 值列表,定义在 crates/zeroclaw-plugins/src/lib.rs 中(序列化为 snake_case)。每个值要么选择插件导出的 WIT world(toolchannelmemory),要么命名一个可观测性后端(observer),要么标记一个仅限 markdown 的技能包(skill)。请阅读该枚举以获取规范集合;它是唯一的真实来源,本页面不再重复说明。

清单必须声明至少一个能力。除仅具有 skill 这一能力的插件外,每个能力都需要 wasm_path;该插件不携带 WASM 载荷,并且如果它省略了有效的 skills/ bundle,则会在发现时被拒绝(host.rs 中的 validate_manifest_shape)。

权限

permissions 是一个 PluginPermission 值列表,也定义在 crates/zeroclaw-plugins/src/lib.rs 中。请阅读该枚举以获取规范集合。

请注意声明与实际强制执行之间的差距:目前在组件宿主中,config_readhttp_client 会产生实际行为影响。请求 config_read 必须提供 config_schema,而在没有该权限的情况下声明此模式也会被拒绝。在使用工具或通道组件之前,宿主会解析其有效授权,将插件的操作员配置值具体化为类型化 JSON,并验证完整对象。runtime.rs 会在工具调用中注入经过验证的非机密值之前,移除调用方提供的任何 __config;标记为 x-secret: true 的顶层直接字符串属性会从公共配置中省略,并通过宿主作用域的 secrets 导入读取。工具会在 execute 期间接收该服务。通道会在 configure 和操作调用期间,通过 config.get 接收公共配置,并通过 secrets.get 获取机密信息;但实例化和静态元数据发现仍不可用。http_client 是必要的授权,但不是完整的权限决策:能力适配器还必须构造 HTTP 上下文并链接 wasi:http。工具和通道适配器会在授权验证后选择启用。内存适配器则有意不这样做,因此仅向内存作用域授予 http_client 不会增加任何网络能力面。其余变体(file_readfile_writememory_readmemory_write)已被清单模式接受,但尚未连接到宿主导入:单独声明它们不会授予任何权限。它们为将来用于控制这些权限的宿主函数保留名称(见下文的宿主导入)。

WIT 接口

插件契约是 wit/v0/ 中的 WIT 文件集合,包为 zeroclaw:plugin@0.1.0。在该包稳定之前,每个条目都受 @unstable(feature = plugins-wit-v0) 保护;兼容性规则请参见 wit/VERSIONING.md。下面的接口仅作概览;.wit 文件才是准确签名的权威来源。

世界

wit/v0/ 定义了三个 world,由 component.rs 中的 bindgen! 绑定。每个 world 都导入 logging(主机)并导出 plugin-info 及其主要接口:tool-plugin 导出 toolchannel-plugin 导出 channel,而 memory-plugin 导出 memory。此外,Tool 还导入 secrets;channel 导入 configsecretsinbound。每个 world 所需的(无默认值)导出项都列在其 .wit 文件中的 world 文档注释里。

tool 接口

wit/v0/tool.wit 定义了单工具接口。宿主在加载时调用一次 namedescriptionparameters-schema,然后在每次调用时分派 execute

record tool-result {
    success: bool,
    output: string,
    error: option<string>,
}

name: func() -> string;
description: func() -> string;
parameters-schema: func() -> json-string;
execute: func(args: json-string) -> result<tool-result, string>;

parameters-schema 返回一个呈现给 LLM 用于工具调用的 JSON Schema 字符串。execute 接收与该 schema 匹配的 JSON 编码参数,并返回一个 tool-result 或错误字符串。json-string 是来自 wit/v0/types.witstring 类型别名;调用方生成有效的 JSON,接收方对其进行解析。

channelmemory 接口

wit/v0/channel.witwit/v0/memory.wit 定义了受能力标志控制的接口面。宿主在加载时只调用一次 get-channel-capabilities / get-memory-capabilities,对于每个未设置的标志,它会使用 Rust trait 默认值,而不是调用插件。插件仍必须导出每个函数(返回文档中所述默认值的存根即可);宿主只是不调用那些对应标志缺失的函数。每个未设置标志所解析出的默认值都在 WIT 中 *-capabilities 标志旁的内联文档里注明,这是标志集合及其默认值的唯一事实来源。

功能标志

可选方法通过 flags channel-capabilitiesflags memory-capabilities 进行公开说明。由于 flags 是位掩码,可以在不破坏兼容性的情况下向 vN/ 包添加新的可选方法,并配套一个新的 @since 函数。移除或重命名 flag、function、field 或 variant case 都属于破坏性更改,并且需要新的 vN+1/ 目录。

主机导入

主机函数由插件导入,并由运行时提供。每个 world 的链接器都会接入 logging(通过 component_logging.rs 中的主机实现,该实现与 add_wasi 一起在 component.rs 中链接)。工具和通道会链接实例作用域内的 secrets 服务。通道还会导入 config,用于其类型化的公共对象;并导入 inbound,用于通过 poll-message 排空的、由主机馈送的消息队列。工具和通道适配器只有在获准的作用域授予 http_client 后,才会链接出站 wasi:httpcomponent.rs 中的 PluginStoreSpec::with_granted_httpadd_wasi_http)。内存既不提供上下文,也不提供链接器接口。文件系统和内存访问权限仍未生效:本应控制这些权限的主机函数尚未接入链接器。插件的环境权限包括 WASI 上下文(无预开放目录、无环境网络访问),以及其授权和适配器选择加入共同启用的、且仅限于这些主机导入。

ZeroClaw 所有的导入在每个由主机调度的服务帧中共享固定的安全预算。规范上限是 crates/zeroclaw-plugins/src/component.rs 中的 MAX_HOST_CALLS_PER_FRAME。预算耗尽后,日志记录变为空操作,入站轮询报告为空,读取公共配置或机密时返回 unavailable。新帧会重置预算。此上限是固定的主机策略,而不是重复的运维人员配置。

inbound

wit/v0/inbound.witchannel-plugin world 导入。channel 插件本身不运行监听器,因此由宿主运行监听器(webhook 服务器、供应商隧道或轮询客户端),并将接收到的每条消息入队。插件通过调用 inbound-poll 从其 poll-message 导出中清空队列,并可使用 inbound-pending 以批量方式清空:

inbound-poll: func() -> option<host-inbound-message>;
inbound-pending: func() -> u32;

主机侧为每个通道持有一个 InboundQueueWasmChannel::inbound 会将一个克隆体交给监听任务,因此入队的流量对插件的 drain 可见。

logging

wit/v0/logging.wit 被这三个 world 都导入。插件调用 log-record 将结构化事件发回宿主:

log-record: func(level: log-level, event: plugin-event);

调用是即发即忘的:它不返回任何内容,宿主(component_logging.rs)会吸收所有错误,因此日志写入失败绝不会导致插件执行崩溃。传递是异步的:该导入将记录交给有界的宿主端队列,由专用线程负责排空,并在不阻塞的情况下返回,因此缓慢或卡死的日志消费者绝不可能让 guest 导出调用持续超过 plugins.limits.call_timeout_ms。延迟处理不会改变事件的含义:每条记录都会捕获 guest 调用点当前的宿主 span,并在该作用域内写入,因此 agent/channel/tool 归因和终止标签与内联发出时一致。该上限是真正的内存上限,因为事件字段是无界字符串,会被复制到 guest 的 max_memory_mb 上限之外的宿主内存中:guest 控制的字节数超过 64 KiB 的记录会被丢弃,而不是截断;排队记录使用固定的 8 MiB 总字节预算,该预算仅在记录写入后释放。队列已满、记录超出上限或预算耗尽时,都会丢弃最新记录;排空线程会在每次写入后以及空闲唤醒时报告累计丢弃数,因此即使被拒绝的记录之后始终没有任何被接受的记录,丢失情况仍然可被观察到。plugin-actionplugin-outcome 对应 zeroclaw-log 中封闭的 Action / EventOutcome 分类体系;不存在用于规避限制的变体,这是有意为之。不要直接调用 wasi:logging,否则插件事件的格式会不一致,也无法到达 zeroclaw_log 写入的所有目标。

config

wit/v0/config.wit 由 channel world 导入。它以 JSON 格式返回当前通过架构验证的非机密对象:

get: func() -> result<json-string, config-error>;

该对象保留 config_schema 声明的类型;标记为 x-secret: true 的属性会被省略。该服务在 configure 期间和操作通道导出期间可用。当已准入的实例缺少有效的 config_read 授权时,它会返回 access-denied。在组件初始化或静态元数据发现期间发起的调用、解析器或验证失败,以及主机调用预算耗尽时,会返回 unavailable,且不会暴露内部详细信息。

config.get 是使用点访问,而不是加载时快照。符合要求的 channel 插件必须在每个使用配置的操作中调用它,并且不得在 warm guest 状态中保留其返回的对象。这是一项插件符合性规则:将 JSON 返回给受信任的 guest 代码后,主机无法阻止恶意组件复制它。

secrets

wit/v0/secrets.wit 由 tool 和 channel 世界导入。来宾仅提供顶层属性名称:

get: func(name: string) -> result<string, secret-error>;

主机从获准的 PluginInstanceScope 中派生出包、能力、绑定和有效授权;这些都不是来宾输入。只有清单架构中直接位于顶层且标记为 x-secret: true 的字符串属性可读。工具可以在主机调度 execute 时读取这些属性。通道可以在 configure 期间以及 send、poll、health 等运行调用和受能力控制的操作期间读取这些属性。组件初始化和静态元数据导出在不解析配置的情况下会返回 unavailable。在一个通道服务帧内,每个 config.getsecrets.get 都使用同一个已解析的规范配置版本;该帧会在所有退出路径上丢弃。因此,合规插件会在下一次操作中同时观察到同一绑定的公开值/机密值轮换。access-deniednot-foundunavailable 会刻意避免泄露任何解析器或架构细节。成功读取会以明文形式返回给受信任的来宾。该服务会防止公开值注入和跨实例选择;它不是一种会让值对插件代码保持隐藏的出站代理。合规的通道插件 must 在每个使用点解析机密,并且不得在热状态中保留第二份副本。主机无法在返回明文后强制执行不保留策略。

每个插件的配置(__configconfig.get

权限: config_read

插件不会读取进程环境变量。其清单必须将 config_read 与 Draft 2020-12 config_schema 配对;二者缺一均会导致清单无效。模式根必须是一个包含 properties 映射且 additionalProperties = false 的对象。每个顶层属性必须直接声明为 stringbooleanintegernumberarrayobject 之一,或通过包本地 JSON Pointer 声明为其中之一。工具和通道使用方可以在直接的顶层字符串属性上设置 x-secret: true;嵌套标记、值为 false 的标记或非布尔标记,以及非字符串的机密属性,都会被拒绝。未知键、格式错误的编码以及约束违规会在值传递给来宾代码之前拒绝该实例。

操作员的规范 plugins.entries.<instance-key>.config 值在内存中仍是带有机密标记的字符串映射,并在持久化时加密。主机根据完整的软件包、能力和绑定标识派生版本化的 zpi1_… 条目键;这样,不同的软件包和能力域就可以安全地复用 main 等别名。获准的软件包清单决定模式。string 值直接存储;booleanintegernumber 值使用 JSON 标量文本,例如 "true""4""0.5"arrayobject 值使用 JSON 文本,例如 '["urgent","ops"]''{"region":"us-east"}'。主机在每次使用时将完整数据具体化为类型化 JSON 并进行验证,然后对每个属性恰好划分一次。对于工具,非机密值会注入保留的 __config 键下:

{
  "提示": "a sunset",
  "__config": {
    "retry_limit": 4,
    "已启用": true,
    "labels": ["紧急", "ops"]
  }
}

如果其架构将被省略的 api_key 标记为机密,则会通过 secrets.get("api_key") 显式读取。runtime.rs 会在注入公共部分之前剥离调用方提供的 __config,因此无法伪造该部分。工具的公共配置注入和同一 execute 帧内的机密读取共享同一个已解析的实时配置版本;该帧会在成功、错误、陷阱、panic 或取消时丢弃。通道的 configure 导出没有配置参数。它会调用 config.get 获取公共对象,并调用 secrets.get 获取机密属性;后续每个使用配置的操作导出也同样如此。一次调用中的两个导入共享同一个已解析版本。宿主会在成功、错误、陷阱、panic 或取消时丢弃该物化视图。因此,当符合规范的来宾在使用点解析这两者时,同一逻辑绑定中的公共配置和凭据变更会在下一次操作中同时可用。

当清单请求 config_read,但主机实际上未授予该权限时,解析会用空对象替代,并在来宾代码运行前验证该对象。包含必填字段的架构会在构造期间安全失败。如果空对象有效,工具会省略空的 __config,而通道 config.getsecrets.get 会返回 access-denied。插件始终只能看到自己的配置节。

频道静态元数据导出项无法调用任一配置服务,并且只在加载时读取一次。因此,更改机器人/账户身份或任何配置派生的能力、自身句柄、提及方式或多消息延迟,都需要重建频道生命周期;而同一逻辑绑定的普通公共配置和凭据轮换则不需要。工具和频道是当前的配置使用者。memory 世界尚未支持配置导入,因此在该 ABI 和运行时接入完成之前,memory 插件不得请求 config_read

WASI 组件宿主

主机(crates/zeroclaw-plugins/src/component.rs)会针对单个异步 wasmtime::Engine 编译并实例化组件。.wasm 文件的加载方式取决于构建的执行后端:

  • plugins-wasm-cranelift:存在 JIT 后端,因此 load_component 会在加载时通过 Component::from_file 编译一个 .wasm 组件。
  • No JIT backend (plugins-wasm-pulley or runtime-only):二进制中没有编译器,因此 load_component 会直接通过 Component::deserialize_file 反序列化该文件,将其视为由匹配的 wasmtime 生成的预编译 .cwasm。版本不匹配的制品会在反序列化的版本检查中被拒绝。

这两个后端功能都会引入 plugins-wasmtime;加载路径取决于构建中是否包含 cranelift 编译器,而不是取决于 pulley。

每次调用的执行限制

每个 guest 导出都在主机为 store 应用的按调用资源限制下运行。引擎启用燃料计量,并为每次调用分配全新的燃料预算,因此失控或恶意组件会触发陷阱,而不会让主机挂起。主机还会为完整的导出 future 设置挂钟截止时间,其中包括等待 wasi:http 等异步主机导入的时间;定期的燃料让出可确保不间断的 guest 计算不会使该计时器得不到调度,而 guest 可访问的主机导入绝不会阻塞执行器(日志记录会交给有界队列,并由专用主机线程写入),因此在主机工作运行期间仍可观察到该截止时间。StoreLimits 上限限制线性内存、表元素和实例数量。工具 world 每次 execute 都会获得一个全新的 store;warm channel 和 memory store 会在每次调用前重新补充燃料,因此长生命周期插件获得的是全新的预算,而不是在其生命周期内逐渐耗尽预算。

这五项限制可由操作员调节,且每个值都会验证为非零:plugins.limits.call_fuel(默认值为 1,000,000,000 个指令单位)、plugins.limits.call_timeout_ms(默认值为 30,000 毫秒)、plugins.limits.max_memory_mb(默认值为 256)、plugins.limits.max_table_elements(默认值为 100,000)和 plugins.limits.max_instances(默认值为 64)。存储只能使用显式限制构建,因此任何加载路径都无法构造未受沙箱隔离的插件。来宾端 wasi:http 请求选项可以更早结束调用,但无法延长主机截止时间。中断的预热存储永远不会恢复:通道会在下一次调用时根据主机拥有的输入重新创建它,而内存实例在其所有者重建它们之前仍不可用。规范字段及默认值位于配置参考

32 位地址空间(wasip2 是 wasm32)

插件目标是 wasm32-wasip2,而宿主引擎是在启用 fuel 计量(Config::consume_fuel(true))且未启用 wasm_memory64 的情况下构建的。插件边界是固定的 32 位格式,这带来一些值得明确说明的后果:

  • 客体地址空间是 32 位。 插件运行在 wasm32 线性内存中。大值通过按值跨越边界:通道插件的 media-attachmentlist<u8> 形式携带其完整字节,而 wit/v0/channel.wit 已经指出这可能达到数兆字节,并将资源句柄模型留给未来修订。在这 32 位空间内,宿主从 plugins.limits.max_memory_mb(默认 256)对每个 store 施加显式内存上限,因此客体受限于 wasm32 地址空间与该 ZeroClaw 配置上限中的较小者。
  • 该组件 ABI 将偏移量降为 32 位,而不论宿主字长大小。 即使在 64 位宿主上,规范 ABI 中的列表和字符串偏移量也是 i32memory64 扩展的是 guest 的线性内存寻址,而不是组件模型的规范 ABI,因此启用它也不会使 WIT 层字段变为 64 位。
  • 没有可供绑定的 64 位 wasip2 目标。 wasm32-wasip2 是目前 rustc 和 LLVM 中唯一的 WASI Preview 2 目标;插件无法编译为 64 位 p2 组件,因此即使引擎启用了 memory64,主机也没有可加载的内容。

这是上游工具链约束,而不是本仓库中的某个标志可以解除的主机限制。当 64 位 p2 目标和更宽的组件 ABI 在上游落地时,bindgen! 接缝会针对它们重新生成,字段宽度也会在 wit/VERSIONING.md 窗口内的 WIT 中重新审视。在那之前,请将插件边界按设计视为 32 位。

签名

插件清单可能包含 Ed25519 签名(crates/zeroclaw-plugins/src/signature.rs)。该签名是对规范化清单字节进行 base64url 编码的结果(解析后的 TOML,仅移除根级别中名称完全为 signaturepublisher_key 的条目);发布者的公钥采用十六进制编码。嵌套架构属性中使用这些名称的属性仍会被签名。主机根据 plugins.security.signature_mode 强制执行三种模式之一:

模式未签名插件不受信任或无效的签名
strict已拒绝已拒绝
permissive带有警告加载带有警告加载
disabled已加载未检查

验证会在发现和安装两个阶段运行。发现阶段会跳过未通过其策略的插件,而不会中止整个主机;安装阶段则会返回该错误。

使用 Rust 编写插件

插件是一个面向组件模型的 cdylib crate。请从宿主使用的同一个 wit/v0 package 生成 guest bindings,实现导出的 world,并编译为 wasm32-wasip2。从空 crate 到已安装插件的完整实操教程,请参见 plugin guides;下面的说明涵盖构建和安装机制。

构建

sh

# 安装 WASI Preview 2 target(一次)
rustup target add wasm32-wasip2

# 构建组件
cargo build --target wasm32-wasip2 --release

输出组件位于 target/wasm32-wasip2/release/<crate_name>.wasm。将它复制到你的 manifest.toml 旁边。对于仅运行时的宿主构建(不含 JIT 后端),请使用匹配的 wasmtime 将该组件预编译为 .cwasm 并改为分发,因为这类宿主在加载时会反序列化,而不是编译。

宿主的工具插件测试不依赖已发布的制品:crates/zeroclaw-plugins/tests/fixtures/tool-fixture 是一个在测试时从源代码构建的源码树内组件,而 reference_plugin.rsreference_plugin_e2e.rs 会通过守护进程使用的相同 PluginHost、config_schema 和配置解析路径驱动它。如果无法构建该 fixture,这些测试就会失败。

安装中

sh

# 复制到插件目录
zeroclaw plugin install /path/to/my-plugin/

# 或手动
cp -r my-plugin/ ~/.zeroclaw/plugins/my-plugin/

配置

目前,操作员提供的值通过通用字符串映射存储:在 TOML 中编辑 [[plugins.entries]],或在工具的 install 操作预置其默认绑定条目后使用 zeroclaw config setzeroclaw plugin info <package> 会打印相同的工具键,供迁移和后续编辑使用。这些自动打印和预置功能仅适用于工具。渠道键取决于其配置的别名,而 install 和 info 并不负责管理该别名。支持别名的构造逻辑已在 #10146 中合并,该逻辑会根据已配置的别名解析渠道的类型化配置;渠道键的自动显示和 install 时的预置在 #9584 中的授权流程完成前仍需手动进行,因此,仅支持渠道的包仍无法仅通过 install 和 info 完成此次迁移。基于 Schema 的表单和内联字段帮助目前尚未实现。当前的入口包括:

  • CLI 通过 listsearchinstallremoveinfomigrate 管理插件生命周期。zeroclaw config set 写入单个原始插件值;它不会解析插件的架构。
  • zerocode 可以编辑 ZeroClaw 的静态插件主机设置,但目前还不会根据 config_schema 为每个插件生成字段。
  • Web 网关对插件是只读的:GET /api/plugins 会报告已加载的插件以及系统是否已启用。
  • 主机在接纳软件包时验证 config_schema,并在来宾使用前再次验证并实例化操作符值。
  • 对于插件作者而言,清单模式是来宾边界处唯一的类型和验证契约。在其中定义所有受支持的键及约束;不要在宿主运行时配置结构体中重复该契约。来宾代码应将经过宿主验证的 JSON 反序列化为其本机类型化结构体。

静态配置架构提供通用的存储和机密标记路径,而不是按插件动态生成的编辑器。crates/zeroclaw-config/src/schema.rs 中的插件配置类型带有 #[prefix = "plugins"]#[prefix = "plugins.entries"]#[prefix = "plugins.security"],而 Configurable 派生会将每个带前缀的字段转换为通用配置路径。机密字段(插件条目的 config 映射标记为 #[secret])会使用相邻的 .secret_key 进行静态加密。主机配置的规范字段、默认值和 signature_mode 值位于配置参考中;该架构是权威来源,而每个插件清单则是其私有配置结构的权威来源。

构建功能

插件宿主是编译时可选启用的。工作区 Cargo.toml 中的二进制级功能决定是否完全内置插件,以及随附哪种执行后端:

  • plugins-wasm 是一个总括特性,会将插件宿主及其运行时集成引入二进制文件。下面的每个后端特性都隐含启用它,因此启用任何执行后端(例如 --features plugins-wasm-cranelift)都会始终包含插件宿主及其 CLI 接口;仅后端构建不会悄然生成一个不带 plugin 子命令的二进制文件。仅启用该总括特性等同于 plugins-wasm-runtime-only:没有 JIT,因此只能加载预编译的 .cwasm 组件。
  • plugins-wasm-runtime-only 是最小且启动最快的:没有 JIT,因此组件会从预编译的 .cwasm 反序列化。
  • plugins-wasm-cranelift 添加了 Cranelift JIT,因此 .wasm 组件会在加载时编译。
  • plugins-wasm-pulley 是最具可移植性的,支持在 Cranelift 不支持的目标上编译。

这些委托给 zeroclaw-plugins crate 的功能(plugins-wasmtimeplugins-wasm-craneliftplugins-wasm-pulley),它们会连接 wasmtime。加载路径会根据构建中是否包含 Cranelift 编译器而定,如 WASI Component Host 中所述。请阅读工作区 Cargo.toml 中的功能注释以获取权威说明。