Clipping 微信公众号

权限审批体系:三级授权模型怎么把 Agent 的执行

by Jameszyh 原文 ↗
Created: 2026-07-01

公众号名称:James的成长日记

作者名称:Jameszyh

发布时间:2026-07-01 17:57

大家好,我是 James。

上一篇我们聊了迭代预算——IterationBudget 用几十行代码定义一个“每轮最多试多少次”的上限,防止子 Agent 无限循环、无限消耗、无限膨胀上下文。那篇文章解决的是“Agent 可以在预算内探索,但超出预算就必须停”。

但迭代预算覆盖的是“执行多少次”的问题,没有覆盖一个更根本的威胁:到底什么操作根本不该给 Agent 执行?

换句话说,上一层的防线是 how many times,这一层的防线是 what exactly

今天这篇,就来讲 Hermes 在安全架构上最重的一块砖:权限审批体系——危险操作为什么必须等人拍板,三级授权模型怎么把 Agent 的执行欲望关进笼子里。

本文使用版本:Hermes Agent 仓库 2026-06,核心文件:tools/approval.py(~1300行)、agent/file_safety.pyagent/tool_guardrails.pyagent/shell_hooks.pyacp_adapter/permissions.pyacp_adapter/edit_approval.py


一、为什么 Agent 需要自己的权限系统:从信任到零信任

写一个会让 AI Agent 帮你执行 shell 命令的系统,你首先面对的不是怎么让模型生成对的命令,而是怎么防止它生成毁灭性的命令。

这不是危言耸听。任何一个可以执行终端命令的 Agent,内部都有一个“指令执行引擎”在跑。LLM 没有善恶判断——它只在给定上下文中优化下一个 token。如果上下文里写着“清理项目目录”,模型完全可能在文件系统上输出一个 rm -rf /——不是因为“恶意”,而是因为它不理解 / 和你项目目录 /project 的区别。

所以 Hermes 在工具层之前建的第一道防线,不是“过滤”,不是“审查”,而是明文写死一个 无条件硬阻断清单(Hardline Blocklist)

HARDLINE_PATTERNS = [ (r'\brm\s+...\s*(/|/\*|/ \*)', "recursive delete of root"), (r'\bmkfs\b', "format filesystem"), (r'\bdd\b.*\bof=/dev/(sd|nvme...)', "dd to raw block device"), (r':\(\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;\s*:', "fork bomb"), (r'\bkill\s+...\s*-1\b', "kill all processes"), (_CMDPOS + r'(shutdown|reboot)', "system shutdown/reboot"),]

为什么这些命令“连 YOLO 都绕不开”?因为它们的共同特征是没有恢复路径:

  • rm -rf / → 整个根文件系统没了

  • mkfs → 磁盘格式化

  • dd of=/dev/sda → 裸设备覆写

  • fork bomb → 系统直接炸

  • shutdown/reboot → 服务器下线

Hermes 的注释直接写清楚了这个设计哲学:

“Opting into yolo is trusting the agent with your files and services, not trusting it to wipe the disk or power the box off.”

翻译:YOLO 信任的是 Agent 改你的文件和重启你的服务,不是让它把你的磁盘格式化了。

硬阻断的设计还做了几个精细的工程处理:

  1. 命令位置锚定:用 _CMDPOS 正则锚定“命令起始位置”,防止 echo rebootgrep 'shutdown' 误触

  2. sudo 等前缀统一归一化sudo shutdownenv VAR=1 shutdownnohup shutdown 都在检测范围内

  3. Unicode 归一化_normalize_command_for_detection() 做 ANSI 转义清洗、全角归一化、反斜杠转义清洗——防止 LLM 用花式写法规避检测

  4. 编译期预构建HARDLINE_PATTERNS_COMPILED 在模块导入时一次性编译所有正则,避免每次 terminal() 调用时重编译

这些细节加起来,构成了一个“你不可能绕过”的地板。


