diff --git a/CHANGELOG.md b/CHANGELOG.md index b60fc383104..d910007b667 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/src/agents/tools/message-tool.test.ts b/src/agents/tools/message-tool.test.ts index 9c541a769a3..c8945029367 100644 --- a/src/agents/tools/message-tool.test.ts +++ b/src/agents/tools/message-tool.test.ts @@ -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: "internal titleDeploy ready", + blocks: [ + { type: "text", text: "internal noteShip it" }, + { + type: "buttons", + buttons: [ + { + label: "button rationaleApprove", + value: "approve", + }, + ], + }, + { + type: "select", + placeholder: "selection rationalePick a lane", + options: [ + { + label: "option rationaleMain", + 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", () => { diff --git a/src/agents/tools/message-tool.ts b/src/agents/tools/message-tool.ts index c74e6480c98..5d8e72a8671 100644 --- a/src/agents/tools/message-tool.ts +++ b/src/agents/tools/message-tool.ts @@ -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) }; + 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) }; + 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) }; + 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) }; + 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,