diff --git a/gateway/run.py b/gateway/run.py index c7ed455001783..84ab9bcc9e850 100644 --- a/gateway/run.py +++ b/gateway/run.py @@ -1742,7 +1742,10 @@ def _load_show_reasoning() -> bool: if cfg_path.exists(): with open(cfg_path, encoding="utf-8") as _f: cfg = _y.safe_load(_f) or {} - return bool(cfg_get(cfg, "display", "show_reasoning", default=False)) + return is_truthy_value( + cfg_get(cfg, "display", "show_reasoning"), + default=False, + ) except Exception: pass return False @@ -8351,7 +8354,10 @@ async def _handle_verbose_command(self, event: MessageEvent) -> str: # --- check config gate ------------------------------------------------ try: user_config = _load_gateway_config() - gate_enabled = cfg_get(user_config, "display", "tool_progress_command", default=False) + gate_enabled = is_truthy_value( + cfg_get(user_config, "display", "tool_progress_command"), + default=False, + ) except Exception: gate_enabled = False @@ -11298,7 +11304,10 @@ def progress_callback(event_type: str, tool_name: str = None, preview: str = Non tool_progress_hint_gateway, ) _cfg = _load_gateway_config() - gate_on = bool(cfg_get(_cfg, "display", "tool_progress_command", default=False)) + gate_on = is_truthy_value( + cfg_get(_cfg, "display", "tool_progress_command"), + default=False, + ) if gate_on and not is_seen(_cfg, TOOL_PROGRESS_FLAG): long_tool_hint_fired[0] = True progress_queue.put(tool_progress_hint_gateway()) diff --git a/hermes_cli/commands.py b/hermes_cli/commands.py index 15e211b997fa6..2acffe331a412 100644 --- a/hermes_cli/commands.py +++ b/hermes_cli/commands.py @@ -19,6 +19,8 @@ from dataclasses import dataclass from typing import Any +from utils import is_truthy_value + # prompt_toolkit is an optional CLI dependency — only needed for # SlashCommandCompleter and SlashCommandAutoSuggest. Gateway and test # environments that lack it must still be able to import this module @@ -371,7 +373,7 @@ def _resolve_config_gates() -> set[str]: else: val = None break - if val: + if is_truthy_value(val, default=False): result.add(cmd.name) return result diff --git a/tests/gateway/test_reasoning_command.py b/tests/gateway/test_reasoning_command.py index 5020df30a749f..f22704dedf672 100644 --- a/tests/gateway/test_reasoning_command.py +++ b/tests/gateway/test_reasoning_command.py @@ -407,3 +407,44 @@ def test_run_agent_homeassistant_uses_default_platform_toolset(self, tmp_path, m assert result["final_response"] == "ok" assert _CapturingAgent.last_init is not None assert "homeassistant" in set(_CapturingAgent.last_init["enabled_toolsets"]) + + +class TestLoadShowReasoningCoercion: + """Regression: display.show_reasoning must be coerced, not bool()'d.""" + + def _load_with_config(self, tmp_path, monkeypatch, yaml_body: str) -> bool: + hermes_home = tmp_path / "hermes" + hermes_home.mkdir() + (hermes_home / "config.yaml").write_text(yaml_body, encoding="utf-8") + monkeypatch.setattr(gateway_run, "_hermes_home", hermes_home) + return gateway_run.GatewayRunner._load_show_reasoning() + + def test_quoted_false_is_false(self, tmp_path, monkeypatch): + assert self._load_with_config( + tmp_path, monkeypatch, + 'display:\n show_reasoning: "false"\n', + ) is False + + def test_quoted_off_is_false(self, tmp_path, monkeypatch): + assert self._load_with_config( + tmp_path, monkeypatch, + 'display:\n show_reasoning: "off"\n', + ) is False + + def test_quoted_true_is_true(self, tmp_path, monkeypatch): + assert self._load_with_config( + tmp_path, monkeypatch, + 'display:\n show_reasoning: "true"\n', + ) is True + + def test_bare_true_is_true(self, tmp_path, monkeypatch): + assert self._load_with_config( + tmp_path, monkeypatch, + 'display:\n show_reasoning: true\n', + ) is True + + def test_missing_is_false(self, tmp_path, monkeypatch): + assert self._load_with_config( + tmp_path, monkeypatch, + 'display: {}\n', + ) is False diff --git a/tests/gateway/test_verbose_command.py b/tests/gateway/test_verbose_command.py index c3743e591541e..d6debebae5997 100644 --- a/tests/gateway/test_verbose_command.py +++ b/tests/gateway/test_verbose_command.py @@ -85,6 +85,25 @@ async def test_enabled_cycles_mode(self, tmp_path, monkeypatch): saved = yaml.safe_load(config_path.read_text(encoding="utf-8")) assert saved["display"]["platforms"]["telegram"]["tool_progress"] == "verbose" + @pytest.mark.asyncio + async def test_quoted_false_keeps_command_disabled(self, tmp_path, monkeypatch): + """Quoted false must not enable the /verbose gateway command.""" + hermes_home = tmp_path / "hermes" + hermes_home.mkdir() + config_path = hermes_home / "config.yaml" + config_path.write_text( + 'display:\n tool_progress_command: "false"\n tool_progress: all\n', + encoding="utf-8", + ) + + monkeypatch.setattr(gateway_run, "_hermes_home", hermes_home) + + runner = _make_runner() + result = await runner._handle_verbose_command(_make_event()) + + assert "not enabled" in result.lower() + assert "tool_progress_command" in result + @pytest.mark.asyncio async def test_cycles_through_all_modes(self, tmp_path, monkeypatch): """Calling /verbose repeatedly cycles through all four modes.""" diff --git a/tests/hermes_cli/test_commands.py b/tests/hermes_cli/test_commands.py index 26bba9d58f1ea..adafe58c64730 100644 --- a/tests/hermes_cli/test_commands.py +++ b/tests/hermes_cli/test_commands.py @@ -405,6 +405,21 @@ def test_config_gate_included_in_help_when_on(self, tmp_path, monkeypatch): joined = "\n".join(lines) assert "`/verbose" in joined + def test_config_gate_quoted_false_stays_disabled_everywhere(self, tmp_path, monkeypatch): + """Quoted false must not enable config-gated gateway commands.""" + config_file = tmp_path / "config.yaml" + config_file.write_text('display:\n tool_progress_command: "false"\n') + monkeypatch.setenv("HERMES_HOME", str(tmp_path)) + + lines = gateway_help_lines() + joined = "\n".join(lines) + names = {name for name, _ in telegram_bot_commands()} + mapping = slack_subcommand_map() + + assert "`/verbose" not in joined + assert "verbose" not in names + assert "verbose" not in mapping + def test_config_gate_excluded_from_telegram_when_off(self, tmp_path, monkeypatch): config_file = tmp_path / "config.yaml" config_file.write_text("display:\n tool_progress_command: false\n") diff --git a/tests/tools/test_skill_manager_tool.py b/tests/tools/test_skill_manager_tool.py index 9fc8957f1e02d..00eaf51ea0492 100644 --- a/tests/tools/test_skill_manager_tool.py +++ b/tests/tools/test_skill_manager_tool.py @@ -567,6 +567,26 @@ def test_guard_flag_handles_config_error(self): with patch("hermes_cli.config.load_config", side_effect=RuntimeError("boom")): assert _guard_agent_created_enabled() is False + def test_guard_flag_quoted_false_stays_disabled(self): + """Quoted 'false' from YAML edits must not enable the guard.""" + from tools.skill_manager_tool import _guard_agent_created_enabled + + for quoted in ("false", "False", "0", "no", "off"): + with patch("hermes_cli.config.load_config", + return_value={"skills": {"guard_agent_created": quoted}}): + assert _guard_agent_created_enabled() is False, \ + f"guard_agent_created={quoted!r} must coerce to False" + + def test_guard_flag_quoted_true_enables(self): + """Quoted truthy strings must enable the guard.""" + from tools.skill_manager_tool import _guard_agent_created_enabled + + for quoted in ("true", "True", "1", "yes", "on"): + with patch("hermes_cli.config.load_config", + return_value={"skills": {"guard_agent_created": quoted}}): + assert _guard_agent_created_enabled() is True, \ + f"guard_agent_created={quoted!r} must coerce to True" + # --------------------------------------------------------------------------- # External skills directories (skills.external_dirs) — mutations in place diff --git a/tools/skill_manager_tool.py b/tools/skill_manager_tool.py index cc8b0fed28f99..e1b9a5f055574 100644 --- a/tools/skill_manager_tool.py +++ b/tools/skill_manager_tool.py @@ -42,7 +42,7 @@ from hermes_constants import get_hermes_home, display_hermes_home from typing import Dict, Any, Optional, Tuple -from utils import atomic_replace +from utils import atomic_replace, is_truthy_value from hermes_cli.config import cfg_get logger = logging.getLogger(__name__) @@ -67,7 +67,10 @@ def _guard_agent_created_enabled() -> bool: try: from hermes_cli.config import load_config cfg = load_config() - return bool(cfg_get(cfg, "skills", "guard_agent_created", default=False)) + return is_truthy_value( + cfg_get(cfg, "skills", "guard_agent_created"), + default=False, + ) except Exception: return False