Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

执行环境后端:从本地到云端的六种选择

本章核心源码tools/terminal_tool.py(1627 行)、tools/environments/base.py(113 行)、tools/environments/local.pytools/environments/docker.pytools/environments/ssh.pytools/environments/daytona.pytools/environments/modal.pytools/environments/singularity.py

定位:本章拆解 Hermes 的终端执行后端——agent 的 terminal 工具如何通过 BaseEnvironment ABC 在本地、Docker、SSH、Daytona、Modal、Singularity 六种后端中执行命令,以及每种后端的安全模型和持久化策略。 前置依赖:第 6 章(工具系统)。适用场景:想理解 agent 如何执行命令,或准备配置隔离执行环境。

为什么需要多后端

Agent 执行 shell 命令是最强大的能力之一,也是最危险的。不同场景对执行环境有截然不同的需求:

场景需求推荐后端
个人开发最快速度、访问本地文件local
团队共享 bot隔离、防止互相干扰Docker
远程服务器管理在目标机器上执行SSH
无状态云执行按需启动、用完销毁Modal
GPU/HPC 工作负载利用集群资源Singularity
Serverless 持久沙箱文件系统跨会话保留Daytona

BaseEnvironment ABC 让 agent 的 terminal 工具完全不知道自己运行在哪种后端上——相同的 execute("ls -la") 调用在六种后端上语义一致。

后端架构

graph TB
    TT["terminal_tool.py<br/>terminal_tool()"]
    
    TT --> BE["BaseEnvironment ABC<br/>environments/base.py:26"]
    
    BE --> L["LocalEnvironment<br/>environments/local.py"]
    BE --> D["DockerEnvironment<br/>environments/docker.py"]
    BE --> S["SSHEnvironment<br/>environments/ssh.py"]
    BE --> DY["DaytonaEnvironment<br/>environments/daytona.py"]
    BE --> M["ModalEnvironment<br/>environments/modal.py"]
    BE --> SG["SingularityEnvironment<br/>environments/singularity.py"]
    
    L --> LS["本地 subprocess"]
    D --> DS["Docker container"]
    S --> SS["SSH ControlMaster"]
    DY --> DYS["Daytona SDK<br/>云沙箱"]
    M --> MS["Modal SDK<br/>云沙箱 + 快照"]
    SG --> SGS["Apptainer/Singularity<br/>容器 + overlay"]

BaseEnvironment ABC

BaseEnvironmenttools/environments/base.py:26)定义了所有后端必须实现的接口:

# tools/environments/base.py:26-59
class BaseEnvironment(ABC):
    """Common interface for all Hermes execution backends."""

    def __init__(self, cwd: str, timeout: int, env: dict = None):
        self.cwd = cwd
        self.timeout = timeout
        self.env = env or {}

    @abstractmethod
    def execute(self, command: str, cwd: str = "", *,
                timeout: int | None = None,
                stdin_data: str | None = None) -> dict:
        """Execute a command, return {"output": str, "returncode": int}."""
        ...

    @abstractmethod
    def cleanup(self):
        """Release backend resources (container, instance, connection)."""
        ...

接口极简——execute()cleanup() 两个方法。返回值是一个 dict,包含 output(stdout + stderr 合并)和 returncode

共享辅助方法

BaseEnvironment 提供了几个共享的辅助方法,消除跨后端的重复代码:

# tools/environments/base.py:64-76
def _prepare_command(self, command: str) -> tuple[str, str | None]:
    """Transform sudo commands if SUDO_PASSWORD is available."""
    from tools.terminal_tool import _transform_sudo_command
    return _transform_sudo_command(command)

def _build_run_kwargs(self, timeout: int | None,
                      stdin_data: str | None = None) -> dict:
    """Build common subprocess.run kwargs for non-interactive execution."""

_prepare_command() 处理 sudo 命令转换——当 SUDO_PASSWORD 可用时,将 sudo xxx 转换为管道密码输入的形式,让非交互式执行可以通过 sudo 认证。

execute_oneshot()base.py:94-105)是一个旁路方法:对于维护持久 shell 的后端(local、SSH),它绕过 shell 锁直接执行命令,支持和长时间运行的 execute() 并发使用。

六种后端详解

1. LocalEnvironment:零开销本地执行

