Skip to main content

Sandbox Isolation

OpenFang runs untrusted code in two isolated sandbox environments:
  1. WASM Sandbox — For WebAssembly modules (user skills, WASM tools)
  2. Subprocess Sandbox — For Python/Node.js script execution
Both sandboxes enforce strict resource limits, capability checks, and isolation boundaries.

WASM Dual Metering

The WASM sandbox uses Wasmtime with two independent metering systems running in parallel:

1. Fuel Metering (Instruction Count)

Tracks the number of WASM instructions executed. Each instruction consumes “fuel.”
store.set_fuel(10_000_000)?;  // 10M instructions
When fuel reaches zero:
  • Execution immediately halts
  • Returns OpenFangError::ResourceExhausted

2. Epoch Interruption (Wall-Clock Time)

A watchdog thread increments an epoch counter every 100ms. The WASM store checks the epoch at regular intervals.
let engine = Engine::new(&config)?;
engine.set_epoch_deadline(10);  // 10 epochs = ~1 second

// Watchdog thread
std::thread::spawn(move || {
    loop {
        std::thread::sleep(Duration::from_millis(100));
        engine.increment_epoch();
    }
});
When epoch deadline is reached:
  • Execution immediately halts
  • Returns OpenFangError::Timeout

Why Dual Metering?

Fuel metering alone is insufficient:
  • A WASM module can execute a single slow instruction (e.g., sleep(), network I/O) that consumes little fuel but blocks indefinitely.
Epoch interruption alone is insufficient:
  • A CPU-bound tight loop might complete in wall-clock time but consume unbounded CPU.
Together, they cover all runaway scenarios:
  • CPU-bound loops → Fuel limit triggers
  • I/O-bound hangs → Epoch limit triggers
  • Mixed workloads → Whichever limit is hit first

WASM Host Functions

WASM modules cannot directly access the file system, network, or memory. They must call host functions provided by OpenFang. Each host function enforces capability checks before granting access.

Example: host_file_read

fn host_file_read(
    mut caller: Caller<'_, WasmState>,
    path_ptr: i32,
    path_len: i32,
) -> Result<i32, Trap> {
    // 1. Extract path from WASM memory
    let memory = caller.get_export("memory").unwrap().into_memory().unwrap();
    let path = read_string_from_wasm(&memory, &caller, path_ptr, path_len)?;

    // 2. Check capability
    let agent_id = caller.data().agent_id;
    let cap = Capability::ToolInvoke("file_read".to_string());
    if !caller.data().capability_manager.check(&agent_id, &cap) {
        return Err(Trap::new("Permission denied: file_read"));
    }

    // 3. Validate path (prevent traversal)
    let safe_path = safe_resolve_path(&path)
        .map_err(|e| Trap::new(format!("Invalid path: {}", e)))?;

    // 4. Read file
    let content = std::fs::read_to_string(safe_path)
        .map_err(|e| Trap::new(format!("Read failed: {}", e)))?;

    // 5. Write result to WASM memory
    write_string_to_wasm(&memory, &mut caller, &content)
}

Available Host Functions

FunctionCapability RequiredDescription
host_file_readToolInvoke("file_read")Read file contents
host_file_writeToolInvoke("file_write")Write file contents
host_file_listToolInvoke("file_list")List directory
host_net_fetchNetConnect(host)HTTP fetch
host_memory_storeMemoryWrite(key)Store memory KV
host_memory_recallMemoryRead(key)Recall memory KV
host_logNoneWrite to agent log

Path Traversal Prevention

All file operations go through safe_resolve_path() or safe_resolve_parent() before execution.

Implementation

pub fn safe_resolve_path(path: &str) -> Result<PathBuf, OpenFangError> {
    // 1. Canonicalize the path (resolves symlinks, ../, etc.)
    let canonical = std::fs::canonicalize(path)
        .map_err(|e| OpenFangError::PathTraversal {
            attempted_path: path.to_string(),
            reason: format!("Canonicalization failed: {}", e),
        })?;

    // 2. Ensure path is within workspace
    let workspace = std::env::current_dir()?;
    if !canonical.starts_with(&workspace) {
        return Err(OpenFangError::PathTraversal {
            attempted_path: path.to_string(),
            reason: "Path escapes workspace".to_string(),
        });
    }

    Ok(canonical)
}

What It Blocks

AttackInputResult
Parent directory escape../../../etc/passwdPathTraversal error
Symlink escapelink_to_root/etc/passwdPathTraversal error
Absolute path escape/etc/passwdPathTraversal error
Encoded traversal..%2F..%2Fetc%2FpasswdPathTraversal error

