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>
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.pyalready implements full plugin discovery across THREE sources:- User dir:
~/.hermes/plugins/<name>/ - Project dir:
./.hermes/plugins/<name>/ - Pip entry_points group:
hermes_agent.plugins
- User dir:
- The discovery loop is at
hermes_cli/plugins.py:433and_scan_entry_points()at line 499. PluginContext(line 124) exposes aregister(ctx)collector with:register_tool(line 133)register_cli_command(line 192)register_command(line 217) — slash commandregister_context_engine(line 295)register_hook(line 327)register_skill(line 346)
- But NOT
register_platform_adapter. Platform adapters remain hardcoded ingateway/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__.pyfor legacy reasons; the modernhermes_cli/plugins.pyis 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: trueetc. continue to work unchanged.- No new mandatory config keys.
- Existing
Platform.XPython 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 existingtests/plugins/shape — see howregister_toolis tested today). Smoke that hermes boot loads it. - Manual (already done locally):
hermes-platform-molecule-a2ascaffold 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 →MessageEventdispatch → 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_idempotentand two siblings assertlen(list_plugins()) == 1after 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 adiscover_only_user_dir=Truetest hook todiscover_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.pyexplaining 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:
- Naming.
register_platform_adaptermatches existingregister_*collector methods. Short forms (register_platform,register_channel) are also possible. Defaulting to the long form for consistency. - Synthetic Platform value. Is a
Platform.from_string()helper (with synthetic plugin entries) acceptable, or do maintainers prefer a different shape — e.g., adding aname: strfield toPlatformConfigso callers know the plugin name without going through the enum? - 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 underplugins/memory/<name>/)? - Multi-account plugins. Existing platforms (Telegram, Slack)
support multi-account via the
extraconfig 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-a2aagainst a forked hermes with the proposedregister_platform_adapterpatch applied →~/hermes-platform-molecule-a2a/, 11/11 unit tests pass, 7/7 production-path E2E checkpoints pass - Patched fork at
~/.hermes/hermes-agentbranchfeat/platform-adapter-plugins(4 commits): 1.PluginContext.register_platform_adapter+ manager registry +get_plugin_platform_adapteraccessor 2.GatewayConfig.plugin_platforms+_create_plugin_adapterboot path 3.PluginPlatformIdentifierhelper forBasePlatformAdapterconstruction 4.resolve_platform_idfor plugin-platform-safe deserialization inSessionSource.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-hermesDocker image - E2E: boot the local image, send A2A messages from a peer agent,
observe
_handle_messagedispatch + reply through A2A queue - Confirm
PluginPlatformIdentifierdoesn't break any downstreamisinstance(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-agentmain(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.pyas 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.