From e3624e00db6ddee7b1bf4009fc19453b5317f2f2 Mon Sep 17 00:00:00 2001 From: jatin godnani Date: Wed, 29 Apr 2026 13:24:50 +0530 Subject: [PATCH] fix: enforce strictly subtractive toolset filtration Refactor tool resolution logic in model_tools.py to ensure that disabled_toolsets are always subtracted at the end, preventing composite toolsets (e.g. 'browser') from implicitly enabling tools that should be hidden. - Added 'disabled_toolsets' to DEFAULT_CONFIG in hermes_cli/config.py - Updated HermesCLI in cli.py to load and propagate disabled toolsets to AIAgent - Implemented robust two-phase resolution (additive then subtractive) in model_tools.py --- cli.py | 7 +++++-- hermes_cli/config.py | 1 + model_tools.py | 15 +++++++++------ 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/cli.py b/cli.py index d045a4e52d..2e50b30a2f 100644 --- a/cli.py +++ b/cli.py @@ -15,9 +15,8 @@ Usage: import logging import os -import re +import platform import shutil -import sys import json import re import concurrent.futures @@ -600,6 +599,7 @@ def load_cli_config() -> Dict[str, Any]: # Load configuration at module startup CLI_CONFIG = load_cli_config() + # Initialize centralized logging early — agent.log + errors.log in ~/.hermes/logs/. # This ensures CLI sessions produce a log trail even before AIAgent is instantiated. try: @@ -2118,6 +2118,8 @@ class HermesCLI: # Parse and validate toolsets self.enabled_toolsets = toolsets + self.disabled_toolsets = CLI_CONFIG["agent"].get("disabled_toolsets") or [] + if toolsets and "all" not in toolsets and "*" not in toolsets: # Validate each toolset — MCP server names are resolved via # live registry aliases (registered during discover_mcp_tools), @@ -3568,6 +3570,7 @@ class HermesCLI: credential_pool=runtime.get("credential_pool"), max_iterations=self.max_turns, enabled_toolsets=self.enabled_toolsets, + disabled_toolsets=self.disabled_toolsets, verbose_logging=self.verbose, quiet_mode=not self.verbose, ephemeral_system_prompt=self.system_prompt if self.system_prompt else None, diff --git a/hermes_cli/config.py b/hermes_cli/config.py index 153b9f5b2d..e765448b7b 100644 --- a/hermes_cli/config.py +++ b/hermes_cli/config.py @@ -457,6 +457,7 @@ DEFAULT_CONFIG = { # remains available as a tool regardless of this setting — the routing # only controls how inbound user images are presented. "image_input_mode": "auto", + "disabled_toolsets": [], }, "terminal": { diff --git a/model_tools.py b/model_tools.py index b991780a61..1eb84d03f9 100644 --- a/model_tools.py +++ b/model_tools.py @@ -23,6 +23,8 @@ Public API (signatures preserved from the original 2,400-line version): import json import asyncio import logging +import os +import sys import threading import time from typing import Dict, Any, List, Optional, Tuple @@ -356,12 +358,17 @@ def _compute_tool_definitions( else: if not quiet_mode: print(f"⚠️ Unknown toolset: {toolset_name}") - - elif disabled_toolsets: + else: + # Default: start with everything from toolsets import get_all_toolsets for ts_name in get_all_toolsets(): tools_to_include.update(resolve_toolset(ts_name)) + # Always apply disabled toolsets as a subtraction step at the end. + # This ensures that even if a composite toolset (like hermes-cli) + # is enabled, any tools belonging to a disabled toolset are strictly + # stripped out. See issue #15291. + if disabled_toolsets: for toolset_name in disabled_toolsets: if validate_toolset(toolset_name): resolved = resolve_toolset(toolset_name) @@ -376,10 +383,6 @@ def _compute_tool_definitions( else: if not quiet_mode: print(f"⚠️ Unknown toolset: {toolset_name}") - else: - from toolsets import get_all_toolsets - for ts_name in get_all_toolsets(): - tools_to_include.update(resolve_toolset(ts_name)) # Plugin-registered tools are now resolved through the normal toolset # path — validate_toolset() / resolve_toolset() / get_all_toolsets()