插件可以替它的 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> # 确认后再启用
如果你自己在发布插件
三点值得记住:
- 给你的写服务器加门禁。任何会花钱、交易、发消息或改账户的 MCP 服务器,都应该在
plugin.json里声明trust: "untrusted"。不要指望技能文本里的“先问用户”——那只是提示,不是门。 - 别在
mcp.json里放密钥。那里声明的env是可见的包数据,不是秘密存储机制。 - 包默认禁用不是门禁。用户启用之后,扩展字段才是唯一由你控制的安全声明。
想了解远程 MCP 服务器的整体信任模型(readOnlyHint 只是服务器自己给的提示、会话过期后未标注工具返回 outcome_uncertain 等细节),可以看远程 MCP 目录的安全模型;想了解插件在宿主里被隔离到什么程度,看插件隔离与宿主边界;无人值守场景下审批到底怎么走,无人值守审批指南讲得更细。命令参考在 hermes plugins。
值得记住的一点
readOnlyHint 是服务器自己声明的提示:一个撒谎的服务器最多让本该问你的工具不问,拿不到额外权限。而 trust: untrusted 是让 Hermes 不再依赖任何声明——凡是没标只读的,一律先问。
两者的区别就是这次修复的意义:安全边界不该由被审查的那一方来画。旧的便携包做不到这一点,现在可以了,而代价只是插件作者在清单里多写一行。