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 runs all untrusted tools in WebAssembly (WASM) sandboxes powered by Wasmtime. This provides strong isolation without the overhead of Docker containers or VMs.

Architecture

┌─────────────────────────────────────────────────────────────────┐
│                    WASM Tool Execution                          │
│                                                                  │
│   WASM Tool ──► Host Function ──► Allowlist ──► Credential     │
│   (untrusted)   (boundary)        Validator     Injector        │
│                                                      │           │
│                                                      ▼           │
│                                                  Execute         │
│                                                  Request         │
│                                                      │           │
│                                                      ▼           │
│                               ◄──── Leak Detector ◄─ Response   │
│                          (sanitized, no secrets)                │
└─────────────────────────────────────────────────────────────────┘

Security Guarantees

Memory Isolation

WASM tools run in linear memory completely isolated from the host:
  • Default limit: 10 MB per tool
  • No heap access: Cannot read host memory
  • No pointers: Cannot access arbitrary addresses
  • Bounds checking: All memory accesses validated by WASM runtime
Configured via ResourceLimiter:
pub const DEFAULT_MEMORY_LIMIT: usize = 10 * 1024 * 1024; // 10 MB

impl ResourceLimiter for WasmResourceLimiter {
    fn memory_growing(
        &mut self,
        current: usize,
        desired: usize,
        _maximum: Option<usize>,
    ) -> Result<bool> {
        if desired > self.max_memory {
            return Ok(false); // Deny growth beyond limit
        }
        Ok(true)
    }
}

CPU Metering

WASM execution is fuel-metered to prevent infinite loops and CPU exhaustion:
  • Fuel: Virtual “gas” consumed per WASM instruction
  • Default limit: Configurable per tool (typically millions of instructions)
  • Epoch interruption: Periodic checks for timeouts
  • Tokio timeout: Hard wall-clock limit (default 30s)
Example from src/tools/wasm/limits.rs:18-28:
// Default fuel limit (approx 10M instructions)
pub const DEFAULT_FUEL_LIMIT: u64 = 10_000_000;

// Configure in engine
config.consume_fuel(true);
store.add_fuel(fuel_config.initial_fuel)?;

// Check remaining fuel during execution
let remaining = store.get_fuel()?;

No System Access

WASM tools have zero system access by default:
  • ❌ No filesystem (no WASI FS imports)
  • ❌ No raw sockets
  • ❌ No subprocess spawning
  • ❌ No environment variable access
  • ❌ No system clock (host provides now_millis())
All capabilities must be explicitly granted via host functions.

Fresh Instances

Each tool execution creates a fresh WASM instance:
  • Compile once: WASM binary validated and compiled at load time
  • Instantiate per execution: New memory, new store, new state
  • No state reuse: Previous execution’s memory is discarded
  • Side-channel prevention: No shared state between executions
From src/tools/wasm/runtime.rs:
// Compile once during registration
let module = engine.compile(wasm_bytes)?;
let prepared = PreparedModule { module, ... };

// Fresh instance per execution
let instance = linker.instantiate(&mut store, &prepared.module)?;

Capability System

All WASM tools start with zero capabilities. Each must be explicitly granted:

Available Capabilities

CapabilityPurposeRisk Level
HTTPMake HTTP requests to allowlisted endpointsMedium-High
SecretsCheck if secrets exist (not read values)Low
WorkspaceRead files from workspace (read-only)Low-Medium
ToolInvokeCall other tools via aliasesMedium

Capability Configuration

Capabilities are defined in *.capabilities.json files:
{
  "http": {
    "allowlist": [
      {
        "host": "api.openai.com",
        "path_prefix": "/v1/",
        "methods": ["POST"]
      }
    ],
    "rate_limit": {
      "requests_per_minute": 60,
      "requests_per_hour": 1000
    }
  },
  "secrets": {
    "allowed_names": ["openai_api_key"]
  },
  "workspace_read": {
    "allowed_prefixes": ["context/", "daily/"]
  }
}

HTTP Capability

The most sensitive capability. Enforces:
  1. Host allowlisting: Only approved domains
  2. Path restrictions: Specific API endpoints only
  3. Method controls: GET/POST/etc. per endpoint
  4. Rate limiting: Requests per minute/hour
  5. Size limits: Max request/response body sizes
  6. HTTPS enforcement: No plaintext HTTP
  7. Credential injection: Secrets added by host, not WASM
From src/tools/wasm/capabilities.rs:102-169:
pub struct HttpCapability {
    pub allowlist: Vec<EndpointPattern>,
    pub credentials: HashMap<String, CredentialMapping>,
    pub rate_limit: RateLimitConfig,
    pub max_request_bytes: usize,   // Default: 1 MB
    pub max_response_bytes: usize,  // Default: 10 MB
    pub timeout: Duration,          // Default: 30s
}

Secrets Capability

