第9篇:插件系统——无限扩展 Hermes
Hermes 功能详解第9篇:插件系统。不满足于内置功能?插件让 Hermes 能力无限扩展。
第9篇:插件系统——无限扩展 Hermes
嘿,朋友们!今天咱们来聊聊 Hermes 的插件系统——说白了,就是给 Hermes 装 App。不用改核心代码,丢个文件夹进去,它就能多出一堆新本事。
一个插件长啥样
在 ~/.hermes/plugins/ 下建个目录,放一个 plugin.yaml 加几个 Python 文件就行:
~/.hermes/plugins/my-plugin/
├── plugin.yaml # 清单
├── __init__.py # register() —— 把 schema 和 handler 接起来
├── schemas.py # 工具 schema(LLM 看到的东西)
└── tools.py # 工具 handler(被调用时真正跑的代码)
重启 Hermes,你的工具就和内置工具并排出现了,模型马上就能调用。想跟着完整例子一步步做,看官方那篇 Build a Hermes Plugin。
插件能干啥
在插件的 register(ctx) 函数里,这些 ctx.* API 都能用:
- 加工具:
ctx.register_tool(...) - 加钩子:
ctx.register_hook("post_tool_call", callback) - 加斜杠命令:
ctx.register_command(...),CLI 和网关会话里都能用/名字(handler 跑在网关进程里:返回文本而不是print(),用agent.runtime_cwd.resolve_agent_cwd()读会话工作目录,别用os.getcwd();handler 可以声明session_id、session_key、platform或source作为关键字参数,Hermes 会自动传入,单参数 handler 照旧可用) - 加 CLI 子命令:
ctx.register_cli_command(...),变成hermes <插件> <子命令> - 注入消息:
ctx.inject_message(content, role="user", session_key=... | origin=...),详见下面的「注入消息」一节 - 发状态消息:
handle = ctx.post_message(text),之后handle.edit(text)原地更新 - 打包技能:
ctx.register_skill(...),命名空间是plugin:skill - 借用宿主 LLM:
ctx.llm.complete(...)/ctx.llm.complete_structured(...),直接借用户当前在用的模型和鉴权跑一次性补全,还能带 JSON schema 校验 - 调 MCP 工具:
ctx.call_mcp(server, tool, arguments, timeout=30)
再往深了玩,插件还能注册一整个后端:
- 网关平台(Discord、Telegram、IRC 等):
ctx.register_platform(...) - 图像生成后端:
ctx.register_image_gen_provider(provider) - 委派策略(每个
delegate_task子代理怎么路由):ctx.register_delegation_provider(provider),由delegation.backend选 - 视频生成后端:
ctx.register_video_gen_provider(provider) - 上下文压缩引擎:
ctx.register_context_engine(engine) - 终端执行后端(云沙箱):
ctx.register_terminal_environment_provider(provider) - 电脑操作驱动(
computer_use工具背后的桌面控制):ctx.register_computer_use_provider(provider),由computer_use.backend选 - 委派策略:在
plugins/delegation/<名字>/__init__.py里注册(default就是现在的路由方式) - 记忆后端:在
plugins/memory/<名字>/__init__.py里继承MemoryProvider(走的是另一套发现机制) - 推理后端(LLM 供应商):在
plugins/model-providers/<名字>/__init__.py里register_provider(ProviderProfile(...))(也是独立发现机制)
插件从哪来
Hermes 会在好几个地方找插件:
| 来源 | 路径 | 用途 |
|---|---|---|
| 内置 | <repo>/plugins/ |
随 Hermes 一起发布 |
| 用户 | ~/.hermes/plugins/ |
你自己的插件 |
| 项目 | .hermes/plugins/ |
网关不加载;见下文「项目本地插件」 |
| pip | hermes_agent.plugins entry_points |
分发的包 |
| Nix | services.hermes-agent.extraPlugins / extraPythonPackages |
NixOS 声明式安装 |
同名冲突时,后面的来源会覆盖前面的——所以你自己写的同名插件能顶掉内置的。
插件默认是关着的
通用插件和用户装的后端默认不加载——Hermes 能发现它们(hermes plugins 和 /plugins 里看得到),但在你把名字加进 ~/.hermes/config.yaml 的 plugins.enabled 之前,带钩子或工具的东西一个都不会跑。这是为了防止第三方代码未经你同意就执行。
plugins:
enabled:
- my-tool-plugin
- disk-cleanup
disabled: # 可选的拒绝名单,两边都出现时它赢
- noisy-plugin
三种切换方式:
hermes plugins # 交互式开关(空格勾选/取消)
hermes plugins enable <名字> # 加进允许名单
hermes plugins disable <名字> # 从允许名单移除并加进 disabled
如果你保存过工具选择(用过 hermes tools,也就是 platform_toolsets 里显式列了工具集),那禁用某个插件时,它的工具集也会从这些保存的列表里被拿掉;重新启用时再放回去。CLI 和 Desktop/TUI/dashboard 上的开关行为一致。没保存过选择的话,这些列表不动:插件的工具集默认就是开着的。
hermes plugins install owner/repo 装完后会问你 Enable 'name' now? [y/N],默认是不启用。脚本化安装可以用 --enable 或 --no-enable 跳过提问。如果装的是记忆后端(__init__.py 里注册了 MemoryProvider 的插件),问的会变成 Use 'name' as the memory provider now?:选是(或 --enable)会去设 memory.provider——这是唯一能激活某个记忆后端的开关;选否就留给 hermes memory setup 去处理。
想装得可复现,可以钉死一个完整的 commit(标签、分支、缩写 SHA 都不收):
hermes plugins install owner/repo --ref 0123456789abcdef0123456789abcdef01234567
批量更新也有对应命令:hermes plugins update 不带名字时会跑一遍检查并列出能更新的插件(目录安装的重新钉到审核过的目录 commit,跟踪的 Git 安装则拉取),跳过哪些、为什么跳过都会点名(已钉住、需要修复、状态未知,或 Hermes 不升级的 pip 分发)。终端里问一次,然后逐个走和 hermes plugins update <名字> 一样的路径,所以新能力、放宽的目录钉和扫描结果仍会逐个插件提示。一个失败不影响其余,最后汇总更新了什么、哪些已是最新、哪些失败及原因(有失败则退出码 1)。没有终端时只打印列表、什么都不更新,除非加 --yes;加了 --yes 后同意类提示按非交互规则失败关闭(新能力不授予、放宽的目录钉不应用)。hermes plugins update <名字> --force 则可以在不问问题的情况下接受 caution 级安全扫描结论。
钉住之后 hermes plugins update 不会动它,要换得显式用 --force --ref <新commit> 重新装。更新前 Hermes 会用同一套规则扫描新代码,所以更新里带进风险代码的插件会在运行前被标出来:Git 更新拿到 caution 结论会问 Update anyway? [y/N],hermes plugins update <名字> --force 可以不问直接接受;目录更新是移到审核过的钉,caution 结论不再追问。Desktop 会在对话框里显示同样的结论和 Update anyway 按钮,Git 安装的插件在上次检查发现新 commit 时也会显示 Update 按钮——网关每 plugins.auto_update_check_hours(默认 24 小时)跑一次检查,hermes plugins check-updates 可以按需触发。桌面端也有对应的 Pin to commit 输入框(Capabilities → Plugins → Install from Git,填完整的 40 位 SHA),Installed 列表里会给钉住的 agent 插件显示 pinned @ <sha8> 徽章。注意这只保证 agent 插件被钉住,不保证独立的桌面端插件安装也被钉住。
顺带说一句,hermes://plugin/install?catalog=<名字> 这种深链会拿名字去查实时目录(就是 Capabilities → Plugins 选择器用的那份数据),然后弹出和 App 内一样的已审核目录条目对话框,装的就是它钉住的那个 commit。
强制重装会保留你的文件
用 --force --ref <新commit> 从同一个来源重装,效果类似升级:插件代码被换掉,但你的文件还在——没被 git 跟踪的、被 git 忽略的文件原地不动,被跟踪文件上的改动会被拷到 ~/.hermes/plugins-backup/<名字>-<sha>/ 里留底。如果换成另一个来源重装,那就是全新开始;想把插件彻底重置,先 hermes plugins remove 再装。
装私有仓库的插件
hermes plugins install 克隆时不会弹用户名密码,所以私有仓库得让 Hermes 自己找得到凭据。每次克隆、--ref 拉取和 hermes plugins update 都会先匿名试一次——公开仓库永远看不到你的凭据,所以过期的 token 也不会把公开安装搞坏。只有远端拒绝匿名访问时,Hermes 才去找凭据。对 https:// 来源,按顺序试:
.env里的GITHUB_TOKEN或GH_TOKEN(仅 GitHub)ghCLI 的登录态(gh auth login,仅 GitHub)- 你的 git credential helper(
git credential fill)——GitLab、Bitbucket、自建服务器只要存过凭据都能用
凭据只在那次安装或更新时作为一次性 HTTP header 发出去,绝不会写进插件的 .git/config 或安装元数据。SSH 来源(git@host:owner/repo.git)还是走 ssh-agent。
如果 token 过期了,hermes doctor 会把配置好的 GITHUB_TOKEN/GH_TOKEN 发到 api.github.com 验证(在 API Connectivity 一节),被 GitHub 拒绝时会直接点名是哪个变量、哪个 .env 文件里带着这个过期 token,方便你删掉或换掉。
哪些东西不受允许名单管
有几类插件会绕过 plugins.enabled——它们是 Hermes 内置能力的一部分,默认关掉会坏事儿:
- 内置平台插件(
plugins/platforms/下的 IRC、Teams 等):自动加载,具体频道由gateway.platforms.<名字>.enabled控制 - 内置后端(
plugins/image_gen/等):自动加载,用<类别>.provider选 - 记忆后端:全部被发现,同时只有一个生效,由
memory.provider选 - 上下文引擎:同上,由
context.engine选 - 模型供应商:内置的全部注册,用
--provider或 config 一次选一个 - pip 装的
backend插件:还是要plugins.enabled显式开启 - 用户装的平台插件(
~/.hermes/plugins/platforms/):也要显式开启,第三方网关适配器得你点头
一句话:内置的“开箱即用”基础设施自动加载,第三方的东西一律要你同意。
网关事件钩子不走允许名单
有个容易搞混的地方:plugins.enabled 只管插件,不管网关事件钩子。放在 ~/.hermes/hooks/<名字>/ 下的钩子不是插件,既不受 plugins.enabled 也不受 plugins.disabled 约束。这个目录靠“位置”来信任——只要某个子目录里有合法的 HOOK.yaml + handler.py,网关启动时就会导入它,把文件放进去本身就是“我同意”。
注入消息
插件可以用 ctx.inject_message() 往 CLI 会话、已知的网关会话,或者某个聊天里新建的网关会话里塞消息:
# 为某个聊天/线程启动(或继续)会话,比如插件刚开的一个 Discord 论坛帖
ctx.inject_message(
"Review the attached spec and post a plan here.",
origin={
"platform": "discord",
"chat_id": "1290000000000000000", # 论坛帖(线程)id
"chat_type": "thread",
"thread_id": "1290000000000000000",
"user_id": "123456789012345678", # 这一轮以谁的身份跑;必须过网关的允许名单
"user_name": "Desk Orchestrator",
},
)
签名: ctx.inject_message(content: str, role: str = "user", *, session_key: str | None = None, origin: Mapping[str, Any] | None = None) -> bool
- 用
session_key时,Hermes 复用那个会话里存的平台、聊天、线程、profile 和对话历史。插件不能通过session_key改这条路由。 session_key和origin都没给(或都给了)、权限没授予、或者没有活着的宿主能接这个请求时,返回False。异步接受之后才发现会话 key 未知或无法路由的,会写进网关日志。- 用
origin启动会话(仅网关模式):origin的字段形状和存储的会话 origin 一样(SessionSource.to_dict())——platform和chat_id必填,chat_type、thread_id、user_id、user_name、chat_name等可选。还没有会话时用它,比如机器人自己发的论坛帖——Discord 这类平台不会把它当入站消息回传给机器人。 - 会话建在插件自己所属的 profile 里(就是加载它的那个插件管理器所在的 profile),走正常会话路径,全新提示词、空历史,回复投递到那个聊天/线程。如果该 profile 里这个聊天已经有会话了,文本会像人类消息一样并进去(忙会话队列照常生效),所以同线程里的人类追问会接着同一个会话。
- 插件永远不能指向别的 profile。origin 里写了不同的
profile、网关自己的 profile 路由把这个聊天分给了别的 profile、插件的 profile 不由这个网关服务、或者那个 profile 没有platform对应的已连接适配器时,返回False(原因写进网关日志)——绝不会借用别的 profile 的机器人。多路复用网关上,这一轮跑在插件 profile 的家目录和密钥下。 - 授权不会被绕过:origin 里的 user 必须过网关当前针对该 profile 的允许名单、配对或 allow-all 配置,和
session_key路径是同一套检查。挑一个已授权的user_id,否则请求会在派发时被丢弃并记日志。 - 文本规则一样:不能是斜杠命令、审批或提示词解析。下面这个权限是从插件自己 profile 的
config.yaml里读的。
只给你信任的插件开网关注入。Hermes 会检查这个宿主 API 权限,把它限制在已有会话路由或插件自己 profile 里的新会话,并施加当前授权——但 Python 插件是进程内跑的,这个设置不是沙箱。
让插件跑在独立进程里(plugins.isolation)
默认情况下,第三方 Python 插件和以前一样,是直接 import 进 Hermes 进程的。把 plugins.isolation 设成 host,它们就会被挪进一个插件宿主:每个 profile 一个独立的 Python 进程,按需启动,只 import 这个 profile 里用户装的插件,通过一条私有管道和 Hermes 通信。
plugins:
isolation: host # 默认是 in_process
host:
launcher: [] # 可选,给宿主进程加 argv 前缀,比如套个沙箱运行器
插件本身不用改:拿到的 ctx 一样,注册工具、钩子、斜杠命令、技能和各种 provider(图像/视频生成、网页搜索、浏览器、TTS/STT、记忆、上下文引擎、模型供应商 profile)的方式也完全一样,Hermes 这边会注册对应的入口转发到宿主。Dashboard 的插件 API 也由宿主提供。内置插件仍然跑在进程内。
切到 host 模式后,变化在这几点:
- 不共享解释器:插件的模块永远不会进 Hermes 进程,所以它读不到别的 profile 在内存里的数据,也改不了 Hermes 内部。多路复用网关下,每个 profile 有自己的宿主,只带这个 profile 的环境变量和密钥启动。
- 崩溃被隔离:插件崩了或退出,死的是它的宿主,不是 Hermes;正在进行的调用会返回一个工具错误,Hermes 重启宿主并重新加载插件(重试次数有上限)。
- 少数接口必须在进程内,这类插件会带着明确原因加载失败,而不是硬上:网关平台适配器(
register_platform)、审批传输、Telegram/平台 handler、自己建 SDK 客户端的模型供应商 profile(create_client)、终端后端(后端的命令会带着这个原因失败,singularity插件就是其中之一)、流式 Dashboard 端点,以及会 monkeypatch Hermes 模块的插件。这些得用isolation: in_process跑。
给共享部署上锁:plugins.isolation 就是普通的 profile 配置,谁能改 profile 的 config.yaml 谁就能关掉它。如果这些 profile 属于你要互相隔离的人,就把它钉在托管作用域里;托管值会压过每个 profile 自己的配置,hermes config set 也拒绝改它:
# /etc/hermes/config.yaml(root 所有,机器上每个 profile 都读它)
plugins:
isolation: host
host:
launcher: [...] # 用了沙箱运行器的话,这里也一并钉住
顺便把 agent 的终端也放到隔离后端上(Docker、SSH 之类),这样 agent 自己碰不到操作员的文件。
hermes plugins validate <dir> 和 hermes plugins show <名字> 会告诉你某个插件是不是跑在宿主里,不是的话原因是什么。就目前插件目录的情况看,348 个条目里有 299 个不用改就能跑在宿主里。
小细节,大用心
- 插件可以注册
hermes_cli.plugins.VALID_HOOKS里那一长串生命周期事件,包括post_tool_call、post_llm_call、pre_api_request、post_api_request、api_request_error、pre_auxiliary_call、post_auxiliary_call、on_stream_start、on_stream_delta、on_stream_end、on_interim_message、on_session_start、on_session_end、on_session_finalize、on_session_reset、agent_loop_stopped、on_skill_lifecycle、subagent_start、subagent_stop、pre_approval_request、post_approval_response、on_human_input_request、on_human_input_resolved、pre_command、kanban_task_claimed、kanban_task_completed、kanban_task_blocked等,具体时机和返回值看 Event Hooks 目录 - 插件安装时可以调两个超时参数:
clone_timeout_seconds(每次 Git clone/fetch/checkout 的截止时间,默认 300,超过 3600 会被夹到 3600;owner/repo/path/to/plugin这种子目录安装只下载那个文件夹的文件)和load_timeout_seconds(单个插件 import + register() 的截止时间,默认 10,设 0 关闭,超过 600 会被夹到 600;超时的插件会被跳过并报load timed out after Ns,其余插件继续加载,卡住的线程被丢弃) - 项目本地插件(
./.hermes/plugins/)网关不会加载:所有会话(hermes、hermes --tui、Desktop、消息网关、cron)都跑在同一个网关里,插件对整个进程只发现一次,从不从你启动会话的目录里找。所以HERMES_ENABLE_PROJECT_PLUGINS=true对会话没有效果——在网关机器上带它启动 CLI/TUI 会话、且该目录的./.hermes/plugins/里有插件时,Hermes 会在会话开头打一条警告,并往gateway.log写同样一行。要用插件就装成用户插件(~/.hermes/plugins/或hermes plugins install),再hermes plugins enable <名字> - 模型看到的工具描述写在
schema["description"]里;ctx.register_tool(description=...)那个是注册表元数据,两处都写的话记得保持同步,模型只认 schema 里的
插件配好了,你的 Hermes 才算真正长出了自己的样子。下篇咱们聊聊怎么让它更懂你!
📖 官方文档
本文根据 Hermes Agent 官方文档编写,原文见:官方文档 › user-guide/features/plugins