插件可以替它的 MCP 服务器申请审批门禁了


你从插件目录里装了一个社区插件,它顺手注册了一个 MCP 服务器。这个服务器能下单、能转账、能替你发消息——而整个过程里,Hermes 一次都没问你。唯一的“防线”是插件说明里那句“记得先跟用户确认”,而模型会不会照做,全看它当时的心情。

这不是哪个插件的锅,是格式本身的缺口:Agent Plugins v1 规范里没有“信任等级”这个概念,所以从便携包里加载出来的 MCP 服务器,一律按最高信任度 trust: full 跑。2026 年 10 月 7 日合并的 #134524 把这个缺口补上了:包可以主动为自己的服务器申请门禁,但不能反过来给自己放行。

缺口是怎么来的

Hermes 支持两类插件。原生插件是 plugin.yaml 加一个 register(ctx) 函数,跟核心长在一起;便携包(portable package)走的是 Agent Plugins v1.0.0 格式,长这样:

my-portable-plugin/
├── plugin.json
├── skills/
│   └── summarize/
│       ├── SKILL.md
│       └── references/
└── mcp.json

plugin.json 是清单,mcp.json 声明这个包要连的 MCP 服务器,skills/ 放它带来的技能。装完之后包默认是关闭状态,要你手动 hermes plugins enable <name> 才会加载——这一步已经是安全设计,但它只解决“要不要用”,不解决“用的时候谁来踩刹车”。

真正的问题在读取器:Hermes 的便携 mcp.json 只认 type、url、headers 三个字段(对应 hermes_cli/agent_plugins.py),而信任等级在原生配置里是 config.yaml 的 mcp_servers.<name>.trust。规范里没有这个字段,读取器自然也不会翻译它,于是每个包服务器都拿到默认值 full。

触发这次修复的是一个真实案例。在插件目录的巡检里发现,某交易类插件自带的托管服务器会在用户的 Hyperliquid 账户上暂停每一笔交易,直到调用 resolve_approval 才继续。也就是说,一个被提示注入的模型,离“替你完成一笔交易”之间,只隔着一句写在技能文本里的请求。Hermes 自己并不会弹窗。

包怎么申请门禁

plugin.json 里已经有一个 Hermes 专用的扩展块(就是携带 app、requires、liveness 的那一块),现在它多接受一个 trust:

{
  "extensions": {
    "com.nousresearch.hermes": {
      "servers": {
        "trade": { "trust": "untrusted" }
      }
    }
  }
}

写完之后,这个 untrusted 会被翻译到该包生成的 mcp.json 服务器条目上,后面走的就完全是 config.yaml 里那一套老门禁(实现在 tools/mcp_tool_handlers.py):

  • 服务器上每一个没有标注 readOnlyHint: true 的工具调用都要先问你,走的是标准审批界面;
  • 在没人能回答的场合——定时任务(cron)、hermes chat -q 的一次性问答、无人值守的网关会话——直接失败关闭(fail closed),不是默默放行;
  • 条目只声明 trust 而不带应用的,不会创建可用性门禁,它只是替服务器上个锁。

只能收紧,不能放宽

这条设计比功能本身更重要。包能声明的 trust 只有两个合法值:

你写的值 结果
untrusted 生效,服务器上的写操作全部需要审批
full 被接受,但本来就是默认值,等于什么都没做
其它任何值 整个包被拒绝加载,和原生配置里写了无法识别的信任值一样失败关闭

还有一条优先级规则兜底:config.yaml 里同名服务器会整体替换包里的条目,包括信任等级。所以包永远覆盖不了用户的决定——你手动把某个服务器设成 full,包再怎么申请 untrusted 也没用(前提是同一个服务器名)。

原始 PR 的动机里有句话说得直白:这是“只允许往安全的方向偏一格”(narrow only)。扩展字段的存在不是为了给插件更多权力,而是让插件能主动交出权力。

装之前先看一眼

hermes plugins validate 现在会把这个门禁列出来,插件目录的审核流程也能看到:

server trust: trade — untrusted (Hermes asks before every write-capable tool call)

拿到一个不认识的便携包时,建议的顺序是:

hermes plugins validate /path/to/package   # 看它声明了什么,包含被门禁的服务器
hermes plugins install owner/repo          # 装进来(默认禁用)
hermes plugins capabilities <name>         # 看声明的能力 vs 实际授予的能力
hermes plugins enable <name>               # 确认后再启用

如果你自己在发布插件

三点值得记住:

  1. 给你的写服务器加门禁。任何会花钱、交易、发消息或改账户的 MCP 服务器,都应该在 plugin.json 里声明 trust: "untrusted"。不要指望技能文本里的“先问用户”——那只是提示,不是门。
  2. 别在 mcp.json 里放密钥。那里声明的 env 是可见的包数据,不是秘密存储机制。
  3. 包默认禁用不是门禁。用户启用之后,扩展字段才是唯一由你控制的安全声明。

想了解远程 MCP 服务器的整体信任模型(readOnlyHint 只是服务器自己给的提示、会话过期后未标注工具返回 outcome_uncertain 等细节),可以看远程 MCP 目录的安全模型;想了解插件在宿主里被隔离到什么程度,看插件隔离与宿主边界;无人值守场景下审批到底怎么走,无人值守审批指南讲得更细。命令参考在 hermes plugins。

值得记住的一点

readOnlyHint 是服务器自己声明的提示:一个撒谎的服务器最多让本该问你的工具不问,拿不到额外权限。而 trust: untrusted 是让 Hermes 不再依赖任何声明——凡是没标只读的,一律先问。

两者的区别就是这次修复的意义:安全边界不该由被审查的那一方来画。旧的便携包做不到这一点,现在可以了,而代价只是插件作者在清单里多写一行。