转载--Hermes Agent 11 | 智能审批与平台化安全:当 AI 来守护 AI

原文:https://mp.weixin.qq.com/s?__biz=MzAwMTYwNzE2Mg==&mid=2651039803&idx=1&sn=48d99baff841c22ae44d6bd35efd9b2d&chksm=81201caeb65795b848649ed0ad0de845f1174da3bb3273aca6a3c15550ee567ed25a01a20894&cur_album_id=4469690989619855360&scene=189&poc_token=HEBvI2qjEuBcEUTbVSckk3vVR6chuJdW6ySURd3h

最好的守门人不是拒绝一切的人,而是能分辨谁该进门的人。


智能审批:让 LLM 判断 LLM 的行为是否安全

问题:正则模式太粗

第 10 讲说过,DANGEROUS_PATTERNS 有约 40 条正则。正则的问题是误报

复制代码
python -c "print('hello world')"

这条命令匹配了 (python[23]?|perl|ruby|node)\s+-[ec]\s+------"脚本执行 via -c flag"。但它显然是安全的。如果每次 Agent 跑 python -c 都要弹确认框,用户会在第三次之后永久关掉审批。

_smart_approve():辅助 LLM 做裁判

tools/approval.py:535-579 实现了 smart 模式的核心:

复制代码
def _smart_approve(command: str, description: str) -> str:
    prompt = f"""You are a security reviewer for an AI coding agent. A terminal command was flagged by pattern matching as potentially dangerous.

Command: {command}
Flagged reason: {description}

Assess the ACTUAL risk of this command. Many flagged commands are false positives --- for example, `python -c "print('hello')"` is flagged as "script execution via -c flag" but is completely harmless.

Rules:
- APPROVE if the command is clearly safe (benign script execution, safe file operations, development tools, package installs, git operations, etc.)
- DENY if the command could genuinely damage the system (recursive delete of important paths, overwriting system files, fork bombs, wiping disks, dropping databases, etc.)
- ESCALATE if you're uncertain

Respond with exactly one word: APPROVE, DENY, or ESCALATE"""

    response = call_llm(
        task="approval",
        messages=[{"role": "user", "content": prompt}],
        temperature=0,
        max_tokens=16,
    )
    answer = (response.choices[0].message.content or "").strip().upper()

    if "APPROVE" in answer:
        return "approve"
    elif "DENY" in answer:
        return "deny"
    else:
        return "escalate"

几个设计点:

1. temperature=0------安全决策必须确定性。不同温度下 LLM 可能给出不同的安全评估。

2. max_tokens=16------只要一个词的回答。不让模型长篇大论分析风险,直接 APPROVE / DENY / ESCALATE。

3. prompt 里主动提 false positive ------"Many flagged commands are false positives --- for example, python -c "print('hello')" is flagged as..." 这不是废话,这是校准提示。如果不提,LLM 会倾向于"既然被标记了,那大概确实危险"------confirmation bias。

4. ESCALATE 是兜底 ------LLM 不确定时不做决定,而是把球踢给人类。异常(LLM 调用失败、解析错误)也走 ESCALATE。安全系统的默认行为应该是"升级给人"而不是"自动放行"。

三级决策链

Smart 模式不是单独工作的------它嵌入在 check_all_command_guards() 的完整决策链中(approval.py:689-948):

复制代码
Phase 1: 收集安全发现
  ├─ Tirith 外部扫描 → block / warn / allow
  └─ 正则危险模式匹配 → 匹配的模式列表

Phase 2: 整合警告
  ├─ Tirith block/warn → 可审批的警告
  └─ 正则匹配 → 检查是否已预审批

Phase 2.5: Smart 审批(仅 mode=smart 时)
  ├─ APPROVE → 批准 + 自动授予 session 级预审批
  ├─ DENY → 拒绝,返回 "BLOCKED by smart approval"
  └─ ESCALATE → 继续到 Phase 3

Phase 3: 人工审批
  ├─ CLI → [o/s/a/D] 交互提示
  └─ Gateway → 平台按钮

Smart 模式不替代人类------它过滤掉大量误报,只把真正需要判断的命令交给人。


多平台审批体验:同一个安全决策,不同的 UX

审批决策是统一的(approval.py),但呈现给用户的方式因平台而异

CLI:经典的交互式提示

复制代码
⚠️  DANGEROUS COMMAND: recursive delete
    rm -rf /tmp/old-project

    [o]nce  |  [s]ession  |  [a]lways  |  [d]eny

    Choice [o/s/a/D]:

