Keyboard shortcuts

Press ← or → to navigate between chapters

Press ? to show this help

Press Esc to hide this help

分发插件

你构建了一个插件;现在它需要离开你的机器,而不必要求安装它的人盲目信任你。ZeroClaw 的分发方案有两个独立层面:Ed25519 清单签名(是谁发布的)和注册表安装(它如何到达那里)。本页涵盖这两者,并结合 crates/zeroclaw-plugins/src/signature.rs、src/plugin_registry.rs 以及 host.rs 中的安装路径进行核对。

签名

什么是已签名的

签名涵盖规范清单字节。主机会解析 TOML,仅移除根级别中完全匹配的 signature 和 publisher_key 条目,保留文档的其余部分,并去除末尾空行(signature.rs 中的 canonical_manifest_bytes)。这使签名可以自嵌入:在不包含这些根字段的情况下对清单签名,然后添加这些字段;验证时会先移除它们再进行检查。

需要了解的影响:

  • 把组件的 SHA-256 摘要写入根级 wasm_sha256 字段,然后再签名。签名会将该摘要与清单绑定在一起。宿主会对其准入并编译的原始字节进行验证;严格模式下,缺少该字段的可执行清单将被拒绝。下方的注册表 sha256 则独立地保护分发归档在传输过程中的完整性。
  • 名称类似于 config_schema.properties.signature 的嵌套字段,以及同样带此前缀的根字段(如 signature_algorithm),仍会被纳入签名。对保留内容重新格式化或重新排序会使签名失效。请在清单最终确定后再进行签名。
  • 因此,诸如 config_schema.properties.api_token.x-secret 之类的配置暴露标记属于受签名保护的策略。在公开配置暴露和受作用域限制的 secrets.get 访问之间切换工具或通道属性,需要重新构建并重新签名。
  • 由之前的基于前缀的规范化器签名的软件包,仅当它们依赖于其中某个边界情况,或依赖于附加在已移除字段上的 TOML 装饰时,才需要重新签名。普通清单的签名内容保持不变。

键和进程

使用 Ed25519 签名,采用主机验证时所用的同一套 ring 原语。签名为 base64url 编码(无填充),公钥为十六进制编码。该 crate 提供完整的工具链(signature.rs):generate_signing_key 生成 PKCS#8 密钥对及其十六进制公钥,sign_manifest 对规范字节生成 base64url 签名,public_key_hex 从已存储的私钥中恢复公钥。sign_manifest_document 执行以上两个步骤,并返回已包含两个根字段的签名清单。不存在 zeroclaw plugin sign 命令;发布者应在其发布流水线中调用这些函数。该 crate 还附带了一个现成的辅助程序 crates/zeroclaw-plugins/examples/sign_manifest.rs。在 ZeroClaw 代码检出目录中运行它,并指向你的插件目录路径;私钥须保存在两个目录树之外。每次发布都请使用新的、独立的包目录和新的归档路径;辅助程序会在写入已签名清单时创建它们:

cargo run -p zeroclaw-plugins --example sign_manifest -- keygen ~/.keys/my-plugin.pk8
cargo run -p zeroclaw-plugins --example sign_manifest -- \
    sign path/to/my-plugin/manifest.toml path/to/my-plugin-package/manifest.toml \
    --key ~/.keys/my-plugin.pk8 --payload path/to/my-plugin/my-plugin.wasm
cp path/to/my-plugin/my-plugin.wasm path/to/my-plugin-package/my-plugin.wasm
(cd path/to/my-plugin-package && zip -r ../my-plugin.zip .)

本示例假设 wasm_path = "my-plugin.wasm"。--payload 只计算组件的哈希值,并不会复制它。请将这些原始字节,以及其他所需资源,复制到清单中声明的、相对于包目录的路径,然后再创建 zip 压缩包。压缩时只归档该目录,其中包含已签名的 manifest.toml;不要包含未签名的源清单,签名后也不要修改已签名的清单。

