Appearance
Hermes 的安全防护不依赖信任 AI 的自我约束,而是在工具调用层做独立拦截。三层防护依次执行:正则检测(毫秒级,无 LLM)→ Unicode 规范化防绕过 → Tirith 深度内容扫描(可选)→ 用户审批队列(多线程安全)。审批有三种粒度:本次、本会话、永久白名单。
Hermes Agent 安全防护层深度解析:危险命令检测、Tirith 扫描与多层审批机制
AI Agent 执行 shell 命令的核心安全问题:AI 可能被精心构造的 prompt(提示注入)诱导执行破坏性操作,或者 AI 本身判断失误。Hermes 的安全设计思路是不信任 AI 的自我约束,在工具调用层独立拦截。
第一层:正则危险命令检测
tools/approval.py 里维护了一份危险命令模式列表(DANGEROUS_PATTERNS),每条命令在执行前都会过这个列表:
python
DANGEROUS_PATTERNS = [
(r'\brm\s+-[^\s]*r', "recursive delete"),
(r'\bDROP\s+(TABLE|DATABASE)\b', "SQL DROP"),
(r'\bDELETE\s+FROM\b(?!.*\bWHERE\b)', "SQL DELETE without WHERE"),
(r'\bkill\s+-9\s+-1\b', "kill all processes"),
(r':\(\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;\s*:', "fork bomb"),
(r'\b(curl|wget)\b.*\|\s*(ba)?sh\b', "pipe remote content to shell"),
# ... 共 30+ 条
]分类一览:
| 类别 | 模式示例 | 描述 |
|---|---|---|
| 文件系统破坏 | rm -rf、find -delete | 递归删除 |
| 系统配置修改 | chmod 777、> /etc/ | 权限/配置覆写 |
| 进程操作 | kill -9 -1、pkill hermes | 全进程 kill 或 self-kill |
| SQL 危险操作 | DROP TABLE、DELETE FROM(无 WHERE) | 数据库破坏 |
| 远程代码执行 | `curl ... | bash、wget ... |
| Shell 注入 | bash -c、python -c | 通过 -c 旗注入 |
| 系统服务 | systemctl stop、systemctl disable | 停止系统服务 |
| 特殊情况 | fork bomb(`:(){: | :&};:`) |
| 自保护 | pkill hermes、killall gateway | 防止 Agent kill 自身 |
值得注意的两条特殊规则:
Gateway 自保护:
python
(r'\b(pkill|killall)\b.*\b(hermes|gateway|cli\.py)\b', "kill hermes/gateway process")防止 AI 被诱导杀掉自己的进程。
Gateway 进程管理保护:
python
(r'gateway\s+run\b.*(&\s*$|&\s*;|\bdisown\b|\bsetsid\b)', "start gateway outside systemd")防止在 Gateway 里启动另一个 Gateway(正确做法是通过 systemctl 管理)。
第二层:Unicode 规范化防绕过
检测前有一个关键步骤——命令规范化:
python
def _normalize_command_for_detection(command: str) -> str:
from tools.ansi_strip import strip_ansi
# 1. 剥离 ANSI 转义序列(颜色码、光标控制等)
command = strip_ansi(command)
# 2. 清除 null 字节
command = command.replace('\x00', '')
# 3. Unicode NFKC 规范化(全角字符 → ASCII)
command = unicodedata.normalize('NFKC', command)
return commandUnicode 规范化解决的攻击:
提示注入攻击常用全角字符绕过正则检测。例如:
- 全角 r:
rm -rf / - 全角 m:
rm -rf /(看起来一样,但字节不同) - 零宽字符:
rm -rf /(中间有不可见字符)
NFKC 规范化把所有这类变体转回 ASCII 等价形式,让正则匹配无法被绕过。
审批决策:四种粒度
检测到危险命令后,不是直接拒绝,而是进入审批流程。用户有四个选项:
| 选项 | 含义 | 存储位置 |
|---|---|---|
once | 只允许这次 | 不存储(一次性) |
session | 本会话内同类命令不再问 | _session_approved(内存) |
always | 永久白名单,以后不再问 | config.yaml command_allowlist |
deny | 拒绝执行 | 不存储 |
「同类命令」匹配的是 pattern key(命令描述字符串),而不是精确命令字符串。比如你批准了一次「递归删除」,本会话里所有 rm -r 系列命令都不会再问——但新会话重新开始。
多线程安全:审批队列
在 Gateway 模式下,多个并发的子代理可能同时触发危险命令,都需要审批。Hermes 用了一个 per-session 的审批队列:
python
_gateway_queues: dict[str, list] = {} # session_key → [_ApprovalEntry, …]
class _ApprovalEntry:
__slots__ = ("event", "data", "result")
def __init__(self, data):
self.event = threading.Event() # 阻塞等待用户响应
self.data = data # 命令信息
self.result = None # "once"|"session"|"always"|"deny"流程:
- Agent 线程检测到危险命令,创建
_ApprovalEntry,加入队列 - Agent 线程在
entry.event.wait()上阻塞 - 用户点击审批按钮,触发
resolve_gateway_approval() entry.event.set(),Agent 线程解除阻塞,读取entry.result
支持 /approve all 一次性批准队列里所有等待的请求(FIFO 顺序)。
Session key 用 contextvars.ContextVar,而不是环境变量:
python
_approval_session_key: contextvars.ContextVar[str] = contextvars.ContextVar(
"approval_session_key", default=""
)Gateway 里多个线程并发执行不同用户的 Agent 任务,每个线程有自己的上下文变量值,不会互相干扰。如果用环境变量或进程全局变量,多线程下 session key 会错乱。
第三层(可选):Tirith 深度内容扫描
Tirith 是一个独立的安全扫描二进制,专门检测正则难以覆盖的威胁:
- 同态字符 URL:看起来是合法域名,实际用了视觉相似的 Unicode 字符(如
аpple.com用西里尔字母а) - Shell 注入模式:比正则更深层的语义分析
- 管道到解释器:复杂情况下的远程代码执行检测
自动安装:Tirith 不在 Python 依赖里,Hermes 启动时在后台线程自动下载:
python
def _auto_install_tirith():
# 从 GitHub Releases 下载
# 验证 SHA-256 checksum
# 如果有 cosign 工具,额外验证供应链签名
# 安装到 $HERMES_HOME/bin/tirithSHA-256 验证确保下载的二进制没被篡改。cosign 供应链验证(可选)进一步确认二进制来自 GitHub Actions 构建流水线。
失败策略(tirith_fail_open):
yaml
security:
tirith_enabled: true
tirith_fail_open: true # 扫描失败时允许通过(默认)fail_open: true(默认)意味着 Tirith 崩溃或超时时,命令不会因为扫描工具失败而被阻塞。这是一个可用性 vs 安全性的权衡——AI Agent 场景里过度阻塞会完全破坏工作流。对于高安全场景,可以设 fail_open: false。
智能审批(辅助 LLM 预评估)
对于已进入审批队列的命令,Hermes 可以用辅助 LLM 做风险预评估,自动处理明显低风险的命令:
配置:
yaml
security:
smart_approval: true
smart_approval_model: "openrouter/google/gemini-flash-1.5"逻辑:辅助 LLM 读取命令 + 当前上下文,判断风险等级。明确低风险(如读取只读目录)→ 自动批准(标记为 once);不确定或高风险 → 仍然推送给用户审批。
永久白名单管理
用户批准 always 后写入 config.yaml:
yaml
command_allowlist:
- "recursive delete"
- "SQL TRUNCATE"可以手动编辑这个文件删除某条白名单。Pattern key 使用人类可读的描述字符串(而不是正则),便于手动管理。
Hermes 还维护 pattern key 的别名映射(_PATTERN_KEY_ALIASES),处理从旧版本正则 key 到新版本描述 key 的兼容性——升级后旧的白名单配置仍然有效。
FAQ
Q: 有没有可能正则检测漏掉危险命令?
有。正则是静态规则,新的攻击手法可以绕过。这也是 Tirith 深度扫描存在的原因。多层防御比单层更健壮。
Q: AI 能自己把自己从白名单里删除吗?
技术上,如果 AI 有写 config.yaml 的权限,可以。这是为什么终端工具的工作目录和 HERMES_HOME 最好分开,对高安全场景用 SSH 远程执行后端(HERMES_HOME 在本地,AI 无法访问)。
Q: Gateway 模式下审批等待超时怎么办?
没有硬超时,但 unregister_gateway_notify() 被调用时(Agent run 结束)会 event.set() 解除所有阻塞,防止线程永远挂起。超时后的默认行为是命令被拒绝(安全方向)。