# 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 ```bash 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) ```bash # 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: ```bash sudo systemctl status openclaw-web-gateway.service sudo systemctl restart openclaw-web-gateway.service journalctl -u openclaw-web-gateway.service -f ``` Health check: ```bash 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)|