Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 13 additions & 4 deletions hermes_cli/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -8355,15 +8355,16 @@ def edit_config():
def _default_value_for_key(dotted_key: str):
"""Return the leaf value declared for *dotted_key* in ``DEFAULT_CONFIG``.

Unknown keys and non-leaf paths return ``None`` so they retain the legacy
best-effort coercion used by ``config set``.
Unknown paths return ``None`` so they retain the legacy best-effort
coercion used by ``config set``. Container defaults are returned too so
the setter can preserve their declared list or mapping type.
"""
node = DEFAULT_CONFIG
for part in dotted_key.split("."):
if not isinstance(node, dict) or part not in node:
return None
node = node[part]
return node if not isinstance(node, dict) else None
return node


def set_config_value(key: str, value: str):
Expand Down Expand Up @@ -8415,7 +8416,15 @@ def set_config_value(key: str, value: str):
# such as approvals.mode="off" must not become YAML booleans. Unknown keys
# retain the historical best-effort coercion behavior.
coerced_value: Any = value
if not isinstance(_default_value_for_key(key), str):
default_value = _default_value_for_key(key)
if isinstance(default_value, (dict, list)):
try:
parsed_value = yaml.safe_load(value)
except yaml.YAMLError:
parsed_value = None
if isinstance(parsed_value, type(default_value)):
coerced_value = parsed_value
elif not isinstance(default_value, str):
if value.lower() in {'true', 'yes', 'on'}:
coerced_value = True
elif value.lower() in {'false', 'no', 'off'}:
Expand Down
20 changes: 20 additions & 0 deletions tests/hermes_cli/test_set_config_value.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,26 @@ def test_terminal_ssh_prefix_routes_to_env(self, _isolated_hermes_home):
class TestConfigYamlRouting:
"""Regular config keys should go to config.yaml, NOT .env."""

def test_list_default_parses_yaml_list_literal(self, _isolated_hermes_home):
"""A list-valued default accepts a shell-friendly YAML list."""
set_config_value("command_allowlist", "['git', 'ollama', 'brew']")

import yaml

saved = yaml.safe_load(_read_config(_isolated_hermes_home))
assert saved["command_allowlist"] == ["git", "ollama", "brew"]
assert isinstance(saved["command_allowlist"], list)

def test_mapping_default_parses_yaml_mapping_literal(self, _isolated_hermes_home):
"""A mapping-valued default accepts a shell-friendly YAML mapping."""
set_config_value("quick_commands", "{hello: {type: exec}}")

import yaml

saved = yaml.safe_load(_read_config(_isolated_hermes_home))
assert saved["quick_commands"] == {"hello": {"type": "exec"}}
assert isinstance(saved["quick_commands"], dict)

def test_simple_key(self, _isolated_hermes_home):
set_config_value("model", "gpt-4o")
config = _read_config(_isolated_hermes_home)
Expand Down