本节摘要:能执行任意命令的 agent 必须自带保险丝。Hermes 的防线是三件套:①approval(5714 行)——危险命令审批:
HARDLINE_PATTERNS(12 条硬线,rm -rf //mkfs/dd 裸设备/fork 炸弹/关机重启,不可绕过)与DANGEROUS_PATTERNS(47 条,递归删除/权限放宽/管道执行/Windows 破坏族,可经用户审批放行),detect_dangerous_command双层匹配后由 approval_callback 走人工确认;②path_security——validate_within_dir用resolve()+relative_to()把技能/凭证等敏感目录锁进边界,拒绝..穿越;③threat_patterns——上下文窗口安全扫描,按 all/context/strict 三档作用域检测提示注入/C2/外传模式与隐形 Unicode。之后精读四件代表工具:terminal(命令执行+审批挂点+输出处理)、browser(五后端)、computer_use(cua-driver 桌面控制)、delegate_task(委派 subagent 的唯一入口)。
内容来源:原项目源码
tools/approval.py:515-568(硬线)/:909-1000(危险模式)/:2505-2531(检测入口)、tools/path_security.py(全文)、tools/threat_patterns.py:63-135/207-255、tools/terminal_tool.py:2743/3151-3198/4089(工具与注册)、tools/computer_use_tool.py:20-33、tools/delegate_tool.py:4767-4876(schema)。
⚠️ 注意:审批不是开关而是分层判定:硬线命中直接拒绝(连 yolo 模式都不放行),危险线命中进入审批流(CLI 弹确认/网关走 ask 往返),白名单/会话记忆可免重复确认。本节讲检测与拦截的源码;
pending_approval状态如何在网关侧完成人机往返,留到第 7 章。
阅读完本节,你应当能够:
_CMDPOS)与引号掩码为何是"引号不是逃生舱"的关键。detect_dangerous_command 的四步流程。relative_to 的判定原理。tools/approval.py 开头 12 条硬线(节选):
515 HARDLINE_PATTERNS = [ 532 (_RM_FLAG_PREFIX + _hardline_rm_path(r'/(?:(?:\.\.?)?/)*(?:\.\.?)?\**|/ \*'), "recursive delete of root filesystem"), 533 (_RM_FLAG_PREFIX + _hardline_rm_path(_HARDLINE_SYSTEM_DIRS), "recursive delete of system directory"), 534 (_RM_FLAG_PREFIX + _hardline_rm_path(r'(?:~|\$\{?HOME\}?)(?:/?|/\*)?'), "recursive delete of home directory"), 538 (_CMDPOS + r'mkfs(\.[a-z0-9]+)?\b', "format filesystem (mkfs)"), 544 (_CMDPOS + r'dd\b[^\n]*\bof=/dev/(sd|nvme|hd|mmcblk|vd|xvd)[a-z0-9]*', "dd to raw block device"), 552 (r'>\s*/dev/(sd|nvme|hd|mmcblk|vd|xvd)[a-z0-9]*\b', "redirect to raw block device"), 556 (r':\(\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;\s*:', "fork bomb"), 564 (_CMDPOS + r'(shutdown|reboot|halt|poweroff)\b', "system shutdown/reboot"), 568 ]
硬线的语义是无条件地板:rm -rf / 的路径正则连 //、/.、/.. 这些"shell 里塌缩回根"的写法都覆盖,${HOME} 花括号与引号变体也由专门的 _hardline_rm_path 处理(519-531 行注释逐条解释)。47 条危险线(909 行起)覆盖更广:递归删除的三种拼写(含 GNU rm 允许 flags 后置的 rm build/ -rf,源码注明移植自 openai/codex 的修复)、chmod 777/chown root、curl | sh、Windows 全家桶(cmd/powershell 破坏性删除、taskkill /F、vssadmin 删卷影、bcdedit 改启动项——注释点名这些从 git-bash 后端也能触达)。防误报同样精密:_CMDPOS 把检测锚定到命令位置(行首/分隔符后/sudo 之后),让 git commit -m "never dd of=/dev/sda" 这类引号里的散文不触发;没有命令名锚点的规则(重定向/fork 炸弹)改用引号掩码变体匹配,而 sh -c/bash -c/eval 这类"引号即代码"的载体(:603 的 _SHELL_CARRIER_NAMES)会剥开引号扫描原始载荷——引号不是逃生舱。检测入口 :2505:
2505 def detect_dangerous_command(command: str) -> tuple: 2511 if _command_parser_limit_exceeded(command): 2512 return (True, _PARSER_LIMIT_DESCRIPTION, _PARSER_LIMIT_DESCRIPTION) # 解析超限视为危险 2513 if _is_verification_artifact_cleanup(command): 2514 return (False, None, None) # 已知良性清理豁免 2516 for command_variant in _command_detection_variants(command): # 变体(反混淆/小写) 2518 for pattern_re, description in DANGEROUS_PATTERNS_COMPILED: 2519 if pattern_re.search(command_lower): 2520 return (True, pattern_key, description) 2522 normalized = _normalize_command_for_detection(command) 2523 for description, _ in _execution_flag_findings(normalized): # 执行标志语义分析 2524 return (True, description, description)
四步:解析器超限兜底(fail closed)→ 良性豁免 → 模式匹配(含混淆变体展开)→ 语义级执行标志分析。命中后的状态机(:2538-2542 的 _pending/_session_approved/_session_yolo/_permanent_approved)支持逐命令/逐会话/永久三级放行,还有"smart approval"自动审批某些低危命中;而 human-wait 记账(:2562 起)把"等人类确认"的墙钟时间从并发工具批的截止时间中剔除——慢用户不会把批拖到超时。
path_security(全文 43 行,小而锋利):
15 def validate_within_dir(path: Path, root: Path) -> Optional[str]: 22 Usage:: 24 error = validate_within_dir(user_path, allowed_root) 25 if error: 26 return tool_error(error) 28 try: 29 resolved = path.resolve() 30 root_resolved = root.resolve() 31 resolved.relative_to(root_resolved) 32 except (ValueError, OSError) as exc: 33 return f"Path escapes allowed directory: {exc}" 34 return None 37 def has_traversal_component(path_str: str) -> bool: 42 parts = Path(path_str).parts 43 return ".." in parts
resolve() 先把符号链接与 .. 归一成真实路径,再要求结果必须落在 root 的 relative_to 之下——技能目录、凭证文件等敏感区用它防穿越。threat_patterns 管的是另一张口:上下文窗口。模式库按攻击类组织,每条是 (regex, pattern_id, scope),三档作用域(:63 起):all(经典注入+外传,任何文本都扫)、context(加 C2/角色劫持,扫上下文文件/记忆/工具结果)、strict(加持久化/SSH 后门/硬编码密钥,只扫记忆写入与技能安装这类用户可介入的路径)。扫描函数 scan_for_threats(:207)有三道工艺:输入截断 64KB 防灾难性回溯;隐形 Unicode 单趟集合交集(零宽字符/双向控制符,17 个码点,报出具体码点号);NFKC 归一化后再跑正则——全角 cat 折叠成 cat,同形字绕过失效(注释同时坦承:跨脚本同形字如西里尔 а 需要 TR#39 混淆库,当前未防)。作用域哲学写在文件头:"工具结果里有网页/GitHub issue——内容不是用户写的,要宽泛检测;但拦截只留给用户能介入的路径。"检测是全域的,阻断是审慎的。
tools/terminal_tool.py 的 terminal_tool(:2743)签名即语义:command/background/timeout/workdir/pty/notify_on_complete/watch_patterns——注意 force 参数注明"internal only, not exposed to model API":模型永远不能自救绕过审批,只有人类确认后的重放才带 force。执行前的审批挂点(:3151-3198):
3151 # Pre-exec security checks (tirith + dangerous command detection) 3158 _approved_run = bool(force) 3159 if not force: 3160 approval = _check_all_guards( 3161 command, env_type, 3162 has_host_access=_docker_has_host_access(config), 3163 ) 3164 if not approval["approved"]: 3165 if approval.get("status") == "pending_approval": 3167 return json.dumps({ ... "status": "pending_approval", 3172 "approval_pending": True, 3175 "pattern_key": approval.get("pattern_key", ""), ...}) 3179 # Command was blocked 3185 return json.dumps({ ... "error": approval.get("message", fallback_msg), 3189 "status": "blocked" }, ensure_ascii=False) 3192 if approval.get("user_approved"): 3194 approval_note = f"Command required approval ({desc}) and was approved by the user."
被标记的命令返回 pending_approval 状态(网关据此发起人机往返),被拒则返回 blocked+原因——拒绝本身也是给模型的观察,它可以换写法。审批之前还有自仓库保护(命令瞄准 Hermes 自己的代码树时拒绝,:3110)与危险 workdir 拦截;审批之后是 PTY 决策(gh auth login --with-token 这类要管道 stdin 的命令强制关 pty)与后台进程注册(process_registry 管理、notify_on_complete 恰好一次的退出通知、watch_patterns 的 15 秒限速三振出局)。命令最终经 env.execute() 落到 6 种后端之一(local/docker/ssh/modal/daytona/singularity/vercel_sandbox,tools/environments/ 一后端一文件)。注册处(:4089)还带 max_result_size_chars=100_000——输出截断这道最后的防爆闸。
computer_use 的注册(tools/computer_use_tool.py:20)是"shim 模式"的样板——真实实现在 tools/computer_use/ 包,顶层文件只为被 AST 发现:
1 """Shim for tool discovery. Registers `computer_use` with tools.registry. 3 The real implementation lives in the `tools/computer_use/` package to keep 4 the file structure clean. This shim exists because tools.registry auto-imports 5 `tools/*.py` — we need a top-level module to trigger the registration. 6 """ 20 registry.register( 21 name="computer_use", 22 toolset="computer_use", 23 schema=COMPUTER_USE_SCHEMA, 24 handler=lambda args, **kw: handle_computer_use(args, **kw), 25 check_fn=check_computer_use_requirements, 27 description=( 28 "Universal desktop control via cua-driver (macOS, Windows, Linux). Works with any " 29 "tool-capable model (Anthropic, OpenAI, OpenRouter, local vLLM, " 30 "etc.). Background computer-use: does NOT steal the user's cursor " 31 "or keyboard focus."), 32 )
要点全在 description 的两个承诺:任何能调工具的模型可用(屏幕操作不绑定特定厂商的 computer-use 协议),以及后台桌面控制不抢用户键鼠焦点——这是与抢焦点方案的本质差异。browser 工具同理走无障碍树,五后端(Browser Use 云/Browserbase 云/本地 Chromium/Camofox/CDP)自动探测配置与凭证,对 agent 面行为一致,云后端还有 SSRF 防护、本地后端豁免。delegate_task 是多 Agent 的唯一闸口(tools/delegate_tool.py:4767 的 schema):goal 与 context 字段强调"自包含——subagent 对你的会话历史一无所知";tasks 数组支持扇出(运行时上限 delegation.max_concurrent_children 默认 3,由上一节的 dynamic_schema_overrides 动态写进 description);output_schema 让子代理的最终答案过 JSON Schema 校验,失败还有一次有界纠错重试;action 枚举 spawn/list/steer/stop 提供对运行中子代理的活体编排(steer 排队纠偏文本而不停机)。而 run_agent 侧的 _dispatch_delegate_task(run_agent.py:8367-8400)补了一条铁律:顶层模型的委派一律后台化,只有编排型子代理(depth>0)的委派保持同步——它要在自己的轮次内拿到工人结果做汇总。schema 层的 background 参数被刻意忽略,"模型不掌握自己的执行模式"是第 8 章的开场白。
多层防护至此成环:toolset 裁剪(面)+check_fn 探针(可见性)+agent 级工具拦截(路径)+approval(命令)+path_security(文件)+threat_patterns(上下文)+max_result_size(输出)+独立终端后端(爆炸半径)。
💡 循环要点:Hermes 的工具安全不是一道墙,而是纵深防御的洋葱——而且每一层都给模型留了"体面的退路":被拒的命令带着原因返回(模型可改写),被拦的内容变成警告(用户可裁决),被标的需要审批就进入人机往返。安全的最高目标是让 agent 既敢干活又不失控,而非把它捆死。同时记住审批的哲学分层:硬线对所有人关闭(包括 yolo),危险线向人类开门——机器不做最终裁量人,人做。
_CMDPOS 命令位置锚定(引号里的散文不触发)、引号掩码变体、shell 载体(eval/sh -c)剥引号扫原始载荷——引号不是逃生舱。detect_dangerous_command 四步:解析超限 fail closed→良性豁免→变体模式匹配→执行标志语义分析;放行有逐命令/会话/永久三级。resolve()+relative_to() 锁边界防符号链接与 .. 穿越,用于技能/凭证目录。行动器官解剖完毕。下一章进入全书高潮——外循环★:技能如何从经验中诞生、被 curator 策展、在使用中改进,最终让这双手越用越巧。