sandbox_patch.py 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111
  1. """
  2. Hermes Agent 沙箱补丁
  3. 强制将 hermes-agent 的所有文件操作限制在 _SESSION_CWD 指定的目录内,
  4. 防止 LLM 通过绝对路径或 ../ 路径遍历逃逸到工作目录之外。
  5. 设计原理:
  6. - _SESSION_CWD 是 hermes-agent 内部的 contextvar(线程隔离),
  7. hermes_bridge.py 在每次 /run 时通过 _SESSION_CWD.set(working_dir) 注入。
  8. - 本模块在 hermes-agent import 后 monkey-patch 三个关键函数,
  9. 使其在 _SESSION_CWD 已设置时强制路径边界检查。
  10. - 未设置 _SESSION_CWD 时(如 hermes-agent 独立运行),patch 函数透传原逻辑,
  11. 不影响 hermes-agent 原生行为。
  12. 三层防御:
  13. 1. tools.file_tools._resolve_path_for_task —— 解析后强制路径必须在 _SESSION_CWD 内
  14. 2. tools.file_tools._resolve_base_dir —— 相对路径基点直接锚定到 _SESSION_CWD
  15. 3. agent.coding_context._git_root —— 隔离目录内不向上递归找 .git
  16. (否则 system_prompt 会注入外层项目根,主动误导 LLM 写到隔离目录之外)
  17. """
  18. import logging
  19. from pathlib import Path
  20. from typing import Optional, Union
  21. logger = logging.getLogger("hermes-bridge.sandbox")
  22. _installed = False
  23. def install_sandbox_patches() -> bool:
  24. """安装 hermes-agent 沙箱补丁。返回是否成功安装。
  25. 幂等:重复调用只安装一次。
  26. """
  27. global _installed
  28. if _installed:
  29. return True
  30. try:
  31. from agent.runtime_cwd import _SESSION_CWD
  32. from tools import file_tools
  33. from agent import coding_context
  34. except ImportError as e:
  35. logger.warning("沙箱补丁安装失败(hermes-agent 未就绪): %s", e)
  36. return False
  37. # ---- 保存原函数 ----
  38. _orig_resolve_path = file_tools._resolve_path_for_task
  39. _orig_resolve_base = file_tools._resolve_base_dir
  40. _orig_git_root = coding_context._git_root
  41. def _session_cwd_or_none() -> Optional[Path]:
  42. """读取当前线程的 _SESSION_CWD,未设置或非法时返回 None。"""
  43. try:
  44. raw = _SESSION_CWD.get()
  45. except LookupError:
  46. return None
  47. # hermes-agent 用 _UNSET 哨兵作为 default,类型不是 str/Path 时视为未设置
  48. if not isinstance(raw, (str, Path)):
  49. return None
  50. try:
  51. p = Path(raw).expanduser().resolve()
  52. except (OSError, ValueError):
  53. return None
  54. if not p.is_absolute():
  55. return None
  56. return p
  57. def _sandbox_resolve_path(filepath: str, task_id: str = "default") -> Path:
  58. """路径解析后强制必须在 _SESSION_CWD 之内。"""
  59. resolved = _orig_resolve_path(filepath, task_id)
  60. sandbox = _session_cwd_or_none()
  61. if sandbox is None:
  62. return resolved # 未启用沙箱,透传原逻辑
  63. # 强制 resolved 必须等于 sandbox 或位于 sandbox 之内
  64. try:
  65. resolved.relative_to(sandbox)
  66. except ValueError:
  67. raise PermissionError(
  68. f"沙箱拒绝访问:路径 {filepath!r} 解析为 {resolved},"
  69. f"超出了当前运行目录 {sandbox}。"
  70. )
  71. return resolved
  72. def _sandbox_resolve_base(task_id: str = "default") -> Path:
  73. """相对路径基点直接锚定到 _SESSION_CWD(不再依赖 TERMINAL_CWD / os.getcwd)。"""
  74. sandbox = _session_cwd_or_none()
  75. if sandbox is not None:
  76. return sandbox
  77. return _orig_resolve_base(task_id)
  78. def _sandbox_git_root(cwd: Path) -> Optional[Path]:
  79. """隔离模式下不识别 .git(防止 system_prompt 注入外层项目根)。"""
  80. if _session_cwd_or_none() is not None:
  81. return None
  82. return _orig_git_root(cwd)
  83. # ---- 替换 ----
  84. file_tools._resolve_path_for_task = _sandbox_resolve_path
  85. file_tools._resolve_base_dir = _sandbox_resolve_base
  86. coding_context._git_root = _sandbox_git_root
  87. _installed = True
  88. logger.info(
  89. "已安装 hermes-agent 沙箱补丁:"
  90. "_resolve_path_for_task / _resolve_base_dir / _git_root "
  91. "已强制锚定到 _SESSION_CWD"
  92. )
  93. return True