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
This commit is contained in:
jatin godnani
2026-04-29 13:24:50 +05:30
committed by Teknium
parent 8e58265b60
commit e3624e00db
3 changed files with 15 additions and 8 deletions

7
cli.py
View File

@@ -15,9 +15,8 @@ Usage:
import logging import logging
import os import os
import re import platform
import shutil import shutil
import sys
import json import json
import re import re
import concurrent.futures import concurrent.futures
@@ -600,6 +599,7 @@ def load_cli_config() -> Dict[str, Any]:
# Load configuration at module startup # Load configuration at module startup
CLI_CONFIG = load_cli_config() CLI_CONFIG = load_cli_config()
# Initialize centralized logging early — agent.log + errors.log in ~/.hermes/logs/. # Initialize centralized logging early — agent.log + errors.log in ~/.hermes/logs/.
# This ensures CLI sessions produce a log trail even before AIAgent is instantiated. # This ensures CLI sessions produce a log trail even before AIAgent is instantiated.
try: try:
@@ -2118,6 +2118,8 @@ class HermesCLI:
# Parse and validate toolsets # Parse and validate toolsets
self.enabled_toolsets = 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: if toolsets and "all" not in toolsets and "*" not in toolsets:
# Validate each toolset — MCP server names are resolved via # Validate each toolset — MCP server names are resolved via
# live registry aliases (registered during discover_mcp_tools), # live registry aliases (registered during discover_mcp_tools),
@@ -3568,6 +3570,7 @@ class HermesCLI:
credential_pool=runtime.get("credential_pool"), credential_pool=runtime.get("credential_pool"),
max_iterations=self.max_turns, max_iterations=self.max_turns,
enabled_toolsets=self.enabled_toolsets, enabled_toolsets=self.enabled_toolsets,
disabled_toolsets=self.disabled_toolsets,
verbose_logging=self.verbose, verbose_logging=self.verbose,
quiet_mode=not self.verbose, quiet_mode=not self.verbose,
ephemeral_system_prompt=self.system_prompt if self.system_prompt else None, ephemeral_system_prompt=self.system_prompt if self.system_prompt else None,

View File

@@ -457,6 +457,7 @@ DEFAULT_CONFIG = {
# remains available as a tool regardless of this setting — the routing # remains available as a tool regardless of this setting — the routing
# only controls how inbound user images are presented. # only controls how inbound user images are presented.
"image_input_mode": "auto", "image_input_mode": "auto",
"disabled_toolsets": [],
}, },
"terminal": { "terminal": {

View File

@@ -23,6 +23,8 @@ Public API (signatures preserved from the original 2,400-line version):
import json import json
import asyncio import asyncio
import logging import logging
import os
import sys
import threading import threading
import time import time
from typing import Dict, Any, List, Optional, Tuple from typing import Dict, Any, List, Optional, Tuple
@@ -356,12 +358,17 @@ def _compute_tool_definitions(
else: else:
if not quiet_mode: if not quiet_mode:
print(f"⚠️ Unknown toolset: {toolset_name}") print(f"⚠️ Unknown toolset: {toolset_name}")
else:
elif disabled_toolsets: # Default: start with everything
from toolsets import get_all_toolsets from toolsets import get_all_toolsets
for ts_name in get_all_toolsets(): for ts_name in get_all_toolsets():
tools_to_include.update(resolve_toolset(ts_name)) 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: for toolset_name in disabled_toolsets:
if validate_toolset(toolset_name): if validate_toolset(toolset_name):
resolved = resolve_toolset(toolset_name) resolved = resolve_toolset(toolset_name)
@@ -376,10 +383,6 @@ def _compute_tool_definitions(
else: else:
if not quiet_mode: if not quiet_mode:
print(f"⚠️ Unknown toolset: {toolset_name}") 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 # Plugin-registered tools are now resolved through the normal toolset
# path — validate_toolset() / resolve_toolset() / get_all_toolsets() # path — validate_toolset() / resolve_toolset() / get_all_toolsets()