Compare commits

...

1 Commits

Author SHA1 Message Date
Teknium
8393e7abc5 refactor(cli): simplify safe-mode startup wiring
Since safe mode already landed on main via #45488, reduce this branch to cleanup: centralize env setup, remove duplicated comments, and tighten tests.
2026-06-13 06:52:15 -07:00
4 changed files with 103 additions and 106 deletions

View File

@@ -2089,6 +2089,8 @@ def cmd_chat(args):
"""Run interactive chat CLI."""
use_tui = _resolve_use_tui(args)
_apply_safe_mode(args)
# Resolve --continue into --resume with the latest session or by name
continue_val = getattr(args, "continue_last", None)
if continue_val and not getattr(args, "resume", None):
@@ -2199,18 +2201,6 @@ def cmd_chat(args):
if getattr(args, "yolo", False):
os.environ["HERMES_YOLO_MODE"] = "1"
# --safe-mode: troubleshooting mode that disables ALL customizations.
# Inspired by Claude Code v2.1.169's --safe-mode (June 2026): run with a
# pristine environment to isolate whether a problem comes from the user's
# setup (config, rules files, plugins, MCP servers) or from Hermes itself.
# Implemented as a superset of --ignore-user-config + --ignore-rules plus
# plugin/MCP discovery suppression (HERMES_SAFE_MODE is checked by
# hermes_cli/plugins.py and tools/mcp_tool.py).
if getattr(args, "safe_mode", False):
os.environ["HERMES_SAFE_MODE"] = "1"
os.environ["HERMES_IGNORE_USER_CONFIG"] = "1"
os.environ["HERMES_IGNORE_RULES"] = "1"
# --ignore-user-config: make load_cli_config() / load_config() skip the
# user's ~/.hermes/config.yaml and return built-in defaults. Set BEFORE
# importing cli (which runs `CLI_CONFIG = load_cli_config()` at module
@@ -10821,6 +10811,8 @@ def _should_background_mcp_startup(args) -> bool:
def _prepare_agent_startup(args) -> None:
"""Discover plugins/MCP/hooks for commands that can run an agent turn."""
_apply_safe_mode(args)
_sub_attr, _sub_set = _AGENT_SUBCOMMANDS.get(args.command, (None, None))
if not (
args.command in _AGENT_COMMANDS
@@ -10886,6 +10878,14 @@ def _prepare_agent_startup(args) -> None:
)
def _apply_safe_mode(args) -> None:
if not getattr(args, "safe_mode", False):
return
os.environ["HERMES_SAFE_MODE"] = "1"
os.environ["HERMES_IGNORE_USER_CONFIG"] = "1"
os.environ["HERMES_IGNORE_RULES"] = "1"
def _set_chat_arg_defaults(args) -> None:
for attr, default in [
("query", None),

View File

@@ -1124,10 +1124,6 @@ class PluginManager:
"""
if self._discovered and not force:
return
# Safe mode (--safe-mode / HERMES_SAFE_MODE=1): troubleshooting run
# with all customizations disabled. Skip plugin discovery entirely so
# no third-party code (hooks, tools, platforms) loads. Mark as
# discovered so callers see a clean empty registry, not a retry loop.
if env_var_enabled("HERMES_SAFE_MODE"):
logger.info("HERMES_SAFE_MODE=1 — plugin discovery skipped")
self._discovered = True

View File

@@ -1,18 +1,10 @@
"""Tests for `hermes chat --safe-mode` — pristine troubleshooting runs.
Inspired by Claude Code v2.1.169's ``--safe-mode`` flag (June 2026), which
disables all customizations (CLAUDE.md, plugins, skills, hooks, MCP) for
troubleshooting. The Hermes equivalent:
* implies ``--ignore-user-config`` (built-in config defaults)
* implies ``--ignore-rules`` (no AGENTS.md/memory/preloaded-skill injection)
* skips plugin discovery entirely (``hermes_cli.plugins``)
* loads zero MCP servers (``tools.mcp_tool._load_mcp_config``)
"""
"""Tests for `hermes chat --safe-mode` isolation."""
from __future__ import annotations
import os
import sys
import types
import pytest
@@ -29,102 +21,112 @@ def _clean_env(monkeypatch):
os.environ.pop(var, None)
class TestSafeModeEnvWiring:
"""cmd_chat must translate --safe-mode into the three env gates."""
def test_cmd_chat_safe_mode_sets_env_before_startup(monkeypatch):
import hermes_cli.main as main_mod
from hermes_cli._parser import build_top_level_parser
def test_safe_mode_sets_all_gates(self):
# Mirrors the cmd_chat logic in hermes_cli/main.py.
class Args:
safe_mode = True
parser, _subparsers, chat_parser = build_top_level_parser()
chat_parser.set_defaults(func=main_mod.cmd_chat)
args = parser.parse_args(["chat", "--safe-mode"])
captured: dict[str, object] = {}
fake_cli = types.ModuleType("cli")
args = Args()
if getattr(args, "safe_mode", False):
os.environ["HERMES_SAFE_MODE"] = "1"
os.environ["HERMES_IGNORE_USER_CONFIG"] = "1"
os.environ["HERMES_IGNORE_RULES"] = "1"
def fake_has_provider() -> bool:
assert os.environ["HERMES_SAFE_MODE"] == "1"
assert os.environ["HERMES_IGNORE_USER_CONFIG"] == "1"
assert os.environ["HERMES_IGNORE_RULES"] == "1"
return True
assert os.environ.get("HERMES_SAFE_MODE") == "1"
assert os.environ.get("HERMES_IGNORE_USER_CONFIG") == "1"
assert os.environ.get("HERMES_IGNORE_RULES") == "1"
def fake_main(**kwargs):
captured.update(kwargs)
monkeypatch.setattr(main_mod, "_has_any_provider_configured", fake_has_provider)
monkeypatch.setattr(main_mod, "_pin_kanban_board_env", lambda: None)
monkeypatch.setattr(main_mod, "_sync_bundled_skills_for_startup", lambda: None)
monkeypatch.setattr(main_mod, "_termux_should_prefetch_update_check", lambda: False)
setattr(fake_cli, "main", fake_main)
monkeypatch.setitem(sys.modules, "cli", fake_cli)
main_mod.cmd_chat(args)
assert captured["ignore_user_config"] is True
assert captured["ignore_rules"] is True
class TestSafeModePluginDiscovery:
"""Plugin discovery must be a no-op under HERMES_SAFE_MODE=1."""
def test_prepare_agent_startup_applies_safe_mode_before_plugin_discovery(monkeypatch):
import hermes_cli.main as main_mod
def test_discovery_skipped(self, monkeypatch):
monkeypatch.setenv("HERMES_SAFE_MODE", "1")
from hermes_cli.plugins import PluginManager
args = types.SimpleNamespace(command="chat", safe_mode=True, tui=False)
plugins = types.ModuleType("hermes_cli.plugins")
mgr = PluginManager()
called = []
monkeypatch.setattr(
mgr, "_discover_and_load_inner", lambda: called.append(True)
)
mgr.discover_and_load()
assert called == [] # inner sweep never ran
assert mgr._discovered is True # registry settled as clean-empty
assert mgr._plugins == {}
def discover_plugins() -> None:
assert os.environ["HERMES_SAFE_MODE"] == "1"
assert os.environ["HERMES_IGNORE_USER_CONFIG"] == "1"
assert os.environ["HERMES_IGNORE_RULES"] == "1"
def test_discovery_runs_without_safe_mode(self, monkeypatch):
monkeypatch.delenv("HERMES_SAFE_MODE", raising=False)
from hermes_cli.plugins import PluginManager
setattr(plugins, "discover_plugins", discover_plugins)
monkeypatch.setitem(sys.modules, "hermes_cli.plugins", plugins)
monkeypatch.setattr(main_mod, "_should_background_mcp_startup", lambda _args: False)
monkeypatch.setattr(main_mod, "_command_has_dedicated_mcp_startup", lambda _args: True)
mgr = PluginManager()
called = []
monkeypatch.setattr(
mgr, "_discover_and_load_inner", lambda: called.append(True)
)
mgr.discover_and_load()
assert called == [True]
main_mod._prepare_agent_startup(args)
class TestSafeModeMCP:
"""_load_mcp_config must return no servers under HERMES_SAFE_MODE=1."""
def test_plugin_discovery_skipped(monkeypatch):
monkeypatch.setenv("HERMES_SAFE_MODE", "1")
from hermes_cli.plugins import PluginManager
def test_mcp_servers_empty(self, monkeypatch):
monkeypatch.setenv("HERMES_SAFE_MODE", "1")
from tools.mcp_tool import _load_mcp_config
mgr = PluginManager()
called = []
monkeypatch.setattr(mgr, "_discover_and_load_inner", lambda: called.append(True))
with pytest.MonkeyPatch.context() as mp:
mp.setattr(
"hermes_cli.config.load_config",
lambda: {"mcp_servers": {"github": {"url": "https://example.com/mcp"}}},
)
assert _load_mcp_config() == {}
mgr.discover_and_load()
def test_mcp_servers_load_without_safe_mode(self, monkeypatch):
monkeypatch.delenv("HERMES_SAFE_MODE", raising=False)
from tools.mcp_tool import _load_mcp_config
with pytest.MonkeyPatch.context() as mp:
mp.setattr(
"hermes_cli.config.load_config",
lambda: {"mcp_servers": {"github": {"url": "https://example.com/mcp"}}},
)
servers = _load_mcp_config()
assert "github" in servers
assert called == []
assert mgr._discovered is True
assert mgr._plugins == {}
class TestSafeModeParser:
"""--safe-mode must parse on both the root parser and `hermes chat`."""
def test_plugin_discovery_runs_without_safe_mode(monkeypatch):
from hermes_cli.plugins import PluginManager
def test_chat_subcommand_accepts_flag(self):
from hermes_cli._parser import build_top_level_parser
mgr = PluginManager()
called = []
monkeypatch.setattr(mgr, "_discover_and_load_inner", lambda: called.append(True))
parser, _subparsers, _chat = build_top_level_parser()
args = parser.parse_args(["chat", "--safe-mode"])
assert getattr(args, "safe_mode", False) is True
mgr.discover_and_load()
def test_root_parser_accepts_flag(self):
from hermes_cli._parser import build_top_level_parser
assert called == [True]
parser, _subparsers, _chat = build_top_level_parser()
args = parser.parse_args(["--safe-mode"])
assert getattr(args, "safe_mode", False) is True
def test_default_is_off(self):
from hermes_cli._parser import build_top_level_parser
def test_mcp_servers_empty(monkeypatch):
monkeypatch.setenv("HERMES_SAFE_MODE", "1")
from tools.mcp_tool import _load_mcp_config
parser, _subparsers, _chat = build_top_level_parser()
args = parser.parse_args(["chat"])
assert getattr(args, "safe_mode", False) is False
monkeypatch.setattr(
"hermes_cli.config.load_config",
lambda: {"mcp_servers": {"github": {"url": "https://example.com/mcp"}}},
)
assert _load_mcp_config() == {}
def test_mcp_servers_load_without_safe_mode(monkeypatch):
from tools.mcp_tool import _load_mcp_config
monkeypatch.setattr(
"hermes_cli.config.load_config",
lambda: {"mcp_servers": {"github": {"url": "https://example.com/mcp"}}},
)
assert "github" in _load_mcp_config()
def test_parser_accepts_safe_mode_on_root_and_chat():
from hermes_cli._parser import build_top_level_parser
parser, _subparsers, _chat = build_top_level_parser()
assert parser.parse_args(["--safe-mode"]).safe_mode is True
assert parser.parse_args(["chat", "--safe-mode"]).safe_mode is True
assert parser.parse_args(["chat"]).safe_mode is False

View File

@@ -2686,9 +2686,8 @@ def _load_mcp_config() -> Dict[str, dict]:
"""
try:
from hermes_cli.config import load_config
# Safe mode (--safe-mode / HERMES_SAFE_MODE=1): troubleshooting run
# with all customizations disabled — no MCP servers connect.
from utils import env_var_enabled as _env_enabled
if _env_enabled("HERMES_SAFE_MODE"):
return {}
config = load_config()