Agent 的终端安全怎么做?6 种沙箱后端 + 危险命令审批 + sudo 无痕处理的完整拆解

安全专题第二篇:从 local 裸金属到 Modal 云沙箱,拆解 terminal_tool.py 的 6 后端 + 危险命令审批 + sudo 密码无痕处理

TL;DR

「Agent 可以执行 rm -rf /」是 AI Agent 最让人恐惧的场景。Hermes Agent 的回答不是「禁止 Agent 执行命令」,而是用 6 种可插拔的终端后端,让用户在「我要速度」和「我要安全」之间自己选。

本文拆解 tools/terminal_tool.py(2978 行,Hermes 工具模块中最大的单文件)的三层安全架构:

  1. 后端隔离:6 种后端(local→docker→modal→singularity→daytona→ssh),隔离等级逐级递增
  2. 危险命令审批check_all_command_guards 在命令执行前做模式匹配,rm -rf / 直接拒绝
  3. sudo 密码安全 :密码通过内存缓存传子进程 stdin,不出现在命令行中、不写入日志

配合 Codex CLI 系统级沙箱(Seatbelt/Landlock)和 Claude Code Docker 方案的横向对比。


一、六种后端:不是一刀切,是可插拔的隔离阶梯

1.1 后端总览

python 复制代码
# terminal_tool.py 支持的 6 种环境
env_type = os.getenv("TERMINAL_ENV", "local")

local       # 直接宿主机 --- 最快,零隔离
docker      # Docker 容器 --- 中等隔离,需要 Docker
modal       # Modal 云沙箱 --- 完全隔离,云端执行
singularity # Singularity 容器 --- HPC 环境,类似 Docker
daytona     # Daytona 沙箱 --- 云端开发环境
ssh         # 远程 SSH --- 远端隔离

1.2 隔离阶梯

scss 复制代码
隔离等级:            速度:
  local ████░░░       ████████ (最快)
  ssh   ███░░░░       ██████░░
  docker ██████░░     █████░░░
  singularity ██████░ ████░░░░
  daytona ███████░    ███░░░░░
  modal  ████████     ██░░░░░░ (最慢)

选择逻辑

  • 你在本地笔记本上开发,信任 Agent → local(零开销,但 rm -rf / 真会执行)
  • 你在服务器上跑 CI/CD → docker(文件系统隔离,容器删除后不留痕迹)
  • 你在跑不可信代码、需要严格隔离 → modal(云端沙箱,连文件系统都是临时的)

1.3 为什么不是「默认用 Docker」?

Hermes 没有默认强制 Docker------因为 Docker 是额外依赖。Codex CLI 用 Landlock 不需要 Docker、Claude Code 的 Docker 是可选的。Hermes 的设计哲学是:

「让用户选,不要替用户选。如果用户在本地测试一个小脚本,没必要启动一个 Docker 容器。」

Modal 是六种后端中隔离等级最高的------代码在 Modal 云端容器中执行,连文件系统都是临时的:

ini 复制代码
Agent 决定执行命令
  → terminal_tool 检测 env_type="modal"
    → Modal SDK 创建云端容器
      → 命令在云端执行
        → 结果通过 API 返回
          → 容器销毁(persistent filesystem 除外)

但 Modal 不是万能药:持久化文件系统不保证长进程存活------容器可能被云端回收。


二、危险命令审批:在 subprocess.run 之前拦截

2.1 审批不是事后审计

Hermes 的审批系统不在命令执行后检查结果------subprocess.Popen 被调用之前就完成了拦截

python 复制代码
# terminal_tool.py 调用审批模块
from tools.approval import check_all_command_guards

# 在命令执行之前:
threats = _check_all_guards_impl(command, ...)
if threats:
    # 生成了威胁报告 → 触发审批流程
    approved = _ask_user_for_approval(threats)
    if not approved:
        return "Command blocked by user."

2.2 审批的几个关键设计

设计 为什么
模式匹配,不是 AI 判断 模式匹配可审计、可预测。AI 判断 rm -rf / 危险不危险 → 取决于 prompt engineering
per-thread 审批回调 ACP 的多会话跑在 ThreadPoolExecutor 里,每个会话独立审批状态,不互相污染
审批结果不缓存 用户对「这次」的审批不等于对「永远」的授权

2.3 危险命令的分级

python 复制代码
# 审批模块对命令做分级(示意)
LEVEL 1 --- 完全拒绝(无审批选项):
  rm -rf /          # 删根
  chmod 777 /etc    # 系统权限开放
  :(){ :|:& };:     # fork bomb

LEVEL 2 --- 需要审批(用户确认后允许):
  pip install       # 安装软件包
  curl | bash       # 管道执行
  git push --force  # 强制推送

LEVEL 3 --- 允许(无需审批):
  ls, cat, grep, echo, cd, pwd

三、sudo 密码:一个绝不出现在日志中的敏感信息

3.1 问题:Agent 需要 sudo,但密码不能泄露

Agent 在部署时需要 sudo apt installsudo systemctl restart。如果让 Agent 直接把密码写在命令行里:

bash 复制代码
# ❌ 绝对不行 --- 密码会出现在 /var/log/ 和 ~/.bash_history 里
echo "mypassword" | sudo -S apt install nginx

3.2 Hermes 的解决:四个安全层级

python 复制代码
# Layer 1: 密码源 --- 不落盘
# 从环境变量 SUDO_PASSWORD 读取,或内存中交互式提示
password = os.getenv("SUDO_PASSWORD") or _prompt_for_sudo_password()