# tools/environments/local.py:0
"""Local execution environment with interrupt support and non-blocking I/O."""

LocalEnvironment 直接在宿主机上通过 subprocess 执行命令。它是最快的后端(零容器/网络开销),但也是最"危险"的——agent 可以访问宿主机的所有文件和进程。

关键设计——环境变量隔离local.py:30-79):

# tools/environments/local.py:28-29
# Hermes-internal env vars that should NOT leak into terminal subprocesses.
_HERMES_PROVIDER_ENV_FORCE_PREFIX = "_HERMES_FORCE_"

_build_provider_env_blocklist()local.py:31-79)动态构建一个环境变量黑名单,防止 Hermes 的 API key、provider 配置等泄露到用户命令的子进程中。这个黑名单从 provider registry 自动派生——新增 provider 自动被覆盖,不需要手动维护。

LocalEnvironment 支持 PersistentShellMixin——通过一个长生命周期的 bash 进程维护跨命令的状态(cwd、环境变量、shell 变量)。

2. DockerEnvironment:安全加固的容器隔离

# tools/environments/docker.py:0-5
"""Docker execution environment for sandboxed command execution.

Security hardened (cap-drop ALL, no-new-privileges, PID limits),
configurable resource limits (CPU, memory, disk), and optional filesystem
persistence via bind mounts.
"""

DockerEnvironment 在 Docker 容器中执行命令。安全加固措施包括:

  • 能力丢弃--cap-drop ALL——容器没有任何 Linux capabilities
  • 禁止提权--security-opt no-new-privileges——容器内的进程不能通过 setuid 等机制获取新权限
  • PID 限制:防止 fork bomb
  • 资源限制:可配置 CPU、内存、磁盘

Docker 可执行文件不仅搜索 PATH,还检查常见安装位置(docker.py:29-33):

# tools/environments/docker.py:29-33
_DOCKER_SEARCH_PATHS = [
    "/usr/local/bin/docker",
    "/opt/homebrew/bin/docker",
    "/Applications/Docker.app/Contents/Resources/bin/docker",
]

这解决了 Docker Desktop 安装后 docker 不在 PATH 中的常见问题(尤其是 macOS)。

环境变量转发经过严格验证(docker.py:39-61):变量名必须匹配 ^[A-Za-z_][A-Za-z0-9_]*$,防止注入攻击。

3. SSHEnvironment:远程执行与连接持久化

# tools/environments/ssh.py:0
"""SSH remote execution environment with ControlMaster connection persistence."""

SSHEnvironment 通过 SSH 在远程机器上执行命令。关键特性:

  • ControlMaster 持久化ssh.py:64-79):首次连接建立后,后续命令通过 Unix socket 复用连接,避免每次执行都经历 TCP 握手 + SSH 认证
  • 安全隔离:agent 不能修改自身代码——执行发生在完全不同的机器上
  • 中断支持:前台命令可中断——本地 SSH 进程被 kill 后,还会通过 ControlMaster socket 向远端发送 kill 信号
# tools/environments/ssh.py:64-79
def _build_ssh_command(self, extra_args: list | None = None) -> list:
    cmd = ["ssh"]
    cmd.extend(["-o", f"ControlPath={self.control_socket}"])
    cmd.extend(["-o", "ControlMaster=auto"])
    cmd.extend(["-o", "ControlPersist=300"])
    cmd.extend(["-o", "BatchMode=yes"])
    cmd.extend(["-o", "StrictHostKeyChecking=accept-new"])
    cmd.extend(["-o", "ConnectTimeout=10"])

ControlPersist=300 让主连接在最后一个 SSH 会话关闭后保留 5 分钟。这意味着连续的工具调用不会反复建立新连接——第一次调用 3 秒,后续调用 100ms。

persistent=True 时,SSHEnvironment 也支持持久 shell——远端维护一个长生命周期的 bash 进程,通过文件 IPC(stdout/stderr/exit-code 写入临时文件,通过 ControlMaster 快速读取)实现状态保持。

4. DaytonaEnvironment:Serverless 持久沙箱

# tools/environments/daytona.py:0-6
"""Daytona cloud execution environment.

Uses the Daytona Python SDK to run commands in cloud sandboxes.
Supports persistent sandboxes: when enabled, sandboxes are stopped on cleanup
and resumed on next creation, preserving the filesystem across sessions.
"""

