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() = 项目根 |
三个缺陷叠加导致隔离失效。即使只修其中一个,另外两个仍可被利用。
在 backend/hermes-bridge/sandbox_patch.py 中,bridge 启动后第一次创建 Agent 实例前,对 hermes-agent 的三个核心函数打补丁。所有补丁都通过 _SESSION_CWD contextvar 读取当前线程的工作目录,实现线程隔离(bridge 每个 /run 在独立 daemon thread 中执行)。
_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") 都会被拦截。
_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(),从根本上避免基点漂移。
_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 原生行为install_sandbox_patches 返回 False 并 warning,bridge 启动不阻塞hermes_bridge.py 的 try-except 兜底,仅 warninghermes_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 |
README.mdbackend/README.md(逃逸到上层)backend/data/workflow-runs/{wid}/{rid}/README.md(正确隔离)PermissionError 在日志中