# Layer 2: 命令改写 --- sudo 变成 stdin 模式
# 原始: sudo apt install nginx
# 改写: sudo -S -p '' apt install nginx
# -S = 从 stdin 读取密码
# -p '' = 禁止密码提示(攻击者看不到暗示)
transformed = "sudo -S -p '' apt install nginx"

# Layer 3: 密码传子进程 stdin --- 不经过 shell 命令行
subprocess.run(
    transformed,
    input=f"{password}\n",  # ← 通过 subprocess stdin,不是命令行参数
    text=True,
)

# Layer 4: 进程隔离 --- 密码只在这一个子进程的内存中
# 子进程结束后,密码随内存回收。不写入任何文件、任何日志。

3.3 密码缓存:per-session,不跨会话泄漏

python 复制代码
# 密码缓存的 scope 分级
_sudo_password_cache = {}  # key: scope, value: password

# scope 的选择优先级:
# 1. HERMES_SESSION_KEY(多会话隔离)
# 2. callback identity(ACP/CLI 隔离)
# 3. thread ID(线程隔离)

关键设计 :同一个 session 内可以缓存 sudo 密码(不需要每次重新输入),但不同 session 绝不共享------ACOP 会话 A 的 sudo 密码不会泄漏到会话 B。


四、三道硬限制:超时、磁盘、清理

4.1 超时硬上限

python 复制代码
FOREGROUND_MAX_TIMEOUT = 600  # 默认 600 秒
# 可通过 TERMINAL_MAX_FOREGROUND_TIMEOUT 覆盖

# 即使 Agent 传 timeout=99999,实际执行也不超过 600 秒

为什么? Agent 可能被 Prompt Injection 诱导调用 terminal(timeout=999999) 来保持一个长时间运行的进程。硬上限确保即使攻击成功,窗口也只有 10 分钟。

4.2 磁盘使用警告

python 复制代码
DISK_USAGE_WARNING_THRESHOLD_GB = 500  # 500GB

# 每次执行前检查 scratch 目录总大小
# 超过阈值 → 日志警告,但不阻断(用户自己判断)

4.3 自动清理

复制代码
Session 结束 → 清理临时文件
空闲超时 → 回收 Modal/SSH 连接
进程退出 → atexit 钩子确保无残留

五、三角对照:三种沙箱范式

Hermes Codex CLI Claude Code
默认后端 local(用户自选) local(可切 Docker) local
可选隔离 Docker/Modal/Singularity Landlock/bubblewrap Docker(CLI Runner)
系统级沙箱 ✅ Seatbelt/Landlock/ACL
云沙箱 ✅ Modal
危险命令审批 ✅ 模式匹配 ✅ 权限系统 ✅ 5 级 Permission
sudo 密码保护 ✅ stdin 传递+缓存
超时硬上限 ✅ 600s
独立程度 依赖用户选择 内核级 依赖 Docker

5.1 什么时候选哪种?

scss 复制代码
我需要最快的迭代速度         → local (Hermes/Docker/Modal 都可以用 local 启动)
我需要在服务器上跑不可信代码   → Docker (Hermes / Claude Code)
我需要内核级的强制隔离         → Codex CLI (Seatbelt/Landlock)
我需要把危险操作推到云端       → Hermes + Modal

六、总结:沙箱设计的三个原则

terminal_tool.py 的 2978 行代码可以提炼出三个普适原则:

原则一:隔离是可选的,不是强制的。

不是所有人都在跑不可信代码。给 local 和 docker 两种选择,让用户自己做 risk/reward 判断。

原则二:拦截在命令执行之前,不在之后。

tools/approval.py 的模式匹配在 subprocess.Popen 之前运行。事后审计可以发现错误,但事前的模式匹配可以阻止错误。

原则三:敏感信息只通过 stdin,不通过命令行。

sudo 密码通过 subprocess.run(input=...) 传子进程,而不是拼进命令行字符串。这确保了密码不出现在 /proc/PID/cmdline、shell history 和日志文件中。


下一篇预告 :多 Agent 协作 Kanban 架构------delegate_task + Kanban Lane 的调度模型,5 个并行 Codex CLI 工作流如何不互相踩脚。

相关推荐
Jolyne_1 天前
Angular基础速通
前端·angular.js
starrysky8101 天前
Flash Attention 安装地狱六重崩溃:CUDA_HOME not set、undefined symbol、预编译轮子不兼容、pip 编译两小时失败——逐一击破
angular.js
starrysky8102 天前
nvidia-smi 显示 8GB 空闲,为什么 PyTorch 报 CUDA out of memory?——CUDA 缓存分配器底层原理
angular.js
starrysky8105 天前
Ollama 部署五大崩溃:llama runner terminated exit 2、10分钟后停止服务、GGUF断言失败——逐一修复
angular.js
starrysky8107 天前
ACP 不是 MCP 的平替:拆解 Claude Code 的子进程 Agent 架构——与 OpenClaw、Hermes 的三角对照
angular.js
starrysky81012 天前
被忽视的Django生产陷阱:为什么ALLOWED_HOSTS通配符救不了你——DisallowedHost根因排查与中间件修复
angular.js
starrysky81013 天前
Hermes Agent 的 70+ 工具不是硬编码的:一套自注册的注册表引擎 [04]
angular.js
巴勒个啦15 天前
Pinia 源码解析:响应式状态管理是如何工作的
angular.js
starrysky81016 天前
拆开 Hermes Agent 的引擎盖:八大子系统、37 个模块,一张地图讲清楚——底层系列开篇
angular.js