fix(windows): align WebUI defaults with Hermes Agent home path

This commit is contained in:
Harlan Zhou
2026-05-25 00:41:26 +00:00
committed by nesquena-hermes
parent 75e0e67af2
commit f8a7726e09
6 changed files with 69 additions and 17 deletions

View File

@@ -241,9 +241,9 @@ For the deep dive on each of these, see [`docs/docker.md`](docs/docker.md).
| Thing | How it finds it |
|---|---|
| Hermes agent dir | `HERMES_WEBUI_AGENT_DIR` env, then `~/.hermes/hermes-agent`, then sibling `../hermes-agent` |
| Hermes agent dir | `HERMES_WEBUI_AGENT_DIR` env, then `$HERMES_HOME/hermes-agent` (Windows default `%LOCALAPPDATA%\hermes\hermes-agent`, POSIX default `~/.hermes/hermes-agent`), then sibling `../hermes-agent` |
| Python executable | Agent venv first, then `.venv` in this repo, then system `python3` |
| State directory | `HERMES_WEBUI_STATE_DIR` env, then `~/.hermes/webui` |
| State directory | `HERMES_WEBUI_STATE_DIR` env, then `$HERMES_HOME/webui` (Windows default `%LOCALAPPDATA%\hermes\webui`, POSIX default `~/.hermes/webui`) |
| Default workspace | `HERMES_WEBUI_DEFAULT_WORKSPACE` env, then `~/workspace`, then state dir |
| Port | `HERMES_WEBUI_PORT` env or first argument, default `8787` |
@@ -275,15 +275,15 @@ Full list of environment variables:
| `HERMES_WEBUI_PYTHON` | auto-discovered | Python executable |
| `HERMES_WEBUI_HOST` | `127.0.0.1` | Bind address (`0.0.0.0` for all IPv4, `::` for all IPv6, `::1` for IPv6 loopback) |
| `HERMES_WEBUI_PORT` | `8787` | Port |
| `HERMES_WEBUI_STATE_DIR` | `~/.hermes/webui` | Where sessions and state are stored |
| `HERMES_WEBUI_STATE_DIR` | `$HERMES_HOME/webui` (Windows default `%LOCALAPPDATA%\hermes\webui`, POSIX default `~/.hermes/webui`) | Where sessions and state are stored |
| `HERMES_WEBUI_DEFAULT_WORKSPACE` | `~/workspace` | Default workspace |
| `HERMES_WEBUI_DEFAULT_MODEL` | *(provider default)* | Optional model override; leave unset to use the active Hermes provider default |
| `HERMES_WEBUI_PASSWORD` | *(unset)* | Set to enable password authentication |
| `HERMES_WEBUI_EXTENSION_DIR` | *(unset)* | Optional local directory served at `/extensions/`; must point to an existing directory before extension injection is enabled |
| `HERMES_WEBUI_EXTENSION_SCRIPT_URLS` | *(unset)* | Optional comma-separated same-origin script URLs to inject; see [WebUI Extensions](docs/EXTENSIONS.md) |
| `HERMES_WEBUI_EXTENSION_STYLESHEET_URLS` | *(unset)* | Optional comma-separated same-origin stylesheet URLs to inject; see [WebUI Extensions](docs/EXTENSIONS.md) |
| `HERMES_HOME` | `~/.hermes` | Base directory for Hermes state (affects all paths) |
| `HERMES_CONFIG_PATH` | `~/.hermes/config.yaml` | Path to Hermes config file |
| `HERMES_HOME` | Windows: `%LOCALAPPDATA%\hermes`; POSIX: `~/.hermes` | Base directory for Hermes state (affects all paths) |
| `HERMES_CONFIG_PATH` | `$HERMES_HOME/config.yaml` | Path to Hermes config file |
---

View File

