@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
JavaScript
"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