给插件目录提 PR 之前,先看看这道门禁到底查什么


你写了个插件,按目录的要求提了个 PR,CI 回来一句话:pinned sha 不是仓库里的 commit 对象。你盯着自己的 YAML 看了半天——sha 明明是从 GitHub 页面上复制下来的。你复制的是标签对象的 sha,而门禁要的是提交对象的 sha,这两个东西在你的 PR 里长得一模一样。

插件目录是别人会在自己机器上执行代码的地方,所以它的门禁不是走流程,而是真的做供应链检查。这篇讲清楚这道门禁查什么、为什么连「重命名」都要管、以及你提交前该自己确认哪几件事。

门禁在哪、什么时候跑

入闸检查是一个独立的工作流,只在改动 plugin-catalog/** 的 PR 上触发——这样它永远不会因为不相关的 PR 而变红。它分成两个任务:

  • 结构检查(structural):便宜的模式校验,不需要安装 Hermes,直接把整个 plugin-catalog/ 目录交给校验脚本,保持「目录里的一切都合法」这个不变量;
  • 固定来源验证(pinned-source-validate):真正的供应链检查——你 pin 的那个 sha 必须在你的仓库里可达,而且正好在那个提交上的插件要能通过 hermes plugins validate。

也就是说,门禁会把你的仓库真的克隆下来,切到你声明的那个提交,然后运行校验。它不是读你的 YAML,而是读那个提交上的代码。

三个字段会被逐个检查

你 YAML 里每一项被 PR 控制的字段,都会在使用之前先被检查一遍:

字段 门禁要求
repo 必须是不含空白的 https URL
sha 必须是 40 位十六进制,而且 git cat-file -t 报告它是 commit 对象
subdir 必须是纯相对路径,解析之后仍要落在被克隆的那个仓库里

那个 sha 的类型检查就是开头那个报错的来源:标签对象也是合法的对象 id,但它会剥离到评审者没有声明过的那个提交上,于是门禁拒绝它。同样地,subdir 的检查不是字面比对,而是把路径真实解析之后再看它有没有跑出克隆目录——这样带 .. 的写法就无效了。

为什么它连「重命名」都要管

以前的改动清单用的是 --diff-filter=AM,也就是「新增 + 修改」。问题在于,一个改名的条目文件不会被算进这份清单——而门禁只校验清单里的条目。也就是说,把危险条目的文件名改一下,就能让它从门禁下面滑过去。

现在过滤条件扩到了 AMRT:新增、修改、重命名、类型变更(比如文件变成符号链接)都会进入这份清单。配套的还有一条更彻底的规则:克隆目录里的每一个符号链接都必须在克隆目录内,而且这条检查是遍历整个克隆做的——因为 find 不会进入符号链接目录,只看插件目录的话,一个「目录到目录」的链接就能把外面的东西藏起来。

为了让插件代码没机会干扰门禁

这是设计上最值得学的一段,因为门禁要执行你没审过的代码(hermes plugins validate 会跑插件声明的安装逻辑):

  • 条目从 fd 3 读入,而不是标准输入。 插件代码如果把标准输入读空,后续条目的名单就会「凭空消失」,验证被静默跳过。换一个文件描述符,这条路就断了;
  • 解析器跑在 python3 -I 下,并且每个条目开始时重置变量,任何解析失败都以哨兵值失败关闭;
  • 条目 PR 只允许是「纯数据」的:只能改 plugin-catalog/** 和 contributors/emails/**。理由很直白——门禁自己的执行代码(工作流、校验脚本、构建出来的 Hermes)都从 PR 的检出里来,所以一个同时改了工具代码的条目 PR 理论上可以让正在审查它的那套门禁失效。

一条被砍掉的「过严规则」

第一版修复里的数据专用检查范围太大:它会在 plugin-catalog/ 下面有任何改动时都要求「只能是数据」。按这个标准,过去 134 个已合并的目录 PR 里有 29 个会被判失败——它们携带的是署名用的邮箱映射文件;此外,每一个碰 plugin-catalog/ 的说明文档、政策、CI PR 也会全部变红。

现在的做法是把它收窄:这条检查只在「本次确实有条目 YAML 改动」的那一步跑一次,并且允许 contributors/emails/** 一起进来。同样是这条 PR 里,克隆策略也被改成「不再全历史克隆」:依赖的是 GitHub 的合并提交对它的第一个父提交做 diff,只取两层深度,重命名检测照样有效。

提交前的自查清单

  • 用提交的 sha,不要用标签的 sha。 在 GitHub 的文件页或者提交列表里复制 commit 的完整 40 位 id,别用 Releases/Tags 页面上那个;
  • subdir 写纯相对路径,不要带 ./、..、绝对路径或变量;
  • 一个 PR 只做一件事。 往目录里加/改条目的 PR 就只动 plugin-catalog/**(署名邮箱映射除外),别顺手改工具、工作流或文档;
  • 本地先跑一次校验,脚本和 CI 用的是同一个:python3 scripts/validate_plugin_catalog.py plugin-catalog/;
  • pin 的提交上,插件要能通过校验。 门禁克隆你的仓库、切到那个提交再跑,所以「最新版能过、pin 的那版过不了」是很常见的红叉原因。

想了解插件目录整体怎么用(浏览、安装、离线安装),看 插件目录指南;命令层面的入口在 hermes plugins 命令参考;如果你更关心 MCP 生态那一侧的信任边界,MCP 目录安全指南是配套的一篇。

小结

这道门禁的设计思路可以总结成一句话:凡是 PR 能控制的输入,在使用之前都要被检查一遍,而且要按解析后的真实结果检查,不是按字面。 改名、..、tag 对象、符号链接、甚至标准输入被读空——这些都不是「代码写错了」,而是「检查点选错了位置」。

对插件作者来说,好消息是它很稳定:把 sha 写成 commit 对象、subdir 写成纯相对路径、一个 PR 只干一件事,剩下的门禁会替你验证「评审者看到的提交」和「用户装到的东西」是同一个东西。