feat(plugins): add ctx.profile_name for session-agnostic profile access (#50346)

Plugins previously had no way to read the active profile name from the
PluginContext. The workaround in the wild — reaching into
ctx._manager._cli_ref — only works in an interactive CLI session;
_cli_ref is None in the gateway and in kanban-spawned worker sessions
(hermes -p <profile> chat -q ...), so the workaround breaks exactly
where multi-profile awareness matters most.

ctx.profile_name wraps hermes_cli.profiles.get_active_profile_name(),
which derives the name from HERMES_HOME and therefore works in every
execution context with zero dependency on _cli_ref.
This commit is contained in:
Teknium 2026-06-21 12:38:11 -07:00 committed by GitHub
parent 7d9f6a24f5
commit 9d883ac90e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 57 additions and 0 deletions

View file

@ -315,6 +315,28 @@ class PluginContext:
self._llm = PluginLlm(plugin_id=plugin_id)
return self._llm
# -- profile awareness --------------------------------------------------
@property
def profile_name(self) -> str:
"""Return the active Hermes profile name (e.g. ``"default"``).
Derived from ``HERMES_HOME`` via
:func:`hermes_cli.profiles.get_active_profile_name`, so it works in
every execution context interactive CLI, gateway, and
kanban-spawned worker sessions alike without depending on
``_cli_ref`` (which is ``None`` outside an interactive CLI run).
Returns ``"default"`` for the default profile, the profile id when
running under ``~/.hermes/profiles/<name>``, or ``"custom"`` when
``HERMES_HOME`` points somewhere unrecognized.
"""
try:
from hermes_cli.profiles import get_active_profile_name
return get_active_profile_name()
except Exception:
return "default"
# -- tool registration --------------------------------------------------
def register_tool(

View file

@ -1867,3 +1867,38 @@ class TestPluginDebugLogging:
plugins_mod._PLUGINS_DEBUG = original_debug
plugins_mod.logger.setLevel(original_level)
plugins_mod.logger.handlers = original_handlers
class TestPluginContextProfileName:
"""ctx.profile_name resolves from HERMES_HOME in every context."""
def _ctx(self):
mgr = PluginManager()
manifest = PluginManifest(name="test-plugin", source="user")
return PluginContext(manifest, mgr)
def test_default_profile(self, tmp_path, monkeypatch):
"""HERMES_HOME at the root resolves to 'default'."""
home = tmp_path / ".hermes"
home.mkdir()
monkeypatch.setattr(Path, "home", lambda: tmp_path)
monkeypatch.setenv("HERMES_HOME", str(home))
assert self._ctx().profile_name == "default"
def test_named_profile(self, tmp_path, monkeypatch):
"""HERMES_HOME under profiles/<name> resolves to that name."""
prof = tmp_path / ".hermes" / "profiles" / "coder"
prof.mkdir(parents=True)
monkeypatch.setattr(Path, "home", lambda: tmp_path)
monkeypatch.setenv("HERMES_HOME", str(prof))
assert self._ctx().profile_name == "coder"
def test_works_without_cli_ref(self, tmp_path, monkeypatch):
"""profile_name does not depend on _cli_ref (None in worker sessions)."""
prof = tmp_path / ".hermes" / "profiles" / "worker1"
prof.mkdir(parents=True)
monkeypatch.setattr(Path, "home", lambda: tmp_path)
monkeypatch.setenv("HERMES_HOME", str(prof))
ctx = self._ctx()
assert ctx._manager._cli_ref is None
assert ctx.profile_name == "worker1"