refactor(config): add cfg_get() helper; migrate 20 nested-get call sites (#17304)
The "cfg.get('X', {}).get('Y', default)" pattern appears 50+ times
across tools/, gateway/, and plugins/. Each call site manually handles
the same three gotchas:
1. Missing intermediate key → empty dict → chain works
2. Non-dict value at intermediate position → AttributeError
(uncaught in most sites, so a misconfigured YAML crashes the tool)
3. cfg is None → AttributeError
Introduces cfg_get(cfg, *keys, default=None) in hermes_cli/config.py
as the canonical helper. Handles all three uniformly, returns default
only when the final key is *absent* (matches dict.get semantics —
explicit None values are preserved, falsy values like 0 / False / ''
are preserved).
Named cfg_get rather than cfg_path to avoid shadowing the existing
'cfg_path = _hermes_home / "config.yaml"' local variable that appears
in gateway/run.py, cron/scheduler.py, hermes_cli/main.py, etc.
Migrated 20 call sites as the first-batch proof-of-value:
gateway/run.py 10 sites (agent/display subtrees)
tools/browser_tool.py 3 sites
tools/vision_tools.py 2 sites
tools/browser_camofox.py 1 site
tools/approval.py 1 site
tools/skills_tool.py 1 site
tools/skill_manager_tool.py 1 site
tools/credential_files.py 1 site
tools/env_passthrough.py 1 site
The remaining ~30 sites across plugins/ and smaller tool files can be
migrated opportunistically — the helper is now available and the
pattern is established.
Fixed a latent bug along the way: tools/vision_tools.py had its
cfg_get usage at line 560 inside a function that locally re-imports
'from hermes_cli.config import load_config', but the AST-based
migration script wrote the top-level cfg_get import to a different
function scope, leaving line 560's cfg_get as a NameError silently
swallowed by the surrounding try/except. Test
test_vision_uses_configured_temperature_and_timeout caught it. Fixed
by including cfg_get in the function-local import.
Verified:
- 7880/7893 tests/tools/ + tests/gateway/ + tests/hermes_cli/test_config
tests pass; all 13 failures pre-existing on main (MCP, delegate,
session_split_brain — verified earlier in the sweep).
- All 20 migrated sites AST-verified to have cfg_get in scope (either
module-level or function-local).
- Live 'hermes chat' smoke: 2 turns + /model switch + tool calls +
/quit, zero errors. Agent correctly counted 20 cfg_get hits across
8 tool files — matching the migration.
Semantic parity verified against the original pattern across 8 edge
cases (missing keys, None values, falsy values, empty strings, string
instead of dict, None cfg, nested levels).
This commit is contained in:
@@ -3477,6 +3477,52 @@ def _normalize_max_turns_config(config: Dict[str, Any]) -> Dict[str, Any]:
|
||||
return config
|
||||
|
||||
|
||||
def cfg_get(cfg: Optional[Dict[str, Any]], *keys: str, default: Any = None) -> Any:
|
||||
"""Traverse nested dict keys safely, returning ``default`` on any miss.
|
||||
|
||||
Canonical helper for the ``cfg.get("X", {}).get("Y", default)`` pattern
|
||||
that appears 50+ times across the codebase. Handles three common gotchas
|
||||
in one place:
|
||||
|
||||
1. Missing intermediate keys (returns ``default``, no KeyError).
|
||||
2. An intermediate value that's not a dict (e.g. a user wrote a string
|
||||
where a section was expected). Returns ``default`` instead of
|
||||
AttributeError on ``.get()``.
|
||||
3. ``cfg is None`` (callers sometimes pass ``load_config() or None``).
|
||||
|
||||
Named ``cfg_get`` rather than ``cfg_path`` to avoid shadowing the
|
||||
ubiquitous ``cfg_path = _hermes_home / "config.yaml"`` local variable
|
||||
that appears in gateway/run.py, cron/scheduler.py, main.py, etc.
|
||||
|
||||
Explicit ``None`` values are returned as-is (matches ``dict.get(key,
|
||||
default)`` semantics — ``default`` is only returned when the key is
|
||||
*absent*, not when it's present but set to ``None``).
|
||||
|
||||
Examples:
|
||||
>>> cfg_get({"agent": {"reasoning_effort": "high"}}, "agent", "reasoning_effort")
|
||||
'high'
|
||||
>>> cfg_get({}, "agent", "reasoning_effort", default="medium")
|
||||
'medium'
|
||||
>>> cfg_get({"agent": "oops_a_string"}, "agent", "reasoning_effort", default="low")
|
||||
'low'
|
||||
>>> cfg_get(None, "anything", default=42)
|
||||
42
|
||||
>>> cfg_get({"a": {"b": None}}, "a", "b", default="def") # explicit None preserved
|
||||
>>> cfg_get({"a": {"b": False}}, "a", "b", default=True) # falsy values preserved
|
||||
False
|
||||
"""
|
||||
if not isinstance(cfg, dict):
|
||||
return default
|
||||
node: Any = cfg
|
||||
for key in keys:
|
||||
if not isinstance(node, dict):
|
||||
return default
|
||||
if key not in node:
|
||||
return default
|
||||
node = node[key]
|
||||
return node
|
||||
|
||||
|
||||
|
||||
def read_raw_config() -> Dict[str, Any]:
|
||||
"""Read ~/.hermes/config.yaml as-is, without merging defaults or migrating.
|
||||
|
||||
Reference in New Issue
Block a user