四个选项,默认是 deny(大写 D 表示默认)。60 秒超时后自动拒绝。

一个细节 :当 Tirith 报告了安全警告时,[a]lways 选项会被隐藏------防止用户把一个有安全隐患的命令模式永久加入白名单。只保留 [o/s/D]

Telegram:内联键盘按钮

Telegram 适配器(gateway/platforms/telegram.py)用 InlineKeyboardMarkup 发送 2×2 的按钮矩阵:

复制代码
keyboard = InlineKeyboardMarkup([
    [
        InlineKeyboardButton("✅ Allow Once", callback_data=f"ea:once:{approval_id}"),
        InlineKeyboardButton("✅ Session", callback_data=f"ea:session:{approval_id}"),
    ],
    [
        InlineKeyboardButton("✅ Always", callback_data=f"ea:always:{approval_id}"),
        InlineKeyboardButton("❌ Deny", callback_data=f"ea:deny:{approval_id}"),
    ],
])

用户点击按钮后,_handle_callback_query() 解析 ea:choice:approval_id,通过 resolve_gateway_approval() 解除等待中的 Agent 线程阻塞。

Slack:Block Kit 按钮

Slack 适配器(gateway/platforms/slack.py)用 Block Kit 的 actions 块发送四个按钮:

复制代码
elements = [
    {"action_id": "hermes_approve_once", "value": session_key, ...},
    {"action_id": "hermes_approve_session", "value": session_key, ...},
    {"action_id": "hermes_approve_always", "value": session_key, ...},
    {"action_id": "hermes_deny", "value": session_key, ...},
]

Slack 的交互回调通过 _handle_approval_action() 处理。有一个防重复点击 机制:_approval_resolved 字典跟踪已处理的 message_ts,防止用户多次点击导致重复审批。

飞书 / Lark:交互卡片

飞书适配器(gateway/platforms/feishu.py)用交互卡片(Interactive Card):

复制代码
card = {
    "header": {"title": {"content": "⚠️ Command Approval Required"}, "template": "orange"},
    "elements": [
        {"tag": "markdown", "content": f"```\n{cmd_preview}\n```\n**Reason:** {description}"},
        {"tag": "action", "actions": [
            _btn("✅ Allow Once", "approve_once", "primary"),
            _btn("✅ Session", "approve_session"),
            _btn("✅ Always", "approve_always"),
            _btn("❌ Deny", "deny", "danger"),
        ]},
    ],
}

其他平台:显式文本命令

对于不支持按钮交互的平台(SMS、Email、部分 Matrix 客户端),Gateway 退回到显式文本命令 模式:用户需要回复 /approve/deny,而不是随手回一个 yes。这是一个刻意的设计:gateway/run.py 明确禁止 bare-text matching,避免普通对话里的"yes"误触发危险命令放行。

如果要做更细粒度的授权,Gateway 这条链路还支持 /approve session/approve always 这样的 scope 变体。

统一的超时机制 :审批统一是 fail-closed(超时即拒绝),但默认值分两档。CLI 走 approval.timeout,默认 60 秒;Gateway 走 approvals.gateway_timeout,默认 300 秒,并且在等待期间会持续刷新活跃心跳,避免长时间等用户点击按钮时被会话超时 watchdog 误杀。


MCP 集成的安全考量

MCP(Model Context Protocol)让 Agent 能连接外部工具服务器------GitHub、数据库、文件系统等等。这极大扩展了能力,但也引入了两类安全风险:认证安全供应链安全

OAuth 2.1 PKCE:认证安全

tools/mcp_oauth.py 实现了完整的 OAuth 2.1 Authorization Code + PKCE 流程。

为什么是 PKCE? 传统的 OAuth Authorization Code 流程依赖 client_secret 来换 token。但 Hermes Agent 是一个本地 CLI 工具------client_secret 不可能安全地嵌入到用户的机器上。PKCE(Proof Key for Code Exchange)用一次性的 code_verifier / code_challenge 对替代 client_secret,让公共客户端也能安全地做 OAuth。

关键实现(mcp_oauth.py:420-435):

复制代码
metadata_kwargs = {
    "client_name": "Hermes Agent",
    "redirect_uris": [f"http://127.0.0.1:{port}/callback"],
    "grant_types": ["authorization_code", "refresh_token"],
    "response_types": ["code"],
    "token_endpoint_auth_method": "none",  # PKCE 不需要 client_secret
}
if cfg.get("client_secret"):
    metadata_kwargs["token_endpoint_auth_method"] = "client_secret_post"

