From 80b7c139d431327111c2b2ec588cb8ba5814e4d9 Mon Sep 17 00:00:00 2001 From: Simon Davies Date: Thu, 7 May 2026 13:07:31 +0100 Subject: [PATCH 1/2] feat: add actionable config hints to all limit error messages Every error message that fires when a configurable limit is breached now tells the LLM which config field to increase via manage_plugin. fs-read (4 messages): - maxFileSizeKb hint on all 'File too large' errors - maxReadChunkKb hint on per-call chunk errors fs-write (8 messages): - maxWriteChunkKb hint on per-call chunk errors (text + binary) - maxWriteSizeKb hint on cumulative write limit errors - maxEntries hint on entry creation limit errors fetch (9 messages): - maxRequestsPerMinute/maxRequestsPerHour on rate limit errors - maxDomainsPerSession on domain count errors - maxDataReceivedKb on data budget errors - maxResponseSizeKb on response too large errors - maxRequestBodySizeKb on request body errors - maxRedirects on redirect limit errors - maxJsonResponseBytes/maxTextResponseBytes on convenience method errors Signed-off-by: Simon Davies --- plugins/fetch/index.ts | 28 +++++++++++++++++++--------- plugins/fs-read/index.ts | 12 ++++++------ plugins/fs-write/index.ts | 30 +++++++++++++++--------------- 3 files changed, 40 insertions(+), 30 deletions(-) diff --git a/plugins/fetch/index.ts b/plugins/fetch/index.ts index 91afd83..26085d7 100644 --- a/plugins/fetch/index.ts +++ b/plugins/fetch/index.ts @@ -1112,7 +1112,8 @@ function createRateLimiter(config: { if (timestamps.length >= config.maxPerMinute) { return { allowed: false, - reason: "fetch blocked: rate limit exceeded (per-minute)", + reason: + "fetch blocked: rate limit exceeded (per-minute). To increase, reconfigure fetch with a larger maxRequestsPerMinute.", }; } @@ -1120,7 +1121,8 @@ function createRateLimiter(config: { if (totalRequests >= config.maxPerHour) { return { allowed: false, - reason: "fetch blocked: rate limit exceeded (per-hour)", + reason: + "fetch blocked: rate limit exceeded (per-hour). To increase, reconfigure fetch with a larger maxRequestsPerHour.", }; } @@ -1128,13 +1130,18 @@ function createRateLimiter(config: { if (!domains.has(hostname) && domains.size >= config.maxDomains) { return { allowed: false, - reason: "fetch blocked: too many unique domains", + reason: + "fetch blocked: too many unique domains. To increase, reconfigure fetch with a larger maxDomainsPerSession.", }; } // Data budget if (totalBytesReceived >= config.maxDataReceivedBytes) { - return { allowed: false, reason: "fetch blocked: data budget exhausted" }; + return { + allowed: false, + reason: + "fetch blocked: data budget exhausted. To increase, reconfigure fetch with a larger maxDataReceivedKb.", + }; } return { allowed: true }; @@ -2239,7 +2246,10 @@ function secureFetchSingle( clearTimeout(readTimer); clearTimeout(hardTimer); res.destroy(); - return settle({ error: "fetch blocked: response too large" }); + return settle({ + error: + "fetch blocked: response too large. To increase, reconfigure fetch with a larger maxResponseSizeKb.", + }); } chunks.push(chunk); }); @@ -2547,7 +2557,7 @@ async function secureFetch( // Exhausted redirect budget return { - error: `fetch blocked: too many redirects (max ${opts.maxRedirects})`, + error: `fetch blocked: too many redirects (max ${opts.maxRedirects}). To increase, reconfigure fetch with a larger maxRedirects.`, }; } @@ -3136,7 +3146,7 @@ export function createHostFunctions(config?: FetchConfig): FetchHostFunctions { }); await enforceMinDelay(startTime, MIN_RESPONSE_DELAY_MS); return { - error: `fetch blocked: request body too large (max ${maxRequestBodyBytes / 1024}KB)`, + error: `fetch blocked: request body too large (max ${maxRequestBodyBytes / 1024}KB). To increase, reconfigure fetch with a larger maxRequestBodySizeKb.`, }; } @@ -3517,7 +3527,7 @@ export function createHostFunctions(config?: FetchConfig): FetchHostFunctions { throw new Error( `fetchJSON: response too large ` + `(${jsonBodyBytes} bytes, max ${maxJsonResponseBytes}). ` + - `Use get() + read() loop to stream large responses instead.`, + `Use get() + read() loop to stream large responses instead, or reconfigure fetch with a larger maxJsonResponseBytes.`, ); } @@ -3582,7 +3592,7 @@ export function createHostFunctions(config?: FetchConfig): FetchHostFunctions { throw new Error( `fetchText: response too large ` + `(${textBodyBytes} bytes, max ${maxTextResponseBytes}). ` + - `Use get() + read() loop to stream large responses instead.`, + `Use get() + read() loop to stream large responses instead, or reconfigure fetch with a larger maxTextResponseBytes.`, ); } diff --git a/plugins/fs-read/index.ts b/plugins/fs-read/index.ts index 00b23ca..44a8856 100644 --- a/plugins/fs-read/index.ts +++ b/plugins/fs-read/index.ts @@ -272,12 +272,12 @@ export function createHostFunctions( } if (fileStat.size > maxFileBytes) { return { - error: `File too large: exceeds read limit of ${maxFileBytes / 1024}KB`, + error: `File too large: exceeds read limit of ${maxFileBytes / 1024}KB. To increase, reconfigure fs-read with a larger maxFileSizeKb.`, }; } if (fileStat.size > maxReadChunkBytes) { return { - error: `File too large for single read: ${fileStat.size} bytes exceeds per-call limit of ${maxReadChunkBytes / 1024}KB. Use readFileChunk(path, offsetBytes, lengthBytes) to read in chunks.`, + error: `File too large for single read: ${fileStat.size} bytes exceeds per-call limit of ${maxReadChunkBytes / 1024}KB. Use readFileChunk(path, offsetBytes, lengthBytes) to read in chunks, or reconfigure fs-read with a larger maxReadChunkKb.`, }; } @@ -343,7 +343,7 @@ export function createHostFunctions( } if (fileStat.size > maxFileBytes) { return { - error: `File too large: exceeds read limit of ${maxFileBytes / 1024}KB`, + error: `File too large: exceeds read limit of ${maxFileBytes / 1024}KB. To increase, reconfigure fs-read with a larger maxFileSizeKb.`, }; } @@ -484,14 +484,14 @@ export function createHostFunctions( } if (fileStat.size > maxFileBytes) { throw new Error( - `File too large: exceeds read limit of ${maxFileBytes / 1024}KB`, + `File too large: exceeds read limit of ${maxFileBytes / 1024}KB. To increase, reconfigure fs-read with a larger maxFileSizeKb.`, ); } if (fileStat.size > maxReadChunkBytes) { throw new Error( `File too large for single read: ${fileStat.size} bytes exceeds ` + `per-call limit of ${maxReadChunkBytes / 1024}KB. ` + - `Use readFileChunkBinary(path, offsetBytes, lengthBytes) to read in chunks.`, + `Use readFileChunkBinary(path, offsetBytes, lengthBytes) to read in chunks, or reconfigure fs-read with a larger maxReadChunkKb.`, ); } @@ -547,7 +547,7 @@ export function createHostFunctions( } if (fileStat.size > maxFileBytes) { throw new Error( - `File too large: exceeds read limit of ${maxFileBytes / 1024}KB`, + `File too large: exceeds read limit of ${maxFileBytes / 1024}KB. To increase, reconfigure fs-read with a larger maxFileSizeKb.`, ); } diff --git a/plugins/fs-write/index.ts b/plugins/fs-write/index.ts index b90b576..eb3e744 100644 --- a/plugins/fs-write/index.ts +++ b/plugins/fs-write/index.ts @@ -243,12 +243,12 @@ export function createHostFunctions( if (contentBytes > maxWriteChunkBytes) { return { - error: `Content too large for single write: ${contentBytes} bytes exceeds per-call limit of ${maxWriteChunkBytes / 1024}KB. Split into multiple appendFile calls.`, + error: `Content too large for single write: ${contentBytes} bytes exceeds per-call limit of ${maxWriteChunkBytes / 1024}KB. Split into multiple appendFile calls, or reconfigure fs-write with a larger maxWriteChunkKb.`, }; } if (contentBytes > maxWriteBytes) { return { - error: `Content too large: exceeds cumulative file write limit of ${maxWriteBytes / 1024}KB`, + error: `Content too large: exceeds cumulative file write limit of ${maxWriteBytes / 1024}KB. To increase, reconfigure fs-write with a larger maxWriteSizeKb.`, }; } @@ -263,7 +263,7 @@ export function createHostFunctions( if (isNew) { if (entriesCreated >= maxEntries) { return { - error: `Entry limit reached: cannot create more than ${maxEntries} files/directories`, + error: `Entry limit reached: cannot create more than ${maxEntries} files/directories. To increase, reconfigure fs-write with a larger maxEntries.`, }; } entriesCreated++; @@ -333,12 +333,12 @@ export function createHostFunctions( if (contentBytes > maxWriteChunkBytes) { return { - error: `Append content too large for single call: ${contentBytes} bytes exceeds per-call limit of ${maxWriteChunkBytes / 1024}KB. Split into smaller appendFile calls.`, + error: `Append content too large for single call: ${contentBytes} bytes exceeds per-call limit of ${maxWriteChunkBytes / 1024}KB. Split into smaller appendFile calls, or reconfigure fs-write with a larger maxWriteChunkKb.`, }; } if (contentBytes > maxWriteBytes) { return { - error: `Append would exceed cumulative file write limit of ${maxWriteBytes / 1024}KB`, + error: `Append would exceed cumulative file write limit of ${maxWriteBytes / 1024}KB. To increase, reconfigure fs-write with a larger maxWriteSizeKb.`, }; } @@ -353,7 +353,7 @@ export function createHostFunctions( if (isNew) { if (entriesCreated >= maxEntries) { return { - error: `Entry limit reached: cannot create more than ${maxEntries} files/directories`, + error: `Entry limit reached: cannot create more than ${maxEntries} files/directories. To increase, reconfigure fs-write with a larger maxEntries.`, }; } entriesCreated++; @@ -372,7 +372,7 @@ export function createHostFunctions( } if (fdStat.size + contentBytes > maxWriteBytes) { return { - error: `Append would exceed cumulative file write limit of ${maxWriteBytes / 1024}KB (current: ${fdStat.size} bytes + new: ${contentBytes} bytes)`, + error: `Append would exceed cumulative file write limit of ${maxWriteBytes / 1024}KB (current: ${fdStat.size} bytes + new: ${contentBytes} bytes). To increase, reconfigure fs-write with a larger maxWriteSizeKb.`, }; } @@ -435,12 +435,12 @@ export function createHostFunctions( : " Split into multiple appendFileBinary calls."; throw new Error( `Content too large for single write: ${contentBytes} bytes exceeds ` + - `per-call limit of ${maxWriteChunkBytes / 1024}KB.${hint}`, + `per-call limit of ${maxWriteChunkBytes / 1024}KB.${hint} Or reconfigure fs-write with a larger maxWriteChunkKb.`, ); } if (contentBytes > maxWriteBytes) { throw new Error( - `Content too large: exceeds cumulative file write limit of ${maxWriteBytes / 1024}KB`, + `Content too large: exceeds cumulative file write limit of ${maxWriteBytes / 1024}KB. To increase, reconfigure fs-write with a larger maxWriteSizeKb.`, ); } @@ -455,7 +455,7 @@ export function createHostFunctions( if (isNew) { if (entriesCreated >= maxEntries) { throw new Error( - `Entry limit reached: cannot create more than ${maxEntries} files/directories`, + `Entry limit reached: cannot create more than ${maxEntries} files/directories. To increase, reconfigure fs-write with a larger maxEntries.`, ); } entriesCreated++; @@ -520,12 +520,12 @@ export function createHostFunctions( throw new Error( `Append content too large for single call: ${contentBytes} bytes exceeds ` + `per-call limit of ${maxWriteChunkBytes / 1024}KB. ` + - `Split into smaller appendFileBinary calls.`, + `Split into smaller appendFileBinary calls, or reconfigure fs-write with a larger maxWriteChunkKb.`, ); } if (contentBytes > maxWriteBytes) { throw new Error( - `Append would exceed cumulative file write limit of ${maxWriteBytes / 1024}KB`, + `Append would exceed cumulative file write limit of ${maxWriteBytes / 1024}KB. To increase, reconfigure fs-write with a larger maxWriteSizeKb.`, ); } @@ -540,7 +540,7 @@ export function createHostFunctions( if (isNew) { if (entriesCreated >= maxEntries) { throw new Error( - `Entry limit reached: cannot create more than ${maxEntries} files/directories`, + `Entry limit reached: cannot create more than ${maxEntries} files/directories. To increase, reconfigure fs-write with a larger maxEntries.`, ); } entriesCreated++; @@ -560,7 +560,7 @@ export function createHostFunctions( if (fdStat.size + contentBytes > maxWriteBytes) { throw new Error( `Append would exceed cumulative file write limit of ` + - `${maxWriteBytes / 1024}KB (current: ${fdStat.size} bytes + new: ${contentBytes} bytes)`, + `${maxWriteBytes / 1024}KB (current: ${fdStat.size} bytes + new: ${contentBytes} bytes). To increase, reconfigure fs-write with a larger maxWriteSizeKb.`, ); } @@ -595,7 +595,7 @@ export function createHostFunctions( } if (entriesCreated >= maxEntries) { return { - error: `Entry limit reached: cannot create more than ${maxEntries} files/directories`, + error: `Entry limit reached: cannot create more than ${maxEntries} files/directories. To increase, reconfigure fs-write with a larger maxEntries.`, }; } entriesCreated++; From adc4b04f60c11feb1441cc466aa8c7beece9b671 Mon Sep 17 00:00:00 2001 From: Simon Davies Date: Thu, 7 May 2026 14:07:34 +0100 Subject: [PATCH 2/2] fix: address PR #112 review feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - writeFile chunk error now advises 'first chunk via writeFile, rest via appendFile' instead of just 'split into appendFile' which would change overwrite→append semantics - writeFileBinary same fix for non-Office fallback message - All maxReadChunkKb and maxWriteChunkKb config hints now warn to ensure the sandbox buffer is large enough to match, preventing VM faults from raising chunk size without the buffer Signed-off-by: Simon Davies --- plugins/fs-read/index.ts | 4 ++-- plugins/fs-write/index.ts | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/plugins/fs-read/index.ts b/plugins/fs-read/index.ts index 44a8856..eee3d3c 100644 --- a/plugins/fs-read/index.ts +++ b/plugins/fs-read/index.ts @@ -277,7 +277,7 @@ export function createHostFunctions( } if (fileStat.size > maxReadChunkBytes) { return { - error: `File too large for single read: ${fileStat.size} bytes exceeds per-call limit of ${maxReadChunkBytes / 1024}KB. Use readFileChunk(path, offsetBytes, lengthBytes) to read in chunks, or reconfigure fs-read with a larger maxReadChunkKb.`, + error: `File too large for single read: ${fileStat.size} bytes exceeds per-call limit of ${maxReadChunkBytes / 1024}KB. Use readFileChunk(path, offsetBytes, lengthBytes) to read in chunks, or reconfigure fs-read with a larger maxReadChunkKb (ensure the sandbox input buffer is large enough to match).`, }; } @@ -491,7 +491,7 @@ export function createHostFunctions( throw new Error( `File too large for single read: ${fileStat.size} bytes exceeds ` + `per-call limit of ${maxReadChunkBytes / 1024}KB. ` + - `Use readFileChunkBinary(path, offsetBytes, lengthBytes) to read in chunks, or reconfigure fs-read with a larger maxReadChunkKb.`, + `Use readFileChunkBinary(path, offsetBytes, lengthBytes) to read in chunks, or reconfigure fs-read with a larger maxReadChunkKb (ensure the sandbox input buffer is large enough to match).`, ); } diff --git a/plugins/fs-write/index.ts b/plugins/fs-write/index.ts index eb3e744..28b0c90 100644 --- a/plugins/fs-write/index.ts +++ b/plugins/fs-write/index.ts @@ -243,7 +243,7 @@ export function createHostFunctions( if (contentBytes > maxWriteChunkBytes) { return { - error: `Content too large for single write: ${contentBytes} bytes exceeds per-call limit of ${maxWriteChunkBytes / 1024}KB. Split into multiple appendFile calls, or reconfigure fs-write with a larger maxWriteChunkKb.`, + error: `Content too large for single write: ${contentBytes} bytes exceeds per-call limit of ${maxWriteChunkBytes / 1024}KB. Write the first chunk with writeFile, then append remaining chunks with appendFile. Or reconfigure fs-write with a larger maxWriteChunkKb (ensure the sandbox output buffer is large enough to match).`, }; } if (contentBytes > maxWriteBytes) { @@ -432,10 +432,10 @@ export function createHostFunctions( filePath.toLowerCase().endsWith(".docx"); const hint = isOffice ? ` For ${isPptx ? "PPTX" : "Office"} files, use exportToFile(pres, filename, fsWrite) from ha:pptx which handles chunking automatically.` - : " Split into multiple appendFileBinary calls."; + : " Write the first chunk with writeFileBinary, then append remaining chunks with appendFileBinary."; throw new Error( `Content too large for single write: ${contentBytes} bytes exceeds ` + - `per-call limit of ${maxWriteChunkBytes / 1024}KB.${hint} Or reconfigure fs-write with a larger maxWriteChunkKb.`, + `per-call limit of ${maxWriteChunkBytes / 1024}KB.${hint} Or reconfigure fs-write with a larger maxWriteChunkKb (ensure the sandbox output buffer is large enough to match).`, ); } if (contentBytes > maxWriteBytes) {