本文对Hermes的架构拆解是基于 v2026.7.30 版本的源码解析
一、从一个删除员工的请求说起
假设我们给 Hermes 添加了一个企业管理工具:
python
delete_employee(employee_id=10086)
用户在聊天窗口输入:
帮我删除员工 10086 的账号。
LLM 理解了用户意图,返回一个 Tool Call:
json
{
"name": "delete_employee",
"arguments": {
"employee_id": 10086
}
}
这里有一个很容易忽略的问题:模型知道"要删除谁",并不代表它就有权删除。
至少要弄清三件事:
- 参数是否正确?
employee_id是不是一个有效的员工编号? - 操作是否经过确认? 删除账号可能不可恢复,是否需要负责人批准?
- 调用者是否有权限? 当前用户能不能删除这个租户下的员工?
这三件事由不同环节负责。理解它们的分工,就理解了 Hermes Tool 安全机制的核心。
二、先看一张总流程图
Hermes 的工具调用从模型开始,但执行权并不在模型手里。
#mermaid-svg-jDyZ5ZBKciN7M922{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-jDyZ5ZBKciN7M922 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-jDyZ5ZBKciN7M922 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-jDyZ5ZBKciN7M922 .error-icon{fill:#552222;}#mermaid-svg-jDyZ5ZBKciN7M922 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-jDyZ5ZBKciN7M922 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-jDyZ5ZBKciN7M922 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-jDyZ5ZBKciN7M922 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-jDyZ5ZBKciN7M922 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-jDyZ5ZBKciN7M922 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-jDyZ5ZBKciN7M922 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-jDyZ5ZBKciN7M922 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-jDyZ5ZBKciN7M922 .marker.cross{stroke:#333333;}#mermaid-svg-jDyZ5ZBKciN7M922 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-jDyZ5ZBKciN7M922 p{margin:0;}#mermaid-svg-jDyZ5ZBKciN7M922 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-jDyZ5ZBKciN7M922 .cluster-label text{fill:#333;}#mermaid-svg-jDyZ5ZBKciN7M922 .cluster-label span{color:#333;}#mermaid-svg-jDyZ5ZBKciN7M922 .cluster-label span p{background-color:transparent;}#mermaid-svg-jDyZ5ZBKciN7M922 .label text,#mermaid-svg-jDyZ5ZBKciN7M922 span{fill:#333;color:#333;}#mermaid-svg-jDyZ5ZBKciN7M922 .node rect,#mermaid-svg-jDyZ5ZBKciN7M922 .node circle,#mermaid-svg-jDyZ5ZBKciN7M922 .node ellipse,#mermaid-svg-jDyZ5ZBKciN7M922 .node polygon,#mermaid-svg-jDyZ5ZBKciN7M922 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-jDyZ5ZBKciN7M922 .rough-node .label text,#mermaid-svg-jDyZ5ZBKciN7M922 .node .label text,#mermaid-svg-jDyZ5ZBKciN7M922 .image-shape .label,#mermaid-svg-jDyZ5ZBKciN7M922 .icon-shape .label{text-anchor:middle;}#mermaid-svg-jDyZ5ZBKciN7M922 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-jDyZ5ZBKciN7M922 .rough-node .label,#mermaid-svg-jDyZ5ZBKciN7M922 .node .label,#mermaid-svg-jDyZ5ZBKciN7M922 .image-shape .label,#mermaid-svg-jDyZ5ZBKciN7M922 .icon-shape .label{text-align:center;}#mermaid-svg-jDyZ5ZBKciN7M922 .node.clickable{cursor:pointer;}#mermaid-svg-jDyZ5ZBKciN7M922 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-jDyZ5ZBKciN7M922 .arrowheadPath{fill:#333333;}#mermaid-svg-jDyZ5ZBKciN7M922 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-jDyZ5ZBKciN7M922 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-jDyZ5ZBKciN7M922 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-jDyZ5ZBKciN7M922 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-jDyZ5ZBKciN7M922 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-jDyZ5ZBKciN7M922 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-jDyZ5ZBKciN7M922 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-jDyZ5ZBKciN7M922 .cluster text{fill:#333;}#mermaid-svg-jDyZ5ZBKciN7M922 .cluster span{color:#333;}#mermaid-svg-jDyZ5ZBKciN7M922 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-jDyZ5ZBKciN7M922 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-jDyZ5ZBKciN7M922 rect.text{fill:none;stroke-width:0;}#mermaid-svg-jDyZ5ZBKciN7M922 .icon-shape,#mermaid-svg-jDyZ5ZBKciN7M922 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-jDyZ5ZBKciN7M922 .icon-shape p,#mermaid-svg-jDyZ5ZBKciN7M922 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-jDyZ5ZBKciN7M922 .icon-shape .label rect,#mermaid-svg-jDyZ5ZBKciN7M922 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-jDyZ5ZBKciN7M922 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-jDyZ5ZBKciN7M922 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-jDyZ5ZBKciN7M922 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 直接拒绝
需要确认
否
是
继续执行
业务工具
Terminal
否
是
LLM 返回 Tool Call
AIAgent 接收调用
工具范围与参数处理
pre_tool_call 插件检查
插件决定
返回 Tool Error
Hermes 人工审批
审批通过?
分发到 Tool Handler
工具类型
业务身份与权限校验
终端命令安全检查
检查通过?
真正执行操作
Tool Result
写回 Messages,交给 LLM
注:这是用于理解职责的主流程。Hermes 实际还包含中间件、Agent Guardrails、不同工具执行路径等;Terminal 的检查发生在其 Handler 内,而不是所有 Tool 共享同一个命令审查器。
从这张图可以看出,安全机制主要分成三层:
| 层次 | 谁负责 | 回答什么问题 |
|---|---|---|
| 调用前检查 | Tool 范围限制、pre_tool_call |
是否拦截?是否要求人工确认? |
| 工具专属检查 | 例如 Terminal 的 approval.py |
这条 Shell 命令是否有风险? |
| 业务最终校验 | 自定义 Handler / 业务服务 | 当前操作者是否有权修改这个资源? |
其中最重要的原则是:人工审批不等于业务授权。 即使有人点击了"允许",也不能跳过数据库和权限系统中的真实校验。
三、普通 Tool 是怎么经过安全检查的?
仍然以 delete_employee(10086) 为例,我们顺着执行过程看。
3.1 第一步:处理工具名和参数
模型返回的其实只有两类信息:工具名,以及一个参数对象。
Hermes 会在工具执行链中处理这些参数。例如 model_tools.py 中的 coerce_tool_args() 会尝试把 "10086" 转成整数 10086。
但它不是完整的业务校验器:即使参数类型正确,也不能说明这个员工存在,更不能说明当前调用者有删除权限。
同样,Tool Schema 告诉模型如何填写参数,ToolRegistry.dispatch() 负责找到 Handler;这两个地方都不应被当成企业系统的最终权限边界。
3.2 第二步:执行前的 pre_tool_call Hook
Hermes 提供了一个适合扩展安全策略的插件 Hook:
python
pre_tool_call
这个 Hook 会在 Tool 真正执行之前收到工具名和参数。插件可以给出三种典型决定:
| 返回值 | 含义 | 接下来发生什么 |
|---|---|---|
None |
不干预 | 继续后续检查 |
{"action": "block", "message": "..."} |
直接拒绝 | 不调用 Handler,返回错误 |
{"action": "approve", "message": "..."} |
需要人工确认 | 进入 Hermes 已有的审批流程 |
例如:
python
def before_tool_call(tool_name, args, **kwargs):
if tool_name != "delete_employee":
return None
return {
"action": "approve",
"message": f"确认删除员工 {args.get('employee_id')} 吗?",
"rule_key": f"employee.delete:{args.get('employee_id')}"
}
这里的 approve 不是"自动批准",而是"发起人工审批"。
换句话说:自定义插件只负责提出审批要求,不需要自己实现 /approve 的等待队列。
3.3 第三步:用户同意后,才进入 Handler
假设用户批准了删除操作,Hermes 才继续把调用交给对应 Handler。
但这里还没结束。Handler 仍然要检查:
- 调用者的真实身份是什么?
- 员工 10086 是否属于调用者可以管理的租户和组织?
- 调用者是否拥有
employee.delete权限? - 目标员工是否受保护,或存在不能删除的业务状态?
只有这些检查都通过,业务服务才能真正修改数据。
为什么不能把这些判断都写进 pre_tool_call? 因为 Hook 只是 Agent 调用路径上的拦截点;真实业务 API 还可能被其他入口调用。权限必须在受信任的业务服务中统一执行,不能依赖某个 Agent 插件始终存在并正常运行。
3.4 第四步:把结果交还给 Agent Loop
工具执行成功,或者被拒绝,最终都要让模型知道发生了什么。
例如:
text
User:删除员工 10086
Assistant:tool_call delete_employee(10086)
Tool:审批未通过,操作未执行
Assistant:该删除操作没有获得批准,因此未执行。
四、Terminal 为什么还有一套危险命令检查?
普通业务 Tool 的接口一般是明确的,例如 delete_employee、send_email。但 terminal 不一样,它接收的是任意 Shell 字符串:
python
terminal(command="rm -rf build/")
命令实际会访问哪些文件、修改哪些系统状态,不能仅靠函数名 terminal 判断。因此 Hermes 为终端执行额外提供了命令安全检查。
4.1 终端检查的执行路径
#mermaid-svg-2X5aUo1c8wAeABb5{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-2X5aUo1c8wAeABb5 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-2X5aUo1c8wAeABb5 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-2X5aUo1c8wAeABb5 .error-icon{fill:#552222;}#mermaid-svg-2X5aUo1c8wAeABb5 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-2X5aUo1c8wAeABb5 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-2X5aUo1c8wAeABb5 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-2X5aUo1c8wAeABb5 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-2X5aUo1c8wAeABb5 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-2X5aUo1c8wAeABb5 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-2X5aUo1c8wAeABb5 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-2X5aUo1c8wAeABb5 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-2X5aUo1c8wAeABb5 .marker.cross{stroke:#333333;}#mermaid-svg-2X5aUo1c8wAeABb5 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-2X5aUo1c8wAeABb5 p{margin:0;}#mermaid-svg-2X5aUo1c8wAeABb5 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-2X5aUo1c8wAeABb5 .cluster-label text{fill:#333;}#mermaid-svg-2X5aUo1c8wAeABb5 .cluster-label span{color:#333;}#mermaid-svg-2X5aUo1c8wAeABb5 .cluster-label span p{background-color:transparent;}#mermaid-svg-2X5aUo1c8wAeABb5 .label text,#mermaid-svg-2X5aUo1c8wAeABb5 span{fill:#333;color:#333;}#mermaid-svg-2X5aUo1c8wAeABb5 .node rect,#mermaid-svg-2X5aUo1c8wAeABb5 .node circle,#mermaid-svg-2X5aUo1c8wAeABb5 .node ellipse,#mermaid-svg-2X5aUo1c8wAeABb5 .node polygon,#mermaid-svg-2X5aUo1c8wAeABb5 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-2X5aUo1c8wAeABb5 .rough-node .label text,#mermaid-svg-2X5aUo1c8wAeABb5 .node .label text,#mermaid-svg-2X5aUo1c8wAeABb5 .image-shape .label,#mermaid-svg-2X5aUo1c8wAeABb5 .icon-shape .label{text-anchor:middle;}#mermaid-svg-2X5aUo1c8wAeABb5 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-2X5aUo1c8wAeABb5 .rough-node .label,#mermaid-svg-2X5aUo1c8wAeABb5 .node .label,#mermaid-svg-2X5aUo1c8wAeABb5 .image-shape .label,#mermaid-svg-2X5aUo1c8wAeABb5 .icon-shape .label{text-align:center;}#mermaid-svg-2X5aUo1c8wAeABb5 .node.clickable{cursor:pointer;}#mermaid-svg-2X5aUo1c8wAeABb5 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-2X5aUo1c8wAeABb5 .arrowheadPath{fill:#333333;}#mermaid-svg-2X5aUo1c8wAeABb5 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-2X5aUo1c8wAeABb5 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-2X5aUo1c8wAeABb5 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-2X5aUo1c8wAeABb5 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-2X5aUo1c8wAeABb5 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-2X5aUo1c8wAeABb5 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-2X5aUo1c8wAeABb5 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-2X5aUo1c8wAeABb5 .cluster text{fill:#333;}#mermaid-svg-2X5aUo1c8wAeABb5 .cluster span{color:#333;}#mermaid-svg-2X5aUo1c8wAeABb5 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-2X5aUo1c8wAeABb5 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-2X5aUo1c8wAeABb5 rect.text{fill:none;stroke-width:0;}#mermaid-svg-2X5aUo1c8wAeABb5 .icon-shape,#mermaid-svg-2X5aUo1c8wAeABb5 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-2X5aUo1c8wAeABb5 .icon-shape p,#mermaid-svg-2X5aUo1c8wAeABb5 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-2X5aUo1c8wAeABb5 .icon-shape .label rect,#mermaid-svg-2X5aUo1c8wAeABb5 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-2X5aUo1c8wAeABb5 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-2X5aUo1c8wAeABb5 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-2X5aUo1c8wAeABb5 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
是
否
否
是
是
否
terminal_tool 收到命令
检查执行环境
本环境是否跳过命令检查?
Environment 执行
硬性禁止规则
命中硬性规则?
拒绝执行
危险正则与 Tirith 检测
是否需要审批?
按审批配置处理
允许?
这是常规交互场景的简化图;manual、smart、off、非交互任务以及已存在的授权记录会影响实际分支。
4.2 Hermes 靠什么发现危险命令?
第一层:内置规则与正则。
tools/approval.py 维护危险命令模式,并对 Shell 命令做一定规范化,再尝试匹配,例如递归删除、系统管理操作等。
第二层:Tirith 安全扫描。
用于补充正则难以覆盖的命令或内容风险。在该版本的主交互流程中,Tirith 的风险发现可以汇总进入审批决定。
第三层:可选的 Smart Approval。
只有启用 approvals.mode: smart 后,Hermes 才会在已经标记风险的情况下,让辅助 LLM 帮助复核,得到 APPROVE、DENY 或 ESCALATE 等判断。
所以答案很明确:Hermes 不会默认把每一条 Terminal 命令都交给 LLM 判断危险性;规则检测是基础,辅助 LLM 是可选层。
4.3 哪些命令会直接拒绝,哪些会要求确认?
这里要区分两种风险:
- 硬性阻止(Hardline):例如在适用的执行环境中尝试删除系统根目录、格式化磁盘等灾难性操作,命中后直接拒绝,普通审批无法覆盖。
- 可审批风险:例如某些递归删除或系统配置修改,匹配到风险规则后,按授权记录和审批模式决定是否需要用户确认。
还有一个容易忽略的前提:执行环境也会影响这套检查是否触发。
对被视为隔离的 Docker 等环境,可以跳过这套 Terminal 命令审批;但 Docker 如果挂载了宿主机路径,就不再按纯隔离环境处理。
这不是说"在 Docker 里执行就一定安全"。如果容器能访问生产数据库、内部 API 或宿主机 Docker Socket,仍然可能产生严重业务影响。
五、人工审批通过之后,Hermes 会记住多久?
无论审批是从 pre_tool_call 发起,还是从 Terminal 风险检测触发,Hermes 都有类似以下的授权选择:
| 选择 | 作用 |
|---|---|
once |
只批准当前这次执行 |
session |
在当前 Session 中记录对应风险规则的授权 |
always |
对支持的规则记录持久化授权 |
deny |
拒绝执行 |
特别注意 session 和 always:它们复用的是规则授权,不一定是"某一次完整参数的快照"。
例如自定义 Hook 每次都使用:
rule_key="employee.delete"
那么它表达的是一类删除操作,而不是"仅删除员工 10086"。后续匹配相同规则的操作可能复用授权,范围就可能超出用户预期。
对于删除账号、转账、生产发布等不可逆业务,更稳妥的方式是:在业务系统里实施一次性审批票据,绑定操作者、租户、目标资源、关键参数及有效期,并在真正执行前再次核验。
六、实战:为自定义 Tool 加上人工审批
下面用 delete_employee 做一个最小示例:插件负责触发审批,业务 Handler 负责最终权限校验。
6.1 插件目录和配置
在用户插件目录建立:
text
~/.hermes/plugins/employee-guard/
├── plugin.yaml
├── __init__.py
└── business.py
plugin.yaml:
yaml
name: employee-guard
version: "1.0.0"
description: Employee deletion approval example
在 Hermes 的 config.yaml 中启用:
yaml
plugins:
enabled:
- employee-guard
6.2 注册 Tool 和审批 Hook
__init__.py:
python
from .business import delete_employee_handler
DELETE_EMPLOYEE_SCHEMA = {
"name": "delete_employee",
"description": "删除指定员工账号,需要确认和权限校验",
"parameters": {
"type": "object",
"properties": {
"employee_id": {"type": "integer", "minimum": 1}
},
"required": ["employee_id"],
"additionalProperties": False
}
}
def before_tool_call(tool_name, args, **kwargs):
if tool_name != "delete_employee":
return None
employee_id = args.get("employee_id")
if type(employee_id) is not int or employee_id <= 0:
return {"action": "block", "message": "员工 ID 必须是正整数"}
return {
"action": "approve",
"message": f"即将删除员工 {employee_id},请确认",
"rule_key": f"delete_employee:{employee_id}"
}
def register(ctx):
ctx.register_tool(
name="delete_employee",
toolset="employee",
schema=DELETE_EMPLOYEE_SCHEMA,
handler=delete_employee_handler,
)
ctx.register_hook("pre_tool_call", before_tool_call)
以上使用的是 v2026.7.30 插件系统支持的 ctx.register_tool()、ctx.register_hook() 和 Hook 返回格式。rule_key 仅用员工 ID 做演示;生产系统还需要处理租户隔离和授权复用粒度,不能把它当作一次性票据。
6.3 Handler 才是最终安全边界
business.py 的职责应当是:
python
# 业务逻辑示意:以下服务接口需要由你的企业系统实现。
import json
def delete_employee_handler(args, **kwargs):
employee_id = args.get("employee_id")
if type(employee_id) is not int or employee_id <= 0:
return json.dumps({"error": "invalid employee_id"})
# 可信身份必须来自服务端认证上下文,不能来自 LLM 参数。
actor = auth_service.require_verified_actor()
# 只在当前租户内查找目标;同时避免泄露跨租户资源信息。
employee = employee_service.find_in_tenant(
employee_id, actor.tenant_id
)
if employee is None:
return json.dumps({"error": "not found or unauthorized"})
permission_service.require(
actor, action="employee.delete", resource=employee
)
employee_service.delete_with_audit(actor, employee)
return json.dumps({"success": True, "employee_id": employee_id})
这里的 auth_service、employee_service、permission_service 都是业务侧示意接口,不是 Hermes 内置模块;在真正接入时,还应实现事务内重复校验、幂等、并发保护和审计日志。
最关键的是:task_id、session_id 和模型传来的 user_id / tenant_id,都不能自动当作经过认证的操作者身份。
到这里,自定义 Tool 的责任划分就清楚了:
text
Schema:告诉模型参数怎么写
↓
pre_tool_call:这个调用需要拦截或人工确认吗
↓
Hermes Approval:收集批准 / 拒绝结果
↓
Handler:当前操作者真的有权限吗
↓
业务服务:执行变更、记录审计
七、三个经常混淆的边界
误区一:只要 Tool 参数符合 Schema,执行就是安全的。
Schema 是调用契约,不是权限系统。类型正确,不能证明资源属于当前租户。
误区二:Hermes 会自动识别所有自定义 Tool 的危险性。
terminal 的危险命令正则不会因为 Tool 名为 delete_employee 就自动介入。业务 Tool 应通过插件策略提出审批要求,并在 Handler 里完成权限检查。
误区三:用户批准了,就可以直接操作数据库。
人工审批解决的是"是否同意这次高风险操作",业务授权解决的是"谁有权操作哪个资源"。它们缺一不可。
八、总结
Hermes Tool 安全机制并不是一个统一的"危险判断函数",而是一套分工明确的执行边界:
- AIAgent / Tool Runtime 接收调用,落实工具范围和执行前检查。
pre_tool_call为任意 Tool 提供可扩展的阻断与人工审批入口。approval.py为 Terminal 命令风险和插件升级请求提供相应的审批能力;Terminal 还拥有自己的命令检测流程。- Handler / 业务服务 对真实身份、租户、权限和数据状态做最终强制校验。
一句话概括:
LLM 决定"想做什么",Hermes 决定"是否进入执行",业务系统决定"有没有资格执行"。