权限审批体系:三级授权模型怎么把 Agent 的执行
公众号名称: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.py、agent/tool_guardrails.py、agent/shell_hooks.py、acp_adapter/permissions.py、acp_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 改你的文件和重启你的服务,不是让它把你的磁盘格式化了。
硬阻断的设计还做了几个精细的工程处理:
-
命令位置锚定:用
_CMDPOS正则锚定“命令起始位置”,防止echo reboot或grep 'shutdown'误触 -
sudo 等前缀统一归一化:
sudo shutdown、env VAR=1 shutdown、nohup shutdown都在检测范围内 -
Unicode 归一化:
_normalize_command_for_detection()做 ANSI 转义清洗、全角归一化、反斜杠转义清洗——防止 LLM 用花式写法规避检测 -
编译期预构建:
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_allowlist | git clean -fd 在你的工作流里永远是安全的 |
| deny | 拒绝 | 这不是你想要的,Agent 做错了 |
always 选项持久化到 config.yaml 的 command_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 审批是阻塞式的。_ApprovalEntry 用 threading.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.mode、yolo 设置和 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.py、audit.sh、deno run security.ts——只要从 stdin 读 JSON、写 JSON 到 stdout 就行。
所有 hooks 通过已有的插件 invoke_hook() 调度,无需改动任何工具调用代码。
六、工具调用护栏:运行时的最后一道防线

前三层防线解决的是“什么不能做”。但还有一种纯执行故障:Agent 进入了死循环——反复调用同一个工具,每次都失败,每次失败都一模一样,却不知道停下来。
agent/tool_guardrails.py 处理的就是这种场景。它把工具分为两类:
幂等工具(Idempotent): read_file、web_search、browser_snapshot 等——只读不写 变更工具(Mutating): terminal、write_file、patch、delegate_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_once | once — 本次通过 |
allow_session | session — 整个会话内通过 |
allow_always | always — 永久加入 allowlist |
deny | deny — 拒绝 |
deny_always | deny — 拒绝(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 时代少走弯路。
内容效果不满意?点此反馈