feat(gateway): wire register_platform_adapter into config + boot path
GatewayConfig: - Adds plugin_platforms: Dict[str, PlatformConfig] sibling to the existing platforms: Dict[Platform, PlatformConfig]. Held separately so the closed Platform enum stays closed (no synthetic enum members, no string-vs-enum sniffing across the codebase). - from_dict triggers plugin discovery before parsing platform names, then routes unknown-but-claimed names into plugin_platforms. Unclaimed unknown names continue to be silently skipped (preserves the original behavior). GatewayRunner: - New _create_plugin_adapter(name, config) — looks up the registered (adapter_class, requirements_check) tuple via hermes_cli.plugins.get_plugin_platform_adapter, validates requirements, instantiates. Distinct from _create_adapter so the in-tree if/elif chain stays free of plugin concerns. - Boot loop in _run_async now iterates self.config.plugin_platforms after the in-tree platforms loop. Plugin platforms get the same message_handler / fatal_error_handler / session_store / busy_session_handler wiring as in-tree adapters; reconnection-queue + status-tracking is intentionally simpler for v1 (no retry loop). Validation: 9/9 checks pass in .hermes-validation/test_register_platform_adapter.py covering registration, discovery, in-tree precedence, duplicate rejection, config routing, and adapter instantiation. 48/48 hermes plugin tests pass; gateway test failures in this branch are env-related (aiohttp, websocket) and don't touch the patched code paths. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
5d5deaa32e
commit
b4ce9e4ea8
@ -222,11 +222,25 @@ class StreamingConfig:
|
||||
class GatewayConfig:
|
||||
"""
|
||||
Main gateway configuration.
|
||||
|
||||
|
||||
Manages all platform connections, session policies, and delivery settings.
|
||||
"""
|
||||
# Platform configurations
|
||||
platforms: Dict[Platform, PlatformConfig] = field(default_factory=dict)
|
||||
|
||||
# Plugin-registered platform configurations.
|
||||
#
|
||||
# Keyed by string platform name (matching what
|
||||
# PluginContext.register_platform_adapter was called with). Populated
|
||||
# by from_dict() when a platform name in config.yaml doesn't map to
|
||||
# a built-in Platform enum value but IS claimed by a loaded plugin.
|
||||
# The gateway runner iterates this dict alongside `platforms` and
|
||||
# creates adapters via _create_plugin_adapter.
|
||||
#
|
||||
# Held as a separate dict (rather than mixing with `platforms`) so
|
||||
# the closed Platform enum stays closed — no synthetic enum members,
|
||||
# no string-vs-enum sniffing throughout the codebase.
|
||||
plugin_platforms: Dict[str, PlatformConfig] = field(default_factory=dict)
|
||||
|
||||
# Session reset policies by type
|
||||
default_reset_policy: SessionResetPolicy = field(default_factory=SessionResetPolicy)
|
||||
@ -361,13 +375,31 @@ class GatewayConfig:
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, Any]) -> "GatewayConfig":
|
||||
# Trigger plugin discovery so register_platform_adapter calls
|
||||
# have populated the plugin manager's adapter registry by the
|
||||
# time we look up unknown platform names below. Idempotent.
|
||||
# Imported inside the method to avoid pulling hermes_cli into
|
||||
# gateway.config's import-time dep graph.
|
||||
try:
|
||||
from hermes_cli.plugins import discover_plugins, get_plugin_platform_adapter
|
||||
discover_plugins()
|
||||
except Exception:
|
||||
# Plugin discovery is optional. If hermes_cli isn't on the
|
||||
# path (e.g. some embedded test scenarios), silently fall
|
||||
# back to the original "skip unknown" behavior.
|
||||
get_plugin_platform_adapter = lambda _name: None # type: ignore[assignment]
|
||||
|
||||
platforms = {}
|
||||
plugin_platforms: Dict[str, PlatformConfig] = {}
|
||||
for platform_name, platform_data in data.get("platforms", {}).items():
|
||||
try:
|
||||
platform = Platform(platform_name)
|
||||
platforms[platform] = PlatformConfig.from_dict(platform_data)
|
||||
except ValueError:
|
||||
pass # Skip unknown platforms
|
||||
# Not a built-in platform. Check if a plugin claims it.
|
||||
if get_plugin_platform_adapter(platform_name) is not None:
|
||||
plugin_platforms[platform_name] = PlatformConfig.from_dict(platform_data)
|
||||
# Else: silently skip — original behavior preserved.
|
||||
|
||||
reset_by_type = {}
|
||||
for type_name, policy_data in data.get("reset_by_type", {}).items():
|
||||
@ -406,6 +438,7 @@ class GatewayConfig:
|
||||
|
||||
return cls(
|
||||
platforms=platforms,
|
||||
plugin_platforms=plugin_platforms,
|
||||
default_reset_policy=default_policy,
|
||||
reset_by_type=reset_by_type,
|
||||
reset_by_platform=reset_by_platform,
|
||||
|
||||
@ -1953,6 +1953,43 @@ class GatewayRunner:
|
||||
"next_retry": time.monotonic() + 30,
|
||||
}
|
||||
|
||||
# Plugin-registered platforms (after the in-tree loop above —
|
||||
# in-tree wins on name collision because it ran first). Plugin
|
||||
# platforms are NOT in the Platform enum, so they don't get the
|
||||
# full reconnection-queue + status-tracking treatment yet —
|
||||
# that's a v1 simplification. If a plugin platform fails on
|
||||
# connect we log + skip without queueing.
|
||||
for plugin_name, platform_config in self.config.plugin_platforms.items():
|
||||
if not platform_config.enabled:
|
||||
continue
|
||||
enabled_platform_count += 1
|
||||
adapter = self._create_plugin_adapter(plugin_name, platform_config)
|
||||
if not adapter:
|
||||
logger.warning("No adapter available for plugin platform %s", plugin_name)
|
||||
continue
|
||||
adapter.set_message_handler(self._handle_message)
|
||||
adapter.set_fatal_error_handler(self._handle_adapter_fatal_error)
|
||||
adapter.set_session_store(self.session_store)
|
||||
adapter.set_busy_session_handler(self._handle_active_session_busy_message)
|
||||
logger.info("Connecting to plugin platform %s...", plugin_name)
|
||||
try:
|
||||
success = await adapter.connect()
|
||||
if success:
|
||||
# Plugin platforms keyed by string in self.adapters
|
||||
# (built-ins use the Platform enum). Downstream code
|
||||
# treats both as opaque dict keys for routing.
|
||||
self.adapters[plugin_name] = adapter
|
||||
connected_count += 1
|
||||
logger.info("✓ %s (plugin) connected", plugin_name)
|
||||
else:
|
||||
logger.warning("✗ %s (plugin) failed to connect", plugin_name)
|
||||
startup_retryable_errors.append(
|
||||
f"{plugin_name}: failed to connect"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.exception("Plugin platform %s: connect raised", plugin_name)
|
||||
startup_retryable_errors.append(f"{plugin_name}: {e}")
|
||||
|
||||
if connected_count == 0:
|
||||
if startup_nonretryable_errors:
|
||||
reason = "; ".join(startup_nonretryable_errors)
|
||||
@ -2421,9 +2458,50 @@ class GatewayRunner:
|
||||
"""Wait for shutdown signal."""
|
||||
await self._shutdown_event.wait()
|
||||
|
||||
def _create_plugin_adapter(
|
||||
self,
|
||||
name: str,
|
||||
config: Any,
|
||||
) -> Optional[BasePlatformAdapter]:
|
||||
"""Create an adapter from a plugin-registered platform.
|
||||
|
||||
Looks up the plugin's registered (adapter_class, requirements_check)
|
||||
tuple via PluginContext.register_platform_adapter, validates
|
||||
requirements, and instantiates. Returns None on any failure
|
||||
(logs the cause).
|
||||
|
||||
Distinct from _create_adapter to keep the in-tree if/elif chain
|
||||
free of plugin concerns; plugin platforms always go through this
|
||||
path.
|
||||
"""
|
||||
from hermes_cli.plugins import get_plugin_platform_adapter
|
||||
entry = get_plugin_platform_adapter(name)
|
||||
if entry is None:
|
||||
logger.warning(
|
||||
"Plugin platform %s: no plugin claims this name. "
|
||||
"Did the plugin's register() function call "
|
||||
"ctx.register_platform_adapter(name=%r, ...)?",
|
||||
name, name,
|
||||
)
|
||||
return None
|
||||
adapter_class, req_check = entry
|
||||
if req_check and not req_check():
|
||||
logger.warning(
|
||||
"Plugin platform %s: requirements_check returned False",
|
||||
name,
|
||||
)
|
||||
return None
|
||||
try:
|
||||
return adapter_class(config)
|
||||
except Exception:
|
||||
logger.exception(
|
||||
"Plugin platform %s: adapter __init__ raised", name,
|
||||
)
|
||||
return None
|
||||
|
||||
def _create_adapter(
|
||||
self,
|
||||
platform: Platform,
|
||||
self,
|
||||
platform: Platform,
|
||||
config: Any
|
||||
) -> Optional[BasePlatformAdapter]:
|
||||
"""Create the appropriate adapter for a platform."""
|
||||
|
||||
Loading…
Reference in New Issue
Block a user