fix(constants): use windows native default hermes home
This commit is contained in:
@ -5,6 +5,7 @@ without risk of circular imports.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import sysconfig
|
||||
from contextvars import ContextVar, Token
|
||||
from pathlib import Path
|
||||
@ -40,17 +41,26 @@ def get_hermes_home_override() -> str | None:
|
||||
return str(override)
|
||||
|
||||
|
||||
def get_hermes_home() -> Path:
|
||||
"""Return the Hermes home directory (default: ~/.hermes).
|
||||
def _get_platform_default_hermes_home() -> Path:
|
||||
"""Return the platform-native default Hermes home path."""
|
||||
if sys.platform == "win32":
|
||||
local_appdata = os.environ.get("LOCALAPPDATA", "").strip()
|
||||
base = Path(local_appdata) if local_appdata else Path.home() / "AppData" / "Local"
|
||||
return base / "hermes"
|
||||
return Path.home() / ".hermes"
|
||||
|
||||
Reads HERMES_HOME env var, falls back to ~/.hermes.
|
||||
|
||||
def get_hermes_home() -> Path:
|
||||
"""Return the Hermes home directory (default: platform-native path).
|
||||
|
||||
Reads HERMES_HOME env var, falls back to the platform-native default.
|
||||
This is the single source of truth — all other copies should import this.
|
||||
|
||||
When ``HERMES_HOME`` is unset but an ``active_profile`` file indicates
|
||||
a non-default profile is active, logs a loud one-shot warning to
|
||||
``errors.log`` so cross-profile data corruption is diagnosable instead
|
||||
of silent. Behavior is unchanged otherwise — we still return
|
||||
``~/.hermes`` — because raising here would brick 30+ module-level
|
||||
the platform-native default — because raising here would brick 30+ module-level
|
||||
callers that import this at load time. Subprocess spawners are
|
||||
expected to propagate ``HERMES_HOME`` explicitly (see the systemd
|
||||
template in ``hermes_cli/gateway.py`` and the kanban dispatcher in
|
||||
@ -69,10 +79,8 @@ def get_hermes_home() -> Path:
|
||||
global _profile_fallback_warned
|
||||
if not _profile_fallback_warned:
|
||||
try:
|
||||
# Inline the default-root resolution from get_default_hermes_root()
|
||||
# to stay import-safe (this function is called from module scope
|
||||
# in 30+ files; we cannot afford to trigger logging setup here).
|
||||
active_path = (Path.home() / ".hermes" / "active_profile")
|
||||
fallback_home = _get_platform_default_hermes_home()
|
||||
active_path = fallback_home / "active_profile"
|
||||
active = active_path.read_text().strip() if active_path.exists() else ""
|
||||
except (UnicodeDecodeError, OSError):
|
||||
active = ""
|
||||
@ -83,10 +91,9 @@ def get_hermes_home() -> Path:
|
||||
# module-import time from 30+ sites, often before logging is
|
||||
# configured, and (b) root-logger propagation would double-emit
|
||||
# on consoles where a StreamHandler is already attached.
|
||||
import sys
|
||||
msg = (
|
||||
f"[HERMES_HOME fallback] HERMES_HOME is unset but active "
|
||||
f"profile is {active!r}. Falling back to ~/.hermes, which "
|
||||
f"profile is {active!r}. Falling back to {fallback_home}, which "
|
||||
f"is the DEFAULT profile — not {active!r}. Any data this "
|
||||
f"process writes will land in the wrong profile. The "
|
||||
f"subprocess spawner should pass HERMES_HOME explicitly "
|
||||
@ -98,13 +105,14 @@ def get_hermes_home() -> Path:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return Path.home() / ".hermes"
|
||||
return _get_platform_default_hermes_home()
|
||||
|
||||
|
||||
def get_default_hermes_root() -> Path:
|
||||
"""Return the root Hermes directory for profile-level operations.
|
||||
|
||||
In standard deployments this is ``~/.hermes``.
|
||||
In standard deployments this is the platform-native Hermes home
|
||||
(``~/.hermes`` on POSIX, ``%LOCALAPPDATA%\\hermes`` on native Windows).
|
||||
|
||||
In Docker or custom deployments where ``HERMES_HOME`` points outside
|
||||
``~/.hermes`` (e.g. ``/opt/data``), returns ``HERMES_HOME`` directly
|
||||
@ -117,7 +125,7 @@ def get_default_hermes_root() -> Path:
|
||||
|
||||
Import-safe — no dependencies beyond stdlib.
|
||||
"""
|
||||
native_home = Path.home() / ".hermes"
|
||||
native_home = _get_platform_default_hermes_home()
|
||||
env_home = os.environ.get("HERMES_HOME", "")
|
||||
if not env_home:
|
||||
return native_home
|
||||
|
||||
@ -9,6 +9,7 @@ import hermes_constants
|
||||
from hermes_constants import (
|
||||
VALID_REASONING_EFFORTS,
|
||||
get_default_hermes_root,
|
||||
get_hermes_home,
|
||||
is_container,
|
||||
parse_reasoning_effort,
|
||||
secure_parent_dir,
|
||||
@ -68,6 +69,41 @@ class TestGetDefaultHermesRoot:
|
||||
monkeypatch.setenv("HERMES_HOME", str(profile))
|
||||
assert get_default_hermes_root() == docker_root
|
||||
|
||||
def test_no_hermes_home_returns_localappdata_root_on_windows(self, tmp_path, monkeypatch):
|
||||
"""Native Windows falls back to %LOCALAPPDATA%\\hermes, not ~/.hermes."""
|
||||
local_appdata = tmp_path / "LocalAppData"
|
||||
monkeypatch.delenv("HERMES_HOME", raising=False)
|
||||
monkeypatch.setenv("LOCALAPPDATA", str(local_appdata))
|
||||
monkeypatch.setattr(Path, "home", lambda: tmp_path / "Home")
|
||||
monkeypatch.setattr(hermes_constants.sys, "platform", "win32")
|
||||
|
||||
assert get_default_hermes_root() == local_appdata / "hermes"
|
||||
|
||||
def test_no_hermes_home_uses_windows_path_when_localappdata_missing(self, tmp_path, monkeypatch):
|
||||
"""Windows fallback still uses AppData/Local/hermes without LOCALAPPDATA."""
|
||||
home = tmp_path / "Home"
|
||||
monkeypatch.delenv("HERMES_HOME", raising=False)
|
||||
monkeypatch.delenv("LOCALAPPDATA", raising=False)
|
||||
monkeypatch.setattr(Path, "home", lambda: home)
|
||||
monkeypatch.setattr(hermes_constants.sys, "platform", "win32")
|
||||
|
||||
assert get_default_hermes_root() == home / "AppData" / "Local" / "hermes"
|
||||
|
||||
|
||||
class TestGetHermesHome:
|
||||
"""Tests for get_hermes_home() platform-aware fallback."""
|
||||
|
||||
def test_windows_fallback_uses_localappdata(self, tmp_path, monkeypatch):
|
||||
"""When HERMES_HOME is unset on Windows, use %LOCALAPPDATA%\\hermes."""
|
||||
local_appdata = tmp_path / "LocalAppData"
|
||||
monkeypatch.delenv("HERMES_HOME", raising=False)
|
||||
monkeypatch.setenv("LOCALAPPDATA", str(local_appdata))
|
||||
monkeypatch.setattr(Path, "home", lambda: tmp_path / "Home")
|
||||
monkeypatch.setattr(hermes_constants.sys, "platform", "win32")
|
||||
monkeypatch.setattr(hermes_constants, "_profile_fallback_warned", False)
|
||||
|
||||
assert get_hermes_home() == local_appdata / "hermes"
|
||||
|
||||
|
||||
class TestIsContainer:
|
||||
"""Tests for is_container() — Docker/Podman detection."""
|
||||
@ -262,4 +298,3 @@ class TestSecureParentDir:
|
||||
assert len(called_with) == 1
|
||||
assert called_with[0] == (str(real_dir), 0o700)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user