Skip to content
Merged
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
33 changes: 29 additions & 4 deletions agent/skill_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,12 @@
from pathlib import Path, PurePath
from typing import Any, Callable, Dict, List, Optional, Set, Tuple

from hermes_constants import get_config_path, get_skills_dir, is_termux
from hermes_constants import (
get_config_path,
get_skills_dir,
get_subprocess_home,
is_termux,
)

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -698,17 +703,37 @@ def _resolve_dotpath(config: Dict[str, Any], dotted_key: str):
return current


_HOME_VAR_RE = re.compile(r"\$(?:\{HOME\}|HOME)(?=$|[/\\])")


def _expand_skill_config_path(value: str) -> str:
"""Expand ``~`` / ``$HOME`` against the HOME Hermes injects into tool subprocesses.

Skill config defaults describe paths the agent hands to tools, so in a container where the
control process HOME (``/opt/data``) differs from the tool HOME (``{HERMES_HOME}/home``) a
plain ``expanduser`` pointed the prompt at a path no tool would ever read (#12260).
"""
subprocess_home = get_subprocess_home()
if subprocess_home:
if value == "~" or value.startswith(("~/", "~\\")):
value = subprocess_home + value[1:]
# Callable replacement: a literal template would parse backslashes in the home path
# as regex escapes.
value = _HOME_VAR_RE.sub(lambda _m: subprocess_home, value)
return os.path.expanduser(os.path.expandvars(value))


def resolve_skill_config_values(config_vars: List[Dict[str, Any]]) -> Dict[str, Any]:
"""Map logical skill config keys to current values (or declared defaults);
path-like string values are ``~``/``${VAR}`` expanded."""
path-like string values are ``~``/``$HOME``/``${VAR}`` expanded against the tool HOME."""
config = _load_raw_config()
resolved: Dict[str, Any] = {}
for var in config_vars:
value = _resolve_dotpath(config, f"{SKILL_CONFIG_PREFIX}.{var['key']}")
if value is None or (isinstance(value, str) and not value.strip()):
value = var.get("default", "")
if isinstance(value, str) and ("~" in value or "${" in value):
value = os.path.expanduser(os.path.expandvars(value))
if isinstance(value, str) and ("~" in value or "$" in value):
value = _expand_skill_config_path(value)
resolved[var["key"]] = value
return resolved

Expand Down
28 changes: 28 additions & 0 deletions tests/agent/test_skill_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,8 +147,36 @@ def test_get_disabled_skill_names_scalar_string_still_single_name(
assert get_disabled_skill_names() == {"hidden-skill"}


def test_skill_config_home_vars_use_subprocess_home(tmp_path, monkeypatch):
"""``~`` / ``$HOME`` / ``${HOME}`` defaults resolve against the HOME tools receive, not the
control process HOME; other variables keep normal expansion (#12260)."""
from agent import skill_utils

# A backslash in the home path must not be read as a regex-replacement escape.
hermes_home = tmp_path / "da\\ta"
subprocess_home = hermes_home / "home"
subprocess_home.mkdir(parents=True)
(hermes_home / "config.yaml").write_text("", encoding="utf-8")
monkeypatch.setenv("HERMES_HOME", str(hermes_home))
monkeypatch.setenv("HOME", str(hermes_home))
monkeypatch.setenv("TERMINAL_HOME_MODE", "profile")
monkeypatch.setenv("PROJECT_ROOT", "/proj")
monkeypatch.setenv("LEAF", "leaf")
getattr(skill_utils, "_raw_config_cache_clear", lambda: None)()

resolved = resolve_skill_config_values([
{"key": "wiki.home_var", "default": "$HOME/wiki"},
{"key": "wiki.braced_home", "default": "${HOME}/notes"},
{"key": "wiki.tilde", "default": "~/scratch"},
{"key": "wiki.other_var", "default": "${PROJECT_ROOT}/cache"},
{"key": "wiki.tilde_var", "default": "~/$LEAF"},
])

assert resolved["wiki.home_var"] == str(subprocess_home / "wiki")
assert resolved["wiki.braced_home"] == str(subprocess_home / "notes")
assert resolved["wiki.tilde"] == str(subprocess_home / "scratch")
assert resolved["wiki.other_var"] == "/proj/cache"
assert resolved["wiki.tilde_var"] == str(subprocess_home / "leaf")


def test_iter_skill_index_files_prunes_skill_support_dirs(tmp_path):
Expand Down
Loading