Skip to content

fix(plugins): register entry-point plugins declared as module:function - #72094

Open
mannnrachman wants to merge 1 commit into
NousResearch:mainfrom
mannnrachman:fix/entrypoint-plugin-register-function
Open

fix(plugins): register entry-point plugins declared as module:function#72094
mannnrachman wants to merge 1 commit into
NousResearch:mainfrom
mannnrachman:fix/entrypoint-plugin-register-function

Conversation

@mannnrachman

Copy link
Copy Markdown
Contributor

What does this PR do?

Makes pip-installed plugins with module:function entry points actually register.

importlib.metadata.EntryPoint.load() resolves the module:function form to the referenced attribute (typically the register callable itself), not the module. PluginManager._load_plugin() then looked for a .register attribute on that function object, found none, and logged Plugin '<name>' has no register() function — once per discovery pass, which for a gateway with an every-minute cron tick means a warning per minute, forever. Such plugins could never register at all.

The loader now detects a non-module callable returned by ep.load() and uses it directly as the register function. LoadedPlugin.module is resolved from sys.modules via the callable's __module__ so attribution and hermes plugins list reporting stay intact. Bare module entry points and directory plugins are unaffected.

Related Issue

Fixes #72052

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)
  • ✨ New feature (non-breaking change that adds functionality)
  • 🔒 Security fix
  • 📝 Documentation update
  • ✅ Tests (adding or improving test coverage)
  • ♻️ Refactor (no behavior change)
  • 🎯 New skill (bundled or hub)

Changes Made

  • hermes_cli/plugins.py
    • _load_plugin(): if the entry-point load result is a callable that is not a module, treat it as the register function directly and resolve the owning module from sys.modules for LoadedPlugin.module.
    • _load_entrypoint_module(): widen the return annotation and document that ep.load() returns the module for bare module entry points, or the referenced attribute for the module:function form.
  • tests/hermes_cli/test_plugins.py
    • New regression test test_entry_point_function_form_registers whose mocked ep.load() returns the function — mirroring real importlib behavior. The pre-existing entry-point test masked this bug because its mock returned the module even though its declared entry-point value was module:function.

How to Test

  1. Install any pip plugin whose entry point is declared name = package.module:register (e.g. mnemosyne-hermes), enable it via hermes plugins enable <name>, and confirm it registers instead of warning has no register() function on every discovery pass.
  2. Run the focused suite:
scripts/run_tests.sh tests/hermes_cli/test_plugins.py
  1. Run the cross-platform check:
python3 scripts/check-windows-footguns.py --diff origin/main

Validation Results

  • tests/hermes_cli/test_plugins.py: 117 passed, 0 failed (Python 3.11.15, Ubuntu 22.04 x86_64).
  • The new regression test fails on unpatched main with exactly the reported error (AssertionError: no register() function) and passes with the fix.
  • Windows-footgun diff check: passed (0 findings).
  • The full repository suite was not run locally; hosted CI remains authoritative for the complete matrix.

Checklist

Code

Documentation & Housekeeping

  • Documentation N/A — no user-facing command or configuration key was added
  • cli-config.yaml.example N/A — no configuration key was added or changed
  • CONTRIBUTING.md / AGENTS.md N/A — no architecture or contributor workflow changed
  • Cross-platform impact considered; scripts/check-windows-footguns.py --diff origin/main passes
  • Tool descriptions/schemas N/A — no tool API changed

Screenshots / Logs

No UI changes. Loader behavior before/after on a module:function entry point:

before: WARNING hermes_cli.plugins: Plugin 'fn_plugin' has no register() function   (every discovery pass)
after : registered — enabled=True, error=None, register(ctx) called once

🤖 Generated with Claude Code

@mannnrachman

Copy link
Copy Markdown
Contributor Author

The check-windows-footguns.py --all CI failure is pre-existing on main, not introduced here: a2c42be missed one bare write_text at scripts/tool_search_livetest2.py:190, so the --all gate currently fails for every PR. Fix submitted separately as #72097; this PR's own diff passes --diff clean.

@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 Jul 26, 2026
@teknium1

Copy link
Copy Markdown
Contributor

Thanks for the focused fix. The premise remains valid on current main: hermes_cli/plugins.py:1786 receives the entry-point result and hermes_cli/plugins.py:1791 only looks for .register, while the documented provider entry-point form is acme_hermes_plugin:register at website/docs/developer-guide/model-provider-plugin.md:254-258.

Problems

  • The regression-test hunk needs relocation during salvage. Its anchor, test_entry_points_scanned, was removed from current main by 39975613b13b418e0eceda178434d7be90ad4f91; current tests/hermes_cli/test_plugins.py therefore has no equivalent entry-point loader test to patch beside.

Suggested changes

  • Preserve the proposed callable-return regression under the remaining TestPluginDiscovery coverage when applying the production change.

This is an automated hermes-sweeper review.

@teknium1 teknium1 added sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:blast-contained Sweeper blast radius: contained — one narrow path / opt-in / few users labels Jul 30, 2026
EntryPoint.load() resolves the module:function form to the referenced
attribute (typically the register callable itself), not its module. The
loader then looked for a .register attribute on that function object,
found none, and warned "Plugin '<name>' has no register() function" on
every discovery pass — so pip plugins with module:function entry points
never registered at all.

Detect a non-module callable from ep.load() and use it directly as the
register function, resolving LoadedPlugin.module from sys.modules via
the callable's __module__ for attribution.

The existing entry-point test masked this because its mocked ep.load()
returned the module even though its declared value was module:function;
the new regression test mirrors real importlib behavior.

Fixes NousResearch#72052

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@mannnrachman
mannnrachman force-pushed the fix/entrypoint-plugin-register-function branch from 221a7b8 to 00633f9 Compare July 30, 2026 13:53
@GottZ

GottZ commented Aug 3, 2026

Copy link
Copy Markdown

This was generated by AI during triage.

Summary

One PR addresses issue #72052. #72094 directly fixes the callable-versus-module mismatch by treating the callable returned for a module:function entry point as the registration function while retaining its owning module for attribution.

Related pull requests

Suggested consolidation

Keep #72094 open with a salvage path: retain its callable-return loader fix and its assertions for successful registration, absence of a loader error, and owning-module attribution, but rebase and relocate the regression coverage to the current TestPluginDiscovery structure. It is the only PR for #72052, so there are no duplicate PRs to close.

Complex graph

flowchart LR
    classDef open fill:#dbeafe,stroke:#1d4ed8,color:#1e3a8a
    classDef merged fill:#dcfce7,stroke:#15803d,color:#14532d
    classDef closed fill:#e5e7eb,stroke:#6b7280,color:#1f2937
    classDef unverified fill:#f3f4f6,stroke:#9ca3af,color:#374151
    classDef best stroke-width:3px,stroke:#b45309
    classDef target stroke-width:3px,stroke:#4338ca
    I72052(["issue #72052 (open)"])
    P72094["PR #72094 (open)"]
    P72094 -->|best fix| I72052
    class I72052 open
    class P72094 open
    class P72094 best
    class P72094 target
    click I72052 "https://github.com/NousResearch/hermes-agent/issues/72052"
    click P72094 "https://github.com/NousResearch/hermes-agent/pull/72094"
Loading

Graph: solid arrow = fixes / best fix, dashed arrow = partial or unverified (see edge label); boxed group = PRs duplicating each other; amber border = best fix; indigo border = target; gray node = closed (state tag in the node label).

Cross-PR triage: Reviewed 1 pull request and 1 issue in this complex. Each diff was read against this issue; Assessment working set: 4 kB of PR diffs, 8 kB of issue/PR text, <1 kB of discussion (1 comments), 2 verify verdicts. verdicts reflect diff content, not PR titles. Part of an automated triage batch.

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 sweeper:blast-contained Sweeper blast radius: contained — one narrow path / opt-in / few users sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Entry-point plugins declared as module:function never register — loader calls getattr(register, 'register') and warns every discovery pass

4 participants