UNPKG

claude-flow

Version:

Ruflo - Enterprise AI agent orchestration for Claude Code. Deploy 60+ specialized agents in coordinated swarms with self-learning, fault-tolerant consensus, vector memory, and MCP integration

563 lines (516 loc) • 15.7 kB
/** * Gas Town Bridge Plugin - Input Validators * * Provides comprehensive input validation for the Gas Town Bridge Plugin: * - validateBeadId: Validate bead IDs (alphanumeric only) * - validateFormulaName: Validate formula names (safe path characters) * - validateConvoyId: Validate convoy IDs (UUID format) * - validateGtArgs: Validate and escape CLI arguments * * Security Features: * - Allowlist-based validation (only permit known-safe patterns) * - Command injection prevention * - Path traversal prevention * - Null byte injection prevention * - Shell metacharacter blocking * * All validators follow OWASP guidelines for input validation. * * @module gastown-bridge/validators * @version 0.1.0 */ import { z } from 'zod'; import { ValidationError, GasTownErrorCode } from './errors.js'; // ============================================================================ // Constants - Allowlists // ============================================================================ /** * Shell metacharacters that are never allowed in any input */ const SHELL_METACHARACTERS = /[;&|`$(){}[\]<>\n\r\0\\'"]/; /** * Path traversal patterns */ const PATH_TRAVERSAL_PATTERNS = [ /\.\.\//, // Unix parent directory /\.\.\\/, // Windows parent directory /%2e%2e%2f/i, // URL-encoded ../ /%2e%2e%5c/i, // URL-encoded ..\ /\x2e\x2e\x2f/, // Hex-encoded ../ /\x2e\x2e\x5c/, // Hex-encoded ..\ /~\//, // Home directory /^\/etc\//, // Absolute /etc path /^\/proc\//, // Absolute /proc path /^\/dev\//, // Absolute /dev path /^C:\\/i, // Windows absolute path ]; /** * Allowed bead ID formats * - gt-{4-16 alphanumeric chars} * - Numeric IDs (1-10 digits) */ const BEAD_ID_PATTERN = /^(gt-[a-zA-Z0-9]{4,16}|\d{1,10})$/; /** * Allowed formula name format * - Starts with letter * - Contains only alphanumeric, dash, underscore * - 1-64 characters */ const FORMULA_NAME_PATTERN = /^[a-zA-Z][a-zA-Z0-9_-]{0,63}$/; /** * UUID v4 pattern for convoy IDs */ const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; /** * Alternative convoy ID format (conv-{hash}) */ const CONVOY_HASH_PATTERN = /^conv-[a-zA-Z0-9]{4,16}$/; /** * Maximum lengths for inputs */ const MAX_LENGTHS = { beadId: 32, formulaName: 64, convoyId: 36, argument: 512, stringValue: 4096, arrayLength: 100, } as const; // ============================================================================ // Zod Schemas // ============================================================================ /** * Schema for bead ID validation */ export const BeadIdSchema = z.string() .min(1, 'Bead ID cannot be empty') .max(MAX_LENGTHS.beadId, `Bead ID exceeds maximum length of ${MAX_LENGTHS.beadId}`) .refine( (val) => !SHELL_METACHARACTERS.test(val), 'Bead ID contains invalid characters' ) .refine( (val) => !PATH_TRAVERSAL_PATTERNS.some(p => p.test(val)), 'Bead ID contains path traversal sequence' ) .refine( (val) => BEAD_ID_PATTERN.test(val.trim()), 'Bead ID must be in format gt-{hash} or numeric' ) .transform(val => val.trim()); /** * Schema for formula name validation */ export const FormulaNameSchema = z.string() .min(1, 'Formula name cannot be empty') .max(MAX_LENGTHS.formulaName, `Formula name exceeds maximum length of ${MAX_LENGTHS.formulaName}`) .refine( (val) => !SHELL_METACHARACTERS.test(val), 'Formula name contains invalid characters' ) .refine( (val) => !PATH_TRAVERSAL_PATTERNS.some(p => p.test(val)), 'Formula name contains path traversal sequence' ) .refine( (val) => FORMULA_NAME_PATTERN.test(val.trim()), 'Formula name must start with letter and contain only alphanumeric, dash, or underscore' ) .transform(val => val.trim()); /** * Schema for convoy ID validation (UUID format) */ export const ConvoyIdSchema = z.string() .min(1, 'Convoy ID cannot be empty') .max(MAX_LENGTHS.convoyId, `Convoy ID exceeds maximum length of ${MAX_LENGTHS.convoyId}`) .refine( (val) => !SHELL_METACHARACTERS.test(val), 'Convoy ID contains invalid characters' ) .refine( (val) => UUID_PATTERN.test(val.trim()) || CONVOY_HASH_PATTERN.test(val.trim()), 'Convoy ID must be a valid UUID or conv-{hash} format' ) .transform(val => val.trim().toLowerCase()); /** * Schema for a single CLI argument */ export const GtArgumentSchema = z.string() .max(MAX_LENGTHS.argument, `Argument exceeds maximum length of ${MAX_LENGTHS.argument}`) .refine( (val) => !val.includes('\0'), 'Argument contains null byte' ) .refine( (val) => !SHELL_METACHARACTERS.test(val), 'Argument contains shell metacharacters' ) .refine( (val) => !PATH_TRAVERSAL_PATTERNS.some(p => p.test(val)), 'Argument contains path traversal sequence' ); /** * Schema for CLI arguments array */ export const GtArgsSchema = z.array(GtArgumentSchema) .max(MAX_LENGTHS.arrayLength, `Too many arguments (max ${MAX_LENGTHS.arrayLength})`); /** * Schema for safe string values */ export const SafeStringSchema = z.string() .max(MAX_LENGTHS.stringValue, `String exceeds maximum length of ${MAX_LENGTHS.stringValue}`) .refine( (val) => !SHELL_METACHARACTERS.test(val), 'String contains shell metacharacters' ); /** * Schema for formula type */ export const FormulaTypeSchema = z.enum(['convoy', 'workflow', 'expansion', 'aspect']); /** * Schema for bead status */ export const BeadStatusSchema = z.enum(['open', 'in_progress', 'closed']); /** * Schema for convoy status */ export const ConvoyStatusSchema = z.enum(['active', 'landed', 'failed', 'paused']); /** * Schema for sling target */ export const SlingTargetSchema = z.enum(['polecat', 'crew', 'mayor']); /** * Schema for rig name */ export const RigNameSchema = z.string() .min(1, 'Rig name cannot be empty') .max(32, 'Rig name exceeds maximum length') .refine( (val) => !SHELL_METACHARACTERS.test(val), 'Rig name contains invalid characters' ) .refine( (val) => /^[a-zA-Z][a-zA-Z0-9-]{0,31}$/.test(val.trim()), 'Rig name must start with letter and contain only alphanumeric or dash' ) .transform(val => val.trim().toLowerCase()); /** * Schema for priority */ export const PrioritySchema = z.number() .int('Priority must be an integer') .min(0, 'Priority cannot be negative') .max(100, 'Priority cannot exceed 100'); /** * Schema for labels array */ export const LabelsSchema = z.array( z.string() .max(50, 'Label exceeds maximum length') .refine( (val) => /^[a-zA-Z][a-zA-Z0-9_-]*$/.test(val), 'Label must start with letter and contain only alphanumeric, dash, or underscore' ) ) .max(20, 'Too many labels (max 20)'); // ============================================================================ // Validation Functions // ============================================================================ /** * Validate a bead ID * * Accepts: * - gt-{4-16 alphanumeric chars} (e.g., "gt-abc12", "gt-a1b2c3d4") * - Numeric IDs (e.g., "123", "9999999999") * * Rejects: * - Empty strings * - Shell metacharacters * - Path traversal sequences * - Invalid formats * * @param id - The bead ID to validate * @returns The validated and trimmed bead ID * @throws {ValidationError} If validation fails * * @example * ```typescript * const validId = validateBeadId('gt-abc12'); // Returns 'gt-abc12' * validateBeadId('gt-abc; rm -rf /'); // Throws ValidationError * ``` */ export function validateBeadId(id: string): string { const result = BeadIdSchema.safeParse(id); if (!result.success) { const errors = result.error.errors.map(e => e.message).join('; '); throw ValidationError.invalidBeadId(id); } return result.data; } /** * Validate a formula name * * Accepts: * - Starts with letter * - Contains only alphanumeric, dash, underscore * - 1-64 characters * * Rejects: * - Starting with number * - Shell metacharacters * - Path traversal sequences * * @param name - The formula name to validate * @returns The validated and trimmed formula name * @throws {ValidationError} If validation fails * * @example * ```typescript * const validName = validateFormulaName('my-formula'); // Returns 'my-formula' * validateFormulaName('../etc/passwd'); // Throws ValidationError * ``` */ export function validateFormulaName(name: string): string { const result = FormulaNameSchema.safeParse(name); if (!result.success) { throw ValidationError.invalidFormulaName(name); } return result.data; } /** * Validate a convoy ID * * Accepts: * - UUID v4 format (e.g., "550e8400-e29b-41d4-a716-446655440000") * - conv-{hash} format (e.g., "conv-abc123def") * * Rejects: * - Invalid UUID format * - Shell metacharacters * - Path traversal sequences * * @param id - The convoy ID to validate * @returns The validated and normalized convoy ID (lowercase) * @throws {ValidationError} If validation fails * * @example * ```typescript * const validId = validateConvoyId('550e8400-e29b-41d4-a716-446655440000'); * validateConvoyId('not-a-uuid'); // Throws ValidationError * ``` */ export function validateConvoyId(id: string): string { const result = ConvoyIdSchema.safeParse(id); if (!result.success) { throw ValidationError.invalidConvoyId(id); } return result.data; } /** * Validate and escape CLI arguments * * Validates each argument in the array: * - No null bytes * - No shell metacharacters * - No path traversal sequences * - Maximum length enforced * * @param args - Array of CLI arguments to validate * @returns Array of validated arguments * @throws {ValidationError} If any argument fails validation * * @example * ```typescript * const validArgs = validateGtArgs(['beads', 'list', '--limit', '10']); * validateGtArgs(['rm', '-rf', '/']); // Throws ValidationError * ``` */ export function validateGtArgs(args: string[]): string[] { if (!Array.isArray(args)) { throw new ValidationError( 'Arguments must be an array', GasTownErrorCode.INVALID_ARGUMENTS, [{ field: 'args', constraint: 'array', actual: typeof args }] ); } const result = GtArgsSchema.safeParse(args); if (!result.success) { const errors = result.error.errors; const constraints = errors.map((e, idx) => ({ field: `args[${e.path[0] ?? idx}]`, constraint: 'safe CLI argument', actual: args[Number(e.path[0])]?.slice(0, 20) + '...', })); throw new ValidationError( 'Invalid CLI arguments', GasTownErrorCode.INVALID_ARGUMENTS, constraints ); } return result.data; } // ============================================================================ // Compound Validators // ============================================================================ /** * Schema for CreateBeadOptions */ export const CreateBeadOptionsSchema = z.object({ title: z.string() .min(1, 'Title cannot be empty') .max(256, 'Title exceeds maximum length') .refine( (val) => !SHELL_METACHARACTERS.test(val), 'Title contains invalid characters' ), description: z.string() .max(4096, 'Description exceeds maximum length') .optional(), priority: PrioritySchema.optional().default(50), labels: LabelsSchema.optional().default([]), parent: BeadIdSchema.optional(), rig: RigNameSchema.optional(), assignee: z.string() .max(64, 'Assignee name exceeds maximum length') .refine( (val) => /^[a-zA-Z][a-zA-Z0-9_-]*$/.test(val), 'Invalid assignee name format' ) .optional(), }); /** * Schema for CreateConvoyOptions */ export const CreateConvoyOptionsSchema = z.object({ name: z.string() .min(1, 'Convoy name cannot be empty') .max(128, 'Convoy name exceeds maximum length') .refine( (val) => !SHELL_METACHARACTERS.test(val), 'Convoy name contains invalid characters' ), issues: z.array(BeadIdSchema) .min(1, 'At least one issue is required') .max(100, 'Too many issues (max 100)'), description: z.string() .max(4096, 'Description exceeds maximum length') .optional(), formula: FormulaNameSchema.optional(), }); /** * Schema for SlingOptions */ export const SlingOptionsSchema = z.object({ beadId: BeadIdSchema, target: SlingTargetSchema, formula: FormulaNameSchema.optional(), priority: PrioritySchema.optional(), }); /** * Validate CreateBeadOptions */ export function validateCreateBeadOptions(options: unknown): z.infer<typeof CreateBeadOptionsSchema> { const result = CreateBeadOptionsSchema.safeParse(options); if (!result.success) { const errors = result.error.errors; const constraints = errors.map(e => ({ field: e.path.join('.'), constraint: e.message, })); throw new ValidationError( 'Invalid bead creation options', GasTownErrorCode.VALIDATION_FAILED, constraints ); } return result.data; } /** * Validate CreateConvoyOptions */ export function validateCreateConvoyOptions(options: unknown): z.infer<typeof CreateConvoyOptionsSchema> { const result = CreateConvoyOptionsSchema.safeParse(options); if (!result.success) { const errors = result.error.errors; const constraints = errors.map(e => ({ field: e.path.join('.'), constraint: e.message, })); throw new ValidationError( 'Invalid convoy creation options', GasTownErrorCode.VALIDATION_FAILED, constraints ); } return result.data; } /** * Validate SlingOptions */ export function validateSlingOptions(options: unknown): z.infer<typeof SlingOptionsSchema> { const result = SlingOptionsSchema.safeParse(options); if (!result.success) { const errors = result.error.errors; const constraints = errors.map(e => ({ field: e.path.join('.'), constraint: e.message, })); throw new ValidationError( 'Invalid sling options', GasTownErrorCode.VALIDATION_FAILED, constraints ); } return result.data; } // ============================================================================ // Security Utilities // ============================================================================ /** * Check if a string contains shell metacharacters */ export function containsShellMetacharacters(input: string): boolean { return SHELL_METACHARACTERS.test(input); } /** * Check if a string contains path traversal sequences */ export function containsPathTraversal(input: string): boolean { return PATH_TRAVERSAL_PATTERNS.some(p => p.test(input)); } /** * Check if a string is safe for use in CLI arguments */ export function isSafeArgument(input: string): boolean { return GtArgumentSchema.safeParse(input).success; } /** * Check if a bead ID is valid */ export function isValidBeadId(id: string): boolean { return BeadIdSchema.safeParse(id).success; } /** * Check if a formula name is valid */ export function isValidFormulaName(name: string): boolean { return FormulaNameSchema.safeParse(name).success; } /** * Check if a convoy ID is valid */ export function isValidConvoyId(id: string): boolean { return ConvoyIdSchema.safeParse(id).success; } // ============================================================================ // Exports // ============================================================================ export { MAX_LENGTHS, SHELL_METACHARACTERS, PATH_TRAVERSAL_PATTERNS, BEAD_ID_PATTERN, FORMULA_NAME_PATTERN, UUID_PATTERN, CONVOY_HASH_PATTERN, };