fix(security): harden untrusted web tool transcripts

This commit is contained in:
Peter Steinberger
2026-02-13 00:46:11 +01:00
parent 4543c401b4
commit da55d70fb0
13 changed files with 484 additions and 18 deletions

View File

@@ -13,8 +13,29 @@ const MERGE_SUMMARIES_INSTRUCTIONS =
"Merge these partial summaries into a single cohesive summary. Preserve decisions," +
" TODOs, open questions, and any constraints.";
function stripToolResultDetails(messages: AgentMessage[]): AgentMessage[] {
let touched = false;
const out: AgentMessage[] = [];
for (const msg of messages) {
if (!msg || typeof msg !== "object" || (msg as { role?: unknown }).role !== "toolResult") {
out.push(msg);
continue;
}
if (!("details" in msg)) {
out.push(msg);
continue;
}
const { details: _details, ...rest } = msg as unknown as Record<string, unknown>;
touched = true;
out.push(rest as unknown as AgentMessage);
}
return touched ? out : messages;
}
export function estimateMessagesTokens(messages: AgentMessage[]): number {
return messages.reduce((sum, message) => sum + estimateTokens(message), 0);
// SECURITY: toolResult.details can contain untrusted/verbose payloads; never include in LLM-facing compaction.
const safe = stripToolResultDetails(messages);
return safe.reduce((sum, message) => sum + estimateTokens(message), 0);
}
function normalizeParts(parts: number, messageCount: number): number {
@@ -151,7 +172,9 @@ async function summarizeChunks(params: {
return params.previousSummary ?? DEFAULT_SUMMARY_FALLBACK;
}
const chunks = chunkMessagesByMaxTokens(params.messages, params.maxChunkTokens);
// SECURITY: never feed toolResult.details into summarization prompts.
const safeMessages = stripToolResultDetails(params.messages);
const chunks = chunkMessagesByMaxTokens(safeMessages, params.maxChunkTokens);
let summary = params.previousSummary;
for (const chunk of chunks) {