安全专题第二篇:从 local 裸金属到 Modal 云沙箱,拆解
terminal_tool.py的 6 后端 + 危险命令审批 + sudo 密码无痕处理
TL;DR
「Agent 可以执行 rm -rf /」是 AI Agent 最让人恐惧的场景。Hermes Agent 的回答不是「禁止 Agent 执行命令」,而是用 6 种可插拔的终端后端,让用户在「我要速度」和「我要安全」之间自己选。
本文拆解 tools/terminal_tool.py(2978 行,Hermes 工具模块中最大的单文件)的三层安全架构:
- 后端隔离:6 种后端(local→docker→modal→singularity→daytona→ssh),隔离等级逐级递增
- 危险命令审批 :
check_all_command_guards在命令执行前做模式匹配,rm -rf /直接拒绝 - 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 容器。」
1.4 Modal 云沙箱:把危险操作推上云端
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 install、sudo 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 工作流如何不互相踩脚。