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:
- Explicitly granted via capabilities file
- Allowlisted to specific hosts and paths
- Rate limited to prevent abuse
- Size constrained for requests and responses
- 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)
Disable HTTPS Requirement (Not Recommended)
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:
- ✓ Host
api.openai.com in allowlist
- ✓ Path
/v1/chat/completions matches prefix /v1/
- ✓ Method
POST is allowed
- ✓ Scheme is
https
- ✓ No secrets leaked in request
- ✓ Rate limit not exceeded
- ✓ Request body < 512 KB
- ✓ Credential injected:
Authorization: Bearer sk-...
- ✓ HTTP request executed
- ✓ Response body < 1 MB
- ✓ No secrets in response
- ✓ 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
- Request minimal access
// ❌ Too broad
{"host": "*.com"}
// ✓ Specific
{"host": "api.specific-service.com", "path_prefix": "/v1/"}
- Use specific methods
// ❌ All methods
{"methods": []}
// ✓ Only what you need
{"methods": ["GET", "POST"]}
- Set conservative limits
{
"rate_limit": {
"requests_per_minute": 10, // Start low, increase if needed
"requests_per_hour": 100
}
}
- 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
- Audit allowlists regularly
# Review all granted endpoints
find ~/.ironclaw/tools -name '*.capabilities.json' -exec jq .http.allowlist {} \;
- Monitor denied requests
# Watch for suspicious activity
RUST_LOG=warn ironclaw 2>&1 | grep "HTTP request denied"
- 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
}
}
- 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
Source Code References
See Also