"use strict"; /** * RuvBot HTTP Server - Cloud Run Entry Point * * Provides REST API endpoints for RuvBot including: * - Health checks (required for Cloud Run) * - Chat API * - Session management * - Agent management */ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const node_http_1 = require("node:http"); const node_url_1 = require("node:url"); const node_crypto_1 = require("node:crypto"); const node_fs_1 = require("node:fs"); const node_path_1 = require("node:path"); const pino_1 = __importDefault(require("pino")); const RuvBot_js_1 = require("./RuvBot.js"); const AIDefenceGuard_js_1 = require("./security/AIDefenceGuard.js"); const ChatEnhancer_js_1 = require("./core/ChatEnhancer.js"); // ============================================================================ // Configuration // ============================================================================ const PORT = parseInt(process.env.PORT || '8080', 10); const HOST = process.env.HOST || '0.0.0.0'; const NODE_ENV = process.env.NODE_ENV || 'development'; const logger = (0, pino_1.default)({ level: process.env.LOG_LEVEL || 'info', transport: NODE_ENV !== 'production' ? { target: 'pino-pretty', options: { colorize: true } } : undefined, }); // ============================================================================ // Server State // ============================================================================ let bot = null; let aiDefence = null; let chatEnhancer = null; const startTime = Date.now(); // ============================================================================ // Utility Functions // ============================================================================ async function parseBody(req) { return new Promise((resolve, reject) => { const chunks = []; req.on('data', (chunk) => chunks.push(chunk)); req.on('end', () => { if (chunks.length === 0) { resolve(null); return; } const rawBody = Buffer.concat(chunks).toString('utf-8'); try { const body = JSON.parse(rawBody); resolve(body); } catch (parseError) { logger.error({ rawBody: rawBody.substring(0, 500), contentType: req.headers['content-type'], contentLength: req.headers['content-length'], err: parseError }, 'JSON parse error'); reject(new Error('Invalid JSON')); } }); req.on('error', (err) => { logger.error({ err }, 'Request body read error'); reject(err); }); }); } function sendJSON(res, statusCode, data) { res.writeHead(statusCode, { 'Content-Type': 'application/json', 'X-Content-Type-Options': 'nosniff', }); res.end(JSON.stringify(data)); } function sendError(res, statusCode, message, code) { sendJSON(res, statusCode, { error: message, code: code || 'ERROR' }); } // ============================================================================ // Static File Serving // ============================================================================ function serveStaticFile(res, filePath, contentType) { try { const content = (0, node_fs_1.readFileSync)(filePath, 'utf-8'); res.writeHead(200, { 'Content-Type': contentType, 'Cache-Control': 'public, max-age=3600', }); res.end(content); return true; } catch { return false; } } function getChatUIPath() { // Try multiple locations for the chat UI // Works in both development (src/) and production (dist/) const cwd = process.cwd(); const possiblePaths = [ // Docker/Cloud Run paths (WORKDIR /app) (0, node_path_1.join)(cwd, 'dist', 'api', 'public', 'index.html'), // Development paths (0, node_path_1.join)(cwd, 'src', 'api', 'public', 'index.html'), // Production paths (ESM) (0, node_path_1.join)(cwd, 'dist', 'esm', 'api', 'public', 'index.html'), // When running from node_modules (0, node_path_1.join)(cwd, 'node_modules', 'ruvbot', 'dist', 'api', 'public', 'index.html'), (0, node_path_1.join)(cwd, 'node_modules', 'ruvbot', 'src', 'api', 'public', 'index.html'), // Absolute paths (for Docker) '/app/dist/api/public/index.html', '/app/src/api/public/index.html', ]; for (const p of possiblePaths) { if ((0, node_fs_1.existsSync)(p)) { logger.info({ path: p }, 'Found chat UI'); return p; } } logger.warn({ cwd, paths: possiblePaths }, 'Chat UI not found, using fallback'); return possiblePaths[0]; // Default to first path } // ============================================================================ // Route Handlers // ============================================================================ async function handleRoot(ctx) { const { res } = ctx; const chatUIPath = getChatUIPath(); if ((0, node_fs_1.existsSync)(chatUIPath)) { serveStaticFile(res, chatUIPath, 'text/html; charset=utf-8'); } else { // Fallback: serve a simple redirect or message res.writeHead(200, { 'Content-Type': 'text/html' }); res.end(` RuvBot