keygen 会生成一个新的私钥,必要时创建其所在目录(在 Unix 上仅所有者可读),拒绝覆盖已存在的文件,并输出十六进制公钥。sign 在提供 --payload 时,会将组件的 SHA-256 记录到 wasm_sha256 中,然后签名,使用宿主的验证器检查签名,写入已签名的清单,并输出相同的公钥。对于声明了 wasm_path 的清单,若未记录 wasm_sha256,则会发出警告,因为严格模式会拒绝此类包。没有 wasm_path 的清单(例如 skill 包)不接受 --payload:宿主会拒绝其中的摘要。

在 Windows 上,密钥文件会继承其所在目录的 ACL。请将私钥保存在仅发布者可读取的目录中。

签名后的清单随后会携带两个额外的 根级字段:signature(base64url 值)和 publisher_key(你的十六进制公钥)。将二者都放在第一个表头(包括 [config_schema])之前;根据 TOML 规则,在表头之后追加它们会使其成为该表的成员,主机将看到一个未签名的清单。

如果你在签名后手动嵌入字段,请将它们直接放在另一个根级字段之后。宿主的 TOML 编辑器会把空行或注释附加到其下方的条目或表头上。如果字段直接位于清单中已有的空行或注释之下(即你签名时的清单内容),验证时会连同字段一起删除该行,剩余字节将与签名内容不一致,严格模式会拒绝该软件包。你在字段正上方添加的空行或注释会与字段一同被删除,不会造成影响。你在字段下方添加的任何内容都会附加到下一个条目或表头上,并改变已签名的字节,因此下方不要添加任何内容。sign_manifest_document 和辅助工具会在签名前放置这些字段,因此不存在这个问题。

name = "my-plugin"
version = "0.1.0"
wasm_path = "my-plugin.wasm"
wasm_sha256 = "<64-hex-character-component-sha256>"
signature = "<base64url-signature>"
publisher_key = "<hex-public-key>"

[config_schema]
type = "object"
properties = {}
additionalProperties = false

希望信任您的运维人员会将该十六进制密钥添加到其 plugins.security.trusted_publisher_keys 列表中:

zeroclaw config set plugins.security.signature_mode strict
zeroclaw config set plugins.security.trusted_publisher_keys '["<your-key-hex>"]'

验证的行为如何

验证在发现和安装时都会运行(enforce_signature_policy 从 host.rs 调用);发现阶段会跳过失败的插件并记录日志,安装阶段则返回错误。从操作者角度看,模式矩阵如下:

模式无符号已签名,但密钥不受信任已签名,签名无效已签名并受信任
disabled加载已加载,未检查已加载,未检查已加载,未检查
permissive加载时带有警告加载时带有警告加载时带有警告已加载,已验证
strict已拒绝已拒绝已拒绝加载

注意 strict 对你作为发布者的含义:严格模式下的运营方只有在你的精确 key 位于其受信任集合中且清单字节校验通过时才会加载你的插件。任何签名后的清单修改,无论是你自己还是分发路径中的任何人所做,都会导致安装失效。这就是其目的。

注册发布

安装路径为本地插件目录;注册表只是一个 JSON 索引,在命令运行时查询(zeroclaw plugin search / install)。这两个命令仅存在于编译了插件宿主的二进制文件中(参见构建特性)。64 位桌面和服务器目标的发布归档包含该功能,每次发布前都会从每个归档中执行一个插件。32 位 ARM 归档、实验性 Android 归档以及默认源码构建不包含该功能(按发布产物划分的表格列出了所有产物)。通过这两个命令获取索引时,索引会缓存到本地。zeroclaw plugin list 随后将已安装的包与缓存合并显示,且不发起网络请求;它将已安装版本与注册表版本分开展示,而不会猜测任意版本字符串是否更新。默认索引为 zeroclaw-labs/zeroclaw-plugins 仓库中的 registry.json;私有注册表只需提供一个 URL 即可使用:可以按命令使用 --registry <url>,也可以使用 ZEROCLAW_PLUGIN_REGISTRY_URL 环境变量,二者按此顺序解析,具体逻辑见 src/plugin_registry.rs 中的 registry_url。

