mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-10-06 02:47:56 +00:00
Redact read_file and mention payloads from ui_messages.json; prevent storing file contents in clineMessages (Fixes #8690)
This commit is contained in:
parent
270dce5050
commit
119210233a
2 changed files with 102 additions and 55 deletions
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -603,11 +603,55 @@ export class Task extends EventEmitter<TaskEvents> 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(/<file_content\b[\s\S]*?<\/file_content>/gi, "<file_content>[omitted]</file_content>")
|
||||
s = s.replace(/<content\b[^>]*>[\s\S]*?<\/content>/gi, "<content>[omitted]</content>")
|
||||
s = s.replace(/<file\b[^>]*>[\s\S]*?<\/file>/gi, "<file>[omitted]</file>")
|
||||
s = s.replace(/<files\b[^>]*>[\s\S]*?<\/files>/gi, "<files>[omitted]</files>")
|
||||
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<ClineMessage[]> {
|
||||
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<TaskEvents> 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<TaskEvents> 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<TaskEvents> 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<TaskEvents> 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<TaskEvents> 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: <files>...</files>, <file>...</file>, <content ...>...</content>, <file_content ...>...</file_content>
|
||||
const looksLikeFilePayload = /<files[\s>]|<file[\s>]|<content\b|<file_content\b/i.test(text)
|
||||
|
||||
// If we see the header, show the header but redact the next text block (payload)
|
||||
if (isReadFileHeader) {
|
||||
redactNext = true
|
||||
return text
|
||||
}
|
||||
|
||||
// If the previous block was a read_file header, or this block itself looks like a file payload, redact it
|
||||
if (redactNext || looksLikeFilePayload) {
|
||||
redactNext = false
|
||||
return "[tool output omitted from UI storage]"
|
||||
}
|
||||
}
|
||||
|
||||
// Default formatting for other blocks
|
||||
return formatContentBlockToMarkdown(block as any)
|
||||
})
|
||||
return parts.join("\n\n")
|
||||
}
|
||||
|
||||
await this.say(
|
||||
"api_req_started",
|
||||
JSON.stringify({
|
||||
request:
|
||||
currentUserContent.map((block) => formatContentBlockToMarkdown(block)).join("\n\n") +
|
||||
"\n\nLoading...",
|
||||
request: formatRequestWithReadFileRedaction(currentUserContent) + "\n\nLoading...",
|
||||
apiProtocol,
|
||||
}),
|
||||
)
|
||||
|
|
@ -1835,7 +1919,7 @@ export class Task extends EventEmitter<TaskEvents> 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)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue