fix(plugins): credit shared hook/middleware/tool names to every plugin

list_plugins() attribution diffed registry names against all already-loaded
plugins, so when a plugin registered a hook / middleware / tool name an
earlier plugin had already used, the shared name was credited to the first
plugin only and later plugins under-reported (0 hooks) in hermes plugins
list. commands_registered right beside it already attributed correctly by
plugin ownership.

Snapshot per-registry counts before register() and attribute the entries
this plugin's register() actually added (per-registration delta). Add a
regression test: two plugins registering the same hook name are each
credited with 1 hook.
This commit is contained in:
kshitijk4poor 2026-06-12 10:57:25 +05:30
parent 889a13696b
commit 44bd478039
2 changed files with 55 additions and 29 deletions

View file

@ -1532,39 +1532,35 @@ class PluginManager:
logger.warning("Plugin '%s' has no register() function", manifest.name)
else:
ctx = PluginContext(manifest, self)
# Snapshot registry state BEFORE register() so each registry's
# attribution counts only what THIS plugin actually added.
# The previous approach diffed names against all already-loaded
# plugins, which mis-credited a plugin that registered a hook /
# middleware / tool name an earlier plugin had already used:
# the shared name was attributed to the first plugin only, so
# later plugins under-reported in `hermes plugins list`.
_tools_before = set(self._plugin_tool_names)
_hook_counts_before = {
h: len(cbs) for h, cbs in self._hooks.items()
}
_mw_counts_before = {
kind: len(cbs) for kind, cbs in self._middleware.items()
}
register_fn(ctx)
loaded.tools_registered = [
t for t in self._plugin_tool_names
if t not in {
n
for name, p in self._plugins.items()
for n in p.tools_registered
}
if t not in _tools_before
]
loaded.hooks_registered = [
h
for h, cbs in self._hooks.items()
if len(cbs) > _hook_counts_before.get(h, 0)
]
loaded.middleware_registered = [
kind
for kind, cbs in self._middleware.items()
if len(cbs) > _mw_counts_before.get(kind, 0)
]
loaded.hooks_registered = list(
{
h
for h, cbs in self._hooks.items()
if cbs # non-empty
}
- {
h
for name, p in self._plugins.items()
for h in p.hooks_registered
}
)
loaded.middleware_registered = list(
{
kind
for kind, cbs in self._middleware.items()
if cbs
}
- {
kind
for name, p in self._plugins.items()
for kind in p.middleware_registered
}
)
loaded.commands_registered = [
c for c in self._plugin_commands
if self._plugin_commands[c].get("plugin") == manifest.name

View file

@ -1203,6 +1203,36 @@ class TestPluginManagerList:
assert "tools" in p
assert "hooks" in p
def test_shared_hook_name_credited_to_every_plugin(self, tmp_path, monkeypatch):
"""Two plugins registering the SAME hook name are each credited.
Regression: hook/middleware/tool attribution diffed names against all
already-loaded plugins, so when a later plugin registered a hook name
an earlier plugin had already used, the shared name was attributed to
the first plugin only and the later plugin reported 0 hooks in
`hermes plugins list`. Attribution now counts what each plugin's own
register() added (per-registration delta), so both get credit.
"""
plugins_dir = tmp_path / "hermes_test" / "plugins"
_make_plugin_dir(
plugins_dir, "first_hooker",
register_body='ctx.register_hook("post_tool_call", lambda **kw: None)',
)
_make_plugin_dir(
plugins_dir, "second_hooker",
register_body='ctx.register_hook("post_tool_call", lambda **kw: None)',
)
monkeypatch.setenv("HERMES_HOME", str(tmp_path / "hermes_test"))
mgr = PluginManager()
mgr.discover_and_load()
by_name = {p["name"]: p for p in mgr.list_plugins()}
assert by_name["first_hooker"]["hooks"] == 1
assert by_name["second_hooker"]["hooks"] == 1, (
"second plugin sharing a hook name was not credited with its hook"
)
class TestPreLlmCallTargetRouting: