fix(SEC-ORCH-28+29): Add Valkey connection timeout + workItems MaxLength
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed

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>
This commit is contained in:
Jason Woltje
2026-02-06 15:19:44 -06:00
parent 144495ae6b
commit 3880993b60
7 changed files with 133 additions and 1 deletions

View File

@@ -230,6 +230,65 @@ describe("SpawnAgentDto validation", () => {
const errors = await validate(dto);
expect(errors.length).toBeGreaterThan(0);
});
// ------ workItems MaxLength / ArrayMaxSize (SEC-ORCH-29) ------ //
it("should reject workItems array exceeding max size of 50", async () => {
const payload = validSpawnPayload();
(payload.context as Record<string, unknown>).workItems = Array.from(
{ length: 51 },
(_, i) => `US-${String(i + 1).padStart(3, "0")}`
);
const dto = plainToInstance(SpawnAgentDto, payload);
const errors = await validate(dto);
expect(errors.length).toBeGreaterThan(0);
});
it("should accept workItems array at max size of 50", async () => {
const payload = validSpawnPayload();
(payload.context as Record<string, unknown>).workItems = Array.from(
{ length: 50 },
(_, i) => `US-${String(i + 1).padStart(3, "0")}`
);
const dto = plainToInstance(SpawnAgentDto, payload);
const errors = await validate(dto);
expect(errors).toHaveLength(0);
});
it("should reject a work item string exceeding 2000 characters", async () => {
const payload = validSpawnPayload();
(payload.context as Record<string, unknown>).workItems = ["x".repeat(2001)];
const dto = plainToInstance(SpawnAgentDto, payload);
const errors = await validate(dto);
expect(errors.length).toBeGreaterThan(0);
});
it("should accept a work item string at exactly 2000 characters", async () => {
const payload = validSpawnPayload();
(payload.context as Record<string, unknown>).workItems = ["x".repeat(2000)];
const dto = plainToInstance(SpawnAgentDto, payload);
const errors = await validate(dto);
expect(errors).toHaveLength(0);
});
// ------ skills MaxLength / ArrayMaxSize (SEC-ORCH-29) ------ //
it("should reject skills array exceeding max size of 20", async () => {
const payload = validSpawnPayload();
(payload.context as Record<string, unknown>).skills = Array.from(
{ length: 21 },
(_, i) => `skill-${i}`
);
const dto = plainToInstance(SpawnAgentDto, payload);
const errors = await validate(dto);
expect(errors.length).toBeGreaterThan(0);
});
it("should reject a skill string exceeding 200 characters", async () => {
const payload = validSpawnPayload();
(payload.context as Record<string, unknown>).skills = ["s".repeat(201)];
const dto = plainToInstance(SpawnAgentDto, payload);
const errors = await validate(dto);
expect(errors.length).toBeGreaterThan(0);
});
});
// ------------------------------------------------------------------ //

View File

@@ -6,6 +6,8 @@ import {
IsArray,
IsOptional,
ArrayNotEmpty,
ArrayMaxSize,
MaxLength,
IsIn,
Validate,
ValidatorConstraint,
@@ -83,12 +85,16 @@ export class AgentContextDto {
@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[];
}