Serialize Hermes config access

This commit is contained in:
BennetYrWang 2026-04-26 05:10:37 -04:00 committed by Teknium
parent 307c85e5c1
commit 34f7297359

View file

@ -21,6 +21,7 @@ import stat
import subprocess import subprocess
import sys import sys
import tempfile import tempfile
import threading
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from typing import Dict, Any, Optional, List, Tuple from typing import Dict, Any, Optional, List, Tuple
@ -42,6 +43,14 @@ _LOAD_CONFIG_CACHE: Dict[str, Tuple[int, int, Dict[str, Any]]] = {}
# _LOAD_CONFIG_CACHE but for read_raw_config() — used when callers want # _LOAD_CONFIG_CACHE but for read_raw_config() — used when callers want
# the user's on-disk values without defaults merged in. # the user's on-disk values without defaults merged in.
_RAW_CONFIG_CACHE: Dict[str, Tuple[int, int, Dict[str, Any]]] = {} _RAW_CONFIG_CACHE: Dict[str, Tuple[int, int, Dict[str, Any]]] = {}
# Serializes all config read/write paths. libyaml's C extension is not
# thread-safe for concurrent safe_load() on the same file, and multiple
# tool threads (approval.py, browser_tool.py, setup flows) hit
# load_config / read_raw_config / save_config from different threads
# during long agent runs. RLock (not Lock) because save_config internally
# calls read_raw_config. Also covers mutation of the module-level cache
# dicts above.
_CONFIG_LOCK = threading.RLock()
# Env var names written to .env that aren't in OPTIONAL_ENV_VARS # Env var names written to .env that aren't in OPTIONAL_ENV_VARS
# (managed by setup/provider flows directly). # (managed by setup/provider flows directly).
_EXTRA_ENV_KEYS = frozenset({ _EXTRA_ENV_KEYS = frozenset({
@ -3941,6 +3950,7 @@ def read_raw_config() -> Dict[str, Any]:
``load_config()``. Returns a deepcopy on every call since some callers ``load_config()``. Returns a deepcopy on every call since some callers
mutate the result before passing to ``save_config()``. mutate the result before passing to ``save_config()``.
""" """
with _CONFIG_LOCK:
try: try:
config_path = get_config_path() config_path = get_config_path()
st = config_path.stat() st = config_path.stat()
@ -3975,6 +3985,7 @@ def load_config() -> Dict[str, Any]:
(which change ``HERMES_HOME`` and therefore ``get_config_path()``) (which change ``HERMES_HOME`` and therefore ``get_config_path()``)
don't collide. don't collide.
""" """
with _CONFIG_LOCK:
ensure_hermes_home() ensure_hermes_home()
config_path = get_config_path() config_path = get_config_path()
path_key = str(config_path) path_key = str(config_path)
@ -4094,6 +4105,7 @@ _COMMENTED_SECTIONS = """
def save_config(config: Dict[str, Any]): def save_config(config: Dict[str, Any]):
"""Save configuration to ~/.hermes/config.yaml.""" """Save configuration to ~/.hermes/config.yaml."""
with _CONFIG_LOCK:
if is_managed(): if is_managed():
managed_error("save configuration") managed_error("save configuration")
return return