一个注册表条目(crates/zeroclaw-plugins/src/registry.rs 中的 PluginRegistryEntry)包含:name、version、可选的 description 和 author、capabilities、压缩包 url,以及可选的 zip 压缩包 sha256 摘要。仅 zeroclaw plugin install 允许摘要可选,并会在摘要存在时进行验证。zeroclaw quickstart 的 Plugins 步骤会拒绝没有摘要的条目,因此请为每个条目发布摘要。

归档契约

zeroclaw plugin install <name> 会解析条目、下载 zip 包、在存在摘要时校验摘要、安全解压,然后将解压后的目录交给与本地安装相同的准入与安装流程。清单会经过签名校验和验证,已安装的名称会被拒绝,组件只读取一次(限定在包内、不跟随符号链接、上限为 64 MiB),并且安装前的加载检查会在安装任何内容之前,依据本主机的 WIT world 实例化这些已准入的字节。无法加载的组件会使安装失败并输出实例化诊断信息;--no-verify 会跳过该检查并仍然安装,此时守护进程在启动时会跳过该插件。解压过程在设计上即采取防御性措施(src/plugin_registry.rs),你的归档文件必须能通过该流程:

  • zip 必须包含一个根级别的 manifest.toml,或者恰好一个包含该文件的嵌套插件目录。没有 manifest 或者多于一个都会导致归档被拒绝。
  • 带有路径穿越、绝对路径或 Windows 驱动器前缀的条目名称会被拒绝。
  • 下载在流式传输时受限(50 MiB),因此一个不提供 Content-Length 的服务器无法强制进行无限缓冲;解压也受同一上限限制,因此 zip bomb 无法无限展开。

版本解析:当安装程序收到不带版本号的名称时,它会在索引中选取最后一个匹配的条目;带固定版本的 name@version 则精确选取该版本。请有意识地按顺序排列注册表中重复的名称,最旧的在前。zeroclaw plugin update 使用相同的选取方式,并且仅通过判断版本字符串是否相等来比较:已安装规则所选版本的用户视为已是最新,不会下载任何内容。每次变更的包都要以新版本发布,因为以已安装版本重新发布的条目永远不会被更新获取到。

搜索不是信任边界

zeroclaw plugin search 是对索引的未认证发现;它绝不会安装、启用或执行任何内容。安全性发生在安装时:摘要校验、安全解压、清单验证,以及操作员的签名策略,与本地路径安装完全相同。因此应相应地发布:在安装之前,默认认为一切都是不受信任的传输。

发布者清单

[!IMPORTANT] 编译后的 .wasm 和 .cwasm 文件是二进制构件,通常每个都有数 MB。不要在没有 Git LFS 的情况下将它们提交到 git 源树中:每次重建都作为普通 blob 提交,会永久膨胀仓库历史,并且 git diff/审查工具会因此卡住。把它们当作其他构建输出一样处理:将 target/ 和 *.wasm/*.cwasm 添加到 .gitignore,并改为通过发布构件或插件注册表归档分发。如果某个构件确实必须留在树中,请在首次提交之前使用 LFS 跟踪该模式(git lfs track "*.wasm")。

  1. 完成清单:名称、版本、功能,以及代码使用的最小权限集。
  2. 构建组件;对于 skill bundles,验证每个 SKILL.md 的 frontmatter(发现会强制要求 name 和 description)。
  3. 签名:生成或加载你的 Ed25519 密钥,对规范化的 manifest 字节进行签名,嵌入 signature 和 publisher_key。
  4. 压缩已组装的软件包目录(包含一个已签名的清单、其引用的组件和所需资源,不使用路径技巧,总大小不超过 50 MiB)。
  5. 计算 zip 的 SHA-256 并发布带有该摘要的 registry 条目。
  6. 将你的公钥十六进制值发布在某个地方,以便操作者可以独立于 registry 进行验证(你的 repository、你的网站)。在 strict 模式下,操作者信任的是密钥,而不是 registry。
  7. 每次发布时:提升 version,重新签名(版本行位于规范字节中),重新生成摘要,将新条目追加到旧条目之后。