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 implements strict network controls to ensure WASM tools can only access approved API endpoints with proper rate limiting and size constraints.

Security Model

WASM tools have zero network access by default. HTTP capability must be:
  1. Explicitly granted via capabilities file
  2. Allowlisted to specific hosts and paths
  3. Rate limited to prevent abuse
  4. Size constrained for requests and responses
  5. HTTPS-only (no plaintext HTTP)

HTTP Request Flow

┌─────────────────────────────────────────────────────────────────┐
│                    HTTP Request Pipeline                        │
│                                                                  │
│   WASM Tool                                                      │
│      │                                                          │
│      ▼                                                          │
│   1. http_request(url, method, headers, body)                   │
│      │                                                          │
│      ▼                                                          │
│   2. Allowlist Validator                                         │
│      ├─► Host in allowlist?                                   │
│      ├─► Path prefix matches?                                 │
│      ├─► HTTP method allowed?                                 │
│      └─► HTTPS only?                                          │
│      │                                                          │
│      ▼                                                          │
│   3. Leak Detector (Outbound)                                    │
│      ├─► Scan URL for secrets                                 │
│      ├─► Scan headers for secrets                             │
│      └─► Scan body for secrets                                │
│      │                                                          │
│      ▼                                                          │
│   4. Rate Limiter                                                │
│      ├─► Check requests/minute                                │
│      └─► Check requests/hour                                  │
│      │                                                          │
│      ▼                                                          │
│   5. Size Validator                                              │
│      ├─► Request body < max_request_bytes?                    │
│      └─► Headers reasonable?                                  │
│      │                                                          │
│      ▼                                                          │
│   6. Credential Injector                                         │
│      └─► Inject secrets for matching hosts                   │
│      │                                                          │
│      ▼                                                          │
│   7. Execute HTTP Request (reqwest)                              │
│      │                                                          │
│      ▼                                                          │
│   8. Response Validator                                          │
│      └─► Response body < max_response_bytes?                  │
│      │                                                          │
│      ▼                                                          │
│   9. Leak Detector (Inbound)                                     │
│      └─► Scan response for secrets                            │
│      │                                                          │
│      ▼                                                          │
│   10. Return to WASM Tool                                        │
└─────────────────────────────────────────────────────────────────┘

Endpoint Allowlisting

The first and most critical defense: WASM tools can only make HTTP requests to explicitly approved endpoints.

Allowlist Configuration

Defined in *.capabilities.json:
{
  "http": {
    "allowlist": [
      {
        "host": "api.openai.com",
        "path_prefix": "/v1/",
        "methods": ["POST"]
      },
      {
        "host": "api.stripe.com",
        "path_prefix": "/v1/",
        "methods": ["GET", "POST"]
      },
      {
        "host": "*.example.com",
        "methods": ["GET"]
      }
    ]
  }
}

EndpointPattern Structure

From src/tools/wasm/capabilities.rs:172-200:
pub struct EndpointPattern {
    /// Hostname pattern (e.g., "api.example.com", "*.example.com")
    pub host: String,
    
    /// Optional path prefix (e.g., "/v1/", "/api/")
    /// None = all paths allowed
    pub path_prefix: Option<String>,
    
    /// Allowed HTTP methods (empty = all methods allowed)
    pub methods: Vec<String>,
}

impl EndpointPattern {
    pub fn host(host: impl Into<String>) -> Self {
        Self {
            host: host.into(),
            path_prefix: None,
            methods: Vec::new(),
        }
    }
    
    pub fn with_path_prefix(mut self, prefix: impl Into<String>) -> Self {
        self.path_prefix = Some(prefix.into());
        self
    }
    
    pub fn with_methods(mut self, methods: Vec<String>) -> Self {
        self.methods = methods;
        self
    }
}

Validation Logic

From src/tools/wasm/allowlist.rs:74-164:
pub struct AllowlistValidator {
    patterns: Vec<EndpointPattern>,
    require_https: bool,  // Default: true
}

