molecule-core/docs/integrations/hermes-platform-plugins-upstream-pr.md
Hongming Wang 2dd5684e73 docs(integrations): update hermes plugin path status to post-merge
PR #32 (workspace template) merged 2026-05-02; image rebuild
succeeded. Plugin baked in. Local full-chain E2E green; caught + fixed
a real KeyError in upstream hermes_cli/tools_config.py. Upstream PR
#18775 still OPEN/CONFLICTING — not on critical path.

Also rewrites hermes-platform-plugins-upstream-pr.md to reflect the
final landing shape (existing hermes_cli/plugins.py, not a new
plugins/platforms/ system).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 04:42:00 -07:00

13 KiB

Upstream PR draft: register_platform_adapter for hermes-agent plugins

Status: Draft — pre-submission review (REWRITTEN 2026-05-02 after deeper research) Target repo: NousResearch/hermes-agent Owner: Molecule AI (hongmingwang@moleculesai.app) Date drafted: 2026-05-02 (rewrite of earlier draft)


Background — what changed in this draft

The first draft proposed adding a plugins/platforms/ discovery directory + a _create_adapter() fallback chain. That was wrong — it duplicated infrastructure that already exists.

Deeper research established (validated by hand-rolling a test plugin under ~/.hermes/plugins/):

  • hermes_cli/plugins.py already implements full plugin discovery across THREE sources:
    • User dir: ~/.hermes/plugins/<name>/
    • Project dir: ./.hermes/plugins/<name>/
    • Pip entry_points group: hermes_agent.plugins
  • The discovery loop is at hermes_cli/plugins.py:433 and _scan_entry_points() at line 499.
  • PluginContext (line 124) exposes a register(ctx) collector with:
    • register_tool (line 133)
    • register_cli_command (line 192)
    • register_command (line 217) — slash command
    • register_context_engine (line 295)
    • register_hook (line 327)
    • register_skill (line 346)
  • But NOT register_platform_adapter. Platform adapters remain hardcoded in gateway/run.py:_create_adapter() (lines 2424-2578), the only major subsystem still closed to plugins.
  • Memory providers have a parallel discovery system at plugins/memory/__init__.py for legacy reasons; the modern hermes_cli/plugins.py is the way forward for new plugin types.
  • Hand-rolled test confirmed user-dir and entry_points discovery both work end-to-end. Zero external plugins exist in the wild today — the system is technically mature but socially unused.

This makes the PR much smaller and more obviously correct: extend the existing plugin pattern by one method, mirror how memory providers work, no novel infrastructure.


Proposed PR title

feat(gateway): platform adapter plugins via PluginContext.register_platform_adapter

Branch: feat/platform-adapter-plugins per CONTRIBUTING.md branch convention.


PR body

Problem

Hermes ships 19 in-tree platform adapters (gateway/run.py:2424-2578). Adding a new platform requires editing two files: append a Platform enum value at gateway/config.py:48-69, then append an elif platform == Platform.X: branch in _create_adapter(). For platforms with broad demand (Telegram, Slack, Discord) this is fine. For narrower channels — enterprise-internal protocols, agent-to-agent inbox bridges, niche regional platforms — the only path is a fork of gateway/run.py.

This is the only major subsystem that's still closed. Tools, CLI commands, slash commands, context engines, hooks, and skills all already extend via hermes_cli/plugins.py's PluginContext collector, with three discovery paths (user dir / project dir / pip entry_points). Platform adapters should follow the same pattern.

Proposal

Add one collector method to PluginContext and one fallback branch to _create_adapter(). That's the entire change.

1. New collector method in hermes_cli/plugins.py, beside the existing register_tool / register_hook etc.:

class PluginContext:
    # ...existing register_* methods...

    def register_platform_adapter(
        self,
        name: str,
        adapter_class: type,
        requirements_check: Callable[[], bool] | None = None,
    ) -> None:
        """Register a custom platform adapter.

        name              — unique platform identifier (matches
                            gateway.platforms.<name> in config.yaml)
        adapter_class     — subclass of BasePlatformAdapter
        requirements_check— optional, returns False if dependencies
                            missing (matches existing
                            check_telegram_requirements pattern).
        """
        self._registered_platform_adapters[name] = (adapter_class, requirements_check)

2. Plugin-registered adapters in _create_adapter() — fall through to the plugin-registered map after the in-tree if/elif chain returns None:

# at gateway/run.py:2578, AFTER the existing chain
plugin_entry = self._plugin_manager.get_platform_adapter(platform.value)
if plugin_entry:
    adapter_class, req_check = plugin_entry
    if req_check and not req_check():
        logger.warning(f"{platform.value}: plugin requirements not met")
        return None
    return adapter_class(config)

return None  # existing return

3. Platform enum stays closed but accepts unknown values through a small loosening: rather than refactor enum-vs-string, introduce Platform.from_string() that returns either an existing enum member OR a synthetic Platform.PLUGIN(value)-equivalent that carries the plugin name through. _create_adapter() then dispatches on the carried name. This is the smallest change preserving backward compatibility — every existing Platform.TELEGRAM reference keeps working unchanged.

Why this is the right shape

  • Symmetric. Mirrors register_tool, register_hook, etc. — same collector, same discovery, same lifecycle. No new mental model.
  • No new infrastructure. Reuses hermes_cli/plugins.py's existing three-source discovery (user dir / project dir / entry_points) — zero new code paths to test.
  • Backward compatible. All 19 in-tree adapters keep their hardcoded path; precedence is in-tree first, plugin fallback. No behavior change for any existing user.
  • Discovery cost is zero. Plugin lookup only fires if the platform name doesn't match an in-tree value.
  • Forward compatible. When external plugins become commonplace (today: zero published, system technically mature but unused), platform adapters benefit from the same ecosystem growth as tools.

What we'll ship as the first consumer

Molecule will publish hermes-platform-molecule-a2a on PyPI with the appropriate [project.entry-points."hermes_agent.plugins"] entry. It delivers Molecule platform A2A inbox messages into the same _handle_message dispatch Telegram uses, with MessageEvent(internal=True) to bypass user-auth (peer agents are authenticated at the platform layer, not the Telegram-user level). Implementation lives in our workspace template; this PR upstream is the contract change that lets us register without forking.

Backward compatibility

  • All 19 in-tree adapters keep their hardcoded path. Precedence: in-tree wins on name collision (matches the memory plugin pattern).
  • gateway.platforms.telegram.enabled: true etc. continue to work unchanged.
  • No new mandatory config keys.
  • Existing Platform.X Python references unchanged.
  • Plugin discovery only adds latency on platforms that don't match an in-tree value — zero cost for existing users.

Test plan

  • Unit: Mock plugin registers an adapter via register_platform_adapter; _create_adapter() returns it for the corresponding platform name.
  • Unit: In-tree precedence — when plugin AND in-tree both register telegram, in-tree wins.
  • Unit: Duplicate plugin registration warns + skips, doesn't replace the original.
  • Integration: Add tests/plugins/platform_example/ (matching the existing tests/plugins/ shape — see how register_tool is tested today). Smoke that hermes boot loads it.
  • Manual (already done locally): hermes-platform-molecule-a2a scaffold validates against the patched fork end-to-end:
    • 11/11 unit tests on the adapter (lifecycle, inbound auth, outbound routing, plugin entry-point shape)
    • 7/7 production-path checkpoints (entry_points discovery → registry → GatewayConfig.from_dict_create_plugin_adapter → live HTTP listener → MessageEvent dispatch → callback POST)
    • 9/9 user-dir-discovery validation against the patched PluginContext / PluginManager
  • Pre-existing test isolation issue (independent of this PR): tests/hermes_cli/test_plugins.py::test_discover_is_idempotent and two siblings assert len(list_plugins()) == 1 after creating one test plugin in a tmp_path. They fail on any dev box that has a hermes plugin pip-installed (entry_points discovery is global, not isolated by HERMES_HOME). Not caused by this patch but surfaced during validation. Worth fixing in a follow-up by either filtering entry-point plugins out of these specific tests, or adding a discover_only_user_dir=True test hook to discover_and_load.

