Files
stack/apps/orchestrator/src/api/agents/dto/spawn-agent.dto.ts
Jason Woltje 3880993b60
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
fix(SEC-ORCH-28+29): Add Valkey connection timeout + workItems MaxLength
SEC-ORCH-28: Add connectTimeout (5000ms default) and commandTimeout
(3000ms default) to Valkey/Redis client to prevent indefinite connection
hangs. Both are configurable via VALKEY_CONNECT_TIMEOUT_MS and
VALKEY_COMMAND_TIMEOUT_MS environment variables.

SEC-ORCH-29: Add @ArrayMaxSize(50) and @MaxLength(2000) to workItems
in AgentContextDto to prevent memory exhaustion from unbounded input.
Also adds @ArrayMaxSize(20) and @MaxLength(200) to skills array.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-06 15:19:44 -06:00

128 lines
3.2 KiB
TypeScript

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;
}
/**
* Response DTO for spawn agent endpoint
*/
export class SpawnAgentResponseDto {
agentId!: string;
status!: "spawning" | "queued";
}