Skip to content

Plugin Authoring Guide

AgentZero supports extending the agent with custom tools via WASM plugins. Plugins run in a sandboxed WebAssembly environment with strict resource limits, capability-based security, and SHA-256 integrity verification.

  • Rust toolchain with wasm32-wasip1 target: rustup target add wasm32-wasip1
  • AgentZero CLI with the plugins feature enabled (included in the default build)
Terminal window
agentzero plugin new --id my-tool --scaffold rust
cd my-tool/

This generates:

my-tool/
├── Cargo.toml # [lib] crate-type = ["cdylib"]
├── manifest.json # Plugin metadata + capabilities
├── src/lib.rs # Tool implementation
└── .cargo/config.toml # Build target = "wasm32-wasip1"

Use the declare_tool! macro from agentzero-plugin-sdk:

src/lib.rs
use agentzero_plugin_sdk::prelude::*;
declare_tool!("my_tool", execute);
fn execute(input: ToolInput) -> ToolOutput {
let req: serde_json::Value = match serde_json::from_str(&input.input) {
Ok(v) => v,
Err(e) => return ToolOutput::error(format!("invalid input: {e}")),
};
let name = req["name"].as_str().unwrap_or("world");
ToolOutput::success(format!("Hello, {name}!"))
}
Terminal window
cargo build --target wasm32-wasip1 --release

Drop the WASM binary into ./plugins/ for instant auto-discovery:

Terminal window
mkdir -p plugins/my-tool/0.1.0
cp target/wasm32-wasip1/release/my_tool.wasm plugins/my-tool/0.1.0/plugin.wasm
cp manifest.json plugins/my-tool/0.1.0/
agentzero agent "use my_tool to say hello to Ari"

Or use the built-in test command:

Terminal window
agentzero plugin test --manifest manifest.json \
--wasm target/wasm32-wasip1/release/my_tool.wasm --execute
Terminal window
agentzero plugin package --manifest manifest.json \
--wasm target/wasm32-wasip1/release/my_tool.wasm \
--out my-tool-0.1.0.tar
agentzero plugin install --package my-tool-0.1.0.tar
Terminal window
agentzero plugin publish --registry github.com/agentzero-project/plugins

[package]
name = "my-tool"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
agentzero-plugin-sdk = "0.1.4"
serde_json = "1"
/// Input passed from the agent to your plugin.
pub struct ToolInput {
pub input: String, // JSON string from the LLM
pub workspace_root: String, // Absolute path to workspace root
}
/// Output returned from your plugin to the agent.
pub struct ToolOutput {
pub output: String,
pub error: Option<String>,
}
impl ToolOutput {
/// Create a successful result.
pub fn success(output: impl Into<String>) -> Self;
/// Create an error result.
pub fn error(msg: impl Into<String>) -> Self;
/// Create a successful result with a non-fatal warning.
/// Use for idempotent operations where the outcome is correct
/// but the caller should know about an edge condition.
pub fn with_warning(output: impl Into<String>, warning: impl Into<String>) -> Self;
}
declare_tool!("tool_name", handler_function);

This macro generates all required WASM ABI v2 exports:

  • az_alloc — bump allocator for linear memory
  • az_tool_name — returns the tool name
  • az_tool_execute — entry point: receives JSON input, returns JSON output

You only write the handler function.

The runtime provides host functions in the "az" WASM import module. Declare them as extern "C" imports in your plugin:

#[link(wasm_import_module = "az")]
extern "C" {
fn az_log(level: i32, msg_ptr: *const u8, msg_len: i32);
}
fn log(level: i32, msg: &str) {
unsafe { az_log(level, msg.as_ptr(), msg.len() as i32) }
}
const LOG_ERROR: i32 = 0;
const LOG_WARN: i32 = 1;
const LOG_INFO: i32 = 2;
const LOG_DEBUG: i32 = 3;

az_log is always available. Other host calls like az_env_get require "az::az_env_get" in the manifest’s allowed_host_calls.

Instead of manually indexing into serde_json::Value, define a typed request struct:

use serde::Deserialize;
#[derive(Deserialize)]
struct Request {
action: String,
#[serde(default)]
note_id: Option<String>,
}
fn execute(input: ToolInput) -> ToolOutput {
let req: Request = match serde_json::from_str(&input.input) {
Ok(r) => r,
Err(e) => return ToolOutput::error(format!("invalid input: {e}")),
};
// ...
}

This gives compile-time field checking and self-documenting code.


The notepad plugin at plugins/agentzero-plugin-reference/notepad/ is the canonical example for plugin authors. It demonstrates every SDK pattern in ~180 lines:

PatternHow
Typed #[derive(Deserialize)] inputFlat request struct with optional fields
az_log host callextern "C" import + safe wrapper
ToolOutput::with_warningIdempotent delete of non-existent note
WASI filesystemFlat .md files in .agentzero/notepad/
Path securityValidates note IDs against /, \, ..
Action dispatchwrite, read, list, delete

Build and test:

Terminal window
cd plugins/agentzero-plugin-reference/notepad
cargo build --target wasm32-wasip1 --release
# Integration tests (from repo root):
cargo test -p agentzero-plugins --test reference_plugin_integration --features wasm-runtime

Every plugin requires a manifest.json:

