UNPKG

@vfarcic/dot-ai

Version:

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

1,078 lines (1,077 loc) 47.6 kB
"use strict"; /** * Generate Manifests Tool - AI-driven manifest generation with validation loop * Supports both capability-based solutions (K8s manifests) and Helm-based solutions (values.yaml) */ 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.GENERATEMANIFESTS_TOOL_INPUT_SCHEMA = exports.GENERATEMANIFESTS_TOOL_DESCRIPTION = exports.GENERATEMANIFESTS_TOOL_NAME = void 0; exports.handleGenerateManifestsTool = handleGenerateManifestsTool; const zod_1 = require("zod"); const child_process_1 = require("child_process"); const util_1 = require("util"); const error_handling_1 = require("../core/error-handling"); const index_1 = require("../core/index"); const schema_1 = require("../core/schema"); const fs = __importStar(require("fs")); const path = __importStar(require("path")); const shared_prompt_loader_1 = require("../core/shared-prompt-loader"); const yaml = __importStar(require("js-yaml")); const generic_session_manager_1 = require("../core/generic-session-manager"); const solution_utils_1 = require("../core/solution-utils"); const platform_utils_1 = require("../core/platform-utils"); const crd_availability_1 = require("../core/crd-availability"); const solution_cr_1 = require("../core/solution-cr"); const packaging_1 = require("../core/packaging"); const visualization_1 = require("../core/visualization"); const plugin_registry_1 = require("../core/plugin-registry"); const request_context_1 = require("../interfaces/request-context"); const rbac_1 = require("../core/rbac"); // PRD #359: All helm operations via unified plugin registry /** * PRD #392 Milestone 2 + PRD #395: Build unified agent instructions. * Presents save locally, deploy to cluster, and push to Git as equal options. * RBAC determines whether deploy is available. */ async function buildAgentInstructions(outputPath, outputFormat) { const formatNote = outputFormat === 'helm' ? ' (Helm chart)' : outputFormat === 'kustomize' ? ' (Kustomize overlay)' : ''; const parts = [ `Manifests generated${formatNote}. Present the user with these options:`, `1. **Save locally**: Write the files to "${outputPath}" — no further server call needed, you already have the file contents.`, ]; const identity = (0, request_context_1.getCurrentIdentity)(); const rbacResult = await (0, rbac_1.checkToolAccess)(identity, { toolName: 'recommend', verb: 'apply', }); if (rbacResult.allowed) { parts.push('2. **Deploy to cluster**: Call the recommend tool with stage: "deployManifests" to apply directly.'); } else { parts.push("2. **Deploy to cluster**: Not available — requires 'apply' permission on 'recommend'."); } parts.push('3. **Push to Git** (GitOps): Call the recommend tool with stage: "pushToGit", providing repoUrl and targetPath. Recommended for Argo CD/Flux workflows.'); return parts.join('\n'); } /** * Ensure tmp directory exists */ function ensureTmpDir() { const tmpDir = path.join(process.cwd(), 'tmp'); if (!fs.existsSync(tmpDir)) { fs.mkdirSync(tmpDir, { recursive: true }); } return tmpDir; } /** * Get the path for Helm values file */ function getHelmValuesPath(solutionId) { const tmpDir = path.join(process.cwd(), 'tmp'); return path.join(tmpDir, `${solutionId}-values.yaml`); } /** * Build Helm command string for display to users */ function buildHelmCommandForDisplay(chart, releaseName, namespace, valuesPath) { const parts = [ 'helm upgrade --install', releaseName, `${chart.repositoryName}/${chart.chartName}`, `--namespace ${namespace}`, '--create-namespace', ]; if (chart.version) { parts.push(`--version ${chart.version}`); } if (valuesPath) { parts.push(`-f ${valuesPath}`); } return parts.join(' '); } const execFileAsync = (0, util_1.promisify)(child_process_1.execFile); // PRD #395: Unified nextActions — save locally, deploy, or push to Git as equal options const NEXT_ACTIONS = [ { action: 'saveLocally', description: 'Save files locally (no server call needed — files are in the response)', }, { action: 'deployManifests', description: 'Apply directly to cluster', stage: 'deployManifests', }, { action: 'pushToGit', description: 'Push to Git repository for GitOps (Argo CD, Flux)', stage: 'pushToGit', requiredParams: ['repoUrl', 'targetPath'], }, ]; // Tool metadata for direct MCP registration exports.GENERATEMANIFESTS_TOOL_NAME = 'generateManifests'; exports.GENERATEMANIFESTS_TOOL_DESCRIPTION = 'Generate final Kubernetes manifests from fully configured solution (ONLY after completing ALL stages: required, basic, advanced, and open)'; // Zod schema for MCP registration exports.GENERATEMANIFESTS_TOOL_INPUT_SCHEMA = { solutionId: zod_1.z .string() .regex(/^sol-\d+-[a-f0-9]{8}$/) .describe('The solution ID to generate manifests for (e.g., sol-1762983784617-9ddae2b8)'), interaction_id: zod_1.z .string() .optional() .describe('INTERNAL ONLY - Do not populate. Used for evaluation dataset generation.'), }; /** * Retrieve schemas for resources specified in the solution */ async function retrieveResourceSchemas(solution, dotAI, logger) { try { // Extract resource references from solution const resourceRefs = (solution.resources || []).map((resource) => ({ kind: resource.kind, apiVersion: resource.apiVersion, group: resource.group, })); if (resourceRefs.length === 0) { logger.warn('No resources found in solution for schema retrieval'); return {}; } logger.info('Retrieving schemas for solution resources', { resourceCount: resourceRefs.length, resources: resourceRefs.map((r) => `${r.kind}@${r.apiVersion}`), }); const schemas = {}; // Retrieve schema for each resource for (const resourceRef of resourceRefs) { try { const resourceKey = `${resourceRef.kind}.${resourceRef.apiVersion}`; logger.debug('Retrieving schema', { resourceKey }); // Use discovery engine to explain the resource const explanation = await dotAI.discovery.explainResource(resourceRef.kind); schemas[resourceKey] = { kind: resourceRef.kind, apiVersion: resourceRef.apiVersion, explanation, retrievedAt: new Date().toISOString(), }; logger.debug('Schema retrieved successfully', { resourceKey, schemaLength: explanation.length, }); } catch (error) { logger.error('Failed to retrieve schema for resource', error, { resource: resourceRef, }); // Fail fast - if we can't get schemas, manifest generation will likely fail throw new Error(`Failed to retrieve schema for ${resourceRef.kind}: ${error instanceof Error ? error.message : String(error)}`, { cause: error }); } } logger.info('All resource schemas retrieved successfully', { schemaCount: Object.keys(schemas).length, }); return schemas; } catch (error) { logger.error('Schema retrieval failed', error); throw new Error(`Failed to retrieve resource schemas: ${error instanceof Error ? error.message : String(error)}`, { cause: error }); } } /** * Validate YAML syntax */ function validateYamlSyntax(yamlContent) { try { yaml.loadAll(yamlContent); return { valid: true }; } catch (error) { return { valid: false, error: error instanceof Error ? error.message : 'Unknown YAML syntax error', }; } } /** * Run helm lint on a chart directory */ async function helmLint(chartDir, logger) { try { // Use execFile with array arguments to prevent command injection logger.debug('Running helm lint', { chartDir }); const { stdout, stderr } = await execFileAsync('helm', ['lint', chartDir]); // Parse helm lint output for warnings const warnings = []; const lines = (stdout + stderr).split('\n'); for (const line of lines) { if (line.includes('[WARNING]')) { warnings.push(line.trim()); } } logger.debug('helm lint passed', { warnings: warnings.length }); return { valid: true, errors: [], warnings }; } catch (error) { // helm lint exits with non-zero on errors // Detailed [ERROR] lines are on stdout, summary on stderr const execError = error; const errorOutput = [execError.stdout, execError.stderr, execError.message].filter(Boolean).join('\n') || String(error); const errors = []; const warnings = []; // Parse output for errors and warnings const lines = errorOutput.split('\n'); for (const line of lines) { if (line.includes('[ERROR]')) { errors.push(line.trim()); } else if (line.includes('[WARNING]')) { warnings.push(line.trim()); } } // If no specific errors found, use the full output if (errors.length === 0) { errors.push(errorOutput.trim()); } logger.warn('helm lint failed', { errors, warnings }); return { valid: false, errors, warnings }; } } /** * Validate manifests using multi-layer approach * PRD #359: Uses unified plugin registry for kubectl operations */ async function validateManifests(yamlPath) { // First check if file exists if (!fs.existsSync(yamlPath)) { return { valid: false, errors: [`Manifest file not found: ${yamlPath}`], warnings: [], }; } // Read YAML content for syntax validation const yamlContent = fs.readFileSync(yamlPath, 'utf8'); // 1. YAML syntax validation const syntaxCheck = validateYamlSyntax(yamlContent); if (!syntaxCheck.valid) { return { valid: false, errors: [`YAML syntax error: ${syntaxCheck.error}`], warnings: [], }; } // 2. kubectl dry-run validation using ManifestValidator // PRD #359: Uses unified plugin registry for kubectl operations const validator = new schema_1.ManifestValidator(); return await validator.validateManifest(yamlPath, { dryRunMode: 'server' }); } /** * Generate manifests using AI provider */ async function generateManifestsWithAI(solution, solutionId, dotAI, logger, errorContext, dotAiLabels, interaction_id) { // Retrieve schemas for solution resources const resourceSchemas = await retrieveResourceSchemas(solution, dotAI, logger); // Prepare template variables const solutionData = JSON.stringify(solution, null, 2); const previousAttempt = errorContext ? ` ### Generated Manifests: \`\`\`yaml ${errorContext.previousManifests} \`\`\` ` : 'None - this is the first attempt.'; const errorDetails = errorContext ? ` **Attempt**: ${errorContext.attempt} **Validation Errors**: ${errorContext.validationResult.errors.join(', ')} **Validation Warnings**: ${errorContext.validationResult.warnings.join(', ')} ` : 'None - this is the first attempt.'; // Prepare template variables const schemasData = JSON.stringify(resourceSchemas, null, 2); const labelsData = dotAiLabels ? JSON.stringify(dotAiLabels, null, 2) : '{}'; const aiPrompt = (0, shared_prompt_loader_1.loadPrompt)('capabilities-generation', { solution: solutionData, schemas: schemasData, previous_attempt: previousAttempt, error_details: errorDetails, labels: labelsData, }); const isRetry = !!errorContext; logger.info('Generating manifests with AI', { isRetry, attempt: errorContext?.attempt, hasErrorContext: !!errorContext, solutionId, }); // Get AI provider from dotAI const aiProvider = dotAI.ai; // Send prompt to AI const response = await aiProvider.sendMessage(aiPrompt, 'recommend-manifests-generation', { user_intent: solution.initialIntent || 'Kubernetes manifest generation', interaction_id: interaction_id, }); // Extract YAML content from response // Use shared utility to extract from code blocks if wrapped const manifestContent = (0, platform_utils_1.extractContentFromMarkdownCodeBlocks)(response.content, 'yaml'); logger.info('AI manifest generation completed', { manifestLength: manifestContent.length, isRetry, solutionId, }); return manifestContent; } /** * Generate Helm values.yaml using AI provider */ async function generateHelmValuesWithAI(solution, solutionId, dotAI, logger, errorContext, interaction_id) { // Fetch chart values.yaml for reference const chart = solution.chart; const { valuesYaml } = await dotAI.schema.fetchHelmChartContent(chart); // Prepare template variables const solutionData = JSON.stringify(solution, null, 2); const previousAttempt = errorContext ? ` ### Generated Values: \`\`\`yaml ${errorContext.previousValues} \`\`\` ` : 'None - this is the first attempt.'; const errorDetails = errorContext ? ` **Attempt**: ${errorContext.attempt} **Validation Errors**: ${errorContext.validationResult.errors.join(', ')} **Validation Warnings**: ${errorContext.validationResult.warnings.join(', ')} ` : 'None - this is the first attempt.'; const aiPrompt = (0, shared_prompt_loader_1.loadPrompt)('helm-generation', { solution: solutionData, chart_values: valuesYaml || '# No default values available', previous_attempt: previousAttempt, error_details: errorDetails, }); const isRetry = !!errorContext; logger.info('Generating Helm values with AI', { isRetry, attempt: errorContext?.attempt, hasErrorContext: !!errorContext, solutionId, chart: `${chart.repositoryName}/${chart.chartName}`, }); // Get AI provider from dotAI const aiProvider = dotAI.ai; // Send prompt to AI const response = await aiProvider.sendMessage(aiPrompt, 'helm-values-generation', { user_intent: solution.intent || 'Helm chart installation', interaction_id: interaction_id, }); // Extract YAML content from response const valuesContent = (0, platform_utils_1.extractContentFromMarkdownCodeBlocks)(response.content, 'yaml'); logger.info('AI Helm values generation completed', { valuesLength: valuesContent.length, isRetry, solutionId, }); return valuesContent; } /** * Validate Helm installation using dry-run via plugin * PRD #343: All Helm operations go through plugin system */ /** * PRD #359: Uses unified plugin registry for helm operations */ async function validateHelmInstallation(chart, releaseName, namespace, valuesYaml, logger) { logger.info('Running Helm dry-run validation via plugin', { chart: `${chart.repositoryName}/${chart.chartName}`, releaseName, namespace, }); try { // PRD #359: First, add/update the Helm repository via unified registry const repoResult = await (0, plugin_registry_1.invokePluginTool)('agentic-tools', 'helm_repo_add', { name: chart.repositoryName, url: chart.repository, }); if (!repoResult.success) { logger.warn('Helm repo add failed', { error: repoResult.error?.message }); return { valid: false, errors: [repoResult.error?.message || 'Failed to add Helm repository'], warnings: [], }; } // Run helm install with dry-run const installResult = await (0, plugin_registry_1.invokePluginTool)('agentic-tools', 'helm_install', { releaseName, chart: `${chart.repositoryName}/${chart.chartName}`, namespace, values: valuesYaml, version: chart.version, dryRun: true, createNamespace: true, }); if (installResult.success) { logger.info('Helm dry-run validation successful'); return { valid: true, errors: [], warnings: [], }; } logger.warn('Helm dry-run validation failed', { error: installResult.error?.message, }); return { valid: false, errors: [installResult.error?.message || 'Unknown Helm validation error'], warnings: [], }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); logger.error('Helm validation error', error); return { valid: false, errors: [errorMessage], warnings: [], }; } } /** * Handle Helm solution generation * PRD #343: pluginManager required for Helm operations */ async function handleHelmGeneration(solution, solutionId, dotAI, logger, requestId, sessionManager, pluginManager, interaction_id) { const maxAttempts = 10; const chart = solution.chart; const userAnswers = (0, solution_utils_1.extractUserAnswers)(solution); // Extract release name and namespace from answers const releaseName = userAnswers.name; const namespace = userAnswers.namespace || 'default'; if (!releaseName) { throw error_handling_1.ErrorHandler.createError(error_handling_1.ErrorCategory.VALIDATION, error_handling_1.ErrorSeverity.HIGH, 'Release name (name) is required for Helm installation', { operation: 'helm_generation', component: 'GenerateManifestsTool', requestId, suggestedActions: [ 'Ensure the "name" question was answered in the configuration', ], }); } // Prepare file paths using shared utilities ensureTmpDir(); const valuesPath = getHelmValuesPath(solutionId); // AI generation and validation loop let lastError; for (let attempt = 1; attempt <= maxAttempts; attempt++) { logger.info('Helm values generation attempt', { attempt, maxAttempts, isRetry: attempt > 1, requestId, chart: `${chart.repositoryName}/${chart.chartName}`, }); try { // Generate values.yaml with AI const valuesYaml = await generateHelmValuesWithAI(solution, solutionId, dotAI, logger, lastError, interaction_id); // Save values to file fs.writeFileSync(valuesPath, valuesYaml, 'utf8'); logger.info('Helm values saved to file', { valuesPath, attempt, requestId, }); // Save attempt for debugging const attemptPath = valuesPath.replace('.yaml', `_attempt_${attempt.toString().padStart(2, '0')}.yaml`); fs.writeFileSync(attemptPath, valuesYaml, 'utf8'); // Validate with helm dry-run via plugin // PRD #359: Uses unified plugin registry - no pluginManager needed const validation = await validateHelmInstallation(chart, releaseName, namespace, valuesYaml, logger); if (validation.valid) { logger.info('Helm validation successful', { attempt, valuesPath, requestId, }); // Build user-friendly helm command with generic values file path // (internal valuesPath is used for actual execution, not shown to user) const helmCommand = buildHelmCommandForDisplay(chart, releaseName, namespace, 'values.yaml'); // PRD #320: Update session with generateManifests data for visualization sessionManager.updateSession(solutionId, { ...solution, stage: 'manifests', generatedManifests: { type: 'helm', valuesYaml: valuesYaml, helmCommand: helmCommand, chart: { repository: chart.repository, repositoryName: chart.repositoryName, chartName: chart.chartName, version: chart.version || 'latest', }, releaseName: releaseName, namespace: namespace, validationAttempts: attempt, }, }); // PRD #320: Generate visualization URL const visualizationUrl = (0, visualization_1.getVisualizationUrl)(solutionId); const response = { success: true, status: 'helm_command_generated', solutionId: solutionId, solutionType: 'helm', helmCommand: helmCommand, valuesYaml: valuesYaml, chart: { repository: chart.repository, repositoryName: chart.repositoryName, chartName: chart.chartName, version: chart.version, }, releaseName: releaseName, namespace: namespace, validationAttempts: attempt, timestamp: new Date().toISOString(), nextActions: [NEXT_ACTIONS[0], NEXT_ACTIONS[1]], // saveLocally + deployManifests for Helm (pushToGit not yet supported) ...(visualizationUrl ? { visualizationUrl } : {}), }; // Build content blocks - JSON for REST API, agent instruction for MCP agents const content = [ { type: 'text', text: JSON.stringify(response, null, 2), }, ]; // Add agent instruction block if visualization URL is present const agentDisplayBlock = (0, index_1.buildAgentDisplayBlock)({ visualizationUrl }); if (agentDisplayBlock) { content.push(agentDisplayBlock); } return { content }; } // Validation failed, prepare error context for next attempt lastError = { attempt, previousValues: valuesYaml, validationResult: validation, }; logger.warn('Helm validation failed', { attempt, maxAttempts, validationErrors: validation.errors, validationWarnings: validation.warnings, requestId, }); } catch (error) { logger.error('Error during Helm values generation attempt', error); if (attempt === maxAttempts) { throw error; } // Prepare error context for retry lastError = { attempt, previousValues: lastError?.previousValues || '', validationResult: { valid: false, errors: [error instanceof Error ? error.message : String(error)], warnings: [], }, }; } } // All attempts failed throw new Error(`Failed to generate valid Helm values after ${maxAttempts} attempts. Last errors: ${lastError?.validationResult.errors.join(', ')}`); } /** * Render packaged output to raw YAML for validation */ async function renderPackageToYaml(packageDir, format, logger) { try { // Use execFile with array arguments to prevent command injection const args = format === 'helm' ? ['template', 'test-release', packageDir] : ['kustomize', packageDir]; const command = format === 'helm' ? 'helm' : 'kubectl'; logger.debug('Rendering package to YAML', { format, command, args }); const { stdout, stderr } = await execFileAsync(command, args); if (stderr && !stdout) { return { success: false, error: stderr }; } return { success: true, yaml: stdout }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); // Check for terminal infrastructure errors that won't be fixed by AI retrying const terminalErrorPatterns = [ 'not found', // command not found 'command not found', // explicit command not found 'ENOENT', // file/command doesn't exist 'permission denied', // permission issues 'EACCES', // access denied ]; const isTerminalError = terminalErrorPatterns.some(pattern => errorMessage.toLowerCase().includes(pattern.toLowerCase())); return { success: false, error: errorMessage, isTerminalError, // Signal to caller to not retry }; } } /** * Write package files to a temporary directory */ function writePackageFiles(files, baseDir) { const resolvedBase = path.resolve(baseDir); for (const file of files) { const filePath = path.join(baseDir, file.relativePath); const resolvedPath = path.resolve(filePath); // Prevent path traversal attacks if (!resolvedPath.startsWith(resolvedBase)) { throw new Error(`Invalid file path: ${file.relativePath} would escape base directory`); } const fileDir = path.dirname(filePath); if (!fs.existsSync(fileDir)) { fs.mkdirSync(fileDir, { recursive: true }); } fs.writeFileSync(filePath, file.content, 'utf8'); } } /** * Package manifests and validate the output * PRD #343: pluginManager required for kubectl operations */ async function packageAndValidate(rawManifests, solution, outputFormat, outputPath, solutionId, dotAI, logger, pluginManager, interaction_id) { const maxAttempts = 5; let packagingError; const tmpDir = path.join(process.cwd(), 'tmp'); const packageDir = path.join(tmpDir, `${solutionId}-${outputFormat}`); // Helper to cleanup temp directory const cleanupPackageDir = () => { if (fs.existsSync(packageDir)) { try { fs.rmSync(packageDir, { recursive: true }); } catch (cleanupError) { logger.warn('Failed to cleanup temp package directory', { packageDir, error: cleanupError, }); } } }; for (let attempt = 1; attempt <= maxAttempts; attempt++) { logger.info('Packaging attempt', { attempt, maxAttempts, format: outputFormat, }); try { const packagingResult = await (0, packaging_1.packageManifests)(rawManifests, // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Different SolutionData types between modules solution, outputFormat, outputPath, dotAI, logger, packagingError, interaction_id); // Write files to temp directory if (fs.existsSync(packageDir)) { fs.rmSync(packageDir, { recursive: true }); } fs.mkdirSync(packageDir, { recursive: true }); writePackageFiles(packagingResult.files, packageDir); // Run helm lint for Helm charts (catches structural issues before rendering) if (outputFormat === 'helm') { const lintResult = await helmLint(packageDir, logger); if (!lintResult.valid) { packagingError = { attempt, previousOutput: JSON.stringify(packagingResult.files.map(f => f.relativePath)), validationError: `helm lint failed: ${lintResult.errors.join(', ')}`, }; logger.warn('helm lint failed', { attempt, errors: lintResult.errors, }); continue; } // Log warnings but don't fail on them if (lintResult.warnings.length > 0) { logger.info('helm lint warnings', { warnings: lintResult.warnings }); } } // Render to raw YAML const renderResult = await renderPackageToYaml(packageDir, outputFormat, logger); if (!renderResult.success) { // Check for terminal infrastructure errors - fail fast, don't retry if (renderResult.isTerminalError) { const terminalError = new Error(`Infrastructure error (not retryable): ${renderResult.error}`); logger.error('Terminal infrastructure error - cannot retry', terminalError, { format: outputFormat, }); throw terminalError; } packagingError = { attempt, previousOutput: JSON.stringify(packagingResult.files.map(f => f.relativePath)), validationError: `Failed to render ${outputFormat}: ${renderResult.error}`, }; logger.warn('Package render failed', { attempt, error: renderResult.error, }); continue; } // Validate rendered YAML const renderedYamlPath = path.join(tmpDir, `${solutionId}-${outputFormat}-rendered.yaml`); if (!renderResult.yaml) { throw new Error('Render succeeded but no YAML content returned'); } fs.writeFileSync(renderedYamlPath, renderResult.yaml, 'utf8'); const validation = await validateManifests(renderedYamlPath); if (validation.valid) { logger.info('Package validation successful', { format: outputFormat, attempt, }); cleanupPackageDir(); return { files: packagingResult.files, attempts: attempt }; } packagingError = { attempt, previousOutput: JSON.stringify(packagingResult.files.map(f => f.relativePath)), validationError: validation.errors.join(', '), }; logger.warn('Package validation failed', { attempt, errors: validation.errors, }); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); logger.error('Packaging attempt failed', error); if (attempt === maxAttempts) { throw error; } packagingError = { attempt, previousOutput: packagingError?.previousOutput || '', validationError: errorMessage, }; } } cleanupPackageDir(); throw new Error(`Failed to generate valid ${outputFormat} package after ${maxAttempts} attempts. Last error: ${packagingError?.validationError}`); } /** * Direct MCP tool handler for generateManifests functionality * PRD #343: pluginManager required for kubectl operations */ async function handleGenerateManifestsTool(args, dotAI, logger, requestId, pluginManager) { return await error_handling_1.ErrorHandler.withErrorHandling(async () => { const maxAttempts = 10; logger.debug('Handling generateManifests request', { requestId, solutionId: args?.solutionId, }); // Input validation is handled automatically by MCP SDK with Zod schema // args are already validated and typed when we reach this point // Initialize session manager const sessionManager = new generic_session_manager_1.GenericSessionManager('sol'); logger.debug('Session manager initialized', { requestId }); // Load solution session const session = sessionManager.getSession(args.solutionId); if (!session) { throw error_handling_1.ErrorHandler.createError(error_handling_1.ErrorCategory.VALIDATION, error_handling_1.ErrorSeverity.HIGH, `Solution not found: ${args.solutionId}`, { operation: 'solution_loading', component: 'GenerateManifestsTool', requestId, input: { solutionId: args.solutionId }, suggestedActions: [ 'Verify the solution ID is correct', 'Ensure the solution was created by the recommend tool', 'Ensure all configuration stages were completed', 'Check that the session has not expired', ], }); } const solution = session.data; logger.debug('Solution loaded successfully', { solutionId: args.solutionId, solutionType: solution.type, hasQuestions: !!solution.questions, primaryResources: solution.resources, }); // Branch based on solution type if (solution.type === 'helm') { logger.info('Detected Helm solution, using Helm generation flow', { solutionId: args.solutionId, chart: solution.chart ? `${solution.chart.repositoryName}/${solution.chart.chartName}` : 'unknown', }); return await handleHelmGeneration(solution, args.solutionId, dotAI, logger, requestId, sessionManager, pluginManager, args.interaction_id); } // Capability-based solution: Generate Kubernetes manifests logger.info('Using capability-based manifest generation flow', { solutionId: args.solutionId, }); // Prepare file path for manifests (store in tmp directory) const tmpDir = path.join(process.cwd(), 'tmp'); if (!fs.existsSync(tmpDir)) { fs.mkdirSync(tmpDir, { recursive: true }); } const yamlPath = path.join(tmpDir, `${args.solutionId}.yaml`); // AI generation and validation loop let lastError; for (let attempt = 1; attempt <= maxAttempts; attempt++) { logger.info('AI manifest generation attempt', { attempt, maxAttempts, isRetry: attempt > 1, requestId, }); try { // Extract user answers and generate required labels const userAnswers = (0, solution_utils_1.extractUserAnswers)(solution); const dotAiLabels = (0, solution_utils_1.addDotAiLabels)(undefined, userAnswers, solution); // Generate manifests with AI (including labels) const aiManifests = await generateManifestsWithAI(solution, args.solutionId, dotAI, logger, lastError, dotAiLabels, args.interaction_id); // Check if Solution CRD is available and generate Solution CR if present // PRD #359: Uses unified plugin registry - no pluginManager param needed let solutionCR = ''; try { const crdAvailable = await (0, crd_availability_1.isSolutionCRDAvailable)(); if (crdAvailable) { solutionCR = (0, solution_cr_1.generateSolutionCR)({ solutionId: args.solutionId, namespace: userAnswers.namespace || 'default', solution: solution, generatedManifestsYaml: aiManifests, }); logger.info('Solution CR generated successfully', { solutionId: args.solutionId, }); } else { logger.info('Solution CRD not available, skipping Solution CR generation (graceful degradation)', { solutionId: args.solutionId }); } } catch (error) { logger.warn('Failed to check CRD availability or generate Solution CR, skipping', { solutionId: args.solutionId, error: error instanceof Error ? error.message : String(error), }); // Graceful degradation - continue without Solution CR } // Combine all manifests (Solution CR + AI manifests) const manifestParts = []; if (solutionCR) { manifestParts.push(solutionCR); } manifestParts.push(aiManifests); const manifests = manifestParts.length > 1 ? manifestParts.join('---\n') : manifestParts[0]; // Save manifests to file fs.writeFileSync(yamlPath, manifests, 'utf8'); logger.info('Manifests saved to file', { yamlPath, attempt, requestId, }); // Save a copy of this attempt for debugging const attemptPath = yamlPath.replace('.yaml', `_attempt_${attempt.toString().padStart(2, '0')}.yaml`); fs.writeFileSync(attemptPath, manifests, 'utf8'); logger.info('Saved manifest attempt for debugging', { attempt, attemptPath, requestId, }); // Validate manifests // PRD #359: Uses unified plugin registry for kubectl operations const validation = await validateManifests(yamlPath); if (validation.valid) { logger.info('Manifest validation successful', { attempt, yamlPath, requestId, }); // Extract packaging options from user answers (with defaults) const outputFormat = (userAnswers.outputFormat || 'raw'); const outputPath = userAnswers.outputPath || './manifests'; // Handle packaging based on outputFormat if (outputFormat === 'helm' || outputFormat === 'kustomize') { const packagingResult = await packageAndValidate(manifests, solution, outputFormat, outputPath, args.solutionId, dotAI, logger, pluginManager, args.interaction_id); // PRD #320: Update session with generateManifests data for visualization sessionManager.updateSession(args.solutionId, { ...solution, stage: 'manifests', generatedManifests: { type: outputFormat, outputPath, files: packagingResult.files, validationAttempts: attempt, packagingAttempts: packagingResult.attempts, }, }); // PRD #320: Generate visualization URL const visualizationUrl = (0, visualization_1.getVisualizationUrl)(args.solutionId); const response = { success: true, status: 'manifests_generated', solutionId: args.solutionId, outputFormat, outputPath, files: packagingResult.files, validationAttempts: attempt, packagingAttempts: packagingResult.attempts, timestamp: new Date().toISOString(), nextActions: NEXT_ACTIONS, agentInstructions: await buildAgentInstructions(outputPath, outputFormat), ...(visualizationUrl ? { visualizationUrl } : {}), }; // Build content blocks - JSON for REST API, agent instruction for MCP agents const content = [ { type: 'text', text: JSON.stringify(response, null, 2), }, ]; // Add agent instruction block if visualization URL is present const agentDisplayBlock = (0, index_1.buildAgentDisplayBlock)({ visualizationUrl, }); if (agentDisplayBlock) { content.push(agentDisplayBlock); } return { content }; } // PRD #320: Update session with generateManifests data for visualization (raw format) sessionManager.updateSession(args.solutionId, { ...solution, stage: 'manifests', generatedManifests: { type: 'raw', outputPath, files: [{ relativePath: 'manifests.yaml', content: manifests }], validationAttempts: attempt, }, }); // PRD #320: Generate visualization URL const visualizationUrl = (0, visualization_1.getVisualizationUrl)(args.solutionId); // Raw format - return manifests as-is const response = { success: true, status: 'manifests_generated', solutionId: args.solutionId, outputFormat, outputPath, files: [{ relativePath: 'manifests.yaml', content: manifests }], validationAttempts: attempt, timestamp: new Date().toISOString(), nextActions: NEXT_ACTIONS, agentInstructions: await buildAgentInstructions(outputPath, outputFormat), ...(visualizationUrl ? { visualizationUrl } : {}), }; // Build content blocks - JSON for REST API, agent instruction for MCP agents const content = [ { type: 'text', text: JSON.stringify(response, null, 2), }, ]; // Add agent instruction block if visualization URL is present const agentDisplayBlock = (0, index_1.buildAgentDisplayBlock)({ visualizationUrl, }); if (agentDisplayBlock) { content.push(agentDisplayBlock); } return { content }; } // Validation failed, prepare error context for next attempt // Only pass AI-generated manifests to avoid duplicates on retry lastError = { attempt, previousManifests: aiManifests, validationResult: validation, }; logger.warn('Manifest validation failed', { attempt, maxAttempts, validationErrors: validation.errors, validationWarnings: validation.warnings, requestId, }); } catch (error) { logger.error('Error during manifest generation attempt', error); // Check if this is a validation error that should not be retried const errorMessage = error instanceof Error ? error.message : 'Unknown error'; const isValidationError = errorMessage.includes('Application name is required') || errorMessage.includes('Application intent is required'); // If this is a validation error or the last attempt, throw the error immediately if (isValidationError || attempt === maxAttempts) { throw error; } // Prepare error context for retry lastError = { attempt, previousManifests: lastError?.previousManifests || '', validationResult: { valid: false, errors: [errorMessage], warnings: [], }, }; } } // If we reach here, all attempts failed throw new Error(`Failed to generate valid manifests after ${maxAttempts} attempts. Last errors: ${lastError?.validationResult.errors.join(', ')}`); }, { operation: 'generate_manifests', component: 'GenerateManifestsTool', requestId, input: args, }); }