# 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` — 路径白名单 ```python 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` — 相对路径锚定 ```python 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` — 阻断外层项目根注入 ```python 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()`: ```python 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` 在日志中