From d91e995e469191eb6c701baf12baf8cc3113a8fc Mon Sep 17 00:00:00 2001 From: Marcus Castro Date: Fri, 13 Feb 2026 14:24:01 -0300 Subject: [PATCH] fix(inbound): preserve literal backslash-n sequences in Windows paths (#11547) * fix(inbound): preserve literal backslash-n sequences in Windows paths The normalizeInboundTextNewlines function was converting literal backslash-n sequences (\n) to actual newlines, corrupting Windows paths like C:\Work\nxxx\README.md when sent through WebUI. This fix removes the .replaceAll("\\n", "\n") operation, preserving literal backslash-n sequences while still normalizing actual CRLF/CR to LF. Fixes #7968 * fix(test): set RawBody to Windows path so BodyForAgent fallback chain tests correctly * fix: tighten Windows path newline regression coverage (#11547) (thanks @mcaxtr) --------- Co-authored-by: Peter Steinberger --- CHANGELOG.md | 1 + src/auto-reply/inbound.test.ts | 25 +++++++++++++--- src/auto-reply/reply/inbound-text.test.ts | 35 +++++++++++++++++++++++ src/auto-reply/reply/inbound-text.ts | 5 +++- 4 files changed, 61 insertions(+), 5 deletions(-) create mode 100644 src/auto-reply/reply/inbound-text.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index cbba8a9472..afe07a0b80 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ Docs: https://docs.openclaw.ai ### Fixes - Agents/Image tool: cap image-analysis completion `maxTokens` by model capability (`min(4096, model.maxTokens)`) to avoid over-limit provider failures while still preventing truncation. (#11770) Thanks @detecti1. +- Inbound/Web UI: preserve literal `\n` sequences when normalizing inbound text so Windows paths like `C:\\Work\\nxxx\\README.md` are not corrupted. (#11547) Thanks @mcaxtr. - Security/Canvas: serve A2UI assets via the shared safe-open path (`openFileWithinRoot`) to close traversal/TOCTOU gaps, with traversal and symlink regression coverage. (#10525) Thanks @abdelsfane. - Security/Gateway: breaking default-behavior change - canvas IP-based auth fallback now only accepts machine-scoped addresses (RFC1918, link-local, ULA IPv6, CGNAT); public-source IP matches now require bearer token auth. (#14661) Thanks @sumleo. - Security/Gateway: sanitize and truncate untrusted WebSocket header values in pre-handshake close logs to reduce log-poisoning risk. Thanks @thewilloftheshadow. diff --git a/src/auto-reply/inbound.test.ts b/src/auto-reply/inbound.test.ts index d91a12ad4e..a56c03457c 100644 --- a/src/auto-reply/inbound.test.ts +++ b/src/auto-reply/inbound.test.ts @@ -61,16 +61,19 @@ describe("normalizeInboundTextNewlines", () => { expect(normalizeInboundTextNewlines("a\rb")).toBe("a\nb"); }); - it("decodes literal \\n to newlines when no real newlines exist", () => { - expect(normalizeInboundTextNewlines("a\\nb")).toBe("a\nb"); + it("preserves literal backslash-n sequences (Windows paths)", () => { + // Windows paths like C:\Work\nxxx should NOT have \n converted to newlines + expect(normalizeInboundTextNewlines("a\\nb")).toBe("a\\nb"); + expect(normalizeInboundTextNewlines("C:\\Work\\nxxx")).toBe("C:\\Work\\nxxx"); }); }); describe("finalizeInboundContext", () => { it("fills BodyForAgent/BodyForCommands and normalizes newlines", () => { const ctx: MsgContext = { - Body: "a\\nb\r\nc", - RawBody: "raw\\nline", + // Use actual CRLF for newline normalization test, not literal \n sequences + Body: "a\r\nb\r\nc", + RawBody: "raw\r\nline", ChatType: "channel", From: "whatsapp:group:123@g.us", GroupSubject: "Test", @@ -87,6 +90,20 @@ describe("finalizeInboundContext", () => { expect(out.ConversationLabel).toContain("Test"); }); + it("preserves literal backslash-n in Windows paths", () => { + const ctx: MsgContext = { + Body: "C:\\Work\\nxxx\\README.md", + RawBody: "C:\\Work\\nxxx\\README.md", + ChatType: "direct", + From: "web:user", + }; + + const out = finalizeInboundContext(ctx); + expect(out.Body).toBe("C:\\Work\\nxxx\\README.md"); + expect(out.BodyForAgent).toBe("C:\\Work\\nxxx\\README.md"); + expect(out.BodyForCommands).toBe("C:\\Work\\nxxx\\README.md"); + }); + it("can force BodyForCommands to follow updated CommandBody", () => { const ctx: MsgContext = { Body: "base", diff --git a/src/auto-reply/reply/inbound-text.test.ts b/src/auto-reply/reply/inbound-text.test.ts new file mode 100644 index 0000000000..2b54a71299 --- /dev/null +++ b/src/auto-reply/reply/inbound-text.test.ts @@ -0,0 +1,35 @@ +import { describe, expect, it } from "vitest"; +import { normalizeInboundTextNewlines } from "./inbound-text.js"; + +describe("normalizeInboundTextNewlines", () => { + it("converts CRLF to LF", () => { + expect(normalizeInboundTextNewlines("hello\r\nworld")).toBe("hello\nworld"); + }); + + it("converts CR to LF", () => { + expect(normalizeInboundTextNewlines("hello\rworld")).toBe("hello\nworld"); + }); + + it("preserves literal backslash-n sequences in Windows paths", () => { + // Windows paths like C:\Work\nxxx should NOT have \n converted to newlines + const windowsPath = "C:\\Work\\nxxx\\README.md"; + expect(normalizeInboundTextNewlines(windowsPath)).toBe("C:\\Work\\nxxx\\README.md"); + }); + + it("preserves backslash-n in messages containing Windows paths", () => { + const message = "Please read the file at C:\\Work\\nxxx\\README.md"; + expect(normalizeInboundTextNewlines(message)).toBe( + "Please read the file at C:\\Work\\nxxx\\README.md", + ); + }); + + it("preserves multiple backslash-n sequences", () => { + const message = "C:\\new\\notes\\nested"; + expect(normalizeInboundTextNewlines(message)).toBe("C:\\new\\notes\\nested"); + }); + + it("still normalizes actual CRLF while preserving backslash-n", () => { + const message = "Line 1\r\nC:\\Work\\nxxx"; + expect(normalizeInboundTextNewlines(message)).toBe("Line 1\nC:\\Work\\nxxx"); + }); +}); diff --git a/src/auto-reply/reply/inbound-text.ts b/src/auto-reply/reply/inbound-text.ts index dd17752b4a..8fdbde117c 100644 --- a/src/auto-reply/reply/inbound-text.ts +++ b/src/auto-reply/reply/inbound-text.ts @@ -1,3 +1,6 @@ export function normalizeInboundTextNewlines(input: string): string { - return input.replaceAll("\r\n", "\n").replaceAll("\r", "\n").replaceAll("\\n", "\n"); + // Normalize actual newline characters (CR+LF and CR to LF). + // Do NOT replace literal backslash-n sequences (\\n) as they may be part of + // Windows paths like C:\Work\nxxx\README.md or user-intended escape sequences. + return input.replaceAll("\r\n", "\n").replaceAll("\r", "\n"); }