Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -2750,7 +2750,22 @@ def _init_toolsets(self, toolsets):
if toolsets and "all" not in toolsets and "*" not in toolsets:
# MCP server names only resolve after discover_mcp_tools runs; skip them here.
mcp_names = set((CLI_CONFIG.get("mcp_servers") or {}).keys())
invalid = [t for t in toolsets if not validate_toolset(t) and t not in mcp_names]
# Plugin toolsets register during plugin discovery, which the startup path
# runs in a background thread that has not necessarily landed in the live
# registry yet. Names declared by plugins -- or persisted by the previous
# launch's discovery sweep, which get_plugin_toolset_keys_nowait serves --
# are not typos, so they must not be reported as unknown ones here.
try:
from hermes_cli.plugins import get_plugin_toolset_keys_nowait

plugin_ts_names = get_plugin_toolset_keys_nowait()
except Exception:
plugin_ts_names = set()
invalid = [
t
for t in toolsets
if not validate_toolset(t) and t not in mcp_names and t not in plugin_ts_names
]
if invalid:
self._console_print(f"[bold red]Warning: Unknown toolsets: {', '.join(invalid)}[/]")

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
byjaps
# PR #116425 fix(cli) plugin toolsets must not be flagged as unknown at startup
48 changes: 48 additions & 0 deletions tests/hermes_cli/test_cli_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -763,4 +763,52 @@ def test_flat_string_default_untouched(self):
assert result["model"]["provider"] == "auto"


class TestPluginToolsetStartupValidation:
"""A toolset that is merely *not registered yet* must not be reported as unknown.

Plugins register their toolsets during background discovery, while the CLI validates
the configured list during construction -- i.e. before that thread has landed. Judging
by the live registry alone therefore flags every configured plugin toolset as a typo on
every launch, including one-shot/quiet runs whose stdout is machine-parsed.
"""

@staticmethod
def _init_toolsets(monkeypatch, toolsets, *, registry, plugin_keys):
import cli as _cli_mod

stub = object.__new__(_cli_mod.HermesCLI)
printed: list[str] = []
stub._console_print = printed.append
monkeypatch.setattr(_cli_mod, "validate_toolset", lambda name: name in registry)
monkeypatch.setattr(_cli_mod, "CLI_CONFIG", {"agent": {}})
monkeypatch.setattr(
"hermes_cli.plugins.get_plugin_toolset_keys_nowait",
lambda: set(plugin_keys),
)
stub._init_toolsets(list(toolsets))
return stub, printed

def test_plugin_toolset_not_yet_registered_is_not_flagged(self, monkeypatch):
stub, printed = self._init_toolsets(
monkeypatch,
["terminal", "voice_stack"],
registry={"terminal"},
plugin_keys={"voice_stack"},
)
assert printed == []
# The configured list is kept verbatim; only the false warning is silenced.
assert stub.enabled_toolsets == ["terminal", "voice_stack"]

def test_real_typo_still_warns(self, monkeypatch):
_, printed = self._init_toolsets(
monkeypatch,
["terminal", "voice_stak"],
registry={"terminal"},
plugin_keys={"voice_stack"},
)
assert len(printed) == 1
assert "voice_stak" in printed[0]
assert "voice_stack" not in printed[0]