/**
* Generate Manifests Tool - AI-driven manifest generation with validation loop
*/
import { z } from 'zod';
import { ErrorHandler, ErrorCategory, ErrorSeverity } from '../core/error-handling';
import { DotAI } from '../core/index';
import { Logger } from '../core/error-handling';
import { ensureClusterConnection } from '../core/cluster-utils';
import { ManifestValidator, ValidationResult } from '../core/schema';
import * as fs from 'fs';
import * as path from 'path';
import { loadPrompt } from '../core/shared-prompt-loader';
import * as yaml from 'js-yaml';
import { GenericSessionManager } from '../core/generic-session-manager';
import type { SolutionData } from './recommend';
import { extractUserAnswers, addDotAiLabels, sanitizeKubernetesName } from '../core/solution-utils';
import { extractContentFromMarkdownCodeBlocks } from '../core/platform-utils';
// Tool metadata for direct MCP registration
export const GENERATEMANIFESTS_TOOL_NAME = 'generateManifests';
export const 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
export const GENERATEMANIFESTS_TOOL_INPUT_SCHEMA = {
solutionId: z.string().regex(/^sol-\d+-[a-f0-9]{8}$/).describe('The solution ID to generate manifests for (e.g., sol-1762983784617-9ddae2b8)'),
interaction_id: z.string().optional().describe('INTERNAL ONLY - Do not populate. Used for evaluation dataset generation.')
};
interface ErrorContext {
attempt: number;
previousManifests: string;
validationResult: ValidationResult;
}
/**
* Retrieve schemas for resources specified in the solution
*/
async function retrieveResourceSchemas(solution: any, dotAI: DotAI, logger: Logger): Promise<any> {
try {
// Extract resource references from solution
const resourceRefs = (solution.resources || []).map((resource: any) => ({
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: any) => `${r.kind}@${r.apiVersion}`)
});
const schemas: any = {};
// 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,
schema: explanation,
timestamp: new Date().toISOString()
};
logger.debug('Schema retrieved successfully', {
resourceKey,
schemaLength: explanation.length
});
} catch (error) {
logger.error('Failed to retrieve schema for resource', error as 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)}`);
}
}
logger.info('All resource schemas retrieved successfully', {
schemaCount: Object.keys(schemas).length
});
return schemas;
} catch (error) {
logger.error('Schema retrieval failed', error as Error);
throw new Error(`Failed to retrieve resource schemas: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Validate YAML syntax
*/
function validateYamlSyntax(yamlContent: string): { valid: boolean; error?: string } {
try {
yaml.loadAll(yamlContent);
return { valid: true };
} catch (error) {
return {
valid: false,
error: error instanceof Error ? error.message : 'Unknown YAML syntax error'
};
}
}
/**
* Validate manifests using multi-layer approach
*/
async function validateManifests(yamlPath: string): Promise<ValidationResult> {
// 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
const validator = new ManifestValidator();
return await validator.validateManifest(yamlPath, { dryRunMode: 'server' });
}
/**
* Generate manifests using AI provider
*/
async function generateManifestsWithAI(
solution: any,
solutionId: string,
dotAI: DotAI,
logger: Logger,
errorContext?: ErrorContext,
dotAiLabels?: Record<string, string>,
interaction_id?: string
): Promise<string> {
// 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 = loadPrompt('manifest-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 = extractContentFromMarkdownCodeBlocks(response.content, 'yaml');
logger.info('AI manifest generation completed', {
manifestLength: manifestContent.length,
isRetry,
solutionId
});
return manifestContent;
}
/**
* Generate dot-ai application metadata ConfigMap
*/
function generateMetadataConfigMap(solution: any, solutionId: string, userAnswers: Record<string, any>, logger: Logger): string {
const appName = userAnswers.name;
const namespace = userAnswers.namespace || 'default';
const originalIntent = solution.intent;
// Validate required fields (will throw if missing)
const dotAiLabels = addDotAiLabels(undefined, userAnswers, solution);
// Extract resource references from solution
const resources = (solution.resources || []).map((resource: any) => ({
apiVersion: resource.apiVersion,
kind: resource.kind,
name: resource.name || appName, // Use app name as fallback
namespace: resource.namespace || namespace
}));
// Create ConfigMap object
const configMap = {
apiVersion: 'v1',
kind: 'ConfigMap',
metadata: {
name: sanitizeKubernetesName(`dot-ai-app-${appName}-${solutionId}`),
namespace: namespace,
labels: dotAiLabels,
annotations: {
'dot-ai.io/original-intent': originalIntent
}
},
data: {
'deployment-info.yaml': yaml.dump({
appName,
deployedAt: new Date().toISOString(),
originalIntent,
resources
})
}
};
try {
return yaml.dump(configMap);
} catch (error) {
logger.error('Failed to generate YAML for ConfigMap', error as Error, {
configMap,
appName,
solutionId,
namespace
});
throw new Error(`ConfigMap YAML generation failed: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Direct MCP tool handler for generateManifests functionality
*/
export async function handleGenerateManifestsTool(
args: { solutionId: string; interaction_id?: string },
dotAI: DotAI,
logger: Logger,
requestId: string
): Promise<{ content: { type: 'text'; text: string }[] }> {
return await 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 GenericSessionManager<SolutionData>('sol');
logger.debug('Session manager initialized', { requestId });
// Ensure cluster connectivity before proceeding
await ensureClusterConnection(dotAI, logger, requestId, 'GenerateManifestsTool');
// Load solution session
const session = sessionManager.getSession(args.solutionId);
if (!session) {
throw ErrorHandler.createError(
ErrorCategory.VALIDATION,
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,
hasQuestions: !!solution.questions,
primaryResources: solution.resources
});
// 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: ErrorContext | undefined;
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 = extractUserAnswers(solution);
const dotAiLabels = addDotAiLabels(undefined, userAnswers, solution);
// Generate manifests with AI (including labels)
const aiManifests = await generateManifestsWithAI(
solution,
args.solutionId,
dotAI,
logger,
lastError,
dotAiLabels,
args.interaction_id
);
// Generate metadata ConfigMap
const metadataConfigMap = generateMetadataConfigMap(solution, args.solutionId, userAnswers, logger);
// Combine ConfigMap with AI-generated manifests
const manifests = metadataConfigMap + '---\n' + aiManifests;
// 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
const validation = await validateManifests(yamlPath);
if (validation.valid) {
logger.info('Manifest validation successful', {
attempt,
yamlPath,
requestId
});
// Success! Return the validated manifests
const response = {
success: true,
status: 'manifests_generated',
solutionId: args.solutionId,
manifests: manifests,
yamlPath: yamlPath,
validationAttempts: attempt,
timestamp: new Date().toISOString()
};
return {
content: [{
type: 'text' as const,
text: JSON.stringify(response, null, 2)
}]
};
}
// Validation failed, prepare error context for next attempt
// Only pass AI-generated manifests (not ConfigMap) to avoid duplicate ConfigMaps 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 as 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
}
);
}