docs/book/src/plugins/distributing-plugins.md
You built a plugin; now it needs to leave your machine without asking the
people who install it to trust you blindly. ZeroClaw's distribution story has
two independent layers: Ed25519 manifest signatures (who published this) and
registry install (how it gets there). This page covers both, checked against
crates/zeroclaw-plugins/src/signature.rs, src/plugin_registry.rs, and the
install path in host.rs.
The signature covers the canonical manifest bytes: the manifest file's
content with every line whose trimmed form starts with signature or
publisher_key followed by = removed, and trailing empty lines stripped
(canonical_manifest_bytes in signature.rs). This makes the signature
self-embeddable: you sign the manifest without the signature fields, then add
them, and verification strips them back out before checking.
Two consequences worth knowing:
.wasm component itself is not covered by the signature. What the
signature attests is the manifest: the name, version, capabilities, and
permissions a publisher stands behind. Pair it with a registry sha256
digest (below) when the artifact integrity matters in transit.Signing uses Ed25519 via the same ring primitives the host verifies with.
The signature is base64url (no padding); the public key is hex-encoded. The
crate exposes the full toolchain (signature.rs): generate_signing_key
produces a PKCS#8 keypair and its hex public key, sign_manifest produces
the base64url signature over the canonical bytes, and public_key_hex
recovers the public key from a stored private key. There is no CLI wrapper
for signing today; publishers drive these functions from a short Rust helper
in their release pipeline.
The signed manifest then carries two extra fields: signature (the base64url
value) and publisher_key (your hex public key). Operators who want to trust
you add that hex key to their plugins.security.trusted_publisher_keys list:
zeroclaw config set plugins.security.signature_mode strict
zeroclaw config set plugins.security.trusted_publisher_keys '["<your-key-hex>"]'
Verification runs at both discovery and install (enforce_signature_policy
called from host.rs); discovery skips a failing plugin and logs, install
returns the error. The mode matrix, from the operator's side:
| Mode | Unsigned | Signed, key not trusted | Signed, signature invalid | Signed and trusted |
|---|---|---|---|---|
disabled | loads | loads, not checked | loads, not checked | loads, not checked |
permissive | loads with warning | loads with warning | loads with warning | loads, verified |
strict | rejected | rejected | rejected | loads |
Note what strict means for you as a publisher: an operator in strict mode
loads your plugin only if your exact key is in their trusted set and the
manifest bytes verify. Any post-signing manifest edit, by you or by anyone in
the distribution path, bricks the install. That is the point.
The install path is the local plugin directory; a registry is only a JSON
index consulted at command time (zeroclaw plugin search / install). The
default index is the zeroclaw-labs/zeroclaw-plugins repository's
registry.json; private registries are a URL away
(--registry <url> per command, or the ZEROCLAW_PLUGIN_REGISTRY_URL
environment variable, resolved in that order per registry_url in
src/plugin_registry.rs).
A registry entry (PluginRegistryEntry in
crates/zeroclaw-plugins/src/registry.rs) carries: name, version,
optional description and author, capabilities, the archive url, and
an optional sha256 digest of the zip.
zeroclaw plugin install <name> resolves the entry, downloads the zip,
verifies the digest when present, safely extracts, and hands the extracted
directory to the same PluginHost::install path a local install uses. The
extraction is defensive by construction (src/plugin_registry.rs), and your
archive must survive it:
manifest.toml or exactly one
nested plugin directory containing one. Zero manifests or more than one is
a rejected archive.Content-Length cannot force unbounded buffering; extraction is capped at
the same bound so a zip bomb cannot expand without limit.Version resolution: when the installer gets a bare name, it picks the last
matching entry in the index; a pinned name@version selects exactly that
version. Order repeated names in your registry intentionally, oldest first.
zeroclaw plugin search is unauthenticated discovery over the index; it
never installs, enables, or executes anything. Install is where the security
happens: digest check, safe extraction, manifest validation, and the
operator's signature policy, identical to a local-path install. Publish
accordingly: assume everything before install is untrusted transport.
{{#include ../_snippets/plugin-wasm-binary-warning.md}}
SKILL.md (discovery enforces name and description).signature and publisher_key.strict mode operators trust.version, re-sign (the version line is inside the
canonical bytes), re-digest, append the new entry after the old one.