Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/nearai/ironclaw/llms.txt

Use this file to discover all available pages before exploring further.

IronClaw’s WASM tool system allows you to build custom tools in Rust that compile to WebAssembly and run in a sandboxed environment with strict security controls. Location: src/tools/wasm/

Architecture

┌─────────────────────────────────────────────────────────────────────────────┐
│                              WASM Tool Execution                             │
│                                                                              │
│   WASM Tool ──▶ Host Function ──▶ Allowlist ──▶ Credential ──▶ Execute     │
│   (untrusted)   (boundary)        Validator     Injector       Request      │
│                                                                    │        │
│                                                                    ▼        │
│                              ◀────── Leak Detector ◀────── Response        │
│                          (sanitized, no secrets)                            │
└─────────────────────────────────────────────────────────────────────────────┘
Key principles:
  • Compile once, instantiate fresh: Tools are validated and compiled at registration time. Each execution creates a fresh instance (NEAR pattern).
  • Fuel metering: CPU usage is limited via Wasmtime’s fuel system.
  • Memory limits: Memory growth is bounded via ResourceLimiter (default 10MB).
  • Capability-based security: Features are opt-in via Capabilities.
  • Credential isolation: Tools never see raw credentials, only placeholder names.
Location: src/tools/wasm/mod.rs:1-75

Security Constraints

ThreatMitigation
CPU exhaustionFuel metering
Memory exhaustionResourceLimiter, 10MB default
Infinite loopsEpoch interruption + tokio timeout
Filesystem accessNo WASI FS, only host workspace_read
Network accessAllowlisted endpoints only
Credential exposureInjection at host boundary only
Secret exfiltrationLeak detector scans all outputs
Log spamMax 1000 entries, 4KB per message
Path traversalValidate paths (no .., no / prefix)
Trap recoveryDiscard instance, never reuse
WASM tamperingBLAKE3 hash verification on load
Location: src/tools/wasm/mod.rs:32-50

WIT Interface

WASM tools implement the sandboxed-tool world defined in wit/tool.wit:
package near:agent@0.2.0;

world sandboxed-tool {
    import host;
    export tool;
}

interface host {
    enum log-level { trace, debug, info, warn, error }
    
    // Logging
    log: func(level: log-level, message: string);
    
    // Time access
    now-millis: func() -> u64;
    
    // Workspace read access
    workspace-read: func(path: string) -> option<string>;
    
    // HTTP requests (with allowlist validation)
    http-request: func(
        method: string,
        url: string,
        headers-json: string,
        body: option<list<u8>>,
        timeout-ms: option<u32>,
    ) -> result<http-response, string>;
    
    record http-response {
        status: u16,
        headers-json: string,
        body: list<u8>,
    }
    
    // Secret existence check (not the value)
    secret-exists: func(name: string) -> bool;
    
    // Invoke other tools (with permission)
    tool-invoke: func(
        name: string,
        params-json: string,
    ) -> result<string, string>;
}

interface tool {
    // Tool metadata
    name: func() -> string;
    description: func() -> string;
    parameters-schema: func() -> string;
    
    // Main execution function
    execute: func(params-json: string) -> result<string, string>;
}
Location: wit/tool.wit

Capabilities System

Capabilities define what a WASM tool is allowed to do. They are declared in a <tool-name>.capabilities.json file. Location: src/tools/wasm/capabilities.rs

HTTP Capability

pub struct HttpCapability {
    pub allowlist: Vec<EndpointPattern>,
    pub rate_limit: Option<RateLimitConfig>,
    pub timeout_secs: Option<u64>,
    pub credentials: HashMap<String, CredentialMapping>,
}

pub struct EndpointPattern {
    pub host: String,
    pub path_prefix: Option<String>,
    pub methods: Vec<String>,
}
Example (github-tool.capabilities.json):
{
  "capabilities": {
    "http": {
      "allowlist": [
        {
          "host": "api.github.com",
          "path_prefix": "/",
          "methods": ["GET", "POST"]
        }
      ],
      "credentials": {
        "github_token": {
          "secret_name": "github_token",
          "location": {"type": "bearer"},
          "host_patterns": ["api.github.com"]
        }
      },
      "rate_limit": {
        "requests_per_minute": 60,
        "requests_per_hour": 3600
      }
    },
    "secrets": {
      "allowed_names": ["github_token", "github_*"]
    }
  }
}
Location: src/tools/wasm/capabilities_schema.rs:28-80

Workspace Capability

pub struct WorkspaceCapability {
    pub allowed_paths: Vec<String>,
    pub read_only: bool,
}
Example:
{
  "workspace": {
    "allowed_paths": ["docs/", "*.md"],
    "read_only": true
  }
}

Secrets Capability

pub struct SecretsCapability {
    pub allowed_names: Vec<String>,
}
Example:
{
  "secrets": {
    "allowed_names": ["github_token", "github_*"]
  }
}

Tool Invoke Capability

pub struct ToolInvokeCapability {
    pub allowed_tools: Vec<String>,
}
Example:
{
  "tool_invoke": {
    "allowed_tools": ["http", "memory_read"]
  }
}

Resource Limits

pub struct ResourceLimits {
    pub memory_bytes: u64,
    pub fuel_limit: u64,
    pub timeout_ms: u64,
}

impl Default for ResourceLimits {
    fn default() -> Self {
        Self {
            memory_bytes: DEFAULT_MEMORY_LIMIT,  // 10 MB
            fuel_limit: DEFAULT_FUEL_LIMIT,      // 100M ops
            timeout_ms: DEFAULT_TIMEOUT,         // 30 seconds
        }
    }
}
Location: src/tools/wasm/limits.rs:10-42 Constants:
pub const DEFAULT_MEMORY_LIMIT: u64 = 10 * 1024 * 1024;  // 10 MB
pub const DEFAULT_FUEL_LIMIT: u64 = 100_000_000;         // 100M operations
pub const DEFAULT_TIMEOUT: u64 = 30_000;                 // 30 seconds
Location: src/tools/wasm/limits.rs:7-9

Credential Injection

WASM tools never see raw credentials. The host injects them at request time. Location: src/tools/wasm/credential_injector.rs

Credential Mapping

pub struct CredentialMapping {
    pub secret_name: String,
    pub location: CredentialLocation,
    pub host_patterns: Vec<String>,
}

pub enum CredentialLocation {
    Bearer,                           // Authorization: Bearer <token>
    Header { name: String },          // Custom-Header: <token>
    QueryParam { name: String },      // ?api_key=<token>
    UrlPlaceholder { placeholder: String },  // https://api.com/{TOKEN}/data
}
Example injection locations:
{
  "credentials": {
    "github_token": {
      "secret_name": "github_token",
      "location": {"type": "bearer"},
      "host_patterns": ["api.github.com"]
    },
    "telegram_token": {
      "secret_name": "telegram_bot_token",
      "location": {
        "type": "url_placeholder",
        "placeholder": "BOT_TOKEN"
      },
      "host_patterns": ["api.telegram.org"]
    },
    "custom_api_key": {
      "secret_name": "api_key",
      "location": {
        "type": "query_param",
        "name": "api_key"
      },
      "host_patterns": ["api.example.com"]
    }
  }
}
Location: src/tools/wasm/credential_injector.rs:28-150

Placeholder Substitution

Tools can reference credentials by placeholder in URLs and headers:
// In WASM tool code:
let url = "https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/sendMessage";
let headers = json!({
    "Authorization": "Bearer {GITHUB_TOKEN}"
});

// Host substitutes before making request
let injected_url = "https://api.telegram.org/bot1234567890:ABC.../sendMessage";
let injected_headers = {"Authorization": "Bearer ghp_abc123..."};
Location: src/tools/wasm/wrapper.rs:129-145

Leak Detection

