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 {