执行环境后端:从本地到云端的六种选择
本章核心源码:
tools/terminal_tool.py(1627 行)、tools/environments/base.py(113 行)、tools/environments/local.py、tools/environments/docker.py、tools/environments/ssh.py、tools/environments/daytona.py、tools/environments/modal.py、tools/environments/singularity.py
定位:本章拆解 Hermes 的终端执行后端——agent 的
terminal工具如何通过BaseEnvironmentABC 在本地、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
BaseEnvironment(tools/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 后端有两个变体:
- ModalEnvironment(
modal.py):直接使用 Modal SDK,通过快照(snapshot)实现文件系统持久化 - ManagedModalEnvironment(
managed_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)自动检测 apptainer 或 singularity——两个名字都支持,因为 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 mount | 1-3 秒 | 团队 bot、CI |
| SSH | 机器 | 远端持久 | 首次 3s,后续 100ms | 服务器管理 |
| Daytona | 云沙箱 | stop/resume | 5-10 秒 | Serverless 隔离 |
| Modal | 云沙箱 | 快照 | 5-15 秒 | GPU 任务 |
| Singularity | 容器(无 root) | overlay | 1-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。
设计启示
六种后端的设计可以提炼出三个原则:
-
ABC 极简化:
BaseEnvironment只有execute()和cleanup()两个抽象方法。复杂的持久化、安全加固、连接管理等逻辑完全在子类中实现。这让新后端的接入成本极低——实现两个方法就能工作 -
持久化策略多样化:Local 天然持久、Docker 用 bind mount、SSH 用远端文件系统、Daytona 用 stop/resume、Modal 用快照、Singularity 用 overlay。每种策略都针对其后端的运行模型优化,而非强制统一
-
安全是分层的:环境变量隔离(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 这一类托管执行路径。