fix: profile paths broken in Docker — profiles go to /root/.hermes instead of mounted volume (#7170)

In Docker, HERMES_HOME=/opt/data (set in Dockerfile) and users mount
their .hermes directory to /opt/data. However, profile operations used
Path.home() / '.hermes' which resolves to /root/.hermes in Docker —
an ephemeral container path, not the mounted volume.

This caused:
- Profiles created at /root/.hermes/profiles/ (lost on container recreate)
- active_profile sticky file written to wrong location
- profile list looking at wrong directory

Fix: Add get_default_hermes_root() to hermes_constants.py that detects
Docker/custom deployments (HERMES_HOME outside ~/.hermes) and returns
HERMES_HOME as the root. Also handles Docker profiles correctly
(<root>/profiles/<name> → root is grandparent).

Files changed:
- hermes_constants.py: new get_default_hermes_root()
- hermes_cli/profiles.py: _get_default_hermes_home() delegates to shared fn
- hermes_cli/main.py: _apply_profile_override() + _invalidate_update_cache()
- hermes_cli/gateway.py: _profile_suffix() + _profile_arg()
- Tests: 12 new tests covering Docker scenarios
This commit is contained in:
Teknium 2026-04-10 05:53:10 -07:00 committed by GitHub
parent 916fbf362c
commit 4a65c9cd08
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 218 additions and 24 deletions

View file

@ -293,12 +293,16 @@ class TestGetActiveProfileName:
monkeypatch.setenv("HERMES_HOME", str(profile_dir))
assert get_active_profile_name() == "coder"
def test_custom_path_returns_custom(self, profile_env, monkeypatch):
def test_custom_path_returns_default(self, profile_env, monkeypatch):
"""A custom HERMES_HOME (Docker, etc.) IS the default root."""
tmp_path = profile_env
custom = tmp_path / "some" / "other" / "path"
custom.mkdir(parents=True)
monkeypatch.setenv("HERMES_HOME", str(custom))
assert get_active_profile_name() == "custom"
# With Docker-aware roots, a custom HERMES_HOME is the default —
# not "custom". The user is on the default profile of their
# custom deployment.
assert get_active_profile_name() == "default"
# ===================================================================
@ -706,6 +710,72 @@ class TestInternalHelpers:
home = _get_default_hermes_home()
assert home == tmp_path / ".hermes"
def test_profiles_root_docker_deployment(self, tmp_path, monkeypatch):
"""In Docker (HERMES_HOME outside ~/.hermes), profiles go under HERMES_HOME."""
docker_home = tmp_path / "opt" / "data"
docker_home.mkdir(parents=True)
monkeypatch.setattr(Path, "home", lambda: tmp_path)
monkeypatch.setenv("HERMES_HOME", str(docker_home))
root = _get_profiles_root()
assert root == docker_home / "profiles"
def test_default_hermes_home_docker(self, tmp_path, monkeypatch):
"""In Docker, _get_default_hermes_home() returns HERMES_HOME itself."""
docker_home = tmp_path / "opt" / "data"
docker_home.mkdir(parents=True)
monkeypatch.setattr(Path, "home", lambda: tmp_path)
monkeypatch.setenv("HERMES_HOME", str(docker_home))
home = _get_default_hermes_home()
assert home == docker_home
def test_profiles_root_profile_mode(self, tmp_path, monkeypatch):
"""In profile mode (HERMES_HOME under ~/.hermes), profiles root is still ~/.hermes/profiles."""
native = tmp_path / ".hermes"
profile_dir = native / "profiles" / "coder"
profile_dir.mkdir(parents=True)
monkeypatch.setattr(Path, "home", lambda: tmp_path)
monkeypatch.setenv("HERMES_HOME", str(profile_dir))
root = _get_profiles_root()
assert root == native / "profiles"
def test_active_profile_path_docker(self, tmp_path, monkeypatch):
"""In Docker, active_profile file lives under HERMES_HOME."""
from hermes_cli.profiles import _get_active_profile_path
docker_home = tmp_path / "opt" / "data"
docker_home.mkdir(parents=True)
monkeypatch.setattr(Path, "home", lambda: tmp_path)
monkeypatch.setenv("HERMES_HOME", str(docker_home))
path = _get_active_profile_path()
assert path == docker_home / "active_profile"
def test_create_profile_docker(self, tmp_path, monkeypatch):
"""Profile created in Docker lands under HERMES_HOME/profiles/."""
docker_home = tmp_path / "opt" / "data"
docker_home.mkdir(parents=True)
monkeypatch.setattr(Path, "home", lambda: tmp_path)
monkeypatch.setenv("HERMES_HOME", str(docker_home))
result = create_profile("orchestrator", no_alias=True)
expected = docker_home / "profiles" / "orchestrator"
assert result == expected
assert expected.is_dir()
def test_active_profile_name_docker_default(self, tmp_path, monkeypatch):
"""In Docker (no profile active), get_active_profile_name() returns 'default'."""
docker_home = tmp_path / "opt" / "data"
docker_home.mkdir(parents=True)
monkeypatch.setattr(Path, "home", lambda: tmp_path)
monkeypatch.setenv("HERMES_HOME", str(docker_home))
assert get_active_profile_name() == "default"
def test_active_profile_name_docker_profile(self, tmp_path, monkeypatch):
"""In Docker with a profile active, get_active_profile_name() returns the profile name."""
docker_home = tmp_path / "opt" / "data"
profile = docker_home / "profiles" / "orchestrator"
profile.mkdir(parents=True)
monkeypatch.setattr(Path, "home", lambda: tmp_path)
monkeypatch.setenv("HERMES_HOME", str(profile))
assert get_active_profile_name() == "orchestrator"
# ===================================================================
# Edge cases and additional coverage