import { Type } from 'class-transformer'; import { IsIn, IsOptional, IsString, IsUUID, MaxLength, MinLength, ValidateIf, ValidateNested, } from 'class-validator'; /** * Agent enrollment command DTOs (design * docs/plans/2026-08-29-agent-enrollment-command-design.md §3.1/§3.2, * contract 5 §4.1 typed boundary). * * The global ValidationPipe runs with whitelist + forbidNonWhitelisted, so * closure is contract surface here exactly as in the hierarchy DTOs: * - EnrollAgentDto declares NO isSystem field — `is_system` is never * settable through this command (design §3.1 rule 4); the pipe refuses it. * - replayMode admits ONLY 'actor-bound': `shared` is seed-only (contract 3 * §4.3), so a shared declaration is refused `validation_failed` at the * boundary, executes nothing, and records no fence row (design §3.1). * Every class here must be registered in PIPE_GUARDED_DTOS so the boot-time * assertion proves the pipe sees the decorators. */ /** * Credential input, discriminated on `mode` (design §3.1): * - `{ mode: 'reference' }` — a stored credential for (actor, provider) * must already exist; `type`/`value` must be ABSENT (the repository * refuses a reference that smuggles a value). * - `{ mode: 'intake', type: 'api_key', value }` — the value is sealed * into the credential store inside the enrollment transaction and is * never echoed anywhere (§3.1 rule 1). */ export class EnrollCredentialDto { @IsIn(['reference', 'intake']) mode!: 'reference' | 'intake'; @ValidateIf((o: EnrollCredentialDto) => o.mode === 'intake') @IsIn(['api_key']) type?: 'api_key'; @ValidateIf((o: EnrollCredentialDto) => o.mode === 'intake') @IsString() @MinLength(1) @MaxLength(4096) value?: string; } export class EnrollAgentDto { /** Registered harness name; a well-formed name missing from the registry is `precondition_failed`. */ @IsString() @MinLength(1) @MaxLength(200) harness!: string; @IsString() @MinLength(1) @MaxLength(200) name!: string; /** Stored as the agent's system prompt; null/absent leaves it unset. */ @IsOptional() @IsString() @MaxLength(20000) persona?: string | null; /** Provider-qualified model id. */ @IsString() @MinLength(1) @MaxLength(200) model!: string; /** Names the credential's provider. */ @IsString() @MinLength(1) @MaxLength(200) provider!: string; @ValidateNested() @Type(() => EnrollCredentialDto) credential!: EnrollCredentialDto; /** REQUIRED — contract 3 §4.3, ratified into contract 5 §4 via §7 item 4. */ @IsUUID() idempotencyKey!: string; /** Optional; generated when absent (contract 5 §4.3). */ @IsOptional() @IsUUID() correlationId?: string; /** Only 'actor-bound' is admissible on this family — see module doc. */ @IsOptional() @IsIn(['actor-bound']) replayMode?: 'actor-bound'; } /** Query envelope for agent.enrollment.get (design §3.2): correlation only, no idempotency key. */ export class GetEnrollmentQueryDto { @IsOptional() @IsUUID() correlationId?: string; }