Skip to content

fix(plugins): discover nested category plugins in 'plugins list' (#41066)#41076

Closed
Elshayib wants to merge 1 commit into
NousResearch:mainfrom
Elshayib:fix/plugins-list-category-discovery-41066
Closed

fix(plugins): discover nested category plugins in 'plugins list' (#41066)#41076
Elshayib wants to merge 1 commit into
NousResearch:mainfrom
Elshayib:fix/plugins-list-category-discovery-41066

Conversation

@Elshayib

@Elshayib Elshayib commented Jun 7, 2026

Copy link
Copy Markdown
Contributor

Problem

Issue #41066: hermes plugins list only scans immediate subdirectories of ~/.hermes/plugins/ and the bundled plugins/ directory. It does not recurse into nested category directories (web/tavily, image_gen/openai, browser/browser_use, etc.).

This means 12+ bundled category plugins are invisible to the CLI listing, even though they load correctly at runtime via PluginManager._scan_directory_level().

Additionally, _plugin_status() only checks the manifest name against enabled/disabled sets, but for category plugins the config key is path-derived (e.g. web/tavily) while the manifest name differs (e.g. web-tavily). This causes enabled category plugins to show as "not enabled".

Root Cause

Two separate code paths implement plugin discovery:

Component Function Recurses into categories?
Runtime PluginManager._scan_directory_level() Yes (depth <= 2)
CLI list _discover_all_plugins() No (flat only)

Fix

  1. _discover_all_plugins(): Replaced flat iterdir() loop with a recursive _scan_level() helper that mirrors PluginManager._scan_directory_level() — recurses into directories without plugin.yaml up to 2 levels deep, computing the path-derived key with the accumulated prefix.

  2. _plugin_status(): Added optional key parameter. Now checks both manifest name AND path-derived key against the enabled/disabled sets.

  3. All callers updated: _filter_plugin_entries, cmd_list (JSON, plain, table paths), cmd_toggle, dashboard_remove_user_plugin — all now pass the 6-tuple with key.

Testing

  • 21 new tests in tests/hermes_cli/test_plugins_cmd_category_discovery.py
    • _read_manifest_info: flat, category, no manifest, .yml extension
    • _discover_all_plugins: flat, category, mixed, depth cap, 6-tuple, user overrides bundled
    • _plugin_status: name/key in enabled/disabled, precedence
    • _filter_plugin_entries: key-aware enabled filter
    • cmd_list JSON: category plugins included, status uses key
  • All 65 existing test_plugins_cmd.py tests pass (0 regressions)
  • All 83 existing test_plugins.py tests pass (0 regressions)

Fixes #41066

NousResearch#41066)

_discover_all_plugins() previously did a flat iterdir() scan, missing
all category-namespaced plugins (web/*, image_gen/*, browser/*, video_gen/*).
Now recurses up to 2 levels deep, matching PluginManager._scan_directory_level().

Also fixes _plugin_status() to check both manifest name AND path-derived
key against enabled/disabled sets, so category plugins like 'web/tavily'
show correct status when enabled via config.
@alt-glitch alt-glitch added type/bug Something isn't working P3 Low — cosmetic, nice to have comp/cli CLI entry point, hermes_cli/, setup wizard comp/plugins Plugin system and bundled plugins labels Jun 7, 2026
kshitijk4poor added a commit to kshitijk4poor/hermes-agent that referenced this pull request Jun 8, 2026
…ble/disable (NousResearch#41066)

Salvage consolidating three complementary community PRs into one coherent fix:

- NousResearch#41076 (islam666): made `hermes plugins list` discover nested category
  plugins (e.g. `observability/nemo_relay`, `image_gen/openai`).
- NousResearch#41613 (mnajafian-nv): the superset — nested discovery aligned with the
  runtime loader (reuses `PluginManager._scan_directory`/`_scan_entry_points`
  so list state can't drift from what actually loads), PLUS the enable/disable
  mutation side NousResearch#41076 left flat.
- NousResearch#41081 (annguyenNous): identified the `web_server.py` dashboard plugins-hub
  caller of `_discover_all_plugins()`, which must also be updated for the new
  tuple shape + key-aware status.

This combines the best of all three and fixes the mutation-side gap that made
nested bundled plugins untoggleable:

    $ hermes plugins enable nemo_relay
    Plugin 'nemo_relay' is not installed or bundled.   # exit 1 (before)

Now `enable`/`disable`/`toggle`, the `hermes plugins list` views, the dashboard
plugins-hub endpoint, and the dashboard enable/disable helpers all resolve a
bare manifest name OR a full path-derived key to the canonical key the loader
gates on, via a single `_resolve_plugin_reference()` normalization point, and
persist that key while clearing any stale legacy bare-name alias so the two
can't drift. `_plugin_status` is key+legacy-name aware.

`_discover_all_plugins` now returns 6-tuples
`(key, legacy_name, version, description, source, dir_path)`; ALL call sites
(`cmd_list` table/plain/JSON, `_filter_plugin_entries`, `cmd_toggle`,
`dashboard_remove_user_plugin`, and `web_server._merged_plugins_hub`) and the
`test_plugins_cmd_list.py` fixtures are updated to match. Updating the
`web_server.py` caller prevents a `ValueError: too many values to unpack` crash
in the dashboard plugins-hub endpoint.

Verified e2e on the real CLI + runtime loader (isolated HERMES_HOME):
`hermes plugins enable nemo_relay` writes `observability/nemo_relay` to
config.yaml and the loader then loads it (`enabled=True, error=None`); a stale
bare-name alias is cleared on disable (no contradictory state); the dashboard
`_merged_plugins_hub()` runs and lists nested plugins by canonical key.
Full `tests/hermes_cli/test_plugins_cmd*` + web_server plugin tests green.

Closes NousResearch#41066. Supersedes NousResearch#41076, NousResearch#41081, and NousResearch#41613.

Co-authored-by: islam666 <islam666@users.noreply.github.com>
Co-authored-by: mnajafian-nv <mnajafian@nvidia.com>
Co-authored-by: annguyenNous <annguyenNous@users.noreply.github.com>
Co-authored-by: kshitijk4poor <82637225+kshitijk4poor@users.noreply.github.com>
kshitijk4poor added a commit to kshitijk4poor/hermes-agent that referenced this pull request Jun 8, 2026
…ins (follow-up to NousResearch#41076)

NousResearch#41076 makes `hermes plugins list` discover nested category plugins (e.g.
observability/nemo_relay). This adds the missing enable/disable mutation path
so those plugins can actually be toggled, and fixes two incomplete-update
breakages on the NousResearch#41076 base.

Before: `hermes plugins enable nemo_relay` -> "Plugin 'nemo_relay' is not
installed or bundled." (exit 1), because cmd_enable/cmd_disable went through
_plugin_exists(), which only checked top-level plugins/<name>/.

Changes:
- Add _resolve_plugin_key(): resolve a bare manifest/leaf name OR a full
  path-derived key (observability/nemo_relay) to the canonical key the runtime
  loader gates on, reusing NousResearch#41076's _discover_all_plugins(). A bare leaf name
  ambiguous across two categories resolves to None rather than silently picking
  one.
- cmd_enable/cmd_disable resolve first, persist the canonical key, and drop any
  stale legacy bare-name alias so the enabled/disabled lists can't drift into a
  contradictory state. _plugin_exists delegates to the same resolver.
- Fix NousResearch#41076 base breakages: _discover_all_plugins now returns 6-tuples, but
  web_server._merged_plugins_hub() still unpacked 5 (ValueError on the
  dashboard plugins-hub endpoint) and several test_plugins_cmd_list.py fixtures
  were still 5-tuples. Both updated; the hub status check is now key-aware.

Verified e2e on the real CLI + runtime loader (isolated HERMES_HOME):
`hermes plugins enable nemo_relay` writes observability/nemo_relay to
config.yaml and the loader then loads it (enabled=True, error=None); a stale
bare-name alias is cleared on disable; the dashboard _merged_plugins_hub() runs
without crashing. Adds resolution + enable/disable tests; full
tests/hermes_cli/test_plugins_cmd* + web_server plugin tests green.

Follow-up to NousResearch#41076 (NousResearch#41066). Branched from that PR's head.
kshitijk4poor added a commit that referenced this pull request Jun 8, 2026
…ble/disable (#41066)

Merge #42076: nested category plugin discovery + alias-normalized enable/disable (#41066)

Lands the complete nested category plugin fix:
- Discovery in `hermes plugins list` (from @islam666's #41076, carried in this PR)
- Alias-normalized enable/disable mutation path so nested plugins can be toggled
- Fixes the #41076 base breakages (web_server 6-tuple unpack + stale test fixtures)

Co-authored work: discovery by @islam666 (#41076).
Closes #41066.
@kshitijk4poor

Copy link
Copy Markdown
Collaborator

The nested category plugin discovery from this PR landed on main via #42076 (merge commit b99c6c4) — your discovery commit is included as-is and credited. #42076 built on it to add the enable/disable mutation path and fix a few caller/fixture spots that needed the new 6-tuple shape (web_server._merged_plugins_hub + some test_plugins_cmd_list fixtures). Thanks for the discovery work — hermes plugins list now shows nested plugins and they're toggleable.

@Elshayib Elshayib deleted the fix/plugins-list-category-discovery-41066 branch June 8, 2026 17:49
jhjaggars-hermes pushed a commit to jhjaggars/hermes-agent that referenced this pull request Jun 8, 2026
…ins (follow-up to NousResearch#41076)

NousResearch#41076 makes `hermes plugins list` discover nested category plugins (e.g.
observability/nemo_relay). This adds the missing enable/disable mutation path
so those plugins can actually be toggled, and fixes two incomplete-update
breakages on the NousResearch#41076 base.

Before: `hermes plugins enable nemo_relay` -> "Plugin 'nemo_relay' is not
installed or bundled." (exit 1), because cmd_enable/cmd_disable went through
_plugin_exists(), which only checked top-level plugins/<name>/.

Changes:
- Add _resolve_plugin_key(): resolve a bare manifest/leaf name OR a full
  path-derived key (observability/nemo_relay) to the canonical key the runtime
  loader gates on, reusing NousResearch#41076's _discover_all_plugins(). A bare leaf name
  ambiguous across two categories resolves to None rather than silently picking
  one.
- cmd_enable/cmd_disable resolve first, persist the canonical key, and drop any
  stale legacy bare-name alias so the enabled/disabled lists can't drift into a
  contradictory state. _plugin_exists delegates to the same resolver.
- Fix NousResearch#41076 base breakages: _discover_all_plugins now returns 6-tuples, but
  web_server._merged_plugins_hub() still unpacked 5 (ValueError on the
  dashboard plugins-hub endpoint) and several test_plugins_cmd_list.py fixtures
  were still 5-tuples. Both updated; the hub status check is now key-aware.

Verified e2e on the real CLI + runtime loader (isolated HERMES_HOME):
`hermes plugins enable nemo_relay` writes observability/nemo_relay to
config.yaml and the loader then loads it (enabled=True, error=None); a stale
bare-name alias is cleared on disable; the dashboard _merged_plugins_hub() runs
without crashing. Adds resolution + enable/disable tests; full
tests/hermes_cli/test_plugins_cmd* + web_server plugin tests green.

Follow-up to NousResearch#41076 (NousResearch#41066). Branched from that PR's head.
changman pushed a commit to changman/hermes-agent that referenced this pull request Jun 10, 2026
…ins (follow-up to NousResearch#41076)

NousResearch#41076 makes `hermes plugins list` discover nested category plugins (e.g.
observability/nemo_relay). This adds the missing enable/disable mutation path
so those plugins can actually be toggled, and fixes two incomplete-update
breakages on the NousResearch#41076 base.

Before: `hermes plugins enable nemo_relay` -> "Plugin 'nemo_relay' is not
installed or bundled." (exit 1), because cmd_enable/cmd_disable went through
_plugin_exists(), which only checked top-level plugins/<name>/.

Changes:
- Add _resolve_plugin_key(): resolve a bare manifest/leaf name OR a full
  path-derived key (observability/nemo_relay) to the canonical key the runtime
  loader gates on, reusing NousResearch#41076's _discover_all_plugins(). A bare leaf name
  ambiguous across two categories resolves to None rather than silently picking
  one.
- cmd_enable/cmd_disable resolve first, persist the canonical key, and drop any
  stale legacy bare-name alias so the enabled/disabled lists can't drift into a
  contradictory state. _plugin_exists delegates to the same resolver.
- Fix NousResearch#41076 base breakages: _discover_all_plugins now returns 6-tuples, but
  web_server._merged_plugins_hub() still unpacked 5 (ValueError on the
  dashboard plugins-hub endpoint) and several test_plugins_cmd_list.py fixtures
  were still 5-tuples. Both updated; the hub status check is now key-aware.

Verified e2e on the real CLI + runtime loader (isolated HERMES_HOME):
`hermes plugins enable nemo_relay` writes observability/nemo_relay to
config.yaml and the loader then loads it (enabled=True, error=None); a stale
bare-name alias is cleared on disable; the dashboard _merged_plugins_hub() runs
without crashing. Adds resolution + enable/disable tests; full
tests/hermes_cli/test_plugins_cmd* + web_server plugin tests green.

Follow-up to NousResearch#41076 (NousResearch#41066). Branched from that PR's head.
alt-glitch pushed a commit that referenced this pull request Jun 14, 2026
…ins (follow-up to #41076)

#41076 makes `hermes plugins list` discover nested category plugins (e.g.
observability/nemo_relay). This adds the missing enable/disable mutation path
so those plugins can actually be toggled, and fixes two incomplete-update
breakages on the #41076 base.

Before: `hermes plugins enable nemo_relay` -> "Plugin 'nemo_relay' is not
installed or bundled." (exit 1), because cmd_enable/cmd_disable went through
_plugin_exists(), which only checked top-level plugins/<name>/.

Changes:
- Add _resolve_plugin_key(): resolve a bare manifest/leaf name OR a full
  path-derived key (observability/nemo_relay) to the canonical key the runtime
  loader gates on, reusing #41076's _discover_all_plugins(). A bare leaf name
  ambiguous across two categories resolves to None rather than silently picking
  one.
- cmd_enable/cmd_disable resolve first, persist the canonical key, and drop any
  stale legacy bare-name alias so the enabled/disabled lists can't drift into a
  contradictory state. _plugin_exists delegates to the same resolver.
- Fix #41076 base breakages: _discover_all_plugins now returns 6-tuples, but
  web_server._merged_plugins_hub() still unpacked 5 (ValueError on the
  dashboard plugins-hub endpoint) and several test_plugins_cmd_list.py fixtures
  were still 5-tuples. Both updated; the hub status check is now key-aware.

Verified e2e on the real CLI + runtime loader (isolated HERMES_HOME):
`hermes plugins enable nemo_relay` writes observability/nemo_relay to
config.yaml and the loader then loads it (enabled=True, error=None); a stale
bare-name alias is cleared on disable; the dashboard _merged_plugins_hub() runs
without crashing. Adds resolution + enable/disable tests; full
tests/hermes_cli/test_plugins_cmd* + web_server plugin tests green.

Follow-up to #41076 (#41066). Branched from that PR's head.
alt-glitch pushed a commit that referenced this pull request Jun 14, 2026
…ble/disable (#41066)

Merge #42076: nested category plugin discovery + alias-normalized enable/disable (#41066)

Lands the complete nested category plugin fix:
- Discovery in `hermes plugins list` (from @islam666's #41076, carried in this PR)
- Alias-normalized enable/disable mutation path so nested plugins can be toggled
- Fixes the #41076 base breakages (web_server 6-tuple unpack + stale test fixtures)

Co-authored work: discovery by @islam666 (#41076).
Closes #41066.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/cli CLI entry point, hermes_cli/, setup wizard comp/plugins Plugin system and bundled plugins P3 Low — cosmetic, nice to have type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(plugins): hermes plugins list does not discover nested category plugins

3 participants