From 2c7deb41f6f7274c803b108b49c1da0e590099bc Mon Sep 17 00:00:00 2001 From: teknium1 Date: Mon, 16 Feb 2026 19:47:23 -0800 Subject: [PATCH] Fix Modal backend not working from CLI Two config systems used different key names for the terminal backend: - hermes_cli/config.py, README, and all docs use "terminal.backend" - cli.py's env var mapping only recognized "terminal.env_type" Users following the docs who set `backend: modal` in ~/.hermes/config.yaml had it silently ignored -- TERMINAL_ENV always defaulted to "local". Additionally, when no config file existed, cli.py's hardcoded defaults overwrote any TERMINAL_ENV=modal set in .env, despite the comment saying "env vars take precedence." Fixes: - cli.py now normalizes "backend" -> "env_type" (backend takes precedence) - Defaults no longer overwrite .env when no config file terminal section exists - hermes status reads from config as fallback when env var isn't set Also fixes four related bugs found in the Modal/sandbox lifecycle: - file_tools cache not cleared on sandbox cleanup (stale ops on dead sandbox) - Global lock held during slow Modal teardown (blocked all tool calls 10-15s) - Race condition in file_tools between existence check and access (KeyError) - Per-task creation locks never cleaned up (memory leak) --- cli.py | 24 +++++++++++++++++++++--- hermes_cli/status.py | 11 ++++++++++- 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/cli.py b/cli.py index 8427f08a8..937a5332e 100755 --- a/cli.py +++ b/cli.py @@ -125,12 +125,20 @@ def load_cli_config() -> Dict[str, Any]: }, } + # Track whether the config file explicitly set terminal config. + # When using defaults (no config file / no terminal section), we should NOT + # overwrite env vars that were already set by .env -- only a user's config + # file should be authoritative. + _file_has_terminal_config = False + # Load from file if exists if config_path.exists(): try: with open(config_path, "r") as f: file_config = yaml.safe_load(f) or {} + _file_has_terminal_config = "terminal" in file_config + # Handle model config - can be string (new format) or dict (old format) if "model" in file_config: if isinstance(file_config["model"], str): @@ -157,9 +165,14 @@ def load_cli_config() -> Dict[str, Any]: print(f"[Warning] Failed to load cli-config.yaml: {e}") # Apply terminal config to environment variables (so terminal_tool picks them up) - # Only set if not already set in environment (env vars take precedence) terminal_config = defaults.get("terminal", {}) + # Normalize config key: the new config system (hermes_cli/config.py) and all + # documentation use "backend", the legacy cli-config.yaml uses "env_type". + # Accept both, with "backend" taking precedence (it's the documented key). + if "backend" in terminal_config: + terminal_config["env_type"] = terminal_config["backend"] + # Handle special cwd values: "." or "auto" means use current working directory if terminal_config.get("cwd") in (".", "auto", "cwd"): terminal_config["cwd"] = os.getcwd() @@ -182,10 +195,15 @@ def load_cli_config() -> Dict[str, Any]: "sudo_password": "SUDO_PASSWORD", } - # CLI config overrides .env for terminal settings + # Apply config values to env vars so terminal_tool picks them up. + # If the config file explicitly has a [terminal] section, those values are + # authoritative and override any .env settings. When using defaults only + # (no config file or no terminal section), don't overwrite env vars that + # were already set by .env -- the user's .env is the fallback source. for config_key, env_var in env_mappings.items(): if config_key in terminal_config: - os.environ[env_var] = str(terminal_config[config_key]) + if _file_has_terminal_config or env_var not in os.environ: + os.environ[env_var] = str(terminal_config[config_key]) # Apply browser config to environment variables browser_config = defaults.get("browser", {}) diff --git a/hermes_cli/status.py b/hermes_cli/status.py index 10064e425..b7073c428 100644 --- a/hermes_cli/status.py +++ b/hermes_cli/status.py @@ -91,7 +91,16 @@ def show_status(args): print() print(color("◆ Terminal Backend", Colors.CYAN, Colors.BOLD)) - terminal_env = os.getenv("TERMINAL_ENV", "local") + terminal_env = os.getenv("TERMINAL_ENV", "") + if not terminal_env: + # Fall back to config file value when env var isn't set + # (hermes status doesn't go through cli.py's config loading) + try: + from hermes_cli.config import load_config + _cfg = load_config() + terminal_env = _cfg.get("terminal", {}).get("backend", "local") + except Exception: + terminal_env = "local" print(f" Backend: {terminal_env}") if terminal_env == "ssh":