pub fn validate(&self, url: &str, method: &str) -> AllowlistResult {
    // 1. Check for empty allowlist
    if self.patterns.is_empty() {
        return AllowlistResult::Denied(DenyReason::EmptyAllowlist);
    }
    
    // 2. Parse URL
    let parsed = parse_url(url)?;
    
    // 3. Check HTTPS requirement
    if self.require_https && parsed.scheme != "https" {
        return AllowlistResult::Denied(DenyReason::InsecureScheme(parsed.scheme));
    }
    
    // 4. Find matching pattern
    for pattern in &self.patterns {
        if pattern.matches(&parsed.host, &parsed.path, method) {
            return AllowlistResult::Allowed;
        }
    }
    
    // 5. Generate specific denial reason
    if no_host_match {
        AllowlistResult::Denied(DenyReason::HostNotAllowed(parsed.host))
    } else if no_path_match {
        AllowlistResult::Denied(DenyReason::PathNotAllowed { host, path })
    } else {
        AllowlistResult::Denied(DenyReason::MethodNotAllowed { method, host })
    }
}

Host Matching

Exact Match

{"host": "api.openai.com"}
Matches:
  • https://api.openai.com/v1/chat
  • https://openai.com/v1/chat (different host)
  • https://api.openai.com.evil.com (suffix attack)

Wildcard Subdomain

{"host": "*.example.com"}
Matches:
  • https://api.example.com/data
  • https://staging.example.com/data
  • https://example.com/data (no subdomain)
  • https://api.example.com.evil.com (suffix attack blocked)

Case-Insensitive

{"host": "API.OpenAI.com"}
Matches:
  • https://api.openai.com
  • https://API.OPENAI.COM
  • https://Api.OpenAi.Com
All hosts normalized to lowercase before matching.

Path Prefix Matching

With Prefix

{
  "host": "api.github.com",
  "path_prefix": "/repos/"
}
Matches:
  • https://api.github.com/repos/owner/repo
  • https://api.github.com/repos/owner/repo/issues
  • https://api.github.com/users/username
  • https://api.github.com/user/repos (different prefix)

Without Prefix

{
  "host": "httpbin.org"
}
Matches:
  • https://httpbin.org/get
  • https://httpbin.org/post
  • https://httpbin.org/anything/goes/here
All paths allowed when path_prefix is null.

Method Filtering

Specific Methods

{
  "host": "api.example.com",
  "methods": ["GET", "POST"]
}
Allows:
  • ✓ GET requests
  • ✓ POST requests
  • ❌ PUT requests
  • ❌ DELETE requests

All Methods

{
  "host": "api.example.com",
  "methods": []
}
Allows:
  • ✓ GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS, etc.
Empty methods array means all methods allowed.

Security Edge Cases

Userinfo in URL (Blocked)

