diff --git a/docs/integrations/hermes-platform-plugins-upstream-pr.md b/docs/integrations/hermes-platform-plugins-upstream-pr.md index 05a13769..8ca096e5 100644 --- a/docs/integrations/hermes-platform-plugins-upstream-pr.md +++ b/docs/integrations/hermes-platform-plugins-upstream-pr.md @@ -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//` + - Project dir: `./.hermes/plugins//` + - 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//` plus user-installed providers in `$HERMES_HOME/plugins//`. 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. 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. 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.5–0.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//`)? +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. diff --git a/docs/integrations/runtime-native-mcp-status.md b/docs/integrations/runtime-native-mcp-status.md index 41d0b044..b322ebc8 100644 --- a/docs/integrations/runtime-native-mcp-status.md +++ b/docs/integrations/runtime-native-mcp-status.md @@ -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//` 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