转载--Hermes Agent 12 | 沙箱与执行环境:六种终端后端的安全隔离

原文连接:Hermes Agent 12 | 沙箱与执行环境:六种终端后端的安全隔离

给危险一个可以犯错的房间,它就不再危险。


为什么需要六种后端

先回答一个直觉上的问题:一个 Agent 框架为什么要支持六种执行环境?

因为不同的部署场景有不同的约束:

场景 核心需求 适合的后端
本地开发 零延迟、直接访问文件系统 Local
安全隔离 破坏性命令不影响宿主机 Docker
远程开发 Agent 在本机,代码在服务器 SSH
云沙箱(有状态) 按需启停、休眠节省成本 Daytona
HPC 集群 Singularity 是唯一选项 Singularity
云 GPU 计算 按秒计费、快照持久化 Modal

一个 Agent runtime 同时服务 CLI 用户、Telegram bot、cron 调度------它不可能假设所有命令都在本机跑。


BaseEnvironment:统一抽象

六种后端共享一个基类------tools/environments/base.py:265BaseEnvironment

复制代码
class BaseEnvironment(ABC):
    """Common interface and unified execution flow for all Hermes backends.

    Subclasses implement _run_bash() and cleanup(). The base class
    provides execute() with session snapshot sourcing, CWD tracking,
    interrupt handling, and timeout enforcement.
    """

子类只需实现两个方法:

方法 职责
_run_bash(cmd_string, *, login, timeout, stdin_data) 在目标环境里启动一个 bash 进程
cleanup() 释放后端资源(停容器、关连接、删沙箱)

基类处理所有"不管在哪个环境里都一样"的逻辑:

Session Snapshot:环境一致性的保证

每个后端启动时(init_session()),基类会在目标环境里执行一段 shell 脚本,导出当前所有环境变量、函数和别名,存成一个 snapshot 文件(/tmp/hermes-snap-{session_id}.sh)。此后每次执行命令前,先 source 这个 snapshot------保证后续命令看到的环境与第一次一致。

为什么不用 bash -l 因为 login shell 每次都要走完 /etc/profile~/.bashrc~/.bash_profile 的完整链路,慢且不可控。Snapshot 是一次性导出、多次复用。

CWD 追踪:跨命令的目录记忆

用户执行 cd /tmp/myproject 后,下一条命令应该在 /tmp/myproject 里。但每次 _run_bash() 都是一个新 bash 进程------cd 的效果会丢失。

基类在每条命令末尾追加 pwd 输出,通过 marker(__HERMES_CWD_{session_id}__)从 stdout 中解析当前目录,存在 self.cwd 里,下次执行前先 cd 过去。

Local 后端 用临时文件读 CWD(更快),远端后端(SSH/Modal/Daytona)从 stdout marker 解析。


六种后端逐个拆

1. Local:零开销,但没有隔离

tools/environments/local.py------最简单的后端,每条命令 fork 一个 bash -c 子进程。

安全措施不在隔离,而在环境变量过滤 。Local 后端有一个 50+ 项的环境变量黑名单(local.py:19-107),过滤掉:

  • 所有 LLM 提供商密钥(ANTHROPIC_API_KEYOPENAI_API_KEY......)

  • 消息平台 Token(Slack、Discord、Telegram......)

  • 基础设施凭证(GITHUB_APP_PRIVATE_KEYMODAL_TOKEN_*......)

Agent 执行的命令看不到这些变量------防止 Agent 被诱导通过 echo $OPENAI_API_KEY 泄露密钥。

进程组隔离 :使用 os.setsid() 创建新会话,确保子进程不会继承 Agent 的进程组------Ctrl-C 杀 Agent 时不会误杀用户正在跑的后台进程。

适用场景:本地开发、信任环境、需要直接访问宿主文件系统的场景。

2. Docker:生产级隔离

tools/environments/docker.py------最常用的隔离后端。

安全参数docker.py:153-163,每个容器都会应用):

复制代码
_SECURITY_ARGS = [
    "--cap-drop", "ALL",                           # 移除所有 Linux 能力
    "--cap-add", "DAC_OVERRIDE",                   # root 可写宿主挂载目录
    "--cap-add", "CHOWN",                          # 包管理器需要改属主
    "--cap-add", "FOWNER",
    "--security-opt", "no-new-privileges",          # 阻止权限提升
    "--pids-limit", "256",                          # 防 fork bomb
    "--tmpfs", "/tmp:rw,nosuid,size=512m",
    "--tmpfs", "/var/tmp:rw,noexec,nosuid,size=256m",
    "--tmpfs", "/run:rw,noexec,nosuid,size=64m",
]

两种持久化模式

模式 存储 生命周期 用途
持久化 bind mount HERMES_HOME/sandboxes/docker/{task_id}/... 挂载目录跨 session 保留 长期开发项目
临时 tmpfs(/workspace:10g, /home:1g 容器删除即消失 一次性任务

这里要注意一个实现细节:持久化的核心不是"复用同一个容器",而是复用宿主机上的 bind mount 目录。 当前实现会把 /root 映射到 HERMES_HOME/sandboxes/docker/{task_id}/home;如果用户没有显式把 /workspace 另行挂载,还会再映射一个 .../workspace。下次创建环境时,Hermes 仍然会重新 docker run -d 一个新的随机容器,但挂载目录里的内容会被重新带进来。

所以真正持久化的是这些挂载目录里的文件,而不是"整个容器原样续跑"。临时模式则一切写入 tmpfs,速度快但不持久。

资源限制(可配置):

复制代码
terminal:
  backend: docker
  container_cpu: 1           # CPU 核数
  container_memory: 5120     # MB(默认 5GB)
  container_disk: 51200      # MB(默认 50GB,需要 overlay2+XFS+pquota)
  container_persistent: true

环境变量转发 :Docker 后端有一个 docker_forward_env 配置------显式声明要传入容器的环境变量。只在 init_session 时通过 -e KEY=VALUE 注入,后续命令通过 snapshot 获取,不再每次传递。

容器内跳过命令审批 :因为容器本身就是安全边界,Docker 后端执行命令时不触发第 2 层的危险命令检查。rm -rf / 在容器里只影响容器自己。

3. SSH:Agent 在本地,代码在远端

tools/environments/ssh.py------通过 SSH 在远程服务器上执行命令。

ControlMaster 连接复用ssh.py:68-82):

复制代码
- o ControlPath={socket_path}        # ~/.hermes/ssh/{user}@{host}:{port}.sock


- o ControlMaster=auto               # 复用已有连接


- o ControlPersist=300                # 5 分钟无命令后关闭


- o BatchMode=yes                     # 禁止密码提示


- o StrictHostKeyChecking=accept-new  # 接受新主机,拒绝已变更的密钥

第一次连接建立 SSH 通道,后续命令复用同一个 socket------避免每条命令都做 TCP+SSH 握手。

文件同步 :SSH 后端使用 FileSyncManager,通过 tar-over-pipe 批量上传文件:

复制代码
tar -chf - -C /staging . | ssh user@host "tar xf - -C /"

一次 TCP 流传几百个文件,比逐个 scp 快一个数量级。

配置

复制代码
TERMINAL_SSH_HOST=dev-server.example.com
TERMINAL_SSH_USER=ubuntu
TERMINAL_SSH_PORT=22
TERMINAL_SSH_KEY=~/.ssh/id_ed25519

4. Daytona:云端有状态沙箱

tools/environments/daytona.py------Daytona 是一个云开发环境 SDK,提供按需启停的托管沙箱。

冷启动 / 休眠 / 恢复

复制代码
首次创建:sandbox = Daytona.create(image=..., auto_stop_interval=0)
正常关闭:sandbox.stop()(保留文件系统)
下次启动:sandbox = Daytona.get(name) → sandbox.start()(恢复文件系统)

