feat: strip webchat envelope metadata from user messages (#10)
When OpenClaw's webchat relays user messages, it wraps them in metadata (conversation info JSON block + UTC timestamp prefix). This strips the envelope at render time so users see only their actual text. - Add hasWebchatEnvelope() and stripWebchatEnvelope() to systemEvent.ts - Chain webchat + webhook stripping via stripAll() helper in ChatMessage - Fix 'webhook' label only appearing for actual webhook messages, not for envelope-stripped webchat messages Co-authored-by: togotago <drewmfleury@gmail.com>
This commit is contained in:
@@ -14,7 +14,7 @@ import { copyToClipboard } from '../lib/clipboard';
|
|||||||
import { Bot, User, Wrench, Copy, Check, CheckCheck, RefreshCw, Zap, Info, Webhook, Braces, Clock, AlertCircle, Bookmark, ChevronDown, Reply } from 'lucide-react';
|
import { Bot, User, Wrench, Copy, Check, CheckCheck, RefreshCw, Zap, Info, Webhook, Braces, Clock, AlertCircle, Bookmark, ChevronDown, Reply } from 'lucide-react';
|
||||||
import { t, getLocale } from '../lib/i18n';
|
import { t, getLocale } from '../lib/i18n';
|
||||||
import { useLocale } from '../hooks/useLocale';
|
import { useLocale } from '../hooks/useLocale';
|
||||||
import { stripWebhookScaffolding, hasWebhookScaffolding } from '../lib/systemEvent';
|
import { stripWebhookScaffolding, hasWebhookScaffolding, hasWebchatEnvelope, stripWebchatEnvelope } from '../lib/systemEvent';
|
||||||
// ChevronDown, ChevronRight, Wrench still used by InternalOnlyMessage
|
// ChevronDown, ChevronRight, Wrench still used by InternalOnlyMessage
|
||||||
|
|
||||||
/** Avatar image with fallback to Bot icon on load error */
|
/** Avatar image with fallback to Bot icon on load error */
|
||||||
@@ -454,26 +454,38 @@ export const ChatMessageComponent = memo(function ChatMessageComponent({ message
|
|||||||
const isLight = resolvedTheme === 'light';
|
const isLight = resolvedTheme === 'light';
|
||||||
const [showRawJson, setShowRawJson] = useState(false);
|
const [showRawJson, setShowRawJson] = useState(false);
|
||||||
|
|
||||||
// Strip webhook/hook scaffolding from user messages before rendering
|
// Strip webhook/hook scaffolding and webchat envelope from user messages before rendering
|
||||||
const message = useMemo(() => {
|
const message = useMemo(() => {
|
||||||
if (rawMessage.role !== 'user') return rawMessage;
|
if (rawMessage.role !== 'user') return rawMessage;
|
||||||
const content = rawMessage.content || '';
|
const content = rawMessage.content || '';
|
||||||
const textBlocks = getTextBlocks(rawMessage.blocks);
|
const textBlocks = getTextBlocks(rawMessage.blocks);
|
||||||
const contentHasScaffolding = hasWebhookScaffolding(content);
|
|
||||||
const anyBlockHasScaffolding = textBlocks.some(b =>
|
// Helper: apply all applicable strip functions to a string
|
||||||
hasWebhookScaffolding((b as Extract<MessageBlock, { type: 'text' }>).text)
|
const stripAll = (text: string): string => {
|
||||||
);
|
let result = text;
|
||||||
if (!contentHasScaffolding && !anyBlockHasScaffolding) return rawMessage;
|
if (hasWebhookScaffolding(result)) result = stripWebhookScaffolding(result);
|
||||||
|
if (hasWebchatEnvelope(result)) result = stripWebchatEnvelope(result);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
const contentNeedsStrip = hasWebhookScaffolding(content) || hasWebchatEnvelope(content);
|
||||||
|
const anyBlockNeedsStrip = textBlocks.some(b => {
|
||||||
|
const t = (b as Extract<MessageBlock, { type: 'text' }>).text;
|
||||||
|
return hasWebhookScaffolding(t) || hasWebchatEnvelope(t);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!contentNeedsStrip && !anyBlockNeedsStrip) return rawMessage;
|
||||||
|
|
||||||
// Clean the content and blocks
|
// Clean the content and blocks
|
||||||
const cleaned: ChatMessageType = { ...rawMessage };
|
const cleaned: ChatMessageType = { ...rawMessage };
|
||||||
if (cleaned.content) {
|
if (cleaned.content) {
|
||||||
cleaned.content = stripWebhookScaffolding(cleaned.content);
|
cleaned.content = stripAll(cleaned.content);
|
||||||
}
|
}
|
||||||
if (cleaned.blocks.length > 0) {
|
if (cleaned.blocks.length > 0) {
|
||||||
cleaned.blocks = cleaned.blocks.map(b => {
|
cleaned.blocks = cleaned.blocks.map(b => {
|
||||||
if (b.type === 'text') {
|
if (b.type === 'text') {
|
||||||
const tb = b as Extract<MessageBlock, { type: 'text' }>;
|
const tb = b as Extract<MessageBlock, { type: 'text' }>;
|
||||||
return { ...tb, text: stripWebhookScaffolding(tb.text) };
|
return { ...tb, text: stripAll(tb.text) };
|
||||||
}
|
}
|
||||||
return b;
|
return b;
|
||||||
});
|
});
|
||||||
@@ -481,7 +493,7 @@ export const ChatMessageComponent = memo(function ChatMessageComponent({ message
|
|||||||
return cleaned;
|
return cleaned;
|
||||||
}, [rawMessage]);
|
}, [rawMessage]);
|
||||||
|
|
||||||
const wasWebhookMessage = rawMessage !== message;
|
const wasWebhookMessage = rawMessage !== message && hasWebhookScaffolding(rawMessage.content || '');
|
||||||
|
|
||||||
const isUser = message.role === 'user';
|
const isUser = message.role === 'user';
|
||||||
|
|
||||||
|
|||||||
@@ -93,3 +93,45 @@ export function hasWebhookScaffolding(text: string): boolean {
|
|||||||
return /<<<EXTERNAL_UNTRUSTED_CONTENT>>>/.test(text) ||
|
return /<<<EXTERNAL_UNTRUSTED_CONTENT>>>/.test(text) ||
|
||||||
/---\s*SECURITY NOTICE\s*---/i.test(text);
|
/---\s*SECURITY NOTICE\s*---/i.test(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detect OpenClaw webchat envelope metadata in a user message.
|
||||||
|
*
|
||||||
|
* Pattern:
|
||||||
|
* Conversation info (untrusted metadata):
|
||||||
|
* ```json
|
||||||
|
* { ... }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* [Wed 2026-02-18 14:06 UTC] actual message
|
||||||
|
*/
|
||||||
|
export function hasWebchatEnvelope(text: string): boolean {
|
||||||
|
return /Conversation info \(untrusted metadata\):/.test(text) ||
|
||||||
|
/^\[(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun) \d{4}-\d{2}-\d{2} \d{2}:\d{2} UTC\] /.test(text.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Strip webchat envelope metadata from a user message, returning only the
|
||||||
|
* actual user text.
|
||||||
|
*
|
||||||
|
* Removes:
|
||||||
|
* - "Conversation info (untrusted metadata):" header + trailing JSON code fence
|
||||||
|
* - Timestamp prefix "[Wed 2026-02-18 14:06 UTC] "
|
||||||
|
*/
|
||||||
|
export function stripWebchatEnvelope(text: string): string {
|
||||||
|
let cleaned = text;
|
||||||
|
|
||||||
|
// Remove the "Conversation info (untrusted metadata):" block + JSON code fence
|
||||||
|
cleaned = cleaned.replace(
|
||||||
|
/Conversation info \(untrusted metadata\):\s*```json\s*[\s\S]*?```\s*/,
|
||||||
|
''
|
||||||
|
);
|
||||||
|
|
||||||
|
// Strip timestamp prefix: [Wed 2026-02-18 14:06 UTC]
|
||||||
|
cleaned = cleaned.replace(
|
||||||
|
/^\[(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun) \d{4}-\d{2}-\d{2} \d{2}:\d{2} UTC\] /,
|
||||||
|
''
|
||||||
|
);
|
||||||
|
|
||||||
|
return cleaned.trim() || text.trim();
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user