开源自托管 Agent 项目 Hermes Agent 的技能溯源与同步
本文基于 hermes-agent 仓库 commit 2d40494(2026-07-29)梳理,该项目仍在高频迭代,具体行为以仓库 https://github.com/NousResearch/hermes-agent 最新代码与文档为准。
装技能这件事的真实风险,不是「技能写得不好」,而是你说不清它从哪来、被谁改过、下次更新会不会把你的改动冲掉。 NousResearch/hermes-agent(下面简称这个项目,注意它是一个常驻自托管的 Agent 程序,不是同名的 Hermes 开源模型系列,也不是别处那些叫 Hermes 的库和商标)把这三个问题拆成了几套互不共用的记录:内置技能有一份带哈希的播种清单,外部装进来的技能有一份带来源标识与扫描结论的锁文件,跨设备同步走的是另一套内容寻址的对象模型。三套记录彼此不混用,恰恰是这个设计里最值得抄的部分。
站内已经有几篇讲相关话题的:Agent 技能机制三体对比比的是不同项目在技能加载上的通用方法论,MCP 推荐必装清单讲的是另一类扩展在装之前该怎么挑,Agent 提示注入防御讲的是注入这件事本身怎么防。本篇不重复这些,只讲这一个具体项目把「来源与同步」落到了哪些文件、哪些哈希、哪些拒绝条件上——机制是可以逐行核对的,方法论不能。
一、技能从哪来:四种来源,四种所有权
先把来源分清,后面所有判断都建立在这上面。
第一类是随仓库发的内置技能。仓库 skills/ 目录下 14 个顶层分类目录,共 70 份 SKILL.md。它们不是原地被读取的,而是由 tools/skills_sync.py 播种(复制)到用户目录 ~/.hermes/skills/ 下,并在 ~/.hermes/skills/.bundled_manifest 里逐行记下 技能名:来源哈希。这个哈希是播种那一刻源目录内容的摘要,后面所有「该不该更新」的判断都拿它当锚点。
第二类是官方可选技能。仓库 optional-skills/ 下 21 个分类目录、共 111 份 SKILL.md,默认不激活。它们不参与内置播种,而是通过技能中心(Skills Hub)的官方源装入,并在锁文件里被标成官方来源、内置信任级。
第三类是从外部装进来的。tools/skills_hub.py 里有一整排源适配器:GitHub 仓库源(默认源里包含 openai/skills 的 skills/.curated/ 与 skills/.system/、anthropics/skills、huggingface/skills、NVIDIA/skills、garrytan/gstack)、读 /.well-known/skills/index.json 的站点源、直接给一个 SKILL.md 链接的 URL 源,以及几个第三方目录站源。你也可以用 hermes skills tap add 自己加 GitHub 源。
第四类是本机产生的:你让 Agent 写的,或者它在后台自我复盘时写的。这两者在这个项目里被明确区分开,见下一节。
顺带说一句规模感:plugins/ 下 18 个顶层插件目录、optional-mcps/ 下 6 个,技能只是这套扩展体系里的一层。这些数字你自己 ls 一遍就能数出来,不用信我。
| 组成部分 | 它负责什么 | 仓库位置 | 你什么时候会碰到它 |
|---|---|---|---|
| 内置技能播种 | 把仓库自带技能复制到用户目录,用带哈希的清单判断更新与否 | tools/skills_sync.py | 每次 hermes update,或首次安装 |
| 官方可选技能 | 默认不激活的官方技能集,装入后回填官方来源标记 | optional-skills/ | 主动装可选技能,或修复被改乱的官方技能 |
| 技能中心与来源适配 | 搜索、下载、隔离、记录来源标识与内容哈希 | tools/skills_hub.py | hermes skills install / list / audit |
| 安装前扫描与信任策略 | 正则静态扫描出结论,按信任级决定放行/追问/拦截 | tools/skills_guard.py | 装社区来源技能被拦时 |
| 写入来源标记 | 区分「后台自我复盘写的」与「你让它写的」 | tools/skill_provenance.py | 自动整理动了不该动的技能时 |
| 使用与策略侧记 | 记使用计数、生命周期状态、管理归属、同步意图 | tools/skill_usage.py | hermes curator 相关命令 |
| 跨设备同步 | 内容寻址对象、比较并交换引用、三方合并 | tools/skills_sync_client.py | hermes sync status / pull / push |
二、装进来之前:隔离区、扫描、信任级三道闸
外部技能不会被直接写进技能目录。流程是先落到隔离区 ~/.hermes/skills/.hub/quarantine/<技能名>,扫描出结论,通过策略判定,才被移动到正式位置。
扫描在 tools/skills_guard.py,是正则静态分析,威胁模式表覆盖数据外泄、提示注入、破坏性命令、驻留、混淆等类别,最终收敛成三档结论:安全、需注意、危险。信任级只有三档:内置(随程序发的,不扫)、受信(写死在受信仓库集合里的那几个)、社区(其余全部)。两者交叉成一张策略表,就在这个文件里:
INSTALL_POLICY = {
# safe caution dangerous
"builtin": ("allow", "allow", "allow"),
"trusted": ("allow", "allow", "block"),
"community": ("allow", "block", "block"),
"agent-created": ("allow", "allow", "ask"),
}
读这张表要读出两个态度。一是社区来源只要扫出东西就拦,不给「需注意」留缝——默认对陌生来源不客气。二是危险结论对社区与受信来源都不给强制放行的余地,代码里对强制安装的判断专门把这一格排除掉了。表里的「受信」不是动态算出来的,是一个写死在这个文件里的仓库集合(openai/skills、anthropics/skills、huggingface/skills、NVIDIA/skills),注释直接把它标成硬编码的信任配置——你要加自己的受信来源,得改源码,不是改配置。
这张表还有一行容易被读岔:最后那一行对应的是 Agent 自己写出来的技能,而它在源码注释里被标明只在一个专门的开关打开时才生效,默认关闭。所以你在默认安装上看到的实际闸口是前三行,第四行是一条备用路径,不是常态。
安装那一步还有几个具体的拒绝条件,值得单独记:隔离区里出现符号链接(Windows 上还包括目录联接)直接拒装,理由写在注释里——链接指到技能目录之外,目标内容会被复制进来并在下次加载时喂给模型;安装路径要过一遍规范化校验,最后一段必须等于技能名,空串、.、绝对路径、带 .. 的路径一律拒,因为这个路径后面是卸载时递归删除的目标。同一套校验在卸载时再跑一次。
装完落两处记录:锁文件 ~/.hermes/skills/.hub/lock.json 里记来源类型、来源标识、信任级、扫描结论、内容哈希、安装路径、文件清单,GitHub 来源还会带上人可读的来源链接与取到的树版本;审计日志 ~/.hermes/skills/.hub/audit.log 一行一条,记时间、动作、技能名、来源与信任级、结论、哈希。这两个文件是你事后回答「这技能哪来的」的唯一依据,hermes skills list 和 hermes skills audit 读的就是它们。
三、更新会不会冲掉你的改动:三方哈希比较
这是内置技能播种里最精巧的一段,也是最容易被误解成 bug 的一段。
清单里存的是「上次播种时源目录的哈希」,播种逻辑每次拿三个值比:源目录当前哈希、清单里记的来源哈希、你磁盘上那份的哈希。四种结果:清单里没有这个技能名,就当新技能复制进去;源没变(当前哈希等于来源哈希),跳过,连你那份都不去读;源变了而你那份还等于来源哈希,说明你没动过,可以安全更新;源变了而你那份也变了,判定为你改过,跳过并打印一条提示——从此这个技能不再接收上游更新,直到你显式重置。
围绕这个主判断还有一圈修补,每一条都对应一个真实事故:更新过程被打断,旧版本停在 .bak 备份里而目标目录没了,下一轮会先把备份挪回来,否则会被误判成「用户删除了」而永久消失;上游把技能改名或换分类,清单键(技能名)还在但目标路径是全新的,会去技能树里按名字找那份旧副本,且只在它与来源哈希逐字节一致时才搬走——证明那份是程序自己放的,不是你的作品;外部技能目录已经提供了同名技能时,本地不写入,避免加载器遇到重名无法解析,并且只在本地那份与源逐字节相同时才清掉这个残影;自动整理裁掉过的内置技能名写在 .curator_suppressed 里,播种时跳过,否则每次更新都会把你故意删掉的技能复活。
还有一个整体开关:~/.hermes/.no-bundled-skills 这个标记文件在,播种整体空转。递归删除的辅助函数里另外加了一道范围守卫,目标必须严格位于技能根目录之下,否则抛错——注释里写明这是为了防止曾经观察到的整目录误删。
对你的意义很直接:这套机制承诺的不是「你的改动永远安全」,而是「程序只覆盖它能证明是自己写的那份」。证明手段就是哈希相等。所以你手改一个内置技能,代价是这个技能从此脱离更新流;想看差异用 hermes skills diff,想找出所有这样的技能用列出已修改的那个子命令,想回到上游用 hermes skills reset(带恢复参数会顺手删掉你那份重新播种)。
四、跨设备同步:内容寻址、比较交换、三方合并
tools/skills_sync_client.py 是另一套东西,别和上面的清单混起来——注释里专门强调了两点:它用完整的 64 位十六进制摘要作为线上地址,而本地去重用的是截断成 16 位的另一套哈希,两个命名空间绝不能混;它刻意放在工具目录而不是命令行目录下,为的是这一层永远不去导入命令行层。
对象模型是熟悉的三件套:文件成 blob,目录成 tree,一次快照成 commit。树和提交的序列化有严格的规范:UTF-8、键按字典序、无多余空白、条目按名字排序,客户端和服务端必须产出逐字节一致的结果,否则推送会被判哈希不符。推送方式是先批量上传对象,再对引用 refs/user/<owner>/HEAD 做一次比较并交换。
冲突处理是这里的重点。比较交换失败时服务端返回当前头,客户端取回来做逐技能三方比较,五种结果:两边一样、只有你动了、只有对面动了、两边都动但结果相同、两边都动且不同。前四种都能自动定;最后一种是真重叠,写到 refs/user/<owner>/conflict/<序号> 上,交出去等人处理,不自动挑一边。非重叠的情况会生成一个双亲的合并提交再重试一次。这套判定的注释里明确说它照的就是内置播种那段「来源/本地/上游」的决策语义——同一套思路在两个层面复用。
先说清楚哪些技能根本没资格进这条线:只有 Agent 自己写的和你自己写的、躺在技能主目录下的那些技能才算有资格,随仓库播种的内置技能与从技能中心装进来的技能被明确排除在外——前者上游有权威副本,后者上游是别人的仓库,两类都没有跨设备同步的必要,也不该把别人的内容当成你的资产往外推。这个判断有一个独立的资格判定函数,是所有同步决策的第一道过滤。
哪些有资格的技能真的同步,也被做成了内容而不是本地开关。根树上挂一个名叫 sync-manifest 的 blob,逐技能记 {名字, 是否启用};本地 .usage.json 里那个同步标记只是「你在这台机器上的意图」,拉取时会从远端清单反过来采纳别的设备上的启用状态,推送时再写回去,权威在远端那份。这样你在一台机器上开启同步,另一台机器拉一次就跟上,不需要两边各点一遍。
还有几层门禁必须同时成立才会真的动:账号要带一个管理员声明(代码注释直说这个声明在线上的名字与它的实际含义不符,是历史包袱,且这只是发布前的临时收口,不是最终的授权设计);实例级总开关要打开;同步平面地址要解析得到。三者缺一,推拉都空转返回。默认策略是逐技能选择加入,也可以通过环境变量翻成「符合条件的技能默认都同步」,注释里说这个默认值是暂定的、预期会变。
组织共享是另一条线:组织的技能拉到 ~/.hermes/skills/_org/<组织 id>/ 这个独立命名空间,只快进不合并,本地在这里的改动会被下一次拉取覆盖;你要把本地技能贡献回组织,走提案路径——管理员身份直接合并,普通成员的比较交换会被服务端转成待审提案。代码里把这两种返回严格分开,并注明待审绝不能当成已生效来展示。
五、边界与代价:这套设计放弃了什么
溯源在这里是声明,不是推断,而且刻意如此。 tools/skill_usage.py 里那个标记字段名字读起来像「谁创建的」,注释花了一整段解释它实际被当作「是否交给自动整理管理」的策略开关:作者身份是历史事实,对早于这个机制的记录根本不可恢复;而是否允许自动整理去改它、归档它,是一个你随时能改的策略选择。前台创建的技能故意不打这个标记——你要它写的技能属于你。后果是有一批技能对自动整理完全不可见,项目的处理方式是把这个数量显式报出来,并提供一条显式接管命令,而不是靠使用次数去猜作者。注释里那句「使用与修补次数是维护的证据,不是作者身份的证据」,比机制本身更值得记。
tools/skill_provenance.py 那个模块只干一件事:用一个上下文变量标出当前这次工具调用是不是发生在后台自我复盘的分支里,让创建技能的工具据此决定要不要打管理标记。它自己都说这个信号是搭在既有字段上的,默认值是前台。所以这不是不可伪造的凭据,只是一个进程内的上下文位。
哈希能证明的事同样有限:它只能证明「这份内容和我上次记下的一致」,不能证明这份内容安全。扫描是正则静态分析,能挡住把密钥拼进 curl 这类明面写法,挡不住换个写法或者纯自然语言的指令。而技能正文本身就是给模型看的自然语言指令——这层根本不在这套溯源机制的射程内,要防得往提示注入那个方向想。
同步这条线还有两处现实边界:客户端在这个仓库里,服务端不在——你能逐行读的只有一半;而且普通账号在当前门禁下推拉全空转,你在本地看到的是一套完整实现,实际是否生效取决于你手上的账号声明。
最后是常驻本身的代价,这一点不该被机制的精巧掩盖。这个 Agent 会常驻在你的机器上、开终端执行命令、往磁盘写文件、访问外部服务,同步开启后还会把技能内容上传到项目自己的服务端。技能目录里的这些文件——审计日志、锁文件、使用侧记、设备标签——都落在你的家目录下。装一个陌生技能,等于在一个能执行命令的常驻程序里加一段它会照着做的指令,权限边界的问题比来源问题更要紧,那是权限给太大要回答的。
明确不管的几件事:技能内容质量它不评判;技能让 Agent 做什么它不干预(只在安装前扫一遍模式);模型服务商侧的规则它管不着——各家规则不同且会调整,以官方最新说明为准。
六、上手与避坑清单
别顺手改内置技能。 会踩是因为改起来太自然:文件就在你家目录里,编辑器一开就改了。代价是哈希一变,此后每次更新都把它判成「你改过」而跳过,你以为自己在用最新版,其实停在改动那天。要避:改之前先想清楚是长期定制还是临时试;已经改了就用差异命令看清改了什么,要回上游用重置命令(带恢复参数会删掉你那份)。
装社区来源技能别一上来就强制。 会踩是因为被拦时第一反应是绕过。但「需注意」在社区来源下被拦是策略而非故障,「危险」结论压根不接受强制。要避:先用检视命令看它的来源标识和来源链接,确认那个仓库你认;真要装,看扫描报告里具体是哪条模式命中、命中在哪个文件哪一行,再决定。
别把管理标记当作者证明。 会踩是因为字段名字确实在骗人。你前台让 Agent 写的技能不会带这个标记,老记录连这个键都没有。要避:要把某个技能交给自动整理,用显式接管命令声明,别指望它自己认领;反过来,看到一个技能带着这个标记,也别推断成「这是 Agent 自己写的」。
多设备别只改本地那个同步开关。 会踩是因为本地开关看起来就是最终状态。实际权威是远端根树上那份清单,拉取会把别的设备的启用状态采纳过来。要避:改完在每台机器上各跑一次状态与拉取,用状态输出确认已选入的技能列表,而不是看本机配置。
别把待审提案当成已共享。 会踩是因为返回是成功形状的。普通成员把技能提交给组织,服务端会转成提案,代码里明确这不能展示成已生效。要避:看返回里是「已合并」还是带提案编号的待审,再去跟管理员。
别在无凭据状态下批量拉外部源。 会踩是因为未认证调用 GitHub 接口会被限流,而多个 GitHub 来源共用同一份配额,索引构建会被一起拉平,最难受的是它可能安静地少几个源而不是报错。要避:按仓库告警文案里说的那样配好访问令牌或本机命令行工具凭据,构建完抽查一下各源是不是都有结果。
自己写技能别撞官方目录名。 会踩是因为撞名后官方来源回填会拒绝在两份同名技能之间猜,那个查找函数在有歧义时直接返回空——你的技能不会被误标,但你也永远等不到那次修复。要避:自己的技能加个前缀,和官方分类目录名保持距离。
别以为可选技能装了就是全部。 会踩是因为仓库里 111 份可选 SKILL.md 默认都不激活,容易误以为「装了这个项目就有这些能力」。要避:清楚区分仓库里有、播种到本机、被激活这三件事,用列表命令确认。
收束:三个自检问题
真要在自己项目里抄这套东西,先回答三个问题:你能不能对每一份被 Agent 加载的指令,指出它来自哪个仓库哪个版本?上游更新时,你靠什么证据区分「这份是程序自己放的」和「这份被用户改过」?两台机器同时改了同一份技能,你是自动挑一边,还是把冲突显式交出去?这个项目对三个问题的回答分别是锁文件、哈希相等、写到独立的冲突引用上——不一定是最优解,但都是可以逐行核对的解。
接着往下读的顺序建议:先 tools/skills_sync.py 的模块开头那段更新逻辑说明,那是整套哈希语义的浓缩版;再 tools/skills_guard.py 的策略表与信任级注释,那是安全态度的表达;最后 tools/skills_sync_client.py 的冲突处理段,看它怎么把同一套三方判定搬到跨设备场景。项目采用 MIT 许可证,署名 Nous Research,读完想改成自己那套也不必客气。
本篇属于一个把开源常驻自托管 Agent 项目 Hermes Agent逐层拆开讲的系列,整体地图见 开源自托管 Agent 项目 Hermes Agent 是什么;沿着这条线往下,还可以看 开源自托管项目 Hermes Agent 怎么兜住技能自动创建 和 开源自托管 Agent 项目 Hermes Agent 的状态层拆法。