feat: scope group mention patterns by channel

Provider-scoped configured regex mention patterns for Discord, Matrix, Slack, Telegram, and WhatsApp.

Native platform mentions keep their existing behavior, and unsupported channels do not opt into the new regex policy path. The new policy supports per-channel allow/deny routing through mentionPatterns.mode with allowIn and denyIn so group auto-reply regexes can be limited without broad global blast radius.

Refs #70864.
Supersedes #87200.
Thanks @patrick-slimelab.
This commit is contained in:
Patrick Star
2026-05-31 05:34:56 -04:00
committed by GitHub
parent f94512cd7f
commit 9c1adf4e51
32 changed files with 307 additions and 42 deletions

View File

@@ -1,4 +1,4 @@
370da2e3a4253f00c3963a3ad8b57707ea3f67a8d0d394b7d2b96db4f3413d32 config-baseline.json
6a66c70d36dacf5fd1a8b7e157d1ff4812e97f518c13ebc3190509df4c269f29 config-baseline.core.json
a9102c0611b8170fac37853cc31771810f31757a9e3b2c6796bbd9625f9b9206 config-baseline.channel.json
923a8cac695c752e51751cc2dea185a3fbe19d0015722f7ea1909f897dfbb898 config-baseline.plugin.json
8162a661edc183008a336db265a092acc90762c6c547b1383ef14fd0d381dea5 config-baseline.json
5ee177382cf32c2816dca0a4e67cd6c01df1045d600b21a6e9c11639ddb10ce8 config-baseline.core.json
7a7aba829deb8b54047b5c9e7dd3f3c9eade721e2f728db339c4ea99b77a162e config-baseline.channel.json
e6a1d6f51f0d9c04bd92d51deebfaca8c7917dd28d7998d225c0074e0a095348 config-baseline.plugin.json

View File

@@ -1,2 +1,2 @@
3cc84cf3d30697d541ba98a5c1835784a4254a9193e51b009372e1620948e430 plugin-sdk-api-baseline.json
515c9e2972f0d79dbed27ffae815a96d432a341005046d603a82a235c4108340 plugin-sdk-api-baseline.jsonl
34d396bf8f1b2963884256e87c8879a378e2ce7c8064ae0c30d734085a305dd6 plugin-sdk-api-baseline.json
bf3e94dcccaf169811990dfd058a16ace8ce2ab44ea9eac6042b525b6d8baf5f plugin-sdk-api-baseline.jsonl

View File

@@ -362,6 +362,8 @@ Replying to a bot message counts as an implicit mention when the channel support
<Accordion title="Mention gating notes">
- `mentionPatterns` are case-insensitive safe regex patterns; invalid patterns and unsafe nested-repetition forms are ignored.
- Surfaces that provide explicit mentions still pass; patterns are a fallback.
- `channels.<channel>.mentionPatterns.mode: "deny"` disables configured mention patterns by default for that channel; opt selected conversations back in with `allowIn`.
- `channels.<channel>.mentionPatterns.denyIn` disables configured mention patterns for specific conversation IDs while native platform @mentions still pass.
- Per-agent override: `agents.list[].groupChat.mentionPatterns` (useful when multiple agents share a group).
- Mention gating is only enforced when mention detection is possible (native mentions or `mentionPatterns` are configured).
- Allowlisting a group or sender does not disable mention gating; set that group's `requireMention` to `false` when all messages should trigger.

View File

@@ -425,7 +425,11 @@ export async function preflightDiscordMessage(
logVerbose(`discord: drop bound-thread bot system message ${message.id}`);
return null;
}
const mentionRegexes = buildMentionRegexes(params.cfg, effectiveRoute.agentId);
const mentionRegexes = buildMentionRegexes(params.cfg, effectiveRoute.agentId, {
provider: "discord",
conversationId: messageChannelId,
providerPolicy: params.discordConfig?.mentionPatterns,
});
const explicitlyMentioned = Boolean(
botId && message.mentionedUsers?.some((user: User) => user.id === botId),
);

View File