{
"id": "my-tool",
"version": "0.1.0",
"entrypoint": "az_tool_execute",
"wasm_file": "plugin.wasm",
"wasm_sha256": "",
"capabilities": ["host:az_log"],
"allowed_host_calls": ["az_log"],
"min_runtime_api": 2,
"max_runtime_api": 2
}
FieldDescription
idUnique plugin identifier (lowercase, hyphens)
versionSemantic version
entrypointWASM export to call (az_tool_execute for ABI v2)
wasm_fileFilename of the compiled WASM module
wasm_sha256SHA-256 hash of the WASM file (set automatically by plugin package)
capabilitiesWASI capabilities and host functions this plugin needs
allowed_host_callsSpecific host functions the plugin may invoke
min_runtime_api / max_runtime_apiCompatible runtime API version range

Capabilities declare what your plugin needs access to. The runtime only grants capabilities that are both declared in the manifest and permitted by the isolation policy.

CapabilityDescription
wasi:filesystem/readRead files (sandboxed to workspace)
wasi:filesystem/read-writeRead and write files
wasi:randomAccess to random number generation
wasi:clockAccess to wall clock and monotonic time
host:az_logStructured logging to the host
host:az_read_fileRead file via host function
host:az_http_getHTTP GET via host (requires allow_network)
host:az_env_getRead environment variable via host

Plugins are discovered from three locations, checked in priority order (later overrides earlier):

PathScopeHot-Reload
~/.local/share/agentzero/plugins/Global (user-wide)No
$PROJECT/.agentzero/plugins/Project-specificNo
./plugins/Current working directory (development)Yes

On startup, the agent scans all three directories, loads valid manifests, and registers plugins alongside native tools. A plugin in ./plugins/ takes highest priority — useful for testing a development version over an installed one.

Versioned layout (installed plugins):

plugins/my-tool/0.1.0/
├── manifest.json
├── plugin.wasm
└── .cache/
├── plugin.cwasm # AOT-compiled (auto-generated)
└── source.sha256 # Cache invalidation hash

Flat layout (development convenience):

plugins/my-tool/
├── manifest.json
└── plugin.wasm

Both layouts are auto-detected. The flat layout is useful during development — no version subdirectory needed. When multiple versions exist in a versioned layout, the latest version (lexicographic) is loaded.


Enable WASM plugins and optionally override the default directories in agentzero.toml:

[security.plugin]
enabled = false # Legacy process plugin (unchanged)
wasm_enabled = true # Enable WASM plugin discovery
# Optional directory overrides (defaults shown):
# global_plugin_dir = "~/.local/share/agentzero/plugins"
# project_plugin_dir = ".agentzero/plugins"
# dev_plugin_dir = "plugins"

When the plugin-dev feature is enabled, the agent watches ./plugins/ for .wasm file changes using the notify crate. When a change is detected:

  1. The old plugin instance is unloaded
  2. The module cache is invalidated
  3. The new .wasm is loaded and re-instantiated
  4. A reload event is logged

Development workflow:

Terminal window
# Terminal 1: watch + rebuild
cargo watch -x 'build --target wasm32-wasip1 --release' \
-s 'cp target/wasm32-wasip1/release/my_tool.wasm plugins/my-tool/0.1.0/plugin.wasm'
# Terminal 2: agent picks up changes automatically
agentzero agent --interactive

Hot-reload is only enabled for ./plugins/ (CWD). Global and project plugins require a restart for stability.


Every plugin runs inside a WebAssembly sandbox with:

  • Memory isolation — plugins cannot access host memory outside their linear memory allocation
  • CPU limits — epoch-based timeout prevents infinite loops (default: 30s)
  • Memory limits — configurable max memory (default: 256MB)
  • Capability gating — filesystem, network, and host function access must be declared and permitted
  • SHA-256 verification — integrity checked on install and every load
[runtime.wasm]
fuel_limit = 1000000
memory_limit_mb = 64
max_module_size_mb = 50
allow_workspace_read = false
allow_workspace_write = false
allowed_hosts = []
LayerProtection
Registry reviewHuman-curated PRs to the registry repo
SHA-256 verificationCLI checks hash on every download and install
WASM sandboxPhysical memory isolation, CPU limits, capability-gated I/O

Terminal window
# Development
agentzero plugin new --id <id> --scaffold rust # Scaffold a new plugin project
agentzero plugin validate --manifest manifest.json # Validate manifest
agentzero plugin test --manifest manifest.json --wasm plugin.wasm --execute # Test
agentzero plugin dev --manifest manifest.json --wasm plugin.wasm # Dev loop
agentzero plugin package --manifest manifest.json --wasm plugin.wasm # Package
# Installation
agentzero plugin install --package my-tool.tar # Install from local file
agentzero plugin install my-tool # Install from registry
agentzero plugin install --url <url> # Install from URL
agentzero plugin update [<id>] # Update plugins
agentzero plugin remove --id my-tool # Remove plugin
# Inventory
agentzero plugin list # List installed plugins
agentzero plugin info <id> # Plugin details
agentzero plugin search <query> # Search registry
agentzero plugin outdated # Check for updates
# State
agentzero plugin enable <id> # Enable a disabled plugin
agentzero plugin disable <id> # Disable without removing
# Publishing
agentzero plugin publish # Submit to registry

Any language that compiles to wasm32-wasip1 can be used to write plugins:

LanguageCompilerNotes
Rustcargo build --target wasm32-wasip1First-class support via SDK
C/C++wasi-sdk / clang --target=wasm32-wasip1Manual ABI implementation
GoGOOS=wasip1 GOARCH=wasm go buildLarger binary size
Zigzig build -Dtarget=wasm32-wasiGood WASM support
AssemblyScriptasc --target wasm32-wasiTypeScript-like syntax

For languages that cannot compile to WASM, see the FFI Bindings guide for registering tools directly from Swift, Kotlin, Python, or Node.js via the callback interface.