Merge pull request #2903 from nesquena/release/stage-batch16

Release DF / v0.51.134 — stage-batch16 (Windows path defaults align with agent)
This commit is contained in:
nesquena-hermes
2026-05-24 19:07:57 -07:00
committed by GitHub
7 changed files with 61 additions and 17 deletions
+6
View File
@@ -3,6 +3,12 @@
## [Unreleased]
## [v0.51.134] — 2026-05-25 — Release DF (stage-batch16 — single-PR Windows path defaults)
### Fixed
- **PR #2897** by @chouzz — On Windows, WebUI default state and config paths now align with Hermes Agent's `%LOCALAPPDATA%\hermes` convention instead of `%USERPROFILE%\.hermes`, so a fresh Windows install finds the same `~/.hermes/config.yaml` / `auth.json` / `webui/` state directory that the Hermes Agent created. POSIX behavior is unchanged (`~/.hermes` remains the default). `HERMES_HOME` and `HERMES_WEBUI_STATE_DIR` env overrides take precedence on both platforms. Closes #2840.
## [v0.51.133] — 2026-05-25 — Release DE (stage-batch15 — 6-PR contributor batch — aux-task validation + workspace artifact gating + update apply guard + Joplin auth header + prefill cache guard + notes drawer i18n)
### Fixed
+5 -5
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 |
---
+21 -6
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,10 @@ 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()
STATE_DIR = (
Path(os.getenv("HERMES_WEBUI_STATE_DIR", str(HOME / ".hermes" / "webui")))
Path(os.getenv("HERMES_WEBUI_STATE_DIR", str(_DEFAULT_HERMES_HOME / "webui")))
.expanduser()
.resolve()
)
@@ -108,7 +123,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 +134,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 +282,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 +2383,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 +3115,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:
+4
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()
+2 -2
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.
+8 -4
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 =================================================
@@ -0,0 +1,15 @@
from pathlib import Path
import api.config as config
import api.profiles as profiles
def test_profiles_unwrap_profile_home_to_base():
base = Path('/tmp/hermes-base')
profile_home = base / 'profiles' / 'webui'
assert profiles._unwrap_profile_home_to_base(profile_home) == base
def test_default_hermes_home_returns_path_object():
home = config._platform_default_hermes_home()
assert isinstance(home, Path)