From 842e91d019f5bf13c7f5d64234847c3b49caf69d Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 12 Jan 2026 08:21:50 +0000 Subject: [PATCH] fix: default groupPolicy to allowlist --- CHANGELOG.md | 1 + docs/concepts/group-messages.md | 4 +- docs/concepts/groups.md | 5 ++- docs/gateway/configuration-examples.md | 6 ++- docs/gateway/configuration.md | 3 +- docs/providers/discord.md | 9 +++- docs/providers/imessage.md | 2 +- docs/providers/signal.md | 2 +- docs/providers/slack.md | 2 +- docs/providers/telegram.md | 7 +-- docs/providers/whatsapp.md | 2 +- docs/start/faq.md | 2 +- src/config/config.test.ts | 60 ++++++++++++++++++++++++++ src/config/types.ts | 12 +++--- src/config/zod-schema.ts | 18 ++++---- src/discord/monitor.ts | 6 +-- src/imessage/monitor.ts | 2 +- src/providers/plugins/discord.ts | 15 +++++++ src/providers/plugins/imessage.ts | 7 +++ src/providers/plugins/signal.ts | 7 +++ src/providers/plugins/slack.ts | 15 +++++++ src/providers/plugins/telegram.ts | 11 +++-- src/providers/plugins/whatsapp.ts | 14 ++++++ src/signal/monitor.ts | 2 +- src/slack/monitor.ts | 2 +- src/telegram/bot.test.ts | 4 +- src/telegram/bot.ts | 6 +-- src/web/inbound.ts | 4 +- 28 files changed, 183 insertions(+), 47 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 76dae4dde..b93d9fba4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -64,6 +64,7 @@ - Agents/Browser: cap Playwright AI snapshots for tool calls (maxChars); CLI snapshots remain full. (#763) — thanks @thesash. - Models: normalize Gemini 3 Pro/Flash IDs to preview names for live model lookups. (#769) — thanks @steipete. - CLI: fix guardCancel typing for configure prompts. (#769) — thanks @steipete. +- Providers: default groupPolicy to allowlist across providers and warn in doctor when groups are open. - Gateway/WebChat: include handshake validation details in the WebSocket close reason for easier debugging; preserve close codes. - Gateway/Auth: send invalid connect responses before closing the handshake; stabilize invalid-connect auth test. - Gateway: tighten gateway listener detection. diff --git a/docs/concepts/group-messages.md b/docs/concepts/group-messages.md index 4b926c22b..809aaf24b 100644 --- a/docs/concepts/group-messages.md +++ b/docs/concepts/group-messages.md @@ -11,7 +11,7 @@ Note: `agents.list[].groupChat.mentionPatterns` is now used by Telegram/Discord/ ## What’s implemented (2025-12-03) - Activation modes: `mention` (default) or `always`. `mention` requires a ping (real WhatsApp @-mentions via `mentionedJids`, regex patterns, or the bot’s E.164 anywhere in the text). `always` wakes the agent on every message but it should reply only when it can add meaningful value; otherwise it returns the silent token `NO_REPLY`. Defaults can be set in config (`whatsapp.groups`) and overridden per group via `/activation`. When `whatsapp.groups` is set, it also acts as a group allowlist (include `"*"` to allow all). -- Group policy: `whatsapp.groupPolicy` controls whether group messages are accepted (`open|disabled|allowlist`). `allowlist` uses `whatsapp.groupAllowFrom` (fallback: explicit `whatsapp.allowFrom`). +- Group policy: `whatsapp.groupPolicy` controls whether group messages are accepted (`open|disabled|allowlist`). `allowlist` uses `whatsapp.groupAllowFrom` (fallback: explicit `whatsapp.allowFrom`). Default is `allowlist` (blocked until you add senders). - Per-group sessions: session keys look like `agent::whatsapp:group:` so commands such as `/verbose on` or `/think high` (sent as standalone messages) are scoped to that group; personal DM state is untouched. Heartbeats are skipped for group threads. - Context injection: last N (default 50) group messages are prefixed under `[Chat messages since your last reply - for context]`, with the triggering line under `[Current message - respond to this]`. - Sender surfacing: every group batch now ends with `[from: Sender Name (+E164)]` so Pi knows who is speaking. @@ -61,7 +61,7 @@ Only the owner number (from `whatsapp.allowFrom`, or the bot’s own E.164 when ## How to use 1) Add Clawd UK (`+447700900123`) to the group. -2) Say `@clawd …` (or `@clawd uk`, `@clawdbot`, or include the number). Anyone in the group can trigger it. +2) Say `@clawd …` (or `@clawd uk`, `@clawdbot`, or include the number). Only allowlisted senders can trigger it unless you set `groupPolicy: "open"`. 3) The agent prompt will include recent group context plus the trailing `[from: …]` marker so it can address the right person. 4) Session-level directives (`/verbose on`, `/think high`, `/new` or `/reset`, `/compact`) apply only to that group’s session; send them as standalone messages so they register. Your personal DM session remains independent. diff --git a/docs/concepts/groups.md b/docs/concepts/groups.md index 77af2bbb5..80d71b0de 100644 --- a/docs/concepts/groups.md +++ b/docs/concepts/groups.md @@ -12,7 +12,7 @@ Clawdbot “lives” on your own messaging accounts. There is no separate WhatsA If **you** are in a group, Clawdbot can see that group and respond there. Default behavior: -- Groups are allowed (`groupPolicy: "open"`). +- Groups are restricted (`groupPolicy: "allowlist"`). - Replies require a mention unless you explicitly disable mention gating. Translation: anyone in the group can trigger Clawdbot by mentioning it. @@ -86,7 +86,7 @@ Control how group/room messages are handled per provider: | Policy | Behavior | |--------|----------| -| `"open"` | Default. Groups bypass allowlists; mention-gating still applies. | +| `"open"` | Groups bypass allowlists; mention-gating still applies. | | `"disabled"` | Block all group messages entirely. | | `"allowlist"` | Only allow groups/rooms that match the configured allowlist. | @@ -97,6 +97,7 @@ Notes: - Slack: allowlist uses `slack.channels`. - Group DMs are controlled separately (`discord.dm.*`, `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. Quick mental model (evaluation order for group messages): 1) `groupPolicy` (open/disabled/allowlist) diff --git a/docs/gateway/configuration-examples.md b/docs/gateway/configuration-examples.md index e86e3a41d..3c39c0bbd 100644 --- a/docs/gateway/configuration-examples.md +++ b/docs/gateway/configuration-examples.md @@ -150,7 +150,8 @@ Save to `~/.clawdbot/clawdbot.json` and you can DM the bot from that number. whatsapp: { dmPolicy: "pairing", allowFrom: ["+15555550123"], - groupPolicy: "open", + groupPolicy: "allowlist", + groupAllowFrom: ["+15555550123"], groups: { "*": { requireMention: true } } }, @@ -158,7 +159,8 @@ Save to `~/.clawdbot/clawdbot.json` and you can DM the bot from that number. enabled: true, botToken: "YOUR_TELEGRAM_BOT_TOKEN", allowFrom: ["123456789"], - groupPolicy: "open", + groupPolicy: "allowlist", + groupAllowFrom: ["123456789"], groups: { "*": { requireMention: true } } }, diff --git a/docs/gateway/configuration.md b/docs/gateway/configuration.md index d1bb09595..21046af13 100644 --- a/docs/gateway/configuration.md +++ b/docs/gateway/configuration.md @@ -545,12 +545,13 @@ Use `*.groupPolicy` to control whether group/room messages are accepted at all: ``` Notes: -- `"open"` (default): groups bypass allowlists; mention-gating still applies. +- `"open"`: groups bypass allowlists; mention-gating still applies. - `"disabled"`: block all group/room messages. - `"allowlist"`: only allow groups/rooms that match the configured allowlist. - WhatsApp/Telegram/Signal/iMessage use `groupAllowFrom` (fallback: explicit `allowFrom`). - Discord/Slack use channel allowlists (`discord.guilds.*.channels`, `slack.channels`). - Group DMs (Discord/Slack) are still controlled by `dm.groupEnabled` + `dm.groupChannels`. +- Default is `groupPolicy: "allowlist"`; if no allowlist is configured, group messages are blocked. ### Multi-agent routing (`agents.list` + `bindings`) diff --git a/docs/providers/discord.md b/docs/providers/discord.md index dbd0c2608..b24ada73e 100644 --- a/docs/providers/discord.md +++ b/docs/providers/discord.md @@ -193,7 +193,14 @@ Outbound Discord API calls retry on rate limits (429) using Discord `retry_after discord: { enabled: true, token: "abc.123", - groupPolicy: "open", + groupPolicy: "allowlist", + guilds: { + "*": { + channels: { + general: { allow: true } + } + } + }, mediaMaxMb: 8, actions: { reactions: true, diff --git a/docs/providers/imessage.md b/docs/providers/imessage.md index b3a3d6965..c68465047 100644 --- a/docs/providers/imessage.md +++ b/docs/providers/imessage.md @@ -170,7 +170,7 @@ Provider options: - `imessage.region`: SMS region. - `imessage.dmPolicy`: `pairing | allowlist | open | disabled` (default: pairing). - `imessage.allowFrom`: DM allowlist (handles or `chat_id:*`). `open` requires `"*"`. -- `imessage.groupPolicy`: `open | allowlist | disabled` (default: open). +- `imessage.groupPolicy`: `open | allowlist | disabled` (default: allowlist). - `imessage.groupAllowFrom`: group sender allowlist. - `imessage.historyLimit` / `imessage.accounts.*.historyLimit`: max group messages to include as context (0 disables). - `imessage.groups`: per-group defaults + allowlist (use `"*"` for global defaults). diff --git a/docs/providers/signal.md b/docs/providers/signal.md index d6ca23eaa..3cea4225b 100644 --- a/docs/providers/signal.md +++ b/docs/providers/signal.md @@ -107,7 +107,7 @@ Provider options: - `signal.sendReadReceipts`: forward read receipts. - `signal.dmPolicy`: `pairing | allowlist | open | disabled` (default: pairing). - `signal.allowFrom`: DM allowlist (E.164 or `uuid:`). `open` requires `"*"`. -- `signal.groupPolicy`: `open | allowlist | disabled` (default: open). +- `signal.groupPolicy`: `open | allowlist | disabled` (default: allowlist). - `signal.groupAllowFrom`: group sender allowlist. - `signal.historyLimit`: max group messages to include as context (0 disables). - `signal.textChunkLimit`: outbound chunk size (chars). diff --git a/docs/providers/slack.md b/docs/providers/slack.md index 7537e6da3..80e16b14b 100644 --- a/docs/providers/slack.md +++ b/docs/providers/slack.md @@ -185,7 +185,7 @@ Slack uses Socket Mode only (no HTTP webhook server). Provide both tokens: "enabled": true, "botToken": "xoxb-...", "appToken": "xapp-...", - "groupPolicy": "open", + "groupPolicy": "allowlist", "dm": { "enabled": true, "policy": "pairing", diff --git a/docs/providers/telegram.md b/docs/providers/telegram.md index 2d2b6873e..a6449e866 100644 --- a/docs/providers/telegram.md +++ b/docs/providers/telegram.md @@ -186,11 +186,12 @@ Two independent controls: - Example: `"groups": { "-1001234567890": {}, "*": {} }` allows all groups **2. Which senders are allowed** (sender filtering via `telegram.groupPolicy`): -- `"open"` (default) = all senders in allowed groups can message +- `"open"` = all senders in allowed groups can message - `"allowlist"` = only senders in `telegram.groupAllowFrom` can message - `"disabled"` = no group messages accepted at all +Default is `groupPolicy: "allowlist"` (blocked unless you add `groupAllowFrom`). -Most users want: `groupPolicy: "open"` + specific groups listed in `telegram.groups` +Most users want: `groupPolicy: "allowlist"` + `groupAllowFrom` + specific groups listed in `telegram.groups` ## Long-polling vs webhook - Default: long-polling (no public URL required). @@ -289,7 +290,7 @@ Provider options: - `telegram.tokenFile`: read token from file path. - `telegram.dmPolicy`: `pairing | allowlist | open | disabled` (default: pairing). - `telegram.allowFrom`: DM allowlist (ids/usernames). `open` requires `"*"`. -- `telegram.groupPolicy`: `open | allowlist | disabled` (default: open). +- `telegram.groupPolicy`: `open | allowlist | disabled` (default: allowlist). - `telegram.groupAllowFrom`: group sender allowlist (ids/usernames). - `telegram.groups`: per-group defaults + allowlist (use `"*"` for global defaults). - `telegram.groups..requireMention`: mention gating default. diff --git a/docs/providers/whatsapp.md b/docs/providers/whatsapp.md index 0ed5e4218..2926e44cc 100644 --- a/docs/providers/whatsapp.md +++ b/docs/providers/whatsapp.md @@ -158,7 +158,7 @@ The wizard uses it to set your **allowlist/owner** so your own DMs are permitted ## Groups - Groups map to `agent::whatsapp:group:` sessions. -- Group policy: `whatsapp.groupPolicy = open|disabled|allowlist` (default `open`). +- Group policy: `whatsapp.groupPolicy = open|disabled|allowlist` (default `allowlist`). - Activation modes: - `mention` (default): requires @mention or regex match. - `always`: always triggers. diff --git a/docs/start/faq.md b/docs/start/faq.md index d8afa42d1..ffda2caf4 100644 --- a/docs/start/faq.md +++ b/docs/start/faq.md @@ -448,7 +448,7 @@ Notes: ### Do I need to add a “bot account” to a WhatsApp group? No. Clawdbot runs on **your own account**, so if you’re in the group, Clawdbot can see it. -By default, anyone in that group can **mention** the bot to trigger a reply. +By default, group replies are blocked until you allow senders (`groupPolicy: "allowlist"`). If you want only **you** to be able to trigger group replies: diff --git a/src/config/config.test.ts b/src/config/config.test.ts index 78703a28a..e8aa90079 100644 --- a/src/config/config.test.ts +++ b/src/config/config.test.ts @@ -1281,6 +1281,16 @@ describe("legacy config detection", () => { } }); + it("defaults telegram.groupPolicy to allowlist when telegram section exists", async () => { + vi.resetModules(); + const { validateConfigObject } = await import("./config.js"); + const res = validateConfigObject({ telegram: {} }); + expect(res.ok).toBe(true); + if (res.ok) { + expect(res.config.telegram?.groupPolicy).toBe("allowlist"); + } + }); + it("defaults telegram.streamMode to partial when telegram section exists", async () => { vi.resetModules(); const { validateConfigObject } = await import("./config.js"); @@ -1325,6 +1335,16 @@ describe("legacy config detection", () => { } }); + it("defaults whatsapp.groupPolicy to allowlist when whatsapp section exists", async () => { + vi.resetModules(); + const { validateConfigObject } = await import("./config.js"); + const res = validateConfigObject({ whatsapp: {} }); + expect(res.ok).toBe(true); + if (res.ok) { + expect(res.config.whatsapp?.groupPolicy).toBe("allowlist"); + } + }); + it('rejects signal.dmPolicy="open" without allowFrom "*"', async () => { vi.resetModules(); const { validateConfigObject } = await import("./config.js"); @@ -1359,6 +1379,16 @@ describe("legacy config detection", () => { } }); + it("defaults signal.groupPolicy to allowlist when signal section exists", async () => { + vi.resetModules(); + const { validateConfigObject } = await import("./config.js"); + const res = validateConfigObject({ signal: {} }); + expect(res.ok).toBe(true); + if (res.ok) { + expect(res.config.signal?.groupPolicy).toBe("allowlist"); + } + }); + it("accepts historyLimit overrides per provider and account", async () => { vi.resetModules(); const { validateConfigObject } = await import("./config.js"); @@ -1421,6 +1451,36 @@ describe("legacy config detection", () => { } }); + it("defaults imessage.groupPolicy to allowlist when imessage section exists", async () => { + vi.resetModules(); + const { validateConfigObject } = await import("./config.js"); + const res = validateConfigObject({ imessage: {} }); + expect(res.ok).toBe(true); + if (res.ok) { + expect(res.config.imessage?.groupPolicy).toBe("allowlist"); + } + }); + + it("defaults discord.groupPolicy to allowlist when discord section exists", async () => { + vi.resetModules(); + const { validateConfigObject } = await import("./config.js"); + const res = validateConfigObject({ discord: {} }); + expect(res.ok).toBe(true); + if (res.ok) { + expect(res.config.discord?.groupPolicy).toBe("allowlist"); + } + }); + + it("defaults slack.groupPolicy to allowlist when slack section exists", async () => { + vi.resetModules(); + const { validateConfigObject } = await import("./config.js"); + const res = validateConfigObject({ slack: {} }); + expect(res.ok).toBe(true); + if (res.ok) { + expect(res.config.slack?.groupPolicy).toBe("allowlist"); + } + }); + it("rejects unsafe executable config values", async () => { vi.resetModules(); const { validateConfigObject } = await import("./config.js"); diff --git a/src/config/types.ts b/src/config/types.ts index 476578c74..89275345a 100644 --- a/src/config/types.ts +++ b/src/config/types.ts @@ -140,7 +140,7 @@ export type WhatsAppConfig = { groupAllowFrom?: string[]; /** * Controls how group messages are handled: - * - "open" (default): groups bypass allowFrom, only mention-gating applies + * - "open": groups bypass allowFrom, only mention-gating applies * - "disabled": block all group messages entirely * - "allowlist": only allow group messages from senders in groupAllowFrom/allowFrom */ @@ -380,7 +380,7 @@ export type TelegramAccountConfig = { groupAllowFrom?: Array; /** * Controls how group messages are handled: - * - "open" (default): groups bypass allowFrom, only mention-gating applies + * - "open": groups bypass allowFrom, only mention-gating applies * - "disabled": block all group messages entirely * - "allowlist": only allow group messages from senders in groupAllowFrom/allowFrom */ @@ -515,7 +515,7 @@ export type DiscordAccountConfig = { token?: string; /** * Controls how guild channel messages are handled: - * - "open" (default): guild channels bypass allowlists; mention-gating applies + * - "open": guild channels bypass allowlists; mention-gating applies * - "disabled": block all guild channel messages * - "allowlist": only allow channels present in discord.guilds.*.channels */ @@ -627,7 +627,7 @@ export type SlackAccountConfig = { allowBots?: boolean; /** * Controls how channel messages are handled: - * - "open" (default): channels bypass allowlists; mention-gating applies + * - "open": channels bypass allowlists; mention-gating applies * - "disabled": block all channel messages * - "allowlist": only allow channels present in slack.channels */ @@ -690,7 +690,7 @@ export type SignalAccountConfig = { groupAllowFrom?: Array; /** * Controls how group messages are handled: - * - "open" (default): groups bypass allowFrom, no extra gating + * - "open": groups bypass allowFrom, no extra gating * - "disabled": block all group messages * - "allowlist": only allow group messages from senders in groupAllowFrom/allowFrom */ @@ -809,7 +809,7 @@ export type IMessageAccountConfig = { groupAllowFrom?: Array; /** * Controls how group messages are handled: - * - "open" (default): groups bypass allowFrom; mention-gating applies + * - "open": groups bypass allowFrom; mention-gating applies * - "disabled": block all group messages entirely * - "allowlist": only allow group messages from senders in groupAllowFrom/allowFrom */ diff --git a/src/config/zod-schema.ts b/src/config/zod-schema.ts index 9cc2af1c2..1733c6128 100644 --- a/src/config/zod-schema.ts +++ b/src/config/zod-schema.ts @@ -95,9 +95,9 @@ const ReplyToModeSchema = z.union([ ]); // GroupPolicySchema: controls how group messages are handled -// Used with .default("open").optional() pattern: +// Used with .default("allowlist").optional() pattern: // - .optional() allows field omission in input config -// - .default("open") ensures runtime always resolves to "open" if not provided +// - .default("allowlist") ensures runtime always resolves to "allowlist" if not provided const GroupPolicySchema = z.enum(["open", "disabled", "allowlist"]); const DmPolicySchema = z.enum(["pairing", "allowlist", "open", "disabled"]); @@ -275,7 +275,7 @@ const TelegramAccountSchemaBase = z.object({ groups: z.record(z.string(), TelegramGroupSchema.optional()).optional(), allowFrom: z.array(z.union([z.string(), z.number()])).optional(), groupAllowFrom: z.array(z.union([z.string(), z.number()])).optional(), - groupPolicy: GroupPolicySchema.optional().default("open"), + groupPolicy: GroupPolicySchema.optional().default("allowlist"), historyLimit: z.number().int().min(0).optional(), dmHistoryLimit: z.number().int().min(0).optional(), dms: z.record(z.string(), DmConfigSchema.optional()).optional(), @@ -366,7 +366,7 @@ const DiscordAccountSchema = z.object({ capabilities: z.array(z.string()).optional(), enabled: z.boolean().optional(), token: z.string().optional(), - groupPolicy: GroupPolicySchema.optional().default("open"), + groupPolicy: GroupPolicySchema.optional().default("allowlist"), historyLimit: z.number().int().min(0).optional(), dmHistoryLimit: z.number().int().min(0).optional(), dms: z.record(z.string(), DmConfigSchema.optional()).optional(), @@ -440,7 +440,7 @@ const SlackAccountSchema = z.object({ botToken: z.string().optional(), appToken: z.string().optional(), allowBots: z.boolean().optional(), - groupPolicy: GroupPolicySchema.optional().default("open"), + groupPolicy: GroupPolicySchema.optional().default("allowlist"), historyLimit: z.number().int().min(0).optional(), dmHistoryLimit: z.number().int().min(0).optional(), dms: z.record(z.string(), DmConfigSchema.optional()).optional(), @@ -496,7 +496,7 @@ const SignalAccountSchemaBase = z.object({ dmPolicy: DmPolicySchema.optional().default("pairing"), allowFrom: z.array(z.union([z.string(), z.number()])).optional(), groupAllowFrom: z.array(z.union([z.string(), z.number()])).optional(), - groupPolicy: GroupPolicySchema.optional().default("open"), + groupPolicy: GroupPolicySchema.optional().default("allowlist"), historyLimit: z.number().int().min(0).optional(), dmHistoryLimit: z.number().int().min(0).optional(), dms: z.record(z.string(), DmConfigSchema.optional()).optional(), @@ -546,7 +546,7 @@ const IMessageAccountSchemaBase = z.object({ dmPolicy: DmPolicySchema.optional().default("pairing"), allowFrom: z.array(z.union([z.string(), z.number()])).optional(), groupAllowFrom: z.array(z.union([z.string(), z.number()])).optional(), - groupPolicy: GroupPolicySchema.optional().default("open"), + groupPolicy: GroupPolicySchema.optional().default("allowlist"), historyLimit: z.number().int().min(0).optional(), dmHistoryLimit: z.number().int().min(0).optional(), dms: z.record(z.string(), DmConfigSchema.optional()).optional(), @@ -1394,7 +1394,7 @@ export const ClawdbotSchema = z selfChatMode: z.boolean().optional(), allowFrom: z.array(z.string()).optional(), groupAllowFrom: z.array(z.string()).optional(), - groupPolicy: GroupPolicySchema.optional().default("open"), + groupPolicy: GroupPolicySchema.optional().default("allowlist"), historyLimit: z.number().int().min(0).optional(), dmHistoryLimit: z.number().int().min(0).optional(), dms: z.record(z.string(), DmConfigSchema.optional()).optional(), @@ -1445,7 +1445,7 @@ export const ClawdbotSchema = z selfChatMode: z.boolean().optional(), allowFrom: z.array(z.string()).optional(), groupAllowFrom: z.array(z.string()).optional(), - groupPolicy: GroupPolicySchema.optional().default("open"), + groupPolicy: GroupPolicySchema.optional().default("allowlist"), historyLimit: z.number().int().min(0).optional(), dmHistoryLimit: z.number().int().min(0).optional(), dms: z.record(z.string(), DmConfigSchema.optional()).optional(), diff --git a/src/discord/monitor.ts b/src/discord/monitor.ts index 458112675..c6053e5d0 100644 --- a/src/discord/monitor.ts +++ b/src/discord/monitor.ts @@ -382,7 +382,7 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) { const discordCfg = account.config; const dmConfig = discordCfg.dm; const guildEntries = discordCfg.guilds; - const groupPolicy = discordCfg.groupPolicy ?? "open"; + const groupPolicy = discordCfg.groupPolicy ?? "allowlist"; const allowFrom = dmConfig?.allowFrom; const mediaMaxBytes = (opts.mediaMaxMb ?? discordCfg.mediaMaxMb ?? 8) * 1024 * 1024; @@ -639,7 +639,7 @@ export function createDiscordMessageHandler(params: { } = params; const logger = getChildLogger({ module: "discord-auto-reply" }); const ackReactionScope = cfg.messages?.ackReactionScope ?? "group-mentions"; - const groupPolicy = discordConfig?.groupPolicy ?? "open"; + const groupPolicy = discordConfig?.groupPolicy ?? "allowlist"; return async (data, client) => { try { @@ -1548,7 +1548,7 @@ function createDiscordNativeCommand(params: { Object.keys(guildInfo?.channels ?? {}).length > 0; const channelAllowed = channelConfig?.allowed !== false; const allowByPolicy = isDiscordGroupAllowedByPolicy({ - groupPolicy: discordConfig?.groupPolicy ?? "open", + groupPolicy: discordConfig?.groupPolicy ?? "allowlist", channelAllowlistConfigured, channelAllowed, }); diff --git a/src/imessage/monitor.ts b/src/imessage/monitor.ts index be8dcb5e9..7de70b8b9 100644 --- a/src/imessage/monitor.ts +++ b/src/imessage/monitor.ts @@ -166,7 +166,7 @@ export async function monitorIMessageProvider( ? imessageCfg.allowFrom : []), ); - const groupPolicy = imessageCfg.groupPolicy ?? "open"; + const groupPolicy = imessageCfg.groupPolicy ?? "allowlist"; const dmPolicy = imessageCfg.dmPolicy ?? "pairing"; const includeAttachments = opts.includeAttachments ?? imessageCfg.includeAttachments ?? false; diff --git a/src/providers/plugins/discord.ts b/src/providers/plugins/discord.ts index 73b5670cd..54f355be0 100644 --- a/src/providers/plugins/discord.ts +++ b/src/providers/plugins/discord.ts @@ -117,6 +117,21 @@ export const discordPlugin: ProviderPlugin = { raw.replace(/^(discord|user):/i, "").replace(/^<@!?(\d+)>$/, "$1"), }; }, + collectWarnings: ({ account }) => { + const groupPolicy = account.config.groupPolicy ?? "allowlist"; + if (groupPolicy !== "open") return []; + const channelAllowlistConfigured = + Boolean(account.config.guilds) && + Object.keys(account.config.guilds ?? {}).length > 0; + if (channelAllowlistConfigured) { + return [ + `- Discord guilds: groupPolicy="open" allows any channel not explicitly denied to trigger (mention-gated). Set discord.groupPolicy="allowlist" and configure discord.guilds..channels.`, + ]; + } + return [ + `- Discord guilds: groupPolicy="open" with no guild/channel allowlist; any channel can trigger (mention-gated). Set discord.groupPolicy="allowlist" and configure discord.guilds..channels.`, + ]; + }, }, groups: { resolveRequireMention: resolveDiscordGroupRequireMention, diff --git a/src/providers/plugins/imessage.ts b/src/providers/plugins/imessage.ts index 1fc620907..3604c08e0 100644 --- a/src/providers/plugins/imessage.ts +++ b/src/providers/plugins/imessage.ts @@ -99,6 +99,13 @@ export const imessagePlugin: ProviderPlugin = { approveHint: formatPairingApproveHint("imessage"), }; }, + collectWarnings: ({ account }) => { + const groupPolicy = account.config.groupPolicy ?? "allowlist"; + if (groupPolicy !== "open") return []; + return [ + `- iMessage groups: groupPolicy="open" allows any member to trigger the bot. Set imessage.groupPolicy="allowlist" + imessage.groupAllowFrom to restrict senders.`, + ]; + }, }, groups: { resolveRequireMention: resolveIMessageGroupRequireMention, diff --git a/src/providers/plugins/signal.ts b/src/providers/plugins/signal.ts index 2a80b79a2..2d52e417f 100644 --- a/src/providers/plugins/signal.ts +++ b/src/providers/plugins/signal.ts @@ -117,6 +117,13 @@ export const signalPlugin: ProviderPlugin = { normalizeE164(raw.replace(/^signal:/i, "").trim()), }; }, + collectWarnings: ({ account }) => { + const groupPolicy = account.config.groupPolicy ?? "allowlist"; + if (groupPolicy !== "open") return []; + return [ + `- Signal groups: groupPolicy="open" allows any member to trigger the bot. Set signal.groupPolicy="allowlist" + signal.groupAllowFrom to restrict senders.`, + ]; + }, }, messaging: { normalizeTarget: normalizeSignalMessagingTarget, diff --git a/src/providers/plugins/slack.ts b/src/providers/plugins/slack.ts index 544de2fc4..949c1b014 100644 --- a/src/providers/plugins/slack.ts +++ b/src/providers/plugins/slack.ts @@ -113,6 +113,21 @@ export const slackPlugin: ProviderPlugin = { normalizeEntry: (raw) => raw.replace(/^(slack|user):/i, ""), }; }, + collectWarnings: ({ account }) => { + const groupPolicy = account.config.groupPolicy ?? "allowlist"; + if (groupPolicy !== "open") return []; + const channelAllowlistConfigured = + Boolean(account.config.channels) && + Object.keys(account.config.channels ?? {}).length > 0; + if (channelAllowlistConfigured) { + return [ + `- Slack channels: groupPolicy="open" allows any channel not explicitly denied to trigger (mention-gated). Set slack.groupPolicy="allowlist" and configure slack.channels.`, + ]; + } + return [ + `- Slack channels: groupPolicy="open" with no channel allowlist; any channel can trigger (mention-gated). Set slack.groupPolicy="allowlist" and configure slack.channels.`, + ]; + }, }, groups: { resolveRequireMention: resolveSlackGroupRequireMention, diff --git a/src/providers/plugins/telegram.ts b/src/providers/plugins/telegram.ts index eec4e9ee6..8c9684eae 100644 --- a/src/providers/plugins/telegram.ts +++ b/src/providers/plugins/telegram.ts @@ -123,12 +123,17 @@ export const telegramPlugin: ProviderPlugin = { }; }, collectWarnings: ({ account }) => { - const groupPolicy = account.config.groupPolicy ?? "open"; + const groupPolicy = account.config.groupPolicy ?? "allowlist"; + if (groupPolicy !== "open") return []; const groupAllowlistConfigured = account.config.groups && Object.keys(account.config.groups).length > 0; - if (groupPolicy !== "open" || groupAllowlistConfigured) return []; + if (groupAllowlistConfigured) { + return [ + `- Telegram groups: groupPolicy="open" allows any member in allowed groups to trigger (mention-gated). Set telegram.groupPolicy="allowlist" + telegram.groupAllowFrom to restrict senders.`, + ]; + } return [ - `- Telegram groups: open (groupPolicy="open") with no telegram.groups allowlist; mention-gating applies but any group can add + ping.`, + `- Telegram groups: groupPolicy="open" with no telegram.groups allowlist; any group can add + ping (mention-gated). Set telegram.groupPolicy="allowlist" + telegram.groupAllowFrom or configure telegram.groups.`, ]; }, }, diff --git a/src/providers/plugins/whatsapp.ts b/src/providers/plugins/whatsapp.ts index e1a60c39e..8d94dd2d1 100644 --- a/src/providers/plugins/whatsapp.ts +++ b/src/providers/plugins/whatsapp.ts @@ -148,6 +148,20 @@ export const whatsappPlugin: ProviderPlugin = { normalizeEntry: (raw) => normalizeE164(raw), }; }, + collectWarnings: ({ account }) => { + const groupPolicy = account.groupPolicy ?? "allowlist"; + if (groupPolicy !== "open") return []; + const groupAllowlistConfigured = + Boolean(account.groups) && Object.keys(account.groups ?? {}).length > 0; + if (groupAllowlistConfigured) { + return [ + `- WhatsApp groups: groupPolicy="open" allows any member in allowed groups to trigger (mention-gated). Set whatsapp.groupPolicy="allowlist" + whatsapp.groupAllowFrom to restrict senders.`, + ]; + } + return [ + `- WhatsApp groups: groupPolicy="open" with no whatsapp.groups allowlist; any group can add + ping (mention-gated). Set whatsapp.groupPolicy="allowlist" + whatsapp.groupAllowFrom or configure whatsapp.groups.`, + ]; + }, }, setup: { resolveAccountId: ({ accountId }) => normalizeAccountId(accountId), diff --git a/src/signal/monitor.ts b/src/signal/monitor.ts index bed4a0cde..8b8d25265 100644 --- a/src/signal/monitor.ts +++ b/src/signal/monitor.ts @@ -355,7 +355,7 @@ export async function monitorSignalProvider( ? accountInfo.config.allowFrom : []), ); - const groupPolicy = accountInfo.config.groupPolicy ?? "open"; + const groupPolicy = accountInfo.config.groupPolicy ?? "allowlist"; const reactionMode = accountInfo.config.reactionNotifications ?? "own"; const reactionAllowlist = normalizeAllowList( accountInfo.config.reactionAllowlist, diff --git a/src/slack/monitor.ts b/src/slack/monitor.ts index af8101a71..82e1306cd 100644 --- a/src/slack/monitor.ts +++ b/src/slack/monitor.ts @@ -493,7 +493,7 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) { const groupDmChannels = normalizeAllowList(dmConfig?.groupChannels); const channelsConfig = slackCfg.channels; const dmEnabled = dmConfig?.enabled ?? true; - const groupPolicy = slackCfg.groupPolicy ?? "open"; + const groupPolicy = slackCfg.groupPolicy ?? "allowlist"; const useAccessGroups = cfg.commands?.useAccessGroups !== false; const reactionMode = slackCfg.reactionNotifications ?? "own"; const reactionAllowlist = slackCfg.reactionAllowlist ?? []; diff --git a/src/telegram/bot.test.ts b/src/telegram/bot.test.ts index 5b510d049..3e5c7de03 100644 --- a/src/telegram/bot.test.ts +++ b/src/telegram/bot.test.ts @@ -1244,7 +1244,7 @@ describe("createTelegramBot", () => { expect(replySpy).toHaveBeenCalledTimes(1); }); - it("allows all group messages when groupPolicy is 'open' (default)", async () => { + it("allows all group messages when groupPolicy is 'open'", async () => { onSpy.mockReset(); const replySpy = replyModule.__replySpy as unknown as ReturnType< typeof vi.fn @@ -1252,7 +1252,7 @@ describe("createTelegramBot", () => { replySpy.mockReset(); loadConfig.mockReturnValue({ telegram: { - // groupPolicy not set, should default to "open" + groupPolicy: "open", groups: { "*": { requireMention: false } }, }, }); diff --git a/src/telegram/bot.ts b/src/telegram/bot.ts index ed78c284f..e80923ec1 100644 --- a/src/telegram/bot.ts +++ b/src/telegram/bot.ts @@ -982,7 +982,7 @@ export function createTelegramBot(opts: TelegramBotOptions) { } if (isGroup && useAccessGroups) { - const groupPolicy = telegramCfg.groupPolicy ?? "open"; + const groupPolicy = telegramCfg.groupPolicy ?? "allowlist"; if (groupPolicy === "disabled") { await bot.api.sendMessage( chatId, @@ -1211,10 +1211,10 @@ export function createTelegramBot(opts: TelegramBotOptions) { } } // Group policy filtering: controls how group messages are handled - // - "open" (default): groups bypass allowFrom, only mention-gating applies + // - "open": groups bypass allowFrom, only mention-gating applies // - "disabled": block all group messages entirely // - "allowlist": only allow group messages from senders in groupAllowFrom/allowFrom - const groupPolicy = telegramCfg.groupPolicy ?? "open"; + const groupPolicy = telegramCfg.groupPolicy ?? "allowlist"; if (groupPolicy === "disabled") { logVerbose(`Blocked telegram group message (groupPolicy: disabled)`); return; diff --git a/src/web/inbound.ts b/src/web/inbound.ts index 3a90479bd..d5fc9a9ca 100644 --- a/src/web/inbound.ts +++ b/src/web/inbound.ts @@ -250,10 +250,10 @@ export async function monitorWebInbox(options: { : []; // Group policy filtering: controls how group messages are handled - // - "open" (default): groups bypass allowFrom, only mention-gating applies + // - "open": groups bypass allowFrom, only mention-gating applies // - "disabled": block all group messages entirely // - "allowlist": only allow group messages from senders in groupAllowFrom/allowFrom - const groupPolicy = account.groupPolicy ?? "open"; + const groupPolicy = account.groupPolicy ?? "allowlist"; if (group && groupPolicy === "disabled") { logVerbose(`Blocked group message (groupPolicy: disabled)`); continue;