Tools can check existence but never read plaintext:
// WASM can call
let exists = host.secret_exists("openai_api_key"); // true/false

// WASM cannot call (not exposed)
let value = host.get_secret("openai_api_key"); // ❌ Not available
Credentials are injected at the host boundary during HTTP requests. See Credential Management.

Workspace Capability

Read-only access to workspace files with prefix restrictions:
// Allowed (if "context/" prefix is granted)
let content = workspace_read("context/notes.md");

// Blocked (prefix not in allowed list)
let content = workspace_read("secrets/api_keys.txt"); // ❌ Access denied

// Blocked (path traversal)
let content = workspace_read("../../../etc/passwd"); // ❌ Validation fails
Path validation from src/channels/wasm/capabilities.rs:136-157:
fn validate_workspace_path(path: &str) -> Result<String, String> {
    // Block absolute paths
    if path.starts_with('/') {
        return Err("Absolute paths not allowed");
    }
    
    // Block path traversal
    if path.contains("..") {
        return Err("Parent directory references not allowed");
    }
    
    // Block null bytes
    if path.contains('\0') {
        return Err("Null bytes not allowed");
    }
    
    Ok(path)
}

ToolInvoke Capability

Indirect tool access via aliases (prevents direct tool enumeration):
{
  "tool_invoke": {
    "aliases": {
      "search": "web_search_tool",
      "summarize": "summarization_tool"
    }
  }
}
WASM only knows aliases, not real tool names.

Security Constraints

ThreatMitigationImplementation
CPU exhaustionFuel meteringstore.add_fuel(), epoch interrupts
Memory exhaustionResourceLimiter10MB default, growth denied
Infinite loopsTimeout + epochstokio::time::timeout()
Filesystem accessNo WASI FSOnly workspace_read host function
Network accessAllowlist onlyAllowlistValidator
Credential exposureInjection boundarySecrets never enter WASM memory
Secret exfiltrationLeak detectorScan requests/responses
Log spamEntry limitsMax 1000 entries, 4KB per message
Path traversalPath validationNo .., no / prefix
Trap recoveryDiscard instanceNever reuse after trap
Side channelsFresh instanceNew memory per execution
Rate abusePer-tool limitsRateLimiter per capability
Binary tamperingBLAKE3 hashingHash verification on load

Host Functions (V2 API)

WASM tools interact with the host via these functions:

Logging

fn log(level: i32, message_ptr: i32, message_len: i32)

Time

fn now_millis() -> i64

Workspace Read

fn workspace_read(path_ptr: i32, path_len: i32) -> i32  // Returns result handle

HTTP Request

fn http_request(
    method_ptr: i32, method_len: i32,
    url_ptr: i32, url_len: i32,
    headers_ptr: i32, headers_len: i32,
    body_ptr: i32, body_len: i32
) -> i32  // Returns result handle

Secret Exists

fn secret_exists(name_ptr: i32, name_len: i32) -> i32  // 1 = exists, 0 = not found

Tool Invoke

fn tool_invoke(
    alias_ptr: i32, alias_len: i32,
    params_ptr: i32, params_len: i32
) -> i32  // Returns result handle
All string parameters are UTF-8 encoded. Buffers are bounds-checked.

Failure Modes

WASM Trap

If WASM code traps (invalid memory access, division by zero, etc.):
  1. Execution stops immediately
  2. Instance is discarded (never reused)
  3. Error returned to caller
  4. No host state corruption
Example trap handling:
match instance.execute(&mut store, params) {
    Ok(result) => result,
    Err(e) if e.is_wasm_trap() => {
        tracing::error!(tool = tool_name, "WASM trap: {}", e);
        return Err(WasmError::Trap { ... });
    }
}

Fuel Exhaustion

WasmError::OutOfFuel {
    tool: "expensive_tool",
    fuel_used: 10000000,
}

Memory Limit Exceeded

WasmError::MemoryLimitExceeded {
    tool: "greedy_tool",
    attempted: 15_000_000,
    limit: 10_000_000,
}

Capability Denied

WasmError::CapabilityDenied {
    tool: "sneaky_tool",
    capability: "http",
    reason: "Host not in allowlist: evil.com",
}

Best Practices

For Tool Authors

  1. Request minimal capabilities: Only what you need
  2. Use tight allowlists: Specific hosts and paths
  3. Handle errors gracefully: Capability denials, rate limits
  4. Avoid secrets in logs: Use secret_exists() checks
  5. Respect fuel limits: Optimize expensive operations

For System Administrators

  1. Audit capabilities.json: Review all granted permissions
  2. Monitor logs: Watch for denied requests (potential attacks)
  3. Rotate secrets: Use expires_at for temporary credentials
  4. Set conservative limits: Start with low fuel/memory, increase if needed
  5. Keep allowlists tight: Only domains you trust

Source Code References

See Also

Build docs developers (and LLMs) love