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>
This commit is contained in:
Hongming Wang 2026-05-02 04:42:00 -07:00
parent 35cb6ba089
commit 2dd5684e73
2 changed files with 321 additions and 151 deletions

View File

@ -1,25 +1,57 @@
# Upstream PR draft: Pluggable platform adapters for hermes-agent
# Upstream PR draft: `register_platform_adapter` for hermes-agent plugins
**Status:** Draft — pre-submission review
**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
**Date drafted:** 2026-05-02 (rewrite of earlier draft)
---
## Why this draft exists
## Background — what changed in this draft
Molecule needs to deliver A2A inbox messages to a hermes-hosted agent the same way Telegram messages reach it today — through `_handle_message`, with `set_busy_session_handler` semantics for mid-turn arrivals. Today this requires forking `gateway/run.py` because the platform adapter system is closed (`_create_adapter` is a hardcoded if/elif chain at lines 2424-2578).
The first draft proposed adding a `plugins/platforms/` discovery
directory + a `_create_adapter()` fallback chain. **That was wrong**
it duplicated infrastructure that already exists.
But hermes already ships a working plugin discovery system for memory backends (`plugins/memory/__init__.py`). Extending the same pattern to platforms is a small, symmetric change — not novel architecture. This draft documents the proposed upstream PR before we open it, so we can iterate locally on tone, scope, and code shape.
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
> Pluggable platform adapters via `plugins/platforms/` discovery
> `feat(gateway): platform adapter plugins via PluginContext.register_platform_adapter`
(Mirrors the existing `plugins/memory/` shape so the title alone signals "this is the same pattern, just for the other subsystem.")
Branch: `feat/platform-adapter-plugins` per
`CONTRIBUTING.md` branch convention.
---
@ -27,136 +59,188 @@ But hermes already ships a working plugin discovery system for memory backends (
### Problem
Hermes ships 19 in-tree platform adapters (Telegram, Discord, WhatsApp, Slack, Signal, Mattermost, Matrix, Email, SMS, DingTalk, Feishu, WeCom variants, Weixin, BlueBubbles, QQBot, HomeAssistant, API server, Webhook). Each is wired by editing two files:
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`.
- `gateway/config.py:48-69` — append a `Platform` enum value
- `gateway/run.py:2424-2578` — append an `elif platform == Platform.X:` branch in `_create_adapter()`
For platforms with broad demand (Telegram, Slack, etc.) this is fine: the maintenance load lives upstream, every user benefits. For platforms with narrow but real demand — enterprise-internal channels (Rocket.Chat, RingCentral, Zulip), agent-to-agent inbox protocols (e.g. Molecule's A2A), niche regional platforms, or experimental transports — the only path today is forking `gateway/run.py`. Forks drift, defeat the purpose of an OSS gateway, and discourage contribution back upstream.
### Prior art (already in hermes)
The memory subsystem solved exactly this problem at `plugins/memory/__init__.py`:
1. **Two-tier discovery** — bundled providers in `plugins/memory/<name>/` plus user-installed providers in `$HERMES_HOME/plugins/<name>/`. Bundled wins on name collision.
2. **`register(ctx)` collector pattern** (`plugins/memory/__init__.py:264-305`) — a plugin's `__init__.py` exposes a `register(ctx)` function; `ctx` already supports `register_memory_provider`, `register_tool`, `register_hook`, `register_cli_command`.
3. **`plugin.yaml` manifest** for description and metadata.
4. **Config-driven activation** (`memory.provider: honcho` selects which provider loads).
Adding `register_platform_adapter` to the same collector and a `plugins/platforms/` discovery directory extends this pattern symmetrically.
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
**Three small changes:**
Add **one collector method** to `PluginContext` and **one fallback
branch** to `_create_adapter()`. That's the entire change.
1. **New collector method** in `plugins/memory/__init__.py:_ProviderCollector` (or a new shared `plugins/_collector.py` if maintainers prefer cleaner separation):
**1. New collector method in `hermes_cli/plugins.py`**, beside the
existing `register_tool` / `register_hook` etc.:
```python
def register_platform_adapter(self, name: str, adapter_class: type, requirements_check=None):
"""Register a platform adapter loadable as plugin.
```python
class PluginContext:
# ...existing register_* methods...
name: unique platform identifier (matches gateway.platforms.<name> in config)
adapter_class: subclass of BasePlatformAdapter
requirements_check: optional callable returning bool — same shape as
existing check_telegram_requirements() etc.
"""
self.platform_adapters[name] = (adapter_class, requirements_check)
```
def register_platform_adapter(
self,
name: str,
adapter_class: type,
requirements_check: Callable[[], bool] | None = None,
) -> None:
"""Register a custom platform adapter.
2. **New `plugins/platforms/__init__.py`** mirroring `plugins/memory/__init__.py``discover_platform_adapters()`, `load_platform_adapter(name)`, two-tier (bundled + `$HERMES_HOME/plugins/`) discovery.
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)
```
3. **`_create_adapter()` fallback** at `gateway/run.py:2578` — after the in-tree if/elif chain returns None, attempt plugin lookup:
**2. Plugin-registered adapters in `_create_adapter()`** —
fall through to the plugin-registered map after the in-tree if/elif
chain returns None:
```python
# Existing in-tree adapters checked first (precedence preserved).
# If no match, fall through to plugin discovery.
from plugins.platforms import load_platform_adapter
plugin_entry = load_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
```
```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)
4. **`Platform` enum becomes open-set.** Today it's `Enum`; switch to a string-backed pattern that accepts unknown values (still validates against the union of in-tree + discovered plugins at config-load time):
return None # existing return
```
```python
# gateway/config.py — replace Enum with frozen dataclass + dynamic registry.
# Keeps the in-tree values as module-level singletons for backward compat:
# Platform.TELEGRAM still works as today.
```
**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.
This is the only "shape change" in the PR. Backward compat is straightforward: every existing `Platform.TELEGRAM` reference continues to work because the module exports the same names.
### 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 in `_create_adapter()` (precedence: in-tree wins on name collision, exactly like memory plugins).
- Existing config files (`gateway.platforms.telegram.enabled: true`) continue to work unchanged.
- 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.
- Plugin discovery only runs if the platform name doesn't match an in-tree value, so cold-start cost is zero for users who don't use plugins.
- Fork-then-add-platform users can migrate to plugins at their own pace; the in-tree path isn't deprecated.
- 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**: discovery scans both bundled and user dirs, respects precedence.
- **Unit**: `_create_adapter()` falls through to plugin lookup only when in-tree doesn't match.
- **Integration**: ship a minimal `plugins/platforms/example/` in-tree (read-only, returns canned messages) so CI exercises the full plugin code path. Same approach `plugins/memory/holographic/` takes today.
- **Manual**: Molecule will publish `hermes-platform-molecule-a2a` as the first external consumer once this lands.
- **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 `CONTRIBUTING.md`'s "Should it be a Skill or a Tool?" section with "Should it be a Platform Plugin or an in-tree Platform?" — same shape, same decision tree.
- Add `plugins/platforms/README.md` mirroring `plugins/memory/`'s convention.
- 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 (intentionally)
### Out of scope
- **Setuptools `entry_points`** — could be added later as a third discovery tier (after bundled + `$HERMES_HOME/plugins/`). Skipping for v1 because the directory-based discovery already covers the demand and matches the memory pattern. Adding entry_points is a non-breaking extension.
- **Hot-reload** — plugins discovered at gateway boot, no live re-scan. Matches memory plugins.
- **Sandboxing** — plugins run with full hermes process privileges. Same trust model as memory plugins; documented in the new README.
### Reference consumer
Molecule AI will ship `hermes-platform-molecule-a2a` as the first external consumer. Use case: deliver agent-to-agent inbox messages (from peer agents authenticated at the platform layer, not the Telegram-user level) into the same `_handle_message` dispatch Telegram uses, with `internal=True` events to bypass user-auth. Expected timeline: within 2 weeks of merge.
- 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 for upstream maintainers
## Open questions to put in the GitHub Discussion
Per `CONTRIBUTING.md`, the right channel for design proposals is
**GitHub Discussions**, not Discord (Discord is for "questions,
showcasing projects, and sharing skills" — Discussions is the
documented channel for "design proposals and architecture discussions").
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:
Open a Discussion at `NousResearch/hermes-agent/discussions` titled
"RFC: pluggable platform adapters via `plugins/platforms/`" with the
problem + proposal + open questions before filing the PR. This gives
maintainers space to weigh in on shape before code is in flight.
Open questions to put in the Discussion:
1. **Preferred naming.** `register_platform_adapter` vs `register_platform` vs `register_channel`. Consistency with memory's `register_memory_provider` argues for the long form.
2. **Enum vs string.** Is the maintainer team open to making `Platform` open-set? If not, fallback design: keep enum, add a single `Platform.PLUGIN` sentinel + a `plugin_name` field on `PlatformConfig`. Slightly uglier but smaller blast radius.
3. **Testing**: `plugins/platforms/example/` checked into the repo, or test-fixtures-only? Memory plugins are real (mem0, honcho, supermemory bundled), so a real example seems consistent.
4. **Discovery ordering**: confirm the user wants bundled-wins precedence (matches memory) vs user-can-override-bundled (would let downstream patch a buggy in-tree adapter without forking). Current memory pattern is bundled-wins; we'll match it unless told otherwise.
---
## Effort estimate
- **Code change**: ~150 LOC across `plugins/platforms/__init__.py` (new), `gateway/config.py` (Platform refactor), `gateway/run.py` (10-line fallback in `_create_adapter`), tests (~50 LOC).
- **Docs**: ~80 LOC across `CONTRIBUTING.md` extension and new `plugins/platforms/README.md`.
- **Review cycle**: depends on maintainer responsiveness. Memory plugin system shipped in v0.50.7 era; platform plugin system would land for v0.11 if accepted.
---
## After this PR lands (Molecule-side follow-up)
1. Publish `hermes-platform-molecule-a2a` (PyPI + `~/.hermes/plugins/molecule-a2a/`).
2. Bump our hermes workspace template to declare `plugins.platforms.molecule_a2a.enabled: true`.
3. Remove the polling shim from `molecule-ai-workspace-template-hermes/adapter.py` once the plugin path is verified end-to-end.
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?
---
@ -165,27 +249,49 @@ Open questions to put in the Discussion:
Per user's gating: "if the plugin works locally in our docker setup
and e2e testing works, yes [submit]". Validation prerequisites:
- [ ] Build a working `plugins/platforms/molecule_a2a/` plugin against
a forked hermes-agent with the proposed change applied
- [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 `Platform` enum refactor doesn't break downstream — grep
for `Platform.X` usages across hermes
- [ ] Confirm `$HERMES_HOME` is the right user-plugin root for
platforms (matches memory convention)
- [ ] Open a GitHub Discussion at
`NousResearch/hermes-agent/discussions` titled
"RFC: pluggable platform adapters via plugins/platforms/" with
design + open questions; wait for maintainer feedback
- [ ] Branch name: `feat/pluggable-platform-adapters` per
CONTRIBUTING.md branch convention
- [ ] Commit prefix: `feat(gateway): pluggable platform adapters
via plugins/platforms/` per Conventional Commits + scope `gateway`
- [ ] PR description covers what/why + how-to-test + platforms tested,
per CONTRIBUTING.md PR-description requirements
- [ ] Open PR against `NousResearch/hermes-agent` main once Discussion
lands consensus
- [ ] 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
fork-and-bundle as fallback for our hermes template image
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.

View File

@ -51,21 +51,77 @@ adapter POSTs A2A messages to it; gateway dispatches through the same
## hermes
**Status:** Upstream PR drafted; short-term shim deemed unnecessary.
**Status:** Workspace template patch PR #32 MERGED 2026-05-02; image
rebuild succeeded; plugin baked into the workspace runtime. Plugin
package published. Real-subprocess full-chain E2E (`scripts/e2e_full_chain.py`)
green — proves wire shape end-to-end against a real `hermes gateway run`
subprocess + stub OpenAI-compat LLM. Caught + fixed a real `KeyError`
in upstream `hermes_cli/tools_config.py` (PLATFORMS dict lookup
crashed on plugin platforms) — fix on the patched fork branch
(`HongmingWang-Rabbit/hermes-agent` `feat/platform-adapter-plugins`,
commit `18e4849e`). Upstream PR #18775 OPEN; CONFLICTING with main.
Not on critical path for our platform — patched fork is what the
workspace image installs.
**Path:** Open the upstream `BasePlatformAdapter` system to external
plugins. Hermes already ships a working plugin discovery system for
memory backends (`plugins/memory/`, `register(ctx)` collector pattern,
`$HERMES_HOME/plugins/<name>/` user-installed tier). The PR extends
the same shape to platforms — `register_platform_adapter(...)` on the
existing collector, new `plugins/platforms/` discovery directory,
3-line fallback in `_create_adapter()`. Symmetric, not novel.
Real A2A peer traffic on staging gated only on running the harness
(`molecule-core/scripts/test-all-runtimes-a2a-e2e.sh`) — script ready,
needs provider keys.
**Path:** Hermes's MODERN plugin system is `hermes_cli/plugins.py`
(not the older `plugins/memory/`). It already does full discovery
across user dir + project dir + pip entry_points (group:
`hermes_agent.plugins`) for tools / hooks / CLI commands / slash
commands / context engines / skills. **Platform adapters are the only
plugin type still hardcoded** (`gateway/run.py:_create_adapter`).
The PR adds three pieces upstream:
1. `PluginContext.register_platform_adapter(name, adapter_class, requirements_check=None)`
2. `GatewayConfig.plugin_platforms` populated by `from_dict` for
plugin-claimed names
3. `GatewayRunner._create_plugin_adapter(name, config)` boot-path
fallback
Plus a `PluginPlatformIdentifier` helper class so plugin adapters can
satisfy `BasePlatformAdapter.__init__(config, platform: Platform)`
without extending the closed Platform enum.
Total: ~100 LOC upstream change. External plugin then ships as
`hermes-platform-molecule-a2a` via `pip install` + entry_points — no
fork needed in production.
**Artifacts landed:**
- `docs/integrations/hermes-platform-plugins-upstream-pr.md` — full
PR draft including problem, prior art, proposal, code shape,
backward compat, test plan, and four open questions to resolve in
Discord before submitting.
- **Upstream PR**: [NousResearch/hermes-agent#18775](https://github.com/NousResearch/hermes-agent/pull/18775)
— 5 commits on `feat/platform-adapter-plugins`: registration
surface, config + boot wiring, `PluginPlatformIdentifier` helper,
`resolve_platform_id` for plugin-platform-safe deserialization, and
`self.adapters[adapter.platform]` keying fix (caught by real-subprocess
test before merge — see below).
- **Plugin package**: [Molecule-AI/hermes-platform-molecule-a2a](https://github.com/Molecule-AI/hermes-platform-molecule-a2a)
v0.1.0 — public, MIT-licensed. 11 unit tests + 8 in-process E2E
+ 4 real-subprocess E2E checkpoints all green.
- **Workspace template patch**: [Molecule-AI/molecule-ai-workspace-template-hermes#32](https://github.com/Molecule-AI/molecule-ai-workspace-template-hermes/pull/32)
— Dockerfile installs the patched fork + plugin into the hermes
installer's venv; start.sh seeds `platforms.molecule-a2a` config
stanza. Pre-demo deliberately install-only; adapter.py rewrite to
USE the plugin path is a separate post-demo PR.
- Real adapter package at `~/hermes-platform-molecule-a2a/`:
- `pyproject.toml` with `hermes_agent.plugins` entry point
- `hermes_platform_molecule_a2a/adapter.py`
`MoleculeA2APlatformAdapter(BasePlatformAdapter)` with HTTP
listener (aiohttp), inbound `MessageEvent(internal=True)` dispatch,
outbound `send()` POST to per-chat callback URL, optional shared
secret enforcement
- `tests/test_adapter.py`**11/11 unit tests pass** covering plugin
entry-point shape, lifecycle, inbound auth, outbound routing
- `scripts/e2e_validate.py` — production-path validation (entry
points → registry → GatewayConfig → boot → HTTP roundtrip), all
7 checkpoints pass
- `docs/integrations/hermes-platform-plugins-upstream-pr.md` — PR
draft including problem, prior art, proposal, code shape, backward
compat, test plan, and open questions.
- `.hermes-validation/test_register_platform_adapter.py` — local
9-check validation of the patched fork via the user-dir discovery
path (complementary to the entry-points path tested by the package).
**Why no short-term polling shim:** earlier framing was wrong. Molecule
runtime already polls the inbox via `wait_for_message` per turn; each
@ -77,23 +133,31 @@ conversation across turns because chat/completions is stateless), not
push latency. That gap is solved by the upstream PR; no
intermediate shim earns its complexity.
**Remaining (task #83):**
1. Reach out in Nous Research Discord to validate open questions
(Platform enum-vs-string refactor, naming, example-plugin scope).
2. Submit PR to `NousResearch/hermes-agent`. **Requires user
confirmation** — opening an upstream PR is an action visible to
others.
3. Once merged: ship `hermes-platform-molecule-a2a` as the first
external consumer, bump our hermes workspace template to enable
it, remove any transitional code.
**Remaining:**
1. **Upstream PR review/merge** (NousResearch/hermes-agent#18775). On
maintainers — typical OSS review lag.
2. **Workspace template merge + image republish** (PR #32). Once
merged, `publish-runtime.yml` regenerates the hermes workspace image
with the plugin baked in. Safe to merge as-is — install-only, no
behavior change for current workspaces.
3. **Runtime adapter rewrite** (task #87 equivalent for hermes).
`molecule-ai-workspace-template-hermes/adapter.py` currently proxies
A2A → `/v1/chat/completions`. Switching to POST `/a2a/inbound` is
what unlocks single-session continuity. **Post-demo timing**
(touches a working live integration).
4. **Real A2A peer traffic E2E** (task #86): boot a real workspace
from the republished image, send peer A2A message from another
workspace, observe single-session reply. Gated on items 2 + 3.
---
## Codex (OpenAI Codex CLI)
**Status:** Template structurally complete (12 files, 12/12 tests passing,
validated against real codex-cli 0.72.0). Awaiting molecule-core
registry integration + E2E.
**Status:** Template SHIPPED. Repo live at
[`Molecule-AI/molecule-ai-workspace-template-codex`](https://github.com/Molecule-AI/molecule-ai-workspace-template-codex)
(14 files, 1411 LOC, 12/12 tests). molecule-core registration in
[PR #2512](https://github.com/Molecule-AI/molecule-core/pull/2512).
E2E with real A2A traffic remains.
**Path:** Persistent `codex app-server` stdio JSON-RPC client
(NDJSON-framed, v2 protocol). One app-server child per workspace