fix: default groupPolicy to allowlist

This commit is contained in:
Peter Steinberger
2026-01-12 08:21:50 +00:00
parent ba3158e01a
commit 842e91d019
28 changed files with 183 additions and 47 deletions

View File

@@ -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");

View File

@@ -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<string | number>;
/**
* 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<string | number>;
/**
* 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<string | number>;
/**
* 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
*/

View File

@@ -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(),

View File

@@ -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,
});

View File

@@ -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;

View File

@@ -117,6 +117,21 @@ export const discordPlugin: ProviderPlugin<ResolvedDiscordAccount> = {
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.<id>.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.<id>.channels.`,
];
},
},
groups: {
resolveRequireMention: resolveDiscordGroupRequireMention,

View File

@@ -99,6 +99,13 @@ export const imessagePlugin: ProviderPlugin<ResolvedIMessageAccount> = {
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,

View File

@@ -117,6 +117,13 @@ export const signalPlugin: ProviderPlugin<ResolvedSignalAccount> = {
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,

View File

@@ -113,6 +113,21 @@ export const slackPlugin: ProviderPlugin<ResolvedSlackAccount> = {
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,

View File

@@ -123,12 +123,17 @@ export const telegramPlugin: ProviderPlugin<ResolvedTelegramAccount> = {
};
},
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.`,
];
},
},

View File

@@ -148,6 +148,20 @@ export const whatsappPlugin: ProviderPlugin<ResolvedWhatsAppAccount> = {
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),

View File

@@ -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,

View File

@@ -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 ?? [];

View File

@@ -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 } },
},
});

View File

@@ -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;

View File

@@ -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;