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,59 @@
|
||||
import type { GatewayRequest, OutboundMessage } from '../protocol.js';
|
||||
import { makeResponse, makeError, ErrorCode } from '../protocol.js';
|
||||
import type { SessionManager } from '../../session/manager.js';
|
||||
|
||||
export interface SessionHandlerDeps {
|
||||
sessionManager: SessionManager;
|
||||
}
|
||||
|
||||
export function createSessionHandlers(deps: SessionHandlerDeps) {
|
||||
return {
|
||||
'sessions.list': async (request: GatewayRequest): Promise<OutboundMessage> => {
|
||||
const sessionIds = deps.sessionManager.listSessions();
|
||||
const sessions = sessionIds.map(id => ({
|
||||
id,
|
||||
messageCount: deps.sessionManager.getSession(
|
||||
id.split(':')[0],
|
||||
id.split(':').slice(1).join(':')
|
||||
).getHistory().length,
|
||||
}));
|
||||
return makeResponse(request.id, { sessions });
|
||||
},
|
||||
|
||||
'sessions.history': async (request: GatewayRequest): Promise<OutboundMessage> => {
|
||||
const params = request.params as { sessionId?: string; limit?: number; offset?: number } | undefined;
|
||||
if (!params?.sessionId) {
|
||||
return makeError(request.id, ErrorCode.InvalidRequest, 'sessionId is required');
|
||||
}
|
||||
|
||||
const { sessionId, limit, offset } = params;
|
||||
const parts = sessionId.split(':');
|
||||
const frontend = parts[0];
|
||||
const userId = parts.slice(1).join(':');
|
||||
const session = deps.sessionManager.getSession(frontend, userId);
|
||||
const allMessages = session.getHistory();
|
||||
|
||||
const start = offset ?? 0;
|
||||
const end = limit ? start + limit : allMessages.length;
|
||||
const messages = allMessages.slice(start, end);
|
||||
|
||||
return makeResponse(request.id, {
|
||||
messages,
|
||||
total: allMessages.length,
|
||||
});
|
||||
},
|
||||
|
||||
'sessions.create': async (request: GatewayRequest): Promise<OutboundMessage> => {
|
||||
const params = request.params as { sessionId?: string } | undefined;
|
||||
const sessionId = params?.sessionId ?? `ws:${Date.now()}`;
|
||||
const parts = sessionId.split(':');
|
||||
const frontend = parts[0];
|
||||
const userId = parts.slice(1).join(':');
|
||||
|
||||
// Creating a session via getSession is idempotent
|
||||
deps.sessionManager.getSession(frontend, userId);
|
||||
|
||||
return makeResponse(request.id, { sessionId });
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user