From 9d883ac90e3e0955b3fe5b7c6321dac6c14dd560 Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Sun, 21 Jun 2026 12:38:11 -0700 Subject: [PATCH] feat(plugins): add ctx.profile_name for session-agnostic profile access (#50346) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 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. --- hermes_cli/plugins.py | 22 ++++++++++++++++++++ tests/hermes_cli/test_plugins.py | 35 ++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/hermes_cli/plugins.py b/hermes_cli/plugins.py index 25bf83af302..b064725186f 100644 --- a/hermes_cli/plugins.py +++ b/hermes_cli/plugins.py @@ -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/``, 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( diff --git a/tests/hermes_cli/test_plugins.py b/tests/hermes_cli/test_plugins.py index effeaa0120f..16e5785c88f 100644 --- a/tests/hermes_cli/test_plugins.py +++ b/tests/hermes_cli/test_plugins.py @@ -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/ 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"