Skip to content

Commit f3e46fc

Browse files
committed
feat(core): centralize UI message redaction in persistence layer to prevent storing file payloads in ui_messages.json; sanitize on save and read
1 parent 0e7a878 commit f3e46fc

File tree

1 file changed

+50
-2
lines changed

1 file changed

+50
-2
lines changed

src/core/task-persistence/taskMessages.ts

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,49 @@ import { fileExistsAtPath } from "../../utils/fs"
99
import { GlobalFileNames } from "../../shared/globalFileNames"
1010
import { getTaskDirectoryPath } from "../../utils/storage"
1111

12+
/**
13+
* Redaction utilities:
14+
* We only need to ensure sensitive file payloads are NOT persisted to disk (ui_messages.json).
15+
* Centralizing the sanitization in the persistence layer keeps Task.ts simple and avoids scattering
16+
* redaction logic across multiple call-sites.
17+
*/
18+
19+
function sanitizeMessageText(text?: string): string | undefined {
20+
if (!text) return text
21+
22+
// Scrub helper that replaces inner contents of known file payload tags with an omission marker
23+
const scrub = (s: string): string => {
24+
// Order matters: scrub more specific tags first
25+
s = s.replace(/<file_content\b[\s\S]*?<\/file_content>/gi, "<file_content>[omitted]</file_content>")
26+
s = s.replace(/<content\b[^>]*>[\s\S]*?<\/content>/gi, "<content>[omitted]</content>")
27+
s = s.replace(/<file\b[^>]*>[\s\S]*?<\/file>/gi, "<file>[omitted]</file>")
28+
s = s.replace(/<files\b[^>]*>[\s\S]*?<\/files>/gi, "<files>[omitted]</files>")
29+
return s
30+
}
31+
32+
// If JSON payload (e.g. api_req_started), try to sanitize its 'request' field
33+
try {
34+
const obj = JSON.parse(text)
35+
if (obj && typeof obj === "object" && typeof obj.request === "string") {
36+
obj.request = scrub(obj.request)
37+
return JSON.stringify(obj)
38+
}
39+
} catch {
40+
// Not JSON; fall through to raw scrub
41+
}
42+
43+
return scrub(text)
44+
}
45+
46+
function sanitizeMessages(messages: ClineMessage[]): ClineMessage[] {
47+
return messages.map((m) => {
48+
if (typeof (m as any).text === "string") {
49+
return { ...m, text: sanitizeMessageText((m as any).text) }
50+
}
51+
return m
52+
})
53+
}
54+
1255
export type ReadTaskMessagesOptions = {
1356
taskId: string
1457
globalStoragePath: string
@@ -23,7 +66,9 @@ export async function readTaskMessages({
2366
const fileExists = await fileExistsAtPath(filePath)
2467

2568
if (fileExists) {
26-
return JSON.parse(await fs.readFile(filePath, "utf8"))
69+
// Sanitize on read as a safety net for any legacy persisted content
70+
const raw = JSON.parse(await fs.readFile(filePath, "utf8"))
71+
return sanitizeMessages(raw)
2772
}
2873

2974
return []
@@ -38,5 +83,8 @@ export type SaveTaskMessagesOptions = {
3883
export async function saveTaskMessages({ messages, taskId, globalStoragePath }: SaveTaskMessagesOptions) {
3984
const taskDir = await getTaskDirectoryPath(globalStoragePath, taskId)
4085
const filePath = path.join(taskDir, GlobalFileNames.uiMessages)
41-
await safeWriteJson(filePath, messages)
86+
87+
// Persist a sanitized copy to disk to avoid storing sensitive file payloads
88+
const sanitized = sanitizeMessages(messages)
89+
await safeWriteJson(filePath, sanitized)
4290
}

0 commit comments

Comments
 (0)