fix(tools): /tools shows the full pre-assembly catalog; adapt tests to tiered disclosure

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.
This commit is contained in:
Teknium 2026-07-22 05:31:43 -07:00
parent a521ed703b
commit 8f6ecbae26
No known key found for this signature in database
4 changed files with 37 additions and 13 deletions

6
cli.py
View file

@ -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")

View file

@ -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 ──────────────────────────────────────────────────

View file

@ -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,

View file

@ -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"]):