From 8f6ecbae26ff7d6754ee6e566d260736f762d0ff Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Wed, 22 Jul 2026 05:31:43 -0700 Subject: [PATCH] fix(tools): /tools shows the full pre-assembly catalog; adapt tests to tiered disclosure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI slices 2 and 7 caught three tests broken by always-defer: - /tools (CLI show_tools + TUI gateway tools.show) now passes skip_tool_search_assembly=True — it's a discovery/inspection surface, so users verifying an MCP installed must see deferred tools, not a collapsed bridge row. This also fixes test_slash_worker_mcp_discovery (profile MCP tool visible in /tools). - test_plugins.py::test_plugin_tools_in_definitions: 'visible' becomes 'reachable' — direct schema OR listed in the bridge description; scope-exclusion assertions unchanged (not direct AND not listed). - test_discord_tool.py dynamic-schema-rebuild test reads the pre-assembly list (the rebuilt schema is what tool_describe serves). Banner/status tool counts intentionally keep the post-assembly view — they reflect what the model actually sees. --- cli.py | 6 +++++- tests/hermes_cli/test_plugins.py | 33 ++++++++++++++++++++++---------- tests/tools/test_discord_tool.py | 6 +++++- tui_gateway/server.py | 5 ++++- 4 files changed, 37 insertions(+), 13 deletions(-) diff --git a/cli.py b/cli.py index 8850a422831e..c3d8a94ea1fc 100644 --- a/cli.py +++ b/cli.py @@ -6874,7 +6874,11 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin): def show_tools(self): """Display available tools with kawaii ASCII art.""" - tools = get_tool_definitions(enabled_toolsets=self.enabled_toolsets, quiet_mode=True) + # Pre-assembly list: /tools is a discovery/inspection surface, so it + # must show the full catalog including tools deferred behind the + # tool_search bridge (users check this to verify an MCP installed). + tools = get_tool_definitions(enabled_toolsets=self.enabled_toolsets, quiet_mode=True, + skip_tool_search_assembly=True) if not tools: print("(;_;) No tools available") diff --git a/tests/hermes_cli/test_plugins.py b/tests/hermes_cli/test_plugins.py index 3fdb6d1812d2..ee9746a34de9 100644 --- a/tests/hermes_cli/test_plugins.py +++ b/tests/hermes_cli/test_plugins.py @@ -1544,7 +1544,14 @@ class TestPluginToolVisibility: """Plugin-registered tools appear in get_tool_definitions().""" def test_plugin_tools_in_definitions(self, tmp_path, monkeypatch): - """Plugin tools are included when their toolset is in enabled_toolsets.""" + """Plugin tools are reachable when their toolset is in enabled_toolsets. + + Under tiered disclosure (any MCP/plugin tool defers behind the + tool_search bridge), a plugin tool no longer appears as a direct + schema — it is deferred and surfaced via the bridge's catalog + listing. 'Reachable' therefore means: present directly OR listed + in the tool_search bridge description. + """ import hermes_cli.plugins as plugins_mod plugins_dir = tmp_path / "hermes_test" / "plugins" @@ -1572,20 +1579,26 @@ class TestPluginToolVisibility: from model_tools import get_tool_definitions - # Plugin tools are included when their toolset is explicitly enabled + def _reachable(tools): + names = [t["function"]["name"] for t in tools] + if "vis_tool" in names: + return True # tool_search inactive → direct schema + search = next((t for t in tools + if t["function"]["name"] == "tool_search"), None) + return bool(search and "vis_tool" in search["function"]["description"]) + + # Reachable when its toolset is explicitly enabled tools = get_tool_definitions(enabled_toolsets=["terminal", "plugin_vis_plugin"], quiet_mode=True) - tool_names = [t["function"]["name"] for t in tools] - assert "vis_tool" in tool_names + assert _reachable(tools) - # Plugin tools are excluded when only other toolsets are enabled + # Excluded entirely when only other toolsets are enabled — not + # direct, not in the deferred listing. tools2 = get_tool_definitions(enabled_toolsets=["terminal"], quiet_mode=True) - tool_names2 = [t["function"]["name"] for t in tools2] - assert "vis_tool" not in tool_names2 + assert not _reachable(tools2) - # Plugin tools are included when no toolset filter is active (all enabled) + # Reachable when no toolset filter is active (all enabled) tools3 = get_tool_definitions(quiet_mode=True) - tool_names3 = [t["function"]["name"] for t in tools3] - assert "vis_tool" in tool_names3 + assert _reachable(tools3) # ── TestPluginManagerList ────────────────────────────────────────────────── diff --git a/tests/tools/test_discord_tool.py b/tests/tools/test_discord_tool.py index 2a7d2793aaaf..2fe22c032238 100644 --- a/tests/tools/test_discord_tool.py +++ b/tests/tools/test_discord_tool.py @@ -1268,7 +1268,11 @@ class TestModelToolsIntegration: mock_req.return_value = {"flags": 0} from model_tools import get_tool_definitions - tools = get_tool_definitions(enabled_toolsets=["hermes-discord"], quiet_mode=True) + # skip_tool_search_assembly: this test exercises the dynamic schema + # rebuild; under tiered disclosure the discord tools defer behind the + # bridge, but the rebuilt schema is what tool_describe serves. + tools = get_tool_definitions(enabled_toolsets=["hermes-discord"], quiet_mode=True, + skip_tool_search_assembly=True) discord_admin_tool = next( (t for t in tools if t.get("function", {}).get("name") == "discord_admin"), None, diff --git a/tui_gateway/server.py b/tui_gateway/server.py index 96e9484767d6..a619435f0dd2 100644 --- a/tui_gateway/server.py +++ b/tui_gateway/server.py @@ -15980,7 +15980,10 @@ def _(rid, params: dict) -> dict: if session else _load_enabled_toolsets() ) - tools = get_tool_definitions(enabled_toolsets=enabled, quiet_mode=True) + # Pre-assembly list: /tools is a discovery surface and must show + # tools deferred behind the tool_search bridge (same as the CLI). + tools = get_tool_definitions(enabled_toolsets=enabled, quiet_mode=True, + skip_tool_search_assembly=True) sections = {} for tool in sorted(tools, key=lambda t: t["function"]["name"]):