From bed8e7abe6e43cd7cf9bd29310fed257cd5ab442 Mon Sep 17 00:00:00 2001 From: Vignesh Natarajan Date: Sun, 15 Feb 2026 19:00:18 -0800 Subject: [PATCH] fix (auto-reply): expose inbound message identifiers in trusted metadata --- src/auto-reply/reply/inbound-meta.test.ts | 71 +++++++++++++++++++++++ src/auto-reply/reply/inbound-meta.ts | 8 +++ 2 files changed, 79 insertions(+) create mode 100644 src/auto-reply/reply/inbound-meta.test.ts diff --git a/src/auto-reply/reply/inbound-meta.test.ts b/src/auto-reply/reply/inbound-meta.test.ts new file mode 100644 index 0000000000..a1daa577d0 --- /dev/null +++ b/src/auto-reply/reply/inbound-meta.test.ts @@ -0,0 +1,71 @@ +import { describe, expect, it } from "vitest"; +import type { TemplateContext } from "../templating.js"; +import { buildInboundMetaSystemPrompt, buildInboundUserContextPrefix } from "./inbound-meta.js"; + +function parseInboundMetaPayload(text: string): Record { + const match = text.match(/```json\n([\s\S]*?)\n```/); + if (!match?.[1]) { + throw new Error("missing inbound meta json block"); + } + return JSON.parse(match[1]) as Record; +} + +describe("buildInboundMetaSystemPrompt", () => { + it("includes trusted message and routing ids for tool actions", () => { + const prompt = buildInboundMetaSystemPrompt({ + MessageSid: "123", + MessageSidFull: "123", + ReplyToId: "99", + OriginatingTo: "telegram:5494292670", + OriginatingChannel: "telegram", + Provider: "telegram", + Surface: "telegram", + ChatType: "direct", + } as TemplateContext); + + const payload = parseInboundMetaPayload(prompt); + expect(payload["schema"]).toBe("openclaw.inbound_meta.v1"); + expect(payload["message_id"]).toBe("123"); + expect(payload["message_id_full"]).toBeUndefined(); + expect(payload["reply_to_id"]).toBe("99"); + expect(payload["chat_id"]).toBe("telegram:5494292670"); + expect(payload["channel"]).toBe("telegram"); + }); + + it("keeps message_id_full only when it differs from message_id", () => { + const prompt = buildInboundMetaSystemPrompt({ + MessageSid: "short-id", + MessageSidFull: "full-provider-message-id", + OriginatingTo: "channel:C1", + OriginatingChannel: "slack", + Provider: "slack", + Surface: "slack", + ChatType: "group", + } as TemplateContext); + + const payload = parseInboundMetaPayload(prompt); + expect(payload["message_id"]).toBe("short-id"); + expect(payload["message_id_full"]).toBe("full-provider-message-id"); + }); +}); + +describe("buildInboundUserContextPrefix", () => { + it("omits conversation label block for direct chats", () => { + const text = buildInboundUserContextPrefix({ + ChatType: "direct", + ConversationLabel: "openclaw-tui", + } as TemplateContext); + + expect(text).toBe(""); + }); + + it("keeps conversation label for group chats", () => { + const text = buildInboundUserContextPrefix({ + ChatType: "group", + ConversationLabel: "ops-room", + } as TemplateContext); + + expect(text).toContain("Conversation info (untrusted metadata):"); + expect(text).toContain('"conversation_label": "ops-room"'); + }); +}); diff --git a/src/auto-reply/reply/inbound-meta.ts b/src/auto-reply/reply/inbound-meta.ts index 8367681023..03c06b7e2b 100644 --- a/src/auto-reply/reply/inbound-meta.ts +++ b/src/auto-reply/reply/inbound-meta.ts @@ -13,11 +13,19 @@ function safeTrim(value: unknown): string | undefined { export function buildInboundMetaSystemPrompt(ctx: TemplateContext): string { const chatType = normalizeChatType(ctx.ChatType); const isDirect = !chatType || chatType === "direct"; + const messageId = safeTrim(ctx.MessageSid); + const messageIdFull = safeTrim(ctx.MessageSidFull); + const replyToId = safeTrim(ctx.ReplyToId); + const chatId = safeTrim(ctx.OriginatingTo); // Keep system metadata strictly free of attacker-controlled strings (sender names, group subjects, etc.). // Those belong in the user-role "untrusted context" blocks. const payload = { schema: "openclaw.inbound_meta.v1", + message_id: messageId, + message_id_full: messageIdFull && messageIdFull !== messageId ? messageIdFull : undefined, + chat_id: chatId, + reply_to_id: replyToId, channel: safeTrim(ctx.OriginatingChannel) ?? safeTrim(ctx.Surface) ?? safeTrim(ctx.Provider), provider: safeTrim(ctx.Provider), surface: safeTrim(ctx.Surface),