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