Files
shivammittal274 7a2a8e09bc feat(agent): add Hermes as 4th ACPX adapter (in-VM container, BrowserOS-managed providers) (#956)
* feat(agent): add Hermes as a 4th ACPX adapter (Phase A)

Adds Hermes Agent (NousResearch/hermes-agent) as a host-process ACPX
adapter, mirroring the Claude Code pattern.

- agent-types.ts: extend AgentAdapter union with 'hermes'
- agent-catalog.ts: add Hermes catalog entry
- lib/agents/hermes/prepare.ts (new): minimal prepare using prepareBrowserosManagedContext
- acpx-agent-adapter.ts: register the adapter
- acpx-runtime.ts: add 'hermes' branch returning 'hermes acp' (host)
- AdapterIcon.tsx: add Hermes icon
- db schema + supporting frontend types/literals updated for the new adapter

Phase A scope: host-process only. Phase A.5 swaps to nerdctl exec
into a Hermes container.

OpenClaw is untouched. Verified by all 6 POC spikes
(plans/features/claude-browseros-hermes-poc/findings.md).

* fix(agent): address Hermes adapter review issues

- NewAgentDialog: add 'hermes' to onValueChange guard so the dropdown
  option actually wires through onRuntimeChange/onHarnessAdapterChange
  (was a no-op before — selecting Hermes silently kept previous value)
- tests/acpx-runtime: add coverage for the new 'hermes' registry branch
- tests/acpx-agent-adapter: fold hermes prepare test into existing file,
  matching the pattern used for claude/codex/openclaw
- Delete tests/lib/agents/hermes-prepare.test.ts (now redundant)
- Reconcile install-mechanism comment between acpx-runtime.ts and
  agent-catalog.ts

* fix(agent): make Hermes adapter actually work end-to-end

Two surgical fixes uncovered while running the Phase A smoke test
through the BrowserOS chat HTTP API:

1. lib/agents/hermes/prepare.ts — seed per-agent HERMES_HOME from
   the user's global ~/.hermes/ on first use. ensureAgentHome only
   writes SOUL.md and MEMORY.md; without seeding config.yaml, .env,
   and auth.json, hermes acp comes up unconfigured and either hangs
   or errors with "No LLM provider configured." Copy is idempotent
   (skip if dest exists) so subsequent prepare calls don't clobber
   per-agent edits.

2. lib/agents/acpx-runtime.ts — wrap the hermes spawn in
   `bash -c "exec hermes acp | tee /dev/null"` to bridge Bun's
   socketpair-based child stdio with Python's asyncio.connect_write_pipe
   (which only drains correctly to a real pipe(2)). Without it, hermes'
   stdout never reaches the harness — verified by inspecting hermes
   process FDs: Bun gives the child unix sockets, asyncio queues writes
   that never become readable on Bun's end. With tee in the middle,
   hermes writes to a real pipe and tee bridges the bytes through the
   socket. Verified 2026-05-06 against hermes-agent 0.12.0 on macOS
   arm64 + Bun 1.3.6.

Smoke-test result with both fixes:
- ACP session created end-to-end
- BrowserOS MCP wired (96 browser tools registered with hermes)
- Reasoning + text streamed back through /agents/:id/sidepanel/chat
- Final stream: text-delta "PONG", finishReason "stop"

Updates the existing acpx-runtime test to assert the new spawn shape
(bash -c, tee /dev/null bridge) so the workaround can't silently regress.

* feat(agent): run Hermes adapter in Lima container (Phase A.5)

Move Hermes ACPX adapter from host-process spawn to running inside
docker.io/nousresearch/hermes-agent:v2026.4.30 in the existing
BrowserOS Lima VM, mirroring the OpenClaw container pattern.

Container lifecycle (api/services/hermes/hermes-container.ts):
- prewarm: ensure VM ready, pull image (or skip if already in
  containerd), start an idle container with /bin/sh -c "exec sleep
  infinity" so the harness can nerdctl exec into it per turn
- Tini bypassed — tini 0.19.0 in upstream image getopt-parses any
  -x token even after PROGRAM, breaking /bin/sh -c
- --add-host host.containers.internal:<vm-gateway> so hermes inside
  the container can reach the BrowserOS HTTP MCP endpoint
- Bind-mount <browserosDir>/vm/hermes/harness onto /data/agents/harness
  so per-agent HERMES_HOME dirs are visible to the container

Spawn (acpx-runtime.ts):
- HermesGatewayAccessor interface (mirrors OpenclawGatewayAccessor)
- resolveHermesAcpCommand builds:
  env LIMA_HOME=... limactl shell --workdir / browseros-vm --
    nerdctl exec -i -e PYTHONUNBUFFERED=1 -e HERMES_HOME=... <container>
    /opt/hermes/.venv/bin/hermes acp
- Absolute path /opt/hermes/.venv/bin/hermes (not bare "hermes") since
  upstream image's PATH is set by its entrypoint script which we
  override to keep the container idle
- Falls back to host-process spawn when no HermesGatewayAccessor wired
  (test path / dev fallback)
- Drops the host-mode bash+tee workaround — limactl/SSH/nerdctl pipe
  chain is sufficient for asyncio's pipe writer

MCP wiring:
- New PreparedAcpxAgentContext.browserosMcpHost field threads through
  prepare → getRuntime → createBrowserosMcpServers
- Hermes prepare sets browserosMcpHost='host.containers.internal' so
  the URL injected into newSession.mcpServers resolves from inside
  the container; other adapters keep '127.0.0.1' default

Per-agent home (lib/agents/hermes/prepare.ts):
- HERMES_HOME points at /data/agents/harness/<agentId>/home (in-container)
- Host-side seedHermesHomeFromGlobal still copies ~/.hermes/{config.yaml,
  .env, auth.json} into the per-agent home; the volume mount makes them
  visible inside the container
- New api/services/hermes/hermes-paths.ts holds host/container path helpers

End-to-end smoke tests against the dev server (clean Lima state):
- Plain text: PONG round-trip via /sidepanel/chat ✓
- Multi-turn context: RUBY-7421 stored + recalled ✓
- Multi-agent isolation: agent 2 doesn't see agent 1's secret ✓
- MCP tool execution: mcp_browseros_browseros_info fires ✓
- Image attachment via /chat: model identifies "Red" from a 128x128 PNG ✓
- Concurrent turns + 409 attachUrl: full attach streams the in-flight
  Pacific Ocean essay turn cleanly ✓
- Cancel midstream + recovery turn: ALIVE response ✓
- Persistence across server restart: agents survive ✓

Companion knowledge doc:
plans/features/claude-browseros-hermes-acp-knowledge.md

* feat(agent): per-agent provider/key for Hermes adapter

Lets users create multiple Hermes agents each with its own provider,
model, and API key. NewAgentDialog now shows provider/model/key fields
inline when 'Hermes' is selected. On submit, the harness writes the
per-agent <browserosDir>/vm/hermes/harness/<agentId>/home/{config.yaml,
.env} directly so the agent has the right config from turn 1 — no
dependency on the user having run `hermes setup` outside BrowserOS.

The existing seedHermesHomeFromGlobal flow remains as a fallback for
agents created without provider fields (e.g. via direct API or with
an existing ~/.hermes/ install).

Backend:
- shared/constants/hermes.ts: HERMES_SUPPORTED_PROVIDERS registry
  (openrouter, anthropic, openai, custom — bedrock follow-up)
- api/services/hermes/hermes-paths.ts: writeHermesPerAgentProvider
- agent-harness-service: writes per-agent config.yaml + .env in
  createAgent when adapter=hermes and apiKey present
- routes/agents.ts: relax modelId catalog validation for adapter=hermes
  (catalog has empty models[] by design; per-agent modelId is free-form)
- tests/agent-harness-service: cover write + skip paths

Frontend:
- HermesProviderFields.tsx (new): provider dropdown, model field, API
  key + optional baseUrl when provider=custom
- NewAgentDialog: render the new fields when adapter=hermes
- agents-page-actions: thread fields through createHarnessAgent
- AgentsPage / agent-harness-types: minor pass-through edits

Smoke-tested end-to-end against the dev server (clean Hermes per-agent
home, no ~/.hermes/ seed): create agent with apiKey + modelId, files
written at the per-agent path with mode 0600, first chat returns the
expected response, all without touching ~/.hermes/.

* feat(agent): source Hermes provider config from BrowserOS LLM providers

Replace the Hermes-specific provider/model/API-key form in New Agent
with a chooser that pulls from the same global LLM providers OpenClaw
uses (Settings → BrowserOS AI). Backend rejects creation with a 400
when the selected provider is missing required fields (apiKey, modelId,
plus baseUrl for openai-compatible) or is not in the Hermes-supported
set; the ~/.hermes/ fallback is removed so Hermes agents always carry
their own per-agent config.
2026-05-07 21:54:36 +05:30
..