@@ -5,6 +5,7 @@ import {
ContextVisibilityModeSchema,
GroupPolicySchema,
MarkdownConfigSchema,
MentionPatternsPolicySchema,
ToolPolicySchema,
} from "openclaw/plugin-sdk/channel-config-schema";
import { buildSecretInputSchema } from "openclaw/plugin-sdk/secret-input";
@@ -121,6 +122,7 @@ export const MatrixConfigSchema = z.object({
allowBots: z.union([z.boolean(), z.literal("mentions")]).optional(),
botLoopProtection: botLoopProtectionSchema,
groupPolicy: GroupPolicySchema.optional(),
mentionPatterns: MentionPatternsPolicySchema.optional(),
contextVisibility: ContextVisibilityModeSchema.optional(),
blockStreaming: z.boolean().optional(),
streaming: z

View File

@@ -965,7 +965,11 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
resolveAgentRoute: core.channel.routing.resolveAgentRoute,
});
const hasExplicitSessionBinding = _configuredBinding !== null || _runtimeBindingId !== null;
const agentMentionRegexes = core.channel.mentions.buildMentionRegexes(cfg, _route.agentId);
const agentMentionRegexes = core.channel.mentions.buildMentionRegexes(cfg, _route.agentId, {
provider: "matrix",
conversationId: roomId,
providerPolicy: accountConfig?.mentionPatterns,
});
const selfDisplayName = content.formatted_body
? await getMemberDisplayName(roomId, selfUserId).catch(() => undefined)
: undefined;

View File

