refactor(channels): centralize runtime group policy handling

This commit is contained in:
Peter Steinberger
2026-02-22 12:35:02 +01:00
parent a4607277a9
commit 85e5ed3f78
34 changed files with 345 additions and 300 deletions

View File

@@ -35,7 +35,7 @@ All channels support DM policies and group policies:
<Note> <Note>
`channels.defaults.groupPolicy` sets the default when a provider's `groupPolicy` is unset. `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**. 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.<provider>` absent), runtime group policy falls back to `allowlist` (fail-closed) with a startup warning.
</Note> </Note>
### Channel model overrides ### Channel model overrides

View File

@@ -22,7 +22,7 @@ import {
resolveDefaultDiscordAccountId, resolveDefaultDiscordAccountId,
resolveDiscordGroupRequireMention, resolveDiscordGroupRequireMention,
resolveDiscordGroupToolPolicy, resolveDiscordGroupToolPolicy,
resolveRuntimeGroupPolicy, resolveOpenProviderRuntimeGroupPolicy,
setAccountEnabledInConfigSection, setAccountEnabledInConfigSection,
type ChannelMessageActionAdapter, type ChannelMessageActionAdapter,
type ChannelPlugin, type ChannelPlugin,
@@ -132,12 +132,10 @@ export const discordPlugin: ChannelPlugin<ResolvedDiscordAccount> = {
collectWarnings: ({ account, cfg }) => { collectWarnings: ({ account, cfg }) => {
const warnings: string[] = []; const warnings: string[] = [];
const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy; const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy;
const { groupPolicy } = resolveRuntimeGroupPolicy({ const { groupPolicy } = resolveOpenProviderRuntimeGroupPolicy({
providerConfigPresent: cfg.channels?.discord !== undefined, providerConfigPresent: cfg.channels?.discord !== undefined,
groupPolicy: account.config.groupPolicy, groupPolicy: account.config.groupPolicy,
defaultGroupPolicy, defaultGroupPolicy,
configuredFallbackPolicy: "open",
missingProviderFallbackPolicy: "allowlist",
}); });
const guildEntries = account.config.guilds ?? {}; const guildEntries = account.config.guilds ?? {};
const guildsConfigured = Object.keys(guildEntries).length > 0; const guildsConfigured = Object.keys(guildEntries).length > 0;

View File

@@ -6,7 +6,8 @@ import {
DEFAULT_GROUP_HISTORY_LIMIT, DEFAULT_GROUP_HISTORY_LIMIT,
type HistoryEntry, type HistoryEntry,
recordPendingHistoryEntryIfEnabled, recordPendingHistoryEntryIfEnabled,
resolveRuntimeGroupPolicy, resolveOpenProviderRuntimeGroupPolicy,
warnMissingProviderGroupPolicyFallbackOnce,
} from "openclaw/plugin-sdk"; } from "openclaw/plugin-sdk";
import { resolveFeishuAccount } from "./accounts.js"; import { resolveFeishuAccount } from "./accounts.js";
import { createFeishuClient } from "./client.js"; import { createFeishuClient } from "./client.js";
@@ -78,7 +79,6 @@ const senderNameCache = new Map<string, { name: string; expireAt: number }>();
// Key: appId or "default", Value: timestamp of last notification // Key: appId or "default", Value: timestamp of last notification
const permissionErrorNotifiedAt = new Map<string, number>(); const permissionErrorNotifiedAt = new Map<string, number>();
const PERMISSION_ERROR_COOLDOWN_MS = 5 * 60 * 1000; // 5 minutes const PERMISSION_ERROR_COOLDOWN_MS = 5 * 60 * 1000; // 5 minutes
const groupPolicyFallbackWarningShown = new Set<string>();
type SenderNameResult = { type SenderNameResult = {
name?: string; name?: string;
@@ -566,19 +566,17 @@ export async function handleFeishuMessage(params: {
if (isGroup) { if (isGroup) {
const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy; const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy;
const { groupPolicy, providerMissingFallbackApplied } = resolveRuntimeGroupPolicy({ const { groupPolicy, providerMissingFallbackApplied } = resolveOpenProviderRuntimeGroupPolicy({
providerConfigPresent: cfg.channels?.feishu !== undefined, providerConfigPresent: cfg.channels?.feishu !== undefined,
groupPolicy: feishuCfg?.groupPolicy, groupPolicy: feishuCfg?.groupPolicy,
defaultGroupPolicy, defaultGroupPolicy,
configuredFallbackPolicy: "open",
missingProviderFallbackPolicy: "allowlist",
}); });
if (providerMissingFallbackApplied && !groupPolicyFallbackWarningShown.has(account.accountId)) { warnMissingProviderGroupPolicyFallbackOnce({
groupPolicyFallbackWarningShown.add(account.accountId); providerMissingFallbackApplied,
log( providerKey: "feishu",
'feishu: channels.feishu is missing; defaulting groupPolicy to "allowlist" (group messages blocked until explicitly configured).', accountId: account.accountId,
); log,
} });
const groupAllowFrom = feishuCfg?.groupAllowFrom ?? []; const groupAllowFrom = feishuCfg?.groupAllowFrom ?? [];
// DEBUG: log(`feishu[${account.accountId}]: groupPolicy=${groupPolicy}`); // DEBUG: log(`feishu[${account.accountId}]: groupPolicy=${groupPolicy}`);

View File

@@ -4,7 +4,7 @@ import {
createDefaultChannelRuntimeState, createDefaultChannelRuntimeState,
DEFAULT_ACCOUNT_ID, DEFAULT_ACCOUNT_ID,
PAIRING_APPROVED_MESSAGE, PAIRING_APPROVED_MESSAGE,
resolveRuntimeGroupPolicy, resolveAllowlistProviderRuntimeGroupPolicy,
} from "openclaw/plugin-sdk"; } from "openclaw/plugin-sdk";
import { import {
resolveFeishuAccount, resolveFeishuAccount,
@@ -226,12 +226,10 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount> = {
const account = resolveFeishuAccount({ cfg, accountId }); const account = resolveFeishuAccount({ cfg, accountId });
const feishuCfg = account.config; const feishuCfg = account.config;
const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy; const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy;
const { groupPolicy } = resolveRuntimeGroupPolicy({ const { groupPolicy } = resolveAllowlistProviderRuntimeGroupPolicy({
providerConfigPresent: cfg.channels?.feishu !== undefined, providerConfigPresent: cfg.channels?.feishu !== undefined,
groupPolicy: feishuCfg?.groupPolicy, groupPolicy: feishuCfg?.groupPolicy,
defaultGroupPolicy, defaultGroupPolicy,
configuredFallbackPolicy: "allowlist",
missingProviderFallbackPolicy: "allowlist",
}); });
if (groupPolicy !== "open") return []; if (groupPolicy !== "open") return [];
return [ return [

View File

@@ -11,7 +11,7 @@ import {
PAIRING_APPROVED_MESSAGE, PAIRING_APPROVED_MESSAGE,
resolveChannelMediaMaxBytes, resolveChannelMediaMaxBytes,
resolveGoogleChatGroupRequireMention, resolveGoogleChatGroupRequireMention,
resolveRuntimeGroupPolicy, resolveAllowlistProviderRuntimeGroupPolicy,
setAccountEnabledInConfigSection, setAccountEnabledInConfigSection,
type ChannelDock, type ChannelDock,
type ChannelMessageActionAdapter, type ChannelMessageActionAdapter,
@@ -200,12 +200,10 @@ export const googlechatPlugin: ChannelPlugin<ResolvedGoogleChatAccount> = {
collectWarnings: ({ account, cfg }) => { collectWarnings: ({ account, cfg }) => {
const warnings: string[] = []; const warnings: string[] = [];
const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy; const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy;
const { groupPolicy } = resolveRuntimeGroupPolicy({ const { groupPolicy } = resolveAllowlistProviderRuntimeGroupPolicy({
providerConfigPresent: cfg.channels?.googlechat !== undefined, providerConfigPresent: cfg.channels?.googlechat !== undefined,
groupPolicy: account.config.groupPolicy, groupPolicy: account.config.groupPolicy,
defaultGroupPolicy, defaultGroupPolicy,
configuredFallbackPolicy: "allowlist",
missingProviderFallbackPolicy: "allowlist",
}); });
if (groupPolicy === "open") { if (groupPolicy === "open") {
warnings.push( warnings.push(

View File

@@ -5,10 +5,11 @@ import {
readJsonBodyWithLimit, readJsonBodyWithLimit,
registerWebhookTarget, registerWebhookTarget,
rejectNonPostWebhookRequest, rejectNonPostWebhookRequest,
resolveRuntimeGroupPolicy, resolveAllowlistProviderRuntimeGroupPolicy,
resolveSingleWebhookTargetAsync, resolveSingleWebhookTargetAsync,
resolveWebhookPath, resolveWebhookPath,
resolveWebhookTargets, resolveWebhookTargets,
warnMissingProviderGroupPolicyFallbackOnce,
requestBodyErrorToText, requestBodyErrorToText,
resolveMentionGatingWithBypass, resolveMentionGatingWithBypass,
} from "openclaw/plugin-sdk"; } from "openclaw/plugin-sdk";
@@ -68,7 +69,6 @@ function logVerbose(core: GoogleChatCoreRuntime, runtime: GoogleChatRuntimeEnv,
} }
const warnedDeprecatedUsersEmailAllowFrom = new Set<string>(); const warnedDeprecatedUsersEmailAllowFrom = new Set<string>();
const warnedMissingProviderGroupPolicy = new Set<string>();
function warnDeprecatedUsersEmailEntries( function warnDeprecatedUsersEmailEntries(
core: GoogleChatCoreRuntime, core: GoogleChatCoreRuntime,
runtime: GoogleChatRuntimeEnv, runtime: GoogleChatRuntimeEnv,
@@ -429,21 +429,19 @@ async function processMessageWithPipeline(params: {
} }
const defaultGroupPolicy = config.channels?.defaults?.groupPolicy; const defaultGroupPolicy = config.channels?.defaults?.groupPolicy;
const { groupPolicy, providerMissingFallbackApplied } = resolveRuntimeGroupPolicy({ const { groupPolicy, providerMissingFallbackApplied } =
providerConfigPresent: config.channels?.googlechat !== undefined, resolveAllowlistProviderRuntimeGroupPolicy({
groupPolicy: account.config.groupPolicy, providerConfigPresent: config.channels?.googlechat !== undefined,
defaultGroupPolicy, groupPolicy: account.config.groupPolicy,
configuredFallbackPolicy: "allowlist", defaultGroupPolicy,
missingProviderFallbackPolicy: "allowlist", });
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({ const groupConfigResolved = resolveGroupConfig({
groupId: spaceId, groupId: spaceId,
groupName: space.displayName ?? null, groupName: space.displayName ?? null,

View File

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

View File

@@ -4,7 +4,7 @@ import {
formatPairingApproveHint, formatPairingApproveHint,
getChatChannelMeta, getChatChannelMeta,
PAIRING_APPROVED_MESSAGE, PAIRING_APPROVED_MESSAGE,
resolveRuntimeGroupPolicy, resolveAllowlistProviderRuntimeGroupPolicy,
setAccountEnabledInConfigSection, setAccountEnabledInConfigSection,
deleteAccountFromConfigSection, deleteAccountFromConfigSection,
type ChannelPlugin, type ChannelPlugin,
@@ -136,12 +136,10 @@ export const ircPlugin: ChannelPlugin<ResolvedIrcAccount, IrcProbe> = {
collectWarnings: ({ account, cfg }) => { collectWarnings: ({ account, cfg }) => {
const warnings: string[] = []; const warnings: string[] = [];
const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy; const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy;
const { groupPolicy } = resolveRuntimeGroupPolicy({ const { groupPolicy } = resolveAllowlistProviderRuntimeGroupPolicy({
providerConfigPresent: cfg.channels?.irc !== undefined, providerConfigPresent: cfg.channels?.irc !== undefined,
groupPolicy: account.config.groupPolicy, groupPolicy: account.config.groupPolicy,
defaultGroupPolicy, defaultGroupPolicy,
configuredFallbackPolicy: "allowlist",
missingProviderFallbackPolicy: "allowlist",
}); });
if (groupPolicy === "open") { if (groupPolicy === "open") {
warnings.push( warnings.push(

View File

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

View File

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

View File

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

View File

@@ -1,8 +1,9 @@
import { format } from "node:util"; import { format } from "node:util";
import { import {
mergeAllowlist, mergeAllowlist,
resolveRuntimeGroupPolicy, resolveAllowlistProviderRuntimeGroupPolicy,
summarizeMapping, summarizeMapping,
warnMissingProviderGroupPolicyFallbackOnce,
type RuntimeEnv, type RuntimeEnv,
} from "openclaw/plugin-sdk"; } from "openclaw/plugin-sdk";
import { resolveMatrixTargets } from "../../resolve-targets.js"; 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 mentionRegexes = core.channel.mentions.buildMentionRegexes(cfg);
const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy; const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy;
const { groupPolicy: groupPolicyRaw, providerMissingFallbackApplied } = resolveRuntimeGroupPolicy( const { groupPolicy: groupPolicyRaw, providerMissingFallbackApplied } =
{ resolveAllowlistProviderRuntimeGroupPolicy({
providerConfigPresent: cfg.channels?.matrix !== undefined, providerConfigPresent: cfg.channels?.matrix !== undefined,
groupPolicy: accountConfig.groupPolicy, groupPolicy: accountConfig.groupPolicy,
defaultGroupPolicy, defaultGroupPolicy,
configuredFallbackPolicy: "allowlist", });
missingProviderFallbackPolicy: "allowlist", warnMissingProviderGroupPolicyFallbackOnce({
}, providerMissingFallbackApplied,
); providerKey: "matrix",
if (providerMissingFallbackApplied) { accountId: account.accountId,
logVerboseMessage( blockedLabel: "room messages",
'matrix: channels.matrix is missing; defaulting groupPolicy to "allowlist" (room messages blocked until explicitly configured).', log: (message) => logVerboseMessage(message),
); });
}
const groupPolicy = allowlistOnly && groupPolicyRaw === "open" ? "allowlist" : groupPolicyRaw; const groupPolicy = allowlistOnly && groupPolicyRaw === "open" ? "allowlist" : groupPolicyRaw;
const replyToMode = opts.replyToMode ?? accountConfig.replyToMode ?? "off"; const replyToMode = opts.replyToMode ?? accountConfig.replyToMode ?? "off";
const threadReplies = accountConfig.threadReplies ?? "inbound"; const threadReplies = accountConfig.threadReplies ?? "inbound";

View File

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

View File

@@ -16,8 +16,9 @@ import {
DEFAULT_GROUP_HISTORY_LIMIT, DEFAULT_GROUP_HISTORY_LIMIT,
recordPendingHistoryEntryIfEnabled, recordPendingHistoryEntryIfEnabled,
resolveControlCommandGate, resolveControlCommandGate,
resolveRuntimeGroupPolicy, resolveAllowlistProviderRuntimeGroupPolicy,
resolveChannelMediaMaxBytes, resolveChannelMediaMaxBytes,
warnMissingProviderGroupPolicyFallbackOnce,
type HistoryEntry, type HistoryEntry,
} from "openclaw/plugin-sdk"; } from "openclaw/plugin-sdk";
import { getMattermostRuntime } from "../runtime.js"; import { getMattermostRuntime } from "../runtime.js";
@@ -244,18 +245,18 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {}
); );
const channelHistories = new Map<string, HistoryEntry[]>(); const channelHistories = new Map<string, HistoryEntry[]>();
const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy; const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy;
const { groupPolicy, providerMissingFallbackApplied } = resolveRuntimeGroupPolicy({ const { groupPolicy, providerMissingFallbackApplied } =
providerConfigPresent: cfg.channels?.mattermost !== undefined, resolveAllowlistProviderRuntimeGroupPolicy({
groupPolicy: account.config.groupPolicy, providerConfigPresent: cfg.channels?.mattermost !== undefined,
defaultGroupPolicy, groupPolicy: account.config.groupPolicy,
configuredFallbackPolicy: "allowlist", defaultGroupPolicy,
missingProviderFallbackPolicy: "allowlist", });
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 fetchWithAuth: FetchLike = (input, init) => {
const headers = new Headers(init?.headers); const headers = new Headers(init?.headers);

View File

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

View File

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

View File

@@ -2,7 +2,8 @@ import {
createReplyPrefixOptions, createReplyPrefixOptions,
logInboundDrop, logInboundDrop,
resolveControlCommandGate, resolveControlCommandGate,
resolveRuntimeGroupPolicy, resolveAllowlistProviderRuntimeGroupPolicy,
warnMissingProviderGroupPolicyFallbackOnce,
type OpenClawConfig, type OpenClawConfig,
type RuntimeEnv, type RuntimeEnv,
} from "openclaw/plugin-sdk"; } from "openclaw/plugin-sdk";
@@ -21,7 +22,6 @@ import { sendMessageNextcloudTalk } from "./send.js";
import type { CoreConfig, GroupPolicy, NextcloudTalkInboundMessage } from "./types.js"; import type { CoreConfig, GroupPolicy, NextcloudTalkInboundMessage } from "./types.js";
const CHANNEL_ID = "nextcloud-talk" as const; const CHANNEL_ID = "nextcloud-talk" as const;
const warnedMissingProviderGroupPolicy = new Set<string>();
async function deliverNextcloudTalkReply(params: { async function deliverNextcloudTalkReply(params: {
payload: { text?: string; mediaUrls?: string[]; mediaUrl?: string; replyToId?: string }; payload: { text?: string; mediaUrls?: string[]; mediaUrl?: string; replyToId?: string };
@@ -91,21 +91,21 @@ export async function handleNextcloudTalkInbound(params: {
| { groupPolicy?: string } | { groupPolicy?: string }
| undefined | undefined
)?.groupPolicy as GroupPolicy | undefined; )?.groupPolicy as GroupPolicy | undefined;
const { groupPolicy, providerMissingFallbackApplied } = resolveRuntimeGroupPolicy({ const { groupPolicy, providerMissingFallbackApplied } =
providerConfigPresent: resolveAllowlistProviderRuntimeGroupPolicy({
((config.channels as Record<string, unknown> | undefined)?.["nextcloud-talk"] ?? providerConfigPresent:
undefined) !== undefined, ((config.channels as Record<string, unknown> | undefined)?.["nextcloud-talk"] ??
groupPolicy: account.config.groupPolicy as GroupPolicy | undefined, undefined) !== undefined,
defaultGroupPolicy, groupPolicy: account.config.groupPolicy as GroupPolicy | undefined,
configuredFallbackPolicy: "allowlist", defaultGroupPolicy,
missingProviderFallbackPolicy: "allowlist", });
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 configAllowFrom = normalizeNextcloudTalkAllowlist(account.config.allowFrom);
const configGroupAllowFrom = normalizeNextcloudTalkAllowlist(account.config.groupAllowFrom); const configGroupAllowFrom = normalizeNextcloudTalkAllowlist(account.config.groupAllowFrom);

View File

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

View File

@@ -19,7 +19,7 @@ import {
resolveDefaultSlackAccountId, resolveDefaultSlackAccountId,
resolveSlackAccount, resolveSlackAccount,
resolveSlackReplyToMode, resolveSlackReplyToMode,
resolveRuntimeGroupPolicy, resolveOpenProviderRuntimeGroupPolicy,
resolveSlackGroupRequireMention, resolveSlackGroupRequireMention,
resolveSlackGroupToolPolicy, resolveSlackGroupToolPolicy,
buildSlackThreadingToolContext, buildSlackThreadingToolContext,
@@ -152,12 +152,10 @@ export const slackPlugin: ChannelPlugin<ResolvedSlackAccount> = {
collectWarnings: ({ account, cfg }) => { collectWarnings: ({ account, cfg }) => {
const warnings: string[] = []; const warnings: string[] = [];
const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy; const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy;
const { groupPolicy } = resolveRuntimeGroupPolicy({ const { groupPolicy } = resolveOpenProviderRuntimeGroupPolicy({
providerConfigPresent: cfg.channels?.slack !== undefined, providerConfigPresent: cfg.channels?.slack !== undefined,
groupPolicy: account.config.groupPolicy, groupPolicy: account.config.groupPolicy,
defaultGroupPolicy, defaultGroupPolicy,
configuredFallbackPolicy: "open",
missingProviderFallbackPolicy: "allowlist",
}); });
const channelAllowlistConfigured = const channelAllowlistConfigured =
Boolean(account.config.channels) && Object.keys(account.config.channels ?? {}).length > 0; Boolean(account.config.channels) && Object.keys(account.config.channels ?? {}).length > 0;

View File

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

View File

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

View File

@@ -3,9 +3,10 @@ import type { OpenClawConfig, MarkdownTableMode, RuntimeEnv } from "openclaw/plu
import { import {
createReplyPrefixOptions, createReplyPrefixOptions,
mergeAllowlist, mergeAllowlist,
resolveRuntimeGroupPolicy, resolveOpenProviderRuntimeGroupPolicy,
resolveSenderCommandAuthorization, resolveSenderCommandAuthorization,
summarizeMapping, summarizeMapping,
warnMissingProviderGroupPolicyFallbackOnce,
} from "openclaw/plugin-sdk"; } from "openclaw/plugin-sdk";
import { getZalouserRuntime } from "./runtime.js"; import { getZalouserRuntime } from "./runtime.js";
import { sendMessageZalouser } from "./send.js"; import { sendMessageZalouser } from "./send.js";
@@ -179,20 +180,17 @@ async function processMessage(
const chatId = threadId; const chatId = threadId;
const defaultGroupPolicy = config.channels?.defaults?.groupPolicy; const defaultGroupPolicy = config.channels?.defaults?.groupPolicy;
const { groupPolicy, providerMissingFallbackApplied } = resolveRuntimeGroupPolicy({ const { groupPolicy, providerMissingFallbackApplied } = resolveOpenProviderRuntimeGroupPolicy({
providerConfigPresent: config.channels?.zalouser !== undefined, providerConfigPresent: config.channels?.zalouser !== undefined,
groupPolicy: account.config.groupPolicy, groupPolicy: account.config.groupPolicy,
defaultGroupPolicy, defaultGroupPolicy,
configuredFallbackPolicy: "open",
missingProviderFallbackPolicy: "allowlist",
}); });
if (providerMissingFallbackApplied) { warnMissingProviderGroupPolicyFallbackOnce({
logVerbose( providerMissingFallbackApplied,
core, providerKey: "zalouser",
runtime, accountId: account.accountId,
'zalouser: channels.zalouser is missing; defaulting groupPolicy to "allowlist" (group messages blocked until explicitly configured).', log: (message) => logVerbose(core, runtime, message),
); });
}
const groups = account.config.groups ?? {}; const groups = account.config.groups ?? {};
if (isGroup) { if (isGroup) {
if (groupPolicy === "disabled") { if (groupPolicy === "disabled") {

View File

@@ -1,32 +1,85 @@
import { describe, expect, it } from "vitest"; 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", () => { describe("resolveRuntimeGroupPolicy", () => {
it("fails closed when provider config is missing and no defaults are set", () => { it.each([
const resolved = resolveRuntimeGroupPolicy({ {
providerConfigPresent: false, title: "fails closed when provider config is missing and no defaults are set",
}); params: { providerConfigPresent: false },
expect(resolved.groupPolicy).toBe("allowlist"); expectedPolicy: "allowlist",
expect(resolved.providerMissingFallbackApplied).toBe(true); 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", () => { describe("resolveOpenProviderRuntimeGroupPolicy", () => {
const resolved = resolveRuntimeGroupPolicy({ it("uses open fallback when provider config exists", () => {
const resolved = resolveOpenProviderRuntimeGroupPolicy({
providerConfigPresent: true, providerConfigPresent: true,
configuredFallbackPolicy: "open",
}); });
expect(resolved.groupPolicy).toBe("open"); expect(resolved.groupPolicy).toBe("open");
expect(resolved.providerMissingFallbackApplied).toBe(false); expect(resolved.providerMissingFallbackApplied).toBe(false);
}); });
});
it("ignores global defaults when provider config is missing", () => { describe("resolveAllowlistProviderRuntimeGroupPolicy", () => {
const resolved = resolveRuntimeGroupPolicy({ it("uses allowlist fallback when provider config exists", () => {
providerConfigPresent: false, const resolved = resolveAllowlistProviderRuntimeGroupPolicy({
defaultGroupPolicy: "disabled", providerConfigPresent: true,
configuredFallbackPolicy: "open",
missingProviderFallbackPolicy: "allowlist",
}); });
expect(resolved.groupPolicy).toBe("allowlist"); 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");
}); });
}); });

View File

@@ -5,13 +5,17 @@ export type RuntimeGroupPolicyResolution = {
providerMissingFallbackApplied: boolean; providerMissingFallbackApplied: boolean;
}; };
export function resolveRuntimeGroupPolicy(params: { export type RuntimeGroupPolicyParams = {
providerConfigPresent: boolean; providerConfigPresent: boolean;
groupPolicy?: GroupPolicy; groupPolicy?: GroupPolicy;
defaultGroupPolicy?: GroupPolicy; defaultGroupPolicy?: GroupPolicy;
configuredFallbackPolicy?: GroupPolicy; configuredFallbackPolicy?: GroupPolicy;
missingProviderFallbackPolicy?: GroupPolicy; missingProviderFallbackPolicy?: GroupPolicy;
}): RuntimeGroupPolicyResolution { };
export function resolveRuntimeGroupPolicy(
params: RuntimeGroupPolicyParams,
): RuntimeGroupPolicyResolution {
const configuredFallbackPolicy = params.configuredFallbackPolicy ?? "open"; const configuredFallbackPolicy = params.configuredFallbackPolicy ?? "open";
const missingProviderFallbackPolicy = params.missingProviderFallbackPolicy ?? "allowlist"; const missingProviderFallbackPolicy = params.missingProviderFallbackPolicy ?? "allowlist";
const groupPolicy = params.providerConfigPresent const groupPolicy = params.providerConfigPresent
@@ -21,3 +25,67 @@ export function resolveRuntimeGroupPolicy(params: {
!params.providerConfigPresent && params.groupPolicy === undefined; !params.providerConfigPresent && params.groupPolicy === undefined;
return { groupPolicy, providerMissingFallbackApplied }; 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<string>();
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;
}

View File

@@ -4,7 +4,7 @@ import {
createInboundDebouncer, createInboundDebouncer,
resolveInboundDebounceMs, resolveInboundDebounceMs,
} from "../../auto-reply/inbound-debounce.js"; } 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 { danger } from "../../globals.js";
import type { DiscordMessageEvent, DiscordMessageHandler } from "./listeners.js"; import type { DiscordMessageEvent, DiscordMessageHandler } from "./listeners.js";
import { preflightDiscordMessage } from "./message-handler.preflight.js"; import { preflightDiscordMessage } from "./message-handler.preflight.js";
@@ -24,12 +24,10 @@ type DiscordMessageHandlerParams = Omit<
export function createDiscordMessageHandler( export function createDiscordMessageHandler(
params: DiscordMessageHandlerParams, params: DiscordMessageHandlerParams,
): DiscordMessageHandler { ): DiscordMessageHandler {
const { groupPolicy } = resolveRuntimeGroupPolicy({ const { groupPolicy } = resolveOpenProviderRuntimeGroupPolicy({
providerConfigPresent: params.cfg.channels?.discord !== undefined, providerConfigPresent: params.cfg.channels?.discord !== undefined,
groupPolicy: params.discordConfig?.groupPolicy, groupPolicy: params.discordConfig?.groupPolicy,
defaultGroupPolicy: params.cfg.channels?.defaults?.groupPolicy, defaultGroupPolicy: params.cfg.channels?.defaults?.groupPolicy,
configuredFallbackPolicy: "open",
missingProviderFallbackPolicy: "allowlist",
}); });
const ackReactionScope = params.cfg.messages?.ackReactionScope ?? "group-mentions"; const ackReactionScope = params.cfg.messages?.ackReactionScope ?? "group-mentions";
const debounceMs = resolveInboundDebounceMs({ cfg: params.cfg, channel: "discord" }); const debounceMs = resolveInboundDebounceMs({ cfg: params.cfg, channel: "discord" });

View File

@@ -39,7 +39,7 @@ import type { ReplyPayload } from "../../auto-reply/types.js";
import { resolveCommandAuthorizedFromAuthorizers } from "../../channels/command-gating.js"; import { resolveCommandAuthorizedFromAuthorizers } from "../../channels/command-gating.js";
import { createReplyPrefixOptions } from "../../channels/reply-prefix.js"; import { createReplyPrefixOptions } from "../../channels/reply-prefix.js";
import type { OpenClawConfig, loadConfig } from "../../config/config.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 { loadSessionStore, resolveStorePath } from "../../config/sessions.js";
import { logVerbose } from "../../globals.js"; import { logVerbose } from "../../globals.js";
import { createSubsystemLogger } from "../../logging/subsystem.js"; import { createSubsystemLogger } from "../../logging/subsystem.js";
@@ -1330,12 +1330,10 @@ async function dispatchDiscordCommandInteraction(params: {
const channelAllowlistConfigured = const channelAllowlistConfigured =
Boolean(guildInfo?.channels) && Object.keys(guildInfo?.channels ?? {}).length > 0; Boolean(guildInfo?.channels) && Object.keys(guildInfo?.channels ?? {}).length > 0;
const channelAllowed = channelConfig?.allowed !== false; const channelAllowed = channelConfig?.allowed !== false;
const { groupPolicy } = resolveRuntimeGroupPolicy({ const { groupPolicy } = resolveOpenProviderRuntimeGroupPolicy({
providerConfigPresent: cfg.channels?.discord !== undefined, providerConfigPresent: cfg.channels?.discord !== undefined,
groupPolicy: discordConfig?.groupPolicy, groupPolicy: discordConfig?.groupPolicy,
defaultGroupPolicy: cfg.channels?.defaults?.groupPolicy, defaultGroupPolicy: cfg.channels?.defaults?.groupPolicy,
configuredFallbackPolicy: "open",
missingProviderFallbackPolicy: "allowlist",
}); });
const allowByPolicy = isDiscordGroupAllowedByPolicy({ const allowByPolicy = isDiscordGroupAllowedByPolicy({
groupPolicy, groupPolicy,

View File

@@ -21,8 +21,10 @@ import {
} from "../../config/commands.js"; } from "../../config/commands.js";
import type { OpenClawConfig, ReplyToMode } from "../../config/config.js"; import type { OpenClawConfig, ReplyToMode } from "../../config/config.js";
import { loadConfig } from "../../config/config.js"; import { loadConfig } from "../../config/config.js";
import { resolveRuntimeGroupPolicy } from "../../config/runtime-group-policy.js"; import {
import type { GroupPolicy } from "../../config/types.base.js"; resolveOpenProviderRuntimeGroupPolicy,
warnMissingProviderGroupPolicyFallbackOnce,
} from "../../config/runtime-group-policy.js";
import { danger, logVerbose, shouldLogVerbose, warn } from "../../globals.js"; import { danger, logVerbose, shouldLogVerbose, warn } from "../../globals.js";
import { formatErrorMessage } from "../../infra/errors.js"; import { formatErrorMessage } from "../../infra/errors.js";
import { createDiscordRetryRunner } from "../../infra/retry-policy.js"; import { createDiscordRetryRunner } from "../../infra/retry-policy.js";
@@ -172,23 +174,6 @@ function dedupeSkillCommandsForDiscord(
return deduped; 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: { async function deployDiscordCommands(params: {
client: Client; client: Client;
runtime: RuntimeEnv; runtime: RuntimeEnv;
@@ -273,20 +258,20 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
let guildEntries = rawDiscordCfg.guilds; let guildEntries = rawDiscordCfg.guilds;
const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy; const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy;
const providerConfigPresent = cfg.channels?.discord !== undefined; const providerConfigPresent = cfg.channels?.discord !== undefined;
const { groupPolicy, providerMissingFallbackApplied } = resolveDiscordRuntimeGroupPolicy({ const { groupPolicy, providerMissingFallbackApplied } = resolveOpenProviderRuntimeGroupPolicy({
providerConfigPresent, providerConfigPresent,
groupPolicy: rawDiscordCfg.groupPolicy, groupPolicy: rawDiscordCfg.groupPolicy,
defaultGroupPolicy, defaultGroupPolicy,
}); });
const discordCfg = const discordCfg =
rawDiscordCfg.groupPolicy === groupPolicy ? rawDiscordCfg : { ...rawDiscordCfg, groupPolicy }; rawDiscordCfg.groupPolicy === groupPolicy ? rawDiscordCfg : { ...rawDiscordCfg, groupPolicy };
if (providerMissingFallbackApplied) { warnMissingProviderGroupPolicyFallbackOnce({
runtime.log?.( providerMissingFallbackApplied,
warn( providerKey: "discord",
'discord: channels.discord is missing; defaulting groupPolicy to "allowlist" (guild messages blocked until explicitly configured).', accountId: account.accountId,
), blockedLabel: "guild messages",
); log: (message) => runtime.log?.(warn(message)),
} });
let allowFrom = discordCfg.allowFrom ?? dmConfig?.allowFrom; let allowFrom = discordCfg.allowFrom ?? dmConfig?.allowFrom;
const mediaMaxBytes = (opts.mediaMaxMb ?? discordCfg.mediaMaxMb ?? 8) * 1024 * 1024; const mediaMaxBytes = (opts.mediaMaxMb ?? discordCfg.mediaMaxMb ?? 8) * 1024 * 1024;
const textLimit = resolveTextChunkLimit(cfg, "discord", account.accountId, { const textLimit = resolveTextChunkLimit(cfg, "discord", account.accountId, {
@@ -643,7 +628,7 @@ async function clearDiscordNativeCommands(params: {
export const __testing = { export const __testing = {
createDiscordGatewayPlugin, createDiscordGatewayPlugin,
dedupeSkillCommandsForDiscord, dedupeSkillCommandsForDiscord,
resolveDiscordRuntimeGroupPolicy, resolveDiscordRuntimeGroupPolicy: resolveOpenProviderRuntimeGroupPolicy,
resolveDiscordRestFetch, resolveDiscordRestFetch,
resolveThreadBindingsEnabled, resolveThreadBindingsEnabled,
}; };

View File

@@ -16,9 +16,11 @@ import { createReplyDispatcher } from "../../auto-reply/reply/reply-dispatcher.j
import { createReplyPrefixOptions } from "../../channels/reply-prefix.js"; import { createReplyPrefixOptions } from "../../channels/reply-prefix.js";
import { recordInboundSession } from "../../channels/session.js"; import { recordInboundSession } from "../../channels/session.js";
import { loadConfig } from "../../config/config.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 { readSessionUpdatedAt, resolveStorePath } from "../../config/sessions.js";
import type { GroupPolicy } from "../../config/types.base.js";
import { danger, logVerbose, shouldLogVerbose, warn } from "../../globals.js"; import { danger, logVerbose, shouldLogVerbose, warn } from "../../globals.js";
import { normalizeScpRemoteHost } from "../../infra/scp-host.js"; import { normalizeScpRemoteHost } from "../../infra/scp-host.js";
import { waitForTransportReady } from "../../infra/transport-ready.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<void> { export async function monitorIMessageProvider(opts: MonitorIMessageOpts = {}): Promise<void> {
const runtime = resolveRuntime(opts); const runtime = resolveRuntime(opts);
const cfg = opts.config ?? loadConfig(); const cfg = opts.config ?? loadConfig();
@@ -163,18 +148,17 @@ export async function monitorIMessageProvider(opts: MonitorIMessageOpts = {}): P
(imessageCfg.allowFrom && imessageCfg.allowFrom.length > 0 ? imessageCfg.allowFrom : []), (imessageCfg.allowFrom && imessageCfg.allowFrom.length > 0 ? imessageCfg.allowFrom : []),
); );
const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy; const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy;
const { groupPolicy, providerMissingFallbackApplied } = resolveIMessageRuntimeGroupPolicy({ const { groupPolicy, providerMissingFallbackApplied } = resolveOpenProviderRuntimeGroupPolicy({
providerConfigPresent: cfg.channels?.imessage !== undefined, providerConfigPresent: cfg.channels?.imessage !== undefined,
groupPolicy: imessageCfg.groupPolicy, groupPolicy: imessageCfg.groupPolicy,
defaultGroupPolicy, defaultGroupPolicy,
}); });
if (providerMissingFallbackApplied) { warnMissingProviderGroupPolicyFallbackOnce({
runtime.log?.( providerMissingFallbackApplied,
warn( providerKey: "imessage",
'imessage: channels.imessage is missing; defaulting groupPolicy to "allowlist" (group messages blocked until explicitly configured).', accountId: accountInfo.accountId,
), log: (message) => runtime.log?.(warn(message)),
); });
}
const dmPolicy = imessageCfg.dmPolicy ?? "pairing"; const dmPolicy = imessageCfg.dmPolicy ?? "pairing";
const includeAttachments = opts.includeAttachments ?? imessageCfg.includeAttachments ?? false; const includeAttachments = opts.includeAttachments ?? imessageCfg.includeAttachments ?? false;
const mediaMaxBytes = (opts.mediaMaxMb ?? imessageCfg.mediaMaxMb ?? 16) * 1024 * 1024; const mediaMaxBytes = (opts.mediaMaxMb ?? imessageCfg.mediaMaxMb ?? 16) * 1024 * 1024;
@@ -540,5 +524,5 @@ export async function monitorIMessageProvider(opts: MonitorIMessageOpts = {}): P
} }
export const __testing = { export const __testing = {
resolveIMessageRuntimeGroupPolicy, resolveIMessageRuntimeGroupPolicy: resolveOpenProviderRuntimeGroupPolicy,
}; };

View File

@@ -8,7 +8,10 @@ import type {
PostbackEvent, PostbackEvent,
} from "@line/bot-sdk"; } from "@line/bot-sdk";
import type { OpenClawConfig } from "../config/config.js"; 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 { danger, logVerbose } from "../globals.js";
import { resolvePairingIdLabel } from "../pairing/pairing-labels.js"; import { resolvePairingIdLabel } from "../pairing/pairing-labels.js";
import { buildPairingReply } from "../pairing/pairing-messages.js"; import { buildPairingReply } from "../pairing/pairing-messages.js";
@@ -41,8 +44,6 @@ export interface LineHandlerContext {
processMessage: (ctx: LineInboundContext) => Promise<void>; processMessage: (ctx: LineInboundContext) => Promise<void>;
} }
let lineGroupPolicyFallbackWarned = false;
function resolveLineGroupConfig(params: { function resolveLineGroupConfig(params: {
config: ResolvedLineAccount["config"]; config: ResolvedLineAccount["config"];
groupId?: string; groupId?: string;
@@ -136,19 +137,18 @@ async function shouldProcessLineEvent(
dmPolicy, dmPolicy,
}); });
const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy; const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy;
const { groupPolicy, providerMissingFallbackApplied } = resolveRuntimeGroupPolicy({ const { groupPolicy, providerMissingFallbackApplied } =
providerConfigPresent: cfg.channels?.line !== undefined, resolveAllowlistProviderRuntimeGroupPolicy({
groupPolicy: account.config.groupPolicy, providerConfigPresent: cfg.channels?.line !== undefined,
defaultGroupPolicy, groupPolicy: account.config.groupPolicy,
configuredFallbackPolicy: "allowlist", defaultGroupPolicy,
missingProviderFallbackPolicy: "allowlist", });
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 (isGroup) {
if (groupConfig?.enabled === false) { if (groupConfig?.enabled === false) {

View File

@@ -133,8 +133,13 @@ export type {
MSTeamsTeamConfig, MSTeamsTeamConfig,
} from "../config/types.js"; } from "../config/types.js";
export { export {
resolveAllowlistProviderRuntimeGroupPolicy,
resolveOpenProviderRuntimeGroupPolicy,
resolveRuntimeGroupPolicy, resolveRuntimeGroupPolicy,
type RuntimeGroupPolicyResolution, type RuntimeGroupPolicyResolution,
type RuntimeGroupPolicyParams,
type ResolveProviderRuntimeGroupPolicyParams,
warnMissingProviderGroupPolicyFallbackOnce,
} from "../config/runtime-group-policy.js"; } from "../config/runtime-group-policy.js";
export { export {
DiscordConfigSchema, DiscordConfigSchema,

View File

@@ -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 { ReplyPayload } from "../auto-reply/types.js";
import type { OpenClawConfig } from "../config/config.js"; import type { OpenClawConfig } from "../config/config.js";
import { loadConfig } 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 type { SignalReactionNotificationMode } from "../config/types.js";
import { waitForTransportReady } from "../infra/transport-ready.js"; import { waitForTransportReady } from "../infra/transport-ready.js";
import { saveMediaBuffer } from "../media/store.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 defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy;
const { groupPolicy, providerMissingFallbackApplied } = resolveRuntimeGroupPolicy({ const { groupPolicy, providerMissingFallbackApplied } =
providerConfigPresent: cfg.channels?.signal !== undefined, resolveAllowlistProviderRuntimeGroupPolicy({
groupPolicy: accountInfo.config.groupPolicy, providerConfigPresent: cfg.channels?.signal !== undefined,
defaultGroupPolicy, groupPolicy: accountInfo.config.groupPolicy,
configuredFallbackPolicy: "allowlist", defaultGroupPolicy,
missingProviderFallbackPolicy: "allowlist", });
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 reactionMode = accountInfo.config.reactionNotifications ?? "own";
const reactionAllowlist = normalizeAllowList(accountInfo.config.reactionAllowlist); const reactionAllowlist = normalizeAllowList(accountInfo.config.reactionAllowlist);
const mediaMaxBytes = (opts.mediaMaxMb ?? accountInfo.config.mediaMaxMb ?? 8) * 1024 * 1024; const mediaMaxBytes = (opts.mediaMaxMb ?? accountInfo.config.mediaMaxMb ?? 8) * 1024 * 1024;

View File

@@ -10,9 +10,11 @@ import {
summarizeMapping, summarizeMapping,
} from "../../channels/allowlists/resolve-utils.js"; } from "../../channels/allowlists/resolve-utils.js";
import { loadConfig } from "../../config/config.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 { SessionScope } from "../../config/sessions.js";
import type { GroupPolicy } from "../../config/types.base.js";
import { warn } from "../../globals.js"; import { warn } from "../../globals.js";
import { installRequestBodyLimitGuard } from "../../infra/http-body.js"; import { installRequestBodyLimitGuard } from "../../infra/http-body.js";
import { normalizeMainKey } from "../../routing/session-key.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_MAX_BODY_BYTES = 1024 * 1024;
const SLACK_WEBHOOK_BODY_TIMEOUT_MS = 30_000; 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) { function parseApiAppIdFromAppToken(raw?: string) {
const token = raw?.trim(); const token = raw?.trim();
if (!token) { if (!token) {
@@ -119,18 +104,17 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) {
let channelsConfig = slackCfg.channels; let channelsConfig = slackCfg.channels;
const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy; const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy;
const providerConfigPresent = cfg.channels?.slack !== undefined; const providerConfigPresent = cfg.channels?.slack !== undefined;
const { groupPolicy, providerMissingFallbackApplied } = resolveSlackRuntimeGroupPolicy({ const { groupPolicy, providerMissingFallbackApplied } = resolveOpenProviderRuntimeGroupPolicy({
providerConfigPresent, providerConfigPresent,
groupPolicy: slackCfg.groupPolicy, groupPolicy: slackCfg.groupPolicy,
defaultGroupPolicy, defaultGroupPolicy,
}); });
if (providerMissingFallbackApplied) { warnMissingProviderGroupPolicyFallbackOnce({
runtime.log?.( providerMissingFallbackApplied,
warn( providerKey: "slack",
'slack: channels.slack is missing; defaulting groupPolicy to "allowlist" (group messages blocked until explicitly configured).', accountId: account.accountId,
), log: (message) => runtime.log?.(warn(message)),
); });
}
const resolveToken = slackCfg.userToken?.trim() || botToken; const resolveToken = slackCfg.userToken?.trim() || botToken;
const useAccessGroups = cfg.commands?.useAccessGroups !== false; const useAccessGroups = cfg.commands?.useAccessGroups !== false;
@@ -384,5 +368,5 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) {
} }
export const __testing = { export const __testing = {
resolveSlackRuntimeGroupPolicy, resolveSlackRuntimeGroupPolicy: resolveOpenProviderRuntimeGroupPolicy,
}; };

View File

@@ -1,6 +1,6 @@
import type { OpenClawConfig } from "../config/config.js"; import type { OpenClawConfig } from "../config/config.js";
import type { ChannelGroupPolicy } from "../config/group-policy.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 { import type {
TelegramAccountConfig, TelegramAccountConfig,
TelegramGroupConfig, TelegramGroupConfig,
@@ -78,12 +78,10 @@ export const resolveTelegramRuntimeGroupPolicy = (params: {
groupPolicy?: TelegramAccountConfig["groupPolicy"]; groupPolicy?: TelegramAccountConfig["groupPolicy"];
defaultGroupPolicy?: TelegramAccountConfig["groupPolicy"]; defaultGroupPolicy?: TelegramAccountConfig["groupPolicy"];
}) => }) =>
resolveRuntimeGroupPolicy({ resolveOpenProviderRuntimeGroupPolicy({
providerConfigPresent: params.providerConfigPresent, providerConfigPresent: params.providerConfigPresent,
groupPolicy: params.groupPolicy, groupPolicy: params.groupPolicy,
defaultGroupPolicy: params.defaultGroupPolicy, defaultGroupPolicy: params.defaultGroupPolicy,
configuredFallbackPolicy: "open",
missingProviderFallbackPolicy: "allowlist",
}); });
export const evaluateTelegramGroupPolicyAccess = (params: { export const evaluateTelegramGroupPolicyAccess = (params: {

View File

@@ -1,5 +1,8 @@
import { loadConfig } from "../../config/config.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 { logVerbose } from "../../globals.js"; import { logVerbose } from "../../globals.js";
import { buildPairingReply } from "../../pairing/pairing-messages.js"; import { buildPairingReply } from "../../pairing/pairing-messages.js";
import { import {
@@ -26,12 +29,10 @@ function resolveWhatsAppRuntimeGroupPolicy(params: {
groupPolicy: "open" | "allowlist" | "disabled"; groupPolicy: "open" | "allowlist" | "disabled";
providerMissingFallbackApplied: boolean; providerMissingFallbackApplied: boolean;
} { } {
return resolveRuntimeGroupPolicy({ return resolveOpenProviderRuntimeGroupPolicy({
providerConfigPresent: params.providerConfigPresent, providerConfigPresent: params.providerConfigPresent,
groupPolicy: params.groupPolicy, groupPolicy: params.groupPolicy,
defaultGroupPolicy: params.defaultGroupPolicy, defaultGroupPolicy: params.defaultGroupPolicy,
configuredFallbackPolicy: "open",
missingProviderFallbackPolicy: "allowlist",
}); });
} }
@@ -105,11 +106,12 @@ export async function checkInboundAccessControl(params: {
groupPolicy: account.groupPolicy, groupPolicy: account.groupPolicy,
defaultGroupPolicy, defaultGroupPolicy,
}); });
if (providerMissingFallbackApplied) { warnMissingProviderGroupPolicyFallbackOnce({
logVerbose( providerMissingFallbackApplied,
'whatsapp: channels.whatsapp is missing; defaulting groupPolicy to "allowlist" (group messages blocked until explicitly configured).', providerKey: "whatsapp",
); accountId: account.accountId,
} log: (message) => logVerbose(message),
});
if (params.group && groupPolicy === "disabled") { if (params.group && groupPolicy === "disabled") {
logVerbose("Blocked group message (groupPolicy: disabled)"); logVerbose("Blocked group message (groupPolicy: disabled)");
return { return {