import { z } from 'zod';
import { executeAzCommand, CommandResult } from '../lib/cli-executor.js';
import { sanitizeInput, generateCommandSummary, RiskLevel } from '../lib/safety.js';
import { createAuditContext } from '../lib/audit.js';
import { getOperatorInfo } from '../lib/config.js';
import { logger } from '../lib/logger.js';
export type PlanStatus = 'WAITING_FOR_CONFIRMATION' | 'EXECUTED' | 'FAILED' | 'REJECTED';
export const ManageAzureResourcesSchema = z.object({
command: z.string().describe('Azure CLI command to execute'),
explanation: z.string().describe('Why this command was chosen'),
execute_now: z.boolean().default(false).describe('If true, execute; if false, plan only'),
});
export type ManageAzureResourcesInput = z.infer<typeof ManageAzureResourcesSchema>;
export interface PlanResponse {
proposed_command: string;
risk_level: RiskLevel;
summary: string;
explanation: string;
status: PlanStatus;
warnings: string[];
next_steps: string;
correlation_id?: string;
operator?: { email?: string; name?: string };
}
export interface ExecutionResponse {
executed_command: string;
status: PlanStatus;
success: boolean;
output?: unknown;
raw_output?: string;
error?: string;
stderr?: string;
error_analysis?: string;
correlation_id: string;
operator?: { email?: string; name?: string };
}
const ERROR_PATTERNS = new Map<RegExp, string>([
[/ResourceNotFound|NotFound/, 'Resource not found. Verify name and resource group.'],
[/ResourceGroup.*not found/i, 'Resource group not found. Create it first: az group create'],
[/AADSTS|authentication|login/i, 'Auth issue. Run "az login" to authenticate.'],
[/subscription.*not found/i, 'Subscription not found. List available: az account list'],
[/AuthorizationFailed|does not have authorization/i, 'Permission denied. Check RBAC role assignments.'],
[/QuotaExceeded|quota/i, 'Quota exceeded. Request increase or change region/SKU.'],
[/InvalidParameter|BadRequest/i, 'Invalid parameter. Review command syntax.'],
[/already exists|AlreadyExists|Conflict/i, 'Resource exists. Use different name or manage existing.'],
]);
function analyzeError(stderr: string): string {
for (const [pattern, suggestion] of ERROR_PATTERNS) {
if (pattern.test(stderr)) return suggestion;
}
return 'Review error message and adjust parameters.';
}
export async function handleManageAzureResources(
input: ManageAzureResourcesInput
): Promise<PlanResponse | ExecutionResponse> {
const { command, explanation, execute_now } = input;
const operator = getOperatorInfo();
const validation = sanitizeInput(command);
if (!validation.isValid) {
logger.warn('Command validation failed', { command, error: validation.error });
return {
proposed_command: command,
risk_level: 'high',
summary: 'Validation failed',
explanation,
status: 'REJECTED',
warnings: [validation.error || 'Unknown error'],
next_steps: 'Provide a valid Azure CLI command.',
operator,
};
}
const cmd = validation.sanitizedCommand!;
const risk = validation.riskLevel;
const audit = createAuditContext(cmd, risk, execute_now ? 'execute' : 'plan');
if (!execute_now) {
const summary = generateCommandSummary(cmd);
const nextSteps = risk === 'high'
? '⚠️ HIGH RISK: Review carefully before execute_now=true'
: 'Call again with execute_now=true to execute.';
logger.command('plan', cmd, 'success', { riskLevel: risk, correlationId: audit.correlationId });
return {
proposed_command: cmd,
risk_level: risk,
summary,
explanation,
status: 'WAITING_FOR_CONFIRMATION',
warnings: validation.warnings,
next_steps: nextSteps,
correlation_id: audit.correlationId,
operator,
};
}
logger.info('Executing', { command: cmd, correlationId: audit.correlationId });
const result: CommandResult = await executeAzCommand(cmd, { applyScope: true, enableRetry: true });
if (result.success) {
await audit.logSuccess();
logger.command('execute', cmd, 'success', { correlationId: audit.correlationId });
return {
executed_command: cmd,
status: 'EXECUTED',
success: true,
output: result.parsedOutput ?? result.stdout,
raw_output: result.stdout,
correlation_id: audit.correlationId,
operator,
};
}
const analysis = analyzeError(result.stderr);
await audit.logFailure(result.stderr);
logger.command('execute', cmd, 'failure', { correlationId: audit.correlationId, error: result.stderr });
return {
executed_command: cmd,
status: 'FAILED',
success: false,
error: result.stderr || 'Execution failed',
stderr: result.stderr,
error_analysis: analysis,
correlation_id: audit.correlationId,
operator,
};
}
export const manageAzureResourcesTool = {
name: 'manage_azure_resources',
description: `Primary tool for all Azure operations via CLI.
FLOW: 1) Call with execute_now=false for plan 2) Review risk 3) Call with execute_now=true to execute
SAFETY: Commands validated for injection. Destructive ops flagged HIGH risk.
AUDIT: All ops logged with operator email and correlation ID.`,
inputSchema: {
type: 'object',
properties: {
command: { type: 'string', description: 'Azure CLI command (e.g., "az aks create ...")' },
explanation: { type: 'string', description: 'Why this command was chosen' },
execute_now: { type: 'boolean', description: 'false: plan only, true: execute', default: false },
},
required: ['command', 'explanation'],
},
};