Fix structured IPC envelope leakage

Normalize EJClaw structured send_message envelopes, preserve attachments, unwrap stored room payloads, and add regression tests.
This commit is contained in:
Eyejoker
2026-04-28 01:35:26 +09:00
committed by GitHub
parent 20e1a1e5e1
commit 8bb77ffaa3
11 changed files with 291 additions and 14 deletions

View File

@@ -1,3 +1,4 @@
import { normalizeEjclawStructuredOutput } from './agent-protocol.js';
import { isWatchCiTask } from './task-watch-status.js';
import type { PairedTurnAttemptRecord, PairedTurnRecord } from './db.js';
import type { StatusSnapshot, UsageRowSnapshot } from './status-dashboard.js';
@@ -182,6 +183,37 @@ function buildRoomPreview(value: string, maxLength: number): string {
return truncateText(redactSensitiveText(value).trim(), maxLength);
}
function decodeCommonHtmlEntities(value: string): string {
return value
.replace(/"|"|"/gi, '"')
.replace(/'|'|'/gi, "'")
.replace(/&lt;|&#60;|&#x3c;/gi, '<')
.replace(/&gt;|&#62;|&#x3e;/gi, '>')
.replace(/&amp;|&#38;|&#x26;/gi, '&');
}
function hasStructuredOutputHint(value: string): boolean {
return value.includes('"ejclaw"') || /&quot;ejclaw&quot;/i.test(value);
}
function normalizeRoomMessageContent(value: string): string | null {
if (!hasStructuredOutputHint(value)) return value;
const candidates = [value, decodeCommonHtmlEntities(value)];
for (const candidate of candidates) {
const normalized = normalizeEjclawStructuredOutput(candidate);
if (normalized.output?.visibility === 'silent') return null;
if (
normalized.output?.visibility === 'public' &&
normalized.output.text !== candidate
) {
return normalized.output.text;
}
}
return value;
}
function stableHash(value: string): string {
let hash = 0;
for (let index = 0; index < value.length; index += 1) {
@@ -405,15 +437,17 @@ export function sanitizeScheduledTask(
};
}
function sanitizeRoomMessage(message: NewMessage): WebDashboardRoomMessage {
function sanitizeRoomMessage(
message: NewMessage,
): WebDashboardRoomMessage | null {
const content = normalizeRoomMessageContent(message.content ?? '');
if (content === null) return null;
return {
id: message.id,
sender: message.sender,
senderName: message.sender_name || message.sender,
content: buildRoomPreview(
message.content ?? '',
ROOM_MESSAGE_PREVIEW_MAX_LENGTH,
),
content: buildRoomPreview(content, ROOM_MESSAGE_PREVIEW_MAX_LENGTH),
timestamp: message.timestamp,
isFromMe: !!message.is_from_me,
isBotMessage: !!message.is_bot_message,
@@ -568,7 +602,10 @@ export function buildWebDashboardRoomActivity(args: {
pendingTasks: args.entry.pendingTasks,
messages: args.messages
.filter((message) => !isTaskStatusMessage(message))
.map(sanitizeRoomMessage),
.map(sanitizeRoomMessage)
.filter((message): message is WebDashboardRoomMessage =>
Boolean(message),
),
pairedTask: args.pairedTask
? {
id: args.pairedTask.id,