Hermes架构拆解之Tool执行安全机制

本文对Hermes的架构拆解是基于 v2026.7.30 版本的源码解析

一、从一个删除员工的请求说起

假设我们给 Hermes 添加了一个企业管理工具:

python 复制代码
delete_employee(employee_id=10086)

用户在聊天窗口输入:

帮我删除员工 10086 的账号。

LLM 理解了用户意图,返回一个 Tool Call:

json 复制代码
{
  "name": "delete_employee",
  "arguments": {
    "employee_id": 10086
  }
}

这里有一个很容易忽略的问题:模型知道"要删除谁",并不代表它就有权删除。

至少要弄清三件事:

  1. 参数是否正确? employee_id 是不是一个有效的员工编号?
  2. 操作是否经过确认? 删除账号可能不可恢复,是否需要负责人批准?
  3. 调用者是否有权限? 当前用户能不能删除这个租户下的员工?

这三件事由不同环节负责。理解它们的分工,就理解了 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 决定"是否进入执行",业务系统决定"有没有资格执行"。

相关推荐
某些鱼1 小时前
系列6 架构治理:让系统别白建,让决策别白做
架构
某些鱼1 小时前
系列5 36 个月路线图:不是项目排期表,是决策的时间化表达
架构
青衫烟雨1 小时前
DeepSeek 崩了 5 小时:你的 AI 应用为什么跟着一起挂?
人工智能·架构
墨云安全1 小时前
AI 漏洞挖掘平台怎么评估?2026 年技术架构与选型要点
网络·人工智能·网络安全·架构·渗透测试
GoGeekBaird1 小时前
我做了一个给 Agent 用的电商开源项目:开店、上架、改版,跟 Agent 说一声就行
后端·github·agent
某些鱼1 小时前
系列2.1 知道公司全部流程,也画不出能力地图:四个信息源教你拆解
架构
不好听6131 小时前
Neo4j:图数据库入门:节点、关系、标
agent
高晶1 小时前
ARM 嵌入式 Linux 内核编译与结构解析实战
前端·架构
李溪白1 小时前
RAG 进阶:Agentic RAG 与多智能体协作(系列二·第1篇):高级工具设计
agent