auto_stop_interval=0------关闭 Daytona 的自动休眠,由 Hermes Agent 完全控制生命周期。

每次命令执行前daytona.py:195-200)会检查沙箱状态------如果意外进入了 STOPPED 或 ARCHIVED 状态,自动调 sandbox.start() 恢复。对用户透明。

持久化 :默认持久(cleanup()stop() 不调 delete())。非持久模式调 delete(),沙箱及其文件系统被彻底删除。

5. Singularity:HPC 集群的容器标准

tools/environments/singularity.py------Singularity(现名 Apptainer)是 HPC 集群上最常用的容器运行时。

核心安全标志

复制代码
apptainer instance start \
  --containall --no-home \
  [--overlay {dir}]         # 持久化模式
  [--writable-tmpfs]        # 临时模式
  {image} {instance_id}
  • --containall:隔离 /tmp、/var/tmp、/dev、/proc、cgroup

  • --no-home:不挂载用户 home 目录

Overlay 文件系统 :持久化模式用 overlay 目录。路径优先级是:先看 TERMINAL_SCRATCH_DIR,否则在可写时优先落到 /scratch/<user>/hermes-agent/hermes-overlays/overlay-{task_id}/,再回退到 HERMES_HOME/sandboxes/singularity/hermes-overlays/overlay-{task_id}/。基础镜像是只读的,所有修改都写入 overlay 层。

SIF 镜像缓存singularity.py:107-153):第一次使用时从 Docker 镜像构建 SIF 文件,缓存到本地。后续启动直接用缓存的 SIF------避免每次都从 Docker Hub 拉取。

HPC 适配 :优先使用 /scratch(HPC 节点本地高速存储),回退到 sandbox 目录。支持 --memory--cpus 通过 cgroup 限制资源。

6. Modal:云端 GPU + 按秒计费

tools/environments/modal.py------Modal 是一个无服务器计算平台,特别适合需要 GPU 的场景。

创建沙箱modal.py:223-237):

复制代码
sandbox = await modal.Sandbox.create.aio(
    "sleep", "infinity",
    image=image_spec,
    app=app,
    timeout=3600,
    **sandbox_kwargs,  # cpu, memory, mounts
)

快照持久化 ------Modal 的独特能力(modal.py:433-451):

复制代码
# cleanup 时拍快照
img = await sandbox.snapshot_filesystem.aio()
_store_direct_snapshot(task_id, img.object_id)

# 下次创建时从快照恢复
snapshot_id = _get_snapshot_restore_candidate(task_id)
sandbox = await Sandbox.create.aio(image=snapshot_id or base_image, ...)

snapshot_filesystem 把整个容器文件系统序列化为一个 Modal Image------下次从快照恢复比冷启动快得多。快照 ID 存在 ~/.hermes/modal_snapshots.json

AsyncWorkermodal.py:115-145):Modal SDK 是纯异步的,但 Hermes 的 BaseEnvironment 是同步接口。_AsyncWorker 类在后台线程维护一个独立事件循环,通过 run_coroutine() 桥接:

复制代码
class _AsyncWorker:
    def run_coroutine(self, coro, timeout=600):
        future = asyncio.run_coroutine_threadsafe(coro, self._loop)
        return future.result(timeout=timeout)

只读凭证挂载modal.py:188-219):通过 modal.Mount.from_local_file() 把凭证文件以只读方式挂载到容器内------沙箱能读取但不能修改凭证。

冷启动:Modal 的冷启动通常 30-60 秒------比 Docker 慢一个数量级。快照恢复可以缩短到 10-20 秒。


六种后端对比

