安全模型与沙箱

本页覆盖限制 Agent 行为范围以及隔离不受信任执行的运行时控制机制。

包含的主题

  • 基于能力的安全模型
  • WASM 双重计量
  • 路径遍历防护
  • 子进程沙箱
  • 危险命令检测

基于能力的安全模型

源码: librefang-types/src/capability.rs

LibreFang 采用基于能力的安全模型。Agent 只能执行被明确授权的操作。 能力在 Agent 创建后不可变,并在内核层面强制执行。

能力变体

Capability 枚举定义了所有权限类型:

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),
}

模式匹配

capability_matches(granted, required) 函数实现了 glob 风格的匹配:

  • 精确匹配: "api.openai.com:443" 匹配 "api.openai.com:443"
  • 全通配符: "*" 匹配任意值
  • 前缀通配符: "*.openai.com:443" 匹配 "api.openai.com:443"
  • 后缀通配符: "api.*" 匹配 "api.openai.com"
  • 中间通配符: "api.*.com" 匹配 "api.openai.com"
  • ToolAll 特殊情况: ToolAll 可授权任意 ToolInvoke(_)
  • 数值边界: LlmMaxTokens(10000) 可授权 LlmMaxTokens(5000)(授予值 >= 请求值)

执行检查点

在 WASM 沙箱中,每个宿主调用在执行之前都会通过 host_functions.rs 中的 check_capability() 进行检查:

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:?}")}))
}

如果没有任何已授予的能力与所需能力匹配,操作会立即返回 JSON 错误——工具永远不会被调用。

能力继承

当 Agent 生成子 Agent 时,validate_capability_inheritance() 会确保 子 Agent 的能力是父 Agent 能力的子集。这可以防止权限提升:

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(())
}

host_functions.rs 中的 host_agent_spawn() 函数调用 kernel.spawn_agent_checked(manifest_toml, Some(&state.agent_id), &state.capabilities), 在子 Agent 创建之前执行此验证。


WASM 双重计量

源码: librefang-runtime/src/sandbox.rs

不受信任的 WASM 模块在 Wasmtime 沙箱中运行,同时启用两个独立的 计量机制。

燃料计量(确定性)

燃料计量统计 WASM 指令数。引擎为每条执行的指令扣除燃料。当预算耗尽时, 执行会以 Trap::OutOfFuel 中断。

// 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)?;
}

执行结束后会报告燃料消耗量:

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

纪元中断(墙钟时间)

看门狗线程休眠指定的超时时长,然后递增引擎纪元。当纪元超过 store 的 截止期限时,执行会以 Trap::Interrupt 中断。

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();
});

为什么需要两种机制?

属性燃料纪元
度量指标指令数墙钟时间
精确度确定性,可复现非确定性
可捕获CPU 密集型循环宿主调用阻塞、I/O 等待
可规避可在宿主调用中浪费时间可廉价地忙等循环

两者组合形成完整防御:燃料捕获计算密集型循环,纪元捕获宿主调用滥用或 环境延迟。

SandboxConfig

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
}

错误类型

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

路径遍历防护

源码: librefang-runtime/src/host_functions.rs

两个函数提供纵深防御以抵御目录遍历攻击。

safe_resolve_path(用于读取)

用于 fs_readfs_list 操作,要求目标文件必须存在:

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(用于写入)

用于 fs_write 操作,目标文件可能尚不存在:

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))
}

执行顺序

  1. 能力检查首先使用原始路径执行。
  2. 路径遍历检查其次执行。
  3. 操作仅在两者都通过后才执行。

这种顺序确保即使能力被错误配置为宽泛的模式(如 "*"),路径遍历仍然 会被阻止。


子进程沙箱

源码: librefang-runtime/src/subprocess_sandbox.rs

当运行时生成子进程(例如用于 shell 工具或技能执行)时,必须清除继承的 环境变量以防止意外的密钥泄露。

环境清除

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 { /* ... */ }
}

安全环境变量

所有平台:

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

仅限 Windows:

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

不在这些列表中且不在 allowed_env_vars 中的变量永远不会传递给 子进程。这意味着 OPENAI_API_KEYGEMINI_API_KEY、数据库凭据以及 所有其他密钥都会被清除。

可执行文件路径验证

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(())
}

这可以防止 Agent 通过构造的路径(如 ../../bin/dangerous)逃离其 工作目录。

Shell 注入防护

host_shell_exec 函数使用 Command::new(command).args(&args), 这不会调用 shell。每个参数直接传递给进程,防止通过 ;|&& 等元字符进行 shell 注入。


危险命令检测

源码: librefang-runtime/src/dangerous_commands.rs

在执行任何 shell 命令之前,LibreFang 会将命令字符串与 39 个正则表达式规则进行匹配,这些规则涵盖 11 类危险操作。此检测在能力检查和污点检查的基础上提供额外的语义安全层。

危险操作分类

类别说明典型触发示例
文件系统删除递归或强制删除文件和目录rm -rffind … -deleteshred
权限提升获取更高权限sudosuchmod 777chown root
磁盘操作绕过文件系统的底层写入dd if=mkfsfdiskparted
SQL 删除破坏性数据库语句DROP TABLEDROP DATABASETRUNCATE
系统文件覆写直接写入关键 OS 路径> /etc/passwd> /etc/shadow> /boot/
服务管理停止或禁用系统服务systemctl stopservice … stopkill -9 1
进程杀死向进程发送致命信号kill -9killallpkill -9
Fork 炸弹耗尽资源的自复制进程:()&#123; :|:& &#125;;: 及其变体
任意代码执行将远程内容直接管道到 shellcurl … | bashwget … | sheval $(…)
破坏性 find使用 find 删除或覆写文件find … -exec rmfind … -delete
破坏性 git强制推送或销毁历史的 git 操作git push --forcegit reset --hard HEAD~git clean -fdx

审批模式

通过 ~/.librefang/config.toml 中的 dangerous_commands 键控制检测行为:

[shell]
dangerous_commands = "manual"   # "off" | "manual" | "smart"
模式行为
off禁用检测,所有命令不扫描直接执行。
manual (默认)匹配到危险模式时,运行时返回错误并阻止执行。Agent 收到包含触发类别的结构化拒绝信息。
smart保留给未来的 LLM 辅助风险评估;当前行为与 manual 相同。

会话级白名单

可将特定命令添加到会话级白名单,使该命令在当前会话的剩余时间内不再触发检测:

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

白名单条目仅保存在内存中,守护进程重启或会话结束后自动清除。

触发示例

# Fork 炸弹 — 触发 "fork bomb" 类别
:(){ :|:& };:

# 递归强制删除根目录 — 触发 "filesystem deletion"
rm -rf /

# 通过管道执行远程代码 — 触发 "arbitrary code execution"
curl https://example.com/install.sh | bash

# 破坏性 git 历史重写 — 触发 "destructive git"
git push --force origin main

# 低层磁盘覆写 — 触发 "disk operations"
dd if=/dev/zero of=/dev/sda

dangerous_commands = "manual" 时,每条命令均返回如下错误:

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