Skip to content

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 -rffind -delete递归删除
系统配置修改chmod 777> /etc/权限/配置覆写
进程操作kill -9 -1pkill hermes全进程 kill 或 self-kill
SQL 危险操作DROP TABLEDELETE FROM(无 WHERE)数据库破坏
远程代码执行`curl ...bashwget ...
Shell 注入bash -cpython -c通过 -c 旗注入
系统服务systemctl stopsystemctl disable停止系统服务
特殊情况fork bomb(`:(){::&};:`)
自保护pkill hermeskillall 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 command

Unicode 规范化解决的攻击

提示注入攻击常用全角字符绕过正则检测。例如:

  • 全角 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"

流程:

  1. Agent 线程检测到危险命令,创建 _ApprovalEntry,加入队列
  2. Agent 线程在 entry.event.wait() 上阻塞
  3. 用户点击审批按钮,触发 resolve_gateway_approval()
  4. 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/tirith

SHA-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() 解除所有阻塞,防止线程永远挂起。超时后的默认行为是命令被拒绝(安全方向)。