Keyboard shortcuts

Press ← or → to navigate between chapters

Press ? to show this help

Press Esc to hide this help

编写 Channel 插件

频道插件是与消息平台的集成:它把智能体的响应投递到平台,并将平台上的消息转交给智能体。它是最复杂的一类插件,因为频道是长期运行且有状态的,它通过一个包含 29 个函数的接口与运行时交互,其中只有 6 个是必需的。

本指南假设你已经构建了 tool plugin,并理解 crate 设置、__config 规则、日志记录和安装。它已针对 wit/v0/channel.wit 和 crates/zeroclaw-plugins/src/wasm_channel.rs 中的主机适配器进行检查。

接入状态。 频道插件由正在运行的守护进程构建。通过 [channels.plugin.<alias>] 绑定的已安装软件包会在启动时被纳入,并接受与原生频道完全相同的监管。请参阅下面的激活频道插件。

生命周期

通道插件的运行时形态与工具的运行时形态在三个基本方面不同,而每一点都会推动你代码中的一个设计决策:

  1. 插件生命周期内使用一个热存储。主机只实例化一次你的组件(WasmChannel::from_wasm),并在异步互斥锁保护下持有该存储。组件可以在调用之间保留由来宾拥有的协议状态,但操作员配置仍由主机持有。符合规范的插件必须在每个需要它们的操作中调用 config.get 和 secrets.get,并且不得将其结果复制到来宾的热状态中。主机会在每次调用后丢弃其物化视图,但无法阻止恶意来宾代码保留返回的 JSON 或明文。主机会在每次调用前为存储重新注入燃料(component.rs 中的 call_channel!),因此长期存活的通道每次调用都会获得全新的燃料预算,而不是在其整个生命周期内逐渐耗尽。
  2. 配置会在使用点请求。 宿主在加载时调用不带参数的 configure 导出,且仅调用一次,并在任何其他导出之前。调用 config.get 获取根据清单的 config_schema 验证的类型化公共 JSON 对象;标记为 x-secret = true 的属性会被省略,必须通过 secrets.get 读取。在 configure 中或任何后续操作导出中进行的公共配置和机密读取,共享同一个已解析的配置修订版本。因此,同一绑定中的公共配置和凭据轮换会在下一次操作中一并可见。实例化和静态发现期间的调用会返回 unavailable,不会解析配置。静态发现包括 name、plugin-info、get-channel-capabilities、self-handle、self-addressed-mention 和 multi-message-delay-ms;更改机器人/账户身份或其他静态元数据需要重建通道生命周期。
  3. 你不监听;主机向你转发。 WASI 上下文没有网络监听能力。入站流量通过导入的 inbound 接口到达你:主机运行实际的监听器(webhook 服务器、供应商隧道、轮询客户端),将收到的每条消息入队到 InboundQueue,而你的 poll-message 导出通过调用 inbound-poll 来清空它。如果有用,可使用 inbound-pending 批量清空。

必需的导出

五个函数没有 Rust trait 默认实现,且必须真正工作(world channel-plugin 文档,channel.wit):

导出合同
name人类可读的频道名称。
configure完成加载时的初始化。它不接受任何参数;针对一个当前修订版本调用 config.get 和 secrets.get。错误字符串会导致加载失败。
send向平台发送一个 send-message(内容、收件人,以及可选的主题/线程/附件)。
poll-message非阻塞:立即返回下一条传入消息或 none。永不阻塞;主机的轮询桥负责节奏控制。
get-channel-capabilities返回你实际实现的可选方法位掩码。在加载时调用一次。

轮询桥接需要特别说明:宿主运行一个轮询转推送循环(wasm_channel.rs 中的 listen),当队列为空时,它以 50ms 到 500ms 的指数退避调用 poll-message,有流量时重置退避。如果你的 poll-message 发生陷阱(trap),宿主会记录该异常、进入退避,并在网关的 /health 快照中将该通道报告为不健康,即使你没有导出自己的 health-check。陷阱会使实例无法运行任何导出函数,因此宿主会丢弃该实例,下一次调用会创建新实例并重新运行 configure。发生陷阱的那次轮询已取出的消息会丢失,但之后的消息仍会正常到达。任何发生陷阱或错过调用截止时间的导出函数都会以同样方式被替换;而导出函数返回的错误字符串则会让实例保持不变。

