fix(toolsets): preserve core tools when a posture toolset is in disabled_toolsets (#57315)

The disabled_toolsets subtraction loop in _compute_tool_definitions
preserved shared core tools only for hermes-* platform bundles (#33924),
subtracting bundle_non_core_tools(); every other name took the else
branch and got a full resolve_toolset() subtraction. The `coding`
toolset is a posture toolset (posture: True) that re-lists the shared
_HERMES_CORE_TOOLS it does not own, so disabled_toolsets=["coding"]
stripped those core tools from the whole schema (34 tools collapsed to a
handful; terminal/read_file/write_file/web_search/execute_code gone).

Extend the core-preserving branch to also match posture toolsets, so
they subtract only the non-core delta. Only `coding` carries
posture: True, so atomic toolsets stay fully removable. The
bundle-misconfiguration info log is gated to hermes-* names, since its
wording is bundle-specific and disabled_toolsets=["coding"] is a
legitimate config written by older `hermes setup` runs.

Adds a regression test (TestDisabledToolsetsPostureToolset) alongside
the existing #33924 bundle tests.
This commit is contained in:
Brett Bonner 2026-07-03 01:24:43 -04:00 committed by Teknium
parent b57fe5ca01
commit e5636da5d8
2 changed files with 61 additions and 7 deletions

View file

@ -399,16 +399,19 @@ def _compute_tool_definitions(
if disabled_toolsets:
for toolset_name in disabled_toolsets:
if validate_toolset(toolset_name):
if toolset_name.startswith("hermes-"):
# Platform bundles (hermes-*) include _HERMES_CORE_TOOLS, so
# subtracting the whole bundle would strip core tools shared
# by other enabled toolsets and empty the tool list (#33924).
# Subtract only the bundle's non-core delta; keep core.
from toolsets import bundle_non_core_tools
from toolsets import bundle_non_core_tools, get_toolset
if toolset_name.startswith("hermes-") or (get_toolset(toolset_name) or {}).get("posture"):
# Platform bundles (hermes-*) include _HERMES_CORE_TOOLS, and
# posture toolsets (`posture: True`, e.g. `coding`) re-list
# those same core tools without owning them, so subtracting
# the whole toolset would strip core tools shared by other
# enabled toolsets and empty the tool list (#33924, #57315).
# Subtract only the non-core delta; keep core.
to_remove = bundle_non_core_tools(toolset_name)
tools_to_include.difference_update(to_remove)
resolved = sorted(to_remove)
if not quiet_mode and toolset_name not in _WARNED_DISABLED_BUNDLES:
if (not quiet_mode and toolset_name.startswith("hermes-")
and toolset_name not in _WARNED_DISABLED_BUNDLES):
_WARNED_DISABLED_BUNDLES.add(toolset_name)
logger.info(
"agent.disabled_toolsets contains platform-bundle "

View file

@ -536,3 +536,54 @@ class TestDisabledToolsetsPlatformBundle:
from toolsets import bundle_non_core_tools
# A non-existent bundle resolves to an empty set (no tools), not a crash.
assert bundle_non_core_tools("hermes-does-not-exist") == set()
class TestDisabledToolsetsPostureToolset:
"""Regression test for #57315: disabling a posture toolset (`coding`,
posture: True) must preserve the shared core tools it re-lists but does
not own -- same non-core-delta subtraction as hermes-* bundles (#33924) --
while atomic toolsets stay fully removable."""
def test_disabling_coding_preserves_core_but_atomic_disables_still_remove(self):
from model_tools import get_tool_definitions
# web_search is check_fn-gated (needs an API key); probe only the core
# tools actually present in baseline so gating cannot mask the fix.
core_probe = {"terminal", "read_file", "write_file", "web_search", "execute_code"}
baseline = {
t["function"]["name"]
for t in get_tool_definitions(quiet_mode=True)
}
present_core = core_probe & baseline
# Sanity: at least some probed core tools are available in this env.
assert present_core, "no probed core tools present in baseline"
no_coding = {
t["function"]["name"]
for t in get_tool_definitions(
disabled_toolsets=["coding"], quiet_mode=True
)
}
# Previously the full resolve_toolset("coding") subtraction stripped
# these shared core tools, collapsing the schema to a handful (#57315).
assert present_core <= no_coding, (
f"Core tools stripped by disabling 'coding': {present_core - no_coding}"
)
# Atomic (non-posture) toolsets must still be fully removable.
no_terminal = {
t["function"]["name"]
for t in get_tool_definitions(
disabled_toolsets=["terminal"], quiet_mode=True
)
}
assert "terminal" not in no_terminal
no_file = {
t["function"]["name"]
for t in get_tool_definitions(
disabled_toolsets=["file"], quiet_mode=True
)
}
assert "write_file" not in no_file