diff --git a/src/acp/event-mapper.ts b/src/acp/event-mapper.ts index 5e1179f1cb..5ba8b02db6 100644 --- a/src/acp/event-mapper.ts +++ b/src/acp/event-mapper.ts @@ -6,25 +6,33 @@ export type GatewayAttachment = { content: string; }; -export function extractTextFromPrompt(prompt: ContentBlock[]): string { +export function extractTextFromPrompt(prompt: ContentBlock[], maxBytes?: number): string { const parts: string[] = []; + // Track accumulated byte count per block to catch oversized prompts before full concatenation + let totalBytes = 0; for (const block of prompt) { + let blockText: string | undefined; if (block.type === "text") { - parts.push(block.text); - continue; - } - if (block.type === "resource") { + blockText = block.text; + } else if (block.type === "resource") { const resource = block.resource as { text?: string } | undefined; if (resource?.text) { - parts.push(resource.text); + blockText = resource.text; } - continue; - } - if (block.type === "resource_link") { + } else if (block.type === "resource_link") { const title = block.title ? ` (${block.title})` : ""; const uri = block.uri ?? ""; - const line = uri ? `[Resource link${title}] ${uri}` : `[Resource link${title}]`; - parts.push(line); + blockText = uri ? `[Resource link${title}] ${uri}` : `[Resource link${title}]`; + } + if (blockText !== undefined) { + // Guard: reject before allocating the full concatenated string + if (maxBytes !== undefined) { + totalBytes += Buffer.byteLength(blockText, "utf-8"); + if (totalBytes > maxBytes) { + throw new Error(`Prompt exceeds maximum allowed size of ${maxBytes} bytes`); + } + } + parts.push(blockText); } } return parts.join("\n"); diff --git a/src/acp/translator.ts b/src/acp/translator.ts index 89f6e1d8bd..e90437fa7a 100644 --- a/src/acp/translator.ts +++ b/src/acp/translator.ts @@ -264,13 +264,15 @@ export class AcpGatewayAgent implements Agent { this.sessionStore.setActiveRun(params.sessionId, runId, abortController); const meta = parseSessionMeta(params._meta); - const userText = extractTextFromPrompt(params.prompt); + // Pass MAX_PROMPT_BYTES so extractTextFromPrompt rejects oversized content + // block-by-block, before the full string is ever assembled in memory (CWE-400) + const userText = extractTextFromPrompt(params.prompt, MAX_PROMPT_BYTES); const attachments = extractAttachmentsFromPrompt(params.prompt); const prefixCwd = meta.prefixCwd ?? this.opts.prefixCwd ?? true; const displayCwd = shortenHomePath(session.cwd); const message = prefixCwd ? `[Working directory: ${displayCwd}]\n\n${userText}` : userText; - // Guard against oversized prompts that could cause memory exhaustion (DoS) + // Defense-in-depth: also check the final assembled message (includes cwd prefix) if (Buffer.byteLength(message, "utf-8") > MAX_PROMPT_BYTES) { throw new Error( `Prompt exceeds maximum allowed size of ${MAX_PROMPT_BYTES} bytes`,