{"repo":"NousResearch/hermes-agent","public_id":"33924","display_label":"disabled_toolsets composite bundles stripping explicitly enabled tools","summary":"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.","subsystem":"agent","counts":{"members":14,"open_issues":0,"open_issues_distinct":0,"open_prs":1,"closed_issues":2,"merged_prs":2,"attempt_prs":10},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":59150,"merged_at":"2026-07-05T21:51:49+00:00"},"resolution":{"state":"resolved","by":"pr-50109,pr-59150"},"freshness":{"state":"verified","last_verified_at":"2026-07-05T21:51:49+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_PARTIAL","link_density":0.13582417582417583},"fix_edges":[{"issue_number":33924,"pr_number":50109,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true,"pr_merged_at":"2026-06-22T10:21:50+00:00"},{"issue_number":57315,"pr_number":59150,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true,"pr_merged_at":"2026-07-05T21:51:49+00:00"},{"issue_number":58281,"pr_number":59150,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true,"pr_merged_at":"2026-07-05T21:51:49+00:00"}],"members":[{"kind":"issue","number":33924,"title":"Bug: Plattform-Bundle name `hermes-yuanbao` in `agent.disabled_toolsets` silently kills ALL tools in gateway path (Telegram + cron), CLI unaffected","url":null,"state":"closed"},{"kind":"issue","number":58281,"title":"[Bug]: [Bug]: Disabling the `coding` toolset silently strips explicitly-enabled `terminal`/`file` tools — model receives Tools: 0","url":null,"state":"closed"},{"kind":"pr","number":33941,"title":"fix(tools): preserve core tools when disabling platform bundles in disabled_toolsets","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#50109","number":50109}],"category":"uncategorized","implication":"needs_llm"},"merged":false},{"kind":"pr","number":34033,"title":"fix(tools): resolve bundle names in disabled_toolsets before subtraction","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#50109","number":50109}],"category":"superseded_by_pr","implication":"superseded"},"merged":false},{"kind":"pr","number":34073,"title":"fix(tools_config): reject platform-bundle names in disabled_toolsets (#33924)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#50109","number":50109}],"category":"superseded_by_pr","implication":"superseded"},"merged":false},{"kind":"pr","number":50109,"title":"fix(tools): preserve core tools when a platform bundle is disabled","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["model_tools.py","tests/test_model_tools.py","toolsets.py"],"fix_hunks":[{"file":"model_tools.py","hunks":["@@ -34,6 +34,10 @@\n \n logger = logging.getLogger(__name__)\n \n+# Tracks platform-bundle names already flagged in disabled_toolsets so the\n+# advisory (#33924) is logged once per name, not on every tool recompute.\n+_WARNED_DISABLED_BUNDLES: set = set()\n+\n \n # =============================================================================\n # Async Bridging  (single source of truth -- used by registry.dispatch too)","@@ -392,8 +396,29 @@ def _compute_tool_definitions(\n     if disabled_toolsets:\n         for toolset_name in disabled_toolsets:\n             if validate_toolset(toolset_name):\n-                resolved = resolve_toolset(toolset_name)\n-                tools_to_include.difference_update(resolved)\n+                if toolset_name.startswith(\"hermes-\"):\n+                    # Platform bundles (hermes-*) include _HERMES_CORE_TOOLS, so\n+                    # subtracting the whole bundle would strip core tools shared\n+                    # by other enabled toolsets and empty the tool list (#33924).\n+                    # Subtract only the bundle's non-core delta; keep core.\n+                    from toolsets import bundle_non_core_tools\n+                    to_remove = bundle_non_core_tools(toolset_name)\n+                    tools_to_include.difference_update(to_remove)\n+                    resolved = sorted(to_remove)\n+                    if not quiet_mode and toolset_name not in _WARNED_DISABLED_BUNDLES:\n+                        _WARNED_DISABLED_BUNDLES.add(toolset_name)\n+                        logger.info(\n+                            \"agent.disabled_toolsets contains platform-bundle \"\n+                            \"name '%s'; core tools are preserved and only its \"\n+                            \"platform-specific tools (%s) are removed. Bundle \"\n+                            \"names usually belong in `toolsets:`, not \"\n+                            \"`disabled_toolsets` (#33924).\",\n+                            toolset_name,\n+                            \", \".join(resolved) if resolved else \"none\",\n+                        )\n+                else:\n+                    resolved = resolve_toolset(toolset_name)\n+                    tools_to_include.difference_update(resolved)\n                 if not quiet_mode:\n                     print(f\"🚫 Disabled toolset '{toolset_name}': {', '.join(resolved) if resolved else 'no tools'}\")\n             elif toolset_name in _LEGACY_TOOLSET_MAP:"]},{"file":"tests/test_model_tools.py","hunks":["@@ -457,3 +457,82 @@ def test_normal_numbers_still_coerce(self):\n         assert _coerce_number(\"42\") == 42\n         assert _coerce_number(\"3.14\") == 3.14\n         assert _coerce_number(\"1e3\") == 1000\n+\n+class TestDisabledToolsetsPlatformBundle:\n+    \"\"\"Regression test for #33924: disabling a platform bundle (hermes-*)\n+    must not remove core tools from other enabled toolsets.\"\"\"\n+\n+    def test_disabling_platform_bundle_preserves_core_tools(self):\n+        \"\"\"Disabling hermes-yuanbao should not strip core tools from hermes-telegram.\"\"\"\n+        from model_tools import get_tool_definitions\n+\n+        tools_telegram = get_tool_definitions(\n+            enabled_toolsets=[\"hermes-telegram\"],\n+            quiet_mode=True,\n+        )\n+        tools_telegram_no_yuanbao = get_tool_definitions(\n+            enabled_toolsets=[\"hermes-telegram\"],\n+            disabled_toolsets=[\"hermes-yuanbao\"],\n+            quiet_mode=True,\n+        )\n+        names_telegram = {t[\"function\"][\"name\"] for t in tools_telegram}\n+        names_no_yuanbao = {t[\"function\"][\"name\"] for t in tools_telegram_no_yuanbao}\n+\n+        # Disabling a *different* platform bundle must not remove any tools\n+        assert names_telegram == names_no_yuanbao, (\n+            f\"Tools lost after disabling hermes-yuanbao: \"\n+            f\"{names_telegram - names_no_yuanbao}\"\n+        )\n+\n+    def test_disabling_platform_bundle_removes_own_tools(self):\n+        \"\"\"Disabling hermes-discord should remove discord-specific tools.\"\"\"\n+        from model_tools import get_tool_definitions\n+\n+        tools = get_tool_definitions(\n+            enabled_toolsets=[\"hermes-discord\"],\n+            disabled_toolsets=[\"hermes-discord\"],\n+            quiet_mode=True,\n+        )\n+        names = {t[\"function\"][\"name\"] for t in tools}\n+        assert \"discord\" not in names\n+\n+    def test_disabling_non_platform_toolset_still_works(self):\n+        \"\"\"Disabling a regular (non-hermes-) toolset still subtracts all tools.\"\"\"\n+        from model_tools import get_tool_definitions\n+\n+        tools_normal = get_tool_definitions(\n+            enabled_toolsets=[\"hermes-telegram\"],\n+            quiet_mode=True,\n+        )\n+        tools_no_web = get_tool_definitions(\n+            enabled_toolsets=[\"hermes-telegram\"],\n+            disabled_toolsets=[\"web\"],\n+            quiet_mode=True,\n+        )\n+        names_normal = {t[\"function\"][\"name\"] for t in tools_normal}\n+        names_no_web = {t[\"function\"][\"name\"] for t in tools_no_web}\n+\n+        web_tools = {\"web_search\", \"web_extract\"}\n+        removed = names_normal - names_no_web\n+        # web tools should be removed (if they were present)\n+        present_web = web_tools & names_normal\n+        assert present_web <= removed, (\n+            f\"Web tools not removed: {present_web - removed}\"\n+        )\n+\n+\n+    def test_disabling_bundle_removes_platform_tools_but_keeps_core(self):\n+        \"\"\"Disabling hermes-discord (when enabled) removes discord/discord_admin\n+        from the resolved delta but keeps core tools — via bundle_non_core_tools.\"\"\"\n+        from toolsets import bundle_non_core_tools, _HERMES_CORE_TOOLS\n+\n+        delta = bundle_non_core_tools(\"hermes-yuanbao\")\n+        # The delta is the bundle's platform-specific tools, NOT core.\n+        assert \"yb_send_dm\" in delta\n+        assert not (delta & set(_HERMES_CORE_TOOLS)), \"core tools must not be in the removal delta\"\n+\n+    def test_bundle_non_core_tools_unknown_falls_back(self):\n+        \"\"\"An unknown/garbage bundle name falls back to full resolution (best effort).\"\"\"\n+        from toolsets import bundle_non_core_tools\n+        # A non-existent bundle resolves to an empty set (no tools), not a crash.\n+        assert bundle_non_core_tools(\"hermes-does-not-exist\") == set()"]},{"file":"toolsets.py","hunks":["@@ -627,6 +627,34 @@ def get_toolset(name: str) -> Optional[Dict[str, Any]]:\n     }\n \n \n+def bundle_non_core_tools(toolset_name: str) -> Set[str]:\n+    \"\"\"Return a ``hermes-*`` bundle's platform-specific tools, excluding core.\n+\n+    Platform bundles are defined as ``_HERMES_CORE_TOOLS + [platform extras]``.\n+    When a bundle name appears in ``disabled_toolsets``, subtracting the whole\n+    bundle would strip core tools (terminal, read_file, …) shared by every\n+    other enabled toolset, emptying the model's tool list (#33924). This\n+    returns only the bundle's non-core delta (its own extras plus those of any\n+    one-level ``includes``), so disabling a bundle removes its platform tools\n+    while leaving core intact.\n+\n+    Bundle nesting is one level deep in practice (only ``hermes-gateway``\n+    includes other bundles, and those leaves don't nest further), so a single\n+    ``includes`` pass is sufficient. Unknown/garbage names fall back to the\n+    full resolution minus core — never re-introducing the core wipe.\n+    \"\"\"\n+    core = set(_HERMES_CORE_TOOLS)\n+    ts_def = get_toolset(toolset_name)\n+    if not (ts_def and \"tools\" in ts_def):\n+        return set(resolve_toolset(toolset_name)) - core\n+    to_remove = set(ts_def[\"tools\"]) - core\n+    for inc in ts_def.get(\"includes\", []):\n+        inc_def = get_toolset(inc)\n+        if inc_def and \"tools\" in inc_def:\n+            to_remove.update(set(inc_def[\"tools\"]) - core)\n+    return to_remove\n+\n+\n def resolve_toolset(name: str, visited: Set[str] = None) -> List[str]:\n     \"\"\"\n     Recursively resolve a toolset to get all tool names.\n"]}]},{"kind":"pr","number":58310,"title":"fix: preserve explicit leaf tools when disabling coding","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"e5636da5d868c088cb2c8dadd2218665cd624671","number":0},{"kind":"pr","value":"#59150","number":59150}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":58496,"title":"fix(model_tools): preserve enabled toolsets when disabling composite toolsets","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#58281","number":58281},{"kind":"pr","value":"#59150","number":59150},{"kind":"pr","value":"#17309","number":17309},{"kind":"pr","value":"#33924","number":33924}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":58654,"title":"fix: preserve explicitly-enabled tools when disabling overlapping composite toolset","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#58281","number":58281},{"kind":"pr","value":"#59150","number":59150},{"kind":"pr","value":"#17309","number":17309},{"kind":"pr","value":"#33924","number":33924}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":58800,"title":"fix(model_tools): preserve explicitly-enabled tools when disabling a composite toolset","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#58281","number":58281},{"kind":"pr","value":"#59150","number":59150},{"kind":"pr","value":"#17309","number":17309},{"kind":"pr","value":"#33924","number":33924}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":59150,"title":"fix(setup+toolsets): blank slate no longer strips its own core tools via the coding posture bundle","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["hermes_cli/setup.py","model_tools.py","tests/hermes_cli/test_setup_blank_slate.py","tests/test_model_tools.py"]},{"kind":"pr","number":59299,"title":"fix(tools): preserve explicitly-enabled tools when disabling composite toolsets","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"e5636da5d","number":0},{"kind":"pr","value":"#58281","number":58281},{"kind":"pr","value":"#57315","number":57315}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":60273,"title":"fix(tools): prevent composite disabled_toolsets from stripping explicitly enabled tools","url":null,"state":"closed","close_reason":{"category":"uncategorized","implication":"needs_llm"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":60399,"title":"fix(tools): prevent composite disabled_toolsets from stripping explicitly enabled tools","url":null,"state":"open","close_reason":{"refs":[{"kind":"pr","value":"#59299","number":59299}],"category":"uncategorized","implication":"needs_llm"},"merged":false}],"trust":{"default_tier":"verified","includes_suggested":false}}