All WASM tool outputs are scanned for credential leakage:
let leak_detector = LeakDetector::new();
for cred in &self.credentials {
    leak_detector.add_secret(&cred.secret_value);
}
let sanitized = leak_detector.scan(&output)?;
Location: src/tools/wasm/wrapper.rs:300-320 Secrets are replaced with [REDACTED:SECRET_NAME] before returning to the LLM.

Building a WASM Tool

1. Create Project

cd tools-src
cargo new --lib my-tool
cd my-tool

2. Configure Cargo.toml

[package]
name = "my-tool"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]

[dependencies]
wit-bindgen = "0.28"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

[profile.release]
opt-level = "z"
lto = true
codegen-units = 1
strip = true

3. Implement Tool

//! My custom WASM tool.

wit_bindgen::generate!({
    world: "sandboxed-tool",
    path: "../../wit/tool.wit",
});

use serde::{Deserialize, Serialize};

struct MyTool;

#[derive(Deserialize)]
struct Params {
    input: String,
}

#[derive(Serialize)]
struct Output {
    result: String,
}

impl exports::near::agent::tool::Guest for MyTool {
    fn name() -> String {
        "my_tool".to_string()
    }

    fn description() -> String {
        "Does something useful".to_string()
    }

    fn parameters_schema() -> String {
        serde_json::json!({
            "type": "object",
            "properties": {
                "input": {
                    "type": "string",
                    "description": "Input text"
                }
            },
            "required": ["input"]
        }).to_string()
    }

    fn execute(params_json: String) -> Result<String, String> {
        let params: Params = serde_json::from_str(&params_json)
            .map_err(|e| format!("Invalid params: {}", e))?;

        // Use host functions
        near::agent::host::log(
            near::agent::host::LogLevel::Info,
            format!("Processing: {}", params.input),
        );

        // Make HTTP request (if allowed)
        let response = near::agent::host::http_request(
            "GET".to_string(),
            "https://api.github.com/zen".to_string(),
            "{}".to_string(),
            None,
            Some(5000),
        ).map_err(|e| format!("HTTP error: {}", e))?;

        let result = String::from_utf8_lossy(&response.body).to_string();

        let output = Output { result };
        Ok(serde_json::to_string(&output).unwrap())
    }
}

export!(MyTool);

4. Create Capabilities File

my-tool.capabilities.json:
{
  "http": {
    "allowlist": [
      {
        "host": "api.github.com",
        "path_prefix": "/",
        "methods": ["GET"]
      }
    ],
    "rate_limit": {
      "requests_per_minute": 60,
      "requests_per_hour": 1000
    }
  }
}

5. Build

cargo build --target wasm32-wasip2 --release
Output: target/wasm32-wasip2/release/my_tool.wasm

6. Install

ironclaw tool install target/wasm32-wasip2/release/my_tool.wasm \
  --capabilities my-tool.capabilities.json

Runtime APIs

WasmToolRuntime

Location: src/tools/wasm/runtime.rs
pub struct WasmToolRuntime {
    engine: Engine,
    linker: Arc<Linker<StoreData>>,
}

impl WasmToolRuntime {
    pub fn new(config: WasmRuntimeConfig) -> Result<Self, WasmError>;
    
    pub async fn prepare(
        &self,
        name: &str,
        wasm_bytes: &[u8],
        limits: Option<ResourceLimits>,
    ) -> Result<PreparedModule, WasmError>;
}
Location: src/tools/wasm/runtime.rs:52-150

WasmToolWrapper

Location: src/tools/wasm/wrapper.rs
pub struct WasmToolWrapper {
    runtime: Arc<WasmToolRuntime>,
    module: PreparedModule,
    capabilities: Capabilities,
    description_override: Option<String>,
    schema_override: Option<serde_json::Value>,
    secrets_store: Option<Arc<dyn SecretsStore>>,
    oauth_refresh: Option<OAuthRefreshConfig>,
}

impl WasmToolWrapper {
    pub fn new(
        runtime: Arc<WasmToolRuntime>,
        module: PreparedModule,
        capabilities: Capabilities,
    ) -> Self;
    