Daytona 提供了 serverless 沙箱,核心优势是文件系统持久化——沙箱在 cleanup 时被 stop(而非 delete),下次创建时 resume:

# tools/environments/daytona.py:74-99(简化)
if self._persistent:
    # 1. Try name-based lookup
    try:
        self._sandbox = self._daytona.get(sandbox_name)
        self._sandbox.start()
        logger.info("Daytona: resumed sandbox %s for task %s",
                    self._sandbox.id, task_id)
    except DaytonaError:
        self._sandbox = None
    
    # 2. Legacy fallback: find sandbox by label
    if self._sandbox is None:
        page = self._daytona.list(labels=labels, page=1, limit=1)
        if page.items:
            self._sandbox = page.items[0]
            self._sandbox.start()

沙箱通过 task_id 关联——同一个任务的多次执行恢复到上次的文件系统状态。资源限制支持 CPU、内存(单位 MB,内部转 GiB)和磁盘(上限 10GB)。

5. ModalEnvironment:快照式云沙箱

# tools/environments/modal.py:0-4
"""Modal cloud execution environment using the native Modal SDK directly.

Uses Sandbox.create() + Sandbox.exec() instead of the older runtime
wrapper, while preserving Hermes' persistent snapshot behavior across sessions.
"""

Modal 后端有两个变体:

  • ModalEnvironmentmodal.py):直接使用 Modal SDK,通过快照(snapshot)实现文件系统持久化
  • ManagedModalEnvironmentmanaged_modal.py):通过 tool-gateway HTTP API 使用 Modal,不需要本地 Modal 凭据

Modal 的快照持久化与 Daytona 的 stop/resume 不同——Modal 在沙箱空闲时销毁沙箱,但保存一个文件系统快照(modal_snapshots.json)。下次创建时从快照恢复:

# tools/environments/modal.py:24-69
_SNAPSHOT_STORE = get_hermes_home() / "modal_snapshots.json"

def _get_snapshot_restore_candidate(task_id: str) -> tuple[str | None, bool]:
    """Return a snapshot id and whether it came from the legacy key format."""
    snapshots = _load_snapshots()
    namespaced_key = _direct_snapshot_key(task_id)
    snapshot_id = snapshots.get(namespaced_key)
    if isinstance(snapshot_id, str) and snapshot_id:
        return snapshot_id, False
    # Legacy fallback
    legacy_snapshot_id = snapshots.get(task_id)
    if isinstance(legacy_snapshot_id, str) and legacy_snapshot_id:
        return legacy_snapshot_id, True
    return None, False

ManagedModalEnvironment(managed_modal.py:35)面向没有 Modal 账号的用户——通过 Nous 的 tool-gateway 代理调用 Modal,凭据由 gateway 管理。

6. SingularityEnvironment:HPC 容器

# tools/environments/singularity.py:0-5
"""Singularity/Apptainer persistent container environment.

Security-hardened with --containall, --no-home, capability dropping.
Supports configurable resource limits and optional filesystem persistence
via writable overlay directories that survive across sessions.
"""

Singularity(现更名为 Apptainer)是 HPC 集群上的主流容器方案。与 Docker 不同,Singularity 不需要 root 权限,适合共享集群环境。

安全加固:

  • --containall:完全隔离容器的文件系统、PID、IPC
  • --no-home:不挂载宿主机的 home 目录
  • Capability 丢弃

文件系统持久化通过 writable overlay 目录实现——overlay 存储在宿主机上的快照目录中(singularity_snapshots.json),跨会话保留。

_find_singularity_executable()singularity.py:27-41)自动检测 apptainersingularity——两个名字都支持,因为 Apptainer 是 Singularity 的社区分支。

安全模型对比

graph LR
    subgraph "无隔离"
        L["Local<br/>完全访问宿主机"]
    end
    
    subgraph "进程隔离"
        SS["SSH<br/>远程机器隔离"]
    end
    
    subgraph "容器隔离"
        D["Docker<br/>cap-drop ALL<br/>no-new-privileges"]
        SG["Singularity<br/>containall<br/>no-home"]
    end
    
    subgraph "云沙箱隔离"
        DY["Daytona<br/>按需沙箱"]
        M["Modal<br/>快照沙箱"]
    end
    
    L -.->|"最快<br/>最危险"| L
    SS -.->|"网络延迟<br/>机器隔离"| SS
    D -.->|"中等开销<br/>强隔离"| D
    SG -.->|"无需 root<br/>HPC 友好"| SG
    DY -.->|"Serverless<br/>持久文件系统"| DY
    M -.->|"Serverless<br/>快照恢复"| M
