Files
flynn/docs/plans/2026-02-05-phase2-websocket-gateway.md
T
William Valentin f30a8bc318 feat(gateway): add WebSocket gateway with JSON-RPC protocol and auth
Phase 2 of the Flynn roadmap. Adds a WebSocket gateway server that
starts alongside the Telegram bot, providing real-time API access to
the agent, sessions, and tools.

Protocol: JSON-RPC-like (request/response/event) over WebSocket.
8 methods: agent.send, agent.cancel, sessions.list, sessions.history,
sessions.create, tools.list, tools.invoke, system.health.

Auth: Bearer token + Tailscale identity header support.
Session bridge: per-connection agent instances with shared model router.

New files: src/gateway/ (protocol, router, server, auth, session-bridge,
handlers for agent/sessions/tools/system).
57 new tests (181 total), typecheck clean.
2026-02-05 19:11:25 -08:00

215 lines
6.0 KiB
Markdown

# Phase 2: WebSocket Gateway — Implementation Plan
## Goal
Add a WebSocket gateway to the Flynn daemon that allows real-time communication from web clients (and eventually any WS client). The gateway wraps existing daemon components (session manager, agent, tool registry) — no refactoring of existing code.
## Approach
Incremental, additive. Existing Telegram bot and TUI continue working unchanged. The gateway is a new module that starts alongside them.
## Architecture
```
src/gateway/
├── protocol.ts # JSON-RPC message types + event types
├── server.ts # WebSocket server (ws library), connection lifecycle
├── router.ts # Method routing → handler dispatch
├── auth.ts # Token auth + Tailscale identity headers
├── session-bridge.ts # Maps WS client connections to agent sessions
└── handlers/
├── agent.ts # agent.send (streaming), agent.cancel, agent.status
├── sessions.ts # sessions.list, sessions.history, sessions.create
├── tools.ts # tools.list, tools.invoke
└── system.ts # system.health, system.info
```
## Protocol
JSON-RPC-like over WebSocket (not full JSON-RPC 2.0 — simpler):
```typescript
// Client → Server
interface GatewayRequest {
id: number; // Client-assigned request ID
method: string; // e.g. "agent.send"
params: object; // Method-specific parameters
}
// Server → Client (success)
interface GatewayResponse {
id: number; // Matches request ID
result: object; // Method-specific result
}
// Server → Client (error)
interface GatewayError {
id: number; // Matches request ID
error: {
code: number; // Error code (negative = protocol, positive = app)
message: string; // Human-readable description
};
}
// Server → Client (streaming event, multiple per request)
interface GatewayEvent {
id: number; // Matches originating request ID
event: string; // Event type name
data: object; // Event-specific payload
}
```
### Event Types (for agent.send streaming)
| Event | Data | When |
|-------|------|------|
| `content` | `{ text: string }` | Text chunk from model |
| `tool_start` | `{ tool: string, args: object }` | Tool execution beginning |
| `tool_end` | `{ tool: string, result: { success, output, error? } }` | Tool execution complete |
| `done` | `{ content: string, usage: { inputTokens, outputTokens } }` | Final response |
| `error` | `{ code: number, message: string }` | Error during processing |
### Error Codes
| Code | Meaning |
|------|---------|
| -1 | Parse error (invalid JSON) |
| -2 | Invalid request (missing id/method) |
| -3 | Method not found |
| -4 | Authentication required |
| -5 | Authentication failed |
| 1 | Session not found |
| 2 | Tool not found |
| 3 | Agent busy (already processing) |
| 4 | Request cancelled |
## Methods
### agent.send
Send a message to the agent and receive streaming response.
```
Params: { message: string, sessionId?: string }
Events: content, tool_start, tool_end, done, error
Response: (none — final state sent as "done" event)
```
### agent.cancel
Cancel the currently running agent request.
```
Params: { sessionId?: string }
Response: { cancelled: boolean }
```
### sessions.list
List all active sessions.
```
Params: {}
Response: { sessions: [{ id: string, messageCount: number }] }
```
### sessions.history
Get message history for a session.
```
Params: { sessionId: string, limit?: number, offset?: number }
Response: { messages: Message[], total: number }
```
### sessions.create
Create a new session.
```
Params: { sessionId?: string }
Response: { sessionId: string }
```
### tools.list
List all registered tools.
```
Params: {}
Response: { tools: [{ name, description, inputSchema }] }
```
### tools.invoke
Directly invoke a tool (bypasses agent).
```
Params: { tool: string, args: object }
Response: { success: boolean, output: string, error?: string }
```
### system.health
Health check.
```
Params: {}
Response: { status: "ok", uptime: number, version: string, sessions: number, tools: number }
```
## Session Bridge
Each WebSocket connection is associated with a session:
1. Client connects → assigned default session ID `ws:{connectionId}`
2. Client can specify `sessionId` in `agent.send` to use a named session
3. Sessions are created on demand via SessionManager
4. Multiple WS clients can share a session (e.g. multiple browser tabs)
5. Disconnection does NOT destroy the session (persistence via SQLite)
## Auth (Phase 2b)
Two auth modes (checked in order):
1. **Token auth**: `Authorization: Bearer <token>` header on WS upgrade, or `{ method: "auth", params: { token: "..." } }` as first message
2. **Tailscale identity**: `Tailscale-User-Login` header set by Tailscale Funnel/proxy
Config addition:
```yaml
server:
port: 18800
auth:
token: "optional-static-token" # If set, required for all WS connections
tailscale_identity: true # Trust Tailscale-User-Login header
```
No auth initially (Phase 2a) — gateway only listens on localhost.
## Implementation Order
1. `protocol.ts` — Types only, no runtime code
2. `router.ts` — Method dispatch (pure function, easy to test)
3. `session-bridge.ts` — Client-to-session mapping
4. `handlers/system.ts` — Simplest handler, proves the pattern
5. `handlers/sessions.ts` — Session listing/history
6. `handlers/tools.ts` — Tool listing/invocation
7. `handlers/agent.ts` — The main handler (streaming, tool events)
8. `server.ts` — WebSocket server, ties everything together
9. Wire into `daemon/index.ts`
10. Tests throughout
## Dependencies
Need `ws` package:
```bash
pnpm add ws
pnpm add -D @types/ws
```
## Test Strategy
- Unit tests for protocol message validation
- Unit tests for router dispatch
- Unit tests for each handler (mock session manager, agent, tool registry)
- Integration test: WS client → server → handler → response
- Test streaming events for agent.send
---
*Plan Version: 1.0*
*Created: 2026-02-05*
*Parent: docs/plans/2026-02-05-openclaw-parity-design.md Phase 2*