    pub fn with_description(mut self, desc: impl Into<String>) -> Self;
    pub fn with_schema(mut self, schema: serde_json::Value) -> Self;
    pub fn with_secrets_store(mut self, store: Arc<dyn SecretsStore>) -> Self;
    pub fn with_oauth_refresh(mut self, config: OAuthRefreshConfig) -> Self;
}
Location: src/tools/wasm/wrapper.rs:67-98

Storage

WASM tools can be stored in a database with integrity verification. Location: src/tools/wasm/storage.rs

Store Tool

pub async fn store_tool(
    &self,
    params: StoreToolParams<'_>,
) -> Result<StoredWasmTool, WasmStorageError>;

pub struct StoreToolParams<'a> {
    pub user_id: &'a str,
    pub name: &'a str,
    pub description: &'a str,
    pub wasm_binary: &'a [u8],
    pub parameters_schema: serde_json::Value,
    pub trust_level: TrustLevel,
    pub capabilities: Option<StoredCapabilities>,
}

Load Tool

pub async fn get_with_binary(
    &self,
    user_id: &str,
    name: &str,
) -> Result<StoredWasmToolWithBinary, WasmStorageError>;
Integrity verification:
  • BLAKE3 hash computed on store
  • Hash verified on load
  • Prevents WASM tampering
Location: src/tools/wasm/storage.rs:120-280

Tool Discovery

Discover WASM tools in the tools-src/ directory: Location: src/tools/wasm/loader.rs
pub fn discover_tools(tools_dir: &Path) -> Vec<DiscoveredTool>;

pub struct DiscoveredTool {
    pub name: String,
    pub wasm_path: PathBuf,
    pub capabilities_path: Option<PathBuf>,
}
Example:
let tools = discover_tools(Path::new("tools-src"));
for tool in tools {
    println!("Found: {} at {:?}", tool.name, tool.wasm_path);
}
Location: src/tools/wasm/loader.rs:28-150

OAuth Authentication

WASM tools can declare OAuth requirements in capabilities:
{
  "auth": {
    "secret_name": "slack_bot_token",
    "display_name": "Slack",
    "oauth": {
      "authorization_url": "https://slack.com/oauth/v2/authorize",
      "token_url": "https://slack.com/api/oauth.v2.access",
      "client_id_env": "SLACK_OAUTH_CLIENT_ID",
      "client_secret_env": "SLACK_OAUTH_CLIENT_SECRET",
      "scopes": ["chat:write", "channels:read"],
      "use_pkce": false
    },
    "env_var": "SLACK_BOT_TOKEN"
  }
}
Auth flow priority:
  1. Check env_var - if set in environment, use it directly
  2. Check oauth - if configured, open browser for OAuth flow
  3. Fall back to instructions + manual token entry
Location: See src/tools/README.md:49-103

Examples

Real-world WASM tools in tools-src/:
  • GitHub (tools-src/github/) - Repository management, issues, PRs, workflows
  • Gmail (tools-src/gmail/) - Search, read, send emails
  • Slack (tools-src/slack/) - Post messages, read channels
  • Telegram (tools-src/telegram/) - Send messages, manage conversations
  • Google Calendar (tools-src/google-calendar/) - Manage events
  • Google Drive (tools-src/google-drive/) - File management
  • Google Sheets (tools-src/google-sheets/) - Spreadsheet operations
  • Google Docs (tools-src/google-docs/) - Document editing

Best Practices

  1. Minimize allowlist scope: Only allow the specific hosts/paths your tool needs
  2. Use credential injection: Never hardcode secrets
  3. Validate inputs: Check parameter lengths and formats
  4. Handle errors gracefully: Return descriptive error messages
  5. Log appropriately: Use host log() for debugging
  6. Optimize binary size: Use opt-level = "z" and strip = true
  7. Test thoroughly: Test with and without credentials
  8. Document schema: Provide clear descriptions for all parameters

Next Steps

MCP Integration

Learn about MCP server integration

Building Tools

Use the software builder to create tools

Build docs developers (and LLMs) love