From 369d5b07e11f00dd3dd8680ce471aaa4fca44cd4 Mon Sep 17 00:00:00 2001 From: teknium1 <127238744+teknium1@users.noreply.github.com> Date: Fri, 18 Sep 2026 04:00:56 -0700 Subject: [PATCH] fix(dashboard): map the default root to profile name "default" in _scope_profile_name Launched from a named profile (HERMES_HOME=/profiles/), the default root is not the process home, so _scope_profile_name fell through to path.name and handed the root directory's basename (".hermes" or a custom root) to _config_profile_scope, which 404s "Profile '' does not exist" -- or, if that basename happens to match a real profile, scopes and writes the wrong one. PUT /api/profiles/default/model and the TUI gateway's _pin_profile_model both reach this helper. Return "default" when the path resolves to get_default_hermes_root() before the path.name fallback; _config_profile_scope("default") already resolves to the root and builds its secret scope. The second invariant test is reshaped to the named-profile-launch -> default-target case: 404 before, 200 after, with the ROOT's .env credential (not the launch profile's) reaching the validator and only the root config.yaml updated. --- hermes_cli/web_routers/profiles.py | 12 +++++-- .../test_web_profiles_model_scope.py | 31 ++++++++++++------- 2 files changed, 29 insertions(+), 14 deletions(-) diff --git a/hermes_cli/web_routers/profiles.py b/hermes_cli/web_routers/profiles.py index 145f07795f..caf831fa52 100644 --- a/hermes_cli/web_routers/profiles.py +++ b/hermes_cli/web_routers/profiles.py @@ -103,9 +103,17 @@ def _profile_setup_command(name: str) -> str: def _scope_profile_name(path: Path) -> Optional[str]: """Map a profile directory onto the query name ``_config_profile_scope`` expects: None for - the process home (current-profile semantics: launch secret scope, no home override), the + the process home (current-profile semantics: launch secret scope, no home override), + ``"default"`` for the default root (its basename -- ``.hermes`` or a custom root -- is not a + profile name; launched from ``profiles/`` the root is a *different* profile), the directory name for ``profiles/``.""" - return None if path.resolve() == get_process_hermes_home().resolve() else path.name + from hermes_constants import get_default_hermes_root + resolved = path.resolve() + if resolved == get_process_hermes_home().resolve(): + return None + if resolved == get_default_hermes_root().resolve(): + return "default" + return path.name def _write_profile_model(profile_dir: Path, provider: str, model: str, validate_in: Optional[Path] = None) -> None: diff --git a/tests/hermes_cli/test_web_profiles_model_scope.py b/tests/hermes_cli/test_web_profiles_model_scope.py index ca06591192..fe10987ba1 100644 --- a/tests/hermes_cli/test_web_profiles_model_scope.py +++ b/tests/hermes_cli/test_web_profiles_model_scope.py @@ -89,19 +89,26 @@ def test_model_pick_resolves_key_env_from_profile_scope(client, homes, probe): assert (load_config().get("model") or {}).get("default") == "acme/mini" -def test_model_pick_for_process_home_uses_launch_scope(client, homes, probe): - """The dashboard's own profile maps to None (current-profile semantics) and still - validates through the launch scope once multiplexing is active.""" - secret_scope.set_multiplex_active(True) - try: +def test_model_pick_for_default_from_named_profile_launch(homes, probe, monkeypatch): + """Dashboard launched from ``profiles/demo``: targeting ``default`` must scope the ROOT + (name ``default``), not the root directory's basename, which is not a profile name.""" + root, demo = homes + monkeypatch.setenv("HERMES_HOME", str(demo)) + (root / ".env").write_text("ACME_RELAY_KEY=root-key\n", encoding="utf-8") + from hermes_cli.config import invalidate_env_cache, load_config + from hermes_cli.web_server_profiles import _hermes_home_scope + invalidate_env_cache() + from hermes_cli import web_server + + with TestClient(web_server.app, raise_server_exceptions=False) as client: + client.headers["Authorization"] = f"Bearer {web_server._SESSION_TOKEN}" resp = client.put( - "/api/profiles/default/model", - json={"provider": "acme", "model": "acme/mini"}, + "/api/profiles/default/model", json={"provider": "acme", "model": "acme/mini"} ) - finally: - secret_scope.set_multiplex_active(False) assert resp.status_code == 200, resp.text - # Launch scope: live process env while single-profile... frozen at activation once - # multiplexed — the dashboard home's value, resolved through get_secret, not a raise. - assert probe["api_key"] == "dashboard-home-key" + assert probe["api_key"] == "root-key" # the root's .env, not the launch profile's + with _hermes_home_scope(root): + assert (load_config().get("model") or {}).get("default") == "acme/mini" + with _hermes_home_scope(demo): + assert (load_config().get("model") or {}).get("default") is None