@vfarcic/dot-ai
Version:
Universal Kubernetes application deployment agent with CLI and MCP interfaces
493 lines (492 loc) • 21.1 kB
JavaScript
;
/**
* Generate Manifests Tool - AI-driven manifest generation with validation loop
*/
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 error_handling_1 = require("../core/error-handling");
const claude_1 = require("../core/claude");
const cluster_utils_1 = require("../core/cluster-utils");
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const yaml = __importStar(require("js-yaml"));
const child_process_1 = require("child_process");
const session_utils_1 = require("../core/session-utils");
const solution_utils_1 = require("../core/solution-utils");
// 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_[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{6}_[a-f0-9]+$/).describe('The solution ID to generate manifests for (e.g., sol_2025-07-01T154349_1e1e242592ff)')
};
/**
* Load solution file and validate structure
*/
function loadSolutionFile(solutionId, sessionDir) {
const solutionPath = path.join(sessionDir, `${solutionId}.json`);
if (!fs.existsSync(solutionPath)) {
throw new Error(`Solution file not found: ${solutionPath}. Available files: ${fs.readdirSync(sessionDir).filter(f => f.endsWith('.json')).join(', ')}`);
}
try {
const content = fs.readFileSync(solutionPath, 'utf8');
const solution = JSON.parse(content);
if (!solution.solutionId || !solution.questions) {
throw new Error(`Invalid solution file structure: ${solutionId}. Missing required fields: solutionId or questions`);
}
return solution;
}
catch (error) {
if (error instanceof SyntaxError) {
throw new Error(`Invalid JSON in solution file: ${solutionId}`);
}
throw error;
}
}
/**
* 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,
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, {
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);
throw new Error(`Failed to retrieve resource schemas: ${error instanceof Error ? error.message : String(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 kubectl dry-run validation
*/
async function runKubectlDryRun(yamlPath) {
return new Promise((resolve) => {
const kubectl = (0, child_process_1.spawn)('kubectl', ['apply', '--dry-run=server', '-f', yamlPath], {
stdio: ['ignore', 'pipe', 'pipe']
});
let stdout = '';
let stderr = '';
kubectl.stdout.on('data', (data) => {
stdout += data.toString();
});
kubectl.stderr.on('data', (data) => {
stderr += data.toString();
});
kubectl.on('close', (code) => {
resolve({
success: code === 0,
yamlSyntaxValid: true, // If we get here, YAML was parseable
kubectlOutput: stderr || stdout,
exitCode: code || 0,
stderr,
stdout
});
});
kubectl.on('error', (error) => {
resolve({
success: false,
yamlSyntaxValid: true,
error: `Failed to run kubectl: ${error.message}`,
kubectlOutput: `kubectl command failed: ${error.message}`,
exitCode: -1,
stderr: error.message,
stdout: ''
});
});
});
}
/**
* Validate manifests using multi-layer approach
*/
async function validateManifests(yamlPath) {
// First check if file exists
if (!fs.existsSync(yamlPath)) {
return {
success: false,
yamlSyntaxValid: false,
error: `Manifest file not found: ${yamlPath}`
};
}
// Read YAML content
const yamlContent = fs.readFileSync(yamlPath, 'utf8');
// 1. YAML syntax validation
const syntaxCheck = validateYamlSyntax(yamlContent);
if (!syntaxCheck.valid) {
return {
success: false,
yamlSyntaxValid: false,
error: `YAML syntax error: ${syntaxCheck.error}`,
kubectlOutput: `YAML parsing failed: ${syntaxCheck.error}`
};
}
// 2. kubectl dry-run validation
return await runKubectlDryRun(yamlPath);
}
/**
* Generate manifests using AI with Claude integration
*/
async function generateManifestsWithAI(solution, dotAI, logger, errorContext, dotAiLabels) {
// Load prompt template
const promptPath = path.join(__dirname, '..', '..', 'prompts', 'manifest-generation.md');
const template = fs.readFileSync(promptPath, 'utf8');
// 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}
**YAML Syntax Valid**: ${errorContext.yamlSyntaxValid}
**kubectl Output**: ${errorContext.kubectlOutput}
**Exit Code**: ${errorContext.exitCode}
**Error Details**: ${errorContext.stderr}
` : 'None - this is the first attempt.';
// Replace template variables
const schemasData = JSON.stringify(resourceSchemas, null, 2);
const labelsData = dotAiLabels ? JSON.stringify(dotAiLabels, null, 2) : '{}';
const aiPrompt = template
.replace('{solution}', solutionData)
.replace('{schemas}', schemasData)
.replace('{previous_attempt}', previousAttempt)
.replace('{error_details}', errorDetails)
.replace('{labels}', labelsData);
const isRetry = !!errorContext;
logger.info('Generating manifests with AI', {
isRetry,
attempt: errorContext?.attempt,
hasErrorContext: !!errorContext,
solutionId: solution.solutionId
});
// Initialize Claude integration
const apiKey = process.env.ANTHROPIC_API_KEY || 'test-key';
const claudeIntegration = new claude_1.ClaudeIntegration(apiKey);
// Send prompt to Claude
const response = await claudeIntegration.sendMessage(aiPrompt);
// Extract YAML content from response
let manifestContent = response.content;
// Try to extract YAML from code blocks if wrapped
const yamlBlockMatch = manifestContent.match(/```(?:yaml|yml)?\s*([\s\S]*?)\s*```/);
if (yamlBlockMatch) {
manifestContent = yamlBlockMatch[1];
}
// Clean up any leading/trailing whitespace
manifestContent = manifestContent.trim();
logger.info('AI manifest generation completed', {
manifestLength: manifestContent.length,
isRetry,
solutionId: solution.solutionId
});
return manifestContent;
}
/**
* Generate dot-ai application metadata ConfigMap
*/
function generateMetadataConfigMap(solution, userAnswers, logger) {
const appName = userAnswers.name;
const namespace = userAnswers.namespace || 'default';
const solutionId = solution.solutionId;
const originalIntent = solution.intent;
// Validate required fields (will throw if missing)
const dotAiLabels = (0, solution_utils_1.addDotAiLabels)(undefined, userAnswers, solution);
// Extract resource references from solution
const resources = (solution.resources || []).map((resource) => ({
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: (0, solution_utils_1.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, {
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
*/
async function handleGenerateManifestsTool(args, dotAI, logger, requestId) {
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
// Get session directory from environment
let sessionDir;
try {
sessionDir = (0, session_utils_1.getAndValidateSessionDirectory)(args, true); // requireWrite=true for manifest generation
logger.debug('Session directory resolved and validated', { sessionDir });
}
catch (error) {
throw error_handling_1.ErrorHandler.createError(error_handling_1.ErrorCategory.VALIDATION, error_handling_1.ErrorSeverity.HIGH, error instanceof Error ? error.message : 'Session directory validation failed', {
operation: 'session_directory_validation',
component: 'GenerateManifestsTool',
requestId,
suggestedActions: [
'Ensure session directory exists and is writable',
'Check directory permissions',
'Verify the directory path is correct',
'Verify DOT_AI_SESSION_DIR environment variable is correctly set'
]
});
}
// Ensure cluster connectivity before proceeding
await (0, cluster_utils_1.ensureClusterConnection)(dotAI, logger, requestId, 'GenerateManifestsTool');
// Load solution file
let solution;
try {
solution = loadSolutionFile(args.solutionId, sessionDir);
logger.debug('Solution file loaded successfully', {
solutionId: args.solutionId,
hasQuestions: !!solution.questions,
primaryResources: solution.resources
});
}
catch (error) {
throw error_handling_1.ErrorHandler.createError(error_handling_1.ErrorCategory.STORAGE, error_handling_1.ErrorSeverity.HIGH, error instanceof Error ? error.message : 'Failed to load solution file', {
operation: 'solution_file_load',
component: 'GenerateManifestsTool',
requestId,
input: { solutionId: args.solutionId, sessionDir },
suggestedActions: [
'Check that the solution ID is correct',
'Verify the solution file exists in the session directory',
'Ensure the solution was fully configured with all stages complete',
'List available solution files in the session directory'
]
});
}
// Prepare file path for manifests
const yamlPath = path.join(sessionDir, `${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, dotAI, logger, lastError, dotAiLabels);
// Generate metadata ConfigMap
const metadataConfigMap = generateMetadataConfigMap(solution, 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.success) {
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',
text: JSON.stringify(response, null, 2)
}]
};
}
// Validation failed, prepare error context for next attempt
lastError = {
attempt,
previousManifests: manifests,
yamlSyntaxValid: validation.yamlSyntaxValid,
kubectlOutput: validation.kubectlOutput,
exitCode: validation.exitCode,
stderr: validation.stderr,
stdout: validation.stdout
};
logger.warn('Manifest validation failed', {
attempt,
maxAttempts,
yamlSyntaxValid: validation.yamlSyntaxValid,
kubectlOutput: validation.kubectlOutput,
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 || '',
yamlSyntaxValid: false,
kubectlOutput: errorMessage,
exitCode: -1,
stderr: errorMessage,
stdout: ''
};
}
}
// If we reach here, all attempts failed
throw new Error(`Failed to generate valid manifests after ${maxAttempts} attempts. Last error: ${lastError?.kubectlOutput}`);
}, {
operation: 'generate_manifests',
component: 'GenerateManifestsTool',
requestId,
input: args
});
}