编写工具插件
这是本系列的入门指南:从空 crate 到模型在对话中调用的工具的完整实操路径。这里构建的工具是 redact,它会对文本中的电子邮件、已知凭据前缀以及由操作员提供的模式进行掩码处理。它刻意采用配置驱动,因为读取自己的受限配置部分是每个非平凡插件都需要做的事情,也是最容易出错的事情。
本页上的所有内容都根据契约源代码进行了核对:wit/v0/tool.wit 中的 tool-plugin world、crates/zeroclaw-plugins/src/runtime.rs 和 wasm_tool.rs 中的宿主侧调用路径,以及 host.rs 中的清单校验。源路径是指向 ZeroClaw 仓库的引用,用于验证;插件本身是你自己的仓库中的你自己的 crate。构建插件时,你完全不需要检出 ZeroClaw,只需要 wit/ 契约文件(在第 1 步获取)以及一个已安装、并已编译进插件宿主的 zeroclaw 二进制来运行它。
确认你使用的二进制文件。 64 位桌面版和服务器版的发布归档包含插件宿主以及加载
.wasm组件的编译器,每次发布前都会在这些目标上运行一个插件,然后才正式发布。32 位 ARM 归档和实验性 Android 归档不包含这些内容(其中zeroclaw plugin ...是无法识别的子命令),并且plugins-wasm不在 crate 的默认功能集中,因此使用 Cargo 默认设置的源码构建也没有宿主。对于这些情况,请从源码构建宿主端并启用一个执行后端。每个后端功能本身都包含plugins-wasm总开关,因此只需一个标志即可:cargo build --release --features plugins-wasm-cranelift协议页面记录了后端选项,并列出了每个发布产物的插件支持情况。
工具调用的流程如何运作
在编写代码之前先了解运行时形状:
- 启动时,发现机制会找到插件目录,验证清单结构,执行签名策略,然后验证
config_schema。注册之前,宿主会将插件的操作器值具体化为带类型的 JSON,并对其进行验证。通过验证的插件会成为 WasmTool 实例。 - 注册时,主机会实例化该组件一次,以读取
name、description和parameters-schema。这些信息会被缓存,之后不会再次请求。如果此次探测失败,注册也会失败;主机绝不会为出问题的组件替换为合成元数据。 - 每次调用时,
WasmTool::execute都会从规范状态解析并验证配置,创建一个全新的存储(新的 WASI 上下文、新的 fuel 预算,不包含上一次调用的状态),并实例化组件。这个解析后的对象将服务于整个帧:主机仅会将其非机密值注入__config,通过作用域限定的secrets导入提供架构标记的机密值,然后调用execute。
fresh-store-per-call 模型是最重要的设计约束:工具插件在设计上是无状态的。任何你想在调用之间持久保存的内容,都必须存在于插件之外(在你返回的文本中,或在 operator 配置中)。
1. Crate 设置
创建 crate 并添加 guest 端依赖项:
cargo new --lib my-plugin
cd my-plugin
cargo add wit-bindgen@0.46
cargo add serde --features derive
cargo add serde_json
然后对包清单进行两处手动编辑:
- 将库的
crate-type设置为["cdylib", "rlib"]。cdylib是组件构建生成的内容;rlib允许同一个 crate 的纯逻辑模块在主机上原生编译并进行单元测试。 - 在发布配置中,设置
opt-level = "s"、lto = true和strip = true。组件大小就是下载和加载时间;没有理由在插件边界传递调试符号。
将 ZeroClaw 仓库中的 wit/v0/ 目录复制到 crate 根目录下的 wit/。你不需要完整检出;只需从与你的目标主机版本匹配的 tag 中获取该目录:
git clone --depth 1 --filter=blob:none --sparse \
https://github.com/zeroclaw-labs/zeroclaw /tmp/zeroclaw-wit
git -C /tmp/zeroclaw-wit sparse-checkout set wit
cp -r /tmp/zeroclaw-wit/wit .
WIT 文件就是 ABI:宿主是根据这些精确的文件生成其绑定的,因此你的 guest 绑定也必须来自同一组文件。请锁定版本:WIT world 会随宿主演进,而基于比宿主绑定更高版本的 world 构建的组件将无法实例化。
2. 将逻辑与胶水代码分离
将实际行为放在一个没有 wit-bindgen 导入的普通 Rust 模块中,并保持组件胶水层尽量薄。原因是可测试性:组件目标不能原生运行 cargo test,因此被困在胶水层中的逻辑只能通过 wasm 主机进行端到端验证。胶水层应当薄到不太可能出错。
src/redact.rs 保存了一个配置结构体和一个纯函数:
#![allow(unused)]
fn main() {
pub const DEFAULT_REPLACEMENT: &str = "[REDACTED]";
/// 脱敏策略根据插件自身的配置段解析。
#[derive(Debug, serde::Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct RedactConfig {
pub replacement: String,
pub redact_emails: bool,
pub patterns: Vec<String>,
}
impl Default for RedactConfig {
fn default() -> Self {
Self {
replacement: DEFAULT_REPLACEMENT.to_string(),
redact_emails: true,
patterns: Vec::new(),
}
}
}
/// 对输入进行脱敏。返回输出以及被遮蔽片段的数量。
pub fn redact(input: &str, cfg: &RedactConfig) -> (String, usize) {
// 当 cfg.redact_emails、凭据前缀
//(sk-、ghp_、AKIA、xoxb-)以及 cfg.patterns 中的每个字面量匹配时进行掩码,
// 用 cfg.replacement 替换每个命中项。
// ...
}
}
guest 会接收由 schema 生成的公开 JSON 对象,因此只需将其反序列化一次,而不是重复进行字符串解析。此示例的 schema 将每个字段都设为可选,当主机提供 {} 时,其行为由 Default 负责。操作员尚未配置插件,或主机拒绝所请求的 config_read 授权时,空对象都是正常情况。如果插件没有某个值就无法运行,请在 config_schema 中将其标记为必需;这样,主机就会在 guest 代码启动前拒绝空对象。
3. 实现 world
wit/v0/tool.wit 定义了你必须导出的接口面。world 是:
world tool-plugin {
import logging;
import secrets;
export plugin-info;
export tool;
}
而 tool 接口包含四个函数:
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>;
src/lib.rs 生成 guest 绑定并实现这两个导出:
#![allow(unused)]
fn main() {
pub mod redact;
#[cfg(target_family = "wasm")]
mod component {
wit_bindgen::generate!({
path: "wit/v0",
world: "tool-plugin",
features: ["plugins-wit-v0"],
});
use crate::redact::{redact, RedactConfig};
use exports::zeroclaw::plugin::plugin_info::Guest as PluginInfo;
use exports::zeroclaw::plugin::tool::{Guest as Tool, ToolResult};
use zeroclaw::plugin::logging::{
log_record, LogLevel, PluginAction, PluginEvent, PluginOutcome,
};
struct RedactPlugin;
#[derive(serde::Deserialize)]
struct ExecuteArgs {
text: String,
#[serde(rename = "__config", default)]
config: RedactConfig,
}
impl PluginInfo for RedactPlugin {
fn plugin_name() -> String {
"my-redact-plugin".to_string()
}
fn plugin_version() -> String {
"0.1.0".to_string()
}
}
impl Tool for RedactPlugin {
fn name() -> String {
"redact".to_string()
}
fn description() -> String {
"在文本到达日志、通道或模型之前,对其中的机密和 PII 进行编辑。屏蔽电子邮件、凭据前缀以及运维人员配置的字面模式。"
.to_string()
}
fn parameters_schema() -> String {
serde_json::json!({
类型: 对象,
属性: {
"文本": {
类型: "字符串",
"描述": “要遮盖的文本。”
}
},
“required”: ["文本"]
})
.to_string()
}
fn execute(args: String) -> Result<ToolResult, String> {
let parsed: ExecuteArgs = match serde_json::from_str(&args) {
Ok(a) => a,
Err(e) => {
return Ok(ToolResult {
success: false,
output: String::new(),
error: Some(format!("无效参数:{e}")),
});
}
};
let (output, count) = redact(&parsed.text, &parsed.config);
log_record(
LogLevel::Info,
&PluginEvent {
function_name: "my_redact_plugin::tool::execute".into(),
action: PluginAction::Complete,
outcome: Some(PluginOutcome::Success),
duration_ms: None,
attrs: Some(format!("{{\"redactions\":{count}}}")),
message: "redacted input".into(),
},
);
Ok(ToolResult { success: true, output, error: None })
}
}
export!(RedactPlugin);
}
}
契约点,每个都锚定在宿主源中:
plugin-info是每个 world 的必需导出。 它会报告组件自身的名称和版本。请使二者与清单保持同步。- 元数据只读取一次。
call_tool_metadata在runtime.rs中于注册时读取name、description和parameters-schema,并将它们缓存。不要从任何动态内容计算它们;它们之后不会被重新观察到。 - 模式是模型对你的工具的全部视图。 主机在加载时将其解析为 JSON(
tool parameters-schema is not valid JSON是硬性注册失败),并将其逐字转发给 LLM。描述每个属性。绝不要在其中声明__config:该键由主机保留,主机会在注入前精确地剥离任何调用方提供的值,以免模型冒充你的操作员。 success: false与Err。success: false的ToolResult会作为普通工具响应返回给模型,模型可以据此作出反应(使用修正后的参数重试、道歉、选择另一个工具)。Err(String)会作为插件故障跨越边界:宿主会将其包装为plugin execute returned error,并使调用失败。请将Err保留给真正损坏的状态,并通过success: false报告错误输入。- 始终通过导入的
logging接口记录日志,绝不要使用wasi:logging。log-record是即发即弃的;宿主会吸收所有错误,因此失败的日志写入绝不会让你的调用崩溃,而且事件会进入zeroclaw_log写入的每个目标,并携带你的调用所运行于其下的宿主 span 的zeroclaw.*归因(agent_alias、session_key、provider、channel)。请注意plugin-event上的attrs字段不是归因:它是日志行的自由形式attributes载荷。归因与别名绑定,继承自宿主侧的环境 tracing span,插件发送的任何内容都不能设置或覆盖它。PluginAction和PluginOutcome是与宿主分类法对应的封闭枚举;刻意不提供自由形式变体。请选择最接近的项。
4. __config 沙箱
插件不会读取进程环境变量,也永远看不到全局配置。请求 config_read 的清单还必须声明 config_schema;没有该权限的架构同样无效。该架构采用 Draft 2020-12,其根必须是包含 properties 映射和 additionalProperties = false 的对象,并且每个顶层属性都必须显式解析为 string、boolean、integer、number、array 或 object。
宿主会解析存储在根据此实例的包、tool 能力和绑定派生出的版本化配置条目键下的部分,根据包模式将其实例化,验证完整的类型化对象,然后才对其进行分区。只有非机密属性会在保留的 __config 键下合并到 execute 中:
- 模型提供的参数中任何已存在的
__config都会先被删除。伪造在结构上是不可能的。 - Operator 的存储仍然是加密的字符串映射。直接存储字符串;将布尔值和数字编码为 JSON 标量(
"true"、"4"、"0.5"),将数组和对象编码为 JSON('["secret-a","secret-b"]')。来宾会收到真正的 JSON 布尔值、数字、数组和对象,而不是这些存储字符串。 - 标记为
x-secret = true的直接顶层字符串属性不会包含在__config中。使用生成的zeroclaw::plugin::secrets::get函数显式读取它。嵌套标记、值为 false 或非布尔值的标记,以及机密非字符串属性都会导致清单准入失败。 - 主机仅在分派
execute期间启用机密读取。来自组件初始化或元数据导出的调用会返回unavailable,而不会解析配置。一次执行期间,公开的__config和机密读取使用相同的已解析配置视图。 - 如果请求了
config_read,但未实际授予该权限,主机会将其解析为{}并进行验证。因此,此示例中的可选架构会使工具省略__config,而#[serde(default)]会选择RedactConfig::default。必需的架构会安全失败,而不会在没有凭据的情况下运行。 - 未知键、无效的 JSON 编码、类型错误以及架构约束失败,会在插件代码运行前拒绝该插件。运维人员目前通过 TOML 或通用的
zeroclaw config set路径,在安装时打印的实例键下设置值;这些值会使用配置的密钥进行静态加密。基于架构的 zerocode 和网关编辑器属于未来的 SDK/配置界面工作。
对于此工具,类型化部分有三个可选键:replacement 是字符串,redact_emails 是布尔值,patterns 是字符串数组。
5. 清单
manifest 是插件目录中名为 manifest.toml 的文件。其字段是 crates/zeroclaw-plugins/src/lib.rs 中 PluginManifest 的 serde 表面,这是唯一的事实来源:
| 字段 | 必填 | 含义 |
|---|---|---|
name | 是 | 唯一的规范包 slug,也是每个派生实例配置键中的包组件。它本身不是操作符配置键。使用 1–128 个小写 ASCII 字符;以 [a-z0-9] 开头和结尾,中间只能包含 [a-z0-9._-]。发现过程会拒绝无效或重复的名称。 |
version | 是 | 版本字符串,例如 0.1.0。 |
description | 不 | 由 zeroclaw plugin list 显示的人类可读描述。 |
author | 不 | 作者姓名或组织。 |
wasm_path | 用于 WASM 能力 | 组件文件名,相对于插件目录。除非唯一的 capability 是 skill,否则为必填。如果指定的文件不存在,发现过程会跳过该插件。 |
wasm_sha256 | 在 WASM 能力的严格模式下 | 十六进制的 SHA-256 值,对应组件的精确字节内容。若存在该值,则始终强制校验;严格签名策略要求必须提供该值,以便已签名的清单能够绑定被准入的可执行文件。 |
capabilities | 是,非空 | 插件类型:tool、channel、memory、observer、skill 中的任意一个(PluginCapability,序列化为 snake_case)。 |
provides | 不 | 面向内置通道的 channel 插件(drop-in):它所镜像的蛇形命名配置 ID,例如 telegram、gmail_push。宿主会为每个已配置、已启用,且被已启用 agent 路由到的 [channels.<id>.<alias>] 准入一个逻辑实例,因此规范的通道配置仍是这些设置的唯一归属。需要 config_read。plugin 系列为保留项,会被拒绝;本构建未定义的 ID 会被拒绝;两个已安装包声明同一 ID 时,两者都会被拒绝。未设置(默认)时,表示从 [channels.plugin.<alias>] 配置的新通道。镜像已准入但尚未构建,参见 crates/zeroclaw-runtime/src/plugin_runtime.rs 中的激活计划。 |
permissions | 不 | 宿主服务(代码可能访问的服务):http_client、config_read、state_read、state_write、file_read、file_write、memory_read、memory_write(PluginPermission)。HTTP、配置和精确实例状态权限仅对工具/通道适配器强制执行;文件系统和内存权限目前不生效。声明 config_read 时必须同时声明 config_schema。 |
config_schema | 恰好使用 config_read | 为此插件的私有配置起草一份 Draft 2020-12 JSON Schema。它包含在规范清单字节中,因此清单签名时会一并覆盖。根对象必须是带有 properties 映射且 additionalProperties = false 的对象。每个顶层属性必须直接或通过本地 JSON Pointer 明确指定一种受支持的类型:string、boolean、integer、number、array 或 object。工具和通道的使用方可以直接在顶层 string 属性上设置 x-secret = true,使其从公共配置中移除,并通过限定作用域的 secrets.get 宿主导入提供。secret 属性名是 1–128 字节的可移植 ASCII 名称,只能包含字母、数字、_、- 或 .。工具通过 __config 接收公共配置,并可在 execute 期间读取 secret。通道在 configure 和运行时操作调用期间,通过 config.get 读取当前公共对象,并通过 secrets.get 读取 secret;这两个导入在实例化和静态元数据发现期间均不可用。嵌套的、值为 false 或非布尔值的 secret 标记,以及非字符串的 secret 属性都会被拒绝。没有 config_read 的 schema,或没有 schema 的 config_read,都会被拒绝。 |
signature | 不 | 对规范化清单字节进行的 Base64url Ed25519 签名。用于发布签名时设置。 |
publisher_key | 不 | 签名者的十六进制编码 Ed25519 公钥。 |
egress | 不 | 可选的 [egress] 表,包含一个 hosts 列表:声明该插件所需访问的目标地址,可以是精确主机名(api.example.com)或显式的后缀模式(*.example.com,匹配子域名但不匹配顶级域名本身)。该表属于规范化的清单字节内容,因此在清单签名时会被一并覆盖,修改它需要重新签名。无效语法会在发现和安装阶段导致整个清单被拒绝。缺失、为空以及“未声明任何内容“三种状态视为相同。 |
只声明代码实际使用的权限。未声明的权限是组件无法触达的宿主表面;不必要声明的权限则是你主动增加的攻击面,也是审查你的插件的人需要承担的审核负担。
[egress] 是一项声明,而非授权。清单文件本身不赋予任何网络访问权限:主机强制执行的白名单是运维人员在该实例自身的 zpi1_… 记录上设置的 plugins.entries[].egress_hosts,因此即使未签名的组件自行写入了 [egress] 表,也无法访问任何地址。声明的作用在于安装流程:zeroclaw plugin install 会将其写入它新建的记录中,并打印已授予的内容;对于已存在的记录,它从不会扩大其权限。参见 Declaring and granting egress。
操作方提供的值在 plugins.entries 中仍保持为字符串,并会在持久化时加密,以一个由宿主拥有的包、能力和绑定标识派生的版本化 zpi1_… 字符串作为键(安装过程会打印并初始化默认工具绑定的完整实例密钥):字符串按原样存储,布尔值和数字使用 JSON 标量文本,数组和对象使用 JSON 文本。在任何来宾代码运行之前,宿主会将这些字符串物化为包模式规定的类型,并针对工具和频道适配器验证完整对象。非机密工具属性构成 __config;频道通过 config.get 获取非机密对象。标记为 x-secret = true 的属性会从两个公共接口中省略,仅可在授权的服务帧中通过 secrets.get("property") 获取。频道在一次调用中的公共和机密读取共享同一个规范修订版本,调用结束时宿主会丢弃该物化视图。符合要求的频道插件 必须 在每个使用点都解析这两类值,并且不得在来宾的热状态中保留配置或凭据值;将明文返回给来宾意味着宿主无法针对恶意代码强制实施不保留策略。如果请求了 config_read 但未实际获授予,宿主会验证一个空对象;因此,包含必需属性的模式会拒绝启动,而不会在缺少必需配置的情况下启动。如果空对象有效,工具会省略空的 __config,而频道的配置/机密导入会返回 access-denied;在授权帧之外的调用、解析失败以及宿主调用预算耗尽会返回 unavailable。
对于此插件:name 和 version 需与 plugin-info 报告的内容一致,wasm_path 命名你将与其一起发布的组件文件,capabilities 需恰好包含 tool,permissions 需恰好包含 config_read。仅当你的工具发起出站 HTTP 调用时,才添加 http_client。工具适配器实现了 wasi:http,但仅在该授权通过验证后才会进行链接;若缺少适配器支持和授权二者之一,则不存在 HTTP 接口。
类型化 RedactConfig 的匹配清单契约为:
name = "my-redact-plugin"
version = "0.1.0"
wasm_path = "my_redact_plugin.wasm"
capabilities = ["tool"]
permissions = ["config_read"]
[config_schema]
"$schema" = "https://json-schema.org/draft/2020-12/schema"
type = "object"
additionalProperties = false
[config_schema.properties.replacement]
type = "string"
minLength = 1
[config_schema.properties.redact_emails]
type = "boolean"
[config_schema.properties.patterns]
type = "array"
items = { type = "string" }
这些属性是可选的,与来宾的默认值一致。对于必须存在的凭据,将其名称添加到 [config_schema] 中的 required;授权被拒绝或值缺失将阻止组件启动。
调用网络的工具
可以说,现实世界中最常见的工具形态并不是像 redact 这样的纯转换,而是连接外部 API 的桥接器:在清单中声明 http_client,通过限定作用域的机密服务读取凭据,并发起出站请求。在已签名的架构中标记该凭据:
[config_schema]
required = ["api_key"]
[config_schema.properties.api_key]
type = "string"
minLength = 1
x-secret = true
相较于本指南,缺少的是一个可在组件内部工作的 HTTP 客户端:reqwest 及其同类库无法做到这一点,因为这里没有套接字接口,只有 wasi:http。已知可与此宿主配合工作的客户端是 waki,它是阻塞式的,因此可以直接适配 execute 的同步签名。将其添加并设置为仅在组件目标下启用,这样你的纯逻辑模块仍可在原生环境中测试:
cargo add waki --target 'cfg(target_family = "wasm")'
在解析公开的 __config 后,execute 内部调用的形式:
#![allow(unused)]
fn main() {
let api_key = zeroclaw::plugin::secrets::get("api_key")
.map_err(|_| "api_key 不可用".to_string())?;
let resp = waki::Client::new()
.get("https://api.example.com/search")
.query([("q", term.as_str())])
.header(授权, format!("Bearer {api_key}"))
.connect_timeout(std::time::Duration::from_secs(5))
.send()
.map_err(|e| format!("请求失败:{e}"))?;
}
两个看起来像破坏性变更但实际上不是的版本事实:waki 随自身 vendored 的 wit-bindgen(0.34)一起提供,而你的 world bindings 使用的是 0.46;两者可以并存,各自生成自己的 bindings。并且 waki 发出的是 wasi:http@0.2.4 imports,而当前工具链基线是 @0.2.6;host 可以无问题地同时链接两者。两者都无需处理。
这一版本事实确实构成破坏性变更:你 vendor 的 world 必须与宿主导出的 world 完全一致。wit/v0 仍处于实验阶段,可能会在相同的 @0.1.0 版本下新增变体(例如 memory-audit 就是 zeroclaw:plugin/logging 的 plugin-action 枚举中的一个情况)。基于旧版副本构建的组件可能编译通过,但在实例化期间失败。宿主会保留 Wasmtime 的错误链,并附加一条有条件触发的 WIT 漂移提示;请检查该错误链,然后将你 vendor 的 wit/v0 与你所运行的宿主版本中的 WIT 进行 diff 对比。
记住 概述 中的信任框架:http_client 要么全部允许,要么全部禁止。沙箱不会限制已授予的插件将数据发送到哪里,因此运行 strict 签名策略的操作者信任的是你的代码,而不是 URL 允许列表。
6. 原生测试逻辑
因为 redact.rs 没有 wasm 依赖,普通的 cargo test 会在主机上覆盖它:
#![allow(unused)]
fn main() {
#[test]
fn empty_config_falls_back_to_defaults() {
let cfg: RedactConfig = serde_json::from_str("{}").unwrap();
let (out, n) = redact("mail me at a@b.example", &cfg);
assert_eq!(n, 1);
assert!(out.contains("[REDACTED]"));
}
}
至少涵盖:jail 情况(空节)、已配置情况,以及没有任何需要屏蔽内容时文本的透明透传。粘合层转发的每一种行为都应能在这里得到证明,而且完全不需要看到 wasm 工具链。
7. 构建
先安装一次 WASI Preview 2 目标,然后构建组件:
rustup target add wasm32-wasip2
cargo build --release --target wasm32-wasip2
该组件位于 target/wasm32-wasip2/release/<crate_name>.wasm(crate 名称中的连字符会变成下划线)。在组装插件目录时,请将其重命名为你的清单中 wasm_path 声明的名称。
[!IMPORTANT] 编译后的
.wasm和.cwasm文件是二进制构件,通常每个都有数 MB。不要在没有 Git LFS 的情况下将它们提交到 git 源树中:每次重建都作为普通 blob 提交,会永久膨胀仓库历史,并且git diff/审查工具会因此卡住。把它们当作其他构建输出一样处理:将target/和*.wasm/*.cwasm添加到.gitignore,并改为通过发布构件或插件注册表归档分发。如果某个构件确实必须留在树中,请在首次提交之前使用 LFS 跟踪该模式(git lfs track "*.wasm")。
如果目标主机是仅运行时构建(未编译 JIT 后端),它在加载时无法编译 .wasm;而是反序列化预编译的 .cwasm。请使用与主机版本匹配的 wasmtime CLI 进行预编译,并将 .cwasm 作为 wasm_path 构件分发。版本不匹配的构件会被 wasmtime 的反序列化检查拒绝,而不会被静默错误加载。
8. 安装并验证
这些命令需要一个编译了插件宿主的二进制文件。 64 位桌面和服务器目标的发布归档包(x86_64 和 aarch64 的 Linux,包括 glibc 和 musl 版本,Apple silicon 与 Intel 的 macOS,以及 Windows x86_64)都包含该宿主,并且每次发布前都会从这些归档中各执行一个插件,然后才会正式发布。两个 32 位 ARM 归档和实验性的 Android 归档不包含该宿主,使用 Cargo 默认配置的源码构建也不包含(
cargo install zeroclaw、install.sh --source或不带特性的cargo build):在这些情况下,zeroclaw plugin ...会被识别为未知子命令,已安装的插件也永远不会被发现。对于这些情况,请使用插件执行后端从源码构建,例如cargo build --release --features plugins-wasm-cranelift。协议页面列出了每个构件的插件支持情况。
每个插件都位于 plugins 目录的各自子目录中(默认 ~/.zeroclaw/plugins/,通过 plugins.plugins_dir 解析),其中包含清单以及与清单的 wasm_path 匹配的组件:
~/.zeroclaw/plugins/
└── my-plugin/
├── manifest.toml
└── my-plugin.wasm
从本地目录安装(这会在复制任何内容之前验证清单形状并运行签名策略):
zeroclaw plugin install ./my-plugin/
启用插件系统并确认发现:
zeroclaw config set plugins.enabled true
zeroclaw plugin list
zeroclaw plugin info my-plugin
zeroclaw plugin list 和 zeroclaw plugin info 可确认软件包已安装且可被发现,但发现并不等于激活。plugins.enabled = true 会开启插件主机;只有同时将 plugins.auto_discover = true 设为 true 时,自动发现的工具和技能功能才会在运行时加载,而该标志默认为 false(故障关闭):
zeroclaw config set plugins.auto_discover true
因此,仅设置 plugins.enabled = true 时,会启用你在 [channels.plugin.<alias>] 下声明的通道,而不会启用任何插件工具或技能:工具或技能包可能会出现在 zeroclaw plugin list 中,但在运行时不会产生任何作用。显式通道绑定由操作员命名,而不是自动发现,因此不需要 auto_discover;该标志仅控制自动发现的工具和技能。
zeroclaw plugin list 中缺失的插件在发现时已被跳过:请检查启动日志中的跳过警告(清单格式错误、缺少 wasm_path 文件,或签名策略拒绝)。
9. 运行它
请让代理使用该工具:
> redact this before you log it: key sk-live-abc123, mail ops@example.com
模型会在其目录中看到带有你的 schema 的 redact,调用它后,宿主会在一个新的 store 中,在配置的 fuel 和内存限制下运行该组件。插件工具不在内置的只读自动批准集合中,因此在非完全自治模式下,这次调用会像其他特权工具一样弹出操作员批准提示;请在你的工具描述中预料到这一点,而不是对此感到意外。你的 log-record 事件会以宿主调用点的 span attribution 形式出现在结构化日志中。
来自 插件概览 的两个操作约束值得再次强调:
- 包名和工具名不得与已注册的工具冲突。 注册时会拒绝冲突,而不是让某个工具遮蔽另一个工具。如果你的包名(清单中的
name)与已注册的工具匹配,该插件会在其组件实例化之前被拒绝,宿主会记录一条WARN事件,其attributes中的error_key为plugin_package_name_conflict。如果name导出函数返回的名称与已注册的工具匹配,则该工具不会被注册,error_key为plugin_tool_name_conflict。包含__的name导出值也永远不会被注册:MCP 工具使用该分隔符(<server>__<tool>),此时error_key为plugin_tool_name_reserved。检查的名称包括本次注册表构建中已注册的工具(包括排在你之前已被接受的插件工具),以及当[pipeline] enabled = true时的execute_pipeline。这并非所有内置名称的固定列表:如果运维人员的配置使某个内置工具未被注册,那么在该构建中该名称并不被保留,因此同一名称在一个宿主上可以加载,在另一个宿主上却可能被拒绝。请选择绝对唯一的名称。运维视角的说明参见工具名称冲突。 - 每个组件一个工具。
tool-plugin世界导出一个单一的tool接口。一个工具箱由多个插件目录组成,每个组件一个。
故障排除
| 症状 | 可能的原因 |
|---|---|
zeroclaw plugin install 在 does not load against this host 时失败 | 该组件是针对与本主机 wit/v0 不同的 WIT 构建的(例如过时的 configure 签名,或主机不具备的 capability 标志),或者是针对错误的目标构建的(核心模块或 wasm32-wasip1)。安装时的加载检查与守护进程启动时执行的实例化相同,因此未安装任何内容。请基于随附的 WIT 重新构建并重新安装;--no-verify 会强制安装,但守护进程启动时会以相同的诊断信息跳过该组件。 |
zeroclaw plugin list 中缺少插件 | 插件系统已禁用;manifest 格式错误;wasm_path 文件缺失;签名策略拒绝了它。启动日志中包含具体的跳过警告。 |
出现在 zeroclaw plugin list 中,但工具始终不会加载 | plugins.auto_discover 为 false(默认值)。自动发现的工具和技能功能仅在 plugins.auto_discover = true 时加载;仅设置 plugins.enabled = true 只会激活显式声明的通道。运行 zeroclaw config set plugins.auto_discover true。 |
| 工具注册遭到拒绝 | 配置校验或元数据探测失败,包名或工具名与已注册的工具冲突,或者工具名包含 MCP 分隔符 __。请查看日志中的具体错误:名称冲突会携带 error_key plugin_package_name_conflict 或 plugin_tool_name_conflict,包含 __ 的名称会携带 plugin_tool_name_reserved,而探测失败通常意味着组件是基于不匹配的 WIT 构建的。 |
| 模型从未选择该工具 | 描述和 schema 没有告诉模型该工具何时适用。如果工具根本没有出现在模型的工具目录中,则属于加载问题。请从上面的行开始。 |
__config 即使已配置该部分仍然缺失 | 有效作用域拒绝了 config_read、条目未使用安装时打印的完整实例密钥、经过验证的对象为空,或所有经过验证的属性都标记为机密。相反,config_schema/权限不匹配会拒绝该插件。 |
secrets.get 返回 not-found | 该属性缺失,或者在已获准的架构中不是标记为 x-secret = true 的直接顶层字符串。 |
secrets.get 返回 unavailable | 该调用在 execute 之外运行、配置解析失败,或执行耗尽了其固定的主机调用预算。 |
| 调用失败或触发陷阱 | 已达到燃料、挂钟时间或内存上限。请酌情提高 plugins.limits.call_fuel、plugins.limits.call_timeout_ms 或 plugins.limits.max_memory_mb,或减少每次调用的工作量。 |
| 仅运行时主机上加载失败 | 您将 .wasm 部署到了一个没有 JIT 的主机上;请改为部署与版本匹配的 .cwasm。 |
下一个
- 编写 channel 插件,用于 warm-store 生命周期、capability 标志以及由宿主提供的 inbound。
- 分发插件 适用于此工具应该离开你的机器时。