重建会重新连接网关类插件,因此宿主对其设有重建预算:只要失败的平均频率不超过每五分钟一次,宿主就会立即重建,并允许最多连续三次的突发重建;超出后则等待预算恢复,在此期间失败的调用将直接返回失败,而不会运行你的插件。request-approval 和 request-choice 会等待人工响应,这两个导出函数错过截止时间不计入预算。如果某个导出函数通常需要超过截止时间的时间(例如大文件上传),则应调高 plugins.limits.call_timeout_ms。

保持简单:排空队列、翻译、返回。

能力标志:23 个可选方法

界面中的其他所有内容都由 channel-capabilities 标志控制。模式(与 memory world 完全相同):

  • 主机会在加载时读取你的标志一次。
  • 对于每个未设置的标志,主机都会使用 Rust trait 默认值,并且不会调用你的导出。
  • 你仍然必须导出每个函数;返回文档中默认值的存根可以编译通过,而且永远不会被调用。

各个标志的默认值已在 channel.wit 中紧邻标志声明的位置进行内联文档说明,这里才是事实来源。总而言之,这些组:

分组标志实现能为你带来什么
健康health-check报告通道是否正常工作。在监听期间,主机大约每 30 秒询问一次,并将结果与轮询健康状况合并显示在 /health 中(参见 健康检查)。
身份self-handle、self-addressed-mention、drop-self-message自环保护(运行时会丢弃机器人自身的消息)以及每个频道系统提示中的正确 @ 提及形式。主机会在加载时缓存 self-handle 和 self-addressed-mention;它们只会被读取一次。
输入start-typing, stop-typing在代理思考时显示指示器。
Draftssupports-draft-updates, send-draft, update-draft, update-draft-progress, finalize-draft, cancel-draft渐进式消息编辑:运行时会将响应流式传输到一个可编辑的平台消息中,而不是等待完成。必须一次性实现全部六项,或者一项也不实现。
多消息流式传输supports-multi-message-streaming, multi-message-delay-ms按段落逐段发送,消息之间的最小延迟(默认 800ms,在加载时缓存)。
审核add-reaction, remove-reaction, pin-message, unpin-message, redact-message表情反应、置顶、消息删除。
交互request-approval, request-choice, supports-free-form-ask在平台上原生呈现的工具调用审批提示和多项选择题。
Webhook 入口webhook-ingress为入站 webhook 提供服务:网关会将你的 webhook-path 段挂载到 /plugin/<segment> 下,并将每个请求交给 parse-webhook 处理。该函数负责验证平台来源的真实性,并返回消息或挑战应答。请同时实现这两个导出。

从所需的 6 项加上 health-check 开始,并随着平台支持逐步添加分组。宣告一个尚未实现的标志,比干脆不声明它更糟糕:宿主会调用你的导出并信任其返回结果。

Health checks

在通道监听期间,主机会在两次轮询之间对正在运行的实例调用 health-check:首次轮询之后立即调用一次,之后大约每 30 秒调用一次。调用结果将成为该通道在网关 /health 快照中的组件,即 channel:plugin.<alias>:

  • starting 直到收到第一个应答。第一个应答为 false 时同样显示 starting,直到下一次检查,因此仍在完成握手的插件不会被报告为失败。
  • ok,而最新答案为 true
  • error:当最新回答为 false 时、检查触发陷阱或错过调用截止时间时、poll-message 触发陷阱时,或在两个间隔加两个调用截止时间的时长内未收到任何回答时(默认 30 秒截止时间下为两分钟)。

zeroclaw channel doctor 也会对新配置的实例调用一次 health-check。

