{"repo":"NousResearch/hermes-agent","public_id":"49995","display_label":"Reconciling agent.disabled_toolsets when enabling a toolset","summary":"Members appear to concern one theme: enabling a toolset while it is listed in agent.disabled_toolsets. Two share the same title for a tools fix, and one bug report describes the Desktop Toolsets UI not re-enabling such a toolset and showing it as OFF on reopen.","subsystem":"apps/desktop","counts":{"members":3,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":1,"merged_prs":1,"attempt_prs":1},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":54334,"merged_at":"2026-06-28T16:29:04+00:00"},"resolution":{"state":"resolved","by":"pr-54334"},"freshness":{"state":"verified","last_verified_at":"2026-06-28T16:29:04+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.6666666666666666},"fix_edges":[{"issue_number":49995,"pr_number":54334,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true,"pr_merged_at":"2026-06-28T16:29:04+00:00"}],"members":[{"kind":"issue","number":49995,"title":"[Bug]: Desktop Toolsets UI can't re-enable a toolset that's in agent.disabled_toolsets (Blank Slate); enabled toolsets revert to OFF on reopen","url":null,"state":"closed"},{"kind":"pr","number":50012,"title":"fix(tools): reconcile agent.disabled_toolsets when a toolset is enabled","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"3e16176ba","number":0},{"kind":"pr","value":"#54334","number":54334},{"kind":"pr","value":"#49995","number":49995}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":54334,"title":"fix(tools): reconcile agent.disabled_toolsets when a toolset is enabled","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["hermes_cli/tools_config.py","tests/hermes_cli/test_tools_config.py"],"fix_hunks":[{"file":"hermes_cli/tools_config.py","hunks":["@@ -1736,6 +1736,32 @@ def _save_platform_tools(config: dict, platform: str, enabled_toolset_keys: Set[\n         config.setdefault(\"known_plugin_toolsets\", {})\n         config[\"known_plugin_toolsets\"][platform] = sorted(plugin_keys)\n \n+    # Reconcile with agent.disabled_toolsets. _get_platform_tools() applies\n+    # that list as a final override AFTER reading platform_toolsets.<platform>,\n+    # so a toolset listed there stays permanently OFF no matter what this\n+    # function writes — the toggle \"saves\" but silently can't ever take\n+    # effect. Blank Slate installs pre-populate this list with ~27 toolsets,\n+    # making most of the desktop Toolsets UI unusable for re-enabling\n+    # anything (issue #49995).\n+    #\n+    # Only toolsets the user just explicitly enabled FOR THIS PLATFORM are\n+    # cleared from the global disabled list — toolsets the user did not\n+    # touch (still unchecked) or that remain disabled on other platforms\n+    # are left alone, so agent.disabled_toolsets keeps working as a\n+    # cross-platform suppression list for anything not actively re-enabled.\n+    agent_cfg = config.get(\"agent\")\n+    if isinstance(agent_cfg, dict):\n+        disabled_toolsets = agent_cfg.get(\"disabled_toolsets\")\n+        if isinstance(disabled_toolsets, list) and disabled_toolsets:\n+            newly_enabled = enabled_toolset_keys - preserved_entries\n+            if newly_enabled:\n+                remaining = [\n+                    ts for ts in disabled_toolsets\n+                    if str(ts) not in newly_enabled\n+                ]\n+                if remaining != disabled_toolsets:\n+                    agent_cfg[\"disabled_toolsets\"] = remaining\n+\n     save_config(config)\n \n "]},{"file":"tests/hermes_cli/test_tools_config.py","hunks":["@@ -1676,3 +1676,83 @@ def test_vision_picker_custom_endpoint(tmp_path, monkeypatch):\n     save_env.assert_called_once_with(\"OPENAI_API_KEY\", \"sk-secret\")\n \n \n+def test_save_platform_tools_clears_newly_enabled_from_disabled_toolsets():\n+    \"\"\"Enabling a toolset via the picker must remove it from\n+    agent.disabled_toolsets, or _get_platform_tools() permanently masks it\n+    back to OFF on the next read no matter what platform_toolsets says.\n+\n+    Blank Slate installs pre-populate disabled_toolsets with ~27 toolsets,\n+    making the desktop Toolsets UI's enable toggle effectively a no-op for\n+    any of them (issue #49995).\n+    \"\"\"\n+    config = {\n+        \"platform_toolsets\": {\"cli\": [\"file\", \"terminal\"]},\n+        \"agent\": {\"disabled_toolsets\": [\"todo\", \"memory\", \"browser\"]},\n+    }\n+\n+    with patch(\"hermes_cli.tools_config.save_config\"):\n+        _save_platform_tools(config, \"cli\", {\"file\", \"terminal\", \"todo\"})\n+\n+    # The toolset the user just enabled is cleared from the block-list...\n+    assert \"todo\" not in config[\"agent\"][\"disabled_toolsets\"]\n+    # ...but toolsets the user did NOT touch stay disabled (no over-reach).\n+    assert \"memory\" in config[\"agent\"][\"disabled_toolsets\"]\n+    assert \"browser\" in config[\"agent\"][\"disabled_toolsets\"]\n+    assert \"todo\" in config[\"platform_toolsets\"][\"cli\"]\n+\n+\n+def test_save_platform_tools_resolves_to_enabled_after_disabled_toolsets_reconcile():\n+    \"\"\"End-to-end: after _save_platform_tools() reconciles disabled_toolsets,\n+    _get_platform_tools() must actually resolve the toolset as enabled --\n+    this is the exact symptom from issue #49995 (toggle saves but the UI/agent\n+    still reads it as OFF after reopen).\n+    \"\"\"\n+    config = {\n+        \"platform_toolsets\": {\"cli\": [\"file\", \"terminal\"]},\n+        \"agent\": {\"disabled_toolsets\": [\"todo\", \"memory\"]},\n+    }\n+\n+    # Before: todo is masked off despite not being in platform_toolsets yet.\n+    assert \"todo\" not in _get_platform_tools(config, \"cli\")\n+\n+    with patch(\"hermes_cli.tools_config.save_config\"):\n+        _save_platform_tools(config, \"cli\", {\"file\", \"terminal\", \"todo\"})\n+\n+    # After: todo must resolve as enabled, and untouched 'memory' must\n+    # remain masked off.\n+    resolved = _get_platform_tools(config, \"cli\")\n+    assert \"todo\" in resolved\n+    assert \"memory\" not in resolved\n+\n+\n+def test_save_platform_tools_no_disabled_toolsets_is_noop():\n+    \"\"\"When agent.disabled_toolsets is absent or empty, the reconcile step\n+    must be a complete no-op (no KeyError, no spurious 'agent' key creation).\n+    \"\"\"\n+    config = {\"platform_toolsets\": {\"cli\": [\"file\", \"terminal\"]}}\n+\n+    with patch(\"hermes_cli.tools_config.save_config\"):\n+        _save_platform_tools(config, \"cli\", {\"file\", \"terminal\", \"todo\"})\n+\n+    assert \"todo\" in config[\"platform_toolsets\"][\"cli\"]\n+    # No 'agent' key should be fabricated when none existed.\n+    assert \"agent\" not in config\n+\n+\n+def test_save_platform_tools_disabling_a_toolset_does_not_touch_disabled_toolsets():\n+    \"\"\"Turning a toolset OFF (not present in enabled_toolset_keys) must not\n+    remove anything from agent.disabled_toolsets -- only toolsets the user\n+    just explicitly enabled are reconciled.\n+    \"\"\"\n+    config = {\n+        \"platform_toolsets\": {\"cli\": [\"file\", \"terminal\", \"todo\"]},\n+        \"agent\": {\"disabled_toolsets\": [\"memory\"]},\n+    }\n+\n+    with patch(\"hermes_cli.tools_config.save_config\"):\n+        # User unchecks 'todo' -- it's no longer in enabled_toolset_keys.\n+        _save_platform_tools(config, \"cli\", {\"file\", \"terminal\"})\n+\n+    assert \"todo\" not in config[\"platform_toolsets\"][\"cli\"]\n+    # disabled_toolsets is untouched by a disable action.\n+    assert config[\"agent\"][\"disabled_toolsets\"] == [\"memory\"]\n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}