mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-11 04:04:32 +00:00
fix(ui): strip injected inbound metadata from user messages in history (#22142)
* fix(ui): strip injected inbound metadata from user messages in history Fixes #21106 Fixes #21109 Fixes #22116 OpenClaw prepends structured metadata blocks ("Conversation info", "Sender:", reply-context) to user messages before sending them to the LLM. These blocks are intentionally AI-context-only and must never reach the chat history that users see. Root cause: `buildInboundUserContextPrefix` in `inbound-meta.ts` prepends the blocks directly to the stored user message content string, so they are persisted verbatim and later shown in webchat, TUI, and every other rendering surface. Fix: • `src/auto-reply/reply/strip-inbound-meta.ts` — new utility with a 6-sentinel fast-path strip (zero-alloc on miss) + 9-test suite. • `src/tui/tui-session-actions.ts` — wraps `chatLog.addUser(...)` with `stripInboundMetadata()` so the TUI never stores the prefix. • `ui/src/ui/chat/message-normalizer.ts` — strips user-role text content items during normalisation so webchat renders clean messages. * fix(ui): strip inbound metadata for user messages in display path * test: fix discord component send test spread typing * fix: strip inbound metadata from mac chat history decode * fix: align Swift metadata stripping parser with TS implementation * fix: normalize line endings in inbound metadata stripper * chore: document Swift/TS metadata-sentinel ownership * chore: update changelog for inbound metadata strip fix * changelog: credit Mellowambience for 22142 --------- Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
This commit is contained in:
85
src/auto-reply/reply/strip-inbound-meta.test.ts
Normal file
85
src/auto-reply/reply/strip-inbound-meta.test.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { stripInboundMetadata } from "./strip-inbound-meta.js";
|
||||
|
||||
const CONV_BLOCK = `Conversation info (untrusted metadata):
|
||||
\`\`\`json
|
||||
{
|
||||
"message_id": "msg-abc",
|
||||
"sender": "+1555000"
|
||||
}
|
||||
\`\`\``;
|
||||
|
||||
const SENDER_BLOCK = `Sender (untrusted metadata):
|
||||
\`\`\`json
|
||||
{
|
||||
"label": "Alice",
|
||||
"name": "Alice"
|
||||
}
|
||||
\`\`\``;
|
||||
|
||||
const REPLY_BLOCK = `Replied message (untrusted, for context):
|
||||
\`\`\`json
|
||||
{
|
||||
"body": "What time is it?"
|
||||
}
|
||||
\`\`\``;
|
||||
|
||||
describe("stripInboundMetadata", () => {
|
||||
it("fast-path: returns same string when no sentinels present", () => {
|
||||
const text = "Hello, how are you?";
|
||||
expect(stripInboundMetadata(text)).toBe(text);
|
||||
});
|
||||
|
||||
it("fast-path: returns empty string unchanged", () => {
|
||||
expect(stripInboundMetadata("")).toBe("");
|
||||
});
|
||||
|
||||
it("strips a single Conversation info block", () => {
|
||||
const input = `${CONV_BLOCK}\n\nWhat is the weather today?`;
|
||||
expect(stripInboundMetadata(input)).toBe("What is the weather today?");
|
||||
});
|
||||
|
||||
it("strips multiple chained metadata blocks", () => {
|
||||
const input = `${CONV_BLOCK}\n\n${SENDER_BLOCK}\n\nCan you help me?`;
|
||||
expect(stripInboundMetadata(input)).toBe("Can you help me?");
|
||||
});
|
||||
|
||||
it("strips Replied message block leaving user message intact", () => {
|
||||
const input = `${REPLY_BLOCK}\n\nGot it, thanks!`;
|
||||
expect(stripInboundMetadata(input)).toBe("Got it, thanks!");
|
||||
});
|
||||
|
||||
it("strips all six known sentinel types", () => {
|
||||
const sentinels = [
|
||||
"Conversation info (untrusted metadata):",
|
||||
"Sender (untrusted metadata):",
|
||||
"Thread starter (untrusted, for context):",
|
||||
"Replied message (untrusted, for context):",
|
||||
"Forwarded message context (untrusted metadata):",
|
||||
"Chat history since last reply (untrusted, for context):",
|
||||
];
|
||||
for (const sentinel of sentinels) {
|
||||
const input = `${sentinel}\n\`\`\`json\n{"x": 1}\n\`\`\`\n\nUser message`;
|
||||
expect(stripInboundMetadata(input)).toBe("User message");
|
||||
}
|
||||
});
|
||||
|
||||
it("handles metadata block with no user text after it", () => {
|
||||
expect(stripInboundMetadata(CONV_BLOCK)).toBe("");
|
||||
});
|
||||
|
||||
it("preserves message containing json fences that are not metadata", () => {
|
||||
const text = `Here is my code:\n\`\`\`json\n{"key": "value"}\n\`\`\``;
|
||||
expect(stripInboundMetadata(text)).toBe(text);
|
||||
});
|
||||
|
||||
it("preserves leading newlines in user content after stripping", () => {
|
||||
const input = `${CONV_BLOCK}\n\nActual message`;
|
||||
expect(stripInboundMetadata(input)).toBe("Actual message");
|
||||
});
|
||||
|
||||
it("preserves leading spaces in user content after stripping", () => {
|
||||
const input = `${CONV_BLOCK}\n\n Indented message`;
|
||||
expect(stripInboundMetadata(input)).toBe(" Indented message");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user