Local Docker SSH Daytona Singularity Modal
隔离级别 无(env 过滤) 容器 网络隔离 云隔离 容器 云隔离
冷启动 即时 5-10s 1-2s 10-20s 5-10s 30-60s
持久化 不适用 bind mount 远端文件系统 stopped sandbox overlay dir 快照
资源限制 CPU/mem/disk/PID CPU/mem/disk CPU/mem CPU/mem/disk
文件同步 不需要 bind mount tar-over-SSH Daytona SDK bind mount tar-over-stdin
命令审批 需要 跳过 需要 跳过 跳过 跳过
适用场景 本地开发 通用隔离 远程开发 云开发 HPC GPU 计算

"命令审批跳过"一行 是一个关键的设计洞察:容器化后端(Docker / Singularity / Modal / Daytona)本身就是安全边界------容器内的破坏性命令不影响宿主机,所以不需要再让用户逐条审批。这让容器化部署的用户体验显著优于 Local 后端------不会被频繁的审批提示打断工作流。


检查点管理器:影子 Git 仓库

容器隔离保护的是宿主机。但如果 Agent 在 Local 后端(或容器的持久化 workspace 里)改坏了文件------怎么恢复?

tools/checkpoint_manager.py 提供了一套不污染用户项目的文件恢复机制

影子 Git 仓库

核心思路:在用户的工作目录之外,维护一个隐藏的 Git 仓库来追踪文件变更。

复制代码
~/.hermes/checkpoints/{sha256(abs_workdir)[:16]}/
    ├── HEAD, refs/, objects/     # 标准 git 内部结构
    ├── HERMES_WORKDIR            # 记录对应的工作目录路径
    └── info/exclude              # 默认排除:node_modules, .env, __pycache__...

为什么叫"影子"? 因为用户的项目目录里没有 .git/ 文件夹------所有 git 元数据都存在 ~/.hermes/checkpoints/ 下。通过 GIT_DIR + GIT_WORK_TREE 环境变量把 git 指向影子仓库,工作树指向用户的项目目录。

Git 环境隔离

影子仓库必须完全隔离于用户的 git 配置checkpoint_manager.py:128-159):

复制代码
env["GIT_DIR"] = str(shadow_repo)
env["GIT_WORK_TREE"] = str(normalized_working_dir)
env["GIT_CONFIG_GLOBAL"] = os.devnull     # 忽略 ~/.gitconfig
env["GIT_CONFIG_SYSTEM"] = os.devnull     # 忽略 /etc/gitconfig
env["GIT_CONFIG_NOSYSTEM"] = "1"          # 兼容旧版 git

为什么要隔离? 因为用户的 ~/.gitconfig 可能有 commit.gpgsign = true------如果影子仓库继承了这个配置,每次自动快照都会弹 GPG pinentry 对话框,卡住整个 Agent。

快照的触发时机

检查点在文件变更前自动创建,但触发条件比"所有写操作"更保守:

  • write_filepatch:执行前直接触发 ensure_checkpoint()

  • terminal:只有命中 _is_destructive_command() 启发式规则时才触发,比如 rmmvsed -igit reset/clean/checkout、覆盖式重定向等

去重:每个工作目录每次 Agent 主循环迭代最多一次快照。也就是说,同一轮用户请求里如果 Agent 经过了多次工具迭代,仍可能拿到多个 checkpoint;但不会因为一次迭代里连改 10 个文件就创建 10 个快照。

安全检查 :跳过根目录 /、home 目录、文件数超过 50K 的目录------避免在巨大目录上做 git add -A 卡死。

恢复流程

恢复前先快照当前状态checkpoint_manager.py:492)------这样如果用户发现恢复搞错了,还能撤销。

复制代码
# 恢复前先快照
ensure_checkpoint(working_dir, reason=f"pre-rollback snapshot (restoring to {commit[:8]})")

# 然后恢复
git checkout {commit_hash} -- .   # 全目录恢复
git checkout {commit_hash} -- {file}  # 单文件恢复

不移动 HEAD------恢复后影子仓库的 HEAD 不变,用户可以继续正常工作或重新提交。


实战:用 Docker 后端搭建安全的代码执行环境

第一步:配置 Docker 后端

编辑 ~/.hermes/config.yaml(或设置环境变量):

