| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111 |
- """
- 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
|