二、第二级防线:47种危险模式检测 + 三种审批粒度

硬阻断守住底线后,第二级防线面向更广泛的危险操作。DANGEROUS_PATTERNS 匹配 ~47 种危险模式,分为几大类:

文件系统危险操作:

  • 递归删除(rm -rf

  • chmod 777 / 666——全局可写

  • chown -R root——回收所有权

  • find -delete / find -exec rm——批量删除

数据库危险操作:

  • DROP TABLE / DROP DATABASE

  • DELETE FROM(无 WHERE 限制)

  • TRUNCATE

系统操作:

  • systemctl stop / restart / disable

  • docker ... restart / stop / kill

  • hermes gateway stop——干掉自己的网关进程

配置篡改:

  • tee / > / >> 覆写系统配置文件(/etc/~/.ssh/、shell rc)

  • sed -i / perl -i / ruby -i 原地编辑配置文件

  • cp / mv 替换敏感文件

代码注入:

  • curl | sh / wget -O- | bash——管道远程内容到 shell

  • bash -c / python -e / perl -e——脚本执行

  • bash < <(curl ...)——进程替换注入

当一条命令匹配了这些模式,Hermes 不是直接拒绝,而是进入审批流程。审批粒度有三种:

选项含义示例场景
once仅这次通过明知这条 rm -rf ./node_modules 安全,只是一次性操作
session整个会话不再追问正在做清理工作,反复用 rm -rf 是正常的
always永久写入 command_allowlistgit clean -fd 在你的工作流里永远是安全的
deny拒绝这不是你想要的,Agent 做错了

always 选项持久化到 config.yamlcommand_allowlist 中,下次会话会自动加载,不再弹出审批提示。这个设计直接解决了“审批疲劳”问题——你不会因为同一个合法操作被反复打断。


三、审批模式的三级进化:Manual → Smart → YOLO

审批粒度是回答“通过多久”,审批模式是回答“谁来审”。

Hermes 支持三种审批模式,形成一个从“人审”到“机审”到“不审”的渐进式降级:

Manual(默认模式):人必须在场

choice = prompt_dangerous_approval(command, description, approval_callback=approval_callback)

CLI 模式下弹出一个交互式文本提示:

⚠️ This command is potentially dangerous (recursive delete) rm -rf ./dist/ [o]nce — allow this once [s]ession — allow for this session [a]lways — permanently allow [d]eny — block Your choice [o/s/a/d]:

Gateway 模式下通过消息队列把审批请求发到 Telegram/Discord/Slack 等渠道,用户用 /approve/deny 回复。/approve all 能一次性批准所有 pending 的审批请求。

这里有一个值得展开的工程细节:Gateway 审批是阻塞式的。_ApprovalEntrythreading.Event 实现线程阻塞,Agent 的执行线程在审批点挂起,等待用户回复。多个并发子 Agent 各自排队,FIFO 按顺序等待用户的决策。

Smart(智能模式):用另一个 LLM 来评估

def _smart_approve(command: str, description: str) -> str: prompt = f"""You are a security reviewer for an AI coding agent. Command: {command} Flagged reason: {description} Assess the ACTUAL risk of this command. Many flagged commands are false positives... APPROVE / DENY / ESCALATE""" response = call_llm(task="approval", ..., temperature=0, max_tokens=16) return answer # "approve" / "deny" / "escalate"

核心逻辑:很多被模式匹配到的命令其实是误报。比如 python -c "print('hello')" 被标记为“通过 -c 标志执行脚本”,但它完全无害。Smart 模式调一个辅助 LLM(temperature=0),对命令做二次评估:通过了就直接执行,不通就降级为 Manual 审批,不确定就“escalate”升级。

这个设计节省了大量人工审批时间——据 Hermes 项目内测,Smart 模式可以自动过滤约 60%-70% 的误报。

YOLO 模式:我信任,后果自负

设置 HERMES_YOLO_MODE=1 或输入 /yolo 后,第二级所有审批提示全部跳过。但注意 YOLO 不绕过硬阻断——格式化磁盘仍然是不可约的。

还有一个细节值得关注:Hermes 的 YOLO 标志在模块加载时就冻结了(_YOLO_MODE_FROZEN),不是每次调用时读环境变量。这是因为在同一个进程内,任何一个加载的 skill 都可以写入环境变量——如果每次调用都读 os.environ,一条恶意 prompt 注入就能瞬开 YOLO。


四、第三级防线:文件写保护的三层规则

权限审批覆盖的是“终端命令行”路径,但如果 Agent 可以通过 write_file 工具直接写入敏感文件呢?那就需要文件写保护(agent/file_safety.py)——它不是对命令做审批,而是对文件操作目标做拒绝。

规则 1:写拒绝列表

build_write_denied_paths() 列出绝对禁止写入的路径:

write_denied = { "~/.ssh/authorized_keys", # SSH 认证 "~/.ssh/id_rsa", # 私钥 "~/.hermes/config.yaml", # 安全策略 ← 关键! "~/.hermes/.env", # API 密钥 "/etc/sudoers", # sudo 权限 "/etc/passwd", # 系统账户 "/etc/shadow", # 密码哈希 "~/.bashrc", "~/.zshrc", # shell 启动文件 "~/.netrc", "~/.git-credentials", # 凭据文件}

其中 ~/.hermes/config.yaml 的写保护是最关键的一条——这个文件里存着 approvals.modeyolo 设置和 command_allowlist。如果 Agent 能直接编辑它,就可以自己把审批关了。Hermes 不仅终端命令检测覆盖了 sed -i ... config.yaml,文件工具的 write_file / patch 也拒绝写入它,双路径覆盖。

此外还有目录前缀拒绝:

denied_prefixes = ["~/.ssh/", "~/.aws/", "~/.gnupg/", "~/.kube/", "/etc/sudoers.d/", "/etc/systemd/", "~/.docker/", "~/.azure/", "~/.config/gh/"]

规则 2:跨 Profile 写保护

Hermes 支持多 Profile(默认 profile、安全 profile、开发 profile 等),每个 Profile 有自己的 skills/plugins/cron/memories/ 目录。classify_cross_profile_target() 检测写入目标是否属于另一个 Profile:

def classify_cross_profile_target(path: str) -> Optional[dict]: # 如果路径打到 ~/.hermes/profiles/security/skills/... # 而当前活跃 profile 是 "default" # → 返回 {"active_profile": "default", "target_profile": "security", ...} return {"active_profile": ..., "target_profile": ..., "area": "skills"}

这不是硬阻断,而是“软护栏”——Agent 收到一个警告,说明它正在跨 profile 写入,需要用户明确确认。设计哲学是:防御纵深(defense-in-depth),不是安全边界。

规则 3:沙箱镜像写保护

当 Agent 在 Docker/Daytona 等非本地环境运行时,Hermes 的配置路径会被 bind mount 映射到类似这样的沙箱路径:

.../sandboxes///home/.hermes/config.yaml

如果 Agent 直接写这个沙箱内路径,写的是容器里的镜像,不会影响宿主机的实际配置——但宿主进程的配置缓存是按路径 key 来查找的,Agent 写了镜像后下次可能以为自己改成功了,实际上宿主机没变。

classify_sandbox_mirror_target() 检测这种“沙箱镜像写入”,给出明确警告:你写的是镜像,不是权威来源。


五、Shell Hooks:用户自定义的安全边界

硬阻断、危险模式检测、文件写保护——这三层都是 Hermes 内置的。但不同用户有不同的安全需求,比如:

  • 某个用户的公司内部规定“Agent 不能执行任何包含 kubectl delete 的命令”

  • 另一个用户开发金融系统,“所有包含 SQL 语句的命令都要审批”

  • 还有用户希望“所有 pip install 操作写入审计日志”

Hermes 给出了一个可插拔的解决方案:Shell Hooks(agent/shell_hooks.py

工作流如下:

cli-config.yaml: hooks: pre_tool_call: terminal: - ~/scripts/security-check.py - ~/scripts/audit-logger.sh

每次 Agent 准备调用终端工具前,Hermes 的插件管理器会把命令上下文以 JSON 格式通过 stdin 传给用户的脚本:

{ "hook_event_name": "pre_tool_call", "tool_name": "terminal", "tool_input": {"command": "kubectl delete pod my-pod"}, "session_id": "sess_abc123", "cwd": "/home/user/project"}

脚本检查后,输出 JSON 决定放行或阻止:

{"action": "block", "message": "kubectl delete is not allowed in this project"}

这个设计的妙处在于:(1) 脚本用 subprocess.run(shell=False) 调用,没有 shell 注入风险;(2) 支持首次使用前的用户确认(allowlist 机制);(3) 完全异步,不阻塞 Agent;(4) 脚本可以是任何语言——check.pyaudit.shdeno run security.ts——只要从 stdin 读 JSON、写 JSON 到 stdout 就行。

所有 hooks 通过已有的插件 invoke_hook() 调度,无需改动任何工具调用代码。


六、工具调用护栏:运行时的最后一道防线

前三层防线解决的是“什么不能做”。但还有一种纯执行故障:Agent 进入了死循环——反复调用同一个工具,每次都失败,每次失败都一模一样,却不知道停下来。

agent/tool_guardrails.py 处理的就是这种场景。它把工具分为两类:

幂等工具(Idempotent): read_fileweb_searchbrowser_snapshot 等——只读不写 变更工具(Mutating): terminalwrite_filepatchdelegate_task 等——会产生副作用

然后追踪三种失败模式:

模式检测逻辑默认配置
精确重复失败(Exact Failure)同一工具 + 相同入参 + 相同失败结果警告 ≥2次,硬停 ≥5次
同类工具失败(Same-Tool Failure)不同入参但同一个工具名警告 ≥3次,硬停 ≥8次
无进度循环(No Progress)幂等工具连续调用没有产生进展警告 ≥2次,硬停 ≥5次

实现上,ToolCallGuardrailController 对每个工具调用的入参做 hash,与上一轮的结果做 diff。exact_failure() 检测完全相同的失败,same_tool_failure() 检测同类工具反复失败,idempotent_no_progress() 检测“一直在读文件但什么都没改”的循环。

硬停模式下,guardrail 不会静默截断——它用 synthetic_tool_failure 机制注入一个合成工具结果,告诉 Agent 它的循环被截断了,并建议换个策略。


七、ACP 协议桥接:让标准 Agent 协议客户端也走审批

Hermes 原生支持 ACP(Agent Communication Protocol)——这是 Anthropic 等厂商推动的 Agent 标准协议。但 ACP 本身没有“危险命令审批”的概念,它的权限模型是一套抽象的 request_permission / AllowedOutcome / PermissionOption 消息。

acp_adapter/permissions.py 做的事是把 ACP 的权限语义映射到 Hermes 的审批体系:

ACP 选项Hermes 语义
allow_onceonce — 本次通过
allow_sessionsession — 整个会话内通过
allow_alwaysalways — 永久加入 allowlist
denydeny — 拒绝
deny_alwaysdeny — 拒绝(ACP 版本支持持久拒绝)

acp_adapter/edit_approval.py 更进一步——它处理 write_file / patch 的“前置审批”:在文件被写入之前,把修改提案(old_text / new_text diff)传给 ACP 客户端,让用户看到“Agent 打算怎么改这个文件”。

这和终端审批是配合关系:终端审批管“命令路径”,编辑审批管“文件工具路径”。两条路径覆盖了 Agent 修改文件系统的所有入口。


八、架构层控制回路图:安全不是最上层的壳,是内建基因

把三层四套机制放在一张图里看,会更清楚它们的分工:

┌──────────────────────────────────────────────┐│ 第三级:工具调用护栏 (tool_guardrails.py) ││ → 运行时最后一道防线:检测死循环、重复失败 │├──────────────────────────────────────────────┤│ 第三级:文件写保护 (file_safety.py) ││ → 写拒绝 / 跨 Profile 保护 / 沙箱镜像保护 │├──────────────────────────────────────────────┤│ 第三级:Shell Hooks (shell_hooks.py) ││ → 用户自定义安全脚本审批 │├──────────────────────────────────────────────┤│ 第二级:危险命令审批 (approval.py DANGEROUS) ││ → 47种模式 + Manual/Smart/YOLO 三模式 ││ → 三种粒度:once/session/always │├──────────────────────────────────────────────┤│ 第一级:硬阻断 (approval.py HARDLINE) ││ → 12种灾难性命令,YOLO 也绕不开 │└──────────────────────────────────────────────┘

这张图的上下顺序不是偶然的:越往下越严格,越往上越灵活。

  • 第一级是地板——绝对不可突破

  • 第二级是护栏——可以审批通过,但不能完全跳过

  • 第三级是兜底——不针对具体命令,而是从文件、脚本、循环三个维度做互补保护

  • ACP 桥接层是横向的——让上面的防线在标准协议客户端上也生效

如果用一个词概括整个设计,那就是内建安全(security-by-design):审批不是事后加的一层壳,不是靠前端 UX 拦截,而是根植在工具调用引擎的每一段代码里。


总结

写完这个系统,我有几个关于 Agent 安全设计的体会想分享:

第一,安全是分层反对,不是一刀切。 如果把所有危险命令都列入硬阻断,用户会骂“根本没法用”;如果完全不拦,Agent 迟早出事。Hermes 的三级模型正好在两者之间:绝不允许的被硬墙挡住,需要注意的需人审批,自动化的由 Smart 模式过滤。这是一种精确制导,不是地毯式轰炸。

第二,审批状态必须持久化。 session 和 always 两种粒度让审批记忆跨越时间和会话,这才是真正缓解审批疲劳的方法。否则用户每天都要回答“你确定要执行这条递归删除吗”,迟早会把审批模式调成 off。

第三,安全策略本身就是安全对象。 ~/.hermes/config.yaml 是唯一存放审批模式、YOLO 开关和 allowlist 的文件。如果 Agent 能编辑它,所有防线都会瓦解。Hermes 对它的保护是双路径的:终端命令检测 + 文件写拒绝。这个闭环如果没有闭合,整个体系就是纸房子。

第四,护栏不是惩罚,是教育。 Smart 模式调另一个 LLM 来审这件事,说明 Hermes 的设计者理解:该被拦住的只有真正危险的操作,非危险的不应该让用户操心。护栏的目的是让 Agent 在安全边界内自由,不是把 Agent 绑起来。

到此为止,“隔离与协作”板块我们已经讲了委托(第22篇)、预算(第23篇)、审批(本篇)。下一篇我们把最后一块拼图补上:检查点与回滚(checkpoint_manager.py)——Hermes 如何用 git shadow store 做文件操作的“保险丝”,出错了也能一键恢复到上一步。


下一篇预告:检查点与回滚——git shadow store 怎么成为 Agent 文件操作的保险丝

子 Agent 可以委托了,预算可以限制了,危险操作也能审批了。但还有一个问题:就算所有防线都正常运作,Agent 仍然可能犯错——改错了文件、删错了目录、不小心覆盖了重要配置。下一篇我们看 Hermes 怎么在文件系统层面做检查点——每次写操作前自动创建 git stash,出错后支持一键回滚到上一个安全节点。最终让「隔离与协作」板块形成完整闭环:委托 → 预算 → 审批 → 回滚。


关注我,James 的成长日记,持续分享干货,帮你在 AI 时代少走弯路。


内容效果不满意?点此反馈

输入关键词开始搜索