Plugins
What Are Plugins?
Section titled “What Are Plugins?”Plugins are WASM modules with a PLUGIN.toml manifest that extend AgentZero with new commands. They run inside the same wasmtime sandbox used for WASM skills, with fuel-based time limits and no ambient filesystem or network access.
Plugins vs. Skills
Section titled “Plugins vs. Skills”Skills are the original extension mechanism. They use SKILL.md frontmatter, live in .agentzero/skills/, and support multiple runtimes (instruction-only, WASM, host-supervised). Plugins are a newer, more structured alternative:
| Feature | Skills | Plugins |
|---|---|---|
| Manifest | SKILL.md (Markdown frontmatter) | PLUGIN.toml (structured TOML) |
| Location | .agentzero/skills/<name>/ | .agentzero/plugins/<name>/ |
| Runtime | none, wasm, host_supervised | wasm only |
| Commands | Single entrypoint | Multiple named commands |
| Host imports | _start or main | az::* host imports (filesystem, clock, logging) |
Plugins are the recommended path for all new extensions (see ADR 0016). They have richer host integration, multiple commands per module, MCP bridge support, and external subcommand dispatch.
External Subcommand Dispatch
Section titled “External Subcommand Dispatch”Installed plugins can be invoked directly as az subcommands:
az brain capture "interesting thought"az my-plugin do-somethingIf az <name> doesn’t match a built-in command, it checks for an installed plugin with that name and dispatches to its WASM module.
Managing Plugins
Section titled “Managing Plugins”List Installed Plugins
Section titled “List Installed Plugins”az plugin listShows all plugins installed in .agentzero/plugins/:
NAME VERSION CMDS DESCRIPTION------------------------------------------------------------brain 0.1.0 10 Personal LLM wiki — manage a Karpathy-style knowledge vaultInstall a Plugin
Section titled “Install a Plugin”# From a local directoryaz plugin install /path/to/plugin-directory
# From a GitHub releaseaz plugin install owner/repoThe source must contain a PLUGIN.toml and at least one .wasm file. GitHub installs download the latest release, verify SHA-256 checksums, and update the lockfile at .agentzero/plugins/plugins.lock.
Show Plugin Details
Section titled “Show Plugin Details”az plugin info brainDisplays the plugin manifest, available commands, WASM path, and install location.
PLUGIN.toml Format
Section titled “PLUGIN.toml Format”Every plugin has a PLUGIN.toml in its root directory:
[plugin]name = "brain"version = "0.1.0"description = "Personal LLM wiki — manage a Karpathy-style knowledge vault"runtime = "wasm"wasm_path = "brain.wasm"
[[commands]]name = "init"description = "Initialize a brain vault"
[[commands]]name = "today"description = "Create or show today's daily note"
[[commands]]name = "capture"description = "Append a timestamped thought to today's daily note"Manifest Fields
Section titled “Manifest Fields”| Field | Required | Description |
|---|---|---|
plugin.name | yes | Plugin identifier (used as the directory name) |
plugin.version | yes | Semantic version |
plugin.description | yes | Human-readable description |
plugin.runtime | no | Runtime type (default: wasm) |
plugin.wasm_path | no | Path to the WASM module relative to the plugin directory. If omitted, the registry scans for any .wasm file. |
plugin.permissions | no | Declared capabilities: file_read, file_write, shell, network |
plugin.keywords | no | Keywords for progressive disclosure — plugin commands auto-injected into LLM tool list when user messages match |
commands | no | Array of {name, description} entries declaring available subcommands |
Building a Plugin
Section titled “Building a Plugin”Plugins compile from Rust to wasm32-unknown-unknown. A typical plugin has two parts:
- A library crate (
crates/agentzero-<name>/) containing the business logic with aBrainFs-style trait for filesystem abstraction. - A WASM guest crate (
plugins/<name>/) that implements the trait usingaz::*host imports and exportsrun(ptr, len) -> i64.
Project Structure
Section titled “Project Structure”plugins/my-plugin/ Cargo.toml # crate-type = ["cdylib"], targets wasm32-unknown-unknown PLUGIN.toml # manifest src/lib.rs # WASM guest: host imports + run() exportCargo.toml
Section titled “Cargo.toml”[package]name = "my-plugin-wasm"version = "0.1.0"edition = "2021"publish = false
[lib]crate-type = ["cdylib"]
[dependencies]serde = { version = "1", features = ["derive"] }serde_json = "1"
[profile.release]opt-level = "s"lto = truestrip = trueBuild Command
Section titled “Build Command”cargo build --manifest-path plugins/my-plugin/Cargo.toml \ --target wasm32-unknown-unknown --releaseOr use the Justfile recipe:
just build-plugin my-pluginInstall After Building
Section titled “Install After Building”just install-plugin my-pluginThis copies the built .wasm and PLUGIN.toml into .agentzero/plugins/<name>/.
The run(input) -> output Protocol
Section titled “The run(input) -> output Protocol”Plugins export two functions:
| Export | Signature | Purpose |
|---|---|---|
alloc | (size: i32) -> i32 | Allocate bytes in guest memory for the host to write into |
run | (ptr: i32, len: i32) -> i64 | Execute a command; input is a JSON string, return is a packed (ptr << 32 | len) pointing to a JSON response |
Input JSON
Section titled “Input JSON”The host writes a JSON object into guest memory. The action field selects which command to run:
{ "action": "capture", "root": "/home/user/brain", "message": "interesting thought about WASM sandboxing"}Output JSON
Section titled “Output JSON”The guest returns a packed pointer to a JSON response:
{ "success": true, "output": "Captured to wiki/daily/2026-05-11.md", "error": null}On failure:
{ "success": false, "output": null, "error": "path traversal detected: ../etc/passwd"}Host Imports
Section titled “Host Imports”Plugins access host capabilities through the az import module. These are the same imports used by dynamically generated WASM tools:
| Import | Signature | Description |
|---|---|---|
az::read_file | (ptr, len) -> i64 | Read a file; returns packed (ptr, len) string or -1 on error |
az::write_file | (path_ptr, path_len, content_ptr, content_len) -> i32 | Write a file; returns 0 on success |
az::append_file | (path_ptr, path_len, content_ptr, content_len) -> i32 | Append to a file; returns 0 on success |
az::list_dir | (ptr, len) -> i64 | List directory entries as JSON array; returns packed string |
az::create_dir | (ptr, len) -> i32 | Create directory and parents; returns 0 on success |
az::file_exists | (ptr, len) -> i32 | Check existence; 0 = exists, 1 = not found |
az::now | () -> i64 | Current time as packed ISO 8601 string |
az::log | (ptr, len) | Write a message to the host log |
az::http_request | (url_ptr, url_len, method_ptr, method_len, headers_ptr, headers_len, body_ptr, body_len) -> i64 | Make an HTTP request; returns packed JSON response string. Policy-checked and PII-scanned. |
az::run_command | (cmd_ptr, cmd_len, args_ptr, args_len) -> i64 | Execute an allowlisted shell command. Returns packed JSON: {exit_code, stdout, stderr}. Allowed: git, rg, grep, find, wc, sort, head, tail. |
All filesystem imports are mediated by the host’s PathValidator, which rejects path traversal (..), null bytes, and access outside the allowed root directory. WASM modules that declare undeclared imports are rejected before execution.
Network Access
Section titled “Network Access”By default, plugins have no network access. To enable outbound HTTP requests, declare a [plugin.network] section in PLUGIN.toml:
[plugin.network]policy = "allow_egress_filtered"allowed_hosts = ["api.slack.com", "hooks.slack.com"]Network Policies
Section titled “Network Policies”| Policy | Behavior |
|---|---|
deny (default) | All HTTP requests blocked |
allow_egress | All outbound URLs allowed |
allow_egress_filtered | Only URLs matching allowed_hosts are allowed (includes subdomains) |
Security Checks
Section titled “Security Checks”Every az::http_request call passes through three checks before the request is sent:
- Capability — the project’s
policy.ymlmust allowNetworkRequest - URL allowlist — the URL’s host must match the plugin’s declared
allowed_hosts - PII scan — the request body is scanned for secrets and PII. If any are detected, the request is blocked.
The response is returned as a JSON string: {"status": 200, "body": "..."}.
Security
Section titled “Security”Plugin execution follows the same security model as WASM skills:
- Policy-gated:
wasm_executionmust bealloworrequire_approvalin.agentzero/policy.yml - Path validation: All filesystem paths pass through the
PathValidatorwhich blocks traversal attacks and sensitive paths (.ssh,.env,.aws/credentials) - Memory capped: 64 MB default
- Time-limited: 30-second fuel budget
- No ambient access: No filesystem or network access except through declared host imports
- Import verification: Modules with undeclared imports are rejected before execution
- Audit trail: Every plugin execution emits a structured audit event
- Network controlled: HTTP requests require explicit
[plugin.network]declaration, URL allowlist, and PII body scan
MCP Bridge
Section titled “MCP Bridge”Plugin commands are automatically exposed as MCP tools when running az mcp. MCP clients (Claude Code, Cursor, Zed) can discover and invoke plugin commands without any configuration.
Plugin tools appear as plugin_<name>_<command> in the MCP tools list:
plugin_brain_query — [plugin:brain] Search the vaultplugin_brain_capture — [plugin:brain] Capture a thought to today's daily notePlugin Lockfile
Section titled “Plugin Lockfile”Installed plugins are tracked in .agentzero/plugins/plugins.lock:
{ "brain": { "version": "0.1.0", "source": "github:auser/brain-plugin", "checksum": "sha256:a1b2c3..." }}Use verify_integrity() to check WASM checksums against the lockfile.