fix(dashboard): map the default root to profile name "default" in _scope_profile_name

Launched from a named profile (HERMES_HOME=<root>/profiles/<name>), 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 '<basename>' 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.
This commit is contained in:
teknium1
2026-09-18 04:00:56 -07:00
committed by Teknium
parent e8f8970bbe
commit 369d5b07e1
2 changed files with 29 additions and 14 deletions

View File

@@ -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/<name>`` the root is a *different* profile), the
directory name for ``profiles/<name>``."""
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:

View File

@@ -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