由于检查会重复执行,应基于插件已持有的状态作答:即其连接是否正常,或其与平台上一次交互的结果如何。health-check 中的网络请求会在每次检查时运行,并延迟下一次轮询;如果需要通过网络验证凭据,请保存上一次的结果,并按你自己的计划刷新它。发生陷阱(trap)或未能在调用截止时间内完成的检查会丢弃该实例,宿主随后通过 configure 重新构建它,因此网关类插件会失去其连接;每次此类失败后,宿主在下一次检查前等待的时间加倍,最长可达十分钟。宿主还会在再发生一次失败就会导致重建等待的情况下推迟检查,因此单次失败的检查本身绝不会导致重建等待。

审批界面

request-approval 是最深层的集成点。运行时会提供一个紧凑的 approval-request(工具名称、参数摘要、可选的原始 JSON 参数),而你的通道会按平台允许的方式将其渲染出来(按钮、表情反应、回复约定)。你返回的 approval-response 变体会驱动安全机制:

  • approve:执行这一次调用
  • deny:拒绝它
  • always-approve:执行并将该工具添加到会话范围的允许列表
  • deny-with-edit(string):拒绝,但提供编辑后的替换参数

当无法显示提示时返回 none;调用方将回退到自动拒绝。请采取失败即关闭。

入站消息形状

将平台事件如实转换为 inbound-message 记录。运行时的线程逻辑以平台载荷字段为依据,而路由标识仅来自主机发放的端点(channel.wit,wasm_channel.rs 中的 from_wit_inbound):

  • id、sender、content:基础字段。reply-target 是响应应发送到的位置(频道 ID、聊天 ID、电子邮件地址)。
  • channel 和 channel-alias 是保留在 v0 记录中的旧版提示。主机在路由时会忽略这两者,并标记已接纳的通道类型和已配置的绑定,因此插件无法选择其他所有者或会话。
  • thread-ts 承载平台的线程标识符,用于线程式回复;subject 用于电子邮件线程。
  • interruption-scope-id 将消息分组用于中断/取消。顶层消息请将其保持为 none。
  • attachments 通过边界携带完整原始字节(media-attachment:文件名、字节、可选 MIME 类型)。语音笔记会以值传递跨越数兆字节;这就是 32 位边界的已记录成本,而资源句柄模型被明确推迟到未来的 WIT 修订版。

在出站侧,send-message 映射同样的字段;Rust 中 SendMessage 的取消令牌被刻意从 WIT 记录中省略,因为它是宿主端概念,在插件内部没有意义。

骨架

结构,省略按平台翻译部分,那才是你的实际工作:

#![allow(unused)]
fn main() {
#[cfg(target_family = "wasm")]
mod component {
    wit_bindgen::generate!({
        path: "wit/v0",
        world: "channel-plugin",
        features: ["plugins-wit-v0"],
    });

    use exports::zeroclaw::plugin::channel::{
        ApprovalRequest, ApprovalResponse, ChannelCapabilities,
        Guest as Channel, InboundMessage, SendMessage,
    };
    use exports::zeroclaw::plugin::plugin_info::Guest as PluginInfo;
    use zeroclaw::plugin::config::get as config_get;
    use zeroclaw::plugin::inbound::inbound_poll;
    use zeroclaw::plugin::secrets::get as secret_get;

    #[derive(serde::Deserialize)]
    #[serde(deny_unknown_fields)]
    struct ChannelConfig {
        api_base: String,
    }

    fn current_config() -> Result<ChannelConfig, String> {
        let json = config_get().map_err(|_| 公共配置不可用.to_string())?;
        serde_json::from_str(&json).map_err(|e| format!("无效的配置 JSON: {e}"))
    }

    fn current_api_token() -> Result<String, String> {
        secret_get("api_token").map_err(|_| "api_token 不可用".to_string())
    }

    fn current_inputs() -> Result<(ChannelConfig, String), String> {
        // 此导出中的两个导入共享同一个已解析的规范修订版本。
        Ok((current_config()?, current_api_token()?))
    }

    struct MyChannel;

    impl Channel for MyChannel {
        fn name() -> String {
            "my-platform".to_string()
        }

        fn configure() -> Result<(), String> {
            let (config, api_token) = current_inputs()?;
            validate_configuration(&config.api_base, &api_token)
        }

        fn send(message: SendMessage) -> Result<(), String> {
            let (config, api_token) = current_inputs()?;
            // 通过 wasi:http 进行平台出站传送
            // (需要在清单中声明 http_client 权限)。使用此调用的值构建
            // 请求;绝不要保留第二份副本。
            send_to_platform(&config.api_base, &api_token, message)
        }

        fn poll_message() -> Option<InboundMessage> {
            // 排空主机馈送的队列并进行翻译。
            inbound_poll().map(translate_inbound)
        }

        fn get_channel_capabilities() -> ChannelCapabilities {
            ChannelCapabilities::HEALTH_CHECK
        }

        fn health_check() -> bool {
            current_inputs().is_ok()
        }

        // 其他方法:返回 WIT 文档中默认值的存根。
        // 在其标志未设置时,宿主从不调用它们。
        // ...
    }

    export!(MyChannel);
}
}

current_inputs 特意在使用点调用。宿主将两个导入都绑定到此获准的包、channel 能力和别名;同一导出中的读取共享一个已解析的配置修订版本,而下一个导出可以观察到同一绑定的公共配置以及凭据轮换。ChannelConfig 是每次调用的类型化视图,会随令牌一并丢弃。不要添加 thread_local 配置或凭据缓存。

清单和权限

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。

对于频道:capabilities 中包含 channel,几乎必然还包括 config_read(没有任何平台能在没有凭据的情况下工作)和 http_client。频道适配器实现了出站 wasi:http,但只有在该授权通过验证后才会链接它;仅有该授权仍无法访问任何网络:由宿主负责出口策略,且默认拒绝。运营者必须在该频道实例的 plugins.entries.<key>.egress_hosts 下列出平台的 API 主机,send 才有网络路径;任何不在该列表中的目标,都会在数据包离开之前被拒绝。已授权的主机如果解析为回环、私有或链路本地地址,同样会被拒绝,除非它也列在 egress_allow_private 下。

在清单的 [egress] 表中声明平台的 API 主机。当运营者使用 zeroclaw plugin bind <package> --channel-alias <alias> --egress declared 绑定实例时,该声明会被写入新实例的 egress_hosts 并打印出来;改用 --egress none 则绑定时不授予任何网络可达性。对于频道实例,该声明适用于任何受管控的传输方式,即 http_client、websocket_client 或 socket_client,因此仅通过套接字通信的频道同样会以相同方式写入。

属于部署配置的主机(例如自托管服务器)不应由你声明:请在 README 中说明该主机由运营者授予。

将 config_read 与 ChannelConfig 使用的架构配对:

name = "my-platform"
version = "0.1.0"
wasm_path = "my_platform.wasm"
capabilities = ["channel"]
permissions = ["config_read", "http_client"]

[config_schema]
"$schema" = "https://json-schema.org/draft/2020-12/schema"
type = "object"
additionalProperties = false
required = ["api_base", "api_token"]

[config_schema.properties.api_base]
type = "string"
minLength = 1

[config_schema.properties.api_token]
type = "string"
minLength = 1
x-secret = true

