12 unversioned skills now versioned at 1.0.0: agent-communication, ascii-video, external-reasoning-augmentation, jotty-notes-api, minecraft-modpack-server, obsidian, pokemon-player, powerpoint, social-search, songwriting-and-ai-music, workspace-context-organization, youtube-content Total repo: 141 skills across all profile scopes
173 lines
8.9 KiB
Markdown
173 lines
8.9 KiB
Markdown
---
|
||
name: hermes-vscode-remote
|
||
description: Connect Hermes Agent to VS Code when they run on different machines (LXC/remote server + local laptop). Covers ACP stdio-to-WebSocket bridging, API server vs ACP distinction, and profile selection.
|
||
version: 1.0.0
|
||
author: agent
|
||
tags: [hermes, vscode, acp, remote, websocket, bridge]
|
||
---
|
||
|
||
# Hermes to VS Code — Remote Connection
|
||
|
||
When Hermes runs on a remote machine (LXC, VPS, Docker) and VS Code runs on your local laptop, ACP's stdio transport doesn't work directly. This skill covers the bridge setup.
|
||
|
||
## Key Concepts
|
||
|
||
- **ACP (Agent Client Protocol)** — stdio-based JSON-RPC protocol. VS Code launches the agent as a child process. This is the correct path for VS Code integration.
|
||
- **API Server** — HTTP/OpenAI-compatible endpoint (port 8642). For Open WebUI, LobeChat, etc. NOT for VS Code.
|
||
- **Profile selection** — per-process, not per-request. No `?profile=xxx` parameter. Each profile needs its own gateway/ACP process.
|
||
|
||
## Prerequisites (Hermes Side)
|
||
|
||
```bash
|
||
# Install ACP extra
|
||
cd ~/.hermes/hermes-agent
|
||
pip install -e '.[acp]'
|
||
|
||
# Verify
|
||
hermes acp --check # should exit 0
|
||
hermes acp --version
|
||
```
|
||
|
||
## Architecture
|
||
|
||
```
|
||
VS Code (laptop) → websocat (stdio↔ws) → LAN → stdio-to-ws (ws↔stdio) → hermes acp (LXC)
|
||
```
|
||
|
||
## Step 1: Start the Bridge on Hermes Host
|
||
|
||
```bash
|
||
# Install the bridge
|
||
npm install -g @rebornix/stdio-to-ws --prefix ~/.local
|
||
|
||
# Start it (persistent, survives disconnects)
|
||
~/.local/bin/stdio-to-ws -p 8643 --persist --grace-period -1 "hermes -p <profile> acp"
|
||
```
|
||
|
||
Verify: `ss -tlnp | grep 8643` should show listening.
|
||
|
||
### Auto-Start with systemd (Recommended)
|
||
|
||
Create a user service so the bridge survives reboots and crashes:
|
||
|
||
```bash
|
||
# Ensure linger is enabled (so user services survive logout)
|
||
sudo loginctl enable-linger $USER
|
||
```
|
||
|
||
Create `~/.config/systemd/user/hermes-acp-bridge.service`:
|
||
|
||
```ini
|
||
[Unit]
|
||
Description=Hermes ACP stdio-to-ws bridge for VS Code
|
||
After=network-online.target
|
||
Wants=network-online.target
|
||
|
||
[Service]
|
||
Type=simple
|
||
ExecStart=/home/<user>/.local/bin/stdio-to-ws -p 8643 --persist --grace-period -1 "/home/<user>/.local/bin/hermes -p <profile> acp"
|
||
Restart=always
|
||
RestartSec=5
|
||
Environment=HOME=/home/<user>
|
||
Environment=HERMES_HOME=/home/<user>/.hermes
|
||
Environment=PATH=/home/<user>/.local/bin:/usr/local/bin:/usr/bin:/bin
|
||
|
||
[Install]
|
||
WantedBy=default.target
|
||
```
|
||
|
||
Then enable and start:
|
||
|
||
```bash
|
||
systemctl --user daemon-reload
|
||
systemctl --user enable --now hermes-acp-bridge.service
|
||
systemctl --user status hermes-acp-bridge.service # verify
|
||
```
|
||
|
||
A ready-to-copy template is at `templates/hermes-acp-bridge.service`.
|
||
|
||
### Files
|
||
|
||
- `templates/hermes-acp-bridge.service` — copy-paste systemd unit (fill in `<user>`)
|
||
- `scripts/acp_repro.py` — JSON-RPC traffic probe (see Debugging section below)
|
||
|
||
## Step 2: Configure VS Code on Laptop
|
||
|
||
1. Install **ACP Client** extension (`formulahendry.acp-client`)
|
||
2. Install `websocat` (WebSocket ↔ stdio bridge):
|
||
- macOS: `brew install websocat`
|
||
- Or download from https://github.com/vi/websocat/releases
|
||
3. Add to VS Code `settings.json`:
|
||
```json
|
||
{
|
||
"acp.agents": {
|
||
"Hermes (<profile>)": {
|
||
"command": "websocat",
|
||
"args": ["ws://<hermes-host>:8643"]
|
||
}
|
||
}
|
||
}
|
||
```
|
||
4. Open ACP Client panel, select the agent, connect.
|
||
|
||
## Pitfalls
|
||
|
||
- **ACP is stdio-only.** There is no `--host`/`--port` flag on `hermes acp`. The bridge is mandatory for remote use.
|
||
- **API server is the wrong tool for VS Code.** Port 8642 serves OpenAI-format HTTP — VS Code ACP Client doesn't speak it.
|
||
- **Profile is locked at process start.** `hermes -p vscode acp` serves the vscode profile. To switch profiles, stop and restart with a different `-p`.
|
||
- **`API_SERVER_KEY` is required** when binding the API server to `0.0.0.0`. ACP has no such requirement.
|
||
- **npm global install may need `--prefix ~/.local`** if the user lacks root on the Hermes host.
|
||
- **`pip install -e '.[acp]'` may downgrade packages** (openai, requests, etc.) to match Hermes's pinned versions. The dependency conflicts are cosmetic — ACP still works.
|
||
- **EADDRINUSE on systemd restart.** If the bridge was previously started manually (background process) and you switch to systemd, the old process still holds port 8643. Kill it first: `ss -tlnp | grep 8643` to find the PID, then `kill <pid>`. The systemd service will auto-restart and bind successfully.
|
||
- **`spawn hermes ENOENT` in systemd.** systemd's default PATH does not include `~/.local/bin`. Use the full path `/home/<user>/.local/bin/hermes` in `ExecStart=` and add `Environment=PATH=/home/<user>/.local/bin:/usr/local/bin:/usr/bin:/bin`. The symptom is the bridge accepting WebSocket connections but immediately dropping them — check `journalctl --user -u hermes-acp-bridge.service` for the error. The bridge log will show `[Child process error: Error: spawn hermes ENOENT]` right when a client connects.
|
||
- **`systemctl --user` needs linger.** Without `loginctl enable-linger $USER`, user services stop on logout. Check with `loginctl show-user $USER | grep Linger`.
|
||
- **`stdio-to-ws` takes ONLY the first positional arg as the command string.** It uses `parseArgsStringToArgv()` to split that single string. Passing `["hermes", "-p", "vscode", "acp"]` as separate args to the bridge will silently drop everything after `hermes`. Always pass the command as one quoted string: `"hermes -p vscode acp"`. This bites when scripting the bridge from Python (`asyncio.create_subprocess_exec`).
|
||
- **`stdio-to-ws` sends a `{"type":"connected","clientId":"..."}` handshake** before any ACP frames. Custom WebSocket clients must read past this handshake before expecting JSON-RPC responses.
|
||
- **`stdio-to-ws` strips LSP `Content-Length:` headers** from incoming messages (`message.replace(/^Content-Length: \d+\r?\n\r?\n/, "")`). This is a no-op for ACP (newline-delimited JSON, no LSP framing) but worth knowing if you ever wire it to an LSP server.
|
||
- **`acp` library `MessageSender.drain()` blocks the response send.** The `acp` Python SDK (v0.9.0) sends all ACP frames — notifications AND the final JSON-RPC response — through a single `MessageSender` queue. The send loop does `await self._writer.drain()` after every write. When the WebSocket transport is slow, `drain()` blocks indefinitely, queuing every subsequent frame behind it. The final `session/prompt` response (with `stopReason`) gets stuck behind the streaming notifications, so VS Code receives all the text but never gets the signal to clear the "thinking" spinner. Fix: patch `MessageSender._loop()` in the installed `acp` package to bound `drain()` with `asyncio.wait_for(timeout=0.5)`. The payload is already in the writer's internal buffer after `write()`; the timeout just prevents the send loop from waiting forever for the OS pipe to drain. The bytes are still delivered when the consumer reads. File: `/home/<user>/.local/lib/python3.13/site-packages/acp/task/sender.py`. This patch gets overwritten on `pip install --upgrade agent-client-protocol` — re-apply or upstream a fix.
|
||
|
||
## Verification
|
||
|
||
```bash
|
||
# On Hermes host
|
||
ss -tlnp | grep 8643 # bridge listening
|
||
hermes acp --check # ACP deps OK
|
||
|
||
# On laptop
|
||
curl -s http://<hermes-host>:8643/ -H "Upgrade: websocket" -H "Connection: Upgrade" -o /dev/null -w "%{http_code}"
|
||
# Should return 400 (WebSocket upgrade attempted, not a plain HTTP endpoint)
|
||
```
|
||
|
||
## Debugging ACP Behavior ("spinner never ends", "stopReason missing")
|
||
|
||
When VS Code reports weird ACP behavior (stuck "thinking" spinner, missing
|
||
final response, no stopReason), the bug could live in three layers:
|
||
|
||
1. **Agent code** — `acp_adapter/server.py::prompt()` returning `PromptResponse`
|
||
2. **Transport** — stdio-to-ws bridge, websocat, or the WebSocket itself
|
||
3. **VS Code client** — the `formulahendry.acp-client` extension
|
||
|
||
Before suspecting the agent code, isolate the transport. Run the repro script
|
||
that spawns `hermes acp` directly and traces every JSON-RPC frame:
|
||
|
||
```bash
|
||
python3 scripts/acp_repro.py
|
||
python3 scripts/acp_repro.py --prompt "say hi"
|
||
```
|
||
|
||
The script sends `initialize → session/new → session/prompt` and prints every
|
||
notification and response. Look for the final `[RESP id=3]` line — if it shows
|
||
`"stopReason": "end_turn"`, the agent is correct and the bug is in the
|
||
transport or VS Code extension. If it's missing entirely, look at the agent's
|
||
stderr (printed by the script) for stack traces.
|
||
|
||
This probe was the decisive tool when investigating a reported "missing
|
||
stopReason" bug — it proved the agent was sending the response correctly and
|
||
the issue was elsewhere (most likely the WebSocket bridge or the client).
|
||
|
||
## Files
|
||
|
||
- `scripts/acp_repro.py` — ACP JSON-RPC traffic trace (initialize → session/new → session/prompt)
|
||
- `templates/hermes-acp-bridge.service` — systemd user unit for auto-start
|
||
- `references/acp-hang-investigation.md` — root cause and fix for the "ACP spinner never ends" bug: `MessageSender.drain()` in the `acp` SDK blocks indefinitely, queuing the final response behind stuck writes. Includes the `asyncio.wait_for(timeout=0.5)` patch.
|