fix: enforce explicit mention gating across channels
This commit is contained in:
@@ -42,6 +42,7 @@ Docs: https://docs.clawd.bot
|
|||||||
- Agents: add CLI log hint to "agent failed before reply" messages. (#1550) Thanks @sweepies.
|
- Agents: add CLI log hint to "agent failed before reply" messages. (#1550) Thanks @sweepies.
|
||||||
- Discord: limit autoThread mention bypass to bot-owned threads; keep ack reactions mention-gated. (#1511) Thanks @pvoo.
|
- Discord: limit autoThread mention bypass to bot-owned threads; keep ack reactions mention-gated. (#1511) Thanks @pvoo.
|
||||||
- Discord: retry rate-limited allowlist resolution + command deploy to avoid gateway crashes.
|
- Discord: retry rate-limited allowlist resolution + command deploy to avoid gateway crashes.
|
||||||
|
- Mentions: ignore mentionPattern matches when another explicit mention is present in group chats (Slack/Discord/Telegram/WhatsApp).
|
||||||
- Gateway: accept null optional fields in exec approval requests. (#1511) Thanks @pvoo.
|
- Gateway: accept null optional fields in exec approval requests. (#1511) Thanks @pvoo.
|
||||||
- Exec: honor tools.exec ask/security defaults for elevated approvals (avoid unwanted prompts).
|
- Exec: honor tools.exec ask/security defaults for elevated approvals (avoid unwanted prompts).
|
||||||
- TUI: forward unknown slash commands (for example, `/context`) to the Gateway.
|
- TUI: forward unknown slash commands (for example, `/context`) to the Gateway.
|
||||||
|
|||||||
@@ -75,6 +75,26 @@ export function matchesMentionPatterns(text: string, mentionRegexes: RegExp[]):
|
|||||||
return mentionRegexes.some((re) => re.test(cleaned));
|
return mentionRegexes.some((re) => re.test(cleaned));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ExplicitMentionSignal = {
|
||||||
|
hasAnyMention: boolean;
|
||||||
|
isExplicitlyMentioned: boolean;
|
||||||
|
canResolveExplicit: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function matchesMentionWithExplicit(params: {
|
||||||
|
text: string;
|
||||||
|
mentionRegexes: RegExp[];
|
||||||
|
explicit?: ExplicitMentionSignal;
|
||||||
|
}): boolean {
|
||||||
|
const cleaned = normalizeMentionText(params.text ?? "");
|
||||||
|
const explicit = params.explicit?.isExplicitlyMentioned === true;
|
||||||
|
const explicitAvailable = params.explicit?.canResolveExplicit === true;
|
||||||
|
const hasAnyMention = params.explicit?.hasAnyMention === true;
|
||||||
|
if (hasAnyMention && explicitAvailable) return explicit;
|
||||||
|
if (!cleaned) return explicit;
|
||||||
|
return explicit || params.mentionRegexes.some((re) => re.test(cleaned));
|
||||||
|
}
|
||||||
|
|
||||||
export function stripStructuralPrefixes(text: string): string {
|
export function stripStructuralPrefixes(text: string): string {
|
||||||
// Ignore wrapper labels, timestamps, and sender prefixes so directive-only
|
// Ignore wrapper labels, timestamps, and sender prefixes so directive-only
|
||||||
// detection still works in group batches that include history/context.
|
// detection still works in group batches that include history/context.
|
||||||
|
|||||||
@@ -6,7 +6,10 @@ import {
|
|||||||
recordPendingHistoryEntryIfEnabled,
|
recordPendingHistoryEntryIfEnabled,
|
||||||
type HistoryEntry,
|
type HistoryEntry,
|
||||||
} from "../../auto-reply/reply/history.js";
|
} from "../../auto-reply/reply/history.js";
|
||||||
import { buildMentionRegexes, matchesMentionPatterns } from "../../auto-reply/reply/mentions.js";
|
import {
|
||||||
|
buildMentionRegexes,
|
||||||
|
matchesMentionWithExplicit,
|
||||||
|
} from "../../auto-reply/reply/mentions.js";
|
||||||
import { logVerbose, shouldLogVerbose } from "../../globals.js";
|
import { logVerbose, shouldLogVerbose } from "../../globals.js";
|
||||||
import { recordChannelActivity } from "../../infra/channel-activity.js";
|
import { recordChannelActivity } from "../../infra/channel-activity.js";
|
||||||
import { enqueueSystemEvent } from "../../infra/system-events.js";
|
import { enqueueSystemEvent } from "../../infra/system-events.js";
|
||||||
@@ -174,10 +177,26 @@ export async function preflightDiscordMessage(
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
const mentionRegexes = buildMentionRegexes(params.cfg, route.agentId);
|
const mentionRegexes = buildMentionRegexes(params.cfg, route.agentId);
|
||||||
|
const explicitlyMentioned = Boolean(
|
||||||
|
botId && message.mentionedUsers?.some((user: User) => user.id === botId),
|
||||||
|
);
|
||||||
|
const hasAnyMention = Boolean(
|
||||||
|
!isDirectMessage &&
|
||||||
|
(message.mentionedEveryone ||
|
||||||
|
(message.mentionedUsers?.length ?? 0) > 0 ||
|
||||||
|
(message.mentionedRoles?.length ?? 0) > 0),
|
||||||
|
);
|
||||||
const wasMentioned =
|
const wasMentioned =
|
||||||
!isDirectMessage &&
|
!isDirectMessage &&
|
||||||
(Boolean(botId && message.mentionedUsers?.some((user: User) => user.id === botId)) ||
|
matchesMentionWithExplicit({
|
||||||
matchesMentionPatterns(baseText, mentionRegexes));
|
text: baseText,
|
||||||
|
mentionRegexes,
|
||||||
|
explicit: {
|
||||||
|
hasAnyMention,
|
||||||
|
isExplicitlyMentioned: explicitlyMentioned,
|
||||||
|
canResolveExplicit: Boolean(botId),
|
||||||
|
},
|
||||||
|
});
|
||||||
const implicitMention = Boolean(
|
const implicitMention = Boolean(
|
||||||
!isDirectMessage &&
|
!isDirectMessage &&
|
||||||
botId &&
|
botId &&
|
||||||
@@ -341,12 +360,6 @@ export async function preflightDiscordMessage(
|
|||||||
channelConfig,
|
channelConfig,
|
||||||
guildInfo,
|
guildInfo,
|
||||||
});
|
});
|
||||||
const hasAnyMention = Boolean(
|
|
||||||
!isDirectMessage &&
|
|
||||||
(message.mentionedEveryone ||
|
|
||||||
(message.mentionedUsers?.length ?? 0) > 0 ||
|
|
||||||
(message.mentionedRoles?.length ?? 0) > 0),
|
|
||||||
);
|
|
||||||
const allowTextCommands = shouldHandleTextCommands({
|
const allowTextCommands = shouldHandleTextCommands({
|
||||||
cfg: params.cfg,
|
cfg: params.cfg,
|
||||||
surface: "discord",
|
surface: "discord",
|
||||||
|
|||||||
@@ -17,7 +17,11 @@ import {
|
|||||||
resolveEnvelopeFormatOptions,
|
resolveEnvelopeFormatOptions,
|
||||||
} from "../../auto-reply/envelope.js";
|
} from "../../auto-reply/envelope.js";
|
||||||
import { dispatchReplyFromConfig } from "../../auto-reply/reply/dispatch-from-config.js";
|
import { dispatchReplyFromConfig } from "../../auto-reply/reply/dispatch-from-config.js";
|
||||||
import { buildMentionRegexes, matchesMentionPatterns } from "../../auto-reply/reply/mentions.js";
|
import {
|
||||||
|
buildMentionRegexes,
|
||||||
|
matchesMentionPatterns,
|
||||||
|
matchesMentionWithExplicit,
|
||||||
|
} from "../../auto-reply/reply/mentions.js";
|
||||||
import { dispatchReplyWithBufferedBlockDispatcher } from "../../auto-reply/reply/provider-dispatcher.js";
|
import { dispatchReplyWithBufferedBlockDispatcher } from "../../auto-reply/reply/provider-dispatcher.js";
|
||||||
import { createReplyDispatcherWithTyping } from "../../auto-reply/reply/reply-dispatcher.js";
|
import { createReplyDispatcherWithTyping } from "../../auto-reply/reply/reply-dispatcher.js";
|
||||||
import { finalizeInboundContext } from "../../auto-reply/reply/inbound-context.js";
|
import { finalizeInboundContext } from "../../auto-reply/reply/inbound-context.js";
|
||||||
@@ -200,6 +204,7 @@ export function createPluginRuntime(): PluginRuntime {
|
|||||||
mentions: {
|
mentions: {
|
||||||
buildMentionRegexes,
|
buildMentionRegexes,
|
||||||
matchesMentionPatterns,
|
matchesMentionPatterns,
|
||||||
|
matchesMentionWithExplicit,
|
||||||
},
|
},
|
||||||
reactions: {
|
reactions: {
|
||||||
shouldAckReaction,
|
shouldAckReaction,
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ type SaveMediaBuffer = typeof import("../../media/store.js").saveMediaBuffer;
|
|||||||
type BuildMentionRegexes = typeof import("../../auto-reply/reply/mentions.js").buildMentionRegexes;
|
type BuildMentionRegexes = typeof import("../../auto-reply/reply/mentions.js").buildMentionRegexes;
|
||||||
type MatchesMentionPatterns =
|
type MatchesMentionPatterns =
|
||||||
typeof import("../../auto-reply/reply/mentions.js").matchesMentionPatterns;
|
typeof import("../../auto-reply/reply/mentions.js").matchesMentionPatterns;
|
||||||
|
type MatchesMentionWithExplicit =
|
||||||
|
typeof import("../../auto-reply/reply/mentions.js").matchesMentionWithExplicit;
|
||||||
type ShouldAckReaction = typeof import("../../channels/ack-reactions.js").shouldAckReaction;
|
type ShouldAckReaction = typeof import("../../channels/ack-reactions.js").shouldAckReaction;
|
||||||
type RemoveAckReactionAfterReply =
|
type RemoveAckReactionAfterReply =
|
||||||
typeof import("../../channels/ack-reactions.js").removeAckReactionAfterReply;
|
typeof import("../../channels/ack-reactions.js").removeAckReactionAfterReply;
|
||||||
@@ -215,6 +217,7 @@ export type PluginRuntime = {
|
|||||||
mentions: {
|
mentions: {
|
||||||
buildMentionRegexes: BuildMentionRegexes;
|
buildMentionRegexes: BuildMentionRegexes;
|
||||||
matchesMentionPatterns: MatchesMentionPatterns;
|
matchesMentionPatterns: MatchesMentionPatterns;
|
||||||
|
matchesMentionWithExplicit: MatchesMentionWithExplicit;
|
||||||
};
|
};
|
||||||
reactions: {
|
reactions: {
|
||||||
shouldAckReaction: ShouldAckReaction;
|
shouldAckReaction: ShouldAckReaction;
|
||||||
|
|||||||
@@ -12,7 +12,10 @@ import {
|
|||||||
recordPendingHistoryEntryIfEnabled,
|
recordPendingHistoryEntryIfEnabled,
|
||||||
} from "../../../auto-reply/reply/history.js";
|
} from "../../../auto-reply/reply/history.js";
|
||||||
import { finalizeInboundContext } from "../../../auto-reply/reply/inbound-context.js";
|
import { finalizeInboundContext } from "../../../auto-reply/reply/inbound-context.js";
|
||||||
import { buildMentionRegexes, matchesMentionPatterns } from "../../../auto-reply/reply/mentions.js";
|
import {
|
||||||
|
buildMentionRegexes,
|
||||||
|
matchesMentionWithExplicit,
|
||||||
|
} from "../../../auto-reply/reply/mentions.js";
|
||||||
import { logVerbose, shouldLogVerbose } from "../../../globals.js";
|
import { logVerbose, shouldLogVerbose } from "../../../globals.js";
|
||||||
import { enqueueSystemEvent } from "../../../infra/system-events.js";
|
import { enqueueSystemEvent } from "../../../infra/system-events.js";
|
||||||
import { buildPairingReply } from "../../../pairing/pairing-messages.js";
|
import { buildPairingReply } from "../../../pairing/pairing-messages.js";
|
||||||
@@ -204,11 +207,22 @@ export async function prepareSlackMessage(params: {
|
|||||||
isThreadReply && ctx.threadHistoryScope === "thread" ? sessionKey : message.channel;
|
isThreadReply && ctx.threadHistoryScope === "thread" ? sessionKey : message.channel;
|
||||||
|
|
||||||
const mentionRegexes = buildMentionRegexes(cfg, route.agentId);
|
const mentionRegexes = buildMentionRegexes(cfg, route.agentId);
|
||||||
|
const hasAnyMention = /<@[^>]+>/.test(message.text ?? "");
|
||||||
|
const explicitlyMentioned = Boolean(
|
||||||
|
ctx.botUserId && message.text?.includes(`<@${ctx.botUserId}>`),
|
||||||
|
);
|
||||||
const wasMentioned =
|
const wasMentioned =
|
||||||
opts.wasMentioned ??
|
opts.wasMentioned ??
|
||||||
(!isDirectMessage &&
|
(!isDirectMessage &&
|
||||||
(Boolean(ctx.botUserId && message.text?.includes(`<@${ctx.botUserId}>`)) ||
|
matchesMentionWithExplicit({
|
||||||
matchesMentionPatterns(message.text ?? "", mentionRegexes)));
|
text: message.text ?? "",
|
||||||
|
mentionRegexes,
|
||||||
|
explicit: {
|
||||||
|
hasAnyMention,
|
||||||
|
isExplicitlyMentioned: explicitlyMentioned,
|
||||||
|
canResolveExplicit: Boolean(ctx.botUserId),
|
||||||
|
},
|
||||||
|
}));
|
||||||
const implicitMention = Boolean(
|
const implicitMention = Boolean(
|
||||||
!isDirectMessage &&
|
!isDirectMessage &&
|
||||||
ctx.botUserId &&
|
ctx.botUserId &&
|
||||||
@@ -232,7 +246,6 @@ export async function prepareSlackMessage(params: {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasAnyMention = /<@[^>]+>/.test(message.text ?? "");
|
|
||||||
const allowTextCommands = shouldHandleTextCommands({
|
const allowTextCommands = shouldHandleTextCommands({
|
||||||
cfg,
|
cfg,
|
||||||
surface: "slack",
|
surface: "slack",
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
type HistoryEntry,
|
type HistoryEntry,
|
||||||
} from "../auto-reply/reply/history.js";
|
} from "../auto-reply/reply/history.js";
|
||||||
import { finalizeInboundContext } from "../auto-reply/reply/inbound-context.js";
|
import { finalizeInboundContext } from "../auto-reply/reply/inbound-context.js";
|
||||||
import { buildMentionRegexes, matchesMentionPatterns } from "../auto-reply/reply/mentions.js";
|
import { buildMentionRegexes, matchesMentionWithExplicit } from "../auto-reply/reply/mentions.js";
|
||||||
import { formatLocationText, toLocationContext } from "../channels/location.js";
|
import { formatLocationText, toLocationContext } from "../channels/location.js";
|
||||||
import { recordInboundSession } from "../channels/session.js";
|
import { recordInboundSession } from "../channels/session.js";
|
||||||
import { formatCliCommand } from "../cli/command-format.js";
|
import { formatCliCommand } from "../cli/command-format.js";
|
||||||
@@ -299,13 +299,20 @@ export const buildTelegramMessageContext = async ({
|
|||||||
if (!bodyText && allMedia.length > 0) {
|
if (!bodyText && allMedia.length > 0) {
|
||||||
bodyText = `<media:image>${allMedia.length > 1 ? ` (${allMedia.length} images)` : ""}`;
|
bodyText = `<media:image>${allMedia.length > 1 ? ` (${allMedia.length} images)` : ""}`;
|
||||||
}
|
}
|
||||||
const computedWasMentioned =
|
|
||||||
(botUsername ? hasBotMention(msg, botUsername) : false) ||
|
|
||||||
matchesMentionPatterns(msg.text ?? msg.caption ?? "", mentionRegexes);
|
|
||||||
const wasMentioned = options?.forceWasMentioned === true ? true : computedWasMentioned;
|
|
||||||
const hasAnyMention = (msg.entities ?? msg.caption_entities ?? []).some(
|
const hasAnyMention = (msg.entities ?? msg.caption_entities ?? []).some(
|
||||||
(ent) => ent.type === "mention",
|
(ent) => ent.type === "mention",
|
||||||
);
|
);
|
||||||
|
const explicitlyMentioned = botUsername ? hasBotMention(msg, botUsername) : false;
|
||||||
|
const computedWasMentioned = matchesMentionWithExplicit({
|
||||||
|
text: msg.text ?? msg.caption ?? "",
|
||||||
|
mentionRegexes,
|
||||||
|
explicit: {
|
||||||
|
hasAnyMention,
|
||||||
|
isExplicitlyMentioned: explicitlyMentioned,
|
||||||
|
canResolveExplicit: Boolean(botUsername),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const wasMentioned = options?.forceWasMentioned === true ? true : computedWasMentioned;
|
||||||
if (isGroup && commandGate.shouldBlock) {
|
if (isGroup && commandGate.shouldBlock) {
|
||||||
logInboundDrop({
|
logInboundDrop({
|
||||||
log: logVerbose,
|
log: logVerbose,
|
||||||
|
|||||||
@@ -43,13 +43,16 @@ export function isBotMentionedFromTargets(
|
|||||||
|
|
||||||
const isSelfChat = isSelfChatMode(targets.selfE164, mentionCfg.allowFrom);
|
const isSelfChat = isSelfChatMode(targets.selfE164, mentionCfg.allowFrom);
|
||||||
|
|
||||||
if (msg.mentionedJids?.length && !isSelfChat) {
|
const hasMentions = (msg.mentionedJids?.length ?? 0) > 0;
|
||||||
|
if (hasMentions && !isSelfChat) {
|
||||||
if (targets.selfE164 && targets.normalizedMentions.includes(targets.selfE164)) return true;
|
if (targets.selfE164 && targets.normalizedMentions.includes(targets.selfE164)) return true;
|
||||||
if (targets.selfJid && targets.selfE164) {
|
if (targets.selfJid) {
|
||||||
// Some mentions use the bare JID; match on E.164 to be safe.
|
// Some mentions use the bare JID; match on E.164 to be safe.
|
||||||
if (targets.normalizedMentions.includes(targets.selfJid)) return true;
|
if (targets.normalizedMentions.includes(targets.selfJid)) return true;
|
||||||
}
|
}
|
||||||
} else if (msg.mentionedJids?.length && isSelfChat) {
|
// If the message explicitly mentions someone else, do not fall back to regex matches.
|
||||||
|
return false;
|
||||||
|
} else if (hasMentions && isSelfChat) {
|
||||||
// Self-chat mode: ignore WhatsApp @mention JIDs, otherwise @mentioning the owner in group chats triggers the bot.
|
// Self-chat mode: ignore WhatsApp @mention JIDs, otherwise @mentioning the owner in group chats triggers the bot.
|
||||||
}
|
}
|
||||||
const bodyClean = clean(msg.body);
|
const bodyClean = clean(msg.body);
|
||||||
|
|||||||
Reference in New Issue
Block a user