UNPKG

seraph-agent

Version:

An extremely lightweight, SRE autonomous AI agent for seamless integration with common observability tasks.

249 lines (248 loc) 10.4 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.resetRequestCounts = resetRequestCounts; exports.startServer = startServer; const http = __importStar(require("http")); const net = __importStar(require("net")); const fs = __importStar(require("fs")); const path = __importStar(require("path")); const metrics_1 = require("./metrics"); const chat = __importStar(require("./chat")); let requestCounts = new Map(); function resetRequestCounts() { requestCounts.clear(); } function startServer(config, agentManager) { const RATE_LIMIT_WINDOW = config.rateLimit?.window || 60000; const RATE_LIMIT_MAX_REQUESTS = config.rateLimit?.maxRequests || 100; const intervalId = setInterval(() => requestCounts.clear(), RATE_LIMIT_WINDOW); const server = http.createServer(async (req, res) => { const clientIp = req.socket.remoteAddress; // Global error handler for the request req.on('error', (err) => { console.error('Request error:', err); if (!res.headersSent) { res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ status: 'error', message: 'Internal Server Error' })); } }); // Authentication middleware if (config.serverApiKey && req.url !== '/metrics') { const authHeader = req.headers.authorization; if (!authHeader || !authHeader.startsWith('Bearer ')) { res.writeHead(401, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ status: 'error', message: 'Unauthorized' })); return; } const token = authHeader.substring(7); if (token !== config.serverApiKey) { res.writeHead(403, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ status: 'error', message: 'Forbidden' })); return; } } if (req.url === '/logs' && req.method === 'POST') { if (clientIp) { const requestCount = (requestCounts.get(clientIp) || 0) + 1; requestCounts.set(clientIp, requestCount); if (requestCount > RATE_LIMIT_MAX_REQUESTS) { res.writeHead(429, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ status: 'error', message: 'Too Many Requests' })); return; } } const MAX_PAYLOAD_SIZE = 1024 * 1024; // 1MB let body = ''; req.on('data', chunk => { if (res.headersSent) return; body += chunk.toString(); if (body.length > MAX_PAYLOAD_SIZE) { res.writeHead(413, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ status: 'error', message: 'Payload Too Large' })); } }); req.on('end', () => { if (res.headersSent) return; if (typeof body !== 'string' || body.trim() === '') { res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ status: 'error', message: 'Request body must be a non-empty string.' })); return; } try { // A simple check to see if it could be JSON if (body.startsWith('{') || body.startsWith('[')) { JSON.parse(body); } } catch (error) { res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ status: 'error', message: 'Invalid JSON format in log.' })); return; } try { agentManager.dispatch(body); res.writeHead(202, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ status: 'accepted' })); } catch (error) { res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ status: 'error', message: 'Internal server error while processing log.' })); } }); } else if (req.url === '/status' && req.method === 'GET') { res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ status: 'ok' })); } else if (req.url === '/metrics' && req.method === 'GET') { res.setHeader('Content-Type', metrics_1.register.contentType); res.end(await metrics_1.register.metrics()); } else if (req.url === '/chat' && req.method === 'POST') { let body = ''; req.on('data', (chunk) => { body += chunk.toString(); }); req.on('end', async () => { try { if (!body) { res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ status: 'error', message: 'message is required' })); return; } const { message } = JSON.parse(body); if (!message) { res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ status: 'error', message: 'message is required' })); return; } const response = await chat.chat(message, config, [], // No MCP tools available in server mode agentManager.getRecentLogs()); res.writeHead(200, { 'Content-Type': 'text/plain' }); res.end(response); } catch (error) { if (error instanceof SyntaxError) { res.writeHead(400, { 'Content-Type': 'application/json', }); res.end(JSON.stringify({ status: 'error', message: 'Invalid JSON format', })); return; } res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ status: 'error', message: 'Internal Server Error', })); } }); } else { res.writeHead(404, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ status: 'error', message: 'Not Found' })); } }); server.listen(config.port, () => { // The listening message is now in index.ts to avoid duplication }); // IPC server const ipcSocketPath = path.join(process.cwd(), '.seraph.sock'); const ipcServer = net.createServer((socket) => { socket.on('data', (data) => { const message = data.toString(); if (message === 'get_logs') { socket.write(JSON.stringify(agentManager.getRecentLogs())); } }); }); const cleanupSocket = async () => { try { await fs.promises.access(ipcSocketPath); await fs.promises.unlink(ipcSocketPath); } catch (error) { if (error.code !== 'ENOENT') { console.error('Error removing old IPC socket file:', error); } } }; cleanupSocket().then(() => { ipcServer.listen(ipcSocketPath, async () => { try { await fs.promises.chmod(ipcSocketPath, 0o600); console.log('IPC server started'); } catch (error) { console.error('Error setting permissions on IPC socket file:', error); } }); }); let isShuttingDown = false; const shutdown = (callback) => { if (isShuttingDown) { if (callback) callback(); return; } isShuttingDown = true; let closedCount = 0; const totalToClose = 2; const onClosed = () => { closedCount++; if (closedCount === totalToClose) { clearInterval(intervalId); // Clear the rate limit interval cleanupSocket().then(() => { if (callback) callback(); }); } }; server.close(() => { onClosed(); }); ipcServer.close(() => { onClosed(); }); }; process.on('SIGTERM', () => shutdown()); process.on('SIGINT', () => shutdown()); return { server, shutdown }; }