diff --git a/docs/gateway/configuration-reference.md b/docs/gateway/configuration-reference.md index b11ea7a37aa..34478bb324f 100644 --- a/docs/gateway/configuration-reference.md +++ b/docs/gateway/configuration-reference.md @@ -35,7 +35,7 @@ All channels support DM policies and group policies: `channels.defaults.groupPolicy` sets the default when a provider's `groupPolicy` is unset. Pairing codes expire after 1 hour. Pending DM pairing requests are capped at **3 per channel**. -Slack/Discord have a special fallback: if their provider section is missing entirely, runtime group policy can resolve to `open` (with a startup warning). +If a provider block is missing entirely (`channels.` absent), runtime group policy falls back to `allowlist` (fail-closed) with a startup warning. ### Channel model overrides diff --git a/extensions/discord/src/channel.ts b/extensions/discord/src/channel.ts index 9922062c4c4..9131ae42ee2 100644 --- a/extensions/discord/src/channel.ts +++ b/extensions/discord/src/channel.ts @@ -22,7 +22,7 @@ import { resolveDefaultDiscordAccountId, resolveDiscordGroupRequireMention, resolveDiscordGroupToolPolicy, - resolveRuntimeGroupPolicy, + resolveOpenProviderRuntimeGroupPolicy, setAccountEnabledInConfigSection, type ChannelMessageActionAdapter, type ChannelPlugin, @@ -132,12 +132,10 @@ export const discordPlugin: ChannelPlugin = { collectWarnings: ({ account, cfg }) => { const warnings: string[] = []; const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy; - const { groupPolicy } = resolveRuntimeGroupPolicy({ + const { groupPolicy } = resolveOpenProviderRuntimeGroupPolicy({ providerConfigPresent: cfg.channels?.discord !== undefined, groupPolicy: account.config.groupPolicy, defaultGroupPolicy, - configuredFallbackPolicy: "open", - missingProviderFallbackPolicy: "allowlist", }); const guildEntries = account.config.guilds ?? {}; const guildsConfigured = Object.keys(guildEntries).length > 0; diff --git a/extensions/feishu/src/bot.ts b/extensions/feishu/src/bot.ts index 7922997c7d5..14b4c95f0a7 100644 --- a/extensions/feishu/src/bot.ts +++ b/extensions/feishu/src/bot.ts @@ -6,7 +6,8 @@ import { DEFAULT_GROUP_HISTORY_LIMIT, type HistoryEntry, recordPendingHistoryEntryIfEnabled, - resolveRuntimeGroupPolicy, + resolveOpenProviderRuntimeGroupPolicy, + warnMissingProviderGroupPolicyFallbackOnce, } from "openclaw/plugin-sdk"; import { resolveFeishuAccount } from "./accounts.js"; import { createFeishuClient } from "./client.js"; @@ -78,7 +79,6 @@ const senderNameCache = new Map(); // Key: appId or "default", Value: timestamp of last notification const permissionErrorNotifiedAt = new Map(); const PERMISSION_ERROR_COOLDOWN_MS = 5 * 60 * 1000; // 5 minutes -const groupPolicyFallbackWarningShown = new Set(); type SenderNameResult = { name?: string; @@ -566,19 +566,17 @@ export async function handleFeishuMessage(params: { if (isGroup) { const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy; - const { groupPolicy, providerMissingFallbackApplied } = resolveRuntimeGroupPolicy({ + const { groupPolicy, providerMissingFallbackApplied } = resolveOpenProviderRuntimeGroupPolicy({ providerConfigPresent: cfg.channels?.feishu !== undefined, groupPolicy: feishuCfg?.groupPolicy, defaultGroupPolicy, - configuredFallbackPolicy: "open", - missingProviderFallbackPolicy: "allowlist", }); - if (providerMissingFallbackApplied && !groupPolicyFallbackWarningShown.has(account.accountId)) { - groupPolicyFallbackWarningShown.add(account.accountId); - log( - 'feishu: channels.feishu is missing; defaulting groupPolicy to "allowlist" (group messages blocked until explicitly configured).', - ); - } + warnMissingProviderGroupPolicyFallbackOnce({ + providerMissingFallbackApplied, + providerKey: "feishu", + accountId: account.accountId, + log, + }); const groupAllowFrom = feishuCfg?.groupAllowFrom ?? []; // DEBUG: log(`feishu[${account.accountId}]: groupPolicy=${groupPolicy}`); diff --git a/extensions/feishu/src/channel.ts b/extensions/feishu/src/channel.ts index dbd1e46facb..c4437247608 100644 --- a/extensions/feishu/src/channel.ts +++ b/extensions/feishu/src/channel.ts @@ -4,7 +4,7 @@ import { createDefaultChannelRuntimeState, DEFAULT_ACCOUNT_ID, PAIRING_APPROVED_MESSAGE, - resolveRuntimeGroupPolicy, + resolveAllowlistProviderRuntimeGroupPolicy, } from "openclaw/plugin-sdk"; import { resolveFeishuAccount, @@ -226,12 +226,10 @@ export const feishuPlugin: ChannelPlugin = { const account = resolveFeishuAccount({ cfg, accountId }); const feishuCfg = account.config; const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy; - const { groupPolicy } = resolveRuntimeGroupPolicy({ + const { groupPolicy } = resolveAllowlistProviderRuntimeGroupPolicy({ providerConfigPresent: cfg.channels?.feishu !== undefined, groupPolicy: feishuCfg?.groupPolicy, defaultGroupPolicy, - configuredFallbackPolicy: "allowlist", - missingProviderFallbackPolicy: "allowlist", }); if (groupPolicy !== "open") return []; return [ diff --git a/extensions/googlechat/src/channel.ts b/extensions/googlechat/src/channel.ts index 9cd9bd182aa..d8a9aed16aa 100644 --- a/extensions/googlechat/src/channel.ts +++ b/extensions/googlechat/src/channel.ts @@ -11,7 +11,7 @@ import { PAIRING_APPROVED_MESSAGE, resolveChannelMediaMaxBytes, resolveGoogleChatGroupRequireMention, - resolveRuntimeGroupPolicy, + resolveAllowlistProviderRuntimeGroupPolicy, setAccountEnabledInConfigSection, type ChannelDock, type ChannelMessageActionAdapter, @@ -200,12 +200,10 @@ export const googlechatPlugin: ChannelPlugin = { collectWarnings: ({ account, cfg }) => { const warnings: string[] = []; const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy; - const { groupPolicy } = resolveRuntimeGroupPolicy({ + const { groupPolicy } = resolveAllowlistProviderRuntimeGroupPolicy({ providerConfigPresent: cfg.channels?.googlechat !== undefined, groupPolicy: account.config.groupPolicy, defaultGroupPolicy, - configuredFallbackPolicy: "allowlist", - missingProviderFallbackPolicy: "allowlist", }); if (groupPolicy === "open") { warnings.push( diff --git a/extensions/googlechat/src/monitor.ts b/extensions/googlechat/src/monitor.ts index 8889ec8d5f5..10501c8e1f2 100644 --- a/extensions/googlechat/src/monitor.ts +++ b/extensions/googlechat/src/monitor.ts @@ -5,10 +5,11 @@ import { readJsonBodyWithLimit, registerWebhookTarget, rejectNonPostWebhookRequest, - resolveRuntimeGroupPolicy, + resolveAllowlistProviderRuntimeGroupPolicy, resolveSingleWebhookTargetAsync, resolveWebhookPath, resolveWebhookTargets, + warnMissingProviderGroupPolicyFallbackOnce, requestBodyErrorToText, resolveMentionGatingWithBypass, } from "openclaw/plugin-sdk"; @@ -68,7 +69,6 @@ function logVerbose(core: GoogleChatCoreRuntime, runtime: GoogleChatRuntimeEnv, } const warnedDeprecatedUsersEmailAllowFrom = new Set(); -const warnedMissingProviderGroupPolicy = new Set(); function warnDeprecatedUsersEmailEntries( core: GoogleChatCoreRuntime, runtime: GoogleChatRuntimeEnv, @@ -429,21 +429,19 @@ async function processMessageWithPipeline(params: { } const defaultGroupPolicy = config.channels?.defaults?.groupPolicy; - const { groupPolicy, providerMissingFallbackApplied } = resolveRuntimeGroupPolicy({ - providerConfigPresent: config.channels?.googlechat !== undefined, - groupPolicy: account.config.groupPolicy, - defaultGroupPolicy, - configuredFallbackPolicy: "allowlist", - missingProviderFallbackPolicy: "allowlist", + const { groupPolicy, providerMissingFallbackApplied } = + resolveAllowlistProviderRuntimeGroupPolicy({ + providerConfigPresent: config.channels?.googlechat !== undefined, + groupPolicy: account.config.groupPolicy, + defaultGroupPolicy, + }); + warnMissingProviderGroupPolicyFallbackOnce({ + providerMissingFallbackApplied, + providerKey: "googlechat", + accountId: account.accountId, + blockedLabel: "space messages", + log: (message) => logVerbose(core, runtime, message), }); - if (providerMissingFallbackApplied && !warnedMissingProviderGroupPolicy.has(account.accountId)) { - warnedMissingProviderGroupPolicy.add(account.accountId); - logVerbose( - core, - runtime, - 'googlechat: channels.googlechat is missing; defaulting groupPolicy to "allowlist" (space messages blocked until explicitly configured).', - ); - } const groupConfigResolved = resolveGroupConfig({ groupId: spaceId, groupName: space.displayName ?? null, diff --git a/extensions/imessage/src/channel.ts b/extensions/imessage/src/channel.ts index aacc3246d25..7cba0174000 100644 --- a/extensions/imessage/src/channel.ts +++ b/extensions/imessage/src/channel.ts @@ -18,7 +18,7 @@ import { resolveIMessageAccount, resolveIMessageGroupRequireMention, resolveIMessageGroupToolPolicy, - resolveRuntimeGroupPolicy, + resolveAllowlistProviderRuntimeGroupPolicy, setAccountEnabledInConfigSection, type ChannelPlugin, type ResolvedIMessageAccount, @@ -99,12 +99,10 @@ export const imessagePlugin: ChannelPlugin = { }, collectWarnings: ({ account, cfg }) => { const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy; - const { groupPolicy } = resolveRuntimeGroupPolicy({ + const { groupPolicy } = resolveAllowlistProviderRuntimeGroupPolicy({ providerConfigPresent: cfg.channels?.imessage !== undefined, groupPolicy: account.config.groupPolicy, defaultGroupPolicy, - configuredFallbackPolicy: "allowlist", - missingProviderFallbackPolicy: "allowlist", }); if (groupPolicy !== "open") { return []; diff --git a/extensions/irc/src/channel.ts b/extensions/irc/src/channel.ts index 18bcece05ad..a9e7a4766ed 100644 --- a/extensions/irc/src/channel.ts +++ b/extensions/irc/src/channel.ts @@ -4,7 +4,7 @@ import { formatPairingApproveHint, getChatChannelMeta, PAIRING_APPROVED_MESSAGE, - resolveRuntimeGroupPolicy, + resolveAllowlistProviderRuntimeGroupPolicy, setAccountEnabledInConfigSection, deleteAccountFromConfigSection, type ChannelPlugin, @@ -136,12 +136,10 @@ export const ircPlugin: ChannelPlugin = { collectWarnings: ({ account, cfg }) => { const warnings: string[] = []; const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy; - const { groupPolicy } = resolveRuntimeGroupPolicy({ + const { groupPolicy } = resolveAllowlistProviderRuntimeGroupPolicy({ providerConfigPresent: cfg.channels?.irc !== undefined, groupPolicy: account.config.groupPolicy, defaultGroupPolicy, - configuredFallbackPolicy: "allowlist", - missingProviderFallbackPolicy: "allowlist", }); if (groupPolicy === "open") { warnings.push( diff --git a/extensions/irc/src/inbound.ts b/extensions/irc/src/inbound.ts index eb6daeff611..31586f01417 100644 --- a/extensions/irc/src/inbound.ts +++ b/extensions/irc/src/inbound.ts @@ -2,7 +2,8 @@ import { createReplyPrefixOptions, logInboundDrop, resolveControlCommandGate, - resolveRuntimeGroupPolicy, + resolveAllowlistProviderRuntimeGroupPolicy, + warnMissingProviderGroupPolicyFallbackOnce, type OpenClawConfig, type RuntimeEnv, } from "openclaw/plugin-sdk"; @@ -20,7 +21,6 @@ import { sendMessageIrc } from "./send.js"; import type { CoreConfig, IrcInboundMessage } from "./types.js"; const CHANNEL_ID = "irc" as const; -const warnedMissingProviderGroupPolicy = new Set(); const escapeIrcRegexLiteral = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); @@ -87,19 +87,19 @@ export async function handleIrcInbound(params: { const dmPolicy = account.config.dmPolicy ?? "pairing"; const defaultGroupPolicy = config.channels?.defaults?.groupPolicy; - const { groupPolicy, providerMissingFallbackApplied } = resolveRuntimeGroupPolicy({ - providerConfigPresent: config.channels?.irc !== undefined, - groupPolicy: account.config.groupPolicy, - defaultGroupPolicy, - configuredFallbackPolicy: "allowlist", - missingProviderFallbackPolicy: "allowlist", + const { groupPolicy, providerMissingFallbackApplied } = + resolveAllowlistProviderRuntimeGroupPolicy({ + providerConfigPresent: config.channels?.irc !== undefined, + groupPolicy: account.config.groupPolicy, + defaultGroupPolicy, + }); + warnMissingProviderGroupPolicyFallbackOnce({ + providerMissingFallbackApplied, + providerKey: "irc", + accountId: account.accountId, + blockedLabel: "channel messages", + log: (message) => runtime.log?.(message), }); - if (providerMissingFallbackApplied && !warnedMissingProviderGroupPolicy.has(account.accountId)) { - warnedMissingProviderGroupPolicy.add(account.accountId); - runtime.log?.( - 'irc: channels.irc is missing; defaulting groupPolicy to "allowlist" (channel messages blocked until explicitly configured).', - ); - } const configAllowFrom = normalizeIrcAllowlist(account.config.allowFrom); const configGroupAllowFrom = normalizeIrcAllowlist(account.config.groupAllowFrom); diff --git a/extensions/line/src/channel.ts b/extensions/line/src/channel.ts index b70aa4f1c05..a2a73a87eb9 100644 --- a/extensions/line/src/channel.ts +++ b/extensions/line/src/channel.ts @@ -3,7 +3,7 @@ import { DEFAULT_ACCOUNT_ID, LineConfigSchema, processLineMessage, - resolveRuntimeGroupPolicy, + resolveAllowlistProviderRuntimeGroupPolicy, type ChannelPlugin, type ChannelStatusIssue, type OpenClawConfig, @@ -163,12 +163,10 @@ export const linePlugin: ChannelPlugin = { }, collectWarnings: ({ account, cfg }) => { const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy; - const { groupPolicy } = resolveRuntimeGroupPolicy({ + const { groupPolicy } = resolveAllowlistProviderRuntimeGroupPolicy({ providerConfigPresent: cfg.channels?.line !== undefined, groupPolicy: account.config.groupPolicy, defaultGroupPolicy, - configuredFallbackPolicy: "allowlist", - missingProviderFallbackPolicy: "allowlist", }); if (groupPolicy !== "open") { return []; diff --git a/extensions/matrix/src/channel.ts b/extensions/matrix/src/channel.ts index 75e4b464660..7547d6f0260 100644 --- a/extensions/matrix/src/channel.ts +++ b/extensions/matrix/src/channel.ts @@ -6,7 +6,7 @@ import { formatPairingApproveHint, normalizeAccountId, PAIRING_APPROVED_MESSAGE, - resolveRuntimeGroupPolicy, + resolveAllowlistProviderRuntimeGroupPolicy, setAccountEnabledInConfigSection, type ChannelPlugin, } from "openclaw/plugin-sdk"; @@ -171,12 +171,10 @@ export const matrixPlugin: ChannelPlugin = { }, collectWarnings: ({ account, cfg }) => { const defaultGroupPolicy = (cfg as CoreConfig).channels?.defaults?.groupPolicy; - const { groupPolicy } = resolveRuntimeGroupPolicy({ + const { groupPolicy } = resolveAllowlistProviderRuntimeGroupPolicy({ providerConfigPresent: (cfg as CoreConfig).channels?.matrix !== undefined, groupPolicy: account.config.groupPolicy, defaultGroupPolicy, - configuredFallbackPolicy: "allowlist", - missingProviderFallbackPolicy: "allowlist", }); if (groupPolicy !== "open") { return []; diff --git a/extensions/matrix/src/matrix/monitor/index.ts b/extensions/matrix/src/matrix/monitor/index.ts index 91648498936..eba8b3703f6 100644 --- a/extensions/matrix/src/matrix/monitor/index.ts +++ b/extensions/matrix/src/matrix/monitor/index.ts @@ -1,8 +1,9 @@ import { format } from "node:util"; import { mergeAllowlist, - resolveRuntimeGroupPolicy, + resolveAllowlistProviderRuntimeGroupPolicy, summarizeMapping, + warnMissingProviderGroupPolicyFallbackOnce, type RuntimeEnv, } from "openclaw/plugin-sdk"; import { resolveMatrixTargets } from "../../resolve-targets.js"; @@ -248,20 +249,19 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi const mentionRegexes = core.channel.mentions.buildMentionRegexes(cfg); const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy; - const { groupPolicy: groupPolicyRaw, providerMissingFallbackApplied } = resolveRuntimeGroupPolicy( - { + const { groupPolicy: groupPolicyRaw, providerMissingFallbackApplied } = + resolveAllowlistProviderRuntimeGroupPolicy({ providerConfigPresent: cfg.channels?.matrix !== undefined, groupPolicy: accountConfig.groupPolicy, defaultGroupPolicy, - configuredFallbackPolicy: "allowlist", - missingProviderFallbackPolicy: "allowlist", - }, - ); - if (providerMissingFallbackApplied) { - logVerboseMessage( - 'matrix: channels.matrix is missing; defaulting groupPolicy to "allowlist" (room messages blocked until explicitly configured).', - ); - } + }); + warnMissingProviderGroupPolicyFallbackOnce({ + providerMissingFallbackApplied, + providerKey: "matrix", + accountId: account.accountId, + blockedLabel: "room messages", + log: (message) => logVerboseMessage(message), + }); const groupPolicy = allowlistOnly && groupPolicyRaw === "open" ? "allowlist" : groupPolicyRaw; const replyToMode = opts.replyToMode ?? accountConfig.replyToMode ?? "off"; const threadReplies = accountConfig.threadReplies ?? "inbound"; diff --git a/extensions/mattermost/src/channel.ts b/extensions/mattermost/src/channel.ts index 55e189b55de..4fcc38d189a 100644 --- a/extensions/mattermost/src/channel.ts +++ b/extensions/mattermost/src/channel.ts @@ -6,7 +6,7 @@ import { formatPairingApproveHint, migrateBaseNameToDefaultAccount, normalizeAccountId, - resolveRuntimeGroupPolicy, + resolveAllowlistProviderRuntimeGroupPolicy, setAccountEnabledInConfigSection, type ChannelMessageActionAdapter, type ChannelMessageActionName, @@ -230,12 +230,10 @@ export const mattermostPlugin: ChannelPlugin = { }, collectWarnings: ({ account, cfg }) => { const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy; - const { groupPolicy } = resolveRuntimeGroupPolicy({ + const { groupPolicy } = resolveAllowlistProviderRuntimeGroupPolicy({ providerConfigPresent: cfg.channels?.mattermost !== undefined, groupPolicy: account.config.groupPolicy, defaultGroupPolicy, - configuredFallbackPolicy: "allowlist", - missingProviderFallbackPolicy: "allowlist", }); if (groupPolicy !== "open") { return []; diff --git a/extensions/mattermost/src/mattermost/monitor.ts b/extensions/mattermost/src/mattermost/monitor.ts index 81777f213e4..176d0e19d73 100644 --- a/extensions/mattermost/src/mattermost/monitor.ts +++ b/extensions/mattermost/src/mattermost/monitor.ts @@ -16,8 +16,9 @@ import { DEFAULT_GROUP_HISTORY_LIMIT, recordPendingHistoryEntryIfEnabled, resolveControlCommandGate, - resolveRuntimeGroupPolicy, + resolveAllowlistProviderRuntimeGroupPolicy, resolveChannelMediaMaxBytes, + warnMissingProviderGroupPolicyFallbackOnce, type HistoryEntry, } from "openclaw/plugin-sdk"; import { getMattermostRuntime } from "../runtime.js"; @@ -244,18 +245,18 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {} ); const channelHistories = new Map(); const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy; - const { groupPolicy, providerMissingFallbackApplied } = resolveRuntimeGroupPolicy({ - providerConfigPresent: cfg.channels?.mattermost !== undefined, - groupPolicy: account.config.groupPolicy, - defaultGroupPolicy, - configuredFallbackPolicy: "allowlist", - missingProviderFallbackPolicy: "allowlist", + const { groupPolicy, providerMissingFallbackApplied } = + resolveAllowlistProviderRuntimeGroupPolicy({ + providerConfigPresent: cfg.channels?.mattermost !== undefined, + groupPolicy: account.config.groupPolicy, + defaultGroupPolicy, + }); + warnMissingProviderGroupPolicyFallbackOnce({ + providerMissingFallbackApplied, + providerKey: "mattermost", + accountId: account.accountId, + log: (message) => logVerboseMessage(message), }); - if (providerMissingFallbackApplied) { - logVerboseMessage( - 'mattermost: channels.mattermost is missing; defaulting groupPolicy to "allowlist" (group messages blocked until explicitly configured).', - ); - } const fetchWithAuth: FetchLike = (input, init) => { const headers = new Headers(init?.headers); diff --git a/extensions/msteams/src/channel.ts b/extensions/msteams/src/channel.ts index 9e35450d77a..b0aff91dd85 100644 --- a/extensions/msteams/src/channel.ts +++ b/extensions/msteams/src/channel.ts @@ -6,7 +6,7 @@ import { DEFAULT_ACCOUNT_ID, MSTeamsConfigSchema, PAIRING_APPROVED_MESSAGE, - resolveRuntimeGroupPolicy, + resolveAllowlistProviderRuntimeGroupPolicy, } from "openclaw/plugin-sdk"; import { listMSTeamsDirectoryGroupsLive, listMSTeamsDirectoryPeersLive } from "./directory-live.js"; import { msteamsOnboardingAdapter } from "./onboarding.js"; @@ -129,12 +129,10 @@ export const msteamsPlugin: ChannelPlugin = { security: { collectWarnings: ({ cfg }) => { const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy; - const { groupPolicy } = resolveRuntimeGroupPolicy({ + const { groupPolicy } = resolveAllowlistProviderRuntimeGroupPolicy({ providerConfigPresent: cfg.channels?.msteams !== undefined, groupPolicy: cfg.channels?.msteams?.groupPolicy, defaultGroupPolicy, - configuredFallbackPolicy: "allowlist", - missingProviderFallbackPolicy: "allowlist", }); if (groupPolicy !== "open") { return []; diff --git a/extensions/nextcloud-talk/src/channel.ts b/extensions/nextcloud-talk/src/channel.ts index 3b7769013f8..eb55a4cbd75 100644 --- a/extensions/nextcloud-talk/src/channel.ts +++ b/extensions/nextcloud-talk/src/channel.ts @@ -5,7 +5,7 @@ import { deleteAccountFromConfigSection, formatPairingApproveHint, normalizeAccountId, - resolveRuntimeGroupPolicy, + resolveAllowlistProviderRuntimeGroupPolicy, setAccountEnabledInConfigSection, type ChannelPlugin, type OpenClawConfig, @@ -130,13 +130,11 @@ export const nextcloudTalkPlugin: ChannelPlugin = }, collectWarnings: ({ account, cfg }) => { const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy; - const { groupPolicy } = resolveRuntimeGroupPolicy({ + const { groupPolicy } = resolveAllowlistProviderRuntimeGroupPolicy({ providerConfigPresent: (cfg.channels as Record | undefined)?.["nextcloud-talk"] !== undefined, groupPolicy: account.config.groupPolicy, defaultGroupPolicy, - configuredFallbackPolicy: "allowlist", - missingProviderFallbackPolicy: "allowlist", }); if (groupPolicy !== "open") { return []; diff --git a/extensions/nextcloud-talk/src/inbound.ts b/extensions/nextcloud-talk/src/inbound.ts index 149bff15818..20195c9b817 100644 --- a/extensions/nextcloud-talk/src/inbound.ts +++ b/extensions/nextcloud-talk/src/inbound.ts @@ -2,7 +2,8 @@ import { createReplyPrefixOptions, logInboundDrop, resolveControlCommandGate, - resolveRuntimeGroupPolicy, + resolveAllowlistProviderRuntimeGroupPolicy, + warnMissingProviderGroupPolicyFallbackOnce, type OpenClawConfig, type RuntimeEnv, } from "openclaw/plugin-sdk"; @@ -21,7 +22,6 @@ import { sendMessageNextcloudTalk } from "./send.js"; import type { CoreConfig, GroupPolicy, NextcloudTalkInboundMessage } from "./types.js"; const CHANNEL_ID = "nextcloud-talk" as const; -const warnedMissingProviderGroupPolicy = new Set(); async function deliverNextcloudTalkReply(params: { payload: { text?: string; mediaUrls?: string[]; mediaUrl?: string; replyToId?: string }; @@ -91,21 +91,21 @@ export async function handleNextcloudTalkInbound(params: { | { groupPolicy?: string } | undefined )?.groupPolicy as GroupPolicy | undefined; - const { groupPolicy, providerMissingFallbackApplied } = resolveRuntimeGroupPolicy({ - providerConfigPresent: - ((config.channels as Record | undefined)?.["nextcloud-talk"] ?? - undefined) !== undefined, - groupPolicy: account.config.groupPolicy as GroupPolicy | undefined, - defaultGroupPolicy, - configuredFallbackPolicy: "allowlist", - missingProviderFallbackPolicy: "allowlist", + const { groupPolicy, providerMissingFallbackApplied } = + resolveAllowlistProviderRuntimeGroupPolicy({ + providerConfigPresent: + ((config.channels as Record | undefined)?.["nextcloud-talk"] ?? + undefined) !== undefined, + groupPolicy: account.config.groupPolicy as GroupPolicy | undefined, + defaultGroupPolicy, + }); + warnMissingProviderGroupPolicyFallbackOnce({ + providerMissingFallbackApplied, + providerKey: "nextcloud-talk", + accountId: account.accountId, + blockedLabel: "room messages", + log: (message) => runtime.log?.(message), }); - if (providerMissingFallbackApplied && !warnedMissingProviderGroupPolicy.has(account.accountId)) { - warnedMissingProviderGroupPolicy.add(account.accountId); - runtime.log?.( - 'nextcloud-talk: channels.nextcloud-talk is missing; defaulting groupPolicy to "allowlist" (room messages blocked until explicitly configured).', - ); - } const configAllowFrom = normalizeNextcloudTalkAllowlist(account.config.allowFrom); const configGroupAllowFrom = normalizeNextcloudTalkAllowlist(account.config.groupAllowFrom); diff --git a/extensions/signal/src/channel.ts b/extensions/signal/src/channel.ts index db309b5a09d..01426dd7ebc 100644 --- a/extensions/signal/src/channel.ts +++ b/extensions/signal/src/channel.ts @@ -17,7 +17,7 @@ import { PAIRING_APPROVED_MESSAGE, resolveChannelMediaMaxBytes, resolveDefaultSignalAccountId, - resolveRuntimeGroupPolicy, + resolveAllowlistProviderRuntimeGroupPolicy, resolveSignalAccount, setAccountEnabledInConfigSection, signalOnboardingAdapter, @@ -125,12 +125,10 @@ export const signalPlugin: ChannelPlugin = { }, collectWarnings: ({ account, cfg }) => { const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy; - const { groupPolicy } = resolveRuntimeGroupPolicy({ + const { groupPolicy } = resolveAllowlistProviderRuntimeGroupPolicy({ providerConfigPresent: cfg.channels?.signal !== undefined, groupPolicy: account.config.groupPolicy, defaultGroupPolicy, - configuredFallbackPolicy: "allowlist", - missingProviderFallbackPolicy: "allowlist", }); if (groupPolicy !== "open") { return []; diff --git a/extensions/slack/src/channel.ts b/extensions/slack/src/channel.ts index 8eda437cfed..050fa213e28 100644 --- a/extensions/slack/src/channel.ts +++ b/extensions/slack/src/channel.ts @@ -19,7 +19,7 @@ import { resolveDefaultSlackAccountId, resolveSlackAccount, resolveSlackReplyToMode, - resolveRuntimeGroupPolicy, + resolveOpenProviderRuntimeGroupPolicy, resolveSlackGroupRequireMention, resolveSlackGroupToolPolicy, buildSlackThreadingToolContext, @@ -152,12 +152,10 @@ export const slackPlugin: ChannelPlugin = { collectWarnings: ({ account, cfg }) => { const warnings: string[] = []; const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy; - const { groupPolicy } = resolveRuntimeGroupPolicy({ + const { groupPolicy } = resolveOpenProviderRuntimeGroupPolicy({ providerConfigPresent: cfg.channels?.slack !== undefined, groupPolicy: account.config.groupPolicy, defaultGroupPolicy, - configuredFallbackPolicy: "open", - missingProviderFallbackPolicy: "allowlist", }); const channelAllowlistConfigured = Boolean(account.config.channels) && Object.keys(account.config.channels ?? {}).length > 0; diff --git a/extensions/telegram/src/channel.ts b/extensions/telegram/src/channel.ts index 858e6405e55..9836e0e139b 100644 --- a/extensions/telegram/src/channel.ts +++ b/extensions/telegram/src/channel.ts @@ -17,7 +17,7 @@ import { parseTelegramReplyToMessageId, parseTelegramThreadId, resolveDefaultTelegramAccountId, - resolveRuntimeGroupPolicy, + resolveAllowlistProviderRuntimeGroupPolicy, resolveTelegramAccount, resolveTelegramGroupRequireMention, resolveTelegramGroupToolPolicy, @@ -197,12 +197,10 @@ export const telegramPlugin: ChannelPlugin { const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy; - const { groupPolicy } = resolveRuntimeGroupPolicy({ + const { groupPolicy } = resolveAllowlistProviderRuntimeGroupPolicy({ providerConfigPresent: cfg.channels?.telegram !== undefined, groupPolicy: account.config.groupPolicy, defaultGroupPolicy, - configuredFallbackPolicy: "allowlist", - missingProviderFallbackPolicy: "allowlist", }); if (groupPolicy !== "open") { return []; diff --git a/extensions/whatsapp/src/channel.ts b/extensions/whatsapp/src/channel.ts index 8796dcc14b6..d7abf02b031 100644 --- a/extensions/whatsapp/src/channel.ts +++ b/extensions/whatsapp/src/channel.ts @@ -19,7 +19,7 @@ import { readStringParam, resolveDefaultWhatsAppAccountId, resolveWhatsAppOutboundTarget, - resolveRuntimeGroupPolicy, + resolveAllowlistProviderRuntimeGroupPolicy, resolveWhatsAppAccount, resolveWhatsAppGroupRequireMention, resolveWhatsAppGroupToolPolicy, @@ -144,12 +144,10 @@ export const whatsappPlugin: ChannelPlugin = { }, collectWarnings: ({ account, cfg }) => { const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy; - const { groupPolicy } = resolveRuntimeGroupPolicy({ + const { groupPolicy } = resolveAllowlistProviderRuntimeGroupPolicy({ providerConfigPresent: cfg.channels?.whatsapp !== undefined, groupPolicy: account.groupPolicy, defaultGroupPolicy, - configuredFallbackPolicy: "allowlist", - missingProviderFallbackPolicy: "allowlist", }); if (groupPolicy !== "open") { return []; diff --git a/extensions/zalouser/src/monitor.ts b/extensions/zalouser/src/monitor.ts index 6d723e0513b..ba2ee890e73 100644 --- a/extensions/zalouser/src/monitor.ts +++ b/extensions/zalouser/src/monitor.ts @@ -3,9 +3,10 @@ import type { OpenClawConfig, MarkdownTableMode, RuntimeEnv } from "openclaw/plu import { createReplyPrefixOptions, mergeAllowlist, - resolveRuntimeGroupPolicy, + resolveOpenProviderRuntimeGroupPolicy, resolveSenderCommandAuthorization, summarizeMapping, + warnMissingProviderGroupPolicyFallbackOnce, } from "openclaw/plugin-sdk"; import { getZalouserRuntime } from "./runtime.js"; import { sendMessageZalouser } from "./send.js"; @@ -179,20 +180,17 @@ async function processMessage( const chatId = threadId; const defaultGroupPolicy = config.channels?.defaults?.groupPolicy; - const { groupPolicy, providerMissingFallbackApplied } = resolveRuntimeGroupPolicy({ + const { groupPolicy, providerMissingFallbackApplied } = resolveOpenProviderRuntimeGroupPolicy({ providerConfigPresent: config.channels?.zalouser !== undefined, groupPolicy: account.config.groupPolicy, defaultGroupPolicy, - configuredFallbackPolicy: "open", - missingProviderFallbackPolicy: "allowlist", }); - if (providerMissingFallbackApplied) { - logVerbose( - core, - runtime, - 'zalouser: channels.zalouser is missing; defaulting groupPolicy to "allowlist" (group messages blocked until explicitly configured).', - ); - } + warnMissingProviderGroupPolicyFallbackOnce({ + providerMissingFallbackApplied, + providerKey: "zalouser", + accountId: account.accountId, + log: (message) => logVerbose(core, runtime, message), + }); const groups = account.config.groups ?? {}; if (isGroup) { if (groupPolicy === "disabled") { diff --git a/src/config/runtime-group-policy.test.ts b/src/config/runtime-group-policy.test.ts index f49acda5cad..230954ca3b9 100644 --- a/src/config/runtime-group-policy.test.ts +++ b/src/config/runtime-group-policy.test.ts @@ -1,32 +1,85 @@ import { describe, expect, it } from "vitest"; -import { resolveRuntimeGroupPolicy } from "./runtime-group-policy.js"; +import { + resolveAllowlistProviderRuntimeGroupPolicy, + resolveOpenProviderRuntimeGroupPolicy, + resolveRuntimeGroupPolicy, + warnMissingProviderGroupPolicyFallbackOnce, +} from "./runtime-group-policy.js"; describe("resolveRuntimeGroupPolicy", () => { - it("fails closed when provider config is missing and no defaults are set", () => { - const resolved = resolveRuntimeGroupPolicy({ - providerConfigPresent: false, - }); - expect(resolved.groupPolicy).toBe("allowlist"); - expect(resolved.providerMissingFallbackApplied).toBe(true); + it.each([ + { + title: "fails closed when provider config is missing and no defaults are set", + params: { providerConfigPresent: false }, + expectedPolicy: "allowlist", + expectedFallbackApplied: true, + }, + { + title: "keeps configured fallback when provider config is present", + params: { providerConfigPresent: true, configuredFallbackPolicy: "open" as const }, + expectedPolicy: "open", + expectedFallbackApplied: false, + }, + { + title: "ignores global defaults when provider config is missing", + params: { + providerConfigPresent: false, + defaultGroupPolicy: "disabled" as const, + configuredFallbackPolicy: "open" as const, + missingProviderFallbackPolicy: "allowlist" as const, + }, + expectedPolicy: "allowlist", + expectedFallbackApplied: true, + }, + ])("$title", ({ params, expectedPolicy, expectedFallbackApplied }) => { + const resolved = resolveRuntimeGroupPolicy(params); + expect(resolved.groupPolicy).toBe(expectedPolicy); + expect(resolved.providerMissingFallbackApplied).toBe(expectedFallbackApplied); }); +}); - it("keeps configured fallback when provider config is present", () => { - const resolved = resolveRuntimeGroupPolicy({ +describe("resolveOpenProviderRuntimeGroupPolicy", () => { + it("uses open fallback when provider config exists", () => { + const resolved = resolveOpenProviderRuntimeGroupPolicy({ providerConfigPresent: true, - configuredFallbackPolicy: "open", }); expect(resolved.groupPolicy).toBe("open"); expect(resolved.providerMissingFallbackApplied).toBe(false); }); +}); - it("ignores global defaults when provider config is missing", () => { - const resolved = resolveRuntimeGroupPolicy({ - providerConfigPresent: false, - defaultGroupPolicy: "disabled", - configuredFallbackPolicy: "open", - missingProviderFallbackPolicy: "allowlist", +describe("resolveAllowlistProviderRuntimeGroupPolicy", () => { + it("uses allowlist fallback when provider config exists", () => { + const resolved = resolveAllowlistProviderRuntimeGroupPolicy({ + providerConfigPresent: true, }); expect(resolved.groupPolicy).toBe("allowlist"); - expect(resolved.providerMissingFallbackApplied).toBe(true); + expect(resolved.providerMissingFallbackApplied).toBe(false); + }); +}); + +describe("warnMissingProviderGroupPolicyFallbackOnce", () => { + it("logs only once per provider/account key", () => { + const lines: string[] = []; + const first = warnMissingProviderGroupPolicyFallbackOnce({ + providerMissingFallbackApplied: true, + providerKey: "runtime-policy-test", + accountId: "account-a", + blockedLabel: "room messages", + log: (message) => lines.push(message), + }); + const second = warnMissingProviderGroupPolicyFallbackOnce({ + providerMissingFallbackApplied: true, + providerKey: "runtime-policy-test", + accountId: "account-a", + blockedLabel: "room messages", + log: (message) => lines.push(message), + }); + + expect(first).toBe(true); + expect(second).toBe(false); + expect(lines).toHaveLength(1); + expect(lines[0]).toContain("channels.runtime-policy-test is missing"); + expect(lines[0]).toContain("room messages blocked"); }); }); diff --git a/src/config/runtime-group-policy.ts b/src/config/runtime-group-policy.ts index 12be2c2f8b9..c2658f3862a 100644 --- a/src/config/runtime-group-policy.ts +++ b/src/config/runtime-group-policy.ts @@ -5,13 +5,17 @@ export type RuntimeGroupPolicyResolution = { providerMissingFallbackApplied: boolean; }; -export function resolveRuntimeGroupPolicy(params: { +export type RuntimeGroupPolicyParams = { providerConfigPresent: boolean; groupPolicy?: GroupPolicy; defaultGroupPolicy?: GroupPolicy; configuredFallbackPolicy?: GroupPolicy; missingProviderFallbackPolicy?: GroupPolicy; -}): RuntimeGroupPolicyResolution { +}; + +export function resolveRuntimeGroupPolicy( + params: RuntimeGroupPolicyParams, +): RuntimeGroupPolicyResolution { const configuredFallbackPolicy = params.configuredFallbackPolicy ?? "open"; const missingProviderFallbackPolicy = params.missingProviderFallbackPolicy ?? "allowlist"; const groupPolicy = params.providerConfigPresent @@ -21,3 +25,67 @@ export function resolveRuntimeGroupPolicy(params: { !params.providerConfigPresent && params.groupPolicy === undefined; return { groupPolicy, providerMissingFallbackApplied }; } + +export type ResolveProviderRuntimeGroupPolicyParams = { + providerConfigPresent: boolean; + groupPolicy?: GroupPolicy; + defaultGroupPolicy?: GroupPolicy; +}; + +/** + * Standard provider runtime policy: + * - configured provider fallback: open + * - missing provider fallback: allowlist (fail-closed) + */ +export function resolveOpenProviderRuntimeGroupPolicy( + params: ResolveProviderRuntimeGroupPolicyParams, +): RuntimeGroupPolicyResolution { + return resolveRuntimeGroupPolicy({ + providerConfigPresent: params.providerConfigPresent, + groupPolicy: params.groupPolicy, + defaultGroupPolicy: params.defaultGroupPolicy, + configuredFallbackPolicy: "open", + missingProviderFallbackPolicy: "allowlist", + }); +} + +/** + * Strict provider runtime policy: + * - configured provider fallback: allowlist + * - missing provider fallback: allowlist (fail-closed) + */ +export function resolveAllowlistProviderRuntimeGroupPolicy( + params: ResolveProviderRuntimeGroupPolicyParams, +): RuntimeGroupPolicyResolution { + return resolveRuntimeGroupPolicy({ + providerConfigPresent: params.providerConfigPresent, + groupPolicy: params.groupPolicy, + defaultGroupPolicy: params.defaultGroupPolicy, + configuredFallbackPolicy: "allowlist", + missingProviderFallbackPolicy: "allowlist", + }); +} + +const warnedMissingProviderGroupPolicy = new Set(); + +export function warnMissingProviderGroupPolicyFallbackOnce(params: { + providerMissingFallbackApplied: boolean; + providerKey: string; + accountId?: string; + blockedLabel?: string; + log: (message: string) => void; +}): boolean { + if (!params.providerMissingFallbackApplied) { + return false; + } + const key = `${params.providerKey}:${params.accountId ?? "*"}`; + if (warnedMissingProviderGroupPolicy.has(key)) { + return false; + } + warnedMissingProviderGroupPolicy.add(key); + const blockedLabel = params.blockedLabel?.trim() || "group messages"; + params.log( + `${params.providerKey}: channels.${params.providerKey} is missing; defaulting groupPolicy to "allowlist" (${blockedLabel} blocked until explicitly configured).`, + ); + return true; +} diff --git a/src/discord/monitor/message-handler.ts b/src/discord/monitor/message-handler.ts index 8beae2e6277..fd69ff4e320 100644 --- a/src/discord/monitor/message-handler.ts +++ b/src/discord/monitor/message-handler.ts @@ -4,7 +4,7 @@ import { createInboundDebouncer, resolveInboundDebounceMs, } from "../../auto-reply/inbound-debounce.js"; -import { resolveRuntimeGroupPolicy } from "../../config/runtime-group-policy.js"; +import { resolveOpenProviderRuntimeGroupPolicy } from "../../config/runtime-group-policy.js"; import { danger } from "../../globals.js"; import type { DiscordMessageEvent, DiscordMessageHandler } from "./listeners.js"; import { preflightDiscordMessage } from "./message-handler.preflight.js"; @@ -24,12 +24,10 @@ type DiscordMessageHandlerParams = Omit< export function createDiscordMessageHandler( params: DiscordMessageHandlerParams, ): DiscordMessageHandler { - const { groupPolicy } = resolveRuntimeGroupPolicy({ + const { groupPolicy } = resolveOpenProviderRuntimeGroupPolicy({ providerConfigPresent: params.cfg.channels?.discord !== undefined, groupPolicy: params.discordConfig?.groupPolicy, defaultGroupPolicy: params.cfg.channels?.defaults?.groupPolicy, - configuredFallbackPolicy: "open", - missingProviderFallbackPolicy: "allowlist", }); const ackReactionScope = params.cfg.messages?.ackReactionScope ?? "group-mentions"; const debounceMs = resolveInboundDebounceMs({ cfg: params.cfg, channel: "discord" }); diff --git a/src/discord/monitor/native-command.ts b/src/discord/monitor/native-command.ts index 9ab2c5c3a4c..adad1be709f 100644 --- a/src/discord/monitor/native-command.ts +++ b/src/discord/monitor/native-command.ts @@ -39,7 +39,7 @@ import type { ReplyPayload } from "../../auto-reply/types.js"; import { resolveCommandAuthorizedFromAuthorizers } from "../../channels/command-gating.js"; import { createReplyPrefixOptions } from "../../channels/reply-prefix.js"; import type { OpenClawConfig, loadConfig } from "../../config/config.js"; -import { resolveRuntimeGroupPolicy } from "../../config/runtime-group-policy.js"; +import { resolveOpenProviderRuntimeGroupPolicy } from "../../config/runtime-group-policy.js"; import { loadSessionStore, resolveStorePath } from "../../config/sessions.js"; import { logVerbose } from "../../globals.js"; import { createSubsystemLogger } from "../../logging/subsystem.js"; @@ -1330,12 +1330,10 @@ async function dispatchDiscordCommandInteraction(params: { const channelAllowlistConfigured = Boolean(guildInfo?.channels) && Object.keys(guildInfo?.channels ?? {}).length > 0; const channelAllowed = channelConfig?.allowed !== false; - const { groupPolicy } = resolveRuntimeGroupPolicy({ + const { groupPolicy } = resolveOpenProviderRuntimeGroupPolicy({ providerConfigPresent: cfg.channels?.discord !== undefined, groupPolicy: discordConfig?.groupPolicy, defaultGroupPolicy: cfg.channels?.defaults?.groupPolicy, - configuredFallbackPolicy: "open", - missingProviderFallbackPolicy: "allowlist", }); const allowByPolicy = isDiscordGroupAllowedByPolicy({ groupPolicy, diff --git a/src/discord/monitor/provider.ts b/src/discord/monitor/provider.ts index cea9303f0da..6fab5af9e67 100644 --- a/src/discord/monitor/provider.ts +++ b/src/discord/monitor/provider.ts @@ -21,8 +21,10 @@ import { } from "../../config/commands.js"; import type { OpenClawConfig, ReplyToMode } from "../../config/config.js"; import { loadConfig } from "../../config/config.js"; -import { resolveRuntimeGroupPolicy } from "../../config/runtime-group-policy.js"; -import type { GroupPolicy } from "../../config/types.base.js"; +import { + resolveOpenProviderRuntimeGroupPolicy, + warnMissingProviderGroupPolicyFallbackOnce, +} from "../../config/runtime-group-policy.js"; import { danger, logVerbose, shouldLogVerbose, warn } from "../../globals.js"; import { formatErrorMessage } from "../../infra/errors.js"; import { createDiscordRetryRunner } from "../../infra/retry-policy.js"; @@ -172,23 +174,6 @@ function dedupeSkillCommandsForDiscord( return deduped; } -function resolveDiscordRuntimeGroupPolicy(params: { - providerConfigPresent: boolean; - groupPolicy?: GroupPolicy; - defaultGroupPolicy?: GroupPolicy; -}): { - groupPolicy: GroupPolicy; - providerMissingFallbackApplied: boolean; -} { - return resolveRuntimeGroupPolicy({ - providerConfigPresent: params.providerConfigPresent, - groupPolicy: params.groupPolicy, - defaultGroupPolicy: params.defaultGroupPolicy, - configuredFallbackPolicy: "open", - missingProviderFallbackPolicy: "allowlist", - }); -} - async function deployDiscordCommands(params: { client: Client; runtime: RuntimeEnv; @@ -273,20 +258,20 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) { let guildEntries = rawDiscordCfg.guilds; const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy; const providerConfigPresent = cfg.channels?.discord !== undefined; - const { groupPolicy, providerMissingFallbackApplied } = resolveDiscordRuntimeGroupPolicy({ + const { groupPolicy, providerMissingFallbackApplied } = resolveOpenProviderRuntimeGroupPolicy({ providerConfigPresent, groupPolicy: rawDiscordCfg.groupPolicy, defaultGroupPolicy, }); const discordCfg = rawDiscordCfg.groupPolicy === groupPolicy ? rawDiscordCfg : { ...rawDiscordCfg, groupPolicy }; - if (providerMissingFallbackApplied) { - runtime.log?.( - warn( - 'discord: channels.discord is missing; defaulting groupPolicy to "allowlist" (guild messages blocked until explicitly configured).', - ), - ); - } + warnMissingProviderGroupPolicyFallbackOnce({ + providerMissingFallbackApplied, + providerKey: "discord", + accountId: account.accountId, + blockedLabel: "guild messages", + log: (message) => runtime.log?.(warn(message)), + }); let allowFrom = discordCfg.allowFrom ?? dmConfig?.allowFrom; const mediaMaxBytes = (opts.mediaMaxMb ?? discordCfg.mediaMaxMb ?? 8) * 1024 * 1024; const textLimit = resolveTextChunkLimit(cfg, "discord", account.accountId, { @@ -643,7 +628,7 @@ async function clearDiscordNativeCommands(params: { export const __testing = { createDiscordGatewayPlugin, dedupeSkillCommandsForDiscord, - resolveDiscordRuntimeGroupPolicy, + resolveDiscordRuntimeGroupPolicy: resolveOpenProviderRuntimeGroupPolicy, resolveDiscordRestFetch, resolveThreadBindingsEnabled, }; diff --git a/src/imessage/monitor/monitor-provider.ts b/src/imessage/monitor/monitor-provider.ts index 2a114e8465e..69f568442a2 100644 --- a/src/imessage/monitor/monitor-provider.ts +++ b/src/imessage/monitor/monitor-provider.ts @@ -16,9 +16,11 @@ import { createReplyDispatcher } from "../../auto-reply/reply/reply-dispatcher.j import { createReplyPrefixOptions } from "../../channels/reply-prefix.js"; import { recordInboundSession } from "../../channels/session.js"; import { loadConfig } from "../../config/config.js"; -import { resolveRuntimeGroupPolicy } from "../../config/runtime-group-policy.js"; +import { + resolveOpenProviderRuntimeGroupPolicy, + warnMissingProviderGroupPolicyFallbackOnce, +} from "../../config/runtime-group-policy.js"; import { readSessionUpdatedAt, resolveStorePath } from "../../config/sessions.js"; -import type { GroupPolicy } from "../../config/types.base.js"; import { danger, logVerbose, shouldLogVerbose, warn } from "../../globals.js"; import { normalizeScpRemoteHost } from "../../infra/scp-host.js"; import { waitForTransportReady } from "../../infra/transport-ready.js"; @@ -122,23 +124,6 @@ class SentMessageCache { } } -function resolveIMessageRuntimeGroupPolicy(params: { - providerConfigPresent: boolean; - groupPolicy?: GroupPolicy; - defaultGroupPolicy?: GroupPolicy; -}): { - groupPolicy: GroupPolicy; - providerMissingFallbackApplied: boolean; -} { - return resolveRuntimeGroupPolicy({ - providerConfigPresent: params.providerConfigPresent, - groupPolicy: params.groupPolicy, - defaultGroupPolicy: params.defaultGroupPolicy, - configuredFallbackPolicy: "open", - missingProviderFallbackPolicy: "allowlist", - }); -} - export async function monitorIMessageProvider(opts: MonitorIMessageOpts = {}): Promise { const runtime = resolveRuntime(opts); const cfg = opts.config ?? loadConfig(); @@ -163,18 +148,17 @@ export async function monitorIMessageProvider(opts: MonitorIMessageOpts = {}): P (imessageCfg.allowFrom && imessageCfg.allowFrom.length > 0 ? imessageCfg.allowFrom : []), ); const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy; - const { groupPolicy, providerMissingFallbackApplied } = resolveIMessageRuntimeGroupPolicy({ + const { groupPolicy, providerMissingFallbackApplied } = resolveOpenProviderRuntimeGroupPolicy({ providerConfigPresent: cfg.channels?.imessage !== undefined, groupPolicy: imessageCfg.groupPolicy, defaultGroupPolicy, }); - if (providerMissingFallbackApplied) { - runtime.log?.( - warn( - 'imessage: channels.imessage is missing; defaulting groupPolicy to "allowlist" (group messages blocked until explicitly configured).', - ), - ); - } + warnMissingProviderGroupPolicyFallbackOnce({ + providerMissingFallbackApplied, + providerKey: "imessage", + accountId: accountInfo.accountId, + log: (message) => runtime.log?.(warn(message)), + }); const dmPolicy = imessageCfg.dmPolicy ?? "pairing"; const includeAttachments = opts.includeAttachments ?? imessageCfg.includeAttachments ?? false; const mediaMaxBytes = (opts.mediaMaxMb ?? imessageCfg.mediaMaxMb ?? 16) * 1024 * 1024; @@ -540,5 +524,5 @@ export async function monitorIMessageProvider(opts: MonitorIMessageOpts = {}): P } export const __testing = { - resolveIMessageRuntimeGroupPolicy, + resolveIMessageRuntimeGroupPolicy: resolveOpenProviderRuntimeGroupPolicy, }; diff --git a/src/line/bot-handlers.ts b/src/line/bot-handlers.ts index 096d7fcc188..b86a4f1a4ee 100644 --- a/src/line/bot-handlers.ts +++ b/src/line/bot-handlers.ts @@ -8,7 +8,10 @@ import type { PostbackEvent, } from "@line/bot-sdk"; import type { OpenClawConfig } from "../config/config.js"; -import { resolveRuntimeGroupPolicy } from "../config/runtime-group-policy.js"; +import { + resolveAllowlistProviderRuntimeGroupPolicy, + warnMissingProviderGroupPolicyFallbackOnce, +} from "../config/runtime-group-policy.js"; import { danger, logVerbose } from "../globals.js"; import { resolvePairingIdLabel } from "../pairing/pairing-labels.js"; import { buildPairingReply } from "../pairing/pairing-messages.js"; @@ -41,8 +44,6 @@ export interface LineHandlerContext { processMessage: (ctx: LineInboundContext) => Promise; } -let lineGroupPolicyFallbackWarned = false; - function resolveLineGroupConfig(params: { config: ResolvedLineAccount["config"]; groupId?: string; @@ -136,19 +137,18 @@ async function shouldProcessLineEvent( dmPolicy, }); const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy; - const { groupPolicy, providerMissingFallbackApplied } = resolveRuntimeGroupPolicy({ - providerConfigPresent: cfg.channels?.line !== undefined, - groupPolicy: account.config.groupPolicy, - defaultGroupPolicy, - configuredFallbackPolicy: "allowlist", - missingProviderFallbackPolicy: "allowlist", + const { groupPolicy, providerMissingFallbackApplied } = + resolveAllowlistProviderRuntimeGroupPolicy({ + providerConfigPresent: cfg.channels?.line !== undefined, + groupPolicy: account.config.groupPolicy, + defaultGroupPolicy, + }); + warnMissingProviderGroupPolicyFallbackOnce({ + providerMissingFallbackApplied, + providerKey: "line", + accountId: account.accountId, + log: (message) => logVerbose(message), }); - if (providerMissingFallbackApplied && !lineGroupPolicyFallbackWarned) { - lineGroupPolicyFallbackWarned = true; - logVerbose( - 'line: channels.line is missing; defaulting groupPolicy to "allowlist" (group messages blocked until explicitly configured).', - ); - } if (isGroup) { if (groupConfig?.enabled === false) { diff --git a/src/plugin-sdk/index.ts b/src/plugin-sdk/index.ts index 07e3c63d7f6..7d64d5ffa27 100644 --- a/src/plugin-sdk/index.ts +++ b/src/plugin-sdk/index.ts @@ -133,8 +133,13 @@ export type { MSTeamsTeamConfig, } from "../config/types.js"; export { + resolveAllowlistProviderRuntimeGroupPolicy, + resolveOpenProviderRuntimeGroupPolicy, resolveRuntimeGroupPolicy, type RuntimeGroupPolicyResolution, + type RuntimeGroupPolicyParams, + type ResolveProviderRuntimeGroupPolicyParams, + warnMissingProviderGroupPolicyFallbackOnce, } from "../config/runtime-group-policy.js"; export { DiscordConfigSchema, diff --git a/src/signal/monitor.ts b/src/signal/monitor.ts index c9bc8dcb219..8424e11cea4 100644 --- a/src/signal/monitor.ts +++ b/src/signal/monitor.ts @@ -3,7 +3,10 @@ import { DEFAULT_GROUP_HISTORY_LIMIT, type HistoryEntry } from "../auto-reply/re import type { ReplyPayload } from "../auto-reply/types.js"; import type { OpenClawConfig } from "../config/config.js"; import { loadConfig } from "../config/config.js"; -import { resolveRuntimeGroupPolicy } from "../config/runtime-group-policy.js"; +import { + resolveAllowlistProviderRuntimeGroupPolicy, + warnMissingProviderGroupPolicyFallbackOnce, +} from "../config/runtime-group-policy.js"; import type { SignalReactionNotificationMode } from "../config/types.js"; import { waitForTransportReady } from "../infra/transport-ready.js"; import { saveMediaBuffer } from "../media/store.js"; @@ -346,18 +349,18 @@ export async function monitorSignalProvider(opts: MonitorSignalOpts = {}): Promi : []), ); const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy; - const { groupPolicy, providerMissingFallbackApplied } = resolveRuntimeGroupPolicy({ - providerConfigPresent: cfg.channels?.signal !== undefined, - groupPolicy: accountInfo.config.groupPolicy, - defaultGroupPolicy, - configuredFallbackPolicy: "allowlist", - missingProviderFallbackPolicy: "allowlist", + const { groupPolicy, providerMissingFallbackApplied } = + resolveAllowlistProviderRuntimeGroupPolicy({ + providerConfigPresent: cfg.channels?.signal !== undefined, + groupPolicy: accountInfo.config.groupPolicy, + defaultGroupPolicy, + }); + warnMissingProviderGroupPolicyFallbackOnce({ + providerMissingFallbackApplied, + providerKey: "signal", + accountId: accountInfo.accountId, + log: (message) => runtime.log?.(message), }); - if (providerMissingFallbackApplied) { - runtime.log?.( - 'signal: channels.signal is missing; defaulting groupPolicy to "allowlist" (group messages blocked until explicitly configured).', - ); - } const reactionMode = accountInfo.config.reactionNotifications ?? "own"; const reactionAllowlist = normalizeAllowList(accountInfo.config.reactionAllowlist); const mediaMaxBytes = (opts.mediaMaxMb ?? accountInfo.config.mediaMaxMb ?? 8) * 1024 * 1024; diff --git a/src/slack/monitor/provider.ts b/src/slack/monitor/provider.ts index 1d52d561036..472d459b35d 100644 --- a/src/slack/monitor/provider.ts +++ b/src/slack/monitor/provider.ts @@ -10,9 +10,11 @@ import { summarizeMapping, } from "../../channels/allowlists/resolve-utils.js"; import { loadConfig } from "../../config/config.js"; -import { resolveRuntimeGroupPolicy } from "../../config/runtime-group-policy.js"; +import { + resolveOpenProviderRuntimeGroupPolicy, + warnMissingProviderGroupPolicyFallbackOnce, +} from "../../config/runtime-group-policy.js"; import type { SessionScope } from "../../config/sessions.js"; -import type { GroupPolicy } from "../../config/types.base.js"; import { warn } from "../../globals.js"; import { installRequestBodyLimitGuard } from "../../infra/http-body.js"; import { normalizeMainKey } from "../../routing/session-key.js"; @@ -43,23 +45,6 @@ const { App, HTTPReceiver } = slackBolt; const SLACK_WEBHOOK_MAX_BODY_BYTES = 1024 * 1024; const SLACK_WEBHOOK_BODY_TIMEOUT_MS = 30_000; -function resolveSlackRuntimeGroupPolicy(params: { - providerConfigPresent: boolean; - groupPolicy?: GroupPolicy; - defaultGroupPolicy?: GroupPolicy; -}): { - groupPolicy: GroupPolicy; - providerMissingFallbackApplied: boolean; -} { - return resolveRuntimeGroupPolicy({ - providerConfigPresent: params.providerConfigPresent, - groupPolicy: params.groupPolicy, - defaultGroupPolicy: params.defaultGroupPolicy, - configuredFallbackPolicy: "open", - missingProviderFallbackPolicy: "allowlist", - }); -} - function parseApiAppIdFromAppToken(raw?: string) { const token = raw?.trim(); if (!token) { @@ -119,18 +104,17 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) { let channelsConfig = slackCfg.channels; const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy; const providerConfigPresent = cfg.channels?.slack !== undefined; - const { groupPolicy, providerMissingFallbackApplied } = resolveSlackRuntimeGroupPolicy({ + const { groupPolicy, providerMissingFallbackApplied } = resolveOpenProviderRuntimeGroupPolicy({ providerConfigPresent, groupPolicy: slackCfg.groupPolicy, defaultGroupPolicy, }); - if (providerMissingFallbackApplied) { - runtime.log?.( - warn( - 'slack: channels.slack is missing; defaulting groupPolicy to "allowlist" (group messages blocked until explicitly configured).', - ), - ); - } + warnMissingProviderGroupPolicyFallbackOnce({ + providerMissingFallbackApplied, + providerKey: "slack", + accountId: account.accountId, + log: (message) => runtime.log?.(warn(message)), + }); const resolveToken = slackCfg.userToken?.trim() || botToken; const useAccessGroups = cfg.commands?.useAccessGroups !== false; @@ -384,5 +368,5 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) { } export const __testing = { - resolveSlackRuntimeGroupPolicy, + resolveSlackRuntimeGroupPolicy: resolveOpenProviderRuntimeGroupPolicy, }; diff --git a/src/telegram/group-access.ts b/src/telegram/group-access.ts index 571457d3b65..dcd0dd2ef6e 100644 --- a/src/telegram/group-access.ts +++ b/src/telegram/group-access.ts @@ -1,6 +1,6 @@ import type { OpenClawConfig } from "../config/config.js"; import type { ChannelGroupPolicy } from "../config/group-policy.js"; -import { resolveRuntimeGroupPolicy } from "../config/runtime-group-policy.js"; +import { resolveOpenProviderRuntimeGroupPolicy } from "../config/runtime-group-policy.js"; import type { TelegramAccountConfig, TelegramGroupConfig, @@ -78,12 +78,10 @@ export const resolveTelegramRuntimeGroupPolicy = (params: { groupPolicy?: TelegramAccountConfig["groupPolicy"]; defaultGroupPolicy?: TelegramAccountConfig["groupPolicy"]; }) => - resolveRuntimeGroupPolicy({ + resolveOpenProviderRuntimeGroupPolicy({ providerConfigPresent: params.providerConfigPresent, groupPolicy: params.groupPolicy, defaultGroupPolicy: params.defaultGroupPolicy, - configuredFallbackPolicy: "open", - missingProviderFallbackPolicy: "allowlist", }); export const evaluateTelegramGroupPolicyAccess = (params: { diff --git a/src/web/inbound/access-control.ts b/src/web/inbound/access-control.ts index 5f5737f3a2b..e4f6454345b 100644 --- a/src/web/inbound/access-control.ts +++ b/src/web/inbound/access-control.ts @@ -1,5 +1,8 @@ import { loadConfig } from "../../config/config.js"; -import { resolveRuntimeGroupPolicy } from "../../config/runtime-group-policy.js"; +import { + resolveOpenProviderRuntimeGroupPolicy, + warnMissingProviderGroupPolicyFallbackOnce, +} from "../../config/runtime-group-policy.js"; import { logVerbose } from "../../globals.js"; import { buildPairingReply } from "../../pairing/pairing-messages.js"; import { @@ -26,12 +29,10 @@ function resolveWhatsAppRuntimeGroupPolicy(params: { groupPolicy: "open" | "allowlist" | "disabled"; providerMissingFallbackApplied: boolean; } { - return resolveRuntimeGroupPolicy({ + return resolveOpenProviderRuntimeGroupPolicy({ providerConfigPresent: params.providerConfigPresent, groupPolicy: params.groupPolicy, defaultGroupPolicy: params.defaultGroupPolicy, - configuredFallbackPolicy: "open", - missingProviderFallbackPolicy: "allowlist", }); } @@ -105,11 +106,12 @@ export async function checkInboundAccessControl(params: { groupPolicy: account.groupPolicy, defaultGroupPolicy, }); - if (providerMissingFallbackApplied) { - logVerbose( - 'whatsapp: channels.whatsapp is missing; defaulting groupPolicy to "allowlist" (group messages blocked until explicitly configured).', - ); - } + warnMissingProviderGroupPolicyFallbackOnce({ + providerMissingFallbackApplied, + providerKey: "whatsapp", + accountId: account.accountId, + log: (message) => logVerbose(message), + }); if (params.group && groupPolicy === "disabled") { logVerbose("Blocked group message (groupPolicy: disabled)"); return {