Skip to content

feat: ship pay_mcp hermes plugin by default (CopyPlugins) + bump hermes v2026.6.19#657

Draft
bussyjd wants to merge 2 commits into
mainfrom
feat/pay-mcp-embed-default
Draft

feat: ship pay_mcp hermes plugin by default (CopyPlugins) + bump hermes v2026.6.19#657
bussyjd wants to merge 2 commits into
mainfrom
feat/pay-mcp-embed-default

Conversation

@bussyjd

@bussyjd bussyjd commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

Ship the pay_mcp hermes plugin by default

This makes obol-stack embed and seed the pay_mcp hermes plugin onto every
agent — the same way embedded skills are seeded — so an agent can settle paid
(x402) MCP tools out of the box, with no image rebuild and the user's profile
left untouched. obol pins the upstream nousresearch/hermes-agent image and
doesn't build it, so the plugin can't ride in via the image; we vendor it into
the binary and drop it into the agent's user-plugins dir on stack-up / reconcile.

The branch also folds in the nousresearch/hermes-agent v2026.6.5 → v2026.6.19
image bump (the image these agents run).

What pay_mcp does

A paid MCP server answers a tool call with an in-band x402 402 (the
amount/asset/payTo travels in _meta). The plugin detects it, signs an
EIP-3009 exact USDC voucher through the pod's remote-signer (no key
in-process), retries with the voucher in _meta["x402/payment"], and returns
the paid result — so a 402 never surfaces to the agent. Settlement requires an
in-chat confirm by default; an opt-in unattended policy (per-call + session caps

  • asset/recipient allowlists) lets autonomous turns settle.

How it integrates now

 obol stack up / agent reconcile
    │  agent image (pinned, UPSTREAM):  nousresearch/hermes-agent:v2026.6.19
    │  env already on the agent:        REMOTE_SIGNER_URL=http://remote-signer:9000
    │
    │  MASTER agent (internal/hermes)            SELL sub-agent (serviceoffercontroller
    │   syncObolPlugins → CopyPlugins             + agentcrd) SeedHostPlugins → CopyPlugins
    │   generateConfig: plugins.enabled            renderHermesConfig: plugins.enabled
    │        │                                          │  (signer only if wallet-bearing)
    ├─ CopySkills   ──► /data/.hermes/obol-skills                      ✓ exists already
    └─ CopyPlugins  ──► /data/.hermes/plugins/pay_mcp/                 ◀── NEW
                                          │
                          agent boots → PluginManager scans ~/.hermes/plugins
                                          │ pay_mcp is source=user → gated by plugins.enabled
                                          │ config seeds plugins.enabled:[pay_mcp] → LOADS
                                          │ register() sees REMOTE_SIGNER_URL → builds rails
                                          │ stock image (no core seam) → recovery.install()
                                          ▼
                          every MCP tool call now auto-pays (confirm default; unattended opt-in)

The plugin stays inert unless a signer is configured, so seeding it
everywhere is safe; sell sub-agents only get REMOTE_SIGNER_URL when
wallet-bearing.

Two non-obvious things this had to get right

  1. User-seeded plugins are opt-in. kind: backend only auto-loads when the
    plugin is bundled in the image; a plugin seeded into ~/.hermes/plugins/
    is source=user and must be named in plugins.enabled. So seeding the
    files isn't enough — the generated config also writes
    plugins.enabled: [pay_mcp] on both agent paths.
  2. Relative imports. Hermes loads a user-dir plugin under the synthetic
    package name hermes_plugins.pay_mcp, and a stock image has no bundled
    plugins.pay_mcp to satisfy an absolute self-import. The plugin's
    intra-package imports were converted to relative (from . import x402) so it
    loads identically whether bundled or user-seeded. (Upstream fix:
    bussyjd/hermes-agent@feat/pay-mcp-plugin; locked by
    tests/plugins/test_pay_mcp_userdir_load.py.)

