UNPKG

@vfarcic/dot-ai

Version:

AI-powered development productivity platform that enhances software development workflows through intelligent automation and AI-driven assistance

1,057 lines 100 kB
"use strict"; /** * REST API Router for MCP Tools * * Provides HTTP REST endpoints for all registered MCP tools. * Handles routing, validation, execution, and response formatting. */ 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.RestApiRouter = exports.HttpStatus = void 0; const node_url_1 = require("node:url"); const openapi_generator_1 = require("./openapi-generator"); const rest_route_registry_1 = require("./rest-route-registry"); const routes_1 = require("./routes"); const resource_sync_handler_1 = require("./resource-sync-handler"); const embedding_migration_handler_1 = require("./embedding-migration-handler"); const prompts_1 = require("../tools/prompts"); const generic_session_manager_1 = require("../core/generic-session-manager"); const session_events_1 = require("../core/session-events"); const shared_prompt_loader_1 = require("../core/shared-prompt-loader"); const visualization_1 = require("../core/visualization"); const ai_provider_factory_1 = require("../core/ai-provider-factory"); const capability_tools_1 = require("../core/capability-tools"); const resource_tools_1 = require("../core/resource-tools"); const mermaid_tools_1 = require("../core/mermaid-tools"); const plugin_registry_1 = require("../core/plugin-registry"); const manage_knowledge_1 = require("../tools/manage-knowledge"); const user_management_1 = require("./oauth/user-management"); const request_context_1 = require("./request-context"); const rbac_1 = require("../core/rbac"); /** * HTTP status codes for REST responses */ var HttpStatus; (function (HttpStatus) { HttpStatus[HttpStatus["OK"] = 200] = "OK"; HttpStatus[HttpStatus["BAD_REQUEST"] = 400] = "BAD_REQUEST"; HttpStatus[HttpStatus["NOT_FOUND"] = 404] = "NOT_FOUND"; HttpStatus[HttpStatus["METHOD_NOT_ALLOWED"] = 405] = "METHOD_NOT_ALLOWED"; HttpStatus[HttpStatus["INTERNAL_SERVER_ERROR"] = 500] = "INTERNAL_SERVER_ERROR"; HttpStatus[HttpStatus["BAD_GATEWAY"] = 502] = "BAD_GATEWAY"; HttpStatus[HttpStatus["SERVICE_UNAVAILABLE"] = 503] = "SERVICE_UNAVAILABLE"; })(HttpStatus || (exports.HttpStatus = HttpStatus = {})); /** * REST API Router for MCP tools */ class RestApiRouter { registry; routeRegistry; logger; dotAI; config; openApiGenerator; requestCounter = 0; pluginManager; constructor(registry, dotAI, logger, pluginManager, config = {}) { this.registry = registry; this.dotAI = dotAI; this.logger = logger; this.pluginManager = pluginManager; this.config = { basePath: '/api', version: 'v1', enableCors: true, requestTimeout: 1800000, // 30 minutes for long-running operations (capability scan with slower AI providers) ...config, }; // Initialize route registry and register all routes (PRD #354) this.routeRegistry = new rest_route_registry_1.RestRouteRegistry(logger); (0, routes_1.registerAllRoutes)(this.routeRegistry); this.logger.info('REST route registry initialized', { routeCount: this.routeRegistry.getRouteCount(), tags: this.routeRegistry.getTags(), }); // Initialize OpenAPI generator with route registry (PRD #354) this.openApiGenerator = new openapi_generator_1.OpenApiGenerator(registry, logger, { basePath: this.config.basePath, apiVersion: this.config.version, }, this.routeRegistry); } /** * Handle incoming HTTP requests for REST API * * PRD #354: Uses route registry for matching, dispatches to handlers based on route path. */ async handleRequest(req, res, body) { const requestId = this.generateRequestId(); const startTime = Date.now(); try { this.logger.debug('REST API request received', { requestId, method: req.method, url: req.url, hasBody: !!body, }); // Handle CORS preflight if (this.config.enableCors) { this.setCorsHeaders(res); if (req.method === 'OPTIONS') { res.writeHead(HttpStatus.OK); res.end(); return; } } // Parse URL const url = new node_url_1.URL(req.url || '/', 'http://localhost'); const method = req.method || 'GET'; // PRD #354: Try route registry first const routeMatch = this.routeRegistry.findRoute(method, url.pathname); if (routeMatch) { this.logger.debug('Route matched via registry', { requestId, path: routeMatch.route.path, method: routeMatch.route.method, params: routeMatch.params, }); // Dispatch to handler based on route path await this.dispatchRoute(req, res, requestId, routeMatch, url.searchParams, body, startTime); return; } // Check if path matches but method is wrong (HTTP 405 per RFC 7231) const allowedMethods = this.routeRegistry.findAllowedMethods(url.pathname); if (allowedMethods.length > 0) { res.setHeader('Allow', allowedMethods.join(', ')); const methodList = allowedMethods.join(', '); const message = allowedMethods.length === 1 ? `Only ${methodList} method allowed` : `Only ${methodList} methods allowed`; await this.sendErrorResponse(res, requestId, HttpStatus.METHOD_NOT_ALLOWED, 'METHOD_NOT_ALLOWED', message); return; } // No match found await this.sendErrorResponse(res, requestId, HttpStatus.NOT_FOUND, 'NOT_FOUND', 'API endpoint not found'); } catch (error) { this.logger.error('REST API request failed', error instanceof Error ? error : new Error(String(error)), { requestId, errorMessage: error instanceof Error ? error.message : String(error), }); await this.sendErrorResponse(res, requestId, HttpStatus.INTERNAL_SERVER_ERROR, 'INTERNAL_ERROR', 'An internal server error occurred'); } } /** * Dispatch request to appropriate handler based on matched route * PRD #354: Central dispatch using handler map for registry-matched routes. */ async dispatchRoute(req, res, requestId, routeMatch, searchParams, body, startTime) { const { route, params } = routeMatch; const routeKey = `${route.method}:${route.path}`; // Handler map: route key -> handler function const handlers = { 'GET:/api/v1/tools': () => this.handleToolDiscovery(req, res, requestId, searchParams), 'POST:/api/v1/tools/:toolName': () => this.handleToolExecution(req, res, requestId, params.toolName, body, startTime), 'GET:/api/v1/openapi': () => this.handleOpenApiSpec(req, res, requestId), 'GET:/api/v1/resources': () => this.handleListResources(req, res, requestId, searchParams), 'GET:/api/v1/resources/kinds': () => this.handleGetResourceKinds(req, res, requestId, searchParams), 'GET:/api/v1/resources/search': () => this.handleSearchResources(req, res, requestId, searchParams), 'POST:/api/v1/resources/sync': () => this.handleResourceSyncRequest(req, res, requestId, body), 'GET:/api/v1/resource': () => this.handleGetResource(req, res, requestId, searchParams), 'GET:/api/v1/namespaces': () => this.handleGetNamespaces(req, res, requestId), 'GET:/api/v1/events': () => this.handleGetEvents(req, res, requestId, searchParams), 'GET:/api/v1/logs': () => this.handleGetLogs(req, res, requestId, searchParams), 'GET:/api/v1/prompts': () => this.handlePromptsListRequest(req, res, requestId), 'POST:/api/v1/prompts/refresh': () => this.handlePromptsCacheRefresh(req, res, requestId), 'POST:/api/v1/prompts/:promptName': () => this.handlePromptsGetRequest(req, res, requestId, params.promptName, body), 'GET:/api/v1/visualize/:sessionId': () => this.handleVisualize(req, res, requestId, params.sessionId, searchParams), 'GET:/api/v1/events/remediations': () => this.handleRemediationSSE(req, res, requestId), 'GET:/api/v1/sessions': () => this.handleListSessions(req, res, requestId, searchParams), 'GET:/api/v1/sessions/:sessionId': () => this.handleSessionRetrieval(req, res, requestId, params.sessionId), 'DELETE:/api/v1/knowledge/source/:sourceIdentifier': () => this.handleDeleteKnowledgeSource(req, res, requestId, params.sourceIdentifier), 'POST:/api/v1/knowledge/ask': () => this.handleKnowledgeAsk(req, res, requestId, body), 'POST:/api/v1/embeddings/migrate': () => this.handleEmbeddingMigrationRequest(req, res, requestId, body), // User management (PRD #380 Task 2.5) 'POST:/api/v1/users': () => this.handleCreateUser(req, res, requestId, body), 'GET:/api/v1/users': () => this.handleListUsers(req, res, requestId), 'DELETE:/api/v1/users/:email': () => this.handleDeleteUser(req, res, requestId, params.email), }; const handler = handlers[routeKey]; if (handler) { await handler(); } else { this.logger.warn('Route matched but no handler found', { requestId, routeKey, }); await this.sendErrorResponse(res, requestId, HttpStatus.NOT_FOUND, 'NOT_FOUND', 'Handler not found for route'); } } /** * Handle tool discovery requests */ async handleToolDiscovery(req, res, requestId, searchParams) { try { const category = searchParams.get('category') || undefined; const tag = searchParams.get('tag') || undefined; const search = searchParams.get('search') || undefined; let tools = this.registry.getToolsFiltered({ category, tag, search }); // RBAC-filtered tool discovery (PRD #392) — OAuth users only see authorized tools const discoveryIdentity = (0, request_context_1.getCurrentIdentity)(); tools = await (0, rbac_1.filterAuthorizedTools)(discoveryIdentity, tools); // Check user management access and include as virtual "users" tool (PRD #392) const userAccessResult = await (0, rbac_1.checkToolAccess)(discoveryIdentity, { toolName: 'manageUsers', resource: 'users', }); if (userAccessResult.allowed) { tools = [ ...tools, { name: 'users', description: 'Manage users (create, list, delete)', schema: { type: 'object', properties: {} }, category: 'Administration', tags: ['users', 'administration', 'management'], }, ]; } const categories = this.registry.getCategories(); const tags = this.registry.getTags(); const response = { success: true, data: { tools, total: tools.length, categories, tags, }, meta: { timestamp: new Date().toISOString(), requestId, version: this.config.version, }, }; await this.sendJsonResponse(res, HttpStatus.OK, response); this.logger.info('Tool discovery request completed', { requestId, totalTools: tools.length, filters: { category, tag, search }, }); } catch { await this.sendErrorResponse(res, requestId, HttpStatus.INTERNAL_SERVER_ERROR, 'DISCOVERY_ERROR', 'Failed to retrieve tool information'); } } /** * Handle tool execution requests */ async handleToolExecution(req, res, requestId, toolName, body, startTime) { try { // Check if tool exists const toolMetadata = this.registry.getTool(toolName); if (!toolMetadata) { await this.sendErrorResponse(res, requestId, HttpStatus.NOT_FOUND, 'TOOL_NOT_FOUND', `Tool '${toolName}' not found`); return; } // RBAC enforcement (PRD #392) — check tool-level authorization for OAuth users const identity = (0, request_context_1.getCurrentIdentity)(); if (identity) { const rbacResult = await (0, rbac_1.checkToolAccess)(identity, { toolName }); if (!rbacResult.allowed) { await this.sendErrorResponse(res, requestId, 403, 'FORBIDDEN', `Access denied: tool '${toolName}' not authorized for user '${identity.email}'`); return; } } // Validate request body if (!body || typeof body !== 'object') { await this.sendErrorResponse(res, requestId, HttpStatus.BAD_REQUEST, 'INVALID_REQUEST', 'Request body must be a JSON object'); return; } this.logger.info('Executing tool via REST API', { requestId, toolName, parameters: Object.keys(body), }); // Execute the tool handler with timeout // Note: Tool handlers expect the same format as MCP calls // PRD #343: Pass pluginManager for kubectl operations via plugin system const timeoutMs = this.config.requestTimeout; const toolPromise = toolMetadata.handler(body, this.dotAI, this.logger, requestId, this.pluginManager); const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('Request timeout exceeded')), timeoutMs)); // Prevent unhandled rejection if toolPromise resolves after timeout toolPromise.catch(() => { }); const mcpResult = (await Promise.race([toolPromise, timeoutPromise])); // Transform MCP format to proper REST JSON // All MCP tools return JSON.stringify() in content[0].text, so parse it back to proper JSON let transformedResult; if (mcpResult?.content?.[0]?.type === 'text') { try { transformedResult = JSON.parse(mcpResult.content[0].text); } catch (parseError) { this.logger.warn('Failed to parse MCP tool result as JSON, returning as text', { requestId, toolName, error: parseError instanceof Error ? parseError.message : String(parseError), }); transformedResult = mcpResult.content[0].text; } } else { // Fallback for unexpected format transformedResult = mcpResult; } const executionTime = Date.now() - startTime; const response = { success: true, data: { result: transformedResult, tool: toolName, executionTime, }, meta: { timestamp: new Date().toISOString(), requestId, version: this.config.version, }, }; await this.sendJsonResponse(res, HttpStatus.OK, response); this.logger.info('Tool execution completed', { requestId, toolName, executionTime, success: true, }); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); this.logger.error('Tool execution failed', error instanceof Error ? error : new Error(String(error)), { requestId, toolName, errorMessage, }); await this.sendErrorResponse(res, requestId, HttpStatus.INTERNAL_SERVER_ERROR, 'EXECUTION_ERROR', errorMessage); } } /** * Handle OpenAPI specification requests */ async handleOpenApiSpec(req, res, requestId) { try { this.logger.debug('Generating OpenAPI specification', { requestId }); const spec = this.openApiGenerator.generateSpec(); await this.sendJsonResponse(res, HttpStatus.OK, spec); this.logger.info('OpenAPI specification served successfully', { requestId, pathCount: Object.keys(spec.paths).length, componentCount: Object.keys(spec.components?.schemas || {}).length, }); } catch (error) { this.logger.error('Failed to generate OpenAPI specification', error instanceof Error ? error : new Error(String(error)), { requestId, errorMessage: error instanceof Error ? error.message : String(error), }); await this.sendErrorResponse(res, requestId, HttpStatus.INTERNAL_SERVER_ERROR, 'OPENAPI_ERROR', 'Failed to generate OpenAPI specification'); } } /** * Handle resource sync requests from controller */ async handleResourceSyncRequest(req, res, requestId, body) { try { this.logger.info('Processing resource sync request', { requestId }); // Validate request body exists if (!body || typeof body !== 'object') { await this.sendErrorResponse(res, requestId, HttpStatus.BAD_REQUEST, 'INVALID_REQUEST', 'Request body must be a JSON object'); return; } // Delegate to the resource sync handler const response = await (0, resource_sync_handler_1.handleResourceSync)(body, this.logger, requestId); // Determine HTTP status based on response and error type let httpStatus = HttpStatus.OK; if (!response.success) { const errorCode = response.error?.code; if (errorCode === 'VECTOR_DB_UNAVAILABLE' || errorCode === 'HEALTH_CHECK_FAILED') { httpStatus = HttpStatus.SERVICE_UNAVAILABLE; } else if (errorCode === 'SERVICE_INIT_FAILED' || errorCode === 'COLLECTION_INIT_FAILED' || errorCode === 'RESYNC_FAILED') { httpStatus = HttpStatus.INTERNAL_SERVER_ERROR; } else { httpStatus = HttpStatus.BAD_REQUEST; } } await this.sendJsonResponse(res, httpStatus, response); this.logger.info('Resource sync request completed', { requestId, success: response.success, upserted: response.data?.upserted, deleted: response.data?.deleted, }); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); this.logger.error('Resource sync request failed', error instanceof Error ? error : new Error(String(error)), { requestId, errorMessage, }); await this.sendErrorResponse(res, requestId, HttpStatus.INTERNAL_SERVER_ERROR, 'SYNC_ERROR', 'Resource sync failed', { error: errorMessage }); } } /** * Handle GET /api/v1/resources/kinds (PRD #328) * Returns all unique resource kinds with counts * Supports optional namespace query parameter for filtering */ async handleGetResourceKinds(req, res, requestId, searchParams) { try { const namespace = searchParams.get('namespace') || undefined; this.logger.info('Processing get resource kinds request', { requestId, namespace, }); const kinds = await (0, resource_tools_1.getResourceKinds)(namespace); const response = { success: true, data: { kinds, }, meta: { timestamp: new Date().toISOString(), requestId, version: this.config.version, }, }; await this.sendJsonResponse(res, HttpStatus.OK, response); this.logger.info('Get resource kinds request completed', { requestId, kindCount: kinds.length, }); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); this.logger.error('Get resource kinds request failed', error instanceof Error ? error : new Error(String(error)), { requestId, errorMessage, }); await this.sendErrorResponse(res, requestId, HttpStatus.INTERNAL_SERVER_ERROR, 'RESOURCE_KINDS_ERROR', 'Failed to retrieve resource kinds', { error: errorMessage }); } } /** * Handle GET /api/v1/resources/search (PRD #328) * Semantic search for resources with optional exact filters */ async handleSearchResources(req, res, requestId, searchParams) { try { // Extract query parameters const q = searchParams.get('q'); const namespace = searchParams.get('namespace') || undefined; const kind = searchParams.get('kind') || undefined; const apiVersion = searchParams.get('apiVersion') || undefined; const limitParam = searchParams.get('limit'); const offsetParam = searchParams.get('offset'); const minScoreParam = searchParams.get('minScore'); // Validate required parameters if (!q) { await this.sendErrorResponse(res, requestId, HttpStatus.BAD_REQUEST, 'MISSING_PARAMETER', 'The "q" query parameter is required for search'); return; } const limit = limitParam ? parseInt(limitParam, 10) : 100; const offset = offsetParam ? parseInt(offsetParam, 10) : 0; const minScore = minScoreParam ? parseFloat(minScoreParam) : undefined; // Validate numeric parameters if (limitParam && (isNaN(limit) || limit < 1)) { await this.sendErrorResponse(res, requestId, HttpStatus.BAD_REQUEST, 'INVALID_PARAMETER', 'The "limit" parameter must be a positive integer'); return; } if (offsetParam && (isNaN(offset) || offset < 0)) { await this.sendErrorResponse(res, requestId, HttpStatus.BAD_REQUEST, 'INVALID_PARAMETER', 'The "offset" parameter must be a non-negative integer'); return; } if (minScoreParam && (isNaN(minScore) || minScore < 0 || minScore > 1)) { await this.sendErrorResponse(res, requestId, HttpStatus.BAD_REQUEST, 'INVALID_PARAMETER', 'The "minScore" parameter must be a number between 0 and 1'); return; } this.logger.info('Processing search resources request', { requestId, query: q, namespace, kind, apiVersion, limit, offset, minScore, }); // Build filters const filters = {}; if (namespace) filters.namespace = namespace; if (kind) filters.kind = kind; if (apiVersion) filters.apiVersion = apiVersion; // Perform search using ResourceVectorService singleton const { getResourceService } = await Promise.resolve().then(() => __importStar(require('../core/resource-tools'))); const service = await getResourceService(); // Request more results than needed for offset pagination const searchLimit = limit + offset; const results = await service.searchResources(q, Object.keys(filters).length > 0 ? filters : undefined, searchLimit, minScore); // Apply offset pagination const paginatedResults = results.slice(offset, offset + limit); // Transform results to include score for relevance ranking const resources = paginatedResults.map(r => ({ name: r.resource.name, namespace: r.resource.namespace, kind: r.resource.kind, apiVersion: r.resource.apiVersion, labels: r.resource.labels || {}, createdAt: r.resource.createdAt, score: r.score, })); const response = { success: true, data: { resources, total: results.length, limit, offset, }, meta: { timestamp: new Date().toISOString(), requestId, version: this.config.version, }, }; await this.sendJsonResponse(res, HttpStatus.OK, response); this.logger.info('Search resources request completed', { requestId, query: q, resultCount: resources.length, totalMatches: results.length, }); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); this.logger.error('Search resources request failed', error instanceof Error ? error : new Error(String(error)), { requestId, errorMessage, }); await this.sendErrorResponse(res, requestId, HttpStatus.INTERNAL_SERVER_ERROR, 'SEARCH_ERROR', 'Failed to search resources', { error: errorMessage }); } } /** * Handle GET /api/v1/resources (PRD #328) * Returns filtered and paginated list of resources * Supports optional live status enrichment from K8s API */ async handleListResources(req, res, requestId, searchParams) { try { // Extract query parameters const kind = searchParams.get('kind'); const apiVersion = searchParams.get('apiVersion'); const namespace = searchParams.get('namespace') || undefined; const includeStatusParam = searchParams.get('includeStatus'); const limitParam = searchParams.get('limit'); const offsetParam = searchParams.get('offset'); // Validate required parameters if (!kind) { await this.sendErrorResponse(res, requestId, HttpStatus.BAD_REQUEST, 'MISSING_PARAMETER', 'The "kind" query parameter is required'); return; } if (!apiVersion) { await this.sendErrorResponse(res, requestId, HttpStatus.BAD_REQUEST, 'MISSING_PARAMETER', 'The "apiVersion" query parameter is required'); return; } const limit = limitParam ? parseInt(limitParam, 10) : undefined; const offset = offsetParam ? parseInt(offsetParam, 10) : undefined; const includeStatus = includeStatusParam === 'true'; // Validate numeric parameters if (limitParam && (isNaN(limit) || limit < 1)) { await this.sendErrorResponse(res, requestId, HttpStatus.BAD_REQUEST, 'INVALID_PARAMETER', 'The "limit" parameter must be a positive integer'); return; } if (offsetParam && (isNaN(offset) || offset < 0)) { await this.sendErrorResponse(res, requestId, HttpStatus.BAD_REQUEST, 'INVALID_PARAMETER', 'The "offset" parameter must be a non-negative integer'); return; } this.logger.info('Processing list resources request', { requestId, kind, apiVersion, namespace, includeStatus, limit, offset, }); // PRD #343: Never pass includeStatus to listResources (it uses direct kubectl) // Fetch status via plugin separately if requested const result = await (0, resource_tools_1.listResources)({ kind, apiVersion, namespace, limit, offset, }); // Enrich with live status via plugin if requested // PRD #359: Use unified plugin registry if (includeStatus && result.resources.length > 0 && (0, plugin_registry_1.isPluginInitialized)()) { // Process status fetches in batches to avoid overwhelming the // agentic-tools pod with concurrent kubectl processes const batchSize = 5; const enrichedResources = []; for (let i = 0; i < result.resources.length; i += batchSize) { const batch = result.resources.slice(i, i + batchSize); const batchResults = await Promise.all(batch.map(async (resource) => { const resourceType = resource.apiGroup ? `${resource.kind.toLowerCase()}.${resource.apiGroup}` : resource.kind.toLowerCase(); const resourceId = `${resourceType}/${resource.name}`; const pluginResponse = await (0, plugin_registry_1.invokePluginTool)('agentic-tools', 'kubectl_get_resource_json', { resource: resourceId, namespace: resource.namespace, field: 'status', }); if (pluginResponse.success && pluginResponse.result) { const pluginResult = pluginResponse.result; if (pluginResult.success && pluginResult.data) { try { return { ...resource, status: JSON.parse(pluginResult.data), }; } catch { return resource; } } } return resource; })); enrichedResources.push(...batchResults); } result.resources = enrichedResources; } const response = { success: true, data: result, meta: { timestamp: new Date().toISOString(), requestId, version: this.config.version, }, }; await this.sendJsonResponse(res, HttpStatus.OK, response); this.logger.info('List resources request completed', { requestId, resourceCount: result.resources.length, total: result.total, includeStatus, }); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); this.logger.error('List resources request failed', error instanceof Error ? error : new Error(String(error)), { requestId, errorMessage, }); await this.sendErrorResponse(res, requestId, HttpStatus.INTERNAL_SERVER_ERROR, 'LIST_RESOURCES_ERROR', 'Failed to list resources', { error: errorMessage }); } } /** * Handle GET /api/v1/namespaces (PRD #328) * Returns all unique namespaces */ async handleGetNamespaces(req, res, requestId) { try { this.logger.info('Processing get namespaces request', { requestId }); const namespaces = await (0, resource_tools_1.getNamespaces)(); const response = { success: true, data: { namespaces, }, meta: { timestamp: new Date().toISOString(), requestId, version: this.config.version, }, }; await this.sendJsonResponse(res, HttpStatus.OK, response); this.logger.info('Get namespaces request completed', { requestId, namespaceCount: namespaces.length, }); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); this.logger.error('Get namespaces request failed', error instanceof Error ? error : new Error(String(error)), { requestId, errorMessage, }); await this.sendErrorResponse(res, requestId, HttpStatus.INTERNAL_SERVER_ERROR, 'NAMESPACES_ERROR', 'Failed to retrieve namespaces', { error: errorMessage }); } } /** * Handle GET /api/v1/resource (PRD #328) * Returns a single resource with full metadata, spec, and status */ async handleGetResource(req, res, requestId, searchParams) { try { const kind = searchParams.get('kind'); const apiVersion = searchParams.get('apiVersion'); const name = searchParams.get('name'); const namespace = searchParams.get('namespace') || undefined; // Validate required parameters if (!kind) { await this.sendErrorResponse(res, requestId, HttpStatus.BAD_REQUEST, 'BAD_REQUEST', 'kind query parameter is required'); return; } if (!apiVersion) { await this.sendErrorResponse(res, requestId, HttpStatus.BAD_REQUEST, 'BAD_REQUEST', 'apiVersion query parameter is required'); return; } if (!name) { await this.sendErrorResponse(res, requestId, HttpStatus.BAD_REQUEST, 'BAD_REQUEST', 'name query parameter is required'); return; } this.logger.info('Processing get resource request', { requestId, kind, apiVersion, name, namespace, }); // Extract apiGroup from apiVersion (e.g., "apps/v1" -> "apps", "v1" -> "") const apiGroup = apiVersion.includes('/') ? apiVersion.split('/')[0] : ''; // PRD #359: Use unified plugin registry if (!(0, plugin_registry_1.isPluginInitialized)()) { await this.sendErrorResponse(res, requestId, HttpStatus.SERVICE_UNAVAILABLE, 'PLUGIN_UNAVAILABLE', 'Plugin system not initialized'); return; } // Build resource identifier (kind.group/name or kind/name for core resources) const resourceType = apiGroup ? `${kind.toLowerCase()}.${apiGroup}` : kind.toLowerCase(); const resourceId = `${resourceType}/${name}`; // PRD #359: Use unified plugin registry for kubectl operations const pluginResponse = await (0, plugin_registry_1.invokePluginTool)('agentic-tools', 'kubectl_get_resource_json', { resource: resourceId, namespace: namespace, }); // Check for plugin-level failures first if (!pluginResponse.success) { const errorMsg = pluginResponse.error?.message || 'Plugin invocation failed'; await this.sendErrorResponse(res, requestId, HttpStatus.BAD_GATEWAY, 'PLUGIN_ERROR', `Kubernetes plugin error: ${errorMsg}`); return; } let resource; let pluginError; if (pluginResponse.result) { const result = pluginResponse.result; if (result.success && result.data) { try { resource = JSON.parse(result.data); } catch (parseError) { pluginError = `Failed to parse resource JSON: ${parseError instanceof Error ? parseError.message : String(parseError)}`; } } else if (!result.success) { // kubectl command failed - check if it's a "not found" error pluginError = result.error || 'kubectl command failed'; } } // Handle parse errors if (pluginError && !pluginError.toLowerCase().includes('not found')) { await this.sendErrorResponse(res, requestId, HttpStatus.BAD_GATEWAY, 'KUBECTL_ERROR', pluginError); return; } if (!resource) { await this.sendErrorResponse(res, requestId, HttpStatus.NOT_FOUND, 'NOT_FOUND', `Resource ${kind}/${name} not found${namespace ? ` in namespace ${namespace}` : ''}`); return; } const response = { success: true, data: { resource, }, meta: { timestamp: new Date().toISOString(), requestId, version: this.config.version, }, }; await this.sendJsonResponse(res, HttpStatus.OK, response); this.logger.info('Get resource request completed', { requestId, kind, name, namespace, }); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); this.logger.error('Get resource request failed', error instanceof Error ? error : new Error(String(error)), { requestId, errorMessage, }); await this.sendErrorResponse(res, requestId, HttpStatus.INTERNAL_SERVER_ERROR, 'RESOURCE_ERROR', 'Failed to retrieve resource', { error: errorMessage }); } } /** * Handle GET /api/v1/events (PRD #328) * Returns Kubernetes events for a specific resource */ async handleGetEvents(req, res, requestId, searchParams) { try { const name = searchParams.get('name'); const kind = searchParams.get('kind'); const namespace = searchParams.get('namespace') || undefined; const uid = searchParams.get('uid') || undefined; // Validate required parameters if (!name) { await this.sendErrorResponse(res, requestId, HttpStatus.BAD_REQUEST, 'BAD_REQUEST', 'name query parameter is required'); return; } if (!kind) { await this.sendErrorResponse(res, requestId, HttpStatus.BAD_REQUEST, 'BAD_REQUEST', 'kind query parameter is required'); return; } this.logger.info('Processing get events request', { requestId, name, kind, namespace, uid, }); // PRD #359: Use unified plugin registry if (!(0, plugin_registry_1.isPluginInitialized)()) { await this.sendErrorResponse(res, requestId, HttpStatus.SERVICE_UNAVAILABLE, 'PLUGIN_UNAVAILABLE', 'Plugin system not initialized'); return; } // Build field selector for involvedObject filtering const fieldSelectors = [ `involvedObject.name=${name}`, `involvedObject.kind=${kind}`, ]; if (uid) { fieldSelectors.push(`involvedObject.uid=${uid}`); } // PRD #359: Use unified plugin registry for kubectl operations const pluginResponse = await (0, plugin_registry_1.invokePluginTool)('agentic-tools', 'kubectl_events', { namespace: namespace, args: [`--field-selector=${fieldSelectors.join(',')}`], }); const events = []; if (pluginResponse.success && pluginResponse.result) { const pluginResult = pluginResponse.result; if (pluginResult.success && pluginResult.data) { // Parse the table output or handle JSON if available // Events output is typically table format, so we need to parse it const lines = pluginResult.data .split('\n') .filter(line => line.trim()); if (lines.length > 1) { // Skip header line, parse remaining lines for (let i = 1; i < lines.length; i++) { const parts = lines[i].split(/\s{2,}/); if (parts.length >= 5) { events.push({ lastTimestamp: parts[0], type: parts[1], reason: parts[2], involvedObject: { kind, name }, message: parts.slice(4).join(' '), }); } } } } } const response = { success: true, data: { events, count: events.length, }, meta: { timestamp: new Date().toISOString(), requestId, version: this.config.version, }, }; await this.sendJsonResponse(res, HttpStatus.OK, response); this.logger.info('Get events request completed', { requestId, name, kind, namespace, eventCount: events.length, }); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); this.logger.error('Get events request failed', error instanceof Error ? error : new Error(String(error)), { requestId, errorMessage, }); await this.sendErrorResponse(res, requestId, HttpStatus.INTERNAL_SERVER_ERROR, 'EVENTS_ERROR', 'Failed to retrieve events', { error: errorMessage }); } } /** * Handle GET /api/v1/logs (PRD #328) * Returns container logs for a pod */ async handleGetLogs(req, res, requestId, searchParams) { try { const name = searchParams.get('name'); const namespace = searchParams.get('namespace'); const container = searchParams.get('container') || undefined; const tailLinesParam = searchParams.get('tailLines'); // Validate required parameters if (!name) { await this.sendErrorResponse(res, requestId, HttpStatus.BAD_REQUEST, 'BAD_REQUEST', 'name query parameter is required'); return; } if (!namespace) { await this.sendErrorResponse(res, requestId, HttpStatus.BAD_REQUEST, 'BAD_REQUEST', 'namespace query parameter is required'); return; } // Parse tailLines with validation let tailLines; if (tailLinesParam) { tailLines = parseInt(tailLinesParam, 10); if (isNaN(tailLines) || tailLines < 1) { await this.sendErrorResponse(res, requestId, HttpStatus.BAD_REQUEST, 'INVALID_PARAMETER', 'tailLines must be a positive integer'); return; } } this.logger.info('Processing get logs request', { requestId, name, namespace, container, tailLines, }); // PRD #359: Use unified plugin registry if (!(0, plugin_registry_1.isPluginInitialized)()) { await this.sendErrorResponse(res, requestId, HttpStatus.SERVICE_UNAVAILABLE, 'PLUGIN_UNAVAILABLE', 'Plugin system not initialized'); return; } // Build args for kubectl_logs const args = []; if (tailLines) { args.push(`--tail=${tailLines}`); } if (container) { args.push('-c', container); } // PRD #359: Use unified plugin registry for kubectl operations const pluginResponse = await (0, plugin_registry_1.invokePluginTool)('agentic-tools', 'kubectl_logs', { resource: name, namespace: namespace, args: args.length > 0 ? args : undefined, }); if (!pluginResponse.success) { const errorMsg = pluginResponse.error?.message || 'Failed to retrieve logs'; await this.sendErrorResponse(res, requestId, HttpStatus.INTERNAL_SERVER_ERROR, 'LOGS_ERROR', 'Failed to retrieve logs', { error: errorMsg }); return; } const result = pluginResponse.result; if (!result.success) { await this.sendErrorResponse(res, requestId, HttpStatus.INTERNAL_SERVER_ERROR, 'LOGS_ERROR', 'Failed to retrieve logs', { error: result.message || 'Unknown error' }); return; } const response = { success: true, data: { logs: result.data, container: container || 'default', containerCount: 1, }, meta: { timestamp: new Date().toISOString(), requestId, version: this.config.version, }, }; await this.sendJsonResponse(res, HttpStatus.OK, response); this.logger.info('Get logs request completed', { requestId, name, namespace, container: container || 'default', logLength: result.data.length, }); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); this.logger.error('Get logs request failed', error instanceof Error ? error : new Error(String(error)), { requestId, errorMessage, }); await this.sendErrorResponse(res, requestId, HttpStatus.INTERNAL_SERVER_ERROR, 'LOGS_ERROR', 'Failed to retrieve logs', { error: errorMessage }); } } /** * Handle prompts list requests */ async handlePromptsListRequest(req, res, requestId) { try { this.logger.info('Processing prompts list request', { requestId }); const result = await (0, prompts_1.handlePromptsListRequest)({}, this.logger, requestId); const response = { success: true, data: result, meta: { timestamp: new Date().toISOString(), requestId, version: this.config.version, }, }; await this.sendJsonResponse(res, HttpStatus.OK, response); this.logger.info('Prompts list request completed', { requestId, promptCo