šŸ¤– RuvBot

Enterprise-grade AI Assistant

View API Status
`); } } async function handleHealth(ctx) { const { res } = ctx; sendJSON(res, 200, { status: 'healthy', version: '0.2.0', uptime: Math.floor((Date.now() - startTime) / 1000), timestamp: new Date().toISOString(), }); } async function handleReady(ctx) { const { res } = ctx; if (bot?.getStatus().isRunning) { sendJSON(res, 200, { status: 'ready' }); } else { sendError(res, 503, 'Service not ready', 'NOT_READY'); } } async function handleStatus(ctx) { const { res } = ctx; if (!bot) { sendError(res, 503, 'Bot not initialized', 'NOT_INITIALIZED'); return; } const status = bot.getStatus(); const config = bot.getConfig(); // Check LLM configuration const hasAnthropicKey = !!(process.env.ANTHROPIC_API_KEY || config.llm?.apiKey); const hasOpenRouterKey = !!process.env.OPENROUTER_API_KEY; const hasGoogleAIKey = !!(process.env.GOOGLE_AI_API_KEY || process.env.GEMINI_API_KEY); const hasAnyKey = hasAnthropicKey || hasOpenRouterKey || hasGoogleAIKey; // Determine active provider let activeProvider = 'none'; if (hasOpenRouterKey) activeProvider = 'openrouter'; else if (hasGoogleAIKey) activeProvider = 'google-ai'; else if (hasAnthropicKey) activeProvider = 'anthropic'; sendJSON(res, 200, { ...status, llm: { configured: hasAnyKey, provider: activeProvider, model: config.llm?.model || 'not set', hasApiKey: hasAnyKey, }, environment: { nodeEnv: NODE_ENV, hasAnthropicKey, hasOpenRouterKey, hasGoogleAIKey, }, }); } async function handleSkills(ctx) { const { res } = ctx; if (!chatEnhancer) { sendJSON(res, 200, { skills: [], message: 'ChatEnhancer not initialized' }); return; } const skills = chatEnhancer.getAvailableSkills(); const memoryStats = chatEnhancer.getMemoryStats(); sendJSON(res, 200, { skills, categories: { search: skills.filter(s => s.id.includes('search')), memory: skills.filter(s => s.id.includes('memory')), code: skills.filter(s => s.id.includes('code')), summarize: skills.filter(s => s.id.includes('summar')), }, memoryStats, usage: 'Include skill trigger words in your message to automatically invoke skills.', examples: [ 'search for TypeScript async patterns', 'remember that my project uses React 18', 'explain this code: function add(a, b) { return a + b; }', 'summarize our conversation', ], }); } async function handleModels(ctx) { const { res } = ctx; sendJSON(res, 200, { models: [ // Gemini 2.x (recommended) { id: 'google/gemini-2.5-pro-preview-05-06', name: 'Gemini 2.5 Pro Preview', provider: 'openrouter' }, { id: 'google/gemini-2.0-flash-001', name: 'Gemini 2.0 Flash', provider: 'openrouter' }, { id: 'google/gemini-2.0-flash-thinking-exp:free', name: 'Gemini 2.0 Flash Thinking (Free)', provider: 'openrouter' }, // Anthropic Claude { id: 'anthropic/claude-3.5-sonnet', name: 'Claude 3.5 Sonnet', provider: 'openrouter' }, { id: 'anthropic/claude-3-opus', name: 'Claude 3 Opus', provider: 'openrouter' }, { id: 'claude-3-5-sonnet-20241022', name: 'Claude 3.5 Sonnet (Direct)', provider: 'anthropic' }, // OpenAI { id: 'openai/gpt-4o', name: 'GPT-4o', provider: 'openrouter' }, { id: 'openai/o1-preview', name: 'O1 Preview (Reasoning)', provider: 'openrouter' }, // Qwen { id: 'qwen/qwq-32b', name: 'Qwen QwQ 32B (Reasoning)', provider: 'openrouter' }, { id: 'qwen/qwq-32b:free', name: 'Qwen QwQ 32B (Free)', provider: 'openrouter' }, // DeepSeek { id: 'deepseek/deepseek-r1', name: 'DeepSeek R1 (Reasoning)', provider: 'openrouter' }, // Meta { id: 'meta-llama/llama-3.1-405b-instruct', name: 'Llama 3.1 405B', provider: 'openrouter' }, ], default: 'google/gemini-2.5-pro-preview-05-06', }); } async function handleCreateAgent(ctx) { const { res, body } = ctx; if (!bot) { sendError(res, 503, 'Bot not initialized', 'NOT_INITIALIZED'); return; } if (!body || typeof body.name !== 'string') { sendError(res, 400, 'Agent name is required', 'INVALID_REQUEST'); return; } const config = { id: body.id || (0, node_crypto_1.randomUUID)(), name: body.name, model: body.model || 'claude-3-haiku-20240307', systemPrompt: body.systemPrompt, temperature: body.temperature, maxTokens: body.maxTokens, }; const agent = await bot.spawnAgent(config); sendJSON(res, 201, agent); } async function handleListAgents(ctx) { const { res } = ctx; if (!bot) { sendError(res, 503, 'Bot not initialized', 'NOT_INITIALIZED'); return; } sendJSON(res, 200, { agents: bot.listAgents() }); } async function handleCreateSession(ctx) { const { res, body } = ctx; if (!bot) { sendError(res, 503, 'Bot not initialized', 'NOT_INITIALIZED'); return; } if (!body || typeof body.agentId !== 'string') { sendError(res, 400, 'Agent ID is required', 'INVALID_REQUEST'); return; } try { const session = await bot.createSession(body.agentId, { userId: body.userId, channelId: body.channelId, platform: body.platform, metadata: body.metadata, }); sendJSON(res, 201, session); } catch (error) { const message = error instanceof Error ? error.message : 'Unknown error'; sendError(res, 400, message, 'SESSION_ERROR'); } } async function handleListSessions(ctx) { const { res } = ctx; if (!bot) { sendError(res, 503, 'Bot not initialized', 'NOT_INITIALIZED'); return; } sendJSON(res, 200, { sessions: bot.listSessions() }); } async function handleChat(ctx) { const { res, body, url } = ctx; if (!bot) { sendError(res, 503, 'Bot not initialized', 'NOT_INITIALIZED'); return; } const sessionId = url.pathname.split('/')[3]; // /api/sessions/:id/chat if (!sessionId) { sendError(res, 400, 'Session ID is required', 'INVALID_REQUEST'); return; } if (!body || typeof body.message !== 'string') { sendError(res, 400, 'Message is required', 'INVALID_REQUEST'); return; } // Validate input with AIDefence if enabled let messageContent = body.message; let inputBlocked = false; if (aiDefence) { const analysisResult = await aiDefence.analyze(messageContent); if (!analysisResult.safe) { logger.warn({ threats: analysisResult.threats, threatLevel: analysisResult.threatLevel, }, 'Threats detected in message'); // Block threats that exceed the configured threshold (safe = false) // This includes critical, high, and medium threats based on blockThreshold sendError(res, 400, 'Message blocked due to security concerns', 'SECURITY_BLOCKED'); inputBlocked = true; return; } } if (inputBlocked) return; try { logger.debug({ sessionId, messageLength: messageContent.length }, 'Processing chat request'); // Step 1: Process with ChatEnhancer for skills and memory let enhancedContext = ''; let skillsUsed = []; let proactiveHints = []; if (chatEnhancer) { try { const enhancedResponse = await chatEnhancer.processMessage(messageContent, { sessionId, userId: body.userId || 'anonymous', tenantId: 'default', conversationHistory: [], }); // If skills were used, include their output in the context if (enhancedResponse.skillsUsed && enhancedResponse.skillsUsed.length > 0) { skillsUsed = enhancedResponse.skillsUsed; if (enhancedResponse.content) { enhancedContext = `\n\n**Skill Results:**\n${enhancedResponse.content}\n\n`; } } // Collect proactive hints if (enhancedResponse.proactiveHints && enhancedResponse.proactiveHints.length > 0) { proactiveHints = enhancedResponse.proactiveHints; } // Log skill usage if (skillsUsed.length > 0) { logger.info({ sessionId, skills: skillsUsed.map(s => s.skillId), memoriesRecalled: enhancedResponse.memoriesRecalled?.length || 0, }, 'Skills executed'); } } catch (enhanceError) { logger.warn({ err: enhanceError }, 'ChatEnhancer processing failed, continuing with standard chat'); } } // Step 2: Get LLM response // Note: enhancedContext is prepended to the response content, not passed to the LLM const response = await bot.chat(sessionId, messageContent, { userId: body.userId, metadata: body.metadata, }); logger.debug({ sessionId, responseId: response.id }, 'Chat response generated'); // Validate output with AIDefence if enabled if (aiDefence && response.content) { try { const outputResult = await aiDefence.validateResponse(response.content, messageContent); if (!outputResult.safe) { logger.warn({ threats: outputResult.threats, }, 'Threats detected in response'); } } catch (defenceError) { // Log but don't fail the request if AIDefence validation fails logger.warn({ err: defenceError }, 'AIDefence output validation failed'); } } // Combine skill output with LLM response let finalContent = response.content; if (enhancedContext && !finalContent.includes(enhancedContext)) { finalContent = enhancedContext + finalContent; } // Add proactive hints if available if (proactiveHints.length > 0) { finalContent += '\n\n---\nšŸ’” ' + proactiveHints.join('\nšŸ’” '); } sendJSON(res, 200, { ...response, content: finalContent, skillsUsed: skillsUsed.length > 0 ? skillsUsed : undefined, proactiveHints: proactiveHints.length > 0 ? proactiveHints : undefined, }); } catch (error) { const message = error instanceof Error ? error.message : String(error); logger.error({ err: error, sessionId, errorMessage: message }, 'Chat request failed'); sendError(res, 400, message, 'CHAT_ERROR'); } } // ============================================================================ // Router // ============================================================================ const routes = [ { method: 'GET', pattern: /^\/$/, handler: handleRoot }, { method: 'GET', pattern: /^\/health$/, handler: handleHealth }, { method: 'GET', pattern: /^\/ready$/, handler: handleReady }, { method: 'GET', pattern: /^\/api\/status$/, handler: handleStatus }, { method: 'GET', pattern: /^\/api\/models$/, handler: handleModels }, { method: 'GET', pattern: /^\/api\/skills$/, handler: handleSkills }, { method: 'POST', pattern: /^\/api\/agents$/, handler: handleCreateAgent }, { method: 'GET', pattern: /^\/api\/agents$/, handler: handleListAgents }, { method: 'POST', pattern: /^\/api\/sessions$/, handler: handleCreateSession }, { method: 'GET', pattern: /^\/api\/sessions$/, handler: handleListSessions }, { method: 'POST', pattern: /^\/api\/sessions\/[^/]+\/chat$/, handler: handleChat }, ]; async function handleRequest(req, res) { const url = new node_url_1.URL(req.url || '/', `http://${req.headers.host || 'localhost'}`); const method = req.method || 'GET'; // CORS headers res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization'); if (method === 'OPTIONS') { res.writeHead(204); res.end(); return; } // Find matching route for (const route of routes) { if (route.method === method && route.pattern.test(url.pathname)) { try { const body = method !== 'GET' && method !== 'HEAD' ? await parseBody(req) : null; await route.handler({ req, res, url, body }); return; } catch (error) { // Use 'err' key for proper pino error serialization const errorMessage = error instanceof Error ? error.message : String(error); logger.error({ err: error, path: url.pathname, errorMessage }, 'Request handler error'); sendError(res, 500, 'Internal server error', 'INTERNAL_ERROR'); return; } } } // 404 Not Found sendError(res, 404, 'Not found', 'NOT_FOUND'); } // ============================================================================ // Server Initialization // ============================================================================ async function initializeBot() { logger.info('Initializing RuvBot...'); bot = (0, RuvBot_js_1.createRuvBot)({ config: { name: process.env.BOT_NAME || 'RuvBot', api: { enabled: false, // We're handling API ourselves port: PORT, host: HOST, cors: true, rateLimit: { max: 100, timeWindow: 60000 }, auth: { enabled: false, type: 'bearer' }, }, llm: { provider: process.env.GOOGLE_AI_API_KEY || process.env.GEMINI_API_KEY ? 'google' : 'anthropic', apiKey: process.env.ANTHROPIC_API_KEY || '', model: process.env.DEFAULT_MODEL || (process.env.GOOGLE_AI_API_KEY || process.env.GEMINI_API_KEY ? 'gemini-2.5-flash' : 'claude-3-haiku-20240307'), temperature: 0.7, maxTokens: 4096, streaming: true, }, slack: { enabled: !!process.env.SLACK_BOT_TOKEN, botToken: process.env.SLACK_BOT_TOKEN, appToken: process.env.SLACK_APP_TOKEN, signingSecret: process.env.SLACK_SIGNING_SECRET, socketMode: true, }, discord: { enabled: !!process.env.DISCORD_TOKEN, token: process.env.DISCORD_TOKEN, clientId: process.env.DISCORD_CLIENT_ID, guildId: process.env.DISCORD_GUILD_ID, }, memory: { dimensions: 384, maxVectors: 100000, indexType: 'hnsw', efConstruction: 200, efSearch: 50, m: 16, }, logging: { level: process.env.LOG_LEVEL || 'info', pretty: NODE_ENV !== 'production', }, }, }); await bot.start(); // Initialize ChatEnhancer with skills and memory chatEnhancer = (0, ChatEnhancer_js_1.createChatEnhancer)({ enableSkills: true, enableMemory: true, enableProactiveAssistance: true, memorySearchThreshold: 0.5, memorySearchLimit: 5, skillConfidenceThreshold: 0.6, tenantId: 'default', }); logger.info('ChatEnhancer initialized with skills: web-search, memory, code, summarize'); // Initialize AIDefence if not in development if (NODE_ENV === 'production') { const aiDefenceConfig = { detectPromptInjection: true, detectJailbreak: true, detectPII: true, blockThreshold: 'medium', enableAuditLog: true, }; aiDefence = (0, AIDefenceGuard_js_1.createAIDefenceGuard)(aiDefenceConfig); logger.info('AIDefence security layer enabled'); } // Create default agent await bot.spawnAgent({ id: 'default-agent', name: 'default-agent', model: process.env.DEFAULT_MODEL || 'claude-3-haiku-20240307', systemPrompt: process.env.SYSTEM_PROMPT || 'You are RuvBot, a helpful AI assistant.', }); logger.info('RuvBot initialized successfully'); } async function startServer() { // Initialize bot first await initializeBot(); // Create HTTP server const server = (0, node_http_1.createServer)((req, res) => { handleRequest(req, res).catch((error) => { const errorMessage = error instanceof Error ? error.message : String(error); logger.error({ err: error, errorMessage }, 'Unhandled request error'); if (!res.headersSent) { sendError(res, 500, 'Internal server error', 'INTERNAL_ERROR'); } }); }); // Graceful shutdown const shutdown = async (signal) => { logger.info({ signal }, 'Received shutdown signal'); server.close(async () => { logger.info('HTTP server closed'); if (bot) { await bot.stop(); logger.info('RuvBot stopped'); } process.exit(0); }); // Force exit after timeout setTimeout(() => { logger.error('Forced shutdown due to timeout'); process.exit(1); }, 10000); }; process.on('SIGTERM', () => shutdown('SIGTERM')); process.on('SIGINT', () => shutdown('SIGINT')); // Start listening server.listen(PORT, HOST, () => { logger.info({ port: PORT, host: HOST, env: NODE_ENV }, 'RuvBot server started'); }); } // ============================================================================ // Main Entry Point // ============================================================================ startServer().catch((error) => { const errorMessage = error instanceof Error ? error.message : String(error); logger.error({ err: error, errorMessage }, 'Failed to start server'); process.exit(1); }); //# sourceMappingURL=server.js.map