""" Hermes Agent 沙箱补丁 强制将 hermes-agent 的所有文件操作限制在 _SESSION_CWD 指定的目录内, 防止 LLM 通过绝对路径或 ../ 路径遍历逃逸到工作目录之外。 设计原理: - _SESSION_CWD 是 hermes-agent 内部的 contextvar(线程隔离), hermes_bridge.py 在每次 /run 时通过 _SESSION_CWD.set(working_dir) 注入。 - 本模块在 hermes-agent import 后 monkey-patch 三个关键函数, 使其在 _SESSION_CWD 已设置时强制路径边界检查。 - 未设置 _SESSION_CWD 时(如 hermes-agent 独立运行),patch 函数透传原逻辑, 不影响 hermes-agent 原生行为。 三层防御: 1. tools.file_tools._resolve_path_for_task —— 解析后强制路径必须在 _SESSION_CWD 内 2. tools.file_tools._resolve_base_dir —— 相对路径基点直接锚定到 _SESSION_CWD 3. agent.coding_context._git_root —— 隔离目录内不向上递归找 .git (否则 system_prompt 会注入外层项目根,主动误导 LLM 写到隔离目录之外) """ import logging from pathlib import Path from typing import Optional, Union logger = logging.getLogger("hermes-bridge.sandbox") _installed = False def install_sandbox_patches() -> bool: """安装 hermes-agent 沙箱补丁。返回是否成功安装。 幂等:重复调用只安装一次。 """ global _installed if _installed: return True try: from agent.runtime_cwd import _SESSION_CWD from tools import file_tools from agent import coding_context except ImportError as e: logger.warning("沙箱补丁安装失败(hermes-agent 未就绪): %s", e) return False # ---- 保存原函数 ---- _orig_resolve_path = file_tools._resolve_path_for_task _orig_resolve_base = file_tools._resolve_base_dir _orig_git_root = coding_context._git_root def _session_cwd_or_none() -> Optional[Path]: """读取当前线程的 _SESSION_CWD,未设置或非法时返回 None。""" try: raw = _SESSION_CWD.get() except LookupError: return None # hermes-agent 用 _UNSET 哨兵作为 default,类型不是 str/Path 时视为未设置 if not isinstance(raw, (str, Path)): return None try: p = Path(raw).expanduser().resolve() except (OSError, ValueError): return None if not p.is_absolute(): return None return p def _sandbox_resolve_path(filepath: str, task_id: str = "default") -> Path: """路径解析后强制必须在 _SESSION_CWD 之内。""" resolved = _orig_resolve_path(filepath, task_id) sandbox = _session_cwd_or_none() if sandbox is None: return resolved # 未启用沙箱,透传原逻辑 # 强制 resolved 必须等于 sandbox 或位于 sandbox 之内 try: resolved.relative_to(sandbox) except ValueError: raise PermissionError( f"沙箱拒绝访问:路径 {filepath!r} 解析为 {resolved}," f"超出了当前运行目录 {sandbox}。" ) return resolved def _sandbox_resolve_base(task_id: str = "default") -> Path: """相对路径基点直接锚定到 _SESSION_CWD(不再依赖 TERMINAL_CWD / os.getcwd)。""" sandbox = _session_cwd_or_none() if sandbox is not None: return sandbox return _orig_resolve_base(task_id) def _sandbox_git_root(cwd: Path) -> Optional[Path]: """隔离模式下不识别 .git(防止 system_prompt 注入外层项目根)。""" if _session_cwd_or_none() is not None: return None return _orig_git_root(cwd) # ---- 替换 ---- file_tools._resolve_path_for_task = _sandbox_resolve_path file_tools._resolve_base_dir = _sandbox_resolve_base coding_context._git_root = _sandbox_git_root _installed = True logger.info( "已安装 hermes-agent 沙箱补丁:" "_resolve_path_for_task / _resolve_base_dir / _git_root " "已强制锚定到 _SESSION_CWD" ) return True