本地回调服务器 :OAuth 的 redirect_uri 是 http://127.0.0.1:{port}/callback------一个临时启动的本地 HTTP 服务器(mcp_oauth.py:242-278)。端口通过 socket.bind(0) 自动选取空闲端口。回调处理完成后服务器立即关闭。

硬编码 localhost------redirect_uri 不允许指向外部地址。这防止了恶意 MCP 服务器通过篡改 redirect_uri 把 OAuth code 外泄到攻击者的服务器。

Token 持久化 :Token 存储在 $HERMES_HOME/mcp-tokens/<server_name>.json,权限 0600。MCP SDK 的 OAuthClientProvider 自动处理 token 刷新------过期时用 refresh_token 静默续期,不需要用户重新授权。

OSV 恶意软件扫描:供应链安全

MCP 服务器通常通过 npxuvx 安装运行。tools/osv_check.py 在启动 MCP 服务器之前,检查包是否有已知的恶意软件通报

复制代码
def check_package_for_malware(command: str, args: list) -> Optional[str]:
    ecosystem = _infer_ecosystem(command)  # npx → "npm", uvx → "PyPI"
    if not ecosystem:
        return None

    package, version = _parse_package_from_args(args, ecosystem)
    if not package:
        return None

    malware = _query_osv(package, ecosystem, version)
    if malware:
        ids = ", ".join(m["id"] for m in malware[:3])
        return f"BLOCKED: Package '{package}' ({ecosystem}) has known malware advisories: {ids}"
    return None

关键设计:

1. 只拦截 MAL- 通报osv_check.py:155)------不拦截普通的 CVE。为什么?因为 CVE 是"已知漏洞",可能只影响特定使用场景;MAL-* 是"确认的恶意软件"------包本身就是恶意的,无论怎么用都不安全。

2. API 是免费的 ------Google 维护的 OSV API(https://api.osv.dev/v1/query\),无需认证,延迟约 300ms。

3. Fail-open------网络错误、超时、解析失败都允许包继续安装。安全检查不应该因为网络问题而阻止正常工作。


散布在代码库各处的安全防御

除了集中的安全模块,Hermes Agent 在代码库的各个角落还有四类防御。

SSRF 防护

tools/url_safety.py 防止 Agent 被诱导访问内部网络地址(云元数据端点、localhost 服务等)。

复制代码
_BLOCKED_HOSTNAMES = frozenset({
    "metadata.google.internal",
    "metadata.goog",
})

_CGNAT_NETWORK = ipaddress.ip_network("100.64.0.0/10")

def _is_blocked_ip(ip) -> bool:
    if ip.is_private or ip.is_loopback or ip.is_link_local or ip.is_reserved:
        return True
    if ip.is_multicast or ip.is_unspecified:
        return True
    if ip in _CGNAT_NETWORK:  # Tailscale/WireGuard/云内部
        return True
    return False

检查流程:解析 URL → DNS 解析得到 IP → 检查 IP 是否在私有/保留/CGNAT 范围内。

已知局限url_safety.py:8-16 的文档注释明确说明):

  • DNS rebinding(TOCTOU) :攻击者控制的 DNS 在安全检查时返回公网 IP,实际连接时返回私有 IP。这个问题在 pre-flight 级别无法彻底修复;源码注释给出的方向是连接级校验库,或独立的 egress proxy。

  • 重定向绕过 :这部分有现实缓解。httpx event hook 会在重定向时重新检查目标地址,因此可以拦住"先跳公网、再跳内网"的 redirect-based bypass。

时序攻击防护

多个文件使用 hmac.compare_digest()常数时间比较------防止攻击者通过响应时间差推断签名/Token 的正确前缀:

  • hermes_cli/web_server.py(Web 面板 auth token)

  • gateway/platforms/webhook.py(GitHub/GitLab webhook 签名)

  • gateway/platforms/feishu.py(飞书 webhook HMAC-SHA256 签名)

  • gateway/platforms/api_server.py(API Key 验证)

Tar 遍历防护

tools/tirith_security.py:348-356 在解压 Tirith 二进制时做路径安全检查:

复制代码
with tarfile.open(archive_path, "r:gz") as tar:
    for member in tar.getmembers():
        if member.name == "tirith" or member.name.endswith("/tirith"):
            if ".." in member.name:  # 路径穿越检查
                continue
            member.name = "tirith"  # 归一化为安全名字
            tar.extract(member, tmpdir)
            break

三层防护:白名单文件名 → 拒绝 .. 组件 → 归一化文件名 → 解压到隔离临时目录。

凭证泄露防护

agent/redact.py 实现了 57+ 条凭证脱敏正则,覆盖:

类别 示例模式
API Key 前缀 sk-ghp_AIzaAKIAsk-ant-
环境变量赋值 OPENAI_API_KEY=sk-abc...
JSON 字段 "apiKey": "value"
Authorization 头 Bearer sk-...
Telegram Bot Token <digits>:<token>
私钥块 -----BEGIN RSA PRIVATE KEY-----
数据库连接串 postgres://user:PASSWORD@host
JWT eyJ...
Discord 提及 <@snowflake_id>
电话号码 E.164 格式 +1234567890

脱敏策略redact.py:117-121):

复制代码
def _mask_token(token: str) -> str:
    if len(token) < 18:
        return "***"
    return f"{token[:6]}...{token[-4:]}"  # 保留前 6 + 后 4,可调试

脱敏应用在三个出口:终端工具输出、日志格式化器、平台适配器的外发日志。


YOLO 模式的精确边界

最后给 YOLO 模式画一条清晰的线。

三种激活方式

方式 作用域 存储
hermes --yolo 进程级(CLI 进程结束即失效) os.environ["HERMES_YOLO_MODE"]
/yolo 命令 Session 级(toggle 开关) _session_yolo 集合
HERMES_YOLO_MODE=1 环境变量级 进程环境

/yolo 是 toggle ------第一次开,第二次关。空字符串 HERMES_YOLO_MODE="" 不会激活(空字符串是 falsy)。

绕过了什么

复制代码
# approval.py:602-603
if os.getenv("HERMES_YOLO_MODE") or is_current_session_yolo_enabled():
    return {"approved": True, "message": None}

这段代码在 check_all_command_guards()最前面------意味着 YOLO 直接跳过整个函数,包括:

  • 危险命令正则匹配

  • Tirith 外部扫描

  • Smart LLM 审批

  • 所有审批提示

没绕过什么

安全机制 YOLO 下是否仍生效 原因
容器隔离(Docker/Singularity/Modal) 独立的执行环境层
MCP 凭证过滤 环境变量白名单独立于审批
MCP OAuth 认证 认证流程不经过审批
OSV 恶意软件扫描 独立的供应链检查
URL 安全(SSRF) 独立的网络层检查
上下文文件扫描 独立的 prompt 注入检测
输入净化(workdir) 独立的参数校验
凭证脱敏 独立的输出过滤
IterationBudget 独立的资源限制
技能安全扫描 独立的文件安全检查

YOLO 只关闭第 2 层这整条 pre-exec 命令审批链。 具体说,被跳过的是 check_all_command_guards() 里的 Tirith 外部扫描 + 危险命令正则 + Smart LLM 审批 + 人工审批。其余 6 层,以及不依赖这条审批链的独立补充机制,仍然生效。

这是一个正确的设计:YOLO 的语义是"我信任 Agent 执行的命令",不是"我信任一切"。 你信任命令不代表你信任 MCP 服务器的供应链、不代表你信任项目文件没有注入、不代表你信任 Agent 不会被诱导访问内部网络。

相关推荐
AIFQuant1 小时前
量化私募回测系统:高质量股票/外汇历史数据 API 选型与接入
python·websocket·金融·ai量化
feiwuw1 小时前
氛围编程是什么?以及为什么它又分出了“规划”和“严肃”模式
人工智能·氛围编程
Mr.Daozhi1 小时前
Playwright实战:抓取Meta Ad Library动态页面的三级降级策略
爬虫·python·自动化·playwright·meta广告
是烨笙啊1 小时前
如何获取 dify-deploy skill 所需要的三个key值
人工智能·ai编程·dify
龙腾AI白云1 小时前
智能体+大模型=新生产力
人工智能·plotly·知识图谱
智塑未来1 小时前
AI耳机哪个牌子好?EARWEISS听智慧凭硬核技术脱颖而出
人工智能
辣香牛肉面1 小时前
Stable Diffusion本地部署教程及模型包
人工智能
升鲜宝供应链及收银系统源代码服务1 小时前
升鲜宝AI助手 E-R 图与操作说明书(三)---升鲜宝生鲜配送供应链管理系统源代码服务
大数据·人工智能·机器学习·生鲜供应链源代码·供应链源代码出售·生鲜配送源代码服务·门店连锁系统源代码
财经资讯数据_灵砚智能1 小时前
基于全球经济类多源新闻的NLP情感分析与数据可视化(日间)2026年6月5日
大数据·人工智能·python·ai·信息可视化·自然语言处理·灵砚智能