# NousResearch/hermes-agent · disabled_toolsets composite bundles stripping explicitly enabled tools

## Open now

- 0 issues, 1 PR open on GitHub.
- PR #60399: fix(tools): prevent composite disabled_toolsets from stripping explicitly enabled tools

Most members concern how disabling a composite or platform toolset bundle (e.g. \`coding\`, \`hermes-yuanbao\`) in \`disabled_toolsets\` may also remove overlapping tools that were explicitly enabled, leaving the model with few or no tools. They include bug reports and several proposed changes that preserve enabled tools, resolve bundle names, or reject platform-bundle names.

## TL;DR

- 14 member(s): 0 open issue(s), 2 closed issue(s), 1 open PR(s), 2 merged PR(s), 9 PR(s) closed without merging.
- A fix was merged to main in PR #59150. Verify it addresses your case.
- **Recommended:** read PR #59150 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `resolved` · deciding PR(s): `pr-50109,pr-59150`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_PARTIAL`): some members are connected by structural evidence (shared changed files, or a closing/superseded reference); the rest are joined by an automated relevance judgement over their text.
- Members outside that structural core may belong to a different underlying problem.
- Pairwise link density `0.14`: the share of member pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. `1.00` means every pair is linked.

## Closed / duplicate issues

- #33924: Bug: Plattform-Bundle name \`hermes-yuanbao\` in \`agent.disabled_toolsets\` silently kills ALL tools in gateway path (Telegram + cron), CLI unaffected
- #58281: \[Bug\]: \[Bug\]: Disabling the \`coding\` toolset silently strips explicitly-enabled \`terminal\`/\`file\` tools — model receives Tools: 0

## Prior & competing PRs

- **#50109: fix(tools): preserve core tools when a platform bundle is disabled**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`
  - files: `model_tools.py`, `tests/test_model_tools.py`, `toolsets.py`
  - fix hunks:
  - `model_tools.py`

    ```diff
    @@ -34,6 +34,10 @@
     
     logger = logging.getLogger(__name__)
     
    +# Tracks platform-bundle names already flagged in disabled_toolsets so the
    +# advisory (#33924) is logged once per name, not on every tool recompute.
    +_WARNED_DISABLED_BUNDLES: set = set()
    +
     
     # =============================================================================
     # Async Bridging  (single source of truth -- used by registry.dispatch too)
    ```

    ```diff
    @@ -392,8 +396,29 @@ def _compute_tool_definitions(
         if disabled_toolsets:
             for toolset_name in disabled_toolsets:
                 if validate_toolset(toolset_name):
    -                resolved = resolve_toolset(toolset_name)
    -                tools_to_include.difference_update(resolved)
    +                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
    +                    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:
    +                        _WARNED_DISABLED_BUNDLES.add(toolset_name)
    +                        logger.info(
    +                            "agent.disabled_toolsets contains platform-bundle "
    +                            "name '%s'; core tools are preserved and only its "
    +                            "platform-specific tools (%s) are removed. Bundle "
    +                            "names usually belong in `toolsets:`, not "
    +                            "`disabled_toolsets` (#33924).",
    +                            toolset_name,
    +                            ", ".join(resolved) if resolved else "none",
    +                        )
    +                else:
    +                    resolved = resolve_toolset(toolset_name)
    +                    tools_to_include.difference_update(resolved)
                     if not quiet_mode:
                         print(f"🚫 Disabled toolset '{toolset_name}': {', '.join(resolved) if resolved else 'no tools'}")
                 elif toolset_name in _LEGACY_TOOLSET_MAP:
    ```
  - `tests/test_model_tools.py`

    ```diff
    @@ -457,3 +457,82 @@ def test_normal_numbers_still_coerce(self):
             assert _coerce_number("42") == 42
             assert _coerce_number("3.14") == 3.14
             assert _coerce_number("1e3") == 1000
    +
    +class TestDisabledToolsetsPlatformBundle:
    +    """Regression test for #33924: disabling a platform bundle (hermes-*)
    +    must not remove core tools from other enabled toolsets."""
    +
    +    def test_disabling_platform_bundle_preserves_core_tools(self):
    +        """Disabling hermes-yuanbao should not strip core tools from hermes-telegram."""
    +        from model_tools import get_tool_definitions
    +
    +        tools_telegram = get_tool_definitions(
    +            enabled_toolsets=["hermes-telegram"],
    +            quiet_mode=True,
    +        )
    +        tools_telegram_no_yuanbao = get_tool_definitions(
    +            enabled_toolsets=["hermes-telegram"],
    +            disabled_toolsets=["hermes-yuanbao"],
    +            quiet_mode=True,
    +        )
    +        names_telegram = {t["function"]["name"] for t in tools_telegram}
    +        names_no_yuanbao = {t["function"]["name"] for t in tools_telegram_no_yuanbao}
    +
    +        # Disabling a *different* platform bundle must not remove any tools
    +        assert names_telegram == names_no_yuanbao, (
    +            f"Tools lost after disabling hermes-yuanbao: "
    +            f"{names_telegram - names_no_yuanbao}"
    +        )
    +
    +    def test_disabling_platform_bundle_removes_own_tools(self):
    +        """Disabling hermes-discord should remove discord-specific tools."""
    +        from model_tools import get_tool_definitions
    +
    +        tools = get_tool_definitions(
    +            enabled_toolsets=["hermes-discord"],
    +            disabled_toolsets=["hermes-discord"],
    +            quiet_mode=True,
    +        )
    +        names = {t["function"]["name"] for t in tools}
    +        assert "discord" not in names
    +
    +    def test_disabling_non_platform_toolset_still_works(self):
    +        """Disabling a regular (non-hermes-) toolset still subtracts all tools."""
    +        from model_tools import get_tool_definitions
    +
    +        tools_normal = get_tool_definitions(
    +            enabled_toolsets=["hermes-telegram"],
    +            quiet_mode=True,
    +        )
    +        tools_no_web = get_tool_definitions(
    +            enabled_toolsets=["hermes-telegram"],
    +            disabled_toolsets=["web"],
    +            quiet_mode=True,
    +        )
    +        names_normal = {t["function"]["name"] for t in tools_normal}
    +        names_no_web = {t["function"]["name"] for t in tools_no_web}
    +
    +        web_tools = {"web_search", "web_extract"}
    +        removed = names_normal - names_no_web
    +        # web tools should be removed (if they were present)
    +        present_web = web_tools & names_normal
    +        assert present_web <= removed, (
    +            f"Web tools not removed: {present_web - removed}"
    +        )
    +
    +
    +    def test_disabling_bundle_removes_platform_tools_but_keeps_core(self):
    +        """Disabling hermes-discord (when enabled) removes discord/discord_admin
    +        from the resolved delta but keeps core tools — via bundle_non_core_tools."""
    +        from toolsets import bundle_non_core_tools, _HERMES_CORE_TOOLS
    +
    +        delta = bundle_non_core_tools("hermes-yuanbao")
    +        # The delta is the bundle's platform-specific tools, NOT core.
    +        assert "yb_send_dm" in delta
    +        assert not (delta & set(_HERMES_CORE_TOOLS)), "core tools must not be in the removal delta"
    +
    +    def test_bundle_non_core_tools_unknown_falls_back(self):
    +        """An unknown/garbage bundle name falls back to full resolution (best effort)."""
    +        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()
    ```
  - `toolsets.py`

    ```diff
    @@ -627,6 +627,34 @@ def get_toolset(name: str) -> Optional[Dict[str, Any]]:
         }
     
     
    +def bundle_non_core_tools(toolset_name: str) -> Set[str]:
    +    """Return a ``hermes-*`` bundle's platform-specific tools, excluding core.
    +
    +    Platform bundles are defined as ``_HERMES_CORE_TOOLS + [platform extras]``.
    +    When a bundle name appears in ``disabled_toolsets``, subtracting the whole
    +    bundle would strip core tools (terminal, read_file, …) shared by every
    +    other enabled toolset, emptying the model's tool list (#33924). This
    +    returns only the bundle's non-core delta (its own extras plus those of any
    +    one-level ``includes``), so disabling a bundle removes its platform tools
    +    while leaving core intact.
    +
    +    Bundle nesting is one level deep in practice (only ``hermes-gateway``
    +    includes other bundles, and those leaves don't nest further), so a single
    +    ``includes`` pass is sufficient. Unknown/garbage names fall back to the
    +    full resolution minus core — never re-introducing the core wipe.
    +    """
    +    core = set(_HERMES_CORE_TOOLS)
    +    ts_def = get_toolset(toolset_name)
    +    if not (ts_def and "tools" in ts_def):
    +        return set(resolve_toolset(toolset_name)) - core
    +    to_remove = set(ts_def["tools"]) - core
    +    for inc in ts_def.get("includes", []):
    +        inc_def = get_toolset(inc)
    +        if inc_def and "tools" in inc_def:
    +            to_remove.update(set(inc_def["tools"]) - core)
    +    return to_remove
    +
    +
     def resolve_toolset(name: str, visited: Set[str] = None) -> List[str]:
         """
         Recursively resolve a toolset to get all tool names.
    ```
- **#59150: fix(setup+toolsets): blank slate no longer strips its own core tools via the coding posture bundle**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`
  - files: `hermes_cli/setup.py`, `model_tools.py`, `tests/hermes_cli/test_setup_blank_slate.py`, `tests/test_model_tools.py`
- **#60273: fix(tools): prevent composite disabled_toolsets from stripping explicitly enabled tools**. closed without merge · fix tier `suggested`
  - maintainer direction: category `uncategorized`; implication `needs_llm`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#33941: fix(tools): preserve core tools when disabling platform bundles in disabled_toolsets**. closed without merge
  - maintainer direction: category `uncategorized`; implication `needs_llm`; refs `#50109`
- **#34033: fix(tools): resolve bundle names in disabled_toolsets before subtraction**. closed without merge
  - maintainer direction: category `superseded_by_pr`; implication `superseded`; refs `#50109`
- **#34073: fix(tools_config): reject platform-bundle names in disabled_toolsets (#33924)**. closed without merge
  - maintainer direction: category `superseded_by_pr`; implication `superseded`; refs `#50109`
- **#58310: fix: preserve explicit leaf tools when disabling coding**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `e5636da5d868c088cb2c8dadd2218665cd624671`, `#59150`
- **#58496: fix(model_tools): preserve enabled toolsets when disabling composite toolsets**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `#58281`, `#59150`, `#17309`, `#33924`
- **#58654: fix: preserve explicitly-enabled tools when disabling overlapping composite toolset**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `#58281`, `#59150`, `#17309`, `#33924`
- **#58800: fix(model_tools): preserve explicitly-enabled tools when disabling a composite toolset**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `#58281`, `#59150`, `#17309`, `#33924`
- **#59299: fix(tools): preserve explicitly-enabled tools when disabling composite toolsets**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `e5636da5d`, `#58281`, `#57315`
- **#60399: fix(tools): prevent composite disabled_toolsets from stripping explicitly enabled tools**. open
  - maintainer direction: category `uncategorized`; implication `needs_llm`; refs `#59299`
  - files: `model_tools.py`, `tests/test_disabled_toolset_enabled_preservation.py`

## Fix status

- Merged to main in PR #59150 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-07-05`.
- Subsystem: `agent`
