diff --git a/CHANGELOG.md b/CHANGELOG.md index a37fdcf4829..319a51b80d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -126,6 +126,7 @@ Docs: https://docs.openclaw.ai - CLI/completion: guard the shell-profile source line written by `openclaw completion --install` with a file existence check (`[ -f ... ] && source ...` for bash/zsh, `test -f ...; and source ...` for fish) so uninstalling OpenClaw no longer makes new login shells error on a missing completion cache. (#78659) Thanks @sjf. - Cron/doctor: repair persisted cron jobs whose `payload.model` was stored as `"default"`, `"null"`, blank, or JSON `null` by removing the bad override during `openclaw doctor --fix` while keeping cron runtime model validation strict. Fixes #78549. Thanks @bizzle12368239. +- Telegram: honor `accessGroup:*` sender allowlists for DMs, groups, native commands, and callback authorization before applying Telegram's numeric sender-ID checks. Fixes #78660. Thanks @manugc. - Doctor/OpenAI Codex: revert the 2026.5.5 `doctor --fix` repair that rewrote valid `openai-codex/*` ChatGPT/Codex OAuth routes to `openai/*`, which could break OAuth-only GPT-5.5 setups or accidentally move users onto the OpenAI API-key route. If 2026.5.5 already changed your default model, run `openclaw models set openai-codex/gpt-5.5 && openclaw config validate` to switch the default agent back to the Codex OAuth PI route. Fixes #78407. - Discord/groups: instruct group-chat agents to stay silent when a message is addressed to someone else, replying only when invited or correcting key facts. (#78615) - Discord/groups: tell Discord-channel agents to wrap bare URLs as `` so link previews do not expand into uninvited embeds. (#78614) diff --git a/extensions/telegram/src/bot-access.ts b/extensions/telegram/src/bot-access.ts index c00236f054b..724eac1a71f 100644 --- a/extensions/telegram/src/bot-access.ts +++ b/extensions/telegram/src/bot-access.ts @@ -4,6 +4,11 @@ import { mergeDmAllowFromSources, type AllowlistMatch, } from "openclaw/plugin-sdk/allow-from"; +import { + parseAccessGroupAllowFromEntry, + resolveAccessGroupAllowFromMatches, +} from "openclaw/plugin-sdk/command-auth"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types"; import { createSubsystemLogger } from "openclaw/plugin-sdk/runtime-env"; import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime"; @@ -76,6 +81,39 @@ export const isSenderAllowed = (params: { return isSenderIdAllowed(allow, senderId, true); }; +export async function expandTelegramAllowFromWithAccessGroups(params: { + cfg?: OpenClawConfig; + allowFrom?: Array; + accountId?: string; + senderId?: string; +}): Promise { + const allowFrom = (params.allowFrom ?? []).map(String); + if (!params.senderId) { + return allowFrom; + } + const matched = await resolveAccessGroupAllowFromMatches({ + cfg: params.cfg, + allowFrom, + channel: "telegram", + accountId: params.accountId ?? "default", + senderId: params.senderId, + isSenderAllowed: (senderId, entries) => + isSenderAllowed({ + allow: normalizeAllowFrom(entries), + senderId, + }), + }); + if (matched.length === 0) { + return allowFrom; + } + const matchedGroups = new Set(matched); + const expanded = allowFrom.filter((entry) => { + const groupName = parseAccessGroupAllowFromEntry(entry); + return groupName == null || !matchedGroups.has(`accessGroup:${groupName}`); + }); + return Array.from(new Set([...expanded, params.senderId])); +} + export { firstDefined }; export const resolveSenderAllowMatch = (params: { diff --git a/extensions/telegram/src/bot-handlers.runtime.ts b/extensions/telegram/src/bot-handlers.runtime.ts index 84d7a61c45d..6f9df50d1fe 100644 --- a/extensions/telegram/src/bot-handlers.runtime.ts +++ b/extensions/telegram/src/bot-handlers.runtime.ts @@ -37,6 +37,7 @@ import { import { resolveTelegramAccount, resolveTelegramMediaRuntimeOptions } from "./accounts.js"; import { withTelegramApiErrorLogging } from "./api-logging.js"; import { + expandTelegramAllowFromWithAccessGroups, isSenderAllowed, normalizeDmAllowFromWithStore, type NormalizedAllowFrom, @@ -709,14 +710,17 @@ export const registerTelegramHandlers = ({ chatId: number; isGroup: boolean; isForum: boolean; + senderId?: string; messageThreadId?: number; groupAllowContext?: TelegramGroupAllowContext; }): Promise => { const groupAllowContext = params.groupAllowContext ?? (await resolveTelegramGroupAllowFromContext({ + cfg, chatId: params.chatId, accountId, + senderId: params.senderId, isGroup: params.isGroup, isForum: params.isForum, messageThreadId: params.messageThreadId, @@ -917,6 +921,7 @@ export const registerTelegramHandlers = ({ chatId, isGroup, isForum, + senderId, }); const senderAuthorization = authorizeTelegramEventSender({ chatId, @@ -1346,10 +1351,13 @@ export const registerTelegramHandlers = ({ isForum: callbackMessage.chat.is_forum, getChat, }); + const senderId = callback.from?.id ? String(callback.from.id) : ""; + const senderUsername = callback.from?.username ?? ""; const eventAuthContext = await resolveTelegramEventAuthorizationContext({ chatId, isGroup, isForum, + senderId, messageThreadId, }); const { resolvedThreadId, dmThreadId, storeAllowFrom, groupConfig } = eventAuthContext; @@ -1360,8 +1368,6 @@ export const registerTelegramHandlers = ({ ); return; } - const senderId = callback.from?.id ? String(callback.from.id) : ""; - const senderUsername = callback.from?.username ?? ""; const authorizationMode: TelegramEventAuthorizationMode = !isGroup || (!execApprovalButtonsEnabled && inlineButtonsScope === "allowlist") ? "callback-allowlist" @@ -1888,6 +1894,7 @@ export const registerTelegramHandlers = ({ chatId: event.chatId, isGroup: event.isGroup, isForum: event.isForum, + senderId: event.senderId, messageThreadId: event.messageThreadId, }); const { @@ -1903,8 +1910,14 @@ export const registerTelegramHandlers = ({ } = eventAuthContext; // For DMs, prefer per-DM/topic allowFrom (groupAllowOverride) over account-level allowFrom const dmAllowFrom = groupAllowOverride ?? allowFrom; - const effectiveDmAllow = normalizeDmAllowFromWithStore({ + const expandedDmAllowFrom = await expandTelegramAllowFromWithAccessGroups({ + cfg, allowFrom: dmAllowFrom, + accountId, + senderId: event.senderId, + }); + const effectiveDmAllow = normalizeDmAllowFromWithStore({ + allowFrom: expandedDmAllowFrom, storeAllowFrom, dmPolicy, }); diff --git a/extensions/telegram/src/bot-message-context.ts b/extensions/telegram/src/bot-message-context.ts index e969c668161..19077725996 100644 --- a/extensions/telegram/src/bot-message-context.ts +++ b/extensions/telegram/src/bot-message-context.ts @@ -10,7 +10,12 @@ import { normalizeAccountId, resolveThreadSessionKeys } from "openclaw/plugin-sd import { logVerbose } from "openclaw/plugin-sdk/runtime-env"; import { mergeTelegramAccountConfig, resolveDefaultTelegramAccountId } from "./accounts.js"; import { withTelegramApiErrorLogging } from "./api-logging.js"; -import { firstDefined, normalizeAllowFrom, normalizeDmAllowFromWithStore } from "./bot-access.js"; +import { + expandTelegramAllowFromWithAccessGroups, + firstDefined, + normalizeAllowFrom, + normalizeDmAllowFromWithStore, +} from "./bot-access.js"; import { resolveTelegramInboundBody } from "./bot-message-context.body.js"; import { buildTelegramInboundContextPayload, @@ -258,13 +263,25 @@ export const buildTelegramMessageContext = async ({ const groupAllowOverride = firstDefined(topicConfig?.allowFrom, groupConfig?.allowFrom); // For DMs, prefer per-DM/topic allowFrom (groupAllowOverride) over account-level allowFrom const dmAllowFrom = groupAllowOverride ?? allowFrom; - const effectiveDmAllow = normalizeDmAllowFromWithStore({ + const expandedDmAllowFrom = await expandTelegramAllowFromWithAccessGroups({ + cfg: freshCfg, allowFrom: dmAllowFrom, + accountId: account.accountId, + senderId, + }); + const effectiveDmAllow = normalizeDmAllowFromWithStore({ + allowFrom: expandedDmAllowFrom, storeAllowFrom, dmPolicy: effectiveDmPolicy, }); // Group sender checks are explicit and must not inherit DM pairing-store entries. - const effectiveGroupAllow = normalizeAllowFrom(groupAllowOverride ?? groupAllowFrom); + const expandedGroupAllowFrom = await expandTelegramAllowFromWithAccessGroups({ + cfg: freshCfg, + allowFrom: groupAllowOverride ?? groupAllowFrom, + accountId: account.accountId, + senderId, + }); + const effectiveGroupAllow = normalizeAllowFrom(expandedGroupAllowFrom); const hasGroupAllowOverride = groupAllowOverride !== undefined; const senderUsername = msg.from?.username ?? ""; const baseAccess = evaluateTelegramGroupBaseAccess({ diff --git a/extensions/telegram/src/bot-native-commands.ts b/extensions/telegram/src/bot-native-commands.ts index 00622c15603..e25c945e39d 100644 --- a/extensions/telegram/src/bot-native-commands.ts +++ b/extensions/telegram/src/bot-native-commands.ts @@ -53,7 +53,11 @@ import { } from "openclaw/plugin-sdk/text-runtime"; import { resolveTelegramAccount } from "./accounts.js"; import { withTelegramApiErrorLogging } from "./api-logging.js"; -import { isSenderAllowed, normalizeDmAllowFromWithStore } from "./bot-access.js"; +import { + expandTelegramAllowFromWithAccessGroups, + isSenderAllowed, + normalizeDmAllowFromWithStore, +} from "./bot-access.js"; import type { TelegramBotDeps } from "./bot-deps.js"; import type { TelegramMediaRef } from "./bot-message-context.js"; import type { TelegramMessageContextOptions } from "./bot-message-context.types.js"; @@ -489,9 +493,13 @@ async function resolveTelegramCommandAuth(params: { } = params; const { chatId, isGroup, isForum, messageThreadId, threadParams } = await resolveTelegramNativeCommandThreadContext({ msg, bot }); + const senderId = msg.from?.id ? String(msg.from.id) : ""; + const senderUsername = msg.from?.username ?? ""; const groupAllowContext = await resolveTelegramGroupAllowFromContext({ + cfg, chatId, accountId, + senderId, isGroup, isForum, messageThreadId, @@ -522,8 +530,12 @@ async function resolveTelegramCommandAuth(params: { } // For DMs, prefer per-DM/topic allowFrom (groupAllowOverride) over account-level allowFrom const dmAllowFrom = groupAllowOverride ?? allowFrom; - const senderId = msg.from?.id ? String(msg.from.id) : ""; - const senderUsername = msg.from?.username ?? ""; + const expandedDmAllowFrom = await expandTelegramAllowFromWithAccessGroups({ + cfg, + allowFrom: dmAllowFrom, + accountId, + senderId, + }); const commandsAllowFrom = cfg.commands?.allowFrom; const commandsAllowFromConfigured = commandsAllowFrom != null && @@ -627,7 +639,7 @@ async function resolveTelegramCommandAuth(params: { } const dmAllow = normalizeDmAllowFromWithStore({ - allowFrom: dmAllowFrom, + allowFrom: expandedDmAllowFrom, storeAllowFrom: isGroup ? [] : storeAllowFrom, dmPolicy: effectiveDmPolicy, }); diff --git a/extensions/telegram/src/bot.create-telegram-bot.test.ts b/extensions/telegram/src/bot.create-telegram-bot.test.ts index 0268303af27..b1bdba57984 100644 --- a/extensions/telegram/src/bot.create-telegram-bot.test.ts +++ b/extensions/telegram/src/bot.create-telegram-bot.test.ts @@ -1580,6 +1580,60 @@ describe("createTelegramBot", () => { }, expectedReplyCount: 1, }, + { + name: "allows group messages from senders in accessGroup allowFrom when groupPolicy is 'allowlist'", + config: { + accessGroups: { + owners: { + type: "message.senders", + members: { + telegram: ["123456789"], + }, + }, + }, + channels: { + telegram: { + groupPolicy: "allowlist", + allowFrom: ["accessGroup:owners"], + groups: { "*": { requireMention: false } }, + }, + }, + }, + message: { + chat: { id: -100123456789, type: "group", title: "Test Group" }, + from: { id: 123456789, username: "testuser" }, + text: "hello", + date: 1736380800, + }, + expectedReplyCount: 1, + }, + { + name: "blocks group messages from senders outside accessGroup allowFrom when groupPolicy is 'allowlist'", + config: { + accessGroups: { + owners: { + type: "message.senders", + members: { + telegram: ["123456789"], + }, + }, + }, + channels: { + telegram: { + groupPolicy: "allowlist", + allowFrom: ["accessGroup:owners"], + groups: { "*": { requireMention: false } }, + }, + }, + }, + message: { + chat: { id: -100123456789, type: "group", title: "Test Group" }, + from: { id: 999999, username: "notallowed" }, + text: "hello", + date: 1736380800, + }, + expectedReplyCount: 0, + }, { name: "blocks group messages when allowFrom is configured with @username entries (numeric IDs required)", config: { @@ -2649,6 +2703,31 @@ describe("createTelegramBot", () => { }, expectedReplyCount: 1, }, + { + name: "allows direct messages with accessGroup allowFrom entries", + config: { + accessGroups: { + owners: { + type: "message.senders", + members: { + telegram: ["123456789"], + }, + }, + }, + channels: { + telegram: { + allowFrom: ["accessGroup:owners"], + }, + }, + }, + message: { + chat: { id: 777777777, type: "private" }, + from: { id: 123456789, username: "testuser" }, + text: "hello", + date: 1736380800, + }, + expectedReplyCount: 1, + }, { name: "falls back to direct message chat id when sender user id is missing", config: { diff --git a/extensions/telegram/src/bot/helpers.ts b/extensions/telegram/src/bot/helpers.ts index f4440e4d0a3..31e62994786 100644 --- a/extensions/telegram/src/bot/helpers.ts +++ b/extensions/telegram/src/bot/helpers.ts @@ -1,6 +1,7 @@ import type { Chat, Message } from "@grammyjs/types"; import { formatLocationText } from "openclaw/plugin-sdk/channel-inbound"; import type { + OpenClawConfig, TelegramAccountConfig, TelegramDirectConfig, TelegramGroupConfig, @@ -10,7 +11,12 @@ import type { import { readChannelAllowFromStore } from "openclaw/plugin-sdk/conversation-runtime"; import { normalizeAccountId } from "openclaw/plugin-sdk/routing"; import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime"; -import { firstDefined, normalizeAllowFrom, type NormalizedAllowFrom } from "../bot-access.js"; +import { + expandTelegramAllowFromWithAccessGroups, + firstDefined, + normalizeAllowFrom, + type NormalizedAllowFrom, +} from "../bot-access.js"; import { normalizeTelegramReplyToMessageId } from "../outbound-params.js"; import { resolveTelegramPreviewStreamMode } from "../preview-streaming.js"; import { @@ -168,8 +174,10 @@ export function withResolvedTelegramForumFlag( } export async function resolveTelegramGroupAllowFromContext(params: { + cfg?: OpenClawConfig; chatId: string | number; accountId?: string; + senderId?: string; isGroup?: boolean; isForum?: boolean; messageThreadId?: number | null; @@ -214,7 +222,13 @@ export async function resolveTelegramGroupAllowFromContext(params: { const groupAllowOverride = firstDefined(topicConfig?.allowFrom, groupConfig?.allowFrom); // Group sender access must remain explicit (groupAllowFrom/per-group allowFrom only). // DM pairing store entries are not a group authorization source. - const effectiveGroupAllow = normalizeAllowFrom(groupAllowOverride ?? params.groupAllowFrom); + const expandedGroupAllowFrom = await expandTelegramAllowFromWithAccessGroups({ + cfg: params.cfg, + allowFrom: groupAllowOverride ?? params.groupAllowFrom, + accountId, + senderId: params.senderId, + }); + const effectiveGroupAllow = normalizeAllowFrom(expandedGroupAllowFrom); const hasGroupAllowOverride = groupAllowOverride !== undefined; return { resolvedThreadId,