"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
`);
}
}
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