// ============================================================================= // Startup validation for seller runtime. // Validates required environment variables and offering modules before starting. // ============================================================================= import * as fs from "fs"; import * as path from "path"; import { fileURLToPath } from "url"; import { listOfferings } from "./offerings.js"; import { createLogger } from "./logger.js"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const logger = createLogger("startup"); export interface ValidationResult { valid: boolean; errors: string[]; warnings: string[]; } interface EnvVarCheck { name: string; required: boolean; description: string; } /** * Environment variables required or recommended for seller runtime. */ const ENV_VARS: EnvVarCheck[] = [ { name: "LITE_AGENT_API_KEY", required: true, description: "API key for authenticating with ACP backend", }, { name: "ACP_SOCKET_URL", required: false, description: "WebSocket URL for ACP backend (defaults to production)", }, { name: "PAGERDUTY_ROUTING_KEY", required: false, description: "PagerDuty routing key for critical alerts", }, { name: "GUARDIAN_INTERNAL_API_TOKEN", required: true, description: "Internal API token for Guardian scan offerings (HMAC signing)", }, { name: "NEXT_PUBLIC_SUPABASE_URL", required: false, description: "Supabase URL for database operations", }, { name: "SUPABASE_SERVICE_ROLE_KEY", required: false, description: "Supabase service role key for database operations", }, ]; /** * Validate environment variables are set correctly. */ function validateEnvironmentVariables(): { errors: string[]; warnings: string[]; } { const errors: string[] = []; const warnings: string[] = []; for (const envVar of ENV_VARS) { const value = process.env[envVar.name]; if (!value || value.trim() === "") { if (envVar.required) { errors.push( `Missing required environment variable: ${envVar.name} (${envVar.description})`, ); } else { warnings.push( `Optional environment variable not set: ${envVar.name} (${envVar.description})`, ); } } else { logger.debug(`Environment variable validated`, { envVar: envVar.name }); } } return { errors, warnings }; } /** * Validate that offering modules exist and are loadable for the given agent. */ function validateOfferings(agentDirName: string): { errors: string[]; warnings: string[]; } { const errors: string[] = []; const warnings: string[] = []; try { const offerings = listOfferings(agentDirName); if (offerings.length === 0) { warnings.push( `No offerings found for agent "${agentDirName}". Seller will not accept any jobs.`, ); return { errors, warnings }; } logger.info(`Validating ${offerings.length} offering(s)`, { agentDirName, offeringCount: offerings.length, }); // Validate each offering has required files const offeringsRoot = path.resolve( __dirname, "..", "offerings", agentDirName, ); for (const offering of offerings) { const offeringDir = path.join(offeringsRoot, offering); const configPath = path.join(offeringDir, "offering.json"); const handlersPath = path.join(offeringDir, "handlers.ts"); if (!fs.existsSync(configPath)) { errors.push( `Offering "${offering}" missing offering.json at ${configPath}`, ); } if (!fs.existsSync(handlersPath)) { errors.push( `Offering "${offering}" missing handlers.ts at ${handlersPath}`, ); } // Try to parse offering.json if (fs.existsSync(configPath)) { try { const content = fs.readFileSync(configPath, "utf-8"); const config = JSON.parse(content); if (!config.name) { errors.push( `Offering "${offering}" offering.json missing required field: name`, ); } } catch (err) { errors.push( `Offering "${offering}" offering.json is not valid JSON: ${err}`, ); } } } } catch (err) { errors.push( `Failed to validate offerings: ${err instanceof Error ? err.message : String(err)}`, ); } return { errors, warnings }; } /** * Validate Node.js version meets minimum requirements. */ function validateNodeVersion(): { errors: string[]; warnings: string[] } { const errors: string[] = []; const warnings: string[] = []; const nodeVersion = process.version; const majorVersion = parseInt(nodeVersion.slice(1).split(".")[0], 10); logger.debug(`Node.js version detected`, { nodeVersion, majorVersion }); if (majorVersion < 18) { errors.push( `Node.js version ${nodeVersion} is too old. Minimum required: v18.0.0`, ); } else if (majorVersion < 20) { warnings.push( `Node.js version ${nodeVersion} is supported but v20+ is recommended`, ); } return { errors, warnings }; } /** * Run all startup validations. * Returns a validation result with errors and warnings. */ export function validateStartup(agentDirName: string): ValidationResult { logger.info("Running startup validation", { agentDirName }); const allErrors: string[] = []; const allWarnings: string[] = []; // 1. Validate Node version const nodeCheck = validateNodeVersion(); allErrors.push(...nodeCheck.errors); allWarnings.push(...nodeCheck.warnings); // 2. Validate environment variables const envCheck = validateEnvironmentVariables(); allErrors.push(...envCheck.errors); allWarnings.push(...envCheck.warnings); // 3. Validate offerings const offeringCheck = validateOfferings(agentDirName); allErrors.push(...offeringCheck.errors); allWarnings.push(...offeringCheck.warnings); const valid = allErrors.length === 0; if (!valid) { logger.error("Startup validation failed", { errorCount: allErrors.length, warningCount: allWarnings.length, }); for (const error of allErrors) { logger.error(` - ${error}`); } } else { logger.info("Startup validation passed", { warningCount: allWarnings.length, }); } if (allWarnings.length > 0) { logger.warn(`Startup validation warnings (${allWarnings.length})`); for (const warning of allWarnings) { logger.warn(` - ${warning}`); } } return { valid, errors: allErrors, warnings: allWarnings, }; } /** * Run startup validation and exit if it fails. */ export function validateStartupOrExit(agentDirName: string): void { const result = validateStartup(agentDirName); if (!result.valid) { logger.error( "Fatal: Startup validation failed. Cannot start seller runtime.", { errorCount: result.errors.length, }, ); process.exit(1); } }