215 lines
9.6 KiB
Markdown
215 lines
9.6 KiB
Markdown
# 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)|
|