diff --git a/hermes_cli/plugins.py b/hermes_cli/plugins.py index 0e639fe60f..fb894d4079 100644 --- a/hermes_cli/plugins.py +++ b/hermes_cli/plugins.py @@ -11,6 +11,7 @@ and an ``__init__.py`` exposing ``register(ctx)``. Plugins register callbacks fo from __future__ import annotations import asyncio +import contextvars import importlib.metadata import inspect import json @@ -2013,7 +2014,10 @@ def resolve_plugin_command_result(result: Any) -> Any: finally: done.set() - threading.Thread(target=_runner, name="hermes-plugin-command-await", daemon=True).start() + # copy_context: the helper thread must see the caller's profile/secret scope, else an + # async hook under a running loop reads the default HERMES_HOME and get_secret raises. + threading.Thread(target=contextvars.copy_context().run, args=(_runner,), + name="hermes-plugin-command-await", daemon=True).start() if not done.wait(timeout=_PLUGIN_COMMAND_AWAIT_TIMEOUT_SECS): raise TimeoutError("Plugin command async handler did not complete within " f"{_PLUGIN_COMMAND_AWAIT_TIMEOUT_SECS:.0f}s") diff --git a/tests/hermes_cli/test_plugins.py b/tests/hermes_cli/test_plugins.py index 22340a753c..02924cdb9e 100644 --- a/tests/hermes_cli/test_plugins.py +++ b/tests/hermes_cli/test_plugins.py @@ -948,20 +948,24 @@ class TestAsyncHookCallbacks: assert results == [{"context": "sync"}, {"context": "async:s1"}] def test_async_hook_resolves_under_a_running_loop(self): - """Gateway handlers call invoke_hook from inside asyncio; a bare asyncio.run would raise.""" + """Gateway handlers call invoke_hook from inside asyncio; a bare asyncio.run would raise. + The helper thread must also carry the caller's ContextVars (profile / secret scope).""" import asyncio + import contextvars + scope = contextvars.ContextVar("hook_scope", default="default") mgr = PluginManager() async def async_hook(**kwargs): - return "from-async" + return f"from-async:{scope.get()}" mgr._hooks.setdefault("post_tool_call", []).append(async_hook) async def driver(): + scope.set("profile-b") return mgr.invoke_hook("post_tool_call", tool_name="t", args={}, result="r", duration_ms=1) - assert asyncio.run(driver()) == ["from-async"] + assert asyncio.run(driver()) == ["from-async:profile-b"] class TestForceReloadSymmetry: