Plugins that only half-worked: Hermes finally unified plugin name resolution


You open the Hermes Desktop plugin hub and install a plugin that ships a desktop panel. The Agent switch lights up, but the panel next to it never does — you go digging and the tools are there, the commands are there, the UI simply is not. Or something simpler: hermes plugins install hermes-memory-wiki says it worked, you follow up with hermes plugins enable hermes-memory-wiki, and it answers No plugin named hermes-memory-wiki. You just installed the thing and it claims not to know the name. You did not misremember it — Hermes had three naming resolvers that disagreed with each other. PR #135030, merged on October 8, cleaned it up.

The symptom: one plugin, three names

Start with the symptoms, because these three errors look unrelated and share one root cause:

  • Installs fine, refuses to be managed. The name a catalog install lands under and the name in the plugin’s own manifest can differ. The numbers are specific: 17 of 484 catalog entries install under a different manifest name — hermes-memory-wiki → memory-wiki, touchdesigner → td, hermes-subscription-meter → subscription-meter. install resolves the catalog name, so it succeeds; but enable, disable, show, remove and update resolved the manifest name or key, so every one of them failed on that same name.
  • One package renders as two rows. Clone it yourself to ~/.hermes/plugins/hermes-subscription-meter, as the plugin’s own README suggests, and the desktop hub shows two rows: a lone desktop half offering “Install here” for an agent half that is already installed, plus a separate agent row. The desktop side joined the agent row by manifest name and its own half by folder name.
  • The switch only covers one half. A desktop half ships opt-in, so turning the Agent switch on never turned the desktop half on with it.

What those three have in common: the name was being resolved in several places, by different rules.

The cause: five resolvers, five rule sets

Before the change there were five places that resolved a plugin name, spread across three paths:

Where What it matched on
hermes_cli/plugins_cmd.py _resolve_plugin_key_and_source, with copies at _find_plugin_entry, _require_installed_plugin and _user_installed_plugin_dir manifest name / canonical key / unique leaf / folder name — each with a different supported subset
plugins_cmd_install.py _catalog_install_on_disk the catalog name, through the install record — install time only
Desktop plugin-packages.ts vs electron/desktop-plugins-root.ts one joined on manifest name, the other on the folder’s basename

“Each with a different supported subset” is the part that actually hurt. The bug was not that a name did not work; it was that two consecutive commands in the same sequence did not agree on the rules, which is how you end up with a successful install followed by a failing enable.

Now: one resolver, one switch

After the change:

  • _find_plugin_entry is the single resolver, used by every verb (enable, disable, show, capabilities, remove, update, and both the dashboard and the Desktop hub). It tries the canonical key or manifest name first, then a unique bare leaf (langfuse → observability/langfuse), then the catalog name the plugin was installed under. It also makes one promise: it never guesses between two candidates — ambiguity fails instead of picking one.
  • The catalog-name match reads the installer’s own record (.install-metadata.json), never a file inside the plugin’s repository. That matters: a URL-installed repo cannot claim a catalog entry’s name using something it shipped itself.
  • One switch turns a whole package on. “Enable agent plugin after install” in the install dialog (or the row’s Agent switch) now turns the desktop half on too — unless you switched that half off yourself. It does not work in reverse: switching off stays per half, because the desktop half is shared by every profile.

On the command line, both names now resolve:

# the classic mismatch: the touchdesigner entry installs as td
hermes plugins install touchdesigner
hermes plugins enable touchdesigner   # same as: hermes plugins enable td

# turn the switch on at install time and skip the second step
hermes plugins install touchdesigner --enable

Before, only hermes plugins install <name> took the catalog name and the other verbs errored out; now enable / disable / show / capabilities / update / remove accept either. Our hermes plugins command reference is the flags list, and the plugin catalog post covers the trust model behind the entries.

Also fixed: “I edited the code and nothing changed”

The same batch includes PR #135017, which solves the same class of problem from a developer’s seat: you edit desktop/plugin.js and the Desktop keeps running a copy.

Electron copies plugins/<name>/desktop/ to desktop-plugins/<name>/, and the renderer loads that copy. It only re-copies when the source plugin.js mtime is newer and something explicitly asks for it; the hot-reload watcher watches the copy, never the source. Worse: the Python loader follows a symlink inside plugins/, but the reconcile code listed packages with isDirectory() — and a symlink is not a directory — so the most natural development setup, a symlinked checkout, never got its desktop half copied at all.

What it does now:

# treat this as dev mode
ln -s ~/src/my-plugin ~/.hermes/plugins/my-plugin

# installed copy edited in place? let doctor tell you whether you are running a stale one
hermes plugins doctor my-plugin

Symlinked packages are recognised (the marker carries linked: true), the copy re-syncs on any byte difference rather than an mtime gate (so a git stash pop works), and the source file is watched so a save hot-reloads the plugin. Normally-installed packages keep the mtime rule. hermes plugins doctor <id> now says plainly that the Desktop is running a stale copy, names it, and tells you how to refresh it (rescan or restart).

Status: on main, in no tag yet

To be unambiguous: #135030, #135017 and #135033 all merged into main on October 8 and are not in the v0.21.6 tag — the tag was cut before them. A git install tracking main has the new resolver today; anything running the tag (Docker images included) does not yet. The check is the command from above: if hermes plugins enable touchdesigner passes, you have the new code; if it says No plugin named, you have the old code.

Wrapping up

  • The symptoms were “installs fine, refuses to be managed” and “the desktop half never lights up”; the cause was five resolvers with different rules.
  • There is one resolver now, _find_plugin_entry, with the order key/manifest name → unique leaf → catalog name from the install record, and it fails on ambiguity instead of guessing.
  • Catalog names resolve through the installer’s own record, so a foreign repo cannot impersonate a catalog entry.
  • One switch per package; switching off is still per half, because the desktop half is shared across profiles.
  • Desktop plugin development over a symlink now works, and hermes plugins doctor points at a stale copy.

If your plugins installed cleanly and you are not building desktop plugins, this change is “more predictable behaviour”, not a migration. To go deeper, read plugin isolation into a host process and the MCP trust gate for portable packages — those two are about where the code runs and what it may touch; this one was about what it is called.