fix(config): apply managed overlay when the user config.yaml is absent

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.
This commit is contained in:
Ben Barclay
2026-09-11 10:28:13 +10:00
parent 337d69856f
commit 1d5debfa65
2 changed files with 17 additions and 18 deletions
+14 -12
View File
@@ -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
+3 -6
View File
@@ -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()