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.
This commit is contained in:
@@ -0,0 +1,214 @@
|
||||
# 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*
|
||||
Reference in New Issue
Block a user