WASM Tools
Building and using WebAssembly sandboxed tools in swarm.
Overview
Swarm supports running tools as WebAssembly components in a sandboxed environment. WASM tools execute with strict resource limits and explicit capability grants, providing isolation from the host system. This feature is gated behind the wasm-sandbox Cargo feature flag.
Prerequisites
- Rust toolchain with
wasm32-wasip2target installed - Swarm built with the
wasm-sandboxfeature flag - A WASM component file (
.wasm) compiled from a WIT interface
Build Swarm with WASM Support
cargo build --release --features wasm-sandbox
Install the WASM Target
rustup target add wasm32-wasip2
Architecture
WASM tools run inside a Wasmtime runtime with the Component Model enabled. Each tool invocation gets a fresh Store with its own HostState, ensuring complete isolation between calls.
Agent → ToolRegistry → WasmTool → Wasmtime Store → WASM Component
↑
HostState (capabilities, limits, secrets)
Key components:
| Component | Role |
|---|---|
WasmRuntime | Engine lifecycle, component compilation, caching |
WasmTool | Tool trait implementation, bridges registry to WASM |
HostState | Per-invocation state with capability gating |
CredentialInjector | Secret injection into headers, response redaction |
ResourceLimits | Memory, CPU, I/O quotas |
Step 1: Define the WIT Interface
WASM tools implement the tool world defined in wit/tool.wit. The interface exposes these host functions:
| Host Function | Required Capability | Description |
|---|---|---|
log(level, message) | Logging | Write a log entry |
read-workspace-file(path) | WorkspaceRead | Read a file from the workspace |
make-http-request(request) | HttpRequest | Make an HTTP request |
invoke-tool(name, params) | ToolInvoke | Call another tool in the registry |
secret-exists(name) | SecretCheck | Check if a secret exists |
The tool must export:
| Export | Signature | Description |
|---|---|---|
name() | () -> String | Tool name |
description() | () -> String | Tool description |
schema() | () -> String | JSON Schema for input |
execute(input) | (String) -> Result<String, String> | Execute with JSON input, return JSON result |
Step 2: Implement the Tool
Write your tool in Rust (or any language targeting WASM Component Model):
#![allow(unused)] fn main() { // In your WASM component crate wit_bindgen::generate!({ world: "tool", path: "../wit/tool.wit", }); struct MyTool; impl Guest for MyTool { fn name() -> String { "my-tool".to_string() } fn description() -> String { "A custom sandboxed tool".to_string() } fn schema() -> String { serde_json::json!({ "type": "object", "properties": { "query": { "type": "string", "description": "Search query" } }, "required": ["query"] }).to_string() } fn execute(input: String) -> Result<String, String> { let params: serde_json::Value = serde_json::from_str(&input) .map_err(|e| e.to_string())?; let query = params["query"].as_str().ok_or("missing query")?; // Use host functions (if capabilities are granted) host::log(LogLevel::Info, &format!("Searching for: {query}")); Ok(serde_json::json!({ "result": format!("Results for: {query}") }).to_string()) } } export!(MyTool); }
Step 3: Compile to WASM
Build the component:
cargo build --target wasm32-wasip2 --release
The output .wasm file will be at target/wasm32-wasip2/release/my_tool.wasm.
Step 4: Configure the Tool
Add the tool to wasm_tools in your project configuration:
{
"wasm_tools": [
{
"name": "my-tool",
"path": "./tools/my_tool.wasm",
"capabilities": ["Logging", "HttpRequest"],
"limits": {
"max_memory_bytes": 67108864,
"fuel_limit": 1000000000,
"execution_timeout_secs": 30
},
"secrets": ["API_TOKEN"],
"workspace_prefixes": ["src/", "data/"],
"endpoint_allowlist": ["api.example.com"]
}
]
}
Configuration Fields
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
name | String | Yes | — | Tool name ([a-z][a-z0-9_-]*) |
path | String | Yes | — | Path to .wasm component file |
capabilities | String[] | No | [] | Granted capabilities |
limits | WasmLimitsConfig | No | Defaults | Resource limit overrides |
secrets | String[] | No | [] | Secret names the tool may query |
workspace_prefixes | String[] | No | [] | Readable workspace path prefixes |
endpoint_allowlist | String[] | No | [] | Allowed HTTP endpoint patterns |
tool_aliases | Map<String, String> | No | {} | Aliases for invoke-tool calls |
Capabilities
Each capability unlocks a specific host function. Tools without a capability cannot call the corresponding function — attempts result in a CapabilityDenied error.
| Capability | Host Function | Description |
|---|---|---|
Logging | log() | Write log entries (up to max_log_entries) |
WorkspaceRead | read-workspace-file() | Read files within allowed prefixes |
HttpRequest | make-http-request() | Make HTTP requests to allowed endpoints |
ToolInvoke | invoke-tool() | Call other registered tools |
SecretCheck | secret-exists() | Check if a named secret exists |
Grant only the capabilities your tool actually needs.
Resource Limits
Every WASM tool runs with resource limits to prevent runaway computation or excessive I/O:
| Limit | Default | Maximum | Description |
|---|---|---|---|
max_memory_bytes | 64 MiB | 512 MiB | Maximum memory allocation |
fuel_limit | 1,000,000,000 | — | Computation fuel (instruction budget) |
execution_timeout_secs | 30 | 300 | Wall-clock timeout |
max_log_entries | 1,000 | — | Max log entries per invocation |
max_http_requests | 50 | — | Max HTTP requests per invocation |
max_tool_invocations | 20 | — | Max tool calls per invocation |
max_file_read_bytes | 10 MiB | — | Cumulative file read limit |
Exceeding a limit produces the corresponding error:
- Memory:
wasmtimememory allocation trap - Fuel:
FuelExhaustederror - Timeout:
TimeoutExceedederror (enforced via epoch interruption at 100ms intervals) - Counters:
RateLimitExceedederror
Security Model
File Access
The read-workspace-file function enforces multiple security layers:
- Path traversal prevention — Rejects paths containing
..components - Canonicalization — Resolves symlinks and normalizes paths
- Boundary check — Canonical path must be within the workspace root
- Prefix matching — If
workspace_prefixesis configured, the file must match at least one prefix - Symlink escape detection — Detects symlinks pointing outside the workspace
- Size validation — Checks cumulative bytes read against
max_file_read_bytes
HTTP Requests
The make-http-request function enforces:
- Endpoint allowlist — URL host must match a glob pattern in
endpoint_allowlist. Empty allowlist denies all requests - HTTPS enforcement — Non-HTTPS URLs are rejected (except
localhost,127.*,::1) - Credential injection —
$SECRET_NAMEpatterns in headers are replaced with actual values from the environment - Response scanning — Leaked secret values in response bodies are redacted as
[REDACTED:<name>]
Secrets
Secrets are sourced from environment variables, restricted to the names listed in secrets:
{
"secrets": ["API_TOKEN", "DB_PASSWORD"]
}
secret-exists("API_TOKEN")→ checks ifAPI_TOKENis set in the environment- HTTP header
"Authorization": "Bearer $API_TOKEN"→ injected with the actual value - Secret values appearing in HTTP response bodies are automatically redacted
Error Types
WASM tools can produce these errors:
| Error | Cause |
|---|---|
CompilationFailed | .wasm file failed to compile |
InstantiationFailed | Missing imports or initialization failure |
ExecutionTrapped | Guest code triggered a trap |
FuelExhausted | Instruction budget exceeded |
TimeoutExceeded | Epoch deadline exceeded |
HostFunctionError | Host function returned an error |
CapabilityDenied | Tool called a function without the required capability |
RateLimitExceeded | Resource quota (HTTP requests, tool invocations, etc.) exceeded |
Complete Example
A weather lookup tool with HTTP access and logging:
Configuration:
{
"wasm_tools": [
{
"name": "weather",
"path": "./tools/weather.wasm",
"capabilities": ["Logging", "HttpRequest"],
"limits": {
"max_http_requests": 5,
"execution_timeout_secs": 15
},
"secrets": ["WEATHER_API_KEY"],
"endpoint_allowlist": ["api.openweathermap.org"]
}
]
}
Tool implementation (pseudocode):
#![allow(unused)] fn main() { fn execute(input: String) -> Result<String, String> { let params: Value = serde_json::from_str(&input)?; let city = params["city"].as_str().ok_or("missing city")?; host::log(LogLevel::Info, &format!("Looking up weather for {city}")); let response = host::make_http_request(HttpRequest { method: HttpMethod::Get, url: format!( "https://api.openweathermap.org/data/2.5/weather?q={city}&appid=$WEATHER_API_KEY" ), headers: vec![], body: None, })?; Ok(response.body) } }
The $WEATHER_API_KEY in the URL header is automatically injected, and any leaked key values in the response are redacted.
Troubleshooting
"CompilationFailed"
- Verify the
.wasmfile was built withwasm32-wasip2target - Ensure the Component Model is enabled in your build
- Check that all WIT imports are satisfied
"CapabilityDenied"
- Add the required capability to the
capabilitiesarray in your config - Check spelling: capabilities are case-sensitive (
Logging, notlogging)
"RateLimitExceeded"
- Increase the relevant limit in the
limitsconfig - Optimize your tool to make fewer requests/invocations
Tool not appearing in registry
- Ensure swarm was built with
--features wasm-sandbox - Verify the
pathis correct and the file exists - Check that
namefollows the[a-z][a-z0-9_-]*pattern
Related
- Tools — How tools work in swarm
- Config Schema — Full
WasmToolConfigreference - ADR-004: WASM Sandbox — Design rationale