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