Hermes Backend
WebSocket gateway between browser clients and the OpenClaw agent runtime.
Stack
- Bun — runtime, HTTP server, WebSocket server (native, no npm deps)
- TypeScript — all source files, run directly by Bun (no compile step)
Files
| File |
Role |
server.ts |
Main entry — HTTP + WebSocket via Bun.serve |
gateway.ts |
Upstream connection to OpenClaw gateway |
auth.ts |
Token auth, session tokens, OTP |
session-sm.ts |
Per-connection state machine |
session-watcher.ts |
JSONL session tail + event dispatch |
session-watcher.ts |
JSONL session tail + prev session lookup |
hud-builder.ts |
HUD event factory, result builders |
message-filter.ts |
Filter sensitive values from tool output |
system-access.ts |
System access request/approve flow |
mcp/index.ts |
MCP Streamable HTTP server, auth, routing |
mcp/dev.ts |
Health, subscribe, push_state, chat tools |
mcp/events.ts |
In-memory event queue + long-poll |
mcp/system.ts |
Start/stop/restart/deploy tools |
mcp/fs.ts |
Remote file read/write/edit/grep/ls |
mcp/deck.ts |
Nextcloud Deck integration |
mcp/takeover.ts |
Browser takeover bridge |
HTTP Endpoints (added in 0.6.x)
| Endpoint |
Auth |
Purpose |
POST /api/tts |
session |
ElevenLabs TTS generation |
GET /api/session-history |
session |
Previous session messages |
POST /api/dev/counter |
session |
Push counter events to MCP |
POST /api/dev/broadcast |
session |
Push WS state to browsers |
Dev Setup
cd projects/hermes/backend
NODE_TLS_REJECT_UNAUTHORIZED=0 PORT=3003 ~/.bun/bin/bun --watch run server.ts
--watch — auto-restarts on file change (rsync push → instant reload)
NODE_TLS_REJECT_UNAUTHORIZED=0 — required because the OpenClaw gateway uses a self-signed TLS cert
- Default port:
3001 (prod) / 3003 (dev)
- Requires a running OpenClaw gateway on
:18789
Dev workflow (from Titan host)
# Pull remote → local mirror
rsync -avz --exclude='node_modules' --exclude='dist' --exclude='.git' \
openclaw:~/.openclaw/workspace-titan/projects/hermes/ \
/d/ClaudeCode/Titan/Openclaw/workspace-titan/projects/hermes/
# Edit locally (Claude Code Read/Edit tools — no escaping issues)
# Push local → remote (bun --watch restarts BE, Vite HMR updates FE)
rsync -avz --exclude='node_modules' --exclude='dist' --exclude='.git' \
/d/ClaudeCode/Titan/Openclaw/workspace-titan/projects/hermes/backend/ \
openclaw:~/.openclaw/workspace-titan/projects/hermes/backend/
Production
Managed by systemd:
sudo systemctl status openclaw-web-gateway.service
sudo systemctl restart openclaw-web-gateway.service
journalctl -u openclaw-web-gateway.service -f
Health check:
curl https://chat.jqxp.org/health
Architecture
Browser (WebSocket / HTTP)
│
▼
server.ts ─── Bun.serve()
│
├── HTTP routes
│ /health, /agents
│ /api/auth, /api/auth/verify, /api/auth/logout
│ /api/viewer/token, /api/viewer/tree, /api/viewer/file
│
└── WebSocket (/ws)
│
├── auth.ts token validation, session tokens
├── gateway.ts RPC → openclaw gateway (:18789, WSS)
├── session-sm.ts state: IDLE / AGENT_RUNNING / HANDOVER_* / SWITCHING
└── session-watcher.ts tails .jsonl, emits events to WS client
WebSocket Protocol
Client → Server
| Type |
Auth |
Description |
connect |
No |
Identify by username |
auth |
No |
Identify by session/static token |
message |
Yes |
Send message to agent |
stop |
Yes |
Request agent stop |
kill |
Yes |
Kill agent turn |
switch_agent |
Yes |
Switch to different agent |
handover_request |
Yes |
Ask agent to write HANDOVER.md |
new |
Yes |
Reset session |
new_with_handover |
Yes |
Handover then reset |
cancel_handover |
Yes |
Cancel pending handover |
ping |
No |
Keepalive |
stats_request |
No |
OpenRouter credits + model info |
disco_request |
Yes |
Disconnect gateway (triggers reconnect) |
disco_chat_request |
Yes |
Close WebSocket |
Server → Client
| Type |
Description |
ready |
Auth success — session info, agents list |
session_state |
State machine transition |
sent |
Message accepted by gateway |
delta |
Streaming text chunk from agent |
done |
Agent turn complete |
tool |
Tool call or result (action: call/result) |
finance_update |
Per-turn cost estimate |
handover_context |
HANDOVER.md content on connect |
handover_done |
Handover written, content included |
handover_writing |
Handover in progress |
new_ok |
Session reset acknowledged |
switch_ok |
Agent switch confirmed |
viewer_file_changed |
Watched file changed (viewer feature) |
stats |
OpenRouter credit + model data |
pong |
Ping reply |
killed / stopped |
Agent turn terminated |
error |
Error with code and message |
HTTP Endpoints
| Method |
Path |
Auth |
Description |
| GET |
/health |
None |
Status, version, gateway state |
| GET |
/agents |
None |
Agent list with models |
| POST |
/api/auth |
None |
Login with static token |
| POST |
/api/auth/verify |
None |
Verify OTP challenge |
| POST |
/api/auth/logout |
Session |
Revoke session token |
| POST |
/api/viewer/token |
Session |
Issue fstoken for file viewer |
| GET |
/api/viewer/tree |
fstoken |
List files/dirs in viewer root |
| GET |
/api/viewer/file |
fstoken |
Serve file content or PDF |
| HEAD |
/api/viewer/file |
fstoken |
Check file existence/type |
Viewer
Read-only file browser exposed over HTTP. Roots:
| Key |
Path |
shared |
/home/openclaw/.openclaw/shared |
titan |
/home/openclaw/.openclaw/workspace-titan |
adoree |
/home/openclaw/.openclaw/workspace-adoree |
alfred |
/home/openclaw/.openclaw/workspace-alfred |
ash |
/home/openclaw/.openclaw/workspace-ash |
eras |
/home/openclaw/.openclaw/workspace-eras |
willi |
/home/openclaw/.openclaw/workspace-willi |
Allowed extensions: .md .txt .ts .js .json .sh .py .pdf .css .woff2 .ttf .otf
Max file size: 2MB. Live reload via fs.watch pushed over WebSocket (viewer_file_changed).
Session State Machine
IDLE
├─ message → AGENT_RUNNING
├─ handover_request → HANDOVER_PENDING
└─ new / new_with_handover → SWITCHING
AGENT_RUNNING
├─ agent_done → IDLE
├─ stop / kill → IDLE
└─ send_error → IDLE
HANDOVER_PENDING
├─ handover_written → HANDOVER_DONE
├─ handover_error → IDLE
└─ cancel_handover → IDLE
HANDOVER_DONE
└─ cancel_handover → IDLE
SWITCHING
├─ switch_ready → IDLE
└─ new_greeting → AGENT_RUNNING
Environment Variables
| Variable |
Default |
Description |
PORT |
3001 |
HTTP/WS listen port |
GATEWAY_HOST |
10.0.0.10 |
OpenClaw gateway host |
GATEWAY_PORT |
18789 |
OpenClaw gateway port |
GATEWAY_TOKEN |
(hardcoded) |
Auth token for gateway connection |
NODE_TLS_REJECT_UNAUTHORIZED |
1 (verify) |
Set to 0 for self-signed certs |
SSL_KEY |
../web-frontend/ssl/key.pem |
TLS key for HTTPS (optional) |
SSL_CERT |
../web-frontend/ssl/cert.pem |
TLS cert for HTTPS (optional) |