From c777b7401b68323cbcba2a627d2e5b5a701f38c3 Mon Sep 17 00:00:00 2001 From: David Matejka Date: Fri, 16 Oct 2026 14:57:08 +0200 Subject: [PATCH] feat(sdk): let the host configure the upload size limit Config.uploadMaxFileSize reaches the uploads plugin as maxFileSize. The default stays 10 MB. A host that takes video attachments needs more: a 15-second 1080p clip straight from an editor is about 20 MB. Co-Authored-By: Claude Opus 5.5 --- packages/sdk/src/bootstrap.ts | 1 + packages/sdk/src/config.ts | 5 ++++ .../sdk/src/core/sessions/session-manager.ts | 4 +++ packages/sdk/src/core/system.ts | 3 ++ packages/sdk/src/plugins/uploads/plugin.ts | 13 ++++++--- .../uploads/uploads.integration.test.ts | 28 +++++++++++++++++++ packages/sdk/src/testing/test-harness.ts | 2 ++ 7 files changed, 52 insertions(+), 4 deletions(-) diff --git a/packages/sdk/src/bootstrap.ts b/packages/sdk/src/bootstrap.ts index 336a7d08..bfa694f3 100644 --- a/packages/sdk/src/bootstrap.ts +++ b/packages/sdk/src/bootstrap.ts @@ -314,6 +314,7 @@ function buildSystem(services: Services, options?: CreateSystemFr onUserOutput: options?.onUserOutput, preprocessorRegistry: services.preprocessorRegistry, uploadArchiveLimits: services.config.uploadArchiveLimits, + uploadMaxFileSize: services.config.uploadMaxFileSize, resourceArchiveLimits: services.config.resourceArchiveLimits, llmLogger: services.llmLogger, portPool: services.portPool, diff --git a/packages/sdk/src/config.ts b/packages/sdk/src/config.ts index 44a9cdd8..085012aa 100644 --- a/packages/sdk/src/config.ts +++ b/packages/sdk/src/config.ts @@ -46,6 +46,8 @@ export interface Config { /** Aggregate limits across every ZIP nested in one attachment upload. */ uploadArchiveLimits?: ArchiveLimitOverrides + /** Largest attachment upload in bytes. Absent keeps the uploads plugin default of 10 MB. */ + uploadMaxFileSize?: number /** Per-archive limits for resource ZIP injection. */ resourceArchiveLimits?: ArchiveLimitOverrides /** @@ -159,6 +161,9 @@ export const validateConfig = (config: Config): string[] => { validateArchiveLimitOverrides('uploadArchiveLimits', config.uploadArchiveLimits, errors) validateArchiveLimitOverrides('resourceArchiveLimits', config.resourceArchiveLimits, errors) + if (config.uploadMaxFileSize !== undefined && (!Number.isSafeInteger(config.uploadMaxFileSize) || config.uploadMaxFileSize <= 0)) { + errors.push(`Invalid uploadMaxFileSize: ${config.uploadMaxFileSize}`) + } if (config.sessionIdleTimeoutMs !== undefined && (!Number.isSafeInteger(config.sessionIdleTimeoutMs) || config.sessionIdleTimeoutMs < 0)) { errors.push(`Invalid sessionIdleTimeoutMs: ${config.sessionIdleTimeoutMs}`) } diff --git a/packages/sdk/src/core/sessions/session-manager.ts b/packages/sdk/src/core/sessions/session-manager.ts index 09145d1f..26b9466e 100644 --- a/packages/sdk/src/core/sessions/session-manager.ts +++ b/packages/sdk/src/core/sessions/session-manager.ts @@ -83,6 +83,7 @@ export interface SessionManagerOptions { onUserOutput?: UserOutputCallback preprocessorRegistry?: PreprocessorRegistry uploadArchiveLimits?: ArchiveLimitOverrides + uploadMaxFileSize?: number resourceArchiveLimits?: ArchiveLimitOverrides llmLogger?: LLMLogger portPool?: PortPool @@ -142,6 +143,7 @@ export class SessionManager { private readonly onUserOutput?: UserOutputCallback private readonly preprocessorRegistry?: PreprocessorRegistry private readonly uploadArchiveLimits?: ArchiveLimitOverrides + private readonly uploadMaxFileSize?: number private readonly resourceArchiveLimits?: ArchiveLimitOverrides private readonly llmLogger?: LLMLogger private readonly portPool?: PortPool @@ -171,6 +173,7 @@ export class SessionManager { this.onUserOutput = options.onUserOutput this.preprocessorRegistry = options.preprocessorRegistry this.uploadArchiveLimits = options.uploadArchiveLimits + this.uploadMaxFileSize = options.uploadMaxFileSize this.resourceArchiveLimits = options.resourceArchiveLimits this.llmLogger = options.llmLogger this.portPool = options.portPool @@ -1079,6 +1082,7 @@ export class SessionManager { dataFileStore: this.dataFileStore, preprocessorRegistry: this.preprocessorRegistry, archiveLimits: this.uploadArchiveLimits, + maxFileSize: this.uploadMaxFileSize, }) if (this.resourceArchiveLimits) { diff --git a/packages/sdk/src/core/system.ts b/packages/sdk/src/core/system.ts index eccf2547..442b2512 100644 --- a/packages/sdk/src/core/system.ts +++ b/packages/sdk/src/core/system.ts @@ -121,6 +121,7 @@ export interface CreateSystemOptions createDomainError('upload_storage_ export interface UploadsPluginConfig { dataFileStore: FileStore preprocessorRegistry?: PreprocessorRegistry + /** Largest accepted file in bytes. Defaults to 10 MB. */ + maxFileSize?: number processingTimeoutMs?: number processingAbortGraceMs?: number /** Bound in ms on the close-time wait for in-flight uploads and mutations. Must be positive; anything else uses the 30s default. */ @@ -181,9 +183,9 @@ function toStoredFilename(filename: string): string { return filename.normalize('NFC') } -function validateUploadInput(input: Pick): string | undefined { +function validateUploadInput(input: Pick, maxFileSize: number): string | undefined { if (!isBasename(input.filename)) return 'Invalid filename' - if (input.size > MAX_FILE_SIZE) return `File too large: max ${MAX_FILE_SIZE / (1024 * 1024)}MB` + if (input.size > maxFileSize) return `File too large: max ${maxFileSize / (1024 * 1024)}MB` if (!isAllowedMimeType(input.mimeType)) return `Unsupported file type: ${input.mimeType}` return undefined } @@ -290,11 +292,12 @@ function runTrackedMutation(args: { async function prepareUpload(args: { pluginContext: UploadsPluginContext dataFileStore: FileStore + maxFileSize: number logger: Logger input: UploadInput }): Promise> { const filename = toStoredFilename(args.input.filename) - const validationError = validateUploadInput({ ...args.input, filename }) + const validationError = validateUploadInput({ ...args.input, filename }, args.maxFileSize) if (validationError) return Err(ValidationErrors.invalid(validationError)) const uploadId = generateUploadId() @@ -1013,6 +1016,7 @@ export const uploadsPlugin = definePlugin('uploads') const preparedResult = await prepareUpload({ pluginContext: ctx.pluginContext, dataFileStore: ctx.pluginConfig.dataFileStore, + maxFileSize: ctx.pluginConfig.maxFileSize ?? DEFAULT_MAX_FILE_SIZE, logger: ctx.logger, input, }) @@ -1076,6 +1080,7 @@ export const uploadsPlugin = definePlugin('uploads') const preparedResult = await prepareUpload({ pluginContext: ctx.pluginContext, dataFileStore: ctx.pluginConfig.dataFileStore, + maxFileSize: ctx.pluginConfig.maxFileSize ?? DEFAULT_MAX_FILE_SIZE, logger: ctx.logger, input, }) diff --git a/packages/sdk/src/plugins/uploads/uploads.integration.test.ts b/packages/sdk/src/plugins/uploads/uploads.integration.test.ts index debc9f41..a9febfe2 100644 --- a/packages/sdk/src/plugins/uploads/uploads.integration.test.ts +++ b/packages/sdk/src/plugins/uploads/uploads.integration.test.ts @@ -178,6 +178,34 @@ describe('uploads plugin', () => { await harness.shutdown() }) + it('configured maxFileSize replaces the 10MB default', async () => { + const harness = new TestHarness({ + presets: [createTestPreset()], + llmProvider: MockLLMProvider.withFixedResponse({ content: 'Ok', toolCalls: [] }), + uploadMaxFileSize: 20 * 1024 * 1024, + }) + + const session = await harness.createSession('test') + const upload = (filename: string, size: number) => + session.callPluginMethod('uploads.upload', { + sessionId: String(session.sessionId), + filename, + mimeType: 'text/plain', + size, + fileBuffer: Buffer.from('tiny'), + }) + + expect(okValue(await upload('medium.txt', 11 * 1024 * 1024), uploadResultSchema).status).toBe('ready') + + const tooLarge = await upload('huge.txt', 21 * 1024 * 1024) + expect(tooLarge.ok).toBe(false) + if (!tooLarge.ok) { + expect(tooLarge.error.message).toContain('File too large: max 20MB') + } + + await harness.shutdown() + }) + it('upload unsupported MIME type → error', async () => { const harness = new TestHarness({ presets: [createTestPreset()], diff --git a/packages/sdk/src/testing/test-harness.ts b/packages/sdk/src/testing/test-harness.ts index 95c01b0e..d1f4788f 100644 --- a/packages/sdk/src/testing/test-harness.ts +++ b/packages/sdk/src/testing/test-harness.ts @@ -58,6 +58,7 @@ export class TestHarness { eventStore?: MemoryEventStore /** Resident session idle timeout. Zero or absent disables eviction. */ sessionIdleTimeoutMs?: number + uploadMaxFileSize?: number /** Extra notification sink, run after the recorder. Throwing here simulates a hostile embedder. */ onUserOutput?: (notification: PluginNotification) => void }) { @@ -116,6 +117,7 @@ export class TestHarness { llmLogger: options.llmLogger, platform, sessionIdleTimeoutMs: options.sessionIdleTimeoutMs, + uploadMaxFileSize: options.uploadMaxFileSize, systemPlugins: mergeSystemPlugins([...fullPlugins, ...(options.systemPlugins ?? [])]), }) }