fix: fail closed missing provider group policy across message channels (#23367) (thanks @bmendonca3)

This commit is contained in:
Peter Steinberger
2026-02-22 12:17:44 +01:00
parent 78c3c2a542
commit 777817392d
45 changed files with 420 additions and 75 deletions

View File

@@ -18,6 +18,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- Channels/Security: fail closed on missing provider group policy config by defaulting runtime group policy to `allowlist` (instead of inheriting `channels.defaults.groupPolicy`) when `channels.<provider>` is absent across message channels, and align runtime + security warnings/docs to the same fallback behavior (Slack, Discord, iMessage, Telegram, WhatsApp, Signal, LINE, Matrix, Mattermost, Google Chat, IRC, Nextcloud Talk, Feishu, and Zalo user flows; plus Discord message/native-command paths). (#23367) Thanks @bmendonca3.
- CLI/Sessions: pass the configured sessions directory when resolving transcript paths in `agentCommand`, so custom `session.store` locations resume sessions reliably. Thanks @davidrudduck.
- Gateway/Chat UI: strip inline reply/audio directive tags from non-streaming final webchat broadcasts (including `chat.inject`) while preserving empty-string message content when tags are the entire reply. (#23298) Thanks @SidQin-cyber.
- Gateway/Restart: fix restart-loop edge cases by keeping `openclaw.mjs -> dist/entry.js` bootstrap detection explicit, reacquiring the gateway lock for in-process restart fallback paths, and tightening restart-loop regression coverage. (#23416) Thanks @jeffwnli.

View File

@@ -425,7 +425,7 @@ Example:
}
```
If you only set `DISCORD_BOT_TOKEN` and do not create a `channels.discord` block, runtime fallback is `groupPolicy="allowlist"` (with a warning in logs).
If you only set `DISCORD_BOT_TOKEN` and do not create a `channels.discord` block, runtime fallback is `groupPolicy="allowlist"` (with a warning in logs), even if `channels.defaults.groupPolicy` is `open`.
</Tab>

View File

@@ -190,6 +190,7 @@ Notes:
- Group DMs are controlled separately (`channels.discord.dm.*`, `channels.slack.dm.*`).
- Telegram allowlist can match user IDs (`"123456789"`, `"telegram:123456789"`, `"tg:123456789"`) or usernames (`"@alice"` or `"alice"`); prefixes are case-insensitive.
- Default is `groupPolicy: "allowlist"`; if your group allowlist is empty, group messages are blocked.
- Runtime safety: when a provider block is completely missing (`channels.<provider>` absent), group policy falls back to a fail-closed mode (typically `allowlist`) instead of inheriting `channels.defaults.groupPolicy`.
Quick mental model (evaluation order for group messages):

View File

@@ -158,6 +158,7 @@ imsg send <handle> "test"
Group sender allowlist: `channels.imessage.groupAllowFrom`.
Runtime fallback: if `groupAllowFrom` is unset, iMessage group sender checks fall back to `allowFrom` when available.
Runtime note: if `channels.imessage` is completely missing, runtime falls back to `groupPolicy="allowlist"` and logs a warning (even if `channels.defaults.groupPolicy` is set).
Mention gating for groups:

View File

@@ -118,6 +118,7 @@ Allowlists and policies:
- `channels.line.groupPolicy`: `allowlist | open | disabled`
- `channels.line.groupAllowFrom`: allowlisted LINE user IDs for groups
- Per-group overrides: `channels.line.groups.<groupId>.allowFrom`
- Runtime note: if `channels.line` is completely missing, runtime falls back to `groupPolicy="allowlist"` for group checks (even if `channels.defaults.groupPolicy` is set).
LINE IDs are case-sensitive. Valid IDs look like:

View File

@@ -195,6 +195,7 @@ Notes:
## Rooms (groups)
- Default: `channels.matrix.groupPolicy = "allowlist"` (mention-gated). Use `channels.defaults.groupPolicy` to override the default when unset.
- Runtime note: if `channels.matrix` is completely missing, runtime falls back to `groupPolicy="allowlist"` for room checks (even if `channels.defaults.groupPolicy` is set).
- Allowlist rooms with `channels.matrix.groups` (room IDs or aliases; names are resolved to IDs when directory search finds a single exact match):
```json5

View File

@@ -103,6 +103,7 @@ Notes:
- Default: `channels.mattermost.groupPolicy = "allowlist"` (mention-gated).
- Allowlist senders with `channels.mattermost.groupAllowFrom` (user IDs or `@username`).
- Open channels: `channels.mattermost.groupPolicy="open"` (mention-gated).
- Runtime note: if `channels.mattermost` is completely missing, runtime falls back to `groupPolicy="allowlist"` for group checks (even if `channels.defaults.groupPolicy` is set).
## Targets for outbound delivery

View File

@@ -195,6 +195,7 @@ Groups:
- `channels.signal.groupPolicy = open | allowlist | disabled`.
- `channels.signal.groupAllowFrom` controls who can trigger in groups when `allowlist` is set.
- Runtime note: if `channels.signal` is completely missing, runtime falls back to `groupPolicy="allowlist"` for group checks (even if `channels.defaults.groupPolicy` is set).
## How it works (behavior)

View File

@@ -165,7 +165,7 @@ For actions/directory reads, user token can be preferred when configured. For wr
Channel allowlist lives under `channels.slack.channels`.
Runtime note: if `channels.slack` is completely missing (env-only setup) and `channels.defaults.groupPolicy` is unset, runtime falls back to `groupPolicy="allowlist"` and logs a warning.
Runtime note: if `channels.slack` is completely missing (env-only setup), runtime falls back to `groupPolicy="allowlist"` and logs a warning (even if `channels.defaults.groupPolicy` is set).
Name/ID resolution:

View File

@@ -148,6 +148,7 @@ curl "https://api.telegram.org/bot<bot_token>/getUpdates"
`groupAllowFrom` is used for group sender filtering. If not set, Telegram falls back to `allowFrom`.
`groupAllowFrom` entries must be numeric Telegram user IDs.
Runtime note: if `channels.telegram` is completely missing, runtime falls back to `groupPolicy="allowlist"` for group policy evaluation (even if `channels.defaults.groupPolicy` is set).
Example: allow any member in one specific group:

View File

@@ -171,7 +171,7 @@ OpenClaw recommends running WhatsApp on a separate number when possible. (The ch
- if `groupAllowFrom` is unset, runtime falls back to `allowFrom` when available
- sender allowlists are evaluated before mention/reply activation
Note: if no `channels.whatsapp` block exists at all, runtime group-policy fallback is effectively `open`.
Note: if no `channels.whatsapp` block exists at all, runtime group-policy fallback is `allowlist` (with a warning log), even if `channels.defaults.groupPolicy` is set.
</Tab>

View File

@@ -22,6 +22,7 @@ import {
resolveDefaultDiscordAccountId,
resolveDiscordGroupRequireMention,
resolveDiscordGroupToolPolicy,
resolveRuntimeGroupPolicy,
setAccountEnabledInConfigSection,
type ChannelMessageActionAdapter,
type ChannelPlugin,
@@ -131,7 +132,13 @@ export const discordPlugin: ChannelPlugin<ResolvedDiscordAccount> = {
collectWarnings: ({ account, cfg }) => {
const warnings: string[] = [];
const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy;
const groupPolicy = account.config.groupPolicy ?? defaultGroupPolicy ?? "open";
const { groupPolicy } = resolveRuntimeGroupPolicy({
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;
const channelAllowlistConfigured = guildsConfigured;

View File

@@ -2,10 +2,11 @@ import type { ClawdbotConfig, RuntimeEnv } from "openclaw/plugin-sdk";
import {
buildAgentMediaPayload,
buildPendingHistoryContextFromMap,
recordPendingHistoryEntryIfEnabled,
clearHistoryEntriesIfEnabled,
DEFAULT_GROUP_HISTORY_LIMIT,
type HistoryEntry,
recordPendingHistoryEntryIfEnabled,
resolveRuntimeGroupPolicy,
} from "openclaw/plugin-sdk";
import { resolveFeishuAccount } from "./accounts.js";
import { createFeishuClient } from "./client.js";
@@ -77,6 +78,7 @@ const senderNameCache = new Map<string, { name: string; expireAt: number }>();
// Key: appId or "default", Value: timestamp of last notification
const permissionErrorNotifiedAt = new Map<string, number>();
const PERMISSION_ERROR_COOLDOWN_MS = 5 * 60 * 1000; // 5 minutes
const groupPolicyFallbackWarningShown = new Set<string>();
type SenderNameResult = {
name?: string;
@@ -563,7 +565,20 @@ export async function handleFeishuMessage(params: {
const useAccessGroups = cfg.commands?.useAccessGroups !== false;
if (isGroup) {
const groupPolicy = feishuCfg?.groupPolicy ?? "open";
const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy;
const { groupPolicy, providerMissingFallbackApplied } = resolveRuntimeGroupPolicy({
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).',
);
}
const groupAllowFrom = feishuCfg?.groupAllowFrom ?? [];
// DEBUG: log(`feishu[${account.accountId}]: groupPolicy=${groupPolicy}`);

View File

@@ -4,6 +4,7 @@ import {
createDefaultChannelRuntimeState,
DEFAULT_ACCOUNT_ID,
PAIRING_APPROVED_MESSAGE,
resolveRuntimeGroupPolicy,
} from "openclaw/plugin-sdk";
import {
resolveFeishuAccount,
@@ -227,7 +228,13 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount> = {
const defaultGroupPolicy = (
cfg.channels as Record<string, { groupPolicy?: string }> | undefined
)?.defaults?.groupPolicy;
const groupPolicy = feishuCfg?.groupPolicy ?? defaultGroupPolicy ?? "allowlist";
const { groupPolicy } = resolveRuntimeGroupPolicy({
providerConfigPresent: cfg.channels?.feishu !== undefined,
groupPolicy: feishuCfg?.groupPolicy,
defaultGroupPolicy,
configuredFallbackPolicy: "allowlist",
missingProviderFallbackPolicy: "allowlist",
});
if (groupPolicy !== "open") return [];
return [
`- Feishu[${account.accountId}] groups: groupPolicy="open" allows any member to trigger (mention-gated). Set channels.feishu.groupPolicy="allowlist" + channels.feishu.groupAllowFrom to restrict senders.`,

View File

@@ -11,6 +11,7 @@ import {
PAIRING_APPROVED_MESSAGE,
resolveChannelMediaMaxBytes,
resolveGoogleChatGroupRequireMention,
resolveRuntimeGroupPolicy,
setAccountEnabledInConfigSection,
type ChannelDock,
type ChannelMessageActionAdapter,
@@ -199,7 +200,13 @@ export const googlechatPlugin: ChannelPlugin<ResolvedGoogleChatAccount> = {
collectWarnings: ({ account, cfg }) => {
const warnings: string[] = [];
const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy;
const groupPolicy = account.config.groupPolicy ?? defaultGroupPolicy ?? "allowlist";
const { groupPolicy } = resolveRuntimeGroupPolicy({
providerConfigPresent: cfg.channels?.googlechat !== undefined,
groupPolicy: account.config.groupPolicy,
defaultGroupPolicy,
configuredFallbackPolicy: "allowlist",
missingProviderFallbackPolicy: "allowlist",
});
if (groupPolicy === "open") {
warnings.push(
`- Google Chat spaces: groupPolicy="open" allows any space to trigger (mention-gated). Set channels.googlechat.groupPolicy="allowlist" and configure channels.googlechat.groups.`,

View File

@@ -5,6 +5,7 @@ import {
readJsonBodyWithLimit,
registerWebhookTarget,
rejectNonPostWebhookRequest,
resolveRuntimeGroupPolicy,
resolveSingleWebhookTargetAsync,
resolveWebhookPath,
resolveWebhookTargets,
@@ -67,6 +68,7 @@ function logVerbose(core: GoogleChatCoreRuntime, runtime: GoogleChatRuntimeEnv,
}
const warnedDeprecatedUsersEmailAllowFrom = new Set<string>();
const warnedMissingProviderGroupPolicy = new Set<string>();
function warnDeprecatedUsersEmailEntries(
core: GoogleChatCoreRuntime,
runtime: GoogleChatRuntimeEnv,
@@ -427,7 +429,21 @@ async function processMessageWithPipeline(params: {
}
const defaultGroupPolicy = config.channels?.defaults?.groupPolicy;
const groupPolicy = account.config.groupPolicy ?? defaultGroupPolicy ?? "allowlist";
const { groupPolicy, providerMissingFallbackApplied } = resolveRuntimeGroupPolicy({
providerConfigPresent: config.channels?.googlechat !== undefined,
groupPolicy: account.config.groupPolicy,
defaultGroupPolicy,
configuredFallbackPolicy: "allowlist",
missingProviderFallbackPolicy: "allowlist",
});
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,

View File

@@ -18,6 +18,7 @@ import {
resolveIMessageAccount,
resolveIMessageGroupRequireMention,
resolveIMessageGroupToolPolicy,
resolveRuntimeGroupPolicy,
setAccountEnabledInConfigSection,
type ChannelPlugin,
type ResolvedIMessageAccount,
@@ -98,7 +99,13 @@ export const imessagePlugin: ChannelPlugin<ResolvedIMessageAccount> = {
},
collectWarnings: ({ account, cfg }) => {
const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy;
const groupPolicy = account.config.groupPolicy ?? defaultGroupPolicy ?? "allowlist";
const { groupPolicy } = resolveRuntimeGroupPolicy({
providerConfigPresent: cfg.channels?.imessage !== undefined,
groupPolicy: account.config.groupPolicy,
defaultGroupPolicy,
configuredFallbackPolicy: "allowlist",
missingProviderFallbackPolicy: "allowlist",
});
if (groupPolicy !== "open") {
return [];
}

View File

@@ -4,6 +4,7 @@ import {
formatPairingApproveHint,
getChatChannelMeta,
PAIRING_APPROVED_MESSAGE,
resolveRuntimeGroupPolicy,
setAccountEnabledInConfigSection,
deleteAccountFromConfigSection,
type ChannelPlugin,
@@ -135,7 +136,13 @@ export const ircPlugin: ChannelPlugin<ResolvedIrcAccount, IrcProbe> = {
collectWarnings: ({ account, cfg }) => {
const warnings: string[] = [];
const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy;
const groupPolicy = account.config.groupPolicy ?? defaultGroupPolicy ?? "allowlist";
const { groupPolicy } = resolveRuntimeGroupPolicy({
providerConfigPresent: cfg.channels?.irc !== undefined,
groupPolicy: account.config.groupPolicy,
defaultGroupPolicy,
configuredFallbackPolicy: "allowlist",
missingProviderFallbackPolicy: "allowlist",
});
if (groupPolicy === "open") {
warnings.push(
'- IRC channels: groupPolicy="open" allows all channels and senders (mention-gated). Prefer channels.irc.groupPolicy="allowlist" with channels.irc.groups.',

View File

@@ -2,6 +2,7 @@ import {
createReplyPrefixOptions,
logInboundDrop,
resolveControlCommandGate,
resolveRuntimeGroupPolicy,
type OpenClawConfig,
type RuntimeEnv,
} from "openclaw/plugin-sdk";
@@ -19,6 +20,7 @@ import { sendMessageIrc } from "./send.js";
import type { CoreConfig, IrcInboundMessage } from "./types.js";
const CHANNEL_ID = "irc" as const;
const warnedMissingProviderGroupPolicy = new Set<string>();
const escapeIrcRegexLiteral = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -85,7 +87,19 @@ export async function handleIrcInbound(params: {
const dmPolicy = account.config.dmPolicy ?? "pairing";
const defaultGroupPolicy = config.channels?.defaults?.groupPolicy;
const groupPolicy = account.config.groupPolicy ?? defaultGroupPolicy ?? "allowlist";
const { groupPolicy, providerMissingFallbackApplied } = resolveRuntimeGroupPolicy({
providerConfigPresent: config.channels?.irc !== undefined,
groupPolicy: account.config.groupPolicy,
defaultGroupPolicy,
configuredFallbackPolicy: "allowlist",
missingProviderFallbackPolicy: "allowlist",
});
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);

View File

@@ -3,6 +3,7 @@ import {
DEFAULT_ACCOUNT_ID,
LineConfigSchema,
processLineMessage,
resolveRuntimeGroupPolicy,
type ChannelPlugin,
type ChannelStatusIssue,
type OpenClawConfig,
@@ -163,7 +164,13 @@ export const linePlugin: ChannelPlugin<ResolvedLineAccount> = {
collectWarnings: ({ account, cfg }) => {
const defaultGroupPolicy = (cfg.channels?.defaults as { groupPolicy?: string } | undefined)
?.groupPolicy;
const groupPolicy = account.config.groupPolicy ?? defaultGroupPolicy ?? "allowlist";
const { groupPolicy } = resolveRuntimeGroupPolicy({
providerConfigPresent: cfg.channels?.line !== undefined,
groupPolicy: account.config.groupPolicy,
defaultGroupPolicy,
configuredFallbackPolicy: "allowlist",
missingProviderFallbackPolicy: "allowlist",
});
if (groupPolicy !== "open") {
return [];
}

View File

@@ -6,6 +6,7 @@ import {
formatPairingApproveHint,
normalizeAccountId,
PAIRING_APPROVED_MESSAGE,
resolveRuntimeGroupPolicy,
setAccountEnabledInConfigSection,
type ChannelPlugin,
} from "openclaw/plugin-sdk";
@@ -170,7 +171,13 @@ export const matrixPlugin: ChannelPlugin<ResolvedMatrixAccount> = {
},
collectWarnings: ({ account, cfg }) => {
const defaultGroupPolicy = (cfg as CoreConfig).channels?.defaults?.groupPolicy;
const groupPolicy = account.config.groupPolicy ?? defaultGroupPolicy ?? "allowlist";
const { groupPolicy } = resolveRuntimeGroupPolicy({
providerConfigPresent: (cfg as CoreConfig).channels?.matrix !== undefined,
groupPolicy: account.config.groupPolicy,
defaultGroupPolicy,
configuredFallbackPolicy: "allowlist",
missingProviderFallbackPolicy: "allowlist",
});
if (groupPolicy !== "open") {
return [];
}

View File

@@ -1,5 +1,10 @@
import { format } from "node:util";
import { mergeAllowlist, summarizeMapping, type RuntimeEnv } from "openclaw/plugin-sdk";
import {
mergeAllowlist,
resolveRuntimeGroupPolicy,
summarizeMapping,
type RuntimeEnv,
} from "openclaw/plugin-sdk";
import { resolveMatrixTargets } from "../../resolve-targets.js";
import { getMatrixRuntime } from "../../runtime.js";
import type { CoreConfig, ReplyToMode } from "../../types.js";
@@ -243,7 +248,20 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi
const mentionRegexes = core.channel.mentions.buildMentionRegexes(cfg);
const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy;
const groupPolicyRaw = accountConfig.groupPolicy ?? defaultGroupPolicy ?? "allowlist";
const { groupPolicy: groupPolicyRaw, providerMissingFallbackApplied } = resolveRuntimeGroupPolicy(
{
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).',
);
}
const groupPolicy = allowlistOnly && groupPolicyRaw === "open" ? "allowlist" : groupPolicyRaw;
const replyToMode = opts.replyToMode ?? accountConfig.replyToMode ?? "off";
const threadReplies = accountConfig.threadReplies ?? "inbound";

View File

@@ -6,6 +6,7 @@ import {
formatPairingApproveHint,
migrateBaseNameToDefaultAccount,
normalizeAccountId,
resolveRuntimeGroupPolicy,
setAccountEnabledInConfigSection,
type ChannelMessageActionAdapter,
type ChannelMessageActionName,
@@ -229,7 +230,13 @@ export const mattermostPlugin: ChannelPlugin<ResolvedMattermostAccount> = {
},
collectWarnings: ({ account, cfg }) => {
const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy;
const groupPolicy = account.config.groupPolicy ?? defaultGroupPolicy ?? "allowlist";
const { groupPolicy } = resolveRuntimeGroupPolicy({
providerConfigPresent: cfg.channels?.mattermost !== undefined,
groupPolicy: account.config.groupPolicy,
defaultGroupPolicy,
configuredFallbackPolicy: "allowlist",
missingProviderFallbackPolicy: "allowlist",
});
if (groupPolicy !== "open") {
return [];
}

View File

@@ -16,6 +16,7 @@ import {
DEFAULT_GROUP_HISTORY_LIMIT,
recordPendingHistoryEntryIfEnabled,
resolveControlCommandGate,
resolveRuntimeGroupPolicy,
resolveChannelMediaMaxBytes,
type HistoryEntry,
} from "openclaw/plugin-sdk";
@@ -242,6 +243,19 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {}
cfg.messages?.groupChat?.historyLimit ?? DEFAULT_GROUP_HISTORY_LIMIT,
);
const channelHistories = new Map<string, HistoryEntry[]>();
const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy;
const { groupPolicy, providerMissingFallbackApplied } = resolveRuntimeGroupPolicy({
providerConfigPresent: cfg.channels?.mattermost !== undefined,
groupPolicy: account.config.groupPolicy,
defaultGroupPolicy,
configuredFallbackPolicy: "allowlist",
missingProviderFallbackPolicy: "allowlist",
});
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);
@@ -375,8 +389,6 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {}
senderId;
const rawText = post.message?.trim() || "";
const dmPolicy = account.config.dmPolicy ?? "pairing";
const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy;
const groupPolicy = account.config.groupPolicy ?? defaultGroupPolicy ?? "allowlist";
const configAllowFrom = normalizeAllowList(account.config.allowFrom ?? []);
const configGroupAllowFrom = normalizeAllowList(account.config.groupAllowFrom ?? []);
const storeAllowFrom = normalizeAllowList(
@@ -887,8 +899,6 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {}
}
}
} else if (kind) {
const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy;
const groupPolicy = account.config.groupPolicy ?? defaultGroupPolicy ?? "allowlist";
if (groupPolicy === "disabled") {
logVerboseMessage(`mattermost: drop reaction (groupPolicy=disabled channel=${channelId})`);
return;

View File

@@ -6,6 +6,7 @@ import {
DEFAULT_ACCOUNT_ID,
MSTeamsConfigSchema,
PAIRING_APPROVED_MESSAGE,
resolveRuntimeGroupPolicy,
} from "openclaw/plugin-sdk";
import { listMSTeamsDirectoryGroupsLive, listMSTeamsDirectoryPeersLive } from "./directory-live.js";
import { msteamsOnboardingAdapter } from "./onboarding.js";
@@ -128,7 +129,13 @@ export const msteamsPlugin: ChannelPlugin<ResolvedMSTeamsAccount> = {
security: {
collectWarnings: ({ cfg }) => {
const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy;
const groupPolicy = cfg.channels?.msteams?.groupPolicy ?? defaultGroupPolicy ?? "allowlist";
const { groupPolicy } = resolveRuntimeGroupPolicy({
providerConfigPresent: cfg.channels?.msteams !== undefined,
groupPolicy: cfg.channels?.msteams?.groupPolicy,
defaultGroupPolicy,
configuredFallbackPolicy: "allowlist",
missingProviderFallbackPolicy: "allowlist",
});
if (groupPolicy !== "open") {
return [];
}

View File

@@ -5,6 +5,7 @@ import {
deleteAccountFromConfigSection,
formatPairingApproveHint,
normalizeAccountId,
resolveRuntimeGroupPolicy,
setAccountEnabledInConfigSection,
type ChannelPlugin,
type OpenClawConfig,
@@ -129,7 +130,14 @@ export const nextcloudTalkPlugin: ChannelPlugin<ResolvedNextcloudTalkAccount> =
},
collectWarnings: ({ account, cfg }) => {
const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy;
const groupPolicy = account.config.groupPolicy ?? defaultGroupPolicy ?? "allowlist";
const { groupPolicy } = resolveRuntimeGroupPolicy({
providerConfigPresent:
(cfg.channels as Record<string, unknown> | undefined)?.["nextcloud-talk"] !== undefined,
groupPolicy: account.config.groupPolicy,
defaultGroupPolicy,
configuredFallbackPolicy: "allowlist",
missingProviderFallbackPolicy: "allowlist",
});
if (groupPolicy !== "open") {
return [];
}

View File

@@ -2,6 +2,7 @@ import {
createReplyPrefixOptions,
logInboundDrop,
resolveControlCommandGate,
resolveRuntimeGroupPolicy,
type OpenClawConfig,
type RuntimeEnv,
} from "openclaw/plugin-sdk";
@@ -20,6 +21,7 @@ import { sendMessageNextcloudTalk } from "./send.js";
import type { CoreConfig, GroupPolicy, NextcloudTalkInboundMessage } from "./types.js";
const CHANNEL_ID = "nextcloud-talk" as const;
const warnedMissingProviderGroupPolicy = new Set<string>();
async function deliverNextcloudTalkReply(params: {
payload: { text?: string; mediaUrls?: string[]; mediaUrl?: string; replyToId?: string };
@@ -84,12 +86,26 @@ export async function handleNextcloudTalkInbound(params: {
statusSink?.({ lastInboundAt: message.timestamp });
const dmPolicy = account.config.dmPolicy ?? "pairing";
const defaultGroupPolicy = (config.channels as Record<string, unknown> | undefined)?.defaults as
| { groupPolicy?: string }
| undefined;
const groupPolicy = (account.config.groupPolicy ??
defaultGroupPolicy?.groupPolicy ??
"allowlist") as GroupPolicy;
const defaultGroupPolicy = (
(config.channels as Record<string, unknown> | undefined)?.defaults as
| { groupPolicy?: string }
| undefined
)?.groupPolicy as GroupPolicy | undefined;
const { groupPolicy, providerMissingFallbackApplied } = resolveRuntimeGroupPolicy({
providerConfigPresent:
((config.channels as Record<string, unknown> | undefined)?.["nextcloud-talk"] ??
undefined) !== undefined,
groupPolicy: account.config.groupPolicy as GroupPolicy | undefined,
defaultGroupPolicy,
configuredFallbackPolicy: "allowlist",
missingProviderFallbackPolicy: "allowlist",
});
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);

View File

@@ -17,6 +17,7 @@ import {
PAIRING_APPROVED_MESSAGE,
resolveChannelMediaMaxBytes,
resolveDefaultSignalAccountId,
resolveRuntimeGroupPolicy,
resolveSignalAccount,
setAccountEnabledInConfigSection,
signalOnboardingAdapter,
@@ -124,7 +125,13 @@ export const signalPlugin: ChannelPlugin<ResolvedSignalAccount> = {
},
collectWarnings: ({ account, cfg }) => {
const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy;
const groupPolicy = account.config.groupPolicy ?? defaultGroupPolicy ?? "allowlist";
const { groupPolicy } = resolveRuntimeGroupPolicy({
providerConfigPresent: cfg.channels?.signal !== undefined,
groupPolicy: account.config.groupPolicy,
defaultGroupPolicy,
configuredFallbackPolicy: "allowlist",
missingProviderFallbackPolicy: "allowlist",
});
if (groupPolicy !== "open") {
return [];
}

View File

@@ -19,6 +19,7 @@ import {
resolveDefaultSlackAccountId,
resolveSlackAccount,
resolveSlackReplyToMode,
resolveRuntimeGroupPolicy,
resolveSlackGroupRequireMention,
resolveSlackGroupToolPolicy,
buildSlackThreadingToolContext,
@@ -151,7 +152,13 @@ export const slackPlugin: ChannelPlugin<ResolvedSlackAccount> = {
collectWarnings: ({ account, cfg }) => {
const warnings: string[] = [];
const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy;
const groupPolicy = account.config.groupPolicy ?? defaultGroupPolicy ?? "open";
const { groupPolicy } = resolveRuntimeGroupPolicy({
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;

View File

@@ -17,6 +17,7 @@ import {
parseTelegramReplyToMessageId,
parseTelegramThreadId,
resolveDefaultTelegramAccountId,
resolveRuntimeGroupPolicy,
resolveTelegramAccount,
resolveTelegramGroupRequireMention,
resolveTelegramGroupToolPolicy,
@@ -196,7 +197,13 @@ export const telegramPlugin: ChannelPlugin<ResolvedTelegramAccount, TelegramProb
},
collectWarnings: ({ account, cfg }) => {
const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy;
const groupPolicy = account.config.groupPolicy ?? defaultGroupPolicy ?? "allowlist";
const { groupPolicy } = resolveRuntimeGroupPolicy({
providerConfigPresent: cfg.channels?.telegram !== undefined,
groupPolicy: account.config.groupPolicy,
defaultGroupPolicy,
configuredFallbackPolicy: "allowlist",
missingProviderFallbackPolicy: "allowlist",
});
if (groupPolicy !== "open") {
return [];
}

View File

@@ -19,6 +19,7 @@ import {
readStringParam,
resolveDefaultWhatsAppAccountId,
resolveWhatsAppOutboundTarget,
resolveRuntimeGroupPolicy,
resolveWhatsAppAccount,
resolveWhatsAppGroupRequireMention,
resolveWhatsAppGroupToolPolicy,
@@ -143,7 +144,13 @@ export const whatsappPlugin: ChannelPlugin<ResolvedWhatsAppAccount> = {
},
collectWarnings: ({ account, cfg }) => {
const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy;
const groupPolicy = account.groupPolicy ?? defaultGroupPolicy ?? "allowlist";
const { groupPolicy } = resolveRuntimeGroupPolicy({
providerConfigPresent: cfg.channels?.whatsapp !== undefined,
groupPolicy: account.groupPolicy,
defaultGroupPolicy,
configuredFallbackPolicy: "allowlist",
missingProviderFallbackPolicy: "allowlist",
});
if (groupPolicy !== "open") {
return [];
}

View File

@@ -3,6 +3,7 @@ import type { OpenClawConfig, MarkdownTableMode, RuntimeEnv } from "openclaw/plu
import {
createReplyPrefixOptions,
mergeAllowlist,
resolveRuntimeGroupPolicy,
resolveSenderCommandAuthorization,
summarizeMapping,
} from "openclaw/plugin-sdk";
@@ -178,7 +179,20 @@ async function processMessage(
const chatId = threadId;
const defaultGroupPolicy = config.channels?.defaults?.groupPolicy;
const groupPolicy = account.config.groupPolicy ?? defaultGroupPolicy ?? "open";
const { groupPolicy, providerMissingFallbackApplied } = resolveRuntimeGroupPolicy({
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).',
);
}
const groups = account.config.groups ?? {};
if (isGroup) {
if (groupPolicy === "disabled") {

View File

@@ -447,7 +447,7 @@ export const zalouserOnboardingAdapter: ChannelOnboardingAdapter = {
const accessConfig = await promptChannelAccessConfig({
prompter,
label: "Zalo groups",
currentPolicy: account.config.groupPolicy ?? "open",
currentPolicy: account.config.groupPolicy ?? "allowlist",
currentEntries: Object.keys(account.config.groups ?? {}),
placeholder: "Family, Work, 123456789",
updatePrompt: Boolean(account.config.groups),

View File

@@ -4,6 +4,7 @@ import {
createInboundDebouncer,
resolveInboundDebounceMs,
} from "../../auto-reply/inbound-debounce.js";
import { resolveRuntimeGroupPolicy } 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";
@@ -23,7 +24,13 @@ type DiscordMessageHandlerParams = Omit<
export function createDiscordMessageHandler(
params: DiscordMessageHandlerParams,
): DiscordMessageHandler {
const groupPolicy = params.discordConfig?.groupPolicy ?? "open";
const { groupPolicy } = resolveRuntimeGroupPolicy({
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" });

View File

@@ -39,6 +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 { loadSessionStore, resolveStorePath } from "../../config/sessions.js";
import { logVerbose } from "../../globals.js";
import { createSubsystemLogger } from "../../logging/subsystem.js";
@@ -1329,8 +1330,15 @@ async function dispatchDiscordCommandInteraction(params: {
const channelAllowlistConfigured =
Boolean(guildInfo?.channels) && Object.keys(guildInfo?.channels ?? {}).length > 0;
const channelAllowed = channelConfig?.allowed !== false;
const { groupPolicy } = resolveRuntimeGroupPolicy({
providerConfigPresent: cfg.channels?.discord !== undefined,
groupPolicy: discordConfig?.groupPolicy,
defaultGroupPolicy: cfg.channels?.defaults?.groupPolicy,
configuredFallbackPolicy: "open",
missingProviderFallbackPolicy: "allowlist",
});
const allowByPolicy = isDiscordGroupAllowedByPolicy({
groupPolicy: discordConfig?.groupPolicy ?? "open",
groupPolicy,
guildAllowlisted: Boolean(guildInfo),
channelAllowlistConfigured,
channelAllowed,

View File

@@ -26,4 +26,13 @@ describe("resolveDiscordRuntimeGroupPolicy", () => {
expect(resolved.groupPolicy).toBe("disabled");
expect(resolved.providerMissingFallbackApplied).toBe(false);
});
it("ignores explicit global defaults when provider config is missing", () => {
const resolved = __testing.resolveDiscordRuntimeGroupPolicy({
providerConfigPresent: false,
defaultGroupPolicy: "open",
});
expect(resolved.groupPolicy).toBe("allowlist");
expect(resolved.providerMissingFallbackApplied).toBe(true);
});
});

View File

@@ -21,6 +21,7 @@ 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 { danger, logVerbose, shouldLogVerbose, warn } from "../../globals.js";
import { formatErrorMessage } from "../../infra/errors.js";
@@ -179,15 +180,13 @@ function resolveDiscordRuntimeGroupPolicy(params: {
groupPolicy: GroupPolicy;
providerMissingFallbackApplied: boolean;
} {
const groupPolicy =
params.groupPolicy ??
params.defaultGroupPolicy ??
(params.providerConfigPresent ? "open" : "allowlist");
const providerMissingFallbackApplied =
!params.providerConfigPresent &&
params.groupPolicy === undefined &&
params.defaultGroupPolicy === undefined;
return { groupPolicy, providerMissingFallbackApplied };
return resolveRuntimeGroupPolicy({
providerConfigPresent: params.providerConfigPresent,
groupPolicy: params.groupPolicy,
defaultGroupPolicy: params.defaultGroupPolicy,
configuredFallbackPolicy: "open",
missingProviderFallbackPolicy: "allowlist",
});
}
async function deployDiscordCommands(params: {
@@ -265,20 +264,22 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
const runtime: RuntimeEnv = opts.runtime ?? createNonExitingRuntime();
const discordCfg = account.config;
const rawDiscordCfg = account.config;
const discordRootThreadBindings = cfg.channels?.discord?.threadBindings;
const discordAccountThreadBindings =
cfg.channels?.discord?.accounts?.[account.accountId]?.threadBindings;
const discordRestFetch = resolveDiscordRestFetch(discordCfg.proxy, runtime);
const dmConfig = discordCfg.dm;
let guildEntries = discordCfg.guilds;
const discordRestFetch = resolveDiscordRestFetch(rawDiscordCfg.proxy, runtime);
const dmConfig = rawDiscordCfg.dm;
let guildEntries = rawDiscordCfg.guilds;
const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy;
const providerConfigPresent = cfg.channels?.discord !== undefined;
const { groupPolicy, providerMissingFallbackApplied } = resolveDiscordRuntimeGroupPolicy({
providerConfigPresent,
groupPolicy: discordCfg.groupPolicy,
groupPolicy: rawDiscordCfg.groupPolicy,
defaultGroupPolicy,
});
const discordCfg =
rawDiscordCfg.groupPolicy === groupPolicy ? rawDiscordCfg : { ...rawDiscordCfg, groupPolicy };
if (providerMissingFallbackApplied) {
runtime.log?.(
warn(

View File

@@ -16,8 +16,10 @@ 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 { readSessionUpdatedAt, resolveStorePath } from "../../config/sessions.js";
import { danger, logVerbose, shouldLogVerbose } from "../../globals.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";
import { mediaKindFromMime } from "../../media/constants.js";
@@ -120,6 +122,23 @@ 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<void> {
const runtime = resolveRuntime(opts);
const cfg = opts.config ?? loadConfig();
@@ -144,7 +163,18 @@ export async function monitorIMessageProvider(opts: MonitorIMessageOpts = {}): P
(imessageCfg.allowFrom && imessageCfg.allowFrom.length > 0 ? imessageCfg.allowFrom : []),
);
const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy;
const groupPolicy = imessageCfg.groupPolicy ?? defaultGroupPolicy ?? "open";
const { groupPolicy, providerMissingFallbackApplied } = resolveIMessageRuntimeGroupPolicy({
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).',
),
);
}
const dmPolicy = imessageCfg.dmPolicy ?? "pairing";
const includeAttachments = opts.includeAttachments ?? imessageCfg.includeAttachments ?? false;
const mediaMaxBytes = (opts.mediaMaxMb ?? imessageCfg.mediaMaxMb ?? 16) * 1024 * 1024;
@@ -508,3 +538,7 @@ export async function monitorIMessageProvider(opts: MonitorIMessageOpts = {}): P
await client.stop();
}
}
export const __testing = {
resolveIMessageRuntimeGroupPolicy,
};

View File

@@ -8,6 +8,7 @@ import type {
PostbackEvent,
} from "@line/bot-sdk";
import type { OpenClawConfig } from "../config/config.js";
import { resolveRuntimeGroupPolicy } 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";
@@ -40,6 +41,8 @@ export interface LineHandlerContext {
processMessage: (ctx: LineInboundContext) => Promise<void>;
}
let lineGroupPolicyFallbackWarned = false;
function resolveLineGroupConfig(params: {
config: ResolvedLineAccount["config"];
groupId?: string;
@@ -133,7 +136,19 @@ async function shouldProcessLineEvent(
dmPolicy,
});
const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy;
const groupPolicy = account.config.groupPolicy ?? defaultGroupPolicy ?? "allowlist";
const { groupPolicy, providerMissingFallbackApplied } = resolveRuntimeGroupPolicy({
providerConfigPresent: cfg.channels?.line !== undefined,
groupPolicy: account.config.groupPolicy,
defaultGroupPolicy,
configuredFallbackPolicy: "allowlist",
missingProviderFallbackPolicy: "allowlist",
});
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) {

View File

@@ -132,6 +132,10 @@ export type {
MSTeamsReplyStyle,
MSTeamsTeamConfig,
} from "../config/types.js";
export {
resolveRuntimeGroupPolicy,
type RuntimeGroupPolicyResolution,
} from "../config/runtime-group-policy.js";
export {
DiscordConfigSchema,
GoogleChatConfigSchema,

View File

@@ -3,6 +3,7 @@ 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 type { SignalReactionNotificationMode } from "../config/types.js";
import { waitForTransportReady } from "../infra/transport-ready.js";
import { saveMediaBuffer } from "../media/store.js";
@@ -345,7 +346,18 @@ export async function monitorSignalProvider(opts: MonitorSignalOpts = {}): Promi
: []),
);
const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy;
const groupPolicy = accountInfo.config.groupPolicy ?? defaultGroupPolicy ?? "allowlist";
const { groupPolicy, providerMissingFallbackApplied } = resolveRuntimeGroupPolicy({
providerConfigPresent: cfg.channels?.signal !== undefined,
groupPolicy: accountInfo.config.groupPolicy,
defaultGroupPolicy,
configuredFallbackPolicy: "allowlist",
missingProviderFallbackPolicy: "allowlist",
});
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;

View File

@@ -18,12 +18,12 @@ describe("resolveSlackRuntimeGroupPolicy", () => {
expect(resolved.providerMissingFallbackApplied).toBe(false);
});
it("respects explicit global defaults", () => {
it("ignores explicit global defaults when provider config is missing", () => {
const resolved = __testing.resolveSlackRuntimeGroupPolicy({
providerConfigPresent: false,
defaultGroupPolicy: "open",
});
expect(resolved.groupPolicy).toBe("open");
expect(resolved.providerMissingFallbackApplied).toBe(false);
expect(resolved.groupPolicy).toBe("allowlist");
expect(resolved.providerMissingFallbackApplied).toBe(true);
});
});

View File

@@ -10,6 +10,7 @@ import {
summarizeMapping,
} from "../../channels/allowlists/resolve-utils.js";
import { loadConfig } from "../../config/config.js";
import { resolveRuntimeGroupPolicy } 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";
@@ -50,15 +51,13 @@ function resolveSlackRuntimeGroupPolicy(params: {
groupPolicy: GroupPolicy;
providerMissingFallbackApplied: boolean;
} {
const groupPolicy =
params.groupPolicy ??
params.defaultGroupPolicy ??
(params.providerConfigPresent ? "open" : "allowlist");
const providerMissingFallbackApplied =
!params.providerConfigPresent &&
params.groupPolicy === undefined &&
params.defaultGroupPolicy === undefined;
return { groupPolicy, providerMissingFallbackApplied };
return resolveRuntimeGroupPolicy({
providerConfigPresent: params.providerConfigPresent,
groupPolicy: params.groupPolicy,
defaultGroupPolicy: params.defaultGroupPolicy,
configuredFallbackPolicy: "open",
missingProviderFallbackPolicy: "allowlist",
});
}
function parseApiAppIdFromAppToken(raw?: string) {

View File

@@ -1,5 +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 type {
TelegramAccountConfig,
TelegramGroupConfig,
@@ -72,6 +73,19 @@ export type TelegramGroupPolicyAccessResult =
groupPolicy: "open" | "disabled" | "allowlist";
};
export const resolveTelegramRuntimeGroupPolicy = (params: {
providerConfigPresent: boolean;
groupPolicy?: TelegramAccountConfig["groupPolicy"];
defaultGroupPolicy?: TelegramAccountConfig["groupPolicy"];
}) =>
resolveRuntimeGroupPolicy({
providerConfigPresent: params.providerConfigPresent,
groupPolicy: params.groupPolicy,
defaultGroupPolicy: params.defaultGroupPolicy,
configuredFallbackPolicy: "open",
missingProviderFallbackPolicy: "allowlist",
});
export const evaluateTelegramGroupPolicyAccess = (params: {
isGroup: boolean;
chatId: string | number;
@@ -90,20 +104,21 @@ export const evaluateTelegramGroupPolicyAccess = (params: {
requireSenderForAllowlistAuthorization: boolean;
checkChatAllowlist: boolean;
}): TelegramGroupPolicyAccessResult => {
const { groupPolicy: runtimeFallbackPolicy } = resolveTelegramRuntimeGroupPolicy({
providerConfigPresent: params.cfg.channels?.telegram !== undefined,
groupPolicy: params.telegramCfg.groupPolicy,
defaultGroupPolicy: params.cfg.channels?.defaults?.groupPolicy,
});
const fallbackPolicy =
firstDefined(
params.telegramCfg.groupPolicy,
params.cfg.channels?.defaults?.groupPolicy,
"open",
) ?? "open";
firstDefined(params.telegramCfg.groupPolicy, params.cfg.channels?.defaults?.groupPolicy) ??
runtimeFallbackPolicy;
const groupPolicy = params.useTopicAndGroupOverrides
? (firstDefined(
params.topicConfig?.groupPolicy,
params.groupConfig?.groupPolicy,
params.telegramCfg.groupPolicy,
params.cfg.channels?.defaults?.groupPolicy,
"open",
) ?? "open")
) ?? runtimeFallbackPolicy)
: fallbackPolicy;
if (!params.isGroup || !params.enforcePolicy) {

View File

@@ -1,4 +1,5 @@
import { loadConfig } from "../../config/config.js";
import { resolveRuntimeGroupPolicy } from "../../config/runtime-group-policy.js";
import { logVerbose } from "../../globals.js";
import { buildPairingReply } from "../../pairing/pairing-messages.js";
import {
@@ -17,6 +18,23 @@ export type InboundAccessControlResult = {
const PAIRING_REPLY_HISTORY_GRACE_MS = 30_000;
function resolveWhatsAppRuntimeGroupPolicy(params: {
providerConfigPresent: boolean;
groupPolicy?: "open" | "allowlist" | "disabled";
defaultGroupPolicy?: "open" | "allowlist" | "disabled";
}): {
groupPolicy: "open" | "allowlist" | "disabled";
providerMissingFallbackApplied: boolean;
} {
return resolveRuntimeGroupPolicy({
providerConfigPresent: params.providerConfigPresent,
groupPolicy: params.groupPolicy,
defaultGroupPolicy: params.defaultGroupPolicy,
configuredFallbackPolicy: "open",
missingProviderFallbackPolicy: "allowlist",
});
}
export async function checkInboundAccessControl(params: {
accountId: string;
from: string;
@@ -82,7 +100,16 @@ export async function checkInboundAccessControl(params: {
// - "disabled": block all group messages entirely
// - "allowlist": only allow group messages from senders in groupAllowFrom/allowFrom
const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy;
const groupPolicy = account.groupPolicy ?? defaultGroupPolicy ?? "open";
const { groupPolicy, providerMissingFallbackApplied } = resolveWhatsAppRuntimeGroupPolicy({
providerConfigPresent: cfg.channels?.whatsapp !== undefined,
groupPolicy: account.groupPolicy,
defaultGroupPolicy,
});
if (providerMissingFallbackApplied) {
logVerbose(
'whatsapp: channels.whatsapp is missing; defaulting groupPolicy to "allowlist" (group messages blocked until explicitly configured).',
);
}
if (params.group && groupPolicy === "disabled") {
logVerbose("Blocked group message (groupPolicy: disabled)");
return {
@@ -191,3 +218,7 @@ export async function checkInboundAccessControl(params: {
resolvedAccountId: account.accountId,
};
}
export const __testing = {
resolveWhatsAppRuntimeGroupPolicy,
};