From 119210233abf3771ca7e3e9a08f7ffd06fd6b994 Mon Sep 17 00:00:00 2001 From: Hannes Rudolph Date: Thu, 16 Oct 2025 17:19:52 -0600 Subject: [PATCH 1/2] Redact read_file and mention payloads from ui_messages.json; prevent storing file contents in clineMessages (Fixes #8690) --- .../src/suite/tools/read-file.test.ts | 57 ++-------- src/core/task/Task.ts | 100 ++++++++++++++++-- 2 files changed, 102 insertions(+), 55 deletions(-) diff --git a/apps/vscode-e2e/src/suite/tools/read-file.test.ts b/apps/vscode-e2e/src/suite/tools/read-file.test.ts index 99e3f184577..13bdf6b65ee 100644 --- a/apps/vscode-e2e/src/suite/tools/read-file.test.ts +++ b/apps/vscode-e2e/src/suite/tools/read-file.test.ts @@ -127,7 +127,7 @@ suite("Roo Code read_file Tool", function () { let taskCompleted = false let errorOccurred: string | null = null let toolExecuted = false - let toolResult: string | null = null + let _toolResult: string | null = null // Listen for messages const messageHandler = ({ message }: { message: ClineMessage }) => { @@ -157,8 +157,8 @@ suite("Roo Code read_file Tool", function () { resultMatch = requestData.request.match(/Result:\s*\n([\s\S]+?)(?:\n\n|$)/) } if (resultMatch) { - toolResult = resultMatch[1] - console.log("Extracted tool result:", toolResult) + _toolResult = resultMatch[1] + console.log("Extracted tool result:", _toolResult) } else { console.log("Could not extract tool result from request") } @@ -235,16 +235,7 @@ suite("Roo Code read_file Tool", function () { // Check that no errors occurred assert.strictEqual(errorOccurred, null, "No errors should have occurred") - // Verify the tool returned the correct content - assert.ok(toolResult !== null, "Tool should have returned a result") - // The tool returns content with line numbers, so we need to extract just the content - // For single line, the format is "1 | Hello, World!" - const actualContent = (toolResult as string).replace(/^\d+\s*\|\s*/, "") - assert.strictEqual( - actualContent.trim(), - "Hello, World!", - "Tool should have returned the exact file content", - ) + // Note: read_file tool result content is redacted from clineMessages; verify via AI response instead. // Also verify the AI mentioned the content in its response const hasContent = messages.some( @@ -270,7 +261,7 @@ suite("Roo Code read_file Tool", function () { const messages: ClineMessage[] = [] let taskCompleted = false let toolExecuted = false - let toolResult: string | null = null + let _toolResult: string | null = null // Listen for messages const messageHandler = ({ message }: { message: ClineMessage }) => { @@ -297,7 +288,7 @@ suite("Roo Code read_file Tool", function () { resultMatch = requestData.request.match(/Result:\s*\n([\s\S]+?)(?:\n\n|$)/) } if (resultMatch) { - toolResult = resultMatch[1] + _toolResult = resultMatch[1] console.log("Extracted multiline tool result") } else { console.log("Could not extract tool result from request") @@ -344,20 +335,7 @@ suite("Roo Code read_file Tool", function () { // Verify the read_file tool was executed assert.ok(toolExecuted, "The read_file tool should have been executed") - // Verify the tool returned the correct multiline content - assert.ok(toolResult !== null, "Tool should have returned a result") - // The tool returns content with line numbers, so we need to extract just the content - const lines = (toolResult as string).split("\n").map((line) => { - const match = line.match(/^\d+\s*\|\s*(.*)$/) - return match ? match[1] : line - }) - const actualContent = lines.join("\n") - const expectedContent = "Line 1\nLine 2\nLine 3\nLine 4\nLine 5" - assert.strictEqual( - actualContent.trim(), - expectedContent, - "Tool should have returned the exact multiline content", - ) + // Note: read_file tool result content is redacted from clineMessages; verify via AI response instead. // Also verify the AI mentioned the correct number of lines const hasLineCount = messages.some( @@ -381,7 +359,7 @@ suite("Roo Code read_file Tool", function () { const messages: ClineMessage[] = [] let taskCompleted = false let toolExecuted = false - let toolResult: string | null = null + let _toolResult: string | null = null // Listen for messages const messageHandler = ({ message }: { message: ClineMessage }) => { @@ -408,7 +386,7 @@ suite("Roo Code read_file Tool", function () { resultMatch = requestData.request.match(/Result:\s*\n([\s\S]+?)(?:\n\n|$)/) } if (resultMatch) { - toolResult = resultMatch[1] + _toolResult = resultMatch[1] console.log("Extracted line range tool result") } else { console.log("Could not extract tool result from request") @@ -455,22 +433,7 @@ suite("Roo Code read_file Tool", function () { // Verify tool was executed assert.ok(toolExecuted, "The read_file tool should have been executed") - // Verify the tool returned the correct lines (when line range is used) - if (toolResult && (toolResult as string).includes(" | ")) { - // The result includes line numbers - assert.ok( - (toolResult as string).includes("2 | Line 2"), - "Tool result should include line 2 with line number", - ) - assert.ok( - (toolResult as string).includes("3 | Line 3"), - "Tool result should include line 3 with line number", - ) - assert.ok( - (toolResult as string).includes("4 | Line 4"), - "Tool result should include line 4 with line number", - ) - } + // Note: read_file tool result content is redacted from clineMessages; verify via AI response instead. // Also verify the AI mentioned the specific lines const hasLines = messages.some( diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index 851df91e6c5..ecf1f8a81db 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -603,11 +603,55 @@ export class Task extends EventEmitter implements TaskLike { // Cline Messages + // Redact file payloads from UI-persisted messages (ui_messages.json) + // while leaving full content intact for apiConversationHistory. + private sanitizeMessageText(text?: string): string | undefined { + if (!text) return text + + const scrub = (s: string): string => { + // Replace inner contents of known file payload tags with an omission marker + // Order matters: scrub more specific tags first. + s = s.replace(//gi, "[omitted]") + s = s.replace(/]*>[\s\S]*?<\/content>/gi, "[omitted]") + s = s.replace(/]*>[\s\S]*?<\/file>/gi, "[omitted]") + s = s.replace(/]*>[\s\S]*?<\/files>/gi, "[omitted]") + return s + } + + // If this is a JSON payload (e.g. api_req_started), try to sanitize the 'request' field. + try { + const obj = JSON.parse(text) + if (obj && typeof obj === "object" && typeof obj.request === "string") { + obj.request = scrub(obj.request) + return JSON.stringify(obj) + } + } catch { + // Not JSON, fall-through to raw scrub + } + + return scrub(text) + } + + // Sanitize an array of messages for persistence to UI storage + private sanitizeMessagesArray(messages: ClineMessage[]): ClineMessage[] { + return messages.map((m) => { + if (typeof (m as any).text === "string") { + return { ...m, text: this.sanitizeMessageText((m as any).text) } + } + return m + }) + } + private async getSavedClineMessages(): Promise { - return readTaskMessages({ taskId: this.taskId, globalStoragePath: this.globalStoragePath }) + const msgs = await readTaskMessages({ taskId: this.taskId, globalStoragePath: this.globalStoragePath }) + return this.sanitizeMessagesArray(msgs) } private async addToClineMessages(message: ClineMessage) { + // Sanitize any UI-persisted text before storing + if (typeof message.text === "string") { + message.text = this.sanitizeMessageText(message.text) + } this.clineMessages.push(message) const provider = this.providerRef.deref() await provider?.postStateToWebview() @@ -625,7 +669,7 @@ export class Task extends EventEmitter implements TaskLike { } public async overwriteClineMessages(newMessages: ClineMessage[]) { - this.clineMessages = newMessages + this.clineMessages = this.sanitizeMessagesArray(newMessages) // If deletion or history truncation leaves a condense_context as the last message, // ensure the next API call suppresses previous_response_id so the condensed context is respected. @@ -643,6 +687,10 @@ export class Task extends EventEmitter implements TaskLike { } private async updateClineMessage(message: ClineMessage) { + // Ensure any updates are also sanitized before persisting/posting + if (typeof message.text === "string") { + message.text = this.sanitizeMessageText(message.text) + } const provider = this.providerRef.deref() await provider?.postMessageToWebview({ type: "messageUpdated", clineMessage: message }) this.emit(RooCodeEventName.Message, { action: "updated", message }) @@ -659,8 +707,11 @@ export class Task extends EventEmitter implements TaskLike { private async saveClineMessages() { try { + // Sanitize just before persisting to ensure any direct mutations are scrubbed + const sanitizedMessages = this.sanitizeMessagesArray(this.clineMessages) + await saveTaskMessages({ - messages: this.clineMessages, + messages: sanitizedMessages, taskId: this.taskId, globalStoragePath: this.globalStoragePath, }) @@ -670,7 +721,7 @@ export class Task extends EventEmitter implements TaskLike { rootTaskId: this.rootTaskId, parentTaskId: this.parentTaskId, taskNumber: this.taskNumber, - messages: this.clineMessages, + messages: sanitizedMessages, globalStoragePath: this.globalStoragePath, workspace: this.cwd, mode: this._taskMode || defaultModeSlug, // Use the task's own mode, not the current provider mode. @@ -1790,12 +1841,45 @@ export class Task extends EventEmitter implements TaskLike { const modelId = getModelId(this.apiConfiguration) const apiProtocol = getApiProtocol(this.apiConfiguration.apiProvider, modelId) + // Redact any read_file results or file payload blocks from UI messages. + // This prevents file contents from being persisted to ui_messages.json while + // still sending full content to the LLM via apiConversationHistory. + const formatRequestWithReadFileRedaction = (blocks: Anthropic.Messages.ContentBlockParam[]) => { + let redactNext = false + const parts = blocks.map((block: any) => { + if (block?.type === "text") { + const text = String(block.text ?? "") + + // 1) Detect the explicit read_file header line emitted by pushToolResult + const isReadFileHeader = /^\[read_file\b[\s\S]*\]\s*Result:/i.test(text) + + // 2) Detect any XML-like file payloads that tools may include + // Examples: ..., ..., ..., ... + const looksLikeFilePayload = /]|]| formatContentBlockToMarkdown(block)).join("\n\n") + - "\n\nLoading...", + request: formatRequestWithReadFileRedaction(currentUserContent) + "\n\nLoading...", apiProtocol, }), ) @@ -1835,7 +1919,7 @@ export class Task extends EventEmitter implements TaskLike { const lastApiReqIndex = findLastIndex(this.clineMessages, (m) => m.say === "api_req_started") this.clineMessages[lastApiReqIndex].text = JSON.stringify({ - request: finalUserContent.map((block) => formatContentBlockToMarkdown(block)).join("\n\n"), + request: formatRequestWithReadFileRedaction(finalUserContent), apiProtocol, } satisfies ClineApiReqInfo) From 0377592f0b0450e8c5c0b760f5dcc9d8b0879322 Mon Sep 17 00:00:00 2001 From: Hannes Rudolph Date: Thu, 16 Oct 2025 18:25:29 -0600 Subject: [PATCH 2/2] test(read-file): remove redundant inline notes per review by @daniel-lxs --- apps/vscode-e2e/src/suite/tools/read-file.test.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/apps/vscode-e2e/src/suite/tools/read-file.test.ts b/apps/vscode-e2e/src/suite/tools/read-file.test.ts index 13bdf6b65ee..8028a8f7a68 100644 --- a/apps/vscode-e2e/src/suite/tools/read-file.test.ts +++ b/apps/vscode-e2e/src/suite/tools/read-file.test.ts @@ -235,8 +235,6 @@ suite("Roo Code read_file Tool", function () { // Check that no errors occurred assert.strictEqual(errorOccurred, null, "No errors should have occurred") - // Note: read_file tool result content is redacted from clineMessages; verify via AI response instead. - // Also verify the AI mentioned the content in its response const hasContent = messages.some( (m) => @@ -335,8 +333,6 @@ suite("Roo Code read_file Tool", function () { // Verify the read_file tool was executed assert.ok(toolExecuted, "The read_file tool should have been executed") - // Note: read_file tool result content is redacted from clineMessages; verify via AI response instead. - // Also verify the AI mentioned the correct number of lines const hasLineCount = messages.some( (m) => @@ -433,8 +429,6 @@ suite("Roo Code read_file Tool", function () { // Verify tool was executed assert.ok(toolExecuted, "The read_file tool should have been executed") - // Note: read_file tool result content is redacted from clineMessages; verify via AI response instead. - // Also verify the AI mentioned the specific lines const hasLines = messages.some( (m) =>