宿主将这两个属性作为一个对象进行校验。config.get 返回包含 api_base 的带类型 JSON,并省略 api_token;api_token 只能通过 secrets.get 获取。由于两者都是必需项,若不授予 config_read,则会在访客代码运行前以失败关闭(fail closed)方式拒绝,而不是在缺少必需配置的情况下启动通道。每个通道实例都会选择 plugins.entries 键,该键由其完整包、channel 能力和绑定标识派生,同时复用这一个由包拥有的 schema。因此,不同包中相同的别名彼此保持隔离。运维人员绑定别名后,该键即存在:zeroclaw plugin bind 会创建该实例的条目,zeroclaw plugin bind 与 zeroclaw plugin info 都会将此 schema 的每个 required 键列为已设置或缺失,对于缺失的键会给出对应的 zeroclaw config set 命令。密钥类键的命令会以无回显方式提示输入值,且任何值都不会被打印。仅当属性名符合可移植键语法(1 至 128 个 ASCII 字母、数字、_、- 或 .)时才打印命令;其他名称则会改为报告需要编辑的配置文件行,因此请将必需属性名保持在该语法范围内。当条目尚不存在时(例如手动编写的绑定),plugin info 会打印用于创建该条目的 plugin bind 命令,以代替键列表;若清单声明了目标地址,则每个出口(egress)决策打印一次,因为 zeroclaw config set 只能解析已存在的条目。

在每个使用它们的操作内部调用 config.get 和 secrets.get。宿主对该调用最多解析一个规范版本,并在调用结束后丢弃其视图。同一逻辑绑定中的公开配置和凭据轮换会在下一次操作中一并可见,无需重新加载守护进程或重建通道。更改机器人/账户身份、公布的能力、自身句柄、提及标识或其他加载时元数据,需要重建通道生命周期,因为这些导出项在静态发现期间只读取一次。

对于空对象有效的可选架构,即使实例未获授有效的 config_read 授权,也可以加载,但 config.get 和 secrets.get 会返回 access-denied。任一导入都会在实例化或静态发现期间、解析器/验证失败后,或共享主机调用预算耗尽时返回 unavailable。对于缺少某个名称或未将其标记为 x-secret = true 的情况,secrets.get 还会返回 not-found。

激活频道插件

安装的包在操作员将其绑定到逻辑通道实例之前不会执行任何操作。绑定只指定包名,不包含其他内容;别名即为实例的标识。操作员使用的路径是 zeroclaw plugin bind,该命令会写入绑定关系、初始化实例的配置项,并报告实例在启动前仍需完成的配置。

zeroclaw plugin bind acme.chat --channel-alias operations --egress declared

--egress declared 授予清单中声明的目标地址的访问权限,--egress none 则以无网络访问权限进行绑定。仅当命令创建实例的配置条目时该标志才有作用;此时,若清单声明了目标地址,且软件包包含可访问这些目标的传输组件(http_client、websocket_client 或 socket_client),则必须指定该标志。zeroclaw plugin install <source> --channel-alias operations 在安装期间执行相同的流程;在此情况下,该标志仅作用于频道实例的条目,而同一软件包的工具条目则按安装时的常规方式进行初始化。绑定频道实例对此有完整说明。

In config.toml 中,已绑定并路由到某个 agent 的实例如下所示:

[plugins]
enabled = true

[channels.plugin.operations]
package = "acme.chat"
enabled = true

[agents.support]
channels = ["plugin.operations"]

plugin bind 只会写入 [channels.plugin.operations] 表和实例对应的 [[plugins.entries]] 条目。新绑定会以 package 写入,并使用默认的 enabled = true。有两个步骤始终由操作者明确决定,该命令从不代为执行:开启插件系统(plugins.enabled),以及归属配置,即把 plugin.operations 添加到已启用代理的 channels 列表(agents.<agent>.channels)。命令打印的就绪报告(zeroclaw plugin info 也会重复输出该报告)会指出第一个仍未满足的前置条件,若存在对应的满足命令,也会一并给出。

别名会变成普通的频道引用,因此 plugin.operations 会像 telegram.main 一样被路由、监督、重启和寻址。两个别名可以指向同一个软件包;每个别名都有自己的实例、存储和 plugins.entries 键,因此它们不共享任何状态。

实例只有在以下所有条件均满足时才会获准。每一项都是有意设计的故障关闭门槛,缺少其中任何一项的声明都将保持不生效,而不会处于半启动状态:

  • plugins.enabled 为 true。
  • 声明中的 enabled 为 true。
  • 指定的软件包已安装,并且其清单声明了 channel 能力。
  • 某个已启用的代理在其 channels 中列出了 plugin.<alias>。未被引用的绑定会运行一个没有投递目标的监听器。