@@ -1,4 +1,7 @@
import type { ChannelBotLoopProtectionConfig } from "openclaw/plugin-sdk/config-contracts";
import type {
ChannelBotLoopProtectionConfig,
MentionPatternsPolicyConfig,
} from "openclaw/plugin-sdk/config-contracts";
import type {
ContextVisibilityMode,
DmPolicy,
@@ -153,6 +156,8 @@ export type MatrixConfig = {
botLoopProtection?: ChannelBotLoopProtectionConfig;
/** Group message policy (default: allowlist). */
groupPolicy?: GroupPolicy;
/** Scope configured groupChat mentionPatterns to selected Matrix room IDs. */
mentionPatterns?: MentionPatternsPolicyConfig;
/** Supplemental context visibility policy (all|allowlist|allowlist_quote). */
contextVisibility?: ContextVisibilityMode;
/**

View File

@@ -205,8 +205,14 @@ async function restoreSlackAssistantThreadContextFromMetadata(params: {
function resolveCachedMentionRegexes(
ctx: SlackMonitorContext,
agentId: string | undefined,
options?: Parameters<typeof buildMentionRegexes>[2],
): RegExp[] {
const key = normalizeOptionalString(agentId) ?? "__default__";
const key = [
normalizeOptionalString(agentId) ?? "__default__",
normalizeOptionalString(options?.provider),
normalizeOptionalString(options?.conversationId ?? undefined),
options?.providerPolicy ? JSON.stringify(options.providerPolicy) : "",
].join("\u001f");
let byAgent = mentionRegexCache.get(ctx);
if (!byAgent) {
byAgent = new Map<string, RegExp[]>();
@@ -216,7 +222,7 @@ function resolveCachedMentionRegexes(
if (cached) {
return cached;
}
const built = buildMentionRegexes(ctx.cfg, agentId);
const built = buildMentionRegexes(ctx.cfg, agentId, options);
byAgent.set(key, built);
return built;
}
@@ -721,7 +727,13 @@ export async function prepareSlackMessage(params: {
canResolveExplicit: Boolean(ctx.botUserId),
},
}));
let mentionRegexes = resolveCachedMentionRegexes(ctx, routing.route.agentId);
const buildPolicyMentionRegexes = (agentId: string | undefined) =>
resolveCachedMentionRegexes(ctx, agentId, {
provider: "slack",
conversationId: message.channel,
providerPolicy: account.config.mentionPatterns,
});
let mentionRegexes = buildPolicyMentionRegexes(routing.route.agentId);
let wasMentioned = resolveWasMentioned(mentionRegexes);
const hasBoundSession = Boolean(
routing.runtimeBoundSessionKey || routing.configuredBindingSessionKey,
@@ -746,7 +758,7 @@ export async function prepareSlackMessage(params: {
seedTopLevelRoomThread: true,
assistantThreadTs: assistantThreadContext?.threadTs,
});
mentionRegexes = resolveCachedMentionRegexes(ctx, routing.route.agentId);
mentionRegexes = buildPolicyMentionRegexes(routing.route.agentId);
wasMentioned = resolveWasMentioned(mentionRegexes);
}
const {

View File

@@ -5,6 +5,7 @@ import {
logInboundDrop,
matchesMentionWithExplicit,
resolveInboundMentionDecision,
type BuildMentionRegexesOptions,
type NormalizedLocation,
} from "openclaw/plugin-sdk/channel-inbound";
import { resolveChannelGroupPolicy } from "openclaw/plugin-sdk/channel-policy";
@@ -148,6 +149,7 @@ export async function resolveTelegramInboundBody(params: {
effectiveDmAllow: NormalizedAllowFrom;
groupConfig?: TelegramGroupConfig | TelegramDirectConfig;
topicConfig?: TelegramTopicConfig;
providerMentionPatterns?: BuildMentionRegexesOptions["providerPolicy"];
requireMention?: boolean;
options?: TelegramMessageContextOptions;
groupHistories: Map<string, HistoryEntry[]>;
@@ -173,6 +175,7 @@ export async function resolveTelegramInboundBody(params: {
effectiveDmAllow,
groupConfig,
topicConfig,
providerMentionPatterns,
requireMention,
options,
groupHistories,
@@ -180,7 +183,11 @@ export async function resolveTelegramInboundBody(params: {
logger,
} = params;
const botUsername = normalizeOptionalLowercaseString(primaryCtx.me?.username);
const mentionRegexes = buildMentionRegexes(cfg, routeAgentId);
const mentionRegexes = buildMentionRegexes(cfg, routeAgentId, {
provider: "telegram",
conversationId: isGroup ? buildTelegramGroupPeerId(chatId, resolvedThreadId) : String(chatId),
providerPolicy: providerMentionPatterns,
});
const messageTextParts = getTelegramTextParts(msg);
const allowForCommands = isGroup ? effectiveGroupAllow : effectiveDmAllow;
const useAccessGroups = cfg.commands?.useAccessGroups !== false;

View File

@@ -471,6 +471,7 @@ export const buildTelegramMessageContext = async ({
effectiveDmAllow: dmAllow.effectiveAllow,
groupConfig,
topicConfig,
providerMentionPatterns: cfg.channels?.telegram?.accounts?.[account.accountId]?.mentionPatterns,
requireMention,
options,
groupHistories,

View File

@@ -33,6 +33,7 @@ export type ResolvedWhatsAppAccount = {
allowFrom?: string[];
groupAllowFrom?: string[];
groupPolicy?: GroupPolicy;
mentionPatterns?: WhatsAppAccountConfig["mentionPatterns"];
dmPolicy?: DmPolicy;
historyLimit?: number;
textChunkLimit?: number;
@@ -141,6 +142,7 @@ export function resolveWhatsAppAccount(params: {
allowFrom: merged.allowFrom,
groupAllowFrom: merged.groupAllowFrom,
groupPolicy: merged.groupPolicy,
mentionPatterns: merged.mentionPatterns,
historyLimit: merged.historyLimit,
textChunkLimit: merged.textChunkLimit,
chunkMode: merged.chunkMode,

View File

@@ -25,8 +25,12 @@ export type MentionTargets = {
self: WhatsAppIdentity;
};
export function buildMentionConfig(cfg: OpenClawConfig, agentId?: string): MentionConfig {
const mentionRegexes = buildMentionRegexes(cfg, agentId);
export function buildMentionConfig(
cfg: OpenClawConfig,
agentId?: string,
options?: Parameters<typeof buildMentionRegexes>[2],
): MentionConfig {
const mentionRegexes = buildMentionRegexes(cfg, agentId, options);
return { mentionRegexes, allowFrom: cfg.channels?.whatsapp?.allowFrom };
}

View File

@@ -1,3 +1,4 @@
import type { BuildMentionRegexesOptions } from "openclaw/plugin-sdk/channel-mention-gating";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
import { resolveWhatsAppGroupsConfigPath } from "../../group-config-path.js";
import {
@@ -41,6 +42,7 @@ type ApplyGroupGatingParams = {
agentId: string;
sessionKey: string;
baseMentionConfig: MentionConfig;
providerMentionPatterns?: BuildMentionRegexesOptions["providerPolicy"];
authDir?: string;
groupHistories: Map<string, GroupHistoryEntry[]>;
groupHistoryLimit: number;
@@ -167,7 +169,11 @@ export async function applyGroupGating(params: ApplyGroupGatingParams) {
allowFrom: inboundPolicy.configuredAllowFrom,
};
const mentionConfig = {
...buildMentionConfig(params.cfg, params.agentId),
...buildMentionConfig(params.cfg, params.agentId, {
provider: "whatsapp",
conversationId: params.conversationId,
providerPolicy: params.providerMentionPatterns,
}),
allowFrom: inboundPolicy.configuredAllowFrom,
};
const mentionMsg =

View File

@@ -250,6 +250,7 @@ export function createWebOnMessageHandler(params: {
agentId: route.agentId,
sessionKey: route.sessionKey,
baseMentionConfig,
providerMentionPatterns: account.mentionPatterns,
authDir: account.authDir,
selfChatMode: account.selfChatMode,
groupHistories: params.groupHistories,
@@ -275,6 +276,7 @@ export function createWebOnMessageHandler(params: {
agentId: route.agentId,
sessionKey: route.sessionKey,
baseMentionConfig,
providerMentionPatterns: account.mentionPatterns,
authDir: account.authDir,
selfChatMode: account.selfChatMode,
groupHistories: params.groupHistories,

View File

@@ -973,6 +973,81 @@ describe("mention helpers", () => {
expect(matchesMentionPatterns("global: hi", regexes)).toBe(false);
});
it("scopes configured mention patterns by provider conversation policy", () => {
const cfg = {
messages: {
groupChat: {
mentionPatterns: ["\\bopenclaw\\b"],
},
},
channels: {
slack: {
mentionPatterns: {
mode: "deny",
allowIn: ["C123"],
},
},
},
} satisfies OpenClawConfig;
const allowed = buildMentionRegexes(cfg, undefined, {
provider: "slack",
conversationId: "C123",
});
const denied = buildMentionRegexes(cfg, undefined, {
provider: "slack",
conversationId: "C999",
});
expect(matchesMentionPatterns("openclaw: hi", allowed)).toBe(true);
expect(matchesMentionPatterns("openclaw: hi", denied)).toBe(false);
});
it("preserves mention patterns for callers without scoped policy facts", () => {
const regexes = buildMentionRegexes({
messages: {
groupChat: {
mentionPatterns: ["\\bopenclaw\\b"],
},
},
});
expect(matchesMentionPatterns("openclaw", regexes)).toBe(true);
});
it("lets provider deny lists override globally allowed mention patterns", () => {
const cfg = {
messages: {
groupChat: {
mentionPatterns: ["\\bopenclaw\\b"],
},
},
channels: {
telegram: {
mentionPatterns: {
denyIn: ["-100:topic:7"],
},
},
},
} satisfies OpenClawConfig;
expect(
buildMentionRegexes(cfg, undefined, {
provider: "telegram",
conversationId: "-100:topic:7",
}),
).toEqual([]);
expect(
matchesMentionPatterns(
"openclaw",
buildMentionRegexes(cfg, undefined, {
provider: "telegram",
conversationId: "-100:topic:8",
}),
),
).toBe(true);
});
it("strips safe mention patterns and ignores unsafe ones", () => {
const stripped = stripMentions("openclaw " + "a".repeat(28) + "!", {} as MsgContext, {
messages: {

View File

@@ -4,6 +4,7 @@ import {
normalizeOptionalString,
} from "@openclaw/normalization-core/string-coerce";
import { resolveAgentConfig } from "../../agents/agent-scope.js";
import { resolveMentionPatternPolicy } from "../../channels/mention-pattern-policy.js";
import type { ChannelId } from "../../channels/plugins/channel-id.types.js";
import { getLoadedChannelPluginById } from "../../channels/plugins/registry-loaded.js";
import type { ChannelPlugin } from "../../channels/plugins/types.plugin.js";
@@ -13,8 +14,8 @@ import { createSubsystemLogger } from "../../logging/subsystem.js";
import { compileConfigRegexes, type ConfigRegexRejectReason } from "../../security/config-regex.js";
import { escapeRegExp } from "../../utils.js";
import type { MsgContext } from "../templating.js";
import type { ExplicitMentionSignal } from "./mentions.types.js";
export type { ExplicitMentionSignal } from "./mentions.types.js";
import type { BuildMentionRegexesOptions, ExplicitMentionSignal } from "./mentions.types.js";
export type { BuildMentionRegexesOptions, ExplicitMentionSignal } from "./mentions.types.js";
function deriveMentionPatterns(identity?: { name?: string; emoji?: string }) {
const patterns: string[] = [];
@@ -127,7 +128,14 @@ function resolveMentionPatterns(cfg: OpenClawConfig | undefined, agentId?: strin
return derived.length > 0 ? derived : [];
}
export function buildMentionRegexes(cfg: OpenClawConfig | undefined, agentId?: string): RegExp[] {
export function buildMentionRegexes(
cfg: OpenClawConfig | undefined,
agentId?: string,
options?: BuildMentionRegexesOptions,
): RegExp[] {
if (!resolveMentionPatternPolicy({ ...options, cfg, agentId }).enabled) {
return [];
}
const patterns = normalizeMentionPatterns(resolveMentionPatterns(cfg, agentId));
return compileMentionPatternsCached({
patterns,

View File

@@ -1,6 +1,13 @@
import type { ResolveMentionPatternPolicyParams } from "../../channels/mention-pattern-policy.js";
import type { OpenClawConfig } from "../../config/types.openclaw.js";
export type BuildMentionRegexes = (cfg: OpenClawConfig | undefined, agentId?: string) => RegExp[];
export type BuildMentionRegexesOptions = Omit<ResolveMentionPatternPolicyParams, "cfg" | "agentId">;
export type BuildMentionRegexes = (
cfg: OpenClawConfig | undefined,
agentId?: string,
options?: BuildMentionRegexesOptions,
) => RegExp[];
export type MatchesMentionPatterns = (text: string, mentionRegexes: RegExp[]) => boolean;

View File

@@ -0,0 +1,63 @@
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
import type { MentionPatternsMode, MentionPatternsPolicyConfig } from "../config/types.messages.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
export type ResolveMentionPatternPolicyParams = {
cfg?: OpenClawConfig;
provider?: string;
conversationId?: string | null;
providerPolicy?: MentionPatternsPolicyConfig;
agentId?: string;
};
export type ResolvedMentionPatternPolicy = {
effectiveMode: MentionPatternsMode;
allowMatched: boolean;
denyMatched: boolean;
enabled: boolean;
};
function normalizeIdList(values?: string[]): Set<string> {
const normalized = new Set<string>();
for (const value of values ?? []) {
const next = normalizeOptionalString(value);
if (next) {
normalized.add(next);
}
}
return normalized;
}
function isMentionPatternsPolicyConfig(value: unknown): value is MentionPatternsPolicyConfig {
return value != null && typeof value === "object" && !Array.isArray(value);
}
function resolveProviderMentionPatternsPolicy(
cfg: OpenClawConfig | undefined,
provider: string | undefined,
): MentionPatternsPolicyConfig | undefined {
if (!cfg || !provider) {
return undefined;
}
const policy = cfg.channels?.[provider]?.mentionPatterns;
return isMentionPatternsPolicyConfig(policy) ? policy : undefined;
}
export function resolveMentionPatternPolicy(
params: ResolveMentionPatternPolicyParams,
): ResolvedMentionPatternPolicy {
const conversationId = normalizeOptionalString(params.conversationId ?? undefined) ?? undefined;
const providerPolicy =
params.providerPolicy ?? resolveProviderMentionPatternsPolicy(params.cfg, params.provider);
const effectiveMode =
providerPolicy?.mode === "allow" || providerPolicy?.mode === "deny"
? providerPolicy.mode
: "allow";
const allowMatched =
conversationId != null && normalizeIdList(providerPolicy?.allowIn).has(conversationId);
const denyMatched =
conversationId != null && normalizeIdList(providerPolicy?.denyIn).has(conversationId);
const enabled = effectiveMode === "allow" ? !denyMatched : allowMatched && !denyMatched;
return { effectiveMode, allowMatched, denyMatched, enabled };
}

File diff suppressed because one or more lines are too long

View File

@@ -8,6 +8,7 @@ import type { DiscordConfig } from "./types.discord.js";
import type { GoogleChatConfig } from "./types.googlechat.js";
import type { IMessageConfig } from "./types.imessage.js";
import type { IrcConfig } from "./types.irc.js";
import type { MentionPatternsPolicyConfig } from "./types.messages.js";
import type { MSTeamsConfig } from "./types.msteams.js";
import type { SignalConfig } from "./types.signal.js";
import type { SlackConfig } from "./types.slack.js";
@@ -50,6 +51,7 @@ export type ExtensionChannelConfig = {
defaultAccount?: string;
dmPolicy?: string;
groupPolicy?: GroupPolicy;
mentionPatterns?: MentionPatternsPolicyConfig;
contextVisibility?: ContextVisibilityMode;
healthMonitor?: ChannelHealthMonitorConfig;
dm?: ExtensionNestedPolicyConfig;

View File

@@ -12,7 +12,11 @@ import type {
ChannelHealthMonitorConfig,
ChannelHeartbeatVisibilityConfig,
} from "./types.channel-health.js";
import type { DmConfig, ProviderCommandsConfig } from "./types.messages.js";
import type {
DmConfig,
MentionPatternsPolicyConfig,
ProviderCommandsConfig,
} from "./types.messages.js";
import type { SecretInput } from "./types.secrets.js";
import type { GroupToolPolicyBySenderConfig, GroupToolPolicyConfig } from "./types.tools.js";
import type { TtsConfig } from "./types.tts.js";
@@ -377,6 +381,8 @@ export type DiscordAccountConfig = {
* - "allowlist": only allow channels present in discord.guilds.*.channels
*/
groupPolicy?: GroupPolicy;
/** Scope configured groupChat mentionPatterns to selected Discord channel IDs. */
mentionPatterns?: MentionPatternsPolicyConfig;
/** Supplemental context visibility policy (all|allowlist|allowlist_quote). */
contextVisibility?: ContextVisibilityMode;
/** Outbound text chunk size (chars). Default: 2000. */

View File

@@ -1,6 +1,14 @@
import type { QueueDropPolicy, QueueMode, QueueModeByProvider } from "./types.queue.js";
import type { TtsConfig } from "./types.tts.js";
export type MentionPatternsMode = "allow" | "deny";
export type MentionPatternsPolicyConfig = {
mode?: MentionPatternsMode;
allowIn?: string[];
denyIn?: string[];
};
export type GroupChatConfig = {
mentionPatterns?: string[];
historyLimit?: number;

View File

@@ -15,7 +15,11 @@ import type {
ChannelHealthMonitorConfig,
ChannelHeartbeatVisibilityConfig,
} from "./types.channel-health.js";
import type { DmConfig, ProviderCommandsConfig } from "./types.messages.js";
import type {
DmConfig,
MentionPatternsPolicyConfig,
ProviderCommandsConfig,
} from "./types.messages.js";
import type { GroupToolPolicyBySenderConfig, GroupToolPolicyConfig } from "./types.tools.js";
export type SlackDmConfig = {
@@ -180,6 +184,8 @@ export type SlackAccountConfig = {
* - "allowlist": only allow channels present in channels.slack.channels
*/
groupPolicy?: GroupPolicy;
/** Scope configured groupChat mentionPatterns to selected Slack channel IDs. */
mentionPatterns?: MentionPatternsPolicyConfig;
/** Supplemental context visibility policy (all|allowlist|allowlist_quote). */
contextVisibility?: ContextVisibilityMode;
/** Max channel messages to keep as history context (0 disables). */

View File

@@ -13,7 +13,11 @@ import type {
ChannelHealthMonitorConfig,
ChannelHeartbeatVisibilityConfig,
} from "./types.channel-health.js";
import type { DmConfig, ProviderCommandsConfig } from "./types.messages.js";
import type {
DmConfig,
MentionPatternsPolicyConfig,
ProviderCommandsConfig,
} from "./types.messages.js";
import type { GroupToolPolicyBySenderConfig, GroupToolPolicyConfig } from "./types.tools.js";
export type TelegramActionConfig = {
@@ -152,6 +156,8 @@ export type TelegramAccountConfig = {
* - "allowlist": only allow group messages from senders in groupAllowFrom/allowFrom
*/
groupPolicy?: GroupPolicy;
/** Scope configured groupChat mentionPatterns to selected Telegram chat/thread IDs. */
mentionPatterns?: MentionPatternsPolicyConfig;
/** Supplemental context visibility policy (all|allowlist|allowlist_quote). */
contextVisibility?: ContextVisibilityMode;
/** Max group messages to keep as history context (0 disables). */

View File

@@ -11,7 +11,7 @@ import type {
ChannelHealthMonitorConfig,
ChannelHeartbeatVisibilityConfig,
} from "./types.channel-health.js";
import type { DmConfig } from "./types.messages.js";
import type { DmConfig, MentionPatternsPolicyConfig } from "./types.messages.js";
import type { GroupToolPolicyBySenderConfig, GroupToolPolicyConfig } from "./types.tools.js";
export type WhatsAppActionConfig = {
@@ -70,6 +70,8 @@ type WhatsAppSharedConfig = {
* - "allowlist": only allow group messages from senders in groupAllowFrom/allowFrom
*/
groupPolicy?: GroupPolicy;
/** Scope configured groupChat mentionPatterns to selected WhatsApp conversation IDs. */
mentionPatterns?: MentionPatternsPolicyConfig;
/** Supplemental context visibility policy (all|allowlist|allowlist_quote). */
contextVisibility?: ContextVisibilityMode;
/** Max group messages to keep as history context (0 disables). */

View File

@@ -549,6 +549,16 @@ export const VisibleRepliesSchema = z
return value;
});
export const MentionPatternsModeSchema = z.union([z.literal("allow"), z.literal("deny")]);
export const MentionPatternsPolicySchema = z
.object({
mode: MentionPatternsModeSchema.optional(),
allowIn: z.array(z.string()).optional(),
denyIn: z.array(z.string()).optional(),
})
.strict();
export const GroupChatSchema = z
.object({
mentionPatterns: z.array(z.string()).optional(),

View File

@@ -23,6 +23,7 @@ import {
GroupPolicySchema,
HexColorSchema,
MarkdownConfigSchema,
MentionPatternsPolicySchema,
MSTeamsReplyStyleSchema,
ProviderCommandsSchema,
SecretInputSchema,
@@ -271,6 +272,7 @@ export const TelegramAccountSchemaBase = z
defaultTo: z.union([z.string(), z.number()]).optional(),
groupAllowFrom: z.array(z.union([z.string(), z.number()])).optional(),
groupPolicy: GroupPolicySchema.optional().default("allowlist"),
mentionPatterns: MentionPatternsPolicySchema.optional(),
contextVisibility: ContextVisibilityModeSchema.optional(),
historyLimit: z.number().int().min(0).optional(),
dmHistoryLimit: z.number().int().min(0).optional(),
@@ -652,6 +654,7 @@ export const DiscordAccountSchema = z
dangerouslyAllowNameMatching: z.boolean().optional(),
mentionAliases: z.record(z.string(), DiscordSnowflakeStringSchema).optional(),
groupPolicy: GroupPolicySchema.optional().default("allowlist"),
mentionPatterns: MentionPatternsPolicySchema.optional(),
contextVisibility: ContextVisibilityModeSchema.optional(),
historyLimit: z.number().int().min(0).optional(),
dmHistoryLimit: z.number().int().min(0).optional(),
@@ -979,6 +982,7 @@ export const SlackAccountSchema = z
dangerouslyAllowNameMatching: z.boolean().optional(),
requireMention: z.boolean().optional(),
groupPolicy: GroupPolicySchema.optional(),
mentionPatterns: MentionPatternsPolicySchema.optional(),
contextVisibility: ContextVisibilityModeSchema.optional(),
historyLimit: z.number().int().min(0).optional(),
dmHistoryLimit: z.number().int().min(0).optional(),
@@ -1039,6 +1043,7 @@ export const SlackConfigSchema = SlackAccountSchema.safeExtend({
signingSecret: SecretInputSchema.optional().register(sensitive),
webhookPath: z.string().optional().default("/slack/events"),
groupPolicy: GroupPolicySchema.optional().default("allowlist"),
mentionPatterns: MentionPatternsPolicySchema.optional(),
contextVisibility: ContextVisibilityModeSchema.optional(),
accounts: z.record(z.string(), SlackAccountSchema.optional()).optional(),
defaultAccount: z.string().optional(),

View File

@@ -13,6 +13,7 @@ import {
DmPolicySchema,
GroupPolicySchema,
MarkdownConfigSchema,
MentionPatternsPolicySchema,
ReplyToModeSchema,
} from "./zod-schema.core.js";
@@ -86,6 +87,7 @@ function buildWhatsAppCommonShape(params: { useDefaults: boolean }) {
groupPolicy: params.useDefaults
? GroupPolicySchema.optional().default("allowlist")
: GroupPolicySchema.optional(),
mentionPatterns: MentionPatternsPolicySchema.optional(),
contextVisibility: ContextVisibilityModeSchema.optional(),
historyLimit: z.number().int().min(0).optional(),
dmHistoryLimit: z.number().int().min(0).optional(),

View File

@@ -13,6 +13,7 @@ export {
DmPolicySchema,
GroupPolicySchema,
MarkdownConfigSchema,
MentionPatternsPolicySchema,
ReplyRuntimeConfigSchemaShape,
requireAllowlistAllowFrom,
requireOpenAllowFrom,

View File

@@ -41,7 +41,13 @@ export {
matchesMentionPatterns,
matchesMentionWithExplicit,
normalizeMentionText,
type BuildMentionRegexesOptions,
} from "../auto-reply/reply/mentions.js";
export {
resolveMentionPatternPolicy,
type ResolveMentionPatternPolicyParams,
type ResolvedMentionPatternPolicy,
} from "../channels/mention-pattern-policy.js";
export {
createChannelInboundDebouncer,
shouldDebounceTextInbound,

View File

@@ -23,4 +23,10 @@ export {
CURRENT_MESSAGE_MARKER,
buildMentionRegexes,
normalizeMentionText,
type BuildMentionRegexesOptions,
} from "../auto-reply/reply/mentions.js";
export {
resolveMentionPatternPolicy,
type ResolveMentionPatternPolicyParams,
type ResolvedMentionPatternPolicy,
} from "../channels/mention-pattern-policy.js";

View File

@@ -28,6 +28,7 @@ export type {
GroupToolPolicyConfig,
MarkdownConfig,
MarkdownTableMode,
MentionPatternsPolicyConfig,
MSTeamsChannelConfig,
MSTeamsCloudName,
MSTeamsConfig,