Enforcement Order

CRITICAL: Capability check runs BEFORE path resolution.
// Correct order:
1. Check capability
2. Validate and resolve path
3. Execute operation

// Why this order?
// - If capability is denied, we don't want to leak whether the path exists
// - Path validation errors can reveal information about the file system

SSRF Protection

The host_net_fetch function blocks requests to private IP ranges and cloud metadata endpoints.

Implementation

fn is_ssrf_target(url: &str) -> Result<bool, OpenFangError> {
    let parsed = Url::parse(url)?;
    let host = parsed.host_str().ok_or(/* ... */)?;

    // 1. Resolve DNS
    let addrs: Vec<IpAddr> = (host, 0)
        .to_socket_addrs()?
        .map(|s| s.ip())
        .collect();

    // 2. Check each resolved IP
    for addr in addrs {
        if is_private_ip(&addr) {
            return Ok(true);
        }
    }

    // 3. Check for cloud metadata endpoints
    if is_cloud_metadata(host) {
        return Ok(true);
    }

    Ok(false)
}

fn is_private_ip(ip: &IpAddr) -> bool {
    match ip {
        IpAddr::V4(ipv4) => {
            ipv4.is_private()         // 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16
                || ipv4.is_loopback()     // 127.0.0.0/8
                || ipv4.is_link_local()   // 169.254.0.0/16
        }
        IpAddr::V6(ipv6) => {
            ipv6.is_loopback()        // ::1
                || ipv6.is_unique_local() // fc00::/7
        }
    }
}

fn is_cloud_metadata(host: &str) -> bool {
    matches!(host,
        "169.254.169.254"        // AWS, GCP, Azure metadata
        | "metadata.google.internal"  // GCP
        | "100.100.100.200"       // Alibaba Cloud
    )
}

Blocked Targets

TargetReason
http://10.0.0.1/adminPrivate IP (internal network)
http://192.168.1.1/configPrivate IP (LAN)
http://169.254.169.254/latest/meta-data/AWS metadata endpoint
http://metadata.google.internal/GCP metadata endpoint
http://localhost:6379/Loopback (Redis, etc.)
http://[::1]:8080/IPv6 loopback

DNS Rebinding Defense

DNS resolution happens at request time, not at capability check time. This prevents DNS rebinding attacks:
  1. Attacker registers evil.com pointing to a public IP
  2. Agent checks capability for evil.com → Allowed
  3. DNS TTL expires, attacker changes evil.com to 169.254.169.254
  4. Agent makes request → Blocked by is_ssrf_target()

Subprocess Sandbox

Python and Node.js skills run in isolated subprocesses with environment clearing.

Environment Isolation

All subprocess invocations use env_clear() followed by selective variable injection:
let mut cmd = Command::new("python3");
cmd.arg(&script_path);

// 1. Clear all environment variables
cmd.env_clear();

// 2. Selectively inject safe variables
cmd.env("PATH", "/usr/bin:/bin");
cmd.env("HOME", home_dir);
cmd.env("LANG", "en_US.UTF-8");

// 3. Inject skill-specific config (if declared in manifest)
if let Some(env_vars) = skill.env_vars {
    for (key, value) in env_vars {
        cmd.env(key, value);
    }
}

// 4. Execute with timeout
tokio::time::timeout(
    Duration::from_secs(60),
    cmd.output()
).await?

What’s Blocked

VariableWhy It’s Cleared
AWS_ACCESS_KEY_IDCloud credentials
GOOGLE_APPLICATION_CREDENTIALSGCP credentials
AZURE_CLIENT_SECRETAzure credentials
ANTHROPIC_API_KEYLLM API keys
OPENAI_API_KEYLLM API keys
DATABASE_URLDatabase connection strings
REDIS_URLRedis connection strings

Restricted PATH

The PATH environment variable is set to a minimal set of directories:
PATH=/usr/bin:/bin
This prevents skills from executing arbitrary binaries in user-writable directories.

Secret Zeroization

All API keys and secrets use Zeroizing<String> from the zeroize crate.

How It Works

use zeroize::Zeroizing;

pub struct AnthropicDriver {
    api_key: Zeroizing<String>,  // NOT String
    // ...
}

impl Drop for AnthropicDriver {
    fn drop(&mut self) {
        // Zeroizing<String> automatically overwrites memory with zeros
        // No explicit action needed
    }
}

When Zeroization Happens

  1. Driver drop: When an LLM driver is dropped (agent killed, kernel shutdown)
  2. Config reload: When config is reloaded with new API keys
  3. Key rotation: When API keys are rotated

Debug Redaction

All config structs implement Debug with secret redaction:
impl Debug for AnthropicDriver {
    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
        f.debug_struct("AnthropicDriver")
            .field("api_key", &"<redacted>")
            .finish()
    }
}
This prevents secrets from appearing in logs or error messages.

Resource Limits

Both sandboxes enforce strict resource limits:

WASM Limits

ResourceLimitEnforced By
Instructions10M fuelFuel metering
Wall-clock time1 secondEpoch interruption
Memory64 MBWasmtime config
Stack depth1024 framesWasmtime config

Subprocess Limits

ResourceLimitEnforced By
Wall-clock time60 secondstokio::time::timeout
Output size50,000 charstruncate_tool_result()
Concurrent processes10BackgroundExecutor semaphore

Sandbox Configuration

Sandbox limits can be configured in config.toml:
[sandbox]
# WASM limits
wasm_fuel_limit = 10_000_000       # Instructions
wasm_epoch_deadline = 10           # ~1 second
wasm_memory_limit_mb = 64          # MB
wasm_stack_limit = 1024            # Frames

# Subprocess limits
subprocess_timeout_secs = 60       # Seconds
subprocess_output_limit = 50_000   # Characters
subprocess_max_concurrent = 10     # Processes

Testing Sandbox Isolation

Test 1: WASM Infinite Loop

// skill.wasm
#[no_mangle]
pub extern "C" fn run() {
    loop {}
}
Expected Result:
  • Fuel limit triggers after ~10M iterations
  • Returns OpenFangError::ResourceExhausted
  • Agent remains responsive

Test 2: WASM Sleep Attack

// skill.wasm
#[no_mangle]
pub extern "C" fn run() {
    unsafe { libc::sleep(999999); }
}
Expected Result:
  • Epoch limit triggers after ~1 second
  • Returns OpenFangError::Timeout
  • Watchdog thread kills the WASM instance

Test 3: Path Traversal

# Python skill
import sys
with open(sys.argv[1], 'r') as f:
    print(f.read())
# Input
../../../etc/passwd
Expected Result:
  • safe_resolve_path() detects traversal
  • Returns OpenFangError::PathTraversal
  • File is never read

Test 4: SSRF Attack

# Python skill
import urllib.request
with urllib.request.urlopen(sys.argv[1]) as r:
    print(r.read())
# Input
http://169.254.169.254/latest/meta-data/iam/security-credentials/
Expected Result:
  • is_ssrf_target() detects metadata endpoint
  • Returns OpenFangError::SsrfBlocked
  • Request is never made

Test 5: Environment Variable Leak

# Python skill
import os
print(os.environ.get('ANTHROPIC_API_KEY', 'not found'))
Expected Result:
  • Output: not found
  • env_clear() removed all secrets from environment

Monitoring Sandbox Events

All sandbox violations are logged to the audit trail:
curl http://localhost:4200/api/audit?event_type=sandbox
Response:
[
  {
    "timestamp": "2026-03-07T10:15:30Z",
    "agent_id": "uuid-here",
    "event": "SandboxViolation",
    "violation_type": "ResourceExhausted",
    "details": "WASM fuel limit exceeded (10M instructions)",
    "hash": "sha256_hash",
    "prev_hash": "previous_hash"
  }
]

Best Practices

1. Always Test Skills in Sandbox First

Never deploy untrusted skills directly to production:
# Test in isolated agent first
openfang agent spawn test-agent --skill untrusted-skill
openfang agent chat test-agent
> "Run the skill"

# Monitor for violations
curl http://localhost:4200/api/audit?agent_id={test-agent-id}&event_type=sandbox

2. Set Conservative Limits

Start with tight limits, expand only if needed:
[sandbox]
wasm_fuel_limit = 1_000_000  # Start conservative
subprocess_timeout_secs = 10  # Start conservative

3. Monitor Resource Usage

Track which skills consume the most resources:
curl http://localhost:4200/api/skills/usage

4. Review Skill Manifests

Before installing a skill, review its requested capabilities:
openfang skill inspect untrusted-skill
Red flags:
  • network = ["*"]
  • shell = ["*"]
  • memory_read = ["*"]

Overview

All 16 security systems

Capabilities

Capability-based access control

Audit Trail

Merkle hash-chain logging

Architecture

How sandboxing integrates across subsystems

Build docs developers (and LLMs) love