@@ -30,6 +30,19 @@ HOME = Path.home()
# REPO_ROOT is the directory that contains this file's parent (api/ -> repo root)
REPO_ROOT = Path(__file__).parent.parent.resolve()
def _platform_default_hermes_home() -> Path:
"""Return the platform-aware default Hermes home when HERMES_HOME is unset.
Native Windows Hermes Agent installs default to %LOCALAPPDATA%\\hermes,
while POSIX installs use ~/.hermes.
"""
if os.name == "nt":
local_app_data = os.getenv("LOCALAPPDATA", "").strip()
if local_app_data:
return Path(local_app_data) / "hermes"
return HOME / ".hermes"
# ── Network config (env-overridable) ─────────────────────────────────────────
HOST = os.getenv("HERMES_WEBUI_HOST", "127.0.0.1")
PORT = int(os.getenv("HERMES_WEBUI_PORT", "8787"))
@@ -40,8 +53,11 @@ TLS_KEY = os.getenv("HERMES_WEBUI_TLS_KEY", "").strip() or None
TLS_ENABLED = TLS_CERT is not None and TLS_KEY is not None
# ── State directory (env-overridable, never inside repo) ──────────────────────
_DEFAULT_HERMES_HOME = _platform_default_hermes_home()
_EFFECTIVE_HERMES_HOME = Path(os.getenv("HERMES_HOME", str(_DEFAULT_HERMES_HOME))).expanduser()
STATE_DIR = (
Path(os.getenv("HERMES_WEBUI_STATE_DIR", str(HOME / ".hermes" / "webui")))
Path(os.getenv("HERMES_WEBUI_STATE_DIR", str(_EFFECTIVE_HERMES_HOME / "webui")))
.expanduser()
.resolve()
)
@@ -108,7 +124,7 @@ def _discover_agent_dir() -> Path:
)
# 2. HERMES_HOME / hermes-agent
hermes_home = os.getenv("HERMES_HOME", str(HOME / ".hermes"))
hermes_home = os.getenv("HERMES_HOME", str(_DEFAULT_HERMES_HOME))
candidates.append(Path(hermes_home).expanduser() / "hermes-agent")
# 3. Sibling: <repo-root>/../hermes-agent
@@ -119,7 +135,7 @@ def _discover_agent_dir() -> Path:
candidates.append(REPO_ROOT.parent)
# 5. ~/.hermes/hermes-agent (explicit common path)
candidates.append(HOME / ".hermes" / "hermes-agent")
candidates.append(_DEFAULT_HERMES_HOME / "hermes-agent")
# 6. ~/hermes-agent
candidates.append(HOME / "hermes-agent")
@@ -267,7 +283,7 @@ def _get_config_path() -> Path:
return get_active_hermes_home() / "config.yaml"
except ImportError:
return HOME / ".hermes" / "config.yaml"
return _DEFAULT_HERMES_HOME / "config.yaml"
_WEBUI_SESSION_SAVE_MODES = {"deferred", "eager"}
@@ -2368,7 +2384,7 @@ def _get_auth_store_path() -> Path:
return _gah() / "auth.json"
except ImportError:
return HOME / ".hermes" / "auth.json"
return _DEFAULT_HERMES_HOME / "auth.json"
def _models_cache_file_fingerprint(path: Path) -> dict:
@@ -3100,7 +3116,7 @@ def get_available_models() -> dict:
hermes_env_path = _gah2() / ".env"
except ImportError:
hermes_env_path = HOME / ".hermes" / ".env"
hermes_env_path = _DEFAULT_HERMES_HOME / ".env"
env_keys = {}
if hermes_env_path.exists():
try:

View File

@@ -150,6 +150,10 @@ def _resolve_base_hermes_home() -> Path:
# If HERMES_HOME points to a profiles/ subdir, walk up two levels to the base
return _unwrap_profile_home_to_base(p)
if os.name == 'nt':
local_app_data = os.getenv('LOCALAPPDATA', '').strip()
if local_app_data:
return Path(local_app_data) / 'hermes'
return Path.home() / '.hermes'
_DEFAULT_HERMES_HOME = _resolve_base_hermes_home()

View File

@@ -162,8 +162,8 @@ The wizard uses the same files and APIs as the normal app:
State normally lives outside the repository. By default:
- Hermes Agent state: `~/.hermes`
- WebUI state: `~/.hermes/webui`
- Hermes Agent state: Windows `%LOCALAPPDATA%\hermes`; POSIX `~/.hermes`
- WebUI state: `$HERMES_HOME/webui` (Windows default `%LOCALAPPDATA%\hermes\webui`, POSIX default `~/.hermes/webui`)
Override these with `HERMES_HOME` and `HERMES_WEBUI_STATE_DIR` when you need an
isolated test install.

View File

@@ -159,11 +159,15 @@ $PortFinal = if ($Port) {
}
$env:HERMES_WEBUI_HOST = $BindHostFinal
$env:HERMES_WEBUI_PORT = "$PortFinal"
if (-not $env:HERMES_WEBUI_STATE_DIR) {
$env:HERMES_WEBUI_STATE_DIR = Join-Path $env:USERPROFILE '.hermes\webui'
}
if (-not $env:HERMES_HOME) {
$env:HERMES_HOME = Join-Path $env:USERPROFILE '.hermes'
if ($env:LOCALAPPDATA) {
$env:HERMES_HOME = Join-Path $env:LOCALAPPDATA 'hermes'
} else {
$env:HERMES_HOME = Join-Path $env:USERPROFILE '.hermes'
}
}
if (-not $env:HERMES_WEBUI_STATE_DIR) {
$env:HERMES_WEBUI_STATE_DIR = Join-Path $env:HERMES_HOME 'webui'
}
# === Ensure dirs exist =================================================

View File

@@ -0,0 +1,28 @@
import importlib
import sys
from pathlib import Path
def _reload(module_name: str):
sys.modules.pop(module_name, None)
return importlib.import_module(module_name)
def test_config_state_dir_defaults_to_hermes_home_webui(monkeypatch, tmp_path):
hermes_home = tmp_path / "hermes-home"
monkeypatch.setenv("HERMES_HOME", str(hermes_home))
monkeypatch.delenv("HERMES_WEBUI_STATE_DIR", raising=False)
cfg = _reload("api.config")
assert cfg.STATE_DIR == hermes_home / "webui"
def test_profiles_resolve_base_home_unwraps_profiles_subdir(monkeypatch, tmp_path):
profile_home = tmp_path / "hermes" / "profiles" / "webui"
monkeypatch.delenv("HERMES_BASE_HOME", raising=False)
monkeypatch.setenv("HERMES_HOME", str(profile_home))
profiles = _reload("api.profiles")
assert profiles._resolve_base_hermes_home() == tmp_path / "hermes"