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.
|
||||
- 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.
|
||||
- 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.
|
||||
- Exec: honor tools.exec ask/security defaults for elevated approvals (avoid unwanted prompts).
|
||||
- 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));
|
||||
}
|
||||
|
||||
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 {
|
||||
// Ignore wrapper labels, timestamps, and sender prefixes so directive-only
|
||||
// detection still works in group batches that include history/context.
|
||||
|
||||
@@ -6,7 +6,10 @@ import {
|
||||
recordPendingHistoryEntryIfEnabled,
|
||||
type HistoryEntry,
|
||||
} 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 { recordChannelActivity } from "../../infra/channel-activity.js";
|
||||
import { enqueueSystemEvent } from "../../infra/system-events.js";
|
||||
@@ -174,10 +177,26 @@ export async function preflightDiscordMessage(
|
||||
},
|
||||
});
|
||||
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 =
|
||||
!isDirectMessage &&
|
||||
(Boolean(botId && message.mentionedUsers?.some((user: User) => user.id === botId)) ||
|
||||
matchesMentionPatterns(baseText, mentionRegexes));
|
||||
matchesMentionWithExplicit({
|
||||
text: baseText,
|
||||
mentionRegexes,
|
||||
explicit: {
|
||||
hasAnyMention,
|
||||
isExplicitlyMentioned: explicitlyMentioned,
|
||||
canResolveExplicit: Boolean(botId),
|
||||
},
|
||||
});
|
||||
const implicitMention = Boolean(
|
||||
!isDirectMessage &&
|
||||
botId &&
|
||||
@@ -341,12 +360,6 @@ export async function preflightDiscordMessage(
|
||||
channelConfig,
|
||||
guildInfo,
|
||||
});
|
||||
const hasAnyMention = Boolean(
|
||||
!isDirectMessage &&
|
||||
(message.mentionedEveryone ||
|
||||
(message.mentionedUsers?.length ?? 0) > 0 ||
|
||||
(message.mentionedRoles?.length ?? 0) > 0),
|
||||
);
|
||||
const allowTextCommands = shouldHandleTextCommands({
|
||||
cfg: params.cfg,
|
||||
surface: "discord",
|
||||
|
||||
@@ -17,7 +17,11 @@ import {
|
||||
resolveEnvelopeFormatOptions,
|
||||
} from "../../auto-reply/envelope.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 { createReplyDispatcherWithTyping } from "../../auto-reply/reply/reply-dispatcher.js";
|
||||
import { finalizeInboundContext } from "../../auto-reply/reply/inbound-context.js";
|
||||
@@ -200,6 +204,7 @@ export function createPluginRuntime(): PluginRuntime {
|
||||
mentions: {
|
||||
buildMentionRegexes,
|
||||
matchesMentionPatterns,
|
||||
matchesMentionWithExplicit,
|
||||
},
|
||||
reactions: {
|
||||
shouldAckReaction,
|
||||
|
||||
@@ -19,6 +19,8 @@ type SaveMediaBuffer = typeof import("../../media/store.js").saveMediaBuffer;
|
||||
type BuildMentionRegexes = typeof import("../../auto-reply/reply/mentions.js").buildMentionRegexes;
|
||||
type 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 RemoveAckReactionAfterReply =
|
||||
typeof import("../../channels/ack-reactions.js").removeAckReactionAfterReply;
|
||||
@@ -215,6 +217,7 @@ export type PluginRuntime = {
|
||||
mentions: {
|
||||
buildMentionRegexes: BuildMentionRegexes;
|
||||
matchesMentionPatterns: MatchesMentionPatterns;
|
||||
matchesMentionWithExplicit: MatchesMentionWithExplicit;
|
||||
};
|
||||
reactions: {
|
||||
shouldAckReaction: ShouldAckReaction;
|
||||
|
||||
@@ -12,7 +12,10 @@ import {
|
||||
recordPendingHistoryEntryIfEnabled,
|
||||
} from "../../../auto-reply/reply/history.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 { enqueueSystemEvent } from "../../../infra/system-events.js";
|
||||
import { buildPairingReply } from "../../../pairing/pairing-messages.js";
|
||||
@@ -204,11 +207,22 @@ export async function prepareSlackMessage(params: {
|
||||
isThreadReply && ctx.threadHistoryScope === "thread" ? sessionKey : message.channel;
|
||||
|
||||
const mentionRegexes = buildMentionRegexes(cfg, route.agentId);
|
||||
const hasAnyMention = /<@[^>]+>/.test(message.text ?? "");
|
||||
const explicitlyMentioned = Boolean(
|
||||
ctx.botUserId && message.text?.includes(`<@${ctx.botUserId}>`),
|
||||
);
|
||||
const wasMentioned =
|
||||
opts.wasMentioned ??
|
||||
(!isDirectMessage &&
|
||||
(Boolean(ctx.botUserId && message.text?.includes(`<@${ctx.botUserId}>`)) ||
|
||||
matchesMentionPatterns(message.text ?? "", mentionRegexes)));
|
||||
matchesMentionWithExplicit({
|
||||
text: message.text ?? "",
|
||||
mentionRegexes,
|
||||
explicit: {
|
||||
hasAnyMention,
|
||||
isExplicitlyMentioned: explicitlyMentioned,
|
||||
canResolveExplicit: Boolean(ctx.botUserId),
|
||||
},
|
||||
}));
|
||||
const implicitMention = Boolean(
|
||||
!isDirectMessage &&
|
||||
ctx.botUserId &&
|
||||
@@ -232,7 +246,6 @@ export async function prepareSlackMessage(params: {
|
||||
return null;
|
||||
}
|
||||
|
||||
const hasAnyMention = /<@[^>]+>/.test(message.text ?? "");
|
||||
const allowTextCommands = shouldHandleTextCommands({
|
||||
cfg,
|
||||
surface: "slack",
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
type HistoryEntry,
|
||||
} from "../auto-reply/reply/history.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 { recordInboundSession } from "../channels/session.js";
|
||||
import { formatCliCommand } from "../cli/command-format.js";
|
||||
@@ -299,13 +299,20 @@ export const buildTelegramMessageContext = async ({
|
||||
if (!bodyText && allMedia.length > 0) {
|
||||
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(
|
||||
(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) {
|
||||
logInboundDrop({
|
||||
log: logVerbose,
|
||||
|
||||
@@ -43,13 +43,16 @@ export function isBotMentionedFromTargets(
|
||||
|
||||
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.selfJid && targets.selfE164) {
|
||||
if (targets.selfJid) {
|
||||
// Some mentions use the bare JID; match on E.164 to be safe.
|
||||
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.
|
||||
}
|
||||
const bodyClean = clean(msg.body);
|
||||
|
||||
Reference in New Issue
Block a user