feat: add channel match metadata logs
Co-authored-by: thewilloftheshadow <thewilloftheshadow@users.noreply.github.com>
This commit is contained in:
@@ -175,6 +175,7 @@ Notes:
|
|||||||
- `agents.list[].groupChat.mentionPatterns` (or `messages.groupChat.mentionPatterns`) also count as mentions for guild messages.
|
- `agents.list[].groupChat.mentionPatterns` (or `messages.groupChat.mentionPatterns`) also count as mentions for guild messages.
|
||||||
- Multi-agent override: set per-agent patterns on `agents.list[].groupChat.mentionPatterns`.
|
- Multi-agent override: set per-agent patterns on `agents.list[].groupChat.mentionPatterns`.
|
||||||
- If `channels` is present, any channel not listed is denied by default.
|
- If `channels` is present, any channel not listed is denied by default.
|
||||||
|
- Threads inherit parent channel config (allowlist, `requireMention`, skills, prompts, etc.) unless you add the thread id explicitly.
|
||||||
- Bot-authored messages are ignored by default; set `channels.discord.allowBots=true` to allow them (own messages remain filtered).
|
- Bot-authored messages are ignored by default; set `channels.discord.allowBots=true` to allow them (own messages remain filtered).
|
||||||
- Warning: If you allow replies to other bots (`channels.discord.allowBots=true`), prevent bot-to-bot reply loops with `requireMention`, `channels.discord.guilds.*.channels.<id>.users` allowlists, and/or clear guardrails in `AGENTS.md` and `SOUL.md`.
|
- Warning: If you allow replies to other bots (`channels.discord.allowBots=true`), prevent bot-to-bot reply loops with `requireMention`, `channels.discord.guilds.*.channels.<id>.users` allowlists, and/or clear guardrails in `AGENTS.md` and `SOUL.md`.
|
||||||
|
|
||||||
|
|||||||
@@ -216,6 +216,7 @@ Telegram forum topics include a `message_thread_id` per message. Clawdbot:
|
|||||||
- General topic (thread id `1`) is special: message sends omit `message_thread_id` (Telegram rejects it), but typing indicators still include it.
|
- General topic (thread id `1`) is special: message sends omit `message_thread_id` (Telegram rejects it), but typing indicators still include it.
|
||||||
- Exposes `MessageThreadId` + `IsForum` in template context for routing/templating.
|
- Exposes `MessageThreadId` + `IsForum` in template context for routing/templating.
|
||||||
- Topic-specific configuration is available under `channels.telegram.groups.<chatId>.topics.<threadId>` (skills, allowlists, auto-reply, system prompts, disable).
|
- Topic-specific configuration is available under `channels.telegram.groups.<chatId>.topics.<threadId>` (skills, allowlists, auto-reply, system prompts, disable).
|
||||||
|
- Topic configs inherit group settings (requireMention, allowlists, skills, prompts, enabled) unless overridden per topic.
|
||||||
|
|
||||||
Private chats can include `message_thread_id` in some edge cases. Clawdbot keeps the DM session key unchanged, but still uses the thread id for replies/draft streaming when it is present.
|
Private chats can include `message_thread_id` in some edge cases. Clawdbot keeps the DM session key unchanged, but still uses the thread id for replies/draft streaming when it is present.
|
||||||
|
|
||||||
|
|||||||
@@ -89,6 +89,18 @@ describe("msteams policy", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("inherits team mention settings when channel config is missing", () => {
|
||||||
|
const policy = resolveMSTeamsReplyPolicy({
|
||||||
|
isDirectMessage: false,
|
||||||
|
globalConfig: { requireMention: true },
|
||||||
|
teamConfig: { requireMention: false },
|
||||||
|
});
|
||||||
|
expect(policy).toEqual({
|
||||||
|
requireMention: false,
|
||||||
|
replyStyle: "top-level",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("uses explicit replyStyle even when requireMention defaults would differ", () => {
|
it("uses explicit replyStyle even when requireMention defaults would differ", () => {
|
||||||
const policy = resolveMSTeamsReplyPolicy({
|
const policy = resolveMSTeamsReplyPolicy({
|
||||||
isDirectMessage: false,
|
isDirectMessage: false,
|
||||||
|
|||||||
2
src/channels/plugins/channel-config.ts
Normal file
2
src/channels/plugins/channel-config.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export type { ChannelEntryMatch } from "../channel-config.js";
|
||||||
|
export { buildChannelKeyCandidates, resolveChannelEntryMatch } from "../channel-config.js";
|
||||||
@@ -84,4 +84,9 @@ export {
|
|||||||
listWhatsAppDirectoryGroupsFromConfig,
|
listWhatsAppDirectoryGroupsFromConfig,
|
||||||
listWhatsAppDirectoryPeersFromConfig,
|
listWhatsAppDirectoryPeersFromConfig,
|
||||||
} from "./directory-config.js";
|
} from "./directory-config.js";
|
||||||
|
export {
|
||||||
|
buildChannelKeyCandidates,
|
||||||
|
resolveChannelEntryMatch,
|
||||||
|
type ChannelEntryMatch,
|
||||||
|
} from "./channel-config.js";
|
||||||
export type { ChannelId, ChannelPlugin } from "./types.js";
|
export type { ChannelId, ChannelPlugin } from "./types.js";
|
||||||
|
|||||||
@@ -249,6 +249,34 @@ describe("discord mention gating", () => {
|
|||||||
}),
|
}),
|
||||||
).toBe(false);
|
).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("inherits parent channel mention rules for threads", () => {
|
||||||
|
const guildInfo: DiscordGuildEntryResolved = {
|
||||||
|
requireMention: true,
|
||||||
|
channels: {
|
||||||
|
"parent-1": { allow: true, requireMention: false },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const channelConfig = resolveDiscordChannelConfigWithFallback({
|
||||||
|
guildInfo,
|
||||||
|
channelId: "thread-1",
|
||||||
|
channelName: "topic",
|
||||||
|
channelSlug: "topic",
|
||||||
|
parentId: "parent-1",
|
||||||
|
parentName: "Parent",
|
||||||
|
parentSlug: "parent",
|
||||||
|
scope: "thread",
|
||||||
|
});
|
||||||
|
expect(channelConfig?.matchSource).toBe("parent");
|
||||||
|
expect(
|
||||||
|
resolveDiscordShouldRequireMention({
|
||||||
|
isGuildMessage: true,
|
||||||
|
isThread: true,
|
||||||
|
channelConfig,
|
||||||
|
guildInfo,
|
||||||
|
}),
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("discord groupPolicy gating", () => {
|
describe("discord groupPolicy gating", () => {
|
||||||
|
|||||||
@@ -252,8 +252,11 @@ export async function preflightDiscordMessage(
|
|||||||
scope: threadChannel ? "thread" : "channel",
|
scope: threadChannel ? "thread" : "channel",
|
||||||
})
|
})
|
||||||
: null;
|
: null;
|
||||||
|
const channelMatchMeta = `matchKey=${channelConfig?.matchKey ?? "none"} matchSource=${
|
||||||
|
channelConfig?.matchSource ?? "none"
|
||||||
|
}`;
|
||||||
if (isGuildMessage && channelConfig?.enabled === false) {
|
if (isGuildMessage && channelConfig?.enabled === false) {
|
||||||
logVerbose(`Blocked discord channel ${message.channelId} (channel disabled)`);
|
logVerbose(`Blocked discord channel ${message.channelId} (channel disabled, ${channelMatchMeta})`);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -280,21 +283,28 @@ export async function preflightDiscordMessage(
|
|||||||
})
|
})
|
||||||
) {
|
) {
|
||||||
if (params.groupPolicy === "disabled") {
|
if (params.groupPolicy === "disabled") {
|
||||||
logVerbose("discord: drop guild message (groupPolicy: disabled)");
|
logVerbose(`discord: drop guild message (groupPolicy: disabled, ${channelMatchMeta})`);
|
||||||
} else if (!channelAllowlistConfigured) {
|
} else if (!channelAllowlistConfigured) {
|
||||||
logVerbose("discord: drop guild message (groupPolicy: allowlist, no channel allowlist)");
|
logVerbose(
|
||||||
|
`discord: drop guild message (groupPolicy: allowlist, no channel allowlist, ${channelMatchMeta})`,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
logVerbose(
|
logVerbose(
|
||||||
`Blocked discord channel ${message.channelId} not in guild channel allowlist (groupPolicy: allowlist)`,
|
`Blocked discord channel ${message.channelId} not in guild channel allowlist (groupPolicy: allowlist, ${channelMatchMeta})`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isGuildMessage && channelConfig?.allowed === false) {
|
if (isGuildMessage && channelConfig?.allowed === false) {
|
||||||
logVerbose(`Blocked discord channel ${message.channelId} not in guild channel allowlist`);
|
logVerbose(
|
||||||
|
`Blocked discord channel ${message.channelId} not in guild channel allowlist (${channelMatchMeta})`,
|
||||||
|
);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
if (isGuildMessage) {
|
||||||
|
logVerbose(`discord: allow channel ${message.channelId} (${channelMatchMeta})`);
|
||||||
|
}
|
||||||
|
|
||||||
const textForHistory = resolveDiscordMessageText(message, {
|
const textForHistory = resolveDiscordMessageText(message, {
|
||||||
includeForwarded: true,
|
includeForwarded: true,
|
||||||
|
|||||||
@@ -28,4 +28,18 @@ describe("resolveSlackChannelConfig", () => {
|
|||||||
});
|
});
|
||||||
expect(res).toMatchObject({ requireMention: true });
|
expect(res).toMatchObject({ requireMention: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("uses wildcard entries when no direct channel config exists", () => {
|
||||||
|
const res = resolveSlackChannelConfig({
|
||||||
|
channelId: "C1",
|
||||||
|
channels: { "*": { allow: true, requireMention: false } },
|
||||||
|
defaultRequireMention: true,
|
||||||
|
});
|
||||||
|
expect(res).toMatchObject({
|
||||||
|
allowed: true,
|
||||||
|
requireMention: false,
|
||||||
|
matchKey: "*",
|
||||||
|
matchSource: "wildcard",
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -310,6 +310,9 @@ export function createSlackMonitorContext(params: {
|
|||||||
channels: params.channelsConfig,
|
channels: params.channelsConfig,
|
||||||
defaultRequireMention,
|
defaultRequireMention,
|
||||||
});
|
});
|
||||||
|
const channelMatchMeta = `matchKey=${channelConfig?.matchKey ?? "none"} matchSource=${
|
||||||
|
channelConfig?.matchSource ?? "none"
|
||||||
|
}`;
|
||||||
const channelAllowed = channelConfig?.allowed !== false;
|
const channelAllowed = channelConfig?.allowed !== false;
|
||||||
const channelAllowlistConfigured =
|
const channelAllowlistConfigured =
|
||||||
Boolean(params.channelsConfig) && Object.keys(params.channelsConfig ?? {}).length > 0;
|
Boolean(params.channelsConfig) && Object.keys(params.channelsConfig ?? {}).length > 0;
|
||||||
@@ -320,9 +323,16 @@ export function createSlackMonitorContext(params: {
|
|||||||
channelAllowed,
|
channelAllowed,
|
||||||
})
|
})
|
||||||
) {
|
) {
|
||||||
|
logVerbose(
|
||||||
|
`slack: drop channel ${p.channelId} (groupPolicy=${params.groupPolicy}, ${channelMatchMeta})`,
|
||||||
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!channelAllowed) return false;
|
if (!channelAllowed) {
|
||||||
|
logVerbose(`slack: drop channel ${p.channelId} (${channelMatchMeta})`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
logVerbose(`slack: allow channel ${p.channelId} (${channelMatchMeta})`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -1159,6 +1159,50 @@ describe("createTelegramBot", () => {
|
|||||||
expect(replySpy).toHaveBeenCalledTimes(1);
|
expect(replySpy).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("inherits group allowlist + requireMention in topics", async () => {
|
||||||
|
onSpy.mockReset();
|
||||||
|
const replySpy = replyModule.__replySpy as unknown as ReturnType<typeof vi.fn>;
|
||||||
|
replySpy.mockReset();
|
||||||
|
loadConfig.mockReturnValue({
|
||||||
|
channels: {
|
||||||
|
telegram: {
|
||||||
|
groupPolicy: "allowlist",
|
||||||
|
groups: {
|
||||||
|
"-1001234567890": {
|
||||||
|
requireMention: false,
|
||||||
|
allowFrom: ["123456789"],
|
||||||
|
topics: {
|
||||||
|
"99": {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
createTelegramBot({ token: "tok" });
|
||||||
|
const handler = getOnHandler("message") as (ctx: Record<string, unknown>) => Promise<void>;
|
||||||
|
|
||||||
|
await handler({
|
||||||
|
message: {
|
||||||
|
chat: {
|
||||||
|
id: -1001234567890,
|
||||||
|
type: "supergroup",
|
||||||
|
title: "Forum Group",
|
||||||
|
is_forum: true,
|
||||||
|
},
|
||||||
|
from: { id: 123456789, username: "testuser" },
|
||||||
|
text: "hello",
|
||||||
|
date: 1736380800,
|
||||||
|
message_thread_id: 99,
|
||||||
|
},
|
||||||
|
me: { username: "clawdbot_bot" },
|
||||||
|
getFile: async () => ({ download: async () => new Uint8Array() }),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(replySpy).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
it("honors groups default when no explicit group override exists", async () => {
|
it("honors groups default when no explicit group override exists", async () => {
|
||||||
onSpy.mockReset();
|
onSpy.mockReset();
|
||||||
const replySpy = replyModule.__replySpy as unknown as ReturnType<typeof vi.fn>;
|
const replySpy = replyModule.__replySpy as unknown as ReturnType<typeof vi.fn>;
|
||||||
|
|||||||
Reference in New Issue
Block a user