复制代码
terminal:
  backend: docker
  docker_image: "nikolaik/python-nodejs:python3.11-nodejs20"
  container_persistent: true
  container_memory: 4096   # 4GB
  container_cpu: 2

或者用环境变量:

复制代码
export TERMINAL_ENV=docker
export TERMINAL_DOCKER_IMAGE="nikolaik/python-nodejs:python3.11-nodejs20"
export TERMINAL_CONTAINER_PERSISTENT=true

第二步:启动 Hermes Agent

复制代码
hermes chat

这一步还不会立刻创建容器。当前实现是按需懒创建 :只有第一次真正调用 terminal 工具时,terminal_tool.py 才会进入 _create_environment(...) 拉起 Docker 容器。

所以更准确的验证顺序是:先进入 hermes chat,再给 Agent 一条明确要调用终端的指令,然后用 docker ps 观察容器出现。

第三步:验证隔离

在 Agent 里执行一条"危险"命令来验证隔离:

复制代码
请在终端里执行 ls / 看看容器的根目录

你会看到容器内的文件系统------与宿主机不同。容器内的 /tmp 是一个 512MB 的 tmpfs。

注意命令审批不会弹出------Docker 后端跳过了第 2 层审批。

第四步:集成到 Telegram

对于 Gateway + Telegram 的部署,Docker 后端是最推荐的选择------来自 Telegram 的命令会落到隔离容器里执行,即使用户输入了恶意命令也不影响宿主机:

复制代码
# .env
TERMINAL_ENV=docker
TERMINAL_CONTAINER_PERSISTENT=true
TELEGRAM_BOT_TOKEN=...
TELEGRAM_ALLOWED_USERS=...

# 启动 Gateway
hermes gateway run

更严谨地说,容器复用粒度是 session ,不是无条件的"一人一容器"。Gateway 侧终端环境按 task_id 复用,而主会话的 task_id 就是 session_idsession_id 又来自 session_key 规则。默认私聊和多数群消息通常能做到按用户隔离,但线程消息在默认配置下可能是"线程内多人共享一个 session"。

第五步:观察持久化

退出 Agent,过一会儿再启动:

复制代码
docker ps -a | grep hermes

你通常会看到旧容器处于 Exited(停止但未删除)状态;这说明上一次环境被 stop 了,但并不意味着下一次一定会 docker start 回这个旧容器。

更准确地说:如果新的会话继续命中了同一个 task_id/session_id,Hermes 会新建一个容器,并把先前 bind mount 目录里的内容重新挂进去。因此 /root/workspace 挂载层里的文件通常还在;但写在容器根文件系统其他位置、又不在挂载目录里的改动,并不承诺持久化。

相关推荐
ylscode1 小时前
CISA紧急拉响警报:SolarWinds Serv-U曝高危漏洞CVE-2026-28318,零认证即可瘫痪文件传输服务
人工智能·安全
PythonFun1 小时前
WPS智能文档:解锁高效写作新体验
人工智能·wps
鹏大师运维1 小时前
统信UOS安装Subtitle Edit并使用Edge-TTS生成AI语音教程
linux·前端·人工智能·edge·麒麟·统信uos·ai语音
小赖同学啊1 小时前
基于MCP与主流AI技术架构 水利 发电 公园中的应用
人工智能·架构
morning_judger1 小时前
Agent开发系列(六)-安全护栏建设
人工智能·安全
2501_946786201 小时前
2026漏洞扫描服务:企业防护痛点解决指南
网络·安全·web安全
2501_912784081 小时前
后端开发实战:反向海淘多币种结算模块自研与SaaS复用对比
大数据·人工智能·taocarts·跨境saas
网络研究院1 小时前
黑客利用人工智能的5种方式(以及如何防御)
人工智能·黑客·攻击·恶意软件·钓鱼邮件
用户5191495848451 小时前
Wux Blog Editor 漏洞利用工具 (CVE-2024-9932)
人工智能·aigc