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

298 lines
13 KiB
Markdown

# 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.:
```python
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:
```python
# 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:
- [x] 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
- [x] 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.