diff --git a/packages/contracts/src/orchestration.test.ts b/packages/contracts/src/orchestration.test.ts index 7c7bade4502e..091922d15e3f 100644 --- a/packages/contracts/src/orchestration.test.ts +++ b/packages/contracts/src/orchestration.test.ts @@ -358,9 +358,6 @@ it.effect("tolerates attachment types from newer builds when decoding messages", }), ); -// The tolerant member must not catch malformed known attachments: a file over -// the size cap or an image with a bad mime has to fail its own schema, not -// slide through the open one with those constraints unchecked. it.effect("rejects malformed known attachment types instead of tolerating them", () => Effect.gen(function* () { const base = { @@ -380,10 +377,13 @@ it.effect("rejects malformed known attachment types instead of tolerating them", updatedAt: "2026-01-01T00:00:00.000Z", }); - const oversizedFile = yield* Effect.exit( - decode({ ...base, type: "file", sizeBytes: PROVIDER_SEND_TURN_MAX_FILE_BYTES + 1 }), - ); - assert.strictEqual(Exit.isFailure(oversizedFile), true); + // A newer build may raise the upload cap; this build must still read those files. + const aboveUploadCap = PROVIDER_SEND_TURN_MAX_FILE_BYTES + 1; + const largeFile = yield* decode({ ...base, type: "file", sizeBytes: aboveUploadCap }); + assert.strictEqual(largeFile.attachments?.[0]?.sizeBytes, aboveUploadCap); + + const emptyFile = yield* Effect.exit(decode({ ...base, type: "file", sizeBytes: 0 })); + assert.strictEqual(Exit.isFailure(emptyFile), true); const badMimeImage = yield* Effect.exit( decode({ ...base, type: "image", mimeType: "application/pdf", sizeBytes: 12 }), diff --git a/packages/contracts/src/orchestration.ts b/packages/contracts/src/orchestration.ts index 3e323e4964d5..9f73f668b290 100644 --- a/packages/contracts/src/orchestration.ts +++ b/packages/contracts/src/orchestration.ts @@ -318,10 +318,10 @@ export const ChatFileAttachment = Schema.Struct({ id: ChatAttachmentId, name: TrimmedNonEmptyString.check(Schema.isMaxLength(255)), mimeType: TrimmedNonEmptyString.check(Schema.isMaxLength(100)), - sizeBytes: NonNegativeInt.check( - Schema.isGreaterThanOrEqualTo(1), - Schema.isLessThanOrEqualTo(PROVIDER_SEND_TURN_MAX_FILE_BYTES), - ), + /** No upper bound: history and thread streams can carry files that a newer + build accepted under a higher limit. `AttachmentCreateUploadUrlInput` + enforces the upload cap. */ + sizeBytes: NonNegativeInt.check(Schema.isGreaterThanOrEqualTo(1)), /** Clipboard text folded by a client. Providers keep these path-only so the agent can inspect the file selectively instead of eagerly spending the same context the fold is intended to preserve. */