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-wasip2 target installed
  • Swarm built with the wasm-sandbox feature 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:

ComponentRole
WasmRuntimeEngine lifecycle, component compilation, caching
WasmToolTool trait implementation, bridges registry to WASM
HostStatePer-invocation state with capability gating
CredentialInjectorSecret injection into headers, response redaction
ResourceLimitsMemory, 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 FunctionRequired CapabilityDescription
log(level, message)LoggingWrite a log entry
read-workspace-file(path)WorkspaceReadRead a file from the workspace
make-http-request(request)HttpRequestMake an HTTP request
invoke-tool(name, params)ToolInvokeCall another tool in the registry
secret-exists(name)SecretCheckCheck if a secret exists

The tool must export:

ExportSignatureDescription
name()() -> StringTool name
description()() -> StringTool description
schema()() -> StringJSON 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

FieldTypeRequiredDefaultDescription
nameStringYesTool name ([a-z][a-z0-9_-]*)
pathStringYesPath to .wasm component file
capabilitiesString[]No[]Granted capabilities
limitsWasmLimitsConfigNoDefaultsResource limit overrides
secretsString[]No[]Secret names the tool may query
workspace_prefixesString[]No[]Readable workspace path prefixes
endpoint_allowlistString[]No[]Allowed HTTP endpoint patterns
tool_aliasesMap<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.

CapabilityHost FunctionDescription
Logginglog()Write log entries (up to max_log_entries)
WorkspaceReadread-workspace-file()Read files within allowed prefixes
HttpRequestmake-http-request()Make HTTP requests to allowed endpoints
ToolInvokeinvoke-tool()Call other registered tools
SecretChecksecret-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:

LimitDefaultMaximumDescription
max_memory_bytes64 MiB512 MiBMaximum memory allocation
fuel_limit1,000,000,000Computation fuel (instruction budget)
execution_timeout_secs30300Wall-clock timeout
max_log_entries1,000Max log entries per invocation
max_http_requests50Max HTTP requests per invocation
max_tool_invocations20Max tool calls per invocation
max_file_read_bytes10 MiBCumulative file read limit

Exceeding a limit produces the corresponding error:

  • Memory: wasmtime memory allocation trap
  • Fuel: FuelExhausted error
  • Timeout: TimeoutExceeded error (enforced via epoch interruption at 100ms intervals)
  • Counters: RateLimitExceeded error

Security Model

File Access

The read-workspace-file function enforces multiple security layers:

  1. Path traversal prevention — Rejects paths containing .. components
  2. Canonicalization — Resolves symlinks and normalizes paths
  3. Boundary check — Canonical path must be within the workspace root
  4. Prefix matching — If workspace_prefixes is configured, the file must match at least one prefix
  5. Symlink escape detection — Detects symlinks pointing outside the workspace
  6. Size validation — Checks cumulative bytes read against max_file_read_bytes

HTTP Requests

The make-http-request function enforces:

  1. Endpoint allowlist — URL host must match a glob pattern in endpoint_allowlist. Empty allowlist denies all requests
  2. HTTPS enforcement — Non-HTTPS URLs are rejected (except localhost, 127.*, ::1)
  3. Credential injection$SECRET_NAME patterns in headers are replaced with actual values from the environment
  4. 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 if API_TOKEN is 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:

ErrorCause
CompilationFailed.wasm file failed to compile
InstantiationFailedMissing imports or initialization failure
ExecutionTrappedGuest code triggered a trap
FuelExhaustedInstruction budget exceeded
TimeoutExceededEpoch deadline exceeded
HostFunctionErrorHost function returned an error
CapabilityDeniedTool called a function without the required capability
RateLimitExceededResource 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 .wasm file was built with wasm32-wasip2 target
  • Ensure the Component Model is enabled in your build
  • Check that all WIT imports are satisfied

"CapabilityDenied"

  • Add the required capability to the capabilities array in your config
  • Check spelling: capabilities are case-sensitive (Logging, not logging)

"RateLimitExceeded"

  • Increase the relevant limit in the limits config
  • Optimize your tool to make fewer requests/invocations

Tool not appearing in registry

  • Ensure swarm was built with --features wasm-sandbox
  • Verify the path is correct and the file exists
  • Check that name follows the [a-z][a-z0-9_-]* pattern