From 754d162d995224955ff2b9a31441fc7c6478695e Mon Sep 17 00:00:00 2001 From: Hongming Wang Date: Sun, 3 May 2026 07:47:51 -0700 Subject: [PATCH] feat: dual-mode for upstream register_platform (post-#17751) + legacy register_platform_adapter NousResearch/hermes-agent#17751 (merged 2026-04-30) shipped a comprehensive pluggable-platform system with: - ctx.register_platform(name, label, adapter_factory, check_fn, ...) - Open Platform enum (Platform('molecule') creates a pseudo-member via _missing_() when the platform_registry knows about it) That supersedes my upstream PR #18775 (which used a narrower register_platform_adapter shape with a closed enum + custom PluginPlatformIdentifier). Closing #18775 as redundant. This plugin previously coupled to my fork's API. Migration: - __init__.py register() now prefers ctx.register_platform when available; falls back to ctx.register_platform_adapter on legacy forks (template-hermes' baked-in fork until it migrates). - adapter.py constructs Platform(name) when the enum accepts 'molecule', else falls back to PluginPlatformIdentifier(name). Same wheel installs cleanly on stock hermes-agent (post-#17751) AND on the legacy template-hermes fork build. Removed the test stub of PluginPlatformIdentifier; tests now stub the open-enum Platform shape with the same _missing_() behavior the upstream ships. Co-Authored-By: Claude Opus 4.7 (1M context) --- hermes_channel_molecule/__init__.py | 44 ++++++++++++++++--- hermes_channel_molecule/adapter.py | 22 +++++++++- tests/test_adapter.py | 67 ++++++++++++++++------------- 3 files changed, 96 insertions(+), 37 deletions(-) diff --git a/hermes_channel_molecule/__init__.py b/hermes_channel_molecule/__init__.py index 86cebfc..7419502 100644 --- a/hermes_channel_molecule/__init__.py +++ b/hermes_channel_molecule/__init__.py @@ -16,9 +16,41 @@ __all__ = [ def register(ctx) -> None: - """Plugin entry point — called by hermes_cli.plugins on discovery.""" - ctx.register_platform_adapter( - name="molecule", - adapter_class=MoleculeAdapter, - requirements_check=check_molecule_requirements, - ) + """Plugin entry point — called by hermes_cli.plugins on discovery. + + Dual-API shim: + - Upstream NousResearch/hermes-agent#17751 (merged 2026-04-30) + shipped ``ctx.register_platform(name, label, adapter_factory, + check_fn, ...)``. This is the canonical API on stock hermes-agent. + - Earlier forks (incl. hermes-agent before #17751 landed) expose + ``ctx.register_platform_adapter(name, adapter_class, + requirements_check)`` instead — narrower signature, no factory. + + Detect at runtime so the same wheel installs cleanly on both. + """ + if hasattr(ctx, "register_platform"): + ctx.register_platform( + name="molecule", + label="Molecule", + adapter_factory=lambda cfg: MoleculeAdapter(cfg), + check_fn=check_molecule_requirements, + required_env=["MOLECULE_WORKSPACE_ID", "MOLECULE_PLATFORM_URL"], + install_hint=( + "set MOLECULE_WORKSPACE_ID, MOLECULE_WORKSPACE_TOKEN, " + "MOLECULE_PLATFORM_URL, MOLECULE_ORG_ID; ensure " + "molecule-ai-workspace-runtime is on the python that " + "MOLECULE_MCP_PYTHON resolves to" + ), + ) + elif hasattr(ctx, "register_platform_adapter"): + ctx.register_platform_adapter( + name="molecule", + adapter_class=MoleculeAdapter, + requirements_check=check_molecule_requirements, + ) + else: + raise RuntimeError( + "hermes-channel-molecule: this hermes-agent version exposes " + "neither register_platform (upstream #17751+) nor " + "register_platform_adapter (legacy fork) — cannot register" + ) diff --git a/hermes_channel_molecule/adapter.py b/hermes_channel_molecule/adapter.py index 24ca6ba..8471204 100644 --- a/hermes_channel_molecule/adapter.py +++ b/hermes_channel_molecule/adapter.py @@ -40,7 +40,25 @@ from gateway.platforms.base import ( MessageType, SendResult, ) -from hermes_cli.plugins import PluginPlatformIdentifier +from gateway.config import Platform + + +def _platform_identity(name: str): + """Pick the right Platform-shaped identity for the installed hermes. + + Upstream #17751 made Platform an open enum (``Platform("molecule")`` + works via ``_missing_()``). Legacy forks have a closed enum and ship + ``PluginPlatformIdentifier`` for plugin-supplied platforms instead. + Detect at import time so the same plugin works on both. + """ + try: + return Platform(name) + except ValueError: + # Closed enum (legacy fork) — fall back to the fork's plugin + # identifier shape. Import lazily so a stock hermes-agent doesn't + # need this symbol to exist. + from hermes_cli.plugins import PluginPlatformIdentifier + return PluginPlatformIdentifier(name) logger = logging.getLogger(__name__) @@ -142,7 +160,7 @@ class MoleculeAdapter(BasePlatformAdapter): """Hermes platform adapter for the molecule platform via A2A MCP.""" def __init__(self, config) -> None: - super().__init__(config, PluginPlatformIdentifier(PLUGIN_NAME)) + super().__init__(config, _platform_identity(PLUGIN_NAME)) self._workspace_id = os.environ.get("MOLECULE_WORKSPACE_ID", "") self._platform_url = os.environ.get( diff --git a/tests/test_adapter.py b/tests/test_adapter.py index 20b2cee..f4372c9 100644 --- a/tests/test_adapter.py +++ b/tests/test_adapter.py @@ -32,44 +32,53 @@ _ADAPTER_PATH = _REPO_ROOT / "hermes_channel_molecule" / "adapter.py" def _load_adapter_module(): - """Import adapter.py without going through hermes_cli.plugins. + """Import adapter.py without going through gateway/* + hermes_cli/*. - The real plugin loader supplies hermes_cli.plugins; in tests we stub - the only symbol the adapter needs (PluginPlatformIdentifier) so the - import doesn't pull the whole hermes-agent tree. + The real plugin loader pulls in gateway.config + gateway.platforms.base; + in tests we stub them so the import doesn't require the whole + hermes-agent tree. """ - fake_plugins = type(sys)("hermes_cli.plugins") - - class PluginPlatformIdentifier: - __slots__ = ("value",) - - def __init__(self, name: str) -> None: - self.value = name - - def __hash__(self) -> int: - return hash(("__plugin_platform__", self.value)) - - def __eq__(self, other: object) -> bool: - return ( - isinstance(other, PluginPlatformIdentifier) - and self.value == other.value - ) - - fake_plugins.PluginPlatformIdentifier = PluginPlatformIdentifier - sys.modules.setdefault("hermes_cli", type(sys)("hermes_cli")) - sys.modules["hermes_cli.plugins"] = fake_plugins - - # Stub gateway.platforms.base — the adapter only uses - # BasePlatformAdapter, MessageEvent, MessageType, SendResult. + # Stub gateway.config.Platform — the adapter constructs Platform("molecule") + # in __init__ to identify itself to the upstream platform_registry. fake_gateway = type(sys)("gateway") - fake_platforms = type(sys)("gateway.platforms") - fake_base = type(sys)("gateway.platforms.base") + fake_config = type(sys)("gateway.config") from dataclasses import dataclass, field from enum import Enum from typing import Optional, List, Dict, Any as TAny from datetime import datetime + class Platform(Enum): + # Open enum (per upstream #17751): Platform("molecule") creates a + # pseudo-member at runtime when not in the in-tree set. Empty + # enums can't be created in Python — seed with a sentinel that + # the adapter never references. + _SENTINEL = "__test_sentinel__" + + @classmethod + def _missing_(cls, value): + if not isinstance(value, str) or not value.strip(): + return None + value = value.strip().lower() + if value in cls._value2member_map_: + return cls._value2member_map_[value] + pseudo = object.__new__(cls) + pseudo._value_ = value + pseudo._name_ = value.upper().replace("-", "_") + cls._value2member_map_[value] = pseudo + cls._member_map_[pseudo._name_] = pseudo + return pseudo + + fake_config.Platform = Platform + fake_gateway.config = fake_config + sys.modules["gateway"] = fake_gateway + sys.modules["gateway.config"] = fake_config + + # Stub gateway.platforms.base — the adapter only uses + # BasePlatformAdapter, MessageEvent, MessageType, SendResult. + fake_platforms = type(sys)("gateway.platforms") + fake_base = type(sys)("gateway.platforms.base") + class MessageType(Enum): TEXT = "text"