hermes-sandbox.md 4.4 KB

Hermes Agent 工作目录沙箱隔离

背景

Hermes Agent 节点(HermesAgentExecutor / HermesSmartActionExecutor)的工作目录由 WorkflowRunDirManager 在每次运行时分配,路径形如:

backend/data/workflow-runs/{workflowId}/{runId}/

早期实现仅通过 agent.runtime_cwd._SESSION_CWD contextvar 做软约束 —— LLM 仍可通过绝对路径terminal 工具轻易逃逸到上层目录(实测:要求 Hermes 在工作目录生成 README.md 时,它写到了整个 backend/ 目录)。

根因

缺陷 位置 影响
绝对路径直接放行 tools/file_tools.py:_resolve_path_for_task LLM 写 D:/... 任意路径可成功
_git_root 向上递归 agent/coding_context.py:_git_root system_prompt 注入外层 .git 根,主动误导 LLM
TERMINAL_CWD 未设置 HermesProcessManager.java 未注入环境变量 hermes-agent 退化到 os.getcwd() = 项目根

三个缺陷叠加导致隔离失效。即使只修其中一个,另外两个仍可被利用。

隔离方案:三层 monkey-patch

backend/hermes-bridge/sandbox_patch.py 中,bridge 启动后第一次创建 Agent 实例前,对 hermes-agent 的三个核心函数打补丁。所有补丁都通过 _SESSION_CWD contextvar 读取当前线程的工作目录,实现线程隔离(bridge 每个 /run 在独立 daemon thread 中执行)。

1. _resolve_path_for_task — 路径白名单

def _sandbox_resolve_path(filepath, task_id="default"):
    resolved = _orig_resolve_path(filepath, task_id)  # 先走原解析
    sandbox = _session_cwd_or_none()
    if sandbox is None:
        return resolved  # 未启用沙箱(hermes-agent 独立运行时)
    try:
        resolved.relative_to(sandbox)  # 必须在 sandbox 之内
    except ValueError:
        raise PermissionError(f"沙箱拒绝访问:路径 ... 超出了运行目录 {sandbox}")
    return resolved

效果:LLM 调用 write_file(filepath="/etc/passwd")write_file(filepath="../../README.md") 都会被拦截。

2. _resolve_base_dir — 相对路径锚定

def _sandbox_resolve_base(task_id="default"):
    sandbox = _session_cwd_or_none()
    if sandbox is not None:
        return sandbox  # 相对路径基点直接锚定到隔离目录
    return _orig_resolve_base(task_id)

效果:相对路径不再依赖 TERMINAL_CWD / os.getcwd(),从根本上避免基点漂移。

3. _git_root — 阻断外层项目根注入

def _sandbox_git_root(cwd):
    if _session_cwd_or_none() is not None:
        return None  # 隔离模式下不识别 .git
    return _orig_git_root(cwd)

效果:coding_context.build_coding_workspace_block 收到 None,不会把外层项目根注入到 system_prompt 的 "Workspace Root" 字段,从源头消除 LLM 误写。

降级策略

  • _SESSION_CWD 未设置(hermes-agent 独立 CLI 运行):所有 patch 函数透传原逻辑,不影响 hermes-agent 原生行为
  • import 失败(hermes-agent 未就绪):install_sandbox_patches 返回 False 并 warning,bridge 启动不阻塞
  • patch 内部异常:被 hermes_bridge.py 的 try-except 兜底,仅 warning

加载时机

hermes_bridge.py_get_or_create_agent() 函数在 from run_agent import AIAgent 之前调用 install_sandbox_patches()

try:
    import sandbox_patch
    sandbox_patch.install_sandbox_patches()
except Exception as e:
    logger.warning("沙箱补丁加载异常(继续但不保证隔离): %s", e)

try:
    from run_agent import AIAgent
    ...

install_sandbox_patches() 是幂等的(用 _installed 全局 flag 保证只生效一次)。

已知限制

风险 说明 缓解策略
terminal 工具执行 shell LLM 可通过 cd ../..echo > xxx 绕过 业务侧通过 Skill 定义约束;如需硬隔离,可考虑禁用 terminal 工具或后续加 shell-level hook
os.chdir() 仍是进程全局 bridge 多线程并发时不安全 本方案不使用 os.chdir,完全依赖 contextvar + monkey-patch

验证

  1. Hermes Agent 节点配置:让 LLM 在工作目录创建 README.md
  2. 检查生成位置:
    • 修复前:backend/README.md(逃逸到上层)
    • 修复后:backend/data/workflow-runs/{wid}/{rid}/README.md(正确隔离)
  3. 尝试让 LLM 用绝对路径写文件,应看到 PermissionError 在日志中