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,119 @@
|
||||
// Gateway protocol types — JSON-RPC-like messages over WebSocket.
|
||||
|
||||
// ── Client → Server ────────────────────────────────────────────
|
||||
|
||||
export interface GatewayRequest {
|
||||
id: number;
|
||||
method: string;
|
||||
params?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
// ── Server → Client ────────────────────────────────────────────
|
||||
|
||||
export interface GatewayResponse {
|
||||
id: number;
|
||||
result: unknown;
|
||||
}
|
||||
|
||||
export interface GatewayError {
|
||||
id: number;
|
||||
error: {
|
||||
code: ErrorCode;
|
||||
message: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface GatewayEvent {
|
||||
id: number;
|
||||
event: EventType;
|
||||
data: unknown;
|
||||
}
|
||||
|
||||
// ── Event types emitted during agent.send ──────────────────────
|
||||
|
||||
export type EventType =
|
||||
| 'content'
|
||||
| 'tool_start'
|
||||
| 'tool_end'
|
||||
| 'done'
|
||||
| 'error';
|
||||
|
||||
export interface ContentEventData {
|
||||
text: string;
|
||||
}
|
||||
|
||||
export interface ToolStartEventData {
|
||||
tool: string;
|
||||
args: unknown;
|
||||
}
|
||||
|
||||
export interface ToolEndEventData {
|
||||
tool: string;
|
||||
result: {
|
||||
success: boolean;
|
||||
output: string;
|
||||
error?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface DoneEventData {
|
||||
content: string;
|
||||
}
|
||||
|
||||
export interface ErrorEventData {
|
||||
code: ErrorCode;
|
||||
message: string;
|
||||
}
|
||||
|
||||
// ── Error codes ────────────────────────────────────────────────
|
||||
|
||||
export enum ErrorCode {
|
||||
ParseError = -1,
|
||||
InvalidRequest = -2,
|
||||
MethodNotFound = -3,
|
||||
AuthRequired = -4,
|
||||
AuthFailed = -5,
|
||||
SessionNotFound = 1,
|
||||
ToolNotFound = 2,
|
||||
AgentBusy = 3,
|
||||
RequestCancelled = 4,
|
||||
InternalError = 5,
|
||||
}
|
||||
|
||||
// ── Outbound message (union of all server → client types) ──────
|
||||
|
||||
export type OutboundMessage = GatewayResponse | GatewayError | GatewayEvent;
|
||||
|
||||
// ── Validation helpers ─────────────────────────────────────────
|
||||
|
||||
export function isValidRequest(msg: unknown): msg is GatewayRequest {
|
||||
if (typeof msg !== 'object' || msg === null) return false;
|
||||
const obj = msg as Record<string, unknown>;
|
||||
return (
|
||||
typeof obj.id === 'number' &&
|
||||
typeof obj.method === 'string' &&
|
||||
(obj.params === undefined || (typeof obj.params === 'object' && obj.params !== null))
|
||||
);
|
||||
}
|
||||
|
||||
export function parseMessage(raw: string): GatewayRequest | null {
|
||||
try {
|
||||
const parsed = JSON.parse(raw);
|
||||
if (isValidRequest(parsed)) return parsed;
|
||||
return null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function makeResponse(id: number, result: unknown): GatewayResponse {
|
||||
return { id, result };
|
||||
}
|
||||
|
||||
export function makeError(id: number, code: ErrorCode, message: string): GatewayError {
|
||||
return { id, error: { code, message } };
|
||||
}
|
||||
|
||||
export function makeEvent(id: number, event: EventType, data: unknown): GatewayEvent {
|
||||
return { id, event, data };
|
||||
}
|
||||
Reference in New Issue
Block a user