Skip to content

fix: normalize named profile base homes#1689

Merged
1 commit merged into
nesquena:masterfrom
Michaelyklam:fix/issue-749-base-home-profile
May 5, 2026
Merged

fix: normalize named profile base homes#1689
1 commit merged into
nesquena:masterfrom
Michaelyklam:fix/issue-749-base-home-profile

Conversation

@Michaelyklam
Copy link
Copy Markdown
Contributor

Thinking Path

  • Hermes WebUI profiles need to resolve to the same Hermes home that Hermes Agent will use for config, runtime env, sessions, and model startup.
  • Issue design(profile): align Web UI profile management with Hermes runtime model #749 still had one concrete open repro: a per-profile deployment sets HERMES_BASE_HOME=/base/profiles/foo while the browser sends hermes_profile=foo.
  • The root cause was that _resolve_base_hermes_home() unwrapped HERMES_HOME=/base/profiles/foo but trusted HERMES_BASE_HOME as already being the base, so later helpers appended profiles/foo again.
  • This PR normalizes both env-var paths through the same base-home helper, then routes active-profile and explicit per-request lookups through one shared profile-home resolver.
  • The result is a focused fix for the doubled profiles/foo/profiles/foo path without redesigning the broader profile UX umbrella.

What Changed

  • Added _unwrap_profile_home_to_base() so HERMES_BASE_HOME=/base/profiles/<name> is normalized to /base, matching the existing HERMES_HOME profile-subdir behavior.
  • Added _resolve_profile_home_for_name() and made both get_active_hermes_home() and get_hermes_home_for_profile() delegate to it.
  • Tightened active-profile resolution so valid named profiles resolve to their profile-scoped path even before the directory exists, matching the existing explicit per-request helper and avoiding silent fallback to the pinned home.
  • Added design(profile): align Web UI profile management with Hermes runtime model #749 regression coverage for:
    • matching profile cookie foo resolving to /base/profiles/foo, not /base/profiles/foo/profiles/foo, even when the doubled path exists;
    • api.models._get_profile_home('foo') and get_profile_runtime_env() reading the intended profile home/config;
    • non-matching profile bar resolving to the sibling /base/profiles/bar, not the pinned foo directory or foo/profiles/bar.

Why It Matters

A dedicated WebUI process per named profile is a useful isolation deployment shape. Before this fix, that setup could make chat/model startup read a nested empty profile directory and report misleading provider/config failures even though the intended profile was configured correctly. This keeps the runtime path model consistent across active profile, per-request profile, model/session startup, and profile runtime env loading.

Verification

/home/michael/.hermes/hermes-agent/venv/bin/python -m pytest tests/test_issue798.py::test_hermes_base_home_named_profile_matches_cookie_without_doubling tests/test_issue798.py::test_hermes_base_home_named_profile_nonmatching_cookie_uses_sibling_profile_path -q
/home/michael/.hermes/hermes-agent/venv/bin/python -m pytest tests/test_issue798.py tests/test_issue803.py tests/test_issue1612_renamed_root_profile.py tests/test_issue1611_session_profile_filtering.py tests/test_profile_terminal_env.py tests/test_model_resolver.py tests/test_session_import_cli_fallback_model.py -q
git diff --check
HERMES_WEBUI_HOST=127.0.0.1 env -u HERMES_CONFIG_PATH /home/michael/.hermes/hermes-agent/venv/bin/python -m pytest tests/test_dashboard_probe.py::test_status_tries_default_loopback_targets_until_dashboard_found tests/test_issue798.py tests/test_sprint1.py::test_health -q
HERMES_WEBUI_HOST=127.0.0.1 env -u HERMES_CONFIG_PATH /home/michael/.hermes/hermes-agent/venv/bin/python -m pytest tests/test_gateway_sync.py::test_importing_older_gateway_session_preserves_original_timestamps_and_order tests/test_onboarding_mvp.py::test_onboarding_setup_rejects_api_key_with_newline tests/test_sprint14.py::test_file_rename tests/test_sprint14.py::test_create_dir tests/test_sprint5.py::test_workspace_suggest_hidden_dirs_only_when_requested -q

Result:

2 passed in 1.79s
74 passed in 16.56s
git diff --check: passed
14 passed in 1.46s
5 passed in 6.47s

Full-suite note:

Attempted full isolated suite with HERMES_WEBUI_HOST=127.0.0.1 and env -u HERMES_CONFIG_PATH:
5 failed, 4474 passed, 2 skipped, 3 xpassed, 1 warning, 8 subtests passed in 432.55s.
The 5 failed tests were order/environment-sensitive existing tests unrelated to this profile-path diff; rerunning those exact 5 tests in isolation passed (listed above).

UI media:

  • Not applicable; backend/profile path resolution only.

Risks / Follow-ups

Model Used

AI assisted.

  • Provider: OpenAI Codex
  • Model: gpt-5.5
  • Notable tool use: Hermes Kanban worker, terminal/git/gh, pytest, source inspection

@nesquena-hermes
Copy link
Copy Markdown
Collaborator

Review

Reading the diff at f6a532d against origin/master, this is the targeted fix for the last open reproduction tied to #749 — the HERMES_BASE_HOME=<profile-home> doubled-path scenario @nesquena-hermes called out in the May 4 comment on that umbrella. Worth merging.

What the PR does

api/profiles.py introduces _unwrap_profile_home_to_base() and routes both the HERMES_BASE_HOME and HERMES_HOME branches through it:

def _unwrap_profile_home_to_base(home: Path) -> Path:
    """Return the base Hermes home when *home* is already a named profile dir."""
    if home.parent.name == 'profiles':
        return home.parent.parent
    return home

Before this PR, _resolve_base_hermes_home() only unwrapped on the HERMES_HOME branch:

# (origin/master)
base_override = os.getenv('HERMES_BASE_HOME', '').strip()
if base_override:
    return Path(base_override).expanduser()    # ← no unwrap, doubled when set to profile-home

…which is exactly the gap @nesquena-hermes flagged: "the unwrap logic only runs on the HERMES_HOME branch, and _resolve_base_hermes_home() trusts HERMES_BASE_HOME as-is by design." With this PR, both env vars route through one helper.

The PR also collapses get_active_hermes_home() and get_hermes_home_for_profile() into a single _resolve_profile_home_for_name() helper so the "active profile" path and the explicit "by name" path can no longer drift apart:

def _resolve_profile_home_for_name(name: str) -> Path:
    if not name or _is_root_profile(name):
        return _DEFAULT_HERMES_HOME
    if not _PROFILE_ID_RE.fullmatch(name):
        return _DEFAULT_HERMES_HOME
    return _resolve_named_profile_home(name)

This matches @nesquena-hermes's prescription almost exactly: "a shared _base_is_named_profile_home() helper called from both get_active_hermes_home() and get_hermes_home_for_profile() so per-request resolution in streaming.py/models.py doesn't resurrect the doubling under load." ✅

One small drift note: get_active_hermes_home() on master had an is_dir() guard that returned _DEFAULT_HERMES_HOME when the profile dir didn't exist on disk. The new code drops that guard (it just returns the named-profile path unconditionally). That's intentional — it matches get_hermes_home_for_profile()'s post-#1373/#1195 behaviour ("agent layer creates on first use") — and the existing test test_get_hermes_home_for_profile_returns_profile_path_for_missing_profile already encodes this contract. Worth calling out in the merge note since it's a behaviour change for the active-profile path, not just the explicit-name path.

Tests

tests/test_issue798.py adds two regression tests using a subprocess probe so the env vars are evaluated at import time (which is the actual code path):

  • test_hermes_base_home_named_profile_matches_cookie_without_doubling — sets HERMES_BASE_HOME=/base/profiles/foo + HERMES_HOME=/base/profiles/foo, writes intentionally-different config.yaml files at the correct path and the doubled path, and asserts the runtime reads the correct one (via terminal.cwd). Brilliant test design — without the config-file divergence, an is_dir() short-circuit could pass the assertion incorrectly.
  • test_hermes_base_home_named_profile_nonmatching_cookie_uses_sibling_profile_path — sets HERMES_BASE_HOME=/base/profiles/foo but asks for profile bar, asserts bar resolves to /base/profiles/bar (the sibling), not /base/profiles/foo/profiles/bar and not /base/profiles/foo.

The second test is the security-critical one — without the unwrap on HERMES_BASE_HOME, asking for bar would have either fallen back to foo (silent profile cross-talk) or doubled the path. Good coverage.

Verdict

LGTM. The fix is exactly what the umbrella issue asked for, the helper unification is the right shape, and the two regression tests target the precise reproductions the maintainer identified. The dropped is_dir() guard in get_active_hermes_home() is the only behaviour-change worth flagging in the merge commit; otherwise this is a pure correctness fix on top of #1373/#1634/#1611.

@nesquena-hermes nesquena-hermes closed this pull request by merging all changes into nesquena:master in 4daa238 May 5, 2026
Michaelyklam pushed a commit to Michaelyklam/hermes-webui that referenced this pull request May 5, 2026
Michaelyklam added a commit to Michaelyklam/hermes-webui that referenced this pull request May 5, 2026
10 PRs (3 surfaces additions, 7 fixes):
- nesquena#1644 model picker chip + group count (@bergeouss, closes nesquena#1425)
- nesquena#1684 update network failures UX (@Michaelyklam, closes nesquena#1321)
- nesquena#1685 Codex spark models (@Michaelyklam, closes nesquena#1680)
- nesquena#1689 normalize profile base homes (@Michaelyklam, refs nesquena#749)
- nesquena#1693 adaptive title refresh deadlock (@ai-ag2026)
- nesquena#1701 normalize update banner URL (@Michaelyklam, closes nesquena#1691)
- nesquena#1702 workspace double-click rename (@Michaelyklam, closes nesquena#1698)
- nesquena#1703 cache invalidation on auth-store drift (@Michaelyklam, closes nesquena#1699)
- nesquena#1704 markdown fence lengths (@Michaelyklam, closes nesquena#1696)
- nesquena#1706 multi-image paste fix (@Michaelyklam, closes nesquena#1697)

Tests: 4477 → 4503 (+26). Opus: SHIP, 7/7 verification clean.

Co-authored-by: Michael Lam <Michaelyklam1@gmail.com>
Co-authored-by: ai-ag2026 <noreply@github.com>
Co-authored-by: bergeouss <noreply@github.com>
@nesquena-hermes
Copy link
Copy Markdown
Collaborator

Closed by the v0.51.4 release in PR #1707 (merged at 4daa238, deployed to production).

Live on production: https://github.com/nesquena/hermes-webui/releases/tag/v0.51.4

🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants