Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/sdk/src/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,7 @@ function buildSystem(services: Services<PluginProfile>, 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,
Expand Down
5 changes: 5 additions & 0 deletions packages/sdk/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
/**
Expand Down Expand Up @@ -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}`)
}
Expand Down
4 changes: 4 additions & 0 deletions packages/sdk/src/core/sessions/session-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ export interface SessionManagerOptions {
onUserOutput?: UserOutputCallback
preprocessorRegistry?: PreprocessorRegistry
uploadArchiveLimits?: ArchiveLimitOverrides
uploadMaxFileSize?: number
resourceArchiveLimits?: ArchiveLimitOverrides
llmLogger?: LLMLogger
portPool?: PortPool
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -1079,6 +1082,7 @@ export class SessionManager {
dataFileStore: this.dataFileStore,
preprocessorRegistry: this.preprocessorRegistry,
archiveLimits: this.uploadArchiveLimits,
maxFileSize: this.uploadMaxFileSize,
})

if (this.resourceArchiveLimits) {
Expand Down
3 changes: 3 additions & 0 deletions packages/sdk/src/core/system.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ export interface CreateSystemOptions<TPlugins extends readonly PluginDefinition<
onUserOutput?: UserOutputCallback
preprocessorRegistry?: PreprocessorRegistry
uploadArchiveLimits?: ArchiveLimitOverrides
uploadMaxFileSize?: number
resourceArchiveLimits?: ArchiveLimitOverrides
llmLogger?: LLMLogger
portPool?: PortPool
Expand Down Expand Up @@ -155,6 +156,7 @@ export function createSystem<const TPlugins extends readonly PluginDefinition<st
onUserOutput,
preprocessorRegistry,
uploadArchiveLimits,
uploadMaxFileSize,
resourceArchiveLimits,
llmLogger,
portPool,
Expand All @@ -181,6 +183,7 @@ export function createSystem<const TPlugins extends readonly PluginDefinition<st
onUserOutput,
preprocessorRegistry,
uploadArchiveLimits,
uploadMaxFileSize,
resourceArchiveLimits,
llmLogger,
portPool,
Expand Down
13 changes: 9 additions & 4 deletions packages/sdk/src/plugins/uploads/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ const statusChangedSchema = z.object({
// Constants
// ============================================================================

const MAX_FILE_SIZE = 10 * 1024 * 1024 // 10MB
const DEFAULT_MAX_FILE_SIZE = 10 * 1024 * 1024 // 10MB

/** Allowed MIME type patterns */
const ALLOWED_MIME_TYPES = [
Expand Down Expand Up @@ -68,6 +68,8 @@ const uploadStorageError = (): DomainError => 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. */
Expand Down Expand Up @@ -181,9 +183,9 @@ function toStoredFilename(filename: string): string {
return filename.normalize('NFC')
}

function validateUploadInput(input: Pick<UploadInput, 'size' | 'mimeType' | 'filename'>): string | undefined {
function validateUploadInput(input: Pick<UploadInput, 'size' | 'mimeType' | 'filename'>, 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
}
Expand Down Expand Up @@ -290,11 +292,12 @@ function runTrackedMutation<T>(args: {
async function prepareUpload(args: {
pluginContext: UploadsPluginContext
dataFileStore: FileStore
maxFileSize: number
logger: Logger
input: UploadInput
}): Promise<Result<PreparedUpload, DomainError>> {
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()
Expand Down Expand Up @@ -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,
})
Expand Down Expand Up @@ -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,
})
Expand Down
28 changes: 28 additions & 0 deletions packages/sdk/src/plugins/uploads/uploads.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()],
Expand Down
2 changes: 2 additions & 0 deletions packages/sdk/src/testing/test-harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}) {
Expand Down Expand Up @@ -116,6 +117,7 @@ export class TestHarness {
llmLogger: options.llmLogger,
platform,
sessionIdleTimeoutMs: options.sessionIdleTimeoutMs,
uploadMaxFileSize: options.uploadMaxFileSize,
systemPlugins: mergeSystemPlugins([...fullPlugins, ...(options.systemPlugins ?? [])]),
})
}
Expand Down
Loading