后端隔离级别持久化启动开销适用场景
Local天然持久0个人开发
Docker容器bind mount1-3 秒团队 bot、CI
SSH机器远端持久首次 3s,后续 100ms服务器管理
Daytona云沙箱stop/resume5-10 秒Serverless 隔离
Modal云沙箱快照5-15 秒GPU 任务
Singularity容器(无 root)overlay1-3 秒HPC 集群

terminal_tool.py:统一入口

terminal_tool.py(1627 行)是 agent 的 terminal 工具的实现。它根据 TERMINAL_ENV 环境变量(或 config.yaml 的 terminal.backend)选择后端:

# tools/terminal_tool.py:0-11
"""
Terminal Tool Module

Environment Selection (via TERMINAL_ENV environment variable):
- "local": Execute directly on the host machine (default, fastest)
- "docker": Execute in Docker containers (isolated, requires Docker)
- "modal": Execute in Modal cloud sandboxes
"""

除了后端选择,terminal_tool.py 还提供了两个跨后端的安全机制:

危险命令审批

_check_all_guards()terminal_tool.py:144-147)在命令执行前检查安全规则:

# tools/terminal_tool.py:144-147
def _check_all_guards(command: str, env_type: str) -> dict:
    """Delegate to consolidated guard (tirith + dangerous cmd) with CLI callback."""
    return _check_all_guards_impl(command, env_type,
                                  approval_callback=_approval_callback)

安全检查包括模式匹配(正则匹配危险命令模式)和 tirith 安全扫描器(基于策略引擎的更细粒度检查)。不安全的命令会弹出审批提示——在 CLI 中通过 prompt_toolkit UI 交互,在 Gateway 中通过 /approve/deny 命令。

Sudo 密码管理

# tools/terminal_tool.py:111-112
_cached_sudo_password: str = ""

Sudo 密码在 CLI 会话中缓存(_cached_sudo_password),避免每次 sudo 命令都需要用户输入。密码输入通过可注册的回调(_sudo_password_callback)路由到 CLI 的密码输入 UI 或 Gateway 的消息流。

中断支持

# tools/terminal_tool.py:54
from tools.interrupt import is_interrupted, _interrupt_event

全局中断事件让终端工具在命令执行期间也能响应用户中断。长时间运行的子进程在检测到中断时被 kill,避免用户等待 timeout。

设计启示

六种后端的设计可以提炼出三个原则:

  1. ABC 极简化BaseEnvironment 只有 execute()cleanup() 两个抽象方法。复杂的持久化、安全加固、连接管理等逻辑完全在子类中实现。这让新后端的接入成本极低——实现两个方法就能工作

  2. 持久化策略多样化:Local 天然持久、Docker 用 bind mount、SSH 用远端文件系统、Daytona 用 stop/resume、Modal 用快照、Singularity 用 overlay。每种策略都针对其后端的运行模型优化,而非强制统一

  3. 安全是分层的:环境变量隔离(local)、能力丢弃(Docker/Singularity)、机器隔离(SSH)、沙箱隔离(Daytona/Modal)提供不同级别的安全保障。危险命令审批和 sudo 管理作为跨后端的通用安全层叠加在上面


设计赌注回扣:本章是 Run Anywhere 赌注的直接体现。同一个 terminal 工具,通过配置切换就能在本地笔记本、Docker 容器、远程服务器、云沙箱和 HPC 集群上执行命令。六种后端覆盖了从 $5 VPS 到 GPU 集群的完整部署谱系,每种后端都有针对性的安全模型和持久化策略。


版本演化说明

本章核心分析基于 Hermes Agent v0.8.0(2026 年 4 月)。 执行环境抽象本身早在 v0.3.0 之前就已经存在;Daytona 后端和 persistent shell 也都在 v0.3.0 发布窗口附近进入主线。之后的几个 release 主要是在扩展后端种类、收紧运行时隔离,以及补上 managed tool gateway 这一类托管执行路径。