mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +00:00
fix(gateway): fall back to default home when routed profile does not exist
_resolve_profile_home_for_source resolved source.profile via get_profile_dir() without checking the profile directory exists. get_profile_dir() returns a path regardless of existence and does NOT raise for a valid-but-absent name, so a routed profile that names a deleted profile (a stale relay binding or a /p/<profile>/ URL prefix pointing at a since-removed profile) silently scoped the whole turn into <root>/profiles/<ghost> — a nonexistent HERMES_HOME. Config, skills, SOUL, and the fail-closed secret scope all then resolve against a missing directory instead of cleanly falling back. Add an explicit profile_exists() check on the ROUTED name only: when the routed profile has no directory, log a warning and fall back to the active/default home (the same clean fallback an empty routed profile already gets). The active/ default fallback name is trusted and not existence-checked. This is a latent bug independent of any one caller — it affects the existing /p/<profile>/ URL prefix today, and hardens the path ahead of per-scope profile routing (Team Gateway) populating source.profile from stored state that can drift from the agent's live profile list. Tests: existing routed profile → its dir; missing routed → active/default (NOT profiles/ghost); empty/None/whitespace → active/default; never raises. Proven fail-without-fix (reverting the check fails exactly the two missing-profile cases).
This commit is contained in:
parent
4d7f8ade3e
commit
a212b37eff
2 changed files with 76 additions and 2 deletions
|
|
@ -16658,10 +16658,32 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew
|
|||
Prefers the profile the source was routed to (``source.profile`` — set
|
||||
by the /p/<profile>/ URL prefix or a per-credential adapter), falling
|
||||
back to the active profile (the multiplexer's own home).
|
||||
|
||||
A routed profile whose directory does not exist (e.g. a relay binding
|
||||
or URL prefix that names a profile the operator has since deleted) must
|
||||
NOT scope the turn into a missing HERMES_HOME — config, skills, and the
|
||||
secret scope would all resolve against a nonexistent dir. In that case
|
||||
we log and fall back to the active/default home, the same clean fallback
|
||||
an empty routed profile already gets. ``get_profile_dir`` returns a path
|
||||
regardless of existence and does not raise for a valid-but-absent name,
|
||||
so an explicit ``profile_exists`` check is required here.
|
||||
"""
|
||||
from hermes_cli.profiles import get_active_profile_name, get_profile_dir
|
||||
from hermes_cli.profiles import (
|
||||
get_active_profile_name,
|
||||
get_profile_dir,
|
||||
profile_exists,
|
||||
)
|
||||
try:
|
||||
name = (source.profile or "").strip() or get_active_profile_name() or "default"
|
||||
routed = (source.profile or "").strip()
|
||||
if routed and not profile_exists(routed):
|
||||
logger.warning(
|
||||
"Routed profile %r has no profile directory; falling back "
|
||||
"to the active/default home. (Stale relay/URL routing to a "
|
||||
"deleted profile?)",
|
||||
routed,
|
||||
)
|
||||
routed = ""
|
||||
name = routed or get_active_profile_name() or "default"
|
||||
return get_profile_dir(name)
|
||||
except Exception:
|
||||
from hermes_constants import get_hermes_home
|
||||
|
|
|
|||
|
|
@ -156,3 +156,55 @@ class TestProfilePathResolutionUnderMultiplexScope:
|
|||
t.join()
|
||||
|
||||
assert seen["home"] == str(prof_b)
|
||||
|
||||
|
||||
class TestResolveProfileHomeMissingProfileFallback:
|
||||
"""_resolve_profile_home_for_source must not scope a turn into a missing home.
|
||||
|
||||
A routed source.profile that names a profile whose directory has been
|
||||
deleted (stale relay binding / URL prefix) must fall back to the active/
|
||||
default home rather than returning <root>/profiles/<ghost> — a nonexistent
|
||||
HERMES_HOME that would make config/skills/secret-scope resolve against a
|
||||
missing dir. get_profile_dir returns a path regardless of existence and does
|
||||
NOT raise for a valid-but-absent name, so the resolver needs an explicit
|
||||
profile_exists() check.
|
||||
"""
|
||||
|
||||
def _runner(self):
|
||||
from gateway.run import GatewayRunner
|
||||
return GatewayRunner.__new__(GatewayRunner)
|
||||
|
||||
def _source(self, profile):
|
||||
from types import SimpleNamespace
|
||||
return SimpleNamespace(profile=profile)
|
||||
|
||||
def test_existing_routed_profile_resolves_to_its_dir(self, tmp_path, monkeypatch):
|
||||
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
||||
(tmp_path / "profiles" / "coder").mkdir(parents=True)
|
||||
runner = self._runner()
|
||||
home = runner._resolve_profile_home_for_source(self._source("coder"))
|
||||
assert home == tmp_path / "profiles" / "coder"
|
||||
|
||||
def test_missing_routed_profile_falls_back_to_default(self, tmp_path, monkeypatch):
|
||||
# HERMES_HOME is the default (pre-profile) home; no profiles/ghost dir.
|
||||
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
||||
runner = self._runner()
|
||||
home = runner._resolve_profile_home_for_source(self._source("ghost"))
|
||||
# Falls back to the active/default home, NOT tmp_path/profiles/ghost.
|
||||
assert home != tmp_path / "profiles" / "ghost"
|
||||
assert home == tmp_path
|
||||
|
||||
def test_empty_routed_profile_uses_active_default(self, tmp_path, monkeypatch):
|
||||
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
||||
runner = self._runner()
|
||||
for empty in (None, "", " "):
|
||||
home = runner._resolve_profile_home_for_source(self._source(empty))
|
||||
assert home == tmp_path
|
||||
|
||||
def test_missing_profile_does_not_raise(self, tmp_path, monkeypatch):
|
||||
# The whole resolver is wrapped in try/except → get_hermes_home; the
|
||||
# fallback must land on a real dir, never propagate an exception.
|
||||
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
||||
runner = self._runner()
|
||||
home = runner._resolve_profile_home_for_source(self._source("nope"))
|
||||
assert home.exists()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue