From dd55800c90db11b4a940ef247b99aac6b73aaceb Mon Sep 17 00:00:00 2001 From: byjaps Date: Sat, 19 Sep 2026 22:38:02 +0100 Subject: [PATCH] fix(cli): don't flag plugin toolsets as unknown at startup Plugins register their toolsets during background discovery, but the CLI validates the configured toolset list while the instance is being constructed -- i.e. before that thread has landed. Judging by the live registry alone therefore reports every configured plugin toolset as a typo on every launch. The warning is not merely cosmetic: it is written to the console, so one-shot and quiet runs (-q / -Q, --format stream-json) hand it to whatever parses their output; an integration reading the response can receive the warning line instead of the answer. Skip names the plugin registry knows about, mirroring how MCP server names are already skipped here. Names persisted by the previous launch's discovery sweep are served by get_plugin_toolset_keys_nowait, so this stays non-blocking on startup. A genuinely unknown name still warns. Refs #71650 (#95529 is the duplicate report of the same bug) Co-authored-by: adamkrawczyk --- cli.py | 17 ++++++- .../emails/byjaps@users.noreply.github.com | 2 + tests/hermes_cli/test_cli_init.py | 48 +++++++++++++++++++ 3 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 contributors/emails/byjaps@users.noreply.github.com diff --git a/cli.py b/cli.py index 1e4d5a5d9bd9..ef065245152f 100644 --- a/cli.py +++ b/cli.py @@ -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)}[/]") diff --git a/contributors/emails/byjaps@users.noreply.github.com b/contributors/emails/byjaps@users.noreply.github.com new file mode 100644 index 000000000000..b2b0e654914f --- /dev/null +++ b/contributors/emails/byjaps@users.noreply.github.com @@ -0,0 +1,2 @@ +byjaps +# PR #116425 fix(cli) plugin toolsets must not be flagged as unknown at startup diff --git a/tests/hermes_cli/test_cli_init.py b/tests/hermes_cli/test_cli_init.py index 7bcb49b1639a..83ba5d041bc3 100644 --- a/tests/hermes_cli/test_cli_init.py +++ b/tests/hermes_cli/test_cli_init.py @@ -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] + +