@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
JavaScript
;
/**
* 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,
});
}