Files
hermes-agent/hermes_cli/config_env_routing.py
T
teknium1 70e4938c07 fix(config): route every registered env setting through .env from config set/get/unset
`hermes config set FEISHU_HOME_CHANNEL oc_x` wrote the top level of config.yaml
while the platform setup flows and /sethome write the same name to .env via
save_env_value, so two writers fed two readers: the gateway bridges the yaml copy
into the environment only when .env lacks the name, one-shot CLI readers never
bridge, and the two copies diverged silently (#111848). Only credential-shaped
names were routed to .env because `_is_env_config_key` is the provider-credential
predicate.

Follow-up to KoNit-K's cherry-picked fix (#111850), which routed the
`setup_hidden_env` suffix family: the predicate now lives in the topical sibling
`hermes_cli/config_env_routing.py` and covers every bare name Hermes itself
registers as an environment variable (OPTIONAL_ENV_VARS, _EXTRA_ENV_KEYS — "env
var names written to .env" — plus the setup-hidden suffixes for plugin adapters
nobody enumerated), so `*_ALLOWED_USERS`, `WHATSAPP_MODE`, `MATRIX_PASSWORD` and
the rest of the adapter-saved family take the same file. `set` and `unset` also
drop a stale same-named top-level config.yaml copy so the reporter's drift cannot
come back, and `get` resolves .env first then that copy — the gateway's own read
order. Provider credentials keep the credential_lifecycle rotation path.

Docs: environment-variables.md tip, hermes_cli/AGENTS.md config rule.
2026-09-15 18:28:49 -07:00

68 lines
2.9 KiB
Python

"""Which ``hermes config`` keys live in ``.env`` instead of ``config.yaml``, and their lifecycle.
Platform setting keys such as ``FEISHU_HOME_CHANNEL`` had two writers: the platform setup flows and
``/sethome`` persist them to ``.env`` through ``save_env_value``, while ``hermes config set`` only
routed credential-shaped names there and wrote every other bare name to the top level of
``config.yaml``. The gateway bridges top-level scalars into the environment only when ``.env`` lacks
the name and one-shot CLI readers never bridge, so the two copies diverged silently (#111848).
Every name Hermes itself registers as an environment variable now routes to ``.env`` from
``set``/``get``/``unset``; provider credentials keep their own rotation lifecycle in
``hermes_cli.credential_lifecycle``.
"""
from typing import Optional
def is_env_setting_key(key: str) -> bool:
"""True for a bare (undotted) name Hermes documents as a ``.env`` variable: registered in
``OPTIONAL_ENV_VARS`` or ``_EXTRA_ENV_KEYS``, or carrying a self-configuring platform suffix so
plugin adapters nobody enumerated (``IRC_HOME_CHANNEL``) get the same routing."""
if "." in key:
return False
from hermes_cli.config import _EXTRA_ENV_KEYS, OPTIONAL_ENV_VARS
from hermes_cli.setup_hidden_env import is_setup_hidden_env
name = key.upper()
return name in OPTIONAL_ENV_VARS or name in _EXTRA_ENV_KEYS or is_setup_hidden_env(name)
def _drop_config_yaml_copies(key: str) -> bool:
"""Remove same-named top-level ``config.yaml`` copies (as typed and upper-cased) so the ``.env``
value is the only one the gateway bridge and CLI readers can disagree about."""
from hermes_cli.config import _write_user_config, get_config_path, require_readable_config_before_write
config_path = get_config_path()
user_config = require_readable_config_before_write(config_path)
stale = [name for name in {key, key.upper()} if name in user_config]
for name in stale:
del user_config[name]
if stale:
_write_user_config(config_path, user_config)
return bool(stale)
def save_env_setting(key: str, value: str) -> None:
from hermes_cli.config import save_env_value
save_env_value(key.upper(), value)
_drop_config_yaml_copies(key)
def remove_env_setting(key: str) -> bool:
"""Remove the ``.env`` entry and any stale ``config.yaml`` copy; False when neither existed."""
from hermes_cli.config import remove_env_value
removed = remove_env_value(key.upper())
return _drop_config_yaml_copies(key) or removed
def read_env_setting(key: str) -> Optional[str]:
"""Resolve like the gateway does: ``.env`` first, then a not-yet-converged top-level
``config.yaml`` copy under the name as typed."""
from hermes_cli.config import get_env_value, read_raw_config_readonly
value = get_env_value(key.upper())
if value is None:
value = read_raw_config_readonly().get(key)
return value