From 3b1290ab1b15f6ab2fb28c8a1900e1f36c7a7019 Mon Sep 17 00:00:00 2001 From: RelaxJonh <92573950+RelaxJonh@users.noreply.github.com> Date: Sat, 1 Aug 2026 21:52:04 +0700 Subject: [PATCH] fix: guard against list-valued keys in hermes config set (#76138) hermes config set always writes a scalar, which corrupts list-typed settings (writes a quoted YAML string instead of a YAML sequence). Add a pre-write guard that checks _default_value_for_key() and the existing user config value. When either is a list, refuse the write with a clear error message pointing to hermes config edit. The guard covers both bare keys (e.g. 'toolsets') and dotted keys (e.g. 'plugins.enabled'). --force bypasses it for scripted use. Closes #76138 --- hermes_cli/config.py | 61 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/hermes_cli/config.py b/hermes_cli/config.py index 6668677bd52c2..c1abd15d67d0d 100644 --- a/hermes_cli/config.py +++ b/hermes_cli/config.py @@ -4945,6 +4945,67 @@ def set_config_value(key: str, value: str, force: bool = False): file=sys.stderr, ) sys.exit(1) + # Guard against list-valued keys (#76138): ``hermes config set`` always + # writes a scalar, which corrupts list-typed settings (writes a quoted + # YAML string instead of a YAML sequence). Check both the existing + # user-config value and the declared default — either being a list is + # enough to refuse (unless --force). + if not force and "." not in key: + _existing_for_list = user_config.get(key) + _default_for_list = _default_value_for_key(key) + _list_val = _existing_for_list if isinstance(_existing_for_list, list) else _default_for_list + if isinstance(_list_val, list): + print( + f"✗ '{key}' is a list-valued setting. `hermes config set` writes a\n" + f" scalar and would corrupt it (quoted string instead of YAML list).\n" + f" Edit config.yaml directly, or use `hermes config edit`.", + file=sys.stderr, + ) + print( + f" Example — in config.yaml:\n" + f" {key}:\n" + f" - item1\n" + f" - item2", + file=sys.stderr, + ) + print( + f" Or use --force to overwrite with a scalar anyway:", + file=sys.stderr, + ) + print( + f" hermes config set --force {key} {value!r}", + file=sys.stderr, + ) + sys.exit(1) + elif not force and "." in key: + # For dotted keys, walk user_config to find the parent's existing value. + _parts = key.split(".") + _parent = user_config + for _p in _parts[:-1]: + if isinstance(_parent, dict): + _parent = _parent.get(_p) + else: + _parent = None + break + _leaf_existing = _parent.get(_parts[-1]) if isinstance(_parent, dict) else None + _leaf_default = _default_value_for_key(key) + _list_val = _leaf_existing if isinstance(_leaf_existing, list) else _leaf_default + if isinstance(_list_val, list): + print( + f"✗ '{key}' is a list-valued setting. `hermes config set` writes a\n" + f" scalar and would corrupt it (quoted string instead of YAML list).\n" + f" Edit config.yaml directly, or use `hermes config edit`.", + file=sys.stderr, + ) + print( + f" Or use --force to overwrite with a scalar anyway:", + file=sys.stderr, + ) + print( + f" hermes config set --force {key} {value!r}", + file=sys.stderr, + ) + sys.exit(1) _set_nested(user_config, key, value) # Normalize the api_base → base_url alias at set-time too (issue #8919), # so a fresh `hermes config set model.api_base ...` lands on the canonical