https://user:pass@api.openai.com/v1/chat
Blocked to prevent:
  • Credential leakage in URLs
  • Host confusion attacks (https://trusted@evil.com)
From src/tools/wasm/allowlist.rs:174-198:
fn parse_url(url: &str) -> Result<ParsedUrl> {
    let parsed = url::Url::parse(url)?;
    
    // Reject URLs with userinfo (user:pass@host)
    if !parsed.username().is_empty() || parsed.password().is_some() {
        return Err("URL contains userinfo (@) which is not allowed");
    }
    
    // ... rest of parsing
}

Path Normalization

All paths normalized before matching:
https://api.example.com/v1/../admin/users
  → Normalized: /admin/users
  → Matched against path_prefix: "/v1/"
  → Result: ❌ Denied (bypassed prefix)
Prevents path traversal bypasses.

IPv6 Addresses

https://[2001:db8::1]/api/data
Brackets stripped before matching:
  • [2001:db8::1]2001:db8::1

Rate Limiting

Per-tool request limits prevent abuse and runaway costs.

Rate Limit Configuration

From src/tools/wasm/capabilities.rs:
pub struct RateLimitConfig {
    /// Maximum requests per minute
    pub requests_per_minute: u32,
    
    /// Maximum requests per hour
    pub requests_per_hour: u32,
}

impl Default for RateLimitConfig {
    fn default() -> Self {
        Self {
            requests_per_minute: 60,
            requests_per_hour: 1000,
        }
    }
}
Capabilities file:
{
  "http": {
    "rate_limit": {
      "requests_per_minute": 30,
      "requests_per_hour": 500
    }
  }
}

Rate Limiter Implementation

Slidding window algorithm:
pub struct RateLimiter {
    minute_window: RateWindow,
    hour_window: RateWindow,
}

struct RateWindow {
    count: u32,
    window_start: Instant,
    window_duration: Duration,
}

impl RateLimiter {
    pub fn check_and_record(&mut self) -> RateLimitResult {
        let now = Instant::now();
        
        // Reset windows if expired
        if now - self.minute_window.window_start > Duration::from_secs(60) {
            self.minute_window.count = 0;
            self.minute_window.window_start = now;
        }
        
        // Check limits
        if self.minute_window.count >= self.config.requests_per_minute {
            return RateLimitResult::Denied(LimitType::PerMinute);
        }
        
        if self.hour_window.count >= self.config.requests_per_hour {
            return RateLimitResult::Denied(LimitType::PerHour);
        }
        
        // Increment counters
        self.minute_window.count += 1;
        self.hour_window.count += 1;
        
        RateLimitResult::Allowed
    }
}

Rate Limit Errors

pub enum RateLimitError {
    PerMinuteExceeded { limit: u32, window_reset: Instant },
    PerHourExceeded { limit: u32, window_reset: Instant },
}
WASM receives:
{
  "error": "RateLimitExceeded",
  "limit_type": "per_minute",
  "limit": 60,
  "reset_in_seconds": 42
}

Size Limits

Prevent memory exhaustion and exfiltration via large payloads.

Request Size Limits

pub struct HttpCapability {
    /// Maximum request body size (default: 1 MB)
    pub max_request_bytes: usize,
    
    /// Maximum response body size (default: 10 MB)
    pub max_response_bytes: usize,
}

impl Default for HttpCapability {
    fn default() -> Self {
        Self {
            max_request_bytes: 1024 * 1024,       // 1 MB
            max_response_bytes: 10 * 1024 * 1024, // 10 MB
            // ...
        }
    }
}
Validation:
if body.len() > http_capability.max_request_bytes {
    return Err(WasmError::RequestTooLarge {
        size: body.len(),
        limit: http_capability.max_request_bytes,
    });
}

Response Size Limits

let response = reqwest::get(url).await?;
let body_bytes = response.bytes().await?;

if body_bytes.len() > http_capability.max_response_bytes {
    return Err(WasmError::ResponseTooLarge {
        size: body_bytes.len(),
        limit: http_capability.max_response_bytes,
    });
}
Prevents:
  • Memory exhaustion: Large responses filling WASM memory
  • Cost overruns: Downloading massive files
  • Exfiltration: Uploading huge datasets

HTTPS Enforcement

Plaintext HTTP is blocked by default.
if self.require_https && parsed.scheme != "https" {
    return AllowlistResult::Denied(DenyReason::InsecureScheme("http"));
}
Attempts:
http://api.example.com/data  → ❌ Denied (insecure scheme)
https://api.example.com/data → ✓ Allowed (if in allowlist)
For local development/testing only:
let validator = AllowlistValidator::new(patterns)
    .allow_http();  // ⚠️ Use with extreme caution
Never disable HTTPS in production.

Timeout Controls

Prevent hanging requests.
pub struct HttpCapability {
    /// Request timeout (default: 30 seconds)
    pub timeout: Duration,
}

impl Default for HttpCapability {
    fn default() -> Self {
        Self {
            timeout: Duration::from_secs(30),
            // ...
        }
    }
}
Enforced via reqwest:
let client = reqwest::Client::builder()
    .timeout(http_capability.timeout)
    .build()?;
After timeout:
WasmError::HttpTimeout {
    url: "https://slow-api.example.com",
    timeout_secs: 30,
}

Complete Example: OpenAI Integration

Capabilities File

{
  "http": {
    "allowlist": [
      {
        "host": "api.openai.com",
        "path_prefix": "/v1/",
        "methods": ["POST"]
      }
    ],
    "credentials": [
      {
        "secret_name": "openai_api_key",
        "location": {"AuthorizationBearer": {}},
        "host_patterns": ["api.openai.com"]
      }
    ],
    "rate_limit": {
      "requests_per_minute": 20,
      "requests_per_hour": 500
    },
    "max_request_bytes": 524288,   // 512 KB
    "max_response_bytes": 1048576, // 1 MB
    "timeout_secs": 30
  },
  "secrets": {
    "allowed_names": ["openai_api_key"]
  }
}

Allowed Request

// WASM code
let response = http_request(
    "POST",
    "https://api.openai.com/v1/chat/completions",
    vec![("Content-Type".to_string(), "application/json".to_string())],
    br#"{"model":"gpt-4","messages":[...]}".to_vec()
)?;
Validation flow:
  1. ✓ Host api.openai.com in allowlist
  2. ✓ Path /v1/chat/completions matches prefix /v1/
  3. ✓ Method POST is allowed
  4. ✓ Scheme is https
  5. ✓ No secrets leaked in request
  6. ✓ Rate limit not exceeded
  7. ✓ Request body < 512 KB
  8. ✓ Credential injected: Authorization: Bearer sk-...
  9. ✓ HTTP request executed
  10. ✓ Response body < 1 MB
  11. ✓ No secrets in response
  12. ✓ Response returned to WASM

Denied Requests

Wrong Host

http_request("GET", "https://evil.com/steal", ...)
❌ Denied: HostNotAllowed("evil.com")

Wrong Path

http_request("GET", "https://api.openai.com/admin/users", ...)
❌ Denied: PathNotAllowed { host: "api.openai.com", path: "/admin/users" }

Wrong Method

http_request("DELETE", "https://api.openai.com/v1/models", ...)
❌ Denied: MethodNotAllowed { method: "DELETE", host: "api.openai.com" }

Insecure Scheme

http_request("GET", "http://api.openai.com/v1/models", ...)
❌ Denied: InsecureScheme("http")

Rate Limit Exceeded

for _ in 0..100 {
    http_request("POST", "https://api.openai.com/v1/chat/completions", ...)
}
❌ After 20 requests in 1 minute: RateLimitExceeded(PerMinute)

Request Too Large

let huge_body = vec![0u8; 10_000_000]; // 10 MB
http_request("POST", "https://api.openai.com/v1/...", ..., huge_body)
❌ Denied: RequestTooLarge { size: 10000000, limit: 524288 }

Monitoring and Logging

All network activity is logged:

Allowed Requests

tracing::info!(
    tool = "openai_tool",
    method = "POST",
    url = "https://api.openai.com/v1/chat/completions",
    "HTTP request allowed"
);

Denied Requests

tracing::warn!(
    tool = "suspicious_tool",
    url = "https://evil.com/exfil",
    reason = "host_not_allowed",
    "HTTP request denied"
);

Rate Limit Hits

tracing::warn!(
    tool = "chatty_tool",
    limit_type = "per_minute",
    limit = 60,
    "Rate limit exceeded"
);

Secret Leaks Blocked

tracing::error!(
    tool = "leaky_tool",
    pattern = "openai_api_key",
    location = "request_url",
    "Secret leak blocked in HTTP request"
);
Enable debug logging:
RUST_LOG=ironclaw::tools::wasm=debug ironclaw

Best Practices

For Tool Authors

  1. Request minimal access
// ❌ Too broad
{"host": "*.com"}

// ✓ Specific
{"host": "api.specific-service.com", "path_prefix": "/v1/"}
  1. Use specific methods
// ❌ All methods
{"methods": []}

// ✓ Only what you need
{"methods": ["GET", "POST"]}
  1. Set conservative limits
{
  "rate_limit": {
    "requests_per_minute": 10,  // Start low, increase if needed
    "requests_per_hour": 100
  }
}
  1. Handle rate limits gracefully
match http_request(...) {
    Err(WasmError::RateLimitExceeded { reset_in_seconds, .. }) => {
        return format!("Rate limit hit, retry in {} seconds", reset_in_seconds);
    }
}

For System Administrators

  1. Audit allowlists regularly
# Review all granted endpoints
find ~/.ironclaw/tools -name '*.capabilities.json' -exec jq .http.allowlist {} \;
  1. Monitor denied requests
# Watch for suspicious activity
RUST_LOG=warn ironclaw 2>&1 | grep "HTTP request denied"
  1. Set resource limits per tool
{
  "http": {
    "max_request_bytes": 102400,   // 100 KB (smaller than default)
    "max_response_bytes": 1048576, // 1 MB
    "timeout_secs": 10             // Shorter timeout
  }
}
  1. Use different limits for different tools
// High-volume tool
{"rate_limit": {"requests_per_minute": 120, "requests_per_hour": 5000}}

// Low-volume admin tool
{"rate_limit": {"requests_per_minute": 5, "requests_per_hour": 50}}

Network Security Checklist

  • Allowlist contains only necessary hosts
  • Path prefixes are as specific as possible
  • HTTP methods are restricted (not empty array)
  • HTTPS is enforced (don’t call .allow_http())
  • Rate limits are appropriate for use case
  • Request/response sizes are reasonable
  • Timeout is not too long (default 30s is good)
  • Credentials are only for allowed hosts
  • Logs are monitored for denied requests
  • Capabilities are reviewed before deploying tools

Source Code References

See Also

Build docs developers (and LLMs) love