From 1d5debfa650d63deb0a01feb0c24402ad4b089b7 Mon Sep 17 00:00:00 2001 From: Ben Barclay Date: Fri, 11 Sep 2026 10:28:13 +1000 Subject: [PATCH] fix(config): apply managed overlay when the user config.yaml is absent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Narrow the PR's load_yaml_layer rewrite to the one behavior the relay fix needs: an absent user file is an empty layer, so administrator- pinned values (a managed relay opt-out among them) still reach the gateway on a host with no config.yaml. Everything else returns to the loader's existing contract. A MALFORMED user file still raises so load_gateway_config() keeps emitting its 'Failed to process config.yaml — falling back to .env / gateway.json' warning instead of silently applying the managed layer over the broken one; the loader keeps its own yaml read rather than importing the CLI's raw reader. Changing the malformed-file contract touches every platform and belongs in its own PR. --- gateway/config_loader.py | 26 +++++++++++--------- tests/gateway/relay/test_explicit_disable.py | 9 +++---- 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/gateway/config_loader.py b/gateway/config_loader.py index 052d3f2fa7..dd638e4b96 100644 --- a/gateway/config_loader.py +++ b/gateway/config_loader.py @@ -336,19 +336,21 @@ def bridge_core_env_settings(yaml_cfg: dict, platforms_data: dict) -> None: def load_yaml_layer(home: Path, gw_data: dict) -> None: - """Overlay raw user + managed YAML onto *gw_data* without introducing CLI defaults.""" - from hermes_cli.config import read_user_config_raw - from hermes_cli import managed_scope + """Overlay ``config.yaml`` + managed YAML onto *gw_data* in place. Raises on a malformed user + file (caller warns + falls back). An ABSENT user file is an empty layer, not a reason to skip + the administrator's values: a fleet host with no ``config.yaml`` must still honor them. + """ + import yaml - # Match the raw runtime/predicate readers: absent, unreadable or malformed - # user YAML is an empty layer, not a reason to skip administrator settings. - # Use the explicit home rather than a process-global path (multiplex-safe). - try: - yaml_cfg = read_user_config_raw(home / "config.yaml") - except Exception as e: - logger.warning("Failed to read %s; applying managed config over an empty user layer: %s", - home / "config.yaml", e) - yaml_cfg = {} + config_yaml_path = home / "config.yaml" + yaml_cfg: dict = {} + if config_yaml_path.exists(): + with open(config_yaml_path, encoding="utf-8") as f: + yaml_cfg = yaml.safe_load(f) or {} + + # Managed scope: overlay administrator-pinned values (this loader bypasses + # hermes_cli.config.load_config, so managed quick_commands / stt would otherwise be ignored). + from hermes_cli import managed_scope yaml_cfg = managed_scope.apply_managed_overlay(yaml_cfg) if not yaml_cfg: return diff --git a/tests/gateway/relay/test_explicit_disable.py b/tests/gateway/relay/test_explicit_disable.py index de5c04485a..305583f486 100644 --- a/tests/gateway/relay/test_explicit_disable.py +++ b/tests/gateway/relay/test_explicit_disable.py @@ -224,21 +224,18 @@ def test_legacy_json_disable_is_advisory_like_other_platforms(profile, monkeypat assert config.platforms[Platform.TELEGRAM].enabled -@pytest.mark.parametrize("user_yaml", ["no-files", "absent", "empty", "null", "list", "malformed", "unreadable", "sibling", "enabled", "disabled"]) +@pytest.mark.parametrize("user_yaml", ["no-files", "absent", "empty", "null", "sibling", "enabled", "disabled"]) @pytest.mark.parametrize("managed_state", ["absent", "url-only", "enabled", "disabled"]) def test_managed_layer_agrees_with_native_config(profile, monkeypatch, user_yaml, managed_state): """Managed leaves override user leaves, not CLI defaults or a second merge policy.""" path = profile / "config.yaml" user_docs = { - "empty": "", "null": "null\n", "list": "- not-a-config\n", - "malformed": "platforms: [\n", + "empty": "", "null": "null\n", "sibling": "platforms:\n relay:\n extra:\n user_sibling: kept\n", "enabled": "platforms:\n relay:\n enabled: true\n", "disabled": "platforms:\n relay:\n enabled: false\n", } - if user_yaml == "unreadable": - path.mkdir() # A directory fails open even when tests run as root. - elif user_yaml not in {"absent", "no-files"}: + if user_yaml not in {"absent", "no-files"}: path.write_text(user_docs[user_yaml]) managed = profile / "managed" managed.mkdir()