准入发生在运行任何来宾代码之前:它依据包宿主已验证的清单作出决定,因此,组件损坏的包与组件完好的包会以完全相同的方式被规划并拒绝。通过准入但随后构造失败的包会被记录并跳过,因此,一个损坏的插件不会阻止守护进程启动其他通道。

plugins.max_active_instances 限制所有能力中可接纳的逻辑实例数量。显式通道绑定的优先级高于自动发现的工具和技能,因此,已满的插件目录无法取代操作员手动配置的通道。

zeroclaw plugin bind 和 zeroclaw plugin info 会向激活计划请求此判定,而不是重复其规则,因此它们报告的第一个未满足的门控(包括上限)与守护进程对相同配置和已安装包所应用的门控一致。判定为就绪仅表示该实例已获准入,并不表示其组件已加载;plugin info 会通过其加载检查另行报告这一点。

同一个获准集合驱动全部三个加载器:通道加载器、工具注册表和插件技能加载器。因此,上限是一个共享预算,而不是每种能力各自独立的预算。一个同时提供通道和工具的软件包确实会占用两个槽位,而超过上限的工具或技能根本不会被构造。准入是当前配置和已安装软件包的纯函数:它不保存任何计数器,因此,按智能体、按 CLI 运行、按委托以及按 SOP 执行分别重建的工具注册表,都会重新推导出同一个集合,而不会在长时间运行的守护进程生命周期内耗尽上限。

工具和技能实例会被_自动发现_,因此只有在 plugins.auto_discover 为 true 时才会被纳入。显式 [channels.plugin.<alias>] 声明无需启用此设置。在 plugins.enabled = true 且 auto_discover = false 的情况下,你得到的恰好是所声明的通道绑定,不会有其他内容。

从 plugins.max_plugins 迁移。 旧键从未生效,现已替换为 plugins.max_active_instances。这两个键统计的对象不同:旧键统计已安装的软件包数量,新键统计已纳入的逻辑实例数量,因此,同时提供通道和工具的软件包会占用两个实例。由于统计单位不同,现有的 max_plugins 值不会沿用:它会被忽略,新键使用其默认值。如果你依赖非默认上限,请显式设置 max_active_instances。

还有哪些部分尚未接入

Plugin 通道是异步构建的,此时同步通道映射接口已经构建完成。因此,按通道寻址的 tools 暂时无法指向 Plugin 通道。通过受监管的监听器进行的入站轮询和出站传递不受影响;缺少的只有工具侧寻址。

构建并安装

先安装一次 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 的反序列化检查拒绝,而不会被静默错误加载。

这些命令需要一个编译了插件宿主的二进制文件。 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 文件,或签名策略拒绝)。

针对主机合约进行测试

主机适配器和配置解析器测试是可执行规范:它们涵盖类型化物化和架构验证、使用点的公开和机密作用域、一致的同修订版本轮换、被拒绝的授权、静态发现拒绝、入站队列移交、能力门控调度以及轮询健康度统计。

要在这些完全相同的语义下运行你自己的组件,请编写一个集成测试,通过真实的宿主适配器实例化它。zeroclaw-plugins 未发布到 crates.io,因此请将其作为 git dev-dependency 引入,并固定到与你的目标宿主匹配的标签:

cargo add --dev zeroclaw-plugins \
  --git https://github.com/zeroclaw-labs/zeroclaw --tag <host-version> \
  --no-default-features --features plugins-wasm-cranelift

随后,测试会在 PluginHostServices 中封装一个由清单和测试操作员值支持的 PluginConfigResolver::new,通过 WasmChannel::from_wasm 加载你的组件,将消息加入其公开的 InboundQueue 句柄,并断言你的 poll-message 会排空并转换该消息。生产守护进程将运行的代码路径与此完全相同;在没有实际主机的情况下,通过该测试是分发前能获得的最强信号。

下一个