Skip to content

Commit 611909f

Browse files
Guard automatic Telegram reply delivery
1 parent af5f4de commit 611909f

2 files changed

Lines changed: 90 additions & 3 deletions

File tree

extensions/telegram/src/bot/delivery.replies.ts

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ import {
3737
wrapFileReferencesInHtml,
3838
} from "../format.js";
3939
import { resolveTelegramInteractiveTextFallback } from "../interactive-fallback.js";
40+
import {
41+
shouldSendTelegramRuntimeCommsMedia,
42+
shouldSendTelegramRuntimeCommsText,
43+
} from "../runtime-comms-guard.js";
4044
import { buildInlineKeyboard } from "../send.js";
4145
import { resolveTelegramVoiceSend } from "../voice.js";
4246
import {
@@ -749,13 +753,12 @@ export async function deliverReplies(params: {
749753
}
750754
for (const originalReply of normalizedReplies) {
751755
let reply = originalReply;
752-
const mediaList = reply?.mediaUrls?.length
756+
let mediaList = reply?.mediaUrls?.length
753757
? reply.mediaUrls
754758
: reply?.mediaUrl
755759
? [reply.mediaUrl]
756760
: [];
757-
const hasMedia = mediaList.length > 0;
758-
const resolvedReplyText =
761+
let resolvedReplyText =
759762
resolveTelegramInteractiveTextFallback({
760763
text: reply?.text,
761764
interactive: reply?.interactive,
@@ -765,6 +768,35 @@ export async function deliverReplies(params: {
765768
if (reply && resolvedReplyText !== (reply.text ?? "")) {
766769
reply = { ...reply, text: resolvedReplyText };
767770
}
771+
772+
const guardContext = {
773+
to: params.chatId,
774+
accountId: params.accountId,
775+
threadId: params.thread?.id,
776+
};
777+
if (
778+
resolvedReplyText &&
779+
!(await shouldSendTelegramRuntimeCommsText({
780+
context: guardContext,
781+
text: resolvedReplyText,
782+
}))
783+
) {
784+
resolvedReplyText = "";
785+
reply = { ...reply, text: "" };
786+
}
787+
if (mediaList.length > 0) {
788+
const allowedMediaList: string[] = [];
789+
for (const mediaUrl of mediaList) {
790+
if (await shouldSendTelegramRuntimeCommsMedia({ context: guardContext, mediaUrl })) {
791+
allowedMediaList.push(mediaUrl);
792+
}
793+
}
794+
mediaList = allowedMediaList;
795+
if (mediaList.length === 0 && reply) {
796+
reply = { ...reply, mediaUrl: undefined, mediaUrls: undefined };
797+
}
798+
}
799+
const hasMedia = mediaList.length > 0;
768800
if (!resolvedReplyText && !hasMedia) {
769801
if (reply?.audioAsVoice) {
770802
logVerbose("telegram reply has audioAsVoice without media/text; skipping");

extensions/telegram/src/bot/delivery.test.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ vi.mock("openclaw/plugin-sdk/plugin-runtime", async (importOriginal) => {
5757

5858
vi.resetModules();
5959
const { deliverReplies } = await import("./delivery.js");
60+
const { clearTelegramRuntimeCommsGuardMemoryForTests } = await import("../runtime-comms-guard.js");
6061

6162
vi.mock("grammy", () => ({
6263
API_CONSTANTS: {
@@ -175,6 +176,7 @@ function createVoiceFailureHarness(params: {
175176

176177
describe("deliverReplies", () => {
177178
beforeEach(() => {
179+
clearTelegramRuntimeCommsGuardMemoryForTests();
178180
loadWebMedia.mockClear();
179181
probeVideoDimensions.mockReset();
180182
probeVideoDimensions.mockResolvedValue(undefined);
@@ -185,6 +187,59 @@ describe("deliverReplies", () => {
185187
messageHookRunner.runMessageSent.mockReset();
186188
});
187189

190+
it("suppresses visible runtime guard text on automatic Telegram replies", async () => {
191+
const runtime = createRuntime();
192+
const sendMessage = vi.fn().mockResolvedValue({ message_id: 4101, chat: { id: "123" } });
193+
const bot = createBot({ sendMessage });
194+
195+
await deliverWith({
196+
bot,
197+
runtime,
198+
replies: [
199+
{
200+
text: "MODEL-GATE\ntask_type: small code/report edit\nproceed_status: PROCEED",
201+
},
202+
],
203+
});
204+
205+
expect(sendMessage).not.toHaveBeenCalled();
206+
});
207+
208+
it("delivers requested markdown files immediately without leaking guard text", async () => {
209+
const runtime = createRuntime();
210+
const sendDocument = vi.fn().mockResolvedValue({ message_id: 4102, chat: { id: "123" } });
211+
const sendMessage = vi.fn().mockResolvedValue({ message_id: 4103, chat: { id: "123" } });
212+
const bot = createBot({ sendDocument, sendMessage });
213+
mockMediaLoad("telegram-response-guide.md", "text/markdown", "# Telegram guide\n");
214+
215+
await deliverWith({
216+
bot,
217+
runtime,
218+
replies: [
219+
{
220+
text: "MODEL-GATE\ntask_type: small code/report edit\nproceed_status: PROCEED",
221+
mediaUrls: ["/tmp/telegram-response-guide.md"],
222+
},
223+
{
224+
text: "Готово: файл доставлен.",
225+
},
226+
],
227+
});
228+
229+
expect(sendDocument).toHaveBeenCalledTimes(1);
230+
expect(sendDocument).toHaveBeenCalledWith(
231+
"123",
232+
expect.anything(),
233+
expect.not.objectContaining({ caption: expect.stringContaining("MODEL-GATE") }),
234+
);
235+
expect(sendMessage).toHaveBeenCalledTimes(1);
236+
expect(sendMessage).toHaveBeenCalledWith(
237+
"123",
238+
expect.stringContaining("Готово: файл доставлен."),
239+
expect.anything(),
240+
);
241+
});
242+
188243
it("skips audioAsVoice-only payloads without logging an error", async () => {
189244
const runtime = createRuntime(false);
190245

0 commit comments

Comments
 (0)