Staging E2E for PR #32 surfaced a workspace boot failure: the deployed
image's hermes gateway never bound :8645, so adapter.setup()'s
/a2a/health probe got httpx.ConnectError and the workspace went
status=failed at ~498s.
Root cause is image-side install/discovery of the molecule-a2a plugin,
NOT the executor wire shape. Local scripts/e2e_full_chain.py runs
against a venv where I'd already installed the plugin manually — it
didn't catch the deployment-shape divergence.
Flip the default off to restore the legacy /v1/chat/completions
fallback (no session continuity, but works). Plugin path stays
opt-in via MOLECULE_A2A_PLATFORM_ENABLED=true so debugging can
continue per-workspace without rolling the whole image again.
Re-enabling will require:
- An image-build smoke test that verifies pip show
hermes-platform-molecule-a2a + hermes config show inside the
built container (filed separately)
- Verifying the molecule-a2a config stanza actually lands in
~/.hermes/config.yaml inside the running container
Tests updated: 37 pass. Plugin-path tests now opt-in via the helper's
default; default-detection test asserts the new chat_completions
fallback.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the synchronous /v1/chat/completions proxy with an async
plugin-path executor that earns single-session continuity for peer
agents.
Behavior:
- Default: POST each A2A turn to the in-container hermes plugin's
/a2a/inbound; await the agent reply via an aiohttp callback server
inside the executor. The plugin POSTs hermes's reply back to the
callback server, correlated by message_id, which resolves the
awaiting Future and emits on the A2A queue.
- Fallback: MOLECULE_A2A_PLATFORM_ENABLED=false reverts to the
legacy /v1/chat/completions transport — same behavior as before
this commit. Lets operators flip the path off if the plugin path
misbehaves in production.
Wire shape:
- Plugin's adapter.send(chat_id, content, reply_to, metadata)
becomes POST <callback_url> with the same fields.
- Correlation is by reply_to (= the inbound message_id), not by
chat_id — two in-flight messages on the same chat would race on
the latter.
- Optional MOLECULE_A2A_PLATFORM_SHARED_SECRET is sent on outbound
POSTs and required on inbound replies.
Tests: 36 unit tests, 98% combined coverage on adapter.py + executor.py.
Covers lifecycle (start/stop/idempotent), happy path (round-trip
through stub plugin), error paths (POST failure, reply timeout, late
delivery for unknown message_id, malformed JSON, missing fields),
auth (shared_secret enforcement both directions), fallback (chat
completions HTTP error, unreachable port, junk response shape), and
chat_id derivation precedence.
Real-LLM E2E remains gated on docker image republish + workspace
provisioning + LLM key — the unit tests bound the wire-shape risk
and the existing scripts/e2e_real_hermes_subprocess.py in
hermes-platform-molecule-a2a covers the plugin side end-to-end against
a real `hermes gateway run` subprocess.
a2a-sdk v1 removed `a2a.utils.new_agent_text_message`. The replacement
lives in `a2a.helpers` with a flatter name: `new_text_message(text,
role=Role.agent, ...)`. The "agent" role is now the default, so the
behavior is preserved without passing `role` explicitly at any call
site.
Symptom before this change: every hermes workspace startup died at
adapter.create_executor → import executor.py → ImportError: cannot
import name 'new_agent_text_message' from 'a2a.utils'. Workspace
container restart-loops, status stays in `provisioning` until the
10-min sweep marks it failed.
The molecule-runtime image's a2a_executor.py has the same import
pattern — but it's no longer reachable from any live workspace path
post-#87 (claude-code uses its own executor, hermes is what we're
fixing here, other adapters don't reach a2a_executor either). That
file is dead code that should get cleaned up separately.
Caught by manual provision after pipeline-2 wire-real E2E failed.
The boot smoke gate in molecule-ci publish-template-image only does
`import adapter`, which doesn't transitively load executor.py —
so this slipped through every prior gate. Worth strengthening the
boot smoke to call `adapter.create_executor()` against a stub
config to exercise that path too.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The old template was a thin OpenAI-compat multi-provider dispatcher
that shared the name "hermes" with Nous Research's hermes-agent but
had none of its actual capabilities (skills, memory, tools, learning
loop, multi-platform gateway). Customers picking "Hermes" in canvas
got a stateless chat shim instead of the agent framework they
expected.
This PR rewrites the template to run the real hermes-agent
(github.com/NousResearch/hermes-agent) inside the workspace
container:
- Dockerfile installs hermes-agent via its upstream install.sh
(same pattern template-claude-code uses for the claude CLI).
- start.sh boots `hermes gateway` with the api_server platform
on 127.0.0.1:8642, waits for /health, then exec's
molecule-runtime on :8000.
- adapter.py / executor.py collapse to a thin A2A proxy that
forwards every incoming message to /v1/chat/completions on
the local gateway and returns the response on the A2A queue.
- providers.py + escalation.py deleted — hermes-agent owns
provider selection (`hermes model`), its own skill/memory loop
supersedes escalation.
- Env vars unchanged: HERMES_API_KEY, OPENROUTER_API_KEY,
ANTHROPIC_API_KEY, OPENAI_API_KEY, GEMINI_API_KEY, MINIMAX_API_KEY
are all forwarded into ~/.hermes/.env at boot.
All planning + rationale lives in this repo under docs/:
- docs/PLANNING.md — why, scope, phases, risks, success criteria
- docs/ARCHITECTURE.md — port map, boot sequence, request flow,
what the bridge deliberately does NOT do
- docs/MIGRATION.md — v1.x → v2.0.0 behaviour changes (no
customer migration needed, v1.x was CI-canary-only)
- docs/CONFIGURATION.md — model picking, persistence, gateway
restart, inspection, timeouts
Net -195 lines of code for a massive capability upgrade.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Change relative imports (from .executor, from .providers) to absolute
since files are copied flat to /app/ in the Docker image
- Patch molecule_runtime.preflight.SUPPORTED_RUNTIMES at build time to
include 'hermes' and 'gemini-cli' (PyPI package v0.1.0 doesn't know
about these runtimes yet)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>