From 24130b7e53abcd434c7d0ce06de93b27b57047f8 Mon Sep 17 00:00:00 2001 From: hharry11 Date: Mon, 27 Apr 2026 06:42:32 +0300 Subject: [PATCH] fix(approval): harden YOLO mode env parsing against quoted-bool strings --- cli.py | 4 ++-- tests/test_tui_gateway_server.py | 15 +++++++++++++++ tests/tools/test_yolo_mode.py | 27 +++++++++++++++++++++++++++ tools/approval.py | 6 ++++-- tui_gateway/server.py | 3 ++- 5 files changed, 50 insertions(+), 5 deletions(-) diff --git a/cli.py b/cli.py index bfe0dcba..bef1d87b 100644 --- a/cli.py +++ b/cli.py @@ -85,7 +85,7 @@ from hermes_cli.browser_connect import ( try_launch_chrome_debug, ) from hermes_cli.env_loader import load_hermes_dotenv -from utils import base_url_host_matches +from utils import base_url_host_matches, is_truthy_value _hermes_home = get_hermes_home() _project_env = Path(__file__).parent / '.env' @@ -7146,7 +7146,7 @@ class HermesCLI: import os from hermes_cli.colors import Colors as _Colors - current = bool(os.environ.get("HERMES_YOLO_MODE")) + current = is_truthy_value(os.environ.get("HERMES_YOLO_MODE")) if current: os.environ.pop("HERMES_YOLO_MODE", None) _cprint( diff --git a/tests/test_tui_gateway_server.py b/tests/test_tui_gateway_server.py index a18a1b39..41b5194d 100644 --- a/tests/test_tui_gateway_server.py +++ b/tests/test_tui_gateway_server.py @@ -1005,6 +1005,21 @@ def test_config_busy_get_and_set(monkeypatch): assert ("display.busy_input_mode", "interrupt") in writes +def test_config_set_yolo_process_scope_treats_false_like_env_as_disabled(monkeypatch): + monkeypatch.setenv("HERMES_YOLO_MODE", "false") + + resp = server.handle_request( + { + "id": "1", + "method": "config.set", + "params": {"key": "yolo"}, + } + ) + + assert resp["result"]["value"] == "1" + assert os.environ.get("HERMES_YOLO_MODE") == "1" + + def test_config_get_statusbar_survives_non_dict_display(monkeypatch): monkeypatch.setattr(server, "_load_cfg", lambda: {"display": "broken"}) diff --git a/tests/tools/test_yolo_mode.py b/tests/tools/test_yolo_mode.py index 866ce8e5..29a68f07 100644 --- a/tests/tools/test_yolo_mode.py +++ b/tests/tools/test_yolo_mode.py @@ -125,6 +125,33 @@ class TestYoloMode: approval_callback=lambda *a: "deny") assert not result["approved"] + @pytest.mark.parametrize("value", ["false", "False", "0", "off", "no"]) + def test_false_like_yolo_values_do_not_bypass_dangerous_command(self, monkeypatch, value): + """False-like env strings must not silently enable YOLO bypass.""" + monkeypatch.setenv("HERMES_YOLO_MODE", value) + monkeypatch.setenv("HERMES_INTERACTIVE", "1") + monkeypatch.setenv("HERMES_SESSION_KEY", "test-session") + + result = check_dangerous_command( + "rm -rf /tmp/stuff", + "local", + approval_callback=lambda *a: "deny", + ) + assert not result["approved"] + + @pytest.mark.parametrize("value", ["false", "False", "0", "off", "no"]) + def test_false_like_yolo_values_do_not_bypass_combined_guard(self, monkeypatch, value): + """Combined guard must treat false-like YOLO env strings as disabled.""" + monkeypatch.setenv("HERMES_YOLO_MODE", value) + monkeypatch.setenv("HERMES_INTERACTIVE", "1") + + result = check_all_command_guards( + "rm -rf /tmp/stuff", + "local", + approval_callback=lambda *a: "deny", + ) + assert not result["approved"] + def test_session_scoped_yolo_only_bypasses_current_session(self, monkeypatch): """Gateway /yolo should only bypass approvals for the active session.""" monkeypatch.delenv("HERMES_YOLO_MODE", raising=False) diff --git a/tools/approval.py b/tools/approval.py index aa20a86a..e13c019c 100644 --- a/tools/approval.py +++ b/tools/approval.py @@ -19,6 +19,8 @@ import unicodedata from typing import Optional from hermes_cli.config import cfg_get +from utils import is_truthy_value + logger = logging.getLogger(__name__) # Per-thread/per-task gateway session identity. @@ -802,7 +804,7 @@ def check_dangerous_command(command: str, env_type: str, # --yolo: bypass all approval prompts. Gateway /yolo is session-scoped; # CLI --yolo remains process-scoped via the env var for local use. - if os.getenv("HERMES_YOLO_MODE") or is_current_session_yolo_enabled(): + if is_truthy_value(os.getenv("HERMES_YOLO_MODE")) or is_current_session_yolo_enabled(): return {"approved": True, "message": None} is_dangerous, pattern_key, description = detect_dangerous_command(command) @@ -927,7 +929,7 @@ def check_all_command_guards(command: str, env_type: str, # --yolo or approvals.mode=off: bypass all approval prompts. # Gateway /yolo is session-scoped; CLI --yolo remains process-scoped. approval_mode = _get_approval_mode() - if os.getenv("HERMES_YOLO_MODE") or is_current_session_yolo_enabled() or approval_mode == "off": + if is_truthy_value(os.getenv("HERMES_YOLO_MODE")) or is_current_session_yolo_enabled() or approval_mode == "off": return {"approved": True, "message": None} is_cli = os.getenv("HERMES_INTERACTIVE") diff --git a/tui_gateway/server.py b/tui_gateway/server.py index 61aa683b..fb8aaa81 100644 --- a/tui_gateway/server.py +++ b/tui_gateway/server.py @@ -17,6 +17,7 @@ from typing import Any, Optional from hermes_constants import get_hermes_home from hermes_cli.env_loader import load_hermes_dotenv +from utils import is_truthy_value from tui_gateway.transport import ( StdioTransport, Transport, @@ -3421,7 +3422,7 @@ def _(rid, params: dict) -> dict: enable_session_yolo(session["session_key"]) nv = "1" else: - current = bool(os.environ.get("HERMES_YOLO_MODE")) + current = is_truthy_value(os.environ.get("HERMES_YOLO_MODE")) if current: os.environ.pop("HERMES_YOLO_MODE", None) nv = "0"