From 3cac220ee8fa41fb6a1a76e94d84826088911269 Mon Sep 17 00:00:00 2001 From: DizzyMii Date: Fri, 29 May 2026 11:58:53 -0600 Subject: [PATCH] fix(landlord): validate maxRetries as a positive integer maxRetries was an unconstrained z.number(). Negative or zero values made a tenant escalate without ever running (the attempt < maxRetries loop never executes), and fractional values produced an extra attempt. Constrain it to int().min(1). --- .changeset/fix-landlord-maxretries.md | 5 +++++ packages/landlord/src/contract.ts | 2 +- packages/landlord/test/contract.test.ts | 15 +++++++++++++++ 3 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 .changeset/fix-landlord-maxretries.md diff --git a/.changeset/fix-landlord-maxretries.md b/.changeset/fix-landlord-maxretries.md new file mode 100644 index 0000000..536a431 --- /dev/null +++ b/.changeset/fix-landlord-maxretries.md @@ -0,0 +1,5 @@ +--- +"landlord": patch +--- + +Validate `maxRetries` on a contract as a positive integer. It was previously an unconstrained `z.number()`, so negative or zero values silently caused a tenant to escalate without ever running, and fractional values produced an unexpected extra attempt in the `attempt < maxRetries` loop. The schema now enforces `int().min(1)`. diff --git a/packages/landlord/src/contract.ts b/packages/landlord/src/contract.ts index 295cff5..4c6f0e5 100644 --- a/packages/landlord/src/contract.ts +++ b/packages/landlord/src/contract.ts @@ -16,7 +16,7 @@ export const ContractSchema = z.object({ toolsAllowed: z.array(z.string()).optional(), toolsDenied: z.array(z.string()).optional(), dependsOn: z.array(z.string()).default([]), - maxRetries: z.number().default(3), + maxRetries: z.number().int().min(1).default(3), }); export type Checkpoint = z.infer; diff --git a/packages/landlord/test/contract.test.ts b/packages/landlord/test/contract.test.ts index a4c80f4..969b2d0 100644 --- a/packages/landlord/test/contract.test.ts +++ b/packages/landlord/test/contract.test.ts @@ -63,4 +63,19 @@ describe('ContractSchema', () => { }); expect(result.success).toBe(false); }); + + it('rejects non-positive, non-integer, and fractional maxRetries', () => { + const base = { + role: 'r', + objective: 'x', + subPrompt: 'x', + checkpoints: [], + outputSchema: {}, + }; + for (const maxRetries of [0, -1, 2.5]) { + const result = ContractSchema.safeParse({ ...base, maxRetries }); + expect(result.success).toBe(false); + } + expect(ContractSchema.safeParse({ ...base, maxRetries: 1 }).success).toBe(true); + }); });