fix: sanitize tool call text in sessions-helpers extractAssistantText
Adds sanitization to extractAssistantText in sessions-helpers.ts to prevent tool call text from leaking to users. Previously, messages retrieved from chat history via sessions-helpers.ts could expose: - Minimax XML tool calls (<invoke>...</invoke>) - Downgraded tool call markers ([Tool Call: name (ID: ...)]) - Thinking tags (<think>...</think>) This fix: - Exports the stripping functions from pi-embedded-utils.ts - Adds a new sanitizeTextContent helper in sessions-helpers.ts - Updates extractAssistantText to sanitize before returning - Updates extractMessageText in commands-subagents.ts to sanitize Fixes #1269 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
committed by
Peter Steinberger
parent
6779ba2367
commit
03bec49299
@@ -9,7 +9,7 @@ import { formatToolDetail, resolveToolDisplay } from "./tool-display.js";
|
|||||||
* - <invoke name="...">...</invoke> blocks
|
* - <invoke name="...">...</invoke> blocks
|
||||||
* - </minimax:tool_call> closing tags
|
* - </minimax:tool_call> closing tags
|
||||||
*/
|
*/
|
||||||
function stripMinimaxToolCallXml(text: string): string {
|
export function stripMinimaxToolCallXml(text: string): string {
|
||||||
if (!text) return text;
|
if (!text) return text;
|
||||||
if (!/minimax:tool_call/i.test(text)) return text;
|
if (!/minimax:tool_call/i.test(text)) return text;
|
||||||
|
|
||||||
@@ -28,7 +28,7 @@ function stripMinimaxToolCallXml(text: string): string {
|
|||||||
* downgraded to text blocks like `[Tool Call: name (ID: ...)]`. These should
|
* downgraded to text blocks like `[Tool Call: name (ID: ...)]`. These should
|
||||||
* not be shown to users.
|
* not be shown to users.
|
||||||
*/
|
*/
|
||||||
function stripDowngradedToolCallText(text: string): string {
|
export function stripDowngradedToolCallText(text: string): string {
|
||||||
if (!text) return text;
|
if (!text) return text;
|
||||||
if (!/\[Tool (?:Call|Result)/i.test(text)) return text;
|
if (!/\[Tool (?:Call|Result)/i.test(text)) return text;
|
||||||
|
|
||||||
@@ -165,7 +165,7 @@ function stripDowngradedToolCallText(text: string): string {
|
|||||||
* This is a safety net for cases where the model outputs <think> tags
|
* This is a safety net for cases where the model outputs <think> tags
|
||||||
* that slip through other filtering mechanisms.
|
* that slip through other filtering mechanisms.
|
||||||
*/
|
*/
|
||||||
function stripThinkingTagsFromText(text: string): string {
|
export function stripThinkingTagsFromText(text: string): string {
|
||||||
if (!text) return text;
|
if (!text) return text;
|
||||||
// Quick check to avoid regex overhead when no tags present.
|
// Quick check to avoid regex overhead when no tags present.
|
||||||
if (!/(?:think(?:ing)?|thought|antthinking)/i.test(text)) return text;
|
if (!/(?:think(?:ing)?|thought|antthinking)/i.test(text)) return text;
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
import type { ClawdbotConfig } from "../../config/config.js";
|
import type { ClawdbotConfig } from "../../config/config.js";
|
||||||
|
import { sanitizeUserFacingText } from "../pi-embedded-helpers.js";
|
||||||
|
import {
|
||||||
|
stripDowngradedToolCallText,
|
||||||
|
stripMinimaxToolCallXml,
|
||||||
|
stripThinkingTagsFromText,
|
||||||
|
} from "../pi-embedded-utils.js";
|
||||||
import { normalizeMainKey } from "../../routing/session-key.js";
|
import { normalizeMainKey } from "../../routing/session-key.js";
|
||||||
|
|
||||||
export type SessionKind = "main" | "group" | "cron" | "hook" | "node" | "other";
|
export type SessionKind = "main" | "group" | "cron" | "hook" | "node" | "other";
|
||||||
@@ -100,6 +106,16 @@ export function stripToolMessages(messages: unknown[]): unknown[] {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sanitize text content to strip tool call markers and thinking tags.
|
||||||
|
* This ensures user-facing text doesn't leak internal tool representations.
|
||||||
|
*/
|
||||||
|
export function sanitizeTextContent(text: string): string {
|
||||||
|
return stripThinkingTagsFromText(
|
||||||
|
stripDowngradedToolCallText(stripMinimaxToolCallXml(text)),
|
||||||
|
).trim();
|
||||||
|
}
|
||||||
|
|
||||||
export function extractAssistantText(message: unknown): string | undefined {
|
export function extractAssistantText(message: unknown): string | undefined {
|
||||||
if (!message || typeof message !== "object") return undefined;
|
if (!message || typeof message !== "object") return undefined;
|
||||||
if ((message as { role?: unknown }).role !== "assistant") return undefined;
|
if ((message as { role?: unknown }).role !== "assistant") return undefined;
|
||||||
@@ -110,10 +126,13 @@ export function extractAssistantText(message: unknown): string | undefined {
|
|||||||
if (!block || typeof block !== "object") continue;
|
if (!block || typeof block !== "object") continue;
|
||||||
if ((block as { type?: unknown }).type !== "text") continue;
|
if ((block as { type?: unknown }).type !== "text") continue;
|
||||||
const text = (block as { text?: unknown }).text;
|
const text = (block as { text?: unknown }).text;
|
||||||
if (typeof text === "string" && text.trim()) {
|
if (typeof text === "string") {
|
||||||
chunks.push(text);
|
const sanitized = sanitizeTextContent(text);
|
||||||
|
if (sanitized) {
|
||||||
|
chunks.push(sanitized);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const joined = chunks.join("").trim();
|
}
|
||||||
return joined ? joined : undefined;
|
const joined = chunks.join("\n").trim();
|
||||||
|
return joined ? sanitizeUserFacingText(joined) : undefined;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
extractAssistantText,
|
extractAssistantText,
|
||||||
resolveInternalSessionKey,
|
resolveInternalSessionKey,
|
||||||
resolveMainSessionAlias,
|
resolveMainSessionAlias,
|
||||||
|
sanitizeTextContent,
|
||||||
stripToolMessages,
|
stripToolMessages,
|
||||||
} from "../../agents/tools/sessions-helpers.js";
|
} from "../../agents/tools/sessions-helpers.js";
|
||||||
import type { SubagentRunRecord } from "../../agents/subagent-registry.js";
|
import type { SubagentRunRecord } from "../../agents/subagent-registry.js";
|
||||||
@@ -110,7 +111,8 @@ function extractMessageText(message: ChatMessage): { role: string; text: string
|
|||||||
const role = typeof message.role === "string" ? message.role : "";
|
const role = typeof message.role === "string" ? message.role : "";
|
||||||
const content = message.content;
|
const content = message.content;
|
||||||
if (typeof content === "string") {
|
if (typeof content === "string") {
|
||||||
const normalized = normalizeMessageText(content);
|
const sanitized = sanitizeTextContent(content);
|
||||||
|
const normalized = normalizeMessageText(sanitized);
|
||||||
return normalized ? { role, text: normalized } : null;
|
return normalized ? { role, text: normalized } : null;
|
||||||
}
|
}
|
||||||
if (!Array.isArray(content)) return null;
|
if (!Array.isArray(content)) return null;
|
||||||
@@ -119,8 +121,11 @@ function extractMessageText(message: ChatMessage): { role: string; text: string
|
|||||||
if (!block || typeof block !== "object") continue;
|
if (!block || typeof block !== "object") continue;
|
||||||
if ((block as { type?: unknown }).type !== "text") continue;
|
if ((block as { type?: unknown }).type !== "text") continue;
|
||||||
const text = (block as { text?: unknown }).text;
|
const text = (block as { text?: unknown }).text;
|
||||||
if (typeof text === "string" && text.trim()) {
|
if (typeof text === "string") {
|
||||||
chunks.push(text);
|
const sanitized = sanitizeTextContent(text);
|
||||||
|
if (sanitized) {
|
||||||
|
chunks.push(sanitized);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const joined = normalizeMessageText(chunks.join(" "));
|
const joined = normalizeMessageText(chunks.join(" "));
|
||||||
|
|||||||
Reference in New Issue
Block a user