Documentation

  • Extend website/docs/developer-guide/build-a-hermes-plugin.md's capability list to mention platform adapters alongside tools, hooks, etc.
  • One-paragraph note in gateway/run.py explaining the in-tree-first, plugin-fallback precedence.

Out of scope

  • Memory provider system migration (still uses plugins/memory/__init__.py's separate discovery). Out of scope for this PR — orthogonal cleanup.
  • A "Plugins Hub" analogous to Skills Hub. Independently useful but separate proposal; ship the contract first, build the distribution/discovery UX later.

Open questions to put in the GitHub Discussion

Per CONTRIBUTING.md, design proposals go in GitHub Discussions at NousResearch/hermes-agent/discussions, not Discord. Open one titled "RFC: PluginContext.register_platform_adapter" before filing the PR. Questions to surface:

  1. Naming. register_platform_adapter matches existing register_* collector methods. Short forms (register_platform, register_channel) are also possible. Defaulting to the long form for consistency.
  2. Synthetic Platform value. Is a Platform.from_string() helper (with synthetic plugin entries) acceptable, or do maintainers prefer a different shape — e.g., adding a name: str field to PlatformConfig so callers know the plugin name without going through the enum?
  3. Test fixture vs example plugin. The tests/plugins/ directory has fixture-only plugins. Should the platform adapter test plugin live there too, or as a real bundled adapter (matching how memory providers ship as real bundled implementations under plugins/memory/<name>/)?
  4. Multi-account plugins. Existing platforms (Telegram, Slack) support multi-account via the extra config dict. Is the plugin-registered adapter expected to handle the same shape, or is single-account a reasonable v1 constraint?

Status checklist (for our own tracking)

Per user's gating: "if the plugin works locally in our docker setup and e2e testing works, yes [submit]". Validation prerequisites:

  • Build hermes-platform-molecule-a2a against a forked hermes with the proposed register_platform_adapter patch applied → ~/hermes-platform-molecule-a2a/, 11/11 unit tests pass, 7/7 production-path E2E checkpoints pass
  • Patched fork at ~/.hermes/hermes-agent branch feat/platform-adapter-plugins (4 commits): 1. PluginContext.register_platform_adapter + manager registry + get_plugin_platform_adapter accessor 2. GatewayConfig.plugin_platforms + _create_plugin_adapter boot path 3. PluginPlatformIdentifier helper for BasePlatformAdapter construction 4. resolve_platform_id for plugin-platform-safe deserialization in SessionSource.from_dict / SessionEntry.from_dict / HomeChannel.from_dict (without this, daemon restart loses every plugin-platform session)
  • Bake the forked hermes + plugin into a local copy of our molecule-ai-workspace-template-hermes Docker image
  • E2E: boot the local image, send A2A messages from a peer agent, observe _handle_message dispatch + reply through A2A queue
  • Confirm PluginPlatformIdentifier doesn't break any downstream isinstance(self.platform, Platform) check — grep for those
  • Open GitHub Discussion for design validation; wait for maintainer feedback (≥1 week)
  • Address Discussion feedback in the PR
  • PR description: what/why + how-to-test + platforms tested per CONTRIBUTING.md
  • Open PR against NousResearch/hermes-agent main (requires user confirmation — visible-to-others action)
  • Track PR; bump cadence weekly; if stalled past 4 weeks, propose bundling our adapter directly under gateway/platforms/molecule_a2a.py as a fallback (smaller upstream maintenance footprint than fork)

What changed from the first draft, in one paragraph

First draft proposed extending the memory-provider pattern to platforms via a new plugins/platforms/ directory and bespoke discovery code in _create_adapter(). Research established that hermes's MODERN plugin system is hermes_cli/plugins.py (not plugins/memory/), already supports user-dir + entry_points discovery for tools/hooks/CLI/skills, and just needs register_platform_adapter added to its collector to cover platforms too. The new draft is ~60 lines of upstream code change instead of ~200, with a tighter conceptual fit and better forward compatibility.