Changes

  • internal/embedCopyPlugins + GetEmbeddedPluginNames (mirror
    CopySkills); vendored plugin under internal/embed/plugins/pay_mcp/
    (VENDORED.md records provenance + the relative-import invariant).
  • internal/hermes (master agent) — syncObolPlugins seeds
    $HERMES_HOME/plugins; generateConfig adds plugins.enabled: [pay_mcp].
  • internal/serviceoffercontroller + internal/agentcrd (sell sub-agents) —
    renderHermesConfig enables pay_mcp; SeedHostPlugins seeds it on the host PVC.
  • Image pin v2026.6.5 → v2026.6.19.

Test proofs

obol-stack (Go) — all green:

internal/embed                 TestCopyPlugins_SeedsPayMCP / _NoPycache /
                               _RelativeImportsOnly / _ManifestNamesPayMCP /
                               _PreservesUserPlugins ; TestGetEmbeddedPluginNames
internal/hermes                TestSyncObolPlugins_SeedsPayMCP ;
                               TestGenerateConfig_EnablesPayMCPPlugin
internal/serviceoffercontroller TestRenderHermesConfig_EnablesPayMCPPlugin
internal/agentcrd              TestSeedHostPlugins_SeedsAndPreserves ;
                               TestSeedHostFiles_FreshAgent (now asserts pay_mcp)

hermes-agent (Python) — inject-by-default integration, GREEN:
scripts/validate_pay_mcp_injection.py drives the real PluginManager
against an obol-seeded agent home (CopyPlugins layout + plugins.enabled) while
simulating a stock image (empty bundled dir + blocked core seam):

[found] source=user kind=backend enabled=True error=None
[wire]  self-wired: ClientSession.call_tool wrapped, rails=['obol']
[PASS]  obol-seeded layout -> PluginManager discovered + ENABLED + LOADED pay_mcp,
        built rails, and self-wired ClientSession.call_tool on a stock image.

On-chain settlement (spark1, Base-Sepolia USDC) — 3 prior smokes GREEN:
crypto 0xba99dd88…, autonomous 0xb22f8d84…, self-wire 0xc5a8835f…
(each Bob→Alice 1000 atomic USDC, balances verified exact). The runtime
settlement is already proven on-chain; this PR adds the seeding + enablement.

Remaining

A live spark1 master-agent redeploy with the rebuilt obol binary is the one
optional end-to-end confirmation not run here; every code path is covered by the
Go tests + the real-PluginManager integration validation + the prior on-chain
smokes.

bussyjd added 2 commits June 21, 2026 11:44
Latest hermes-agent release (2026-06-19; image verified published on Docker Hub).
Bumps both pinned constants (agent_render.go defaultHermesImage, hermes.go
defaultImage). Per the agent-contract integration test header, re-run
flow-16-sell-agent.sh + internal/agentcrd contract test (bundled-skills-empty +
marker invariants) before merging.
Embed the pay_mcp plugin in the obol-stack binary and seed it onto every
agent's user-plugins dir, mirroring how embedded skills are seeded — so an
agent can settle paid (x402) MCP tools out of the box. obol pins the upstream
hermes image and can't ride the plugin in via the image, so we vendor + seed.

- internal/embed: CopyPlugins + GetEmbeddedPluginNames (mirror CopySkills),
  embed internal/embed/plugins/pay_mcp (vendored, relative-imports invariant).
- internal/hermes (master agent): syncObolPlugins seeds $HERMES_HOME/plugins;
  generateConfig sets plugins.enabled: [pay_mcp] (user plugins are opt-in).
- internal/serviceoffercontroller + internal/agentcrd (sell sub-agents):
  renderHermesConfig enables pay_mcp; SeedHostPlugins seeds it on the host PVC.
  Inert unless the pod has REMOTE_SIGNER_URL (wallet-bearing agents only).
- Tests: CopyPlugins (seed, no __pycache__, relative-imports-only, manifest
  name, preserve user plugins), config-enables-pay_mcp on both agent paths,
  SeedHostPlugins seed+preserve.

Plugin stays inert without a signer, so it's safe to ship everywhere; users
remain free to customize their own profile/plugins.
@bussyjd bussyjd force-pushed the feat/pay-mcp-embed-default branch from 3567648 to 7604d76 Compare June 21, 2026 07:46
@bussyjd bussyjd marked this pull request as draft June 21, 2026 13:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant