fix(agents): sanitize presentation reasoning

This commit is contained in:
Vincent Koc
2026-05-03 23:17:21 -07:00
parent bbdf1fe11c
commit 92d33e4de8
3 changed files with 102 additions and 0 deletions

View File

@@ -58,6 +58,7 @@ Docs: https://docs.openclaw.ai
- Telegram/streaming: sanitize tool-progress draft preview backticks before shared compaction, so long backtick-heavy progress text still renders inside the safe code-formatted preview instead of collapsing to an ellipsis.
- Agents/Pi: suppress persistence for synthetic mid-turn overflow continuation prompts, so transcript-retry recovery does not write the "continue from transcript" prompt as a new user turn. Thanks @vincentkoc.
- Agents/tools: strip reasoning text from visible rich presentation titles, blocks, buttons, and select labels before message-tool sends, so structured channel payloads cannot leak hidden planning. Thanks @vincentkoc.
- Telegram: keep reply-dispatch lazy provider runtime chunks behind stable dist names and delete `/reasoning stream` previews after final delivery so package updates and live reasoning drafts do not leave Telegram turns broken or noisy. Thanks @BunsDev.
- Exec approvals: detect `env -S` split-string command-carrier risks when `-S`/`-s` is combined with other env short options, so approval explanations do not miss split payloads hidden behind `env -iS...`. Thanks @vincentkoc.
- Google Meet: log the concrete agent-mode TTS provider, model, voice, output format, and sample rate after speech synthesis, so Meet logs show which voice backend spoke each reply.

View File

@@ -1135,6 +1135,57 @@ describe("message tool reasoning tag sanitization", () => {
expect(call?.params?.[field]).toBe(expected);
},
);
it("sanitizes visible presentation text before sending", async () => {
mockSendResult({ channel: "slack", to: "slack:C123" });
const call = await executeSend({
action: {
target: "slack:C123",
presentation: {
title: "<think>internal title</think>Deploy ready",
blocks: [
{ type: "text", text: "<think>internal note</think>Ship it" },
{
type: "buttons",
buttons: [
{
label: "<think>button rationale</think>Approve",
value: "approve",
},
],
},
{
type: "select",
placeholder: "<think>selection rationale</think>Pick a lane",
options: [
{
label: "<think>option rationale</think>Main",
value: "main",
},
],
},
],
},
},
});
expect(call?.params?.presentation).toEqual({
title: "Deploy ready",
blocks: [
{ type: "text", text: "Ship it" },
{
type: "buttons",
buttons: [{ label: "Approve", value: "approve" }],
},
{
type: "select",
placeholder: "Pick a lane",
options: [{ label: "Main", value: "main" }],
},
],
});
});
});
describe("message tool sandbox passthrough", () => {

View File

@@ -65,6 +65,55 @@ function stripFormattedReasoningMessage(text: string): string {
return lines.slice(index).join("\n").trim();
}
function sanitizePresentationTextFields(value: unknown): unknown {
if (!value || typeof value !== "object" || Array.isArray(value)) {
return value;
}
const presentation = { ...(value as Record<string, unknown>) };
if (typeof presentation.title === "string") {
presentation.title = stripFormattedReasoningMessage(presentation.title);
}
if (Array.isArray(presentation.blocks)) {
presentation.blocks = presentation.blocks.map((block) => {
if (!block || typeof block !== "object" || Array.isArray(block)) {
return block;
}
const sanitizedBlock = { ...(block as Record<string, unknown>) };
for (const field of ["text", "placeholder"]) {
if (typeof sanitizedBlock[field] === "string") {
sanitizedBlock[field] = stripFormattedReasoningMessage(sanitizedBlock[field]);
}
}
if (Array.isArray(sanitizedBlock.buttons)) {
sanitizedBlock.buttons = sanitizedBlock.buttons.map((button) => {
if (!button || typeof button !== "object" || Array.isArray(button)) {
return button;
}
const sanitizedButton = { ...(button as Record<string, unknown>) };
if (typeof sanitizedButton.label === "string") {
sanitizedButton.label = stripFormattedReasoningMessage(sanitizedButton.label);
}
return sanitizedButton;
});
}
if (Array.isArray(sanitizedBlock.options)) {
sanitizedBlock.options = sanitizedBlock.options.map((option) => {
if (!option || typeof option !== "object" || Array.isArray(option)) {
return option;
}
const sanitizedOption = { ...(option as Record<string, unknown>) };
if (typeof sanitizedOption.label === "string") {
sanitizedOption.label = stripFormattedReasoningMessage(sanitizedOption.label);
}
return sanitizedOption;
});
}
return sanitizedBlock;
});
}
return presentation;
}
function buildRoutingSchema() {
return {
channel: Type.Optional(Type.String()),
@@ -715,6 +764,7 @@ export function createMessageTool(options?: MessageToolOptions): AnyAgentTool {
params[field] = stripFormattedReasoningMessage(params[field]);
}
}
params.presentation = sanitizePresentationTextFields(params.presentation);
const action = readStringParam(params, "action", {
required: true,