fix(slack): respect top-level requireMention config

The `channels.slack.requireMention` setting was defined in the schema
but never passed to `resolveSlackChannelConfig()`, which always
defaulted to `true`. This meant setting `requireMention: false` at the
top level had no effect—channels still required mentions.

Pass `slackCfg.requireMention` as `defaultRequireMention` to the
resolver and use it as the fallback instead of hardcoded `true`.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Jonathan Wilkins
2026-01-13 14:21:23 +00:00
committed by Peter Steinberger
parent 6ffd7111a6
commit 09ce6ff99e
9 changed files with 96 additions and 5 deletions

View File

@@ -75,6 +75,8 @@ export type SlackAccountConfig = {
appToken?: string;
/** Allow bot-authored messages to trigger replies (default: false). */
allowBots?: boolean;
/** Default mention requirement for channel messages (default: true). */
requireMention?: boolean;
/**
* Controls how channel messages are handled:
* - "open": channels bypass allowlists; mention-gating applies

View File

@@ -212,6 +212,7 @@ export const SlackAccountSchema = z.object({
botToken: z.string().optional(),
appToken: z.string().optional(),
allowBots: z.boolean().optional(),
requireMention: z.boolean().optional(),
groupPolicy: GroupPolicySchema.optional().default("allowlist"),
historyLimit: z.number().int().min(0).optional(),
dmHistoryLimit: z.number().int().min(0).optional(),

View File

@@ -423,6 +423,49 @@ describe("monitorSlackProvider tool results", () => {
expect(replyMock.mock.calls[0][0].WasMentioned).toBe(true);
});
it("accepts channel messages without mention when channels.slack.requireMention is false", async () => {
config = {
channels: {
slack: {
dm: { enabled: true, policy: "open", allowFrom: ["*"] },
groupPolicy: "open",
requireMention: false,
},
},
};
replyMock.mockResolvedValue({ text: "hi" });
const controller = new AbortController();
const run = monitorSlackProvider({
botToken: "bot-token",
appToken: "app-token",
abortSignal: controller.signal,
});
await waitForEvent("message");
const handler = getSlackHandlers()?.get("message");
if (!handler) throw new Error("Slack message handler not registered");
await handler({
event: {
type: "message",
user: "U1",
text: "hello",
ts: "123",
channel: "C1",
channel_type: "channel",
},
});
await flush();
controller.abort();
await run;
expect(replyMock).toHaveBeenCalledTimes(1);
expect(replyMock.mock.calls[0][0].WasMentioned).toBe(false);
expect(sendMock).toHaveBeenCalledTimes(1);
});
it("treats control commands as mentions for group bypass", async () => {
replyMock.mockResolvedValue({ text: "ok" });

View File

@@ -0,0 +1,32 @@
import { describe, expect, it } from "vitest";
import { resolveSlackChannelConfig } from "./channel-config.js";
describe("resolveSlackChannelConfig", () => {
it("uses defaultRequireMention when channels config is empty", () => {
const res = resolveSlackChannelConfig({
channelId: "C1",
channels: {},
defaultRequireMention: false,
});
expect(res).toEqual({ allowed: true, requireMention: false });
});
it("defaults defaultRequireMention to true when not provided", () => {
const res = resolveSlackChannelConfig({
channelId: "C1",
channels: {},
});
expect(res).toEqual({ allowed: true, requireMention: true });
});
it("prefers explicit channel/fallback requireMention over defaultRequireMention", () => {
const res = resolveSlackChannelConfig({
channelId: "C1",
channels: { "*": { requireMention: true } },
defaultRequireMention: false,
});
expect(res).toMatchObject({ requireMention: true });
});
});

View File

@@ -70,8 +70,9 @@ export function resolveSlackChannelConfig(params: {
systemPrompt?: string;
}
>;
defaultRequireMention?: boolean;
}): SlackChannelConfigResolved | null {
const { channelId, channelName, channels } = params;
const { channelId, channelName, channels, defaultRequireMention } = params;
const entries = channels ?? {};
const keys = Object.keys(entries);
const normalizedName = channelName ? normalizeSlackSlug(channelName) : "";
@@ -102,11 +103,12 @@ export function resolveSlackChannelConfig(params: {
}
const fallback = entries["*"];
const requireMentionDefault = defaultRequireMention ?? true;
if (keys.length === 0) {
return { allowed: true, requireMention: true };
return { allowed: true, requireMention: requireMentionDefault };
}
if (!matched && !fallback) {
return { allowed: false, requireMention: true };
return { allowed: false, requireMention: requireMentionDefault };
}
const resolved = matched ?? fallback ?? {};
@@ -114,7 +116,8 @@ export function resolveSlackChannelConfig(params: {
firstDefined(resolved.enabled, resolved.allow, fallback?.enabled, fallback?.allow, true) ??
true;
const requireMention =
firstDefined(resolved.requireMention, fallback?.requireMention, true) ?? true;
firstDefined(resolved.requireMention, fallback?.requireMention, requireMentionDefault) ??
requireMentionDefault;
const allowBots = firstDefined(resolved.allowBots, fallback?.allowBots);
const users = firstDefined(resolved.users, fallback?.users);
const skills = firstDefined(resolved.skills, fallback?.skills);

View File

@@ -34,6 +34,7 @@ export type SlackMonitorContext = {
allowFrom: string[];
groupDmEnabled: boolean;
groupDmChannels: string[];
defaultRequireMention: boolean;
channelsConfig?: Record<
string,
{
@@ -103,6 +104,7 @@ export function createSlackMonitorContext(params: {
allowFrom: Array<string | number> | undefined;
groupDmEnabled: boolean;
groupDmChannels: Array<string | number> | undefined;
defaultRequireMention?: boolean;
channelsConfig?: SlackMonitorContext["channelsConfig"];
groupPolicy: SlackMonitorContext["groupPolicy"];
useAccessGroups: boolean;
@@ -132,6 +134,7 @@ export function createSlackMonitorContext(params: {
const allowFrom = normalizeAllowList(params.allowFrom);
const groupDmChannels = normalizeAllowList(params.groupDmChannels);
const defaultRequireMention = params.defaultRequireMention ?? true;
const markMessageSeen = (channelId: string | undefined, ts?: string) => {
if (!channelId || !ts) return false;
@@ -274,6 +277,7 @@ export function createSlackMonitorContext(params: {
channelId: p.channelId,
channelName: p.channelName,
channels: params.channelsConfig,
defaultRequireMention,
});
const channelAllowed = channelConfig?.allowed !== false;
const channelAllowlistConfigured =
@@ -330,6 +334,7 @@ export function createSlackMonitorContext(params: {
allowFrom,
groupDmEnabled: params.groupDmEnabled,
groupDmChannels,
defaultRequireMention,
channelsConfig: params.channelsConfig,
groupPolicy: params.groupPolicy,
useAccessGroups: params.useAccessGroups,

View File

@@ -56,6 +56,7 @@ export async function prepareSlackMessage(params: {
channelId: message.channel,
channelName,
channels: ctx.channelsConfig,
defaultRequireMention: ctx.defaultRequireMention,
})
: null;
@@ -200,7 +201,9 @@ export async function prepareSlackMessage(params: {
cfg,
surface: "slack",
});
const shouldRequireMention = isRoom ? (channelConfig?.requireMention ?? true) : false;
const shouldRequireMention = isRoom
? (channelConfig?.requireMention ?? ctx.defaultRequireMention)
: false;
// Allow "control commands" to bypass mention gating if sender is authorized.
const shouldBypassMention =

View File

@@ -122,6 +122,7 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) {
allowFrom,
groupDmEnabled,
groupDmChannels,
defaultRequireMention: slackCfg.requireMention,
channelsConfig,
groupPolicy,
useAccessGroups,

View File

@@ -136,6 +136,7 @@ export function registerSlackMonitorSlashCommands(params: {
channelId: command.channel_id,
channelName: channelInfo?.name,
channels: ctx.channelsConfig,
defaultRequireMention: ctx.defaultRequireMention,
});
if (ctx.useAccessGroups) {
const channelAllowlistConfigured =