import { IsString, IsNotEmpty, IsEnum, ValidateNested, IsArray, IsOptional, ArrayNotEmpty, ArrayMaxSize, MaxLength, IsIn, Validate, ValidatorConstraint, ValidatorConstraintInterface, ValidationArguments, } from "class-validator"; import { Type } from "class-transformer"; import { AgentType } from "../../../spawner/types/agent-spawner.types"; import { GateProfileType } from "../../../coordinator/types/gate-config.types"; import { validateBranchName, validateRepositoryUrl } from "../../../git/git-validation.util"; /** * Custom validator for git branch names * Uses whitelist-based validation to prevent command injection */ @ValidatorConstraint({ name: "isValidBranchName", async: false }) export class IsValidBranchName implements ValidatorConstraintInterface { validate(branchName: string, _args: ValidationArguments): boolean { try { validateBranchName(branchName); return true; } catch { return false; } } defaultMessage(args: ValidationArguments): string { try { validateBranchName(args.value as string); return "Branch name is invalid"; } catch (error) { return error instanceof Error ? error.message : "Branch name is invalid"; } } } /** * Custom validator for git repository URLs * Prevents SSRF and command injection via dangerous protocols */ @ValidatorConstraint({ name: "isValidRepositoryUrl", async: false }) export class IsValidRepositoryUrl implements ValidatorConstraintInterface { validate(repositoryUrl: string, _args: ValidationArguments): boolean { try { validateRepositoryUrl(repositoryUrl); return true; } catch { return false; } } defaultMessage(args: ValidationArguments): string { try { validateRepositoryUrl(args.value as string); return "Repository URL is invalid"; } catch (error) { return error instanceof Error ? error.message : "Repository URL is invalid"; } } } /** * Context DTO for agent spawn request */ export class AgentContextDto { @IsString() @IsNotEmpty() @Validate(IsValidRepositoryUrl) repository!: string; @IsString() @IsNotEmpty() @Validate(IsValidBranchName) branch!: string; @IsArray() @ArrayNotEmpty() @ArrayMaxSize(50, { message: "workItems must contain at most 50 items" }) @IsString({ each: true }) @MaxLength(2000, { each: true, message: "Each work item must be at most 2000 characters" }) workItems!: string[]; @IsArray() @IsOptional() @ArrayMaxSize(20, { message: "skills must contain at most 20 items" }) @IsString({ each: true }) @MaxLength(200, { each: true, message: "Each skill must be at most 200 characters" }) skills?: string[]; } /** * Request DTO for spawning an agent */ export class SpawnAgentDto { @IsString() @IsNotEmpty() taskId!: string; @IsEnum(["worker", "reviewer", "tester"]) agentType!: AgentType; @ValidateNested() @Type(() => AgentContextDto) context!: AgentContextDto; @IsOptional() @IsIn(["strict", "standard", "minimal", "custom"]) gateProfile?: GateProfileType; @IsOptional() @IsString() parentAgentId?: string; } /** * Response DTO for spawn agent endpoint */ export class SpawnAgentResponseDto { agentId!: string; status!: "spawning" | "queued"; }