diff --git a/gateway/hooks.py b/gateway/hooks.py index f887cf5df04..5ab45119202 100644 --- a/gateway/hooks.py +++ b/gateway/hooks.py @@ -21,6 +21,7 @@ Errors in hooks are caught and logged but never block the main pipeline. import asyncio import importlib.util +import sys from typing import Any, Callable, Dict, List, Optional import yaml @@ -97,16 +98,28 @@ class HookRegistry: print(f"[hooks] Skipping {hook_name}: no events declared", flush=True) continue - # Dynamically load the handler module + # Dynamically load the handler module. + # Register in sys.modules BEFORE exec_module so Pydantic / + # dataclasses / typing introspection can resolve forward + # references (triggered by `from __future__ import annotations` + # in the handler). Without this, a handler that declares a + # Pydantic BaseModel for webhook/event payloads fails at first + # dispatch with "TypeAdapter ... is not fully defined". + module_name = f"hermes_hook_{hook_name}" spec = importlib.util.spec_from_file_location( - f"hermes_hook_{hook_name}", handler_path + module_name, handler_path ) if spec is None or spec.loader is None: print(f"[hooks] Skipping {hook_name}: could not load handler.py", flush=True) continue module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(module) + sys.modules[module_name] = module + try: + spec.loader.exec_module(module) + except Exception: + sys.modules.pop(module_name, None) + raise handle_fn = getattr(module, "handle", None) if handle_fn is None: diff --git a/hermes_cli/web_server.py b/hermes_cli/web_server.py index 569449f188d..c9f8f6bf1b9 100644 --- a/hermes_cli/web_server.py +++ b/hermes_cli/web_server.py @@ -3224,13 +3224,23 @@ def _mount_plugin_api_routes(): _log.warning("Plugin %s declares api=%s but file not found", plugin["name"], api_file_name) continue try: - spec = importlib.util.spec_from_file_location( - f"hermes_dashboard_plugin_{plugin['name']}", api_path, - ) + module_name = f"hermes_dashboard_plugin_{plugin['name']}" + spec = importlib.util.spec_from_file_location(module_name, api_path) if spec is None or spec.loader is None: continue mod = importlib.util.module_from_spec(spec) - spec.loader.exec_module(mod) + # Register in sys.modules BEFORE exec_module so pydantic/FastAPI + # can resolve forward references (e.g. models defined in a file + # that uses `from __future__ import annotations`). Without this, + # TypeAdapter lazy-build fails at first request with + # "is not fully defined" because the module namespace isn't + # reachable by name for string-annotation resolution. + sys.modules[module_name] = mod + try: + spec.loader.exec_module(mod) + except Exception: + sys.modules.pop(module_name, None) + raise router = getattr(mod, "router", None) if router is None: _log.warning("Plugin %s api file has no 'router' attribute", plugin["name"])