diff --git a/cli.py b/cli.py index af1077744c0f..e5abc2c00d40 100644 --- a/cli.py +++ b/cli.py @@ -301,7 +301,11 @@ def load_cli_config() -> Dict[str, Any]: defaults["agent"]["max_turns"] = file_config["max_turns"] except Exception as e: logger.warning("Failed to load cli-config.yaml: %s", e) - + + # Expand ${ENV_VAR} references in config values before bridging to env vars. + from hermes_cli.config import _expand_env_vars + defaults = _expand_env_vars(defaults) + # Apply terminal config to environment variables (so terminal_tool picks them up) terminal_config = defaults.get("terminal", {}) diff --git a/gateway/run.py b/gateway/run.py index 7876565b4024..95d1f43e92e3 100644 --- a/gateway/run.py +++ b/gateway/run.py @@ -93,6 +93,9 @@ if _config_path.exists(): import yaml as _yaml with open(_config_path, encoding="utf-8") as _f: _cfg = _yaml.safe_load(_f) or {} + # Expand ${ENV_VAR} references before bridging to env vars. + from hermes_cli.config import _expand_env_vars + _cfg = _expand_env_vars(_cfg) # Top-level simple values (fallback only — don't override .env) for _key, _val in _cfg.items(): if isinstance(_val, (str, int, float, bool)) and _key not in os.environ: diff --git a/hermes_cli/config.py b/hermes_cli/config.py index bacd853a73d9..6b45ea8f829a 100644 --- a/hermes_cli/config.py +++ b/hermes_cli/config.py @@ -1179,7 +1179,6 @@ def _expand_env_vars(obj): None are left untouched. Unresolved references (variable not in ``os.environ``) are kept verbatim so callers can detect them. """ - import re if isinstance(obj, str): return re.sub( r"\${([^}]+)}", @@ -1191,6 +1190,8 @@ def _expand_env_vars(obj): if isinstance(obj, list): return [_expand_env_vars(item) for item in obj] return obj + + def _normalize_max_turns_config(config: Dict[str, Any]) -> Dict[str, Any]: """Normalize legacy root-level max_turns into agent.max_turns.""" config = dict(config) diff --git a/tests/test_config_env_expansion.py b/tests/test_config_env_expansion.py index 87702bd18724..860129ce8198 100644 --- a/tests/test_config_env_expansion.py +++ b/tests/test_config_env_expansion.py @@ -3,6 +3,7 @@ import os import pytest from hermes_cli.config import _expand_env_vars, load_config +from unittest.mock import patch as mock_patch class TestExpandEnvVars: @@ -90,3 +91,42 @@ class TestLoadConfigExpansion: config = load_config() assert config["model"]["api_key"] == "${NOT_SET_XYZ_123}" + + +class TestLoadCliConfigExpansion: + """Verify that load_cli_config() also expands ${VAR} references.""" + + def test_cli_config_expands_auxiliary_api_key(self, tmp_path, monkeypatch): + config_yaml = ( + "auxiliary:\n" + " vision:\n" + " api_key: ${TEST_VISION_KEY_XYZ}\n" + ) + config_file = tmp_path / "config.yaml" + config_file.write_text(config_yaml) + + monkeypatch.setenv("TEST_VISION_KEY_XYZ", "vis-key-123") + # Patch the hermes home so load_cli_config finds our test config + monkeypatch.setattr("cli._hermes_home", tmp_path) + + from cli import load_cli_config + config = load_cli_config() + + assert config["auxiliary"]["vision"]["api_key"] == "vis-key-123" + + def test_cli_config_unresolved_kept_verbatim(self, tmp_path, monkeypatch): + config_yaml = ( + "auxiliary:\n" + " vision:\n" + " api_key: ${UNSET_CLI_VAR_ABC}\n" + ) + config_file = tmp_path / "config.yaml" + config_file.write_text(config_yaml) + + monkeypatch.delenv("UNSET_CLI_VAR_ABC", raising=False) + monkeypatch.setattr("cli._hermes_home", tmp_path) + + from cli import load_cli_config + config = load_cli_config() + + assert config["auxiliary"]["vision"]["api_key"] == "${UNSET_CLI_VAR_ABC}"