WASM Plugins
WASM plugins run in a sandboxed Wasmtime runtime with no host access by default. They’re ideal for pure-computation tasks (linting, formatting, analysis, code generation) where you want strong isolation without trusting arbitrary native binaries.
Quick start
fledge plugins create fledge-my-lint --wasm
cd fledge-my-lint
cargo build --target wasm32-wasip1 --release
fledge plugins validate
How it works
When fledge runs a WASM plugin:
- The
.wasmbinary is compiled to native code and cached as.cwasm(version-stamped, invalidated on Wasmtime upgrades) - A Wasmtime instance is created with the declared capabilities
- The plugin communicates via the same fledge-v1 protocol as native plugins. JSON messages over host-provided
send/recvfunctions - Execution is bounded by fuel (CPU) and a 60-second wall-clock timeout
- Memory is capped at 256 MB
Capabilities
WASM plugins declare capabilities in plugin.toml. All default to denied.
| Capability | Values | Effect |
|---|---|---|
exec | true/false | Execute shell commands on the host |
store | true/false | Persist key-value data between runs |
metadata | true/false | Read project metadata (language, name, git info) |
filesystem | "none", "project", "plugin" | "project" mounts project root read-only. "plugin" adds a read-write plugin directory. |
network | true/false | Inherit the host network stack |
Users are prompted to approve capabilities at install time. Denied capabilities fail gracefully at runtime. No crashes.
plugin.toml for WASM
[plugin]
name = "fledge-my-lint"
version = "0.1.0"
description = "Custom linting rules"
protocol = "fledge-v1"
runtime = "wasm"
[[commands]]
name = "my-lint"
description = "Run custom lint rules"
binary = "target/wasm32-wasip1/release/fledge-my-lint.wasm"
[hooks]
build = "cargo build --target wasm32-wasip1 --release"
[capabilities]
filesystem = "project"
network = false
The key differences from native plugins:
runtime = "wasm"tells fledge to use the Wasmtime sandboxbinarypoints to a.wasmfile instead of a native executablefilesystemandnetworkare WASM-specific capability fields
Writing a WASM plugin in Rust
The scaffold (fledge plugins create --wasm) generates a working starter. The core pattern:
#[link(wasm_import_module = "fledge")]
extern "C" {
fn send(ptr: *const u8, len: u32) -> i32;
fn recv(ptr: *mut u8, len: u32) -> i32;
}
fn send_message(msg: &str) {
let bytes = msg.as_bytes();
unsafe { send(bytes.as_ptr(), bytes.len() as u32) };
}
fn recv_message(buf: &mut [u8]) -> Option<&str> {
let n = unsafe { recv(buf.as_mut_ptr(), buf.len() as u32) };
if n <= 0 { return None; }
std::str::from_utf8(&buf[..n as usize]).ok()
}
fn main() {
// Read init message
let mut buf = vec![0u8; 65536];
let init = recv_message(&mut buf);
// Do your work...
// Send output
send_message(r#"{"type":"output","data":"Lint passed!"}"#);
}
Building
# One-time setup
rustup target add wasm32-wasip1
# Build
cargo build --target wasm32-wasip1 --release
The [hooks] build field in plugin.toml runs this automatically during fledge plugins install.
Testing locally
# Validate manifest and binary
fledge plugins validate
# Install from local directory (push to GitHub first, or copy manually)
cp -r . ~/Library/Application\ Support/fledge/plugins/fledge-my-lint/
# Run it
fledge plugins run my-lint
Resource limits
| Resource | Limit | Behavior on exceed |
|---|---|---|
| CPU | Fuel-bounded | Plugin traps, host continues |
| Wall clock | 60 seconds | Plugin traps, host continues |
| Memory | 256 MB | Allocation fails, plugin traps |
| Stack | Wasmtime default | Plugin traps on overflow |
All limits result in a clean trap. The host process never crashes or enters an invalid state.
When to use WASM vs native
| Use WASM when… | Use native when… |
|---|---|
| Pure computation over project files | Need to shell out to external tools |
| Untrusted or community plugins | Need unrestricted filesystem access |
| Cross-platform distribution (single binary) | Need pipes, redirects, or shell features |
| You want capability enforcement | Performance-critical host integrations |
Security model
- No ambient authority: WASM plugins start with zero access. Every capability is opt-in and user-approved.
- Memory isolation: Guest memory is separate from host memory. Out-of-bounds access traps the plugin.
- Deterministic execution: Same inputs produce same outputs (no access to system clock, random, etc. unless granted).
- Cache integrity: Compiled
.cwasmfiles are SHA-256 verified and version-stamped. Corruption or Wasmtime version mismatch triggers recompilation.
Limitations
- No interactive UI (prompt/confirm/select). WASM plugins must use non-interactive output
- No direct access to environment variables or the host filesystem beyond declared mounts
- Build requires the
wasm32-wasip1target (rustup target add wasm32-wasip1) - Currently Rust-only for the scaffold; any language that compiles to WASI works in practice