Skip to content

沙箱后端

源码版本v2026.7.20

职责

agent 要执行命令、跑代码,但不能让任意命令直接打到宿主机。Hermes 的终端工具支持 6 种执行后端,按安全/隔离/成本权衡选择:Local(本机直跑)、Docker(容器隔离)、SSH(远端机器)、Singularity(HPC 容器)、Modal(云沙箱,direct 或 managed)、Daytona(云开发环境)。容器硬化 + 命名空间隔离,危险命令还经审批回调拦截。

设计动机

为什么是 6 种而不是 1 种?agent 的执行场景跨度太大——开发时只想在本机跑得快,生产跑批需要容器隔离,HPC 用户必须在 Singularity 里跑(没法用 Docker),云端无人值守要 Modal 这种按秒计费的云沙箱。任何一种后端都覆盖不全。Hermes 把「执行」抽象成统一接口,后端按场景拼装,危险命令审批回调跨后端共用。Docker 后端要专门检测 bind mount 是否暴露宿主路径,因为容器隔离的安全前提是「容器看不到外面」,一旦 docker run -v /:/host 就破功;Modal 要区分 direct(用户自己的凭据)和 managed(网关托管),因为前者算用户账单、后者算 Nous 门户额度,权限和计费路径完全不同。这些检测逻辑不写在后端里,而是抽到 tool_backend_helpers.py,让选后端和判安全级别解耦。

关键文件

数据流

  1. agent 决定执行命令 → tool_call terminaltools/terminal_tool.py
  2. 按作业配置选后端(local/docker/modal/ssh/singularity/daytona):
  3. 危险命令经审批回调拦截——主循环用交互式审批;子代理默认 auto-deny(tools/delegate_tool.py:75),cron/批量可 opt-in auto-approve。
  4. 命令输出回填主循环;长任务可后台化(tools/daemon_pool.py)。

后端列表的声明很简单,terminal_tool.py 顶部就一句注释:「A terminal tool that executes commands in local, Docker, Modal, SSH, Singularity, and Daytona environments.」

Docker 后端检测 bind mount 是不是宿主路径,看 spec 字首:以 /~./../ 开头,或像 Windows 盘符 C:\ 那样第二个字符是冒号的,都算宿主路径,触发 has_host_access=True,后续权限走更严格的分支:

python
def _docker_volume_uses_host_path(volume_spec: str) -> bool:
    """Return True when a docker volume spec bind-mounts a host path."""
    if not isinstance(volume_spec, str):
        return False
    vol = volume_spec.strip()
    return bool(vol) and (
        vol.startswith(("/", "~", "./", "../")) or
        (len(vol) >= 3 and vol[1] == ":" and vol[2] in ("/", "\\"))
    )

Modal 解析有三档:direct(用户自己的 Modal 凭据)、managed(Nous 门户托管)、auto(默认,managed 不可用时 fallback 到 direct)。resolve_modal_backend_state 集中在一处,选不出后端就返回 Nonehas_direct_modal_credentials 同时看 MODAL_TOKEN_ID/MODAL_TOKEN_SECRET 环境变量和 ~/.modal.toml 文件,两个都缺才算 direct 不可用。managed_nous_tools_enabled 走 Nous Portal 账户查询,失败时 fail closed,不会因查询出错就误开通 managed 路径。

Singularity 是 HPC 场景的特殊存在——用户没法装 Docker,只有 Apptainer/Singularity。_find_singularity_executable 先找 apptainer 再找 singularity,两个都没有就报错指明安装地址。scratch 目录优先用 /scratch(HPC 集群标准挂载),没有就落到 get_sandbox_dir() / "singularity"。容器是 --containall --no-home 加 capability dropping——HPC 共享集群上必须这么跑,否则会越权看到其他用户家目录。

边界与失败

  • Docker bind mount 误判:检测函数认 /~./../ 开头为宿主路径,但 named volume(如 mydata:/data)会被放行。命名卷背后挂宿主路径(自定义 driver)时,_docker_has_host_access 看不到,会误判为低风险。
  • Modal managed 失败的回退:auto 模式下 managed 不可用 fallback 到 direct,但如果用户只配了 managed 没配 direct 凭据,selected_backendNone,后端选择失败到运行时才炸,创建终端那一刻可能看不到清晰错误。
  • Singularity SIF 缓存膨胀:SIF 镜像常几百 MB 到几 GB,scratch 目录没轮转策略会撑爆磁盘;HPC 节点的 /scratch 通常有集群级清理策略,两者对不齐时容易出现「缓存被集群清掉,agent 还以为 SIF 在」。
  • 远端后端网络抖动:SSH/Daytona 的命令执行经网络,连接断开后正在跑的子进程输出就丢了。输出回填假设单次同步返回,没有重连续传,长任务在远端后端上风险最大。
  • 超时与 OOM:local/docker 有 script_timeout,但远端后端的超时要靠远端进程自己 kill,本地调度器等不到响应就会卡住,或被 _run_job_script_with_claim_heartbeat 的租约 TTL 误判为死掉。

小结

沙箱层是「执行隔离 + 危险命令管控」:6 后端覆盖从本机到 HPC 到云;Docker 检测 bind mount 防越权;Modal 区分 direct/managed 凭据;危险命令统一走审批回调。子代理与 cron 走 auto-deny/auto-approve 的非交互路径。

非官方社区学习站,内容以 MIT 许可的 NousResearch/hermes-agent 源码为依据。