basant307/AI_Governance_Project
045
1/**2 * @license3 * Copyright 2025 Qwen4 * SPDX-License-Identifier: Apache-2.05 */6 7import { SubagentError, SubagentErrorCode } from './types.js';8import type { SubagentConfig, ValidationResult } from './types.js';9import type { RunConfig } from '../agents/runtime/agent-types.js';10import { resolveModelId } from '../utils/modelId.js';11 12/**13 * Validates subagent configurations to ensure they are well-formed14 * and compatible with the runtime system.15 */16export class SubagentValidator {17 /**18 * Validates a complete subagent configuration.19 *20 * @param config - The subagent configuration to validate21 * @returns ValidationResult with errors and warnings22 */23 validateConfig(config: SubagentConfig): ValidationResult {24 const errors: string[] = [];25 const warnings: string[] = [];26 27 const nameValidation = this.validateName(config.name);28 if (!nameValidation.isValid) {29 errors.push(...nameValidation.errors);30 }31 32 if (!config.description || config.description.trim().length === 0) {33 errors.push('Description is required and cannot be empty');34 } else if (config.description.length > 1000) {35 warnings.push(36 'Description is quite long (>1,000 chars), consider shortening for better readability',37 );38 }39 40 const promptValidation = this.validateSystemPrompt(config.systemPrompt);41 if (!promptValidation.isValid) {42 errors.push(...promptValidation.errors);43 }44 warnings.push(...promptValidation.warnings);45 46 if (config.tools) {47 const toolsValidation = this.validateTools(config.tools);48 if (!toolsValidation.isValid) {49 errors.push(...toolsValidation.errors);50 }51 warnings.push(...toolsValidation.warnings);52 }53 54 if (config.disallowedTools && config.disallowedTools.length > 0) {55 const disallowedValidation = this.validateTools(config.disallowedTools);56 if (!disallowedValidation.isValid) {57 errors.push(...disallowedValidation.errors);58 }59 warnings.push(...disallowedValidation.warnings);60 }61 62 if (config.model) {63 const modelValidation = this.validateModel(config.model);64 if (!modelValidation.isValid) {65 errors.push(...modelValidation.errors);66 }67 warnings.push(...modelValidation.warnings);68 }69 70 if (config.runConfig) {71 const runValidation = this.validateRunConfig(config.runConfig);72 if (!runValidation.isValid) {73 errors.push(...runValidation.errors);74 }75 warnings.push(...runValidation.warnings);76 }77 78 return {79 isValid: errors.length === 0,80 errors,81 warnings,82 };83 }84 85 /**86 * Validates a subagent name.87 * Names must be valid identifiers that can be used in file paths and tool calls.88 *89 * @param name - The name to validate90 * @returns ValidationResult91 */92 validateName(name: string): ValidationResult {93 const errors: string[] = [];94 const warnings: string[] = [];95 96 if (!name || name.trim().length === 0) {97 errors.push('Name is required and cannot be empty');98 return { isValid: false, errors, warnings };99 }100 101 const trimmedName = name.trim();102 103 if (trimmedName.length < 2) {104 errors.push('Name must be at least 2 characters long');105 }106 107 if (trimmedName.length > 50) {108 errors.push('Name must be 50 characters or less');109 }110 111 const validNameRegex = /^[\p{L}\p{N}_-]+$/u;112 if (!validNameRegex.test(trimmedName)) {113 errors.push(114 'Name can only contain letters, numbers, hyphens, and underscores',115 );116 }117 118 if (trimmedName.startsWith('-') || trimmedName.startsWith('_')) {119 errors.push('Name cannot start with a hyphen or underscore');120 }121 122 if (trimmedName.endsWith('-') || trimmedName.endsWith('_')) {123 errors.push('Name cannot end with a hyphen or underscore');124 }125 126 // Check for reserved names. `main` is the sentinel used by the /stats127 // attribution pipeline to label the main (non-subagent) conversation;128 // a subagent named `main` would collide with that sentinel and be129 // silently merged into the main bucket.130 const reservedNames = [131 'self',132 'system',133 'user',134 'model',135 'tool',136 'config',137 'default',138 'main',139 ];140 if (reservedNames.includes(trimmedName.toLowerCase())) {141 errors.push(`"${trimmedName}" is a reserved name and cannot be used`);142 }143 144 // Only warn about naming conventions for names that contain case distinctions145 if (146 trimmedName !== trimmedName.toLowerCase() &&147 /[a-zA-Z]/.test(trimmedName)148 ) {149 warnings.push('Consider using lowercase names for consistency');150 }151 152 if (trimmedName.includes('_') && trimmedName.includes('-')) {153 warnings.push(154 'Consider using either hyphens or underscores consistently, not both',155 );156 }157 158 return {159 isValid: errors.length === 0,160 errors,161 warnings,162 };163 }164 165 /**166 * Validates a system prompt.167 *168 * @param prompt - The system prompt to validate169 * @returns ValidationResult170 */171 validateSystemPrompt(prompt: string): ValidationResult {172 const errors: string[] = [];173 const warnings: string[] = [];174 175 if (!prompt || prompt.trim().length === 0) {176 errors.push('System prompt is required and cannot be empty');177 return { isValid: false, errors, warnings };178 }179 180 const trimmedPrompt = prompt.trim();181 182 if (trimmedPrompt.length < 10) {183 errors.push('System prompt must be at least 10 characters long');184 }185 186 if (trimmedPrompt.length > 10000) {187 warnings.push(188 'System prompt is quite long (>10,000 characters), consider shortening',189 );190 }191 192 return {193 isValid: errors.length === 0,194 errors,195 warnings,196 };197 }198 199 /**200 * Validates a list of tool names.201 *202 * @param tools - Array of tool names to validate203 * @returns ValidationResult204 */205 validateTools(tools: string[]): ValidationResult {206 const errors: string[] = [];207 const warnings: string[] = [];208 209 if (!Array.isArray(tools)) {210 errors.push('Tools must be an array of strings');211 return { isValid: false, errors, warnings };212 }213 214 if (tools.length === 0) {215 warnings.push(216 'Empty tools array - subagent will inherit all available tools',217 );218 return { isValid: true, errors, warnings };219 }220 221 const uniqueTools = new Set(tools);222 if (uniqueTools.size !== tools.length) {223 warnings.push('Duplicate tool names found in tools array');224 }225 226 for (const tool of tools) {227 if (typeof tool !== 'string') {228 errors.push(`Tool name must be a string, got: ${typeof tool}`);229 continue;230 }231 232 if (tool.trim().length === 0) {233 errors.push('Tool name cannot be empty');234 continue;235 }236 }237 238 return {239 isValid: errors.length === 0,240 errors,241 warnings,242 };243 }244 245 /**246 * Validates a subagent model selector.247 *248 * @param model - Model selector to validate249 * @returns ValidationResult250 */251 validateModel(model: string): ValidationResult {252 const errors: string[] = [];253 const warnings: string[] = [];254 255 if (typeof model !== 'string' || model.trim().length === 0) {256 errors.push('Model must be a non-empty string');257 return {258 isValid: false,259 errors,260 warnings,261 };262 }263 264 try {265 resolveModelId(model);266 } catch (error) {267 errors.push(error instanceof Error ? error.message : 'Invalid model');268 }269 270 if (model.trim() === 'inherit') {271 warnings.push(272 'Explicit "inherit" is optional because omitting the model uses the main conversation model',273 );274 }275 276 return {277 isValid: errors.length === 0,278 errors,279 warnings,280 };281 }282 283 /**284 * Validates runtime configuration.285 *286 * @param runConfig - Partial run configuration to validate287 * @returns ValidationResult288 */289 validateRunConfig(runConfig: RunConfig): ValidationResult {290 const errors: string[] = [];291 const warnings: string[] = [];292 293 if (runConfig.max_time_minutes !== undefined) {294 if (typeof runConfig.max_time_minutes !== 'number') {295 errors.push('max_time_minutes must be a number');296 } else if (runConfig.max_time_minutes <= 0) {297 errors.push('max_time_minutes must be greater than 0');298 } else if (runConfig.max_time_minutes > 60) {299 warnings.push(300 'Very long execution time (>60 minutes) may cause resource issues',301 );302 }303 }304 305 if (runConfig.max_turns !== undefined) {306 if (typeof runConfig.max_turns !== 'number') {307 errors.push('max_turns must be a number');308 } else if (runConfig.max_turns <= 0) {309 errors.push('max_turns must be greater than 0');310 } else if (!Number.isInteger(runConfig.max_turns)) {311 errors.push('max_turns must be an integer');312 } else if (runConfig.max_turns > 100) {313 warnings.push(314 'Very high turn limit (>100) may cause long execution times',315 );316 }317 }318 319 return {320 isValid: errors.length === 0,321 errors,322 warnings,323 };324 }325 326 /**327 * Throws a SubagentError if validation fails.328 *329 * @param config - Configuration to validate330 * @param subagentName - Name for error context331 * @throws SubagentError if validation fails332 */333 validateOrThrow(config: SubagentConfig, subagentName?: string): void {334 const result = this.validateConfig(config);335 if (!result.isValid) {336 throw new SubagentError(337 `Validation failed: ${result.errors.join(', ')}`,338 SubagentErrorCode.VALIDATION_ERROR,339 subagentName || config.name,340 );341 }342 }343}344 