feat(channels): add resolve command + defaults

This commit is contained in:
Peter Steinberger
2026-01-18 00:41:57 +00:00
parent b543339373
commit c7ea47e886
60 changed files with 4418 additions and 101 deletions

View File

@@ -9,11 +9,9 @@ import {
collectDiscordAuditChannelIds,
} from "../../discord/audit.js";
import { probeDiscord } from "../../discord/probe.js";
import {
listGuildChannelsDiscord,
sendMessageDiscord,
sendPollDiscord,
} from "../../discord/send.js";
import { resolveDiscordChannelAllowlist } from "../../discord/resolve-channels.js";
import { resolveDiscordUserAllowlist } from "../../discord/resolve-users.js";
import { sendMessageDiscord, sendPollDiscord } from "../../discord/send.js";
import { shouldLogVerbose } from "../../globals.js";
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../../routing/session-key.js";
import { getChatChannelMeta } from "../registry.js";
@@ -42,6 +40,10 @@ import {
listDiscordDirectoryGroupsFromConfig,
listDiscordDirectoryPeersFromConfig,
} from "./directory-config.js";
import {
listDiscordDirectoryGroupsLive,
listDiscordDirectoryPeersLive,
} from "../../discord/directory-live.js";
const meta = getChatChannelMeta("discord");
@@ -123,9 +125,10 @@ export const discordPlugin: ChannelPlugin<ResolvedDiscordAccount> = {
normalizeEntry: (raw) => raw.replace(/^(discord|user):/i, "").replace(/^<@!?(\d+)>$/, "$1"),
};
},
collectWarnings: ({ account }) => {
collectWarnings: ({ account, cfg }) => {
const warnings: string[] = [];
const groupPolicy = account.config.groupPolicy ?? "allowlist";
const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy;
const groupPolicy = account.config.groupPolicy ?? defaultGroupPolicy ?? "open";
const guildEntries = account.config.guilds ?? {};
const guildsConfigured = Object.keys(guildEntries).length > 0;
const channelAllowlistConfigured = guildsConfigured;
@@ -165,29 +168,41 @@ export const discordPlugin: ChannelPlugin<ResolvedDiscordAccount> = {
self: async () => null,
listPeers: async (params) => listDiscordDirectoryPeersFromConfig(params),
listGroups: async (params) => listDiscordDirectoryGroupsFromConfig(params),
listGroupsLive: async ({ cfg, accountId, query, limit }) => {
listPeersLive: async (params) => listDiscordDirectoryPeersLive(params),
listGroupsLive: async (params) => listDiscordDirectoryGroupsLive(params),
},
resolver: {
resolveTargets: async ({ cfg, accountId, inputs, kind }) => {
const account = resolveDiscordAccount({ cfg, accountId });
const q = query?.trim().toLowerCase() || "";
const guildIds = Object.keys(account.config.guilds ?? {}).filter((id) => /^\d+$/.test(id));
const rows: Array<{ kind: "group"; id: string; name?: string; raw?: unknown }> = [];
for (const guildId of guildIds) {
const channels = await listGuildChannelsDiscord(guildId, {
accountId: account.accountId,
});
for (const channel of channels) {
const name = typeof channel.name === "string" ? channel.name : undefined;
if (q && name && !name.toLowerCase().includes(q)) continue;
rows.push({
kind: "group",
id: `channel:${channel.id}`,
name: name ?? undefined,
raw: channel,
});
}
const token = account.token?.trim();
if (!token) {
return inputs.map((input) => ({
input,
resolved: false,
note: "missing Discord token",
}));
}
const filtered = q ? rows.filter((row) => row.name?.toLowerCase().includes(q)) : rows;
const limited = typeof limit === "number" && limit > 0 ? filtered.slice(0, limit) : filtered;
return limited;
if (kind === "group") {
const resolved = await resolveDiscordChannelAllowlist({ token, entries: inputs });
return resolved.map((entry) => ({
input: entry.input,
resolved: entry.resolved,
id: entry.channelId ?? entry.guildId,
name:
entry.channelName ??
entry.guildName ??
(entry.guildId && !entry.channelId ? entry.guildId : undefined),
note: entry.note,
}));
}
const resolved = await resolveDiscordUserAllowlist({ token, entries: inputs });
return resolved.map((entry) => ({
input: entry.input,
resolved: entry.resolved,
id: entry.id,
name: entry.name,
note: entry.note,
}));
},
},
actions: discordMessageActions,

View File

@@ -95,8 +95,9 @@ export const imessagePlugin: ChannelPlugin<ResolvedIMessageAccount> = {
approveHint: formatPairingApproveHint("imessage"),
};
},
collectWarnings: ({ account }) => {
const groupPolicy = account.config.groupPolicy ?? "allowlist";
collectWarnings: ({ account, cfg }) => {
const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy;
const groupPolicy = account.config.groupPolicy ?? defaultGroupPolicy ?? "allowlist";
if (groupPolicy !== "open") return [];
return [
`- iMessage groups: groupPolicy="open" allows any member to trigger the bot. Set channels.imessage.groupPolicy="allowlist" + channels.imessage.groupAllowFrom to restrict senders.`,

View File

@@ -0,0 +1,93 @@
import type { WizardPrompter } from "../../../wizard/prompts.js";
export type ChannelAccessPolicy = "allowlist" | "open" | "disabled";
export function parseAllowlistEntries(raw: string): string[] {
return String(raw ?? "")
.split(/[,\n]/g)
.map((entry) => entry.trim())
.filter(Boolean);
}
export function formatAllowlistEntries(entries: string[]): string {
return entries.map((entry) => entry.trim()).filter(Boolean).join(", ");
}
export async function promptChannelAccessPolicy(params: {
prompter: WizardPrompter;
label: string;
currentPolicy?: ChannelAccessPolicy;
allowOpen?: boolean;
allowDisabled?: boolean;
}): Promise<ChannelAccessPolicy> {
const options: Array<{ value: ChannelAccessPolicy; label: string }> = [
{ value: "allowlist", label: "Allowlist (recommended)" },
];
if (params.allowOpen !== false) {
options.push({ value: "open", label: "Open (allow all channels)" });
}
if (params.allowDisabled !== false) {
options.push({ value: "disabled", label: "Disabled (block all channels)" });
}
const initialValue = params.currentPolicy ?? "allowlist";
return (await params.prompter.select({
message: `${params.label} access`,
options,
initialValue,
})) as ChannelAccessPolicy;
}
export async function promptChannelAllowlist(params: {
prompter: WizardPrompter;
label: string;
currentEntries?: string[];
placeholder?: string;
}): Promise<string[]> {
const initialValue =
params.currentEntries && params.currentEntries.length > 0
? formatAllowlistEntries(params.currentEntries)
: undefined;
const raw = await params.prompter.text({
message: `${params.label} allowlist (comma-separated)`,
placeholder: params.placeholder,
initialValue,
});
return parseAllowlistEntries(raw);
}
export async function promptChannelAccessConfig(params: {
prompter: WizardPrompter;
label: string;
currentPolicy?: ChannelAccessPolicy;
currentEntries?: string[];
placeholder?: string;
allowOpen?: boolean;
allowDisabled?: boolean;
defaultPrompt?: boolean;
updatePrompt?: boolean;
}): Promise<{ policy: ChannelAccessPolicy; entries: string[] } | null> {
const hasEntries = (params.currentEntries ?? []).length > 0;
const shouldPrompt = params.defaultPrompt ?? !hasEntries;
const wants = await params.prompter.confirm({
message: params.updatePrompt
? `Update ${params.label} access?`
: `Configure ${params.label} access?`,
initialValue: shouldPrompt,
});
if (!wants) return null;
const policy = await promptChannelAccessPolicy({
prompter: params.prompter,
label: params.label,
currentPolicy: params.currentPolicy,
allowOpen: params.allowOpen,
allowDisabled: params.allowDisabled,
});
if (policy !== "allowlist") return { policy, entries: [] };
const entries = await promptChannelAllowlist({
prompter: params.prompter,
label: params.label,
currentEntries: params.currentEntries,
placeholder: params.placeholder,
});
return { policy, entries };
}

View File

@@ -5,10 +5,13 @@ import {
resolveDefaultDiscordAccountId,
resolveDiscordAccount,
} from "../../../discord/accounts.js";
import { normalizeDiscordSlug } from "../../../discord/monitor/allow-list.js";
import { resolveDiscordChannelAllowlist } from "../../../discord/resolve-channels.js";
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../../../routing/session-key.js";
import { formatDocsLink } from "../../../terminal/links.js";
import type { WizardPrompter } from "../../../wizard/prompts.js";
import type { ChannelOnboardingAdapter, ChannelOnboardingDmPolicy } from "../onboarding-types.js";
import { promptChannelAccessConfig } from "./channel-access.js";
import { addWildcardAllowFrom, promptAccountId } from "./helpers.js";
const channel = "discord" as const;
@@ -46,6 +49,103 @@ async function noteDiscordTokenHelp(prompter: WizardPrompter): Promise<void> {
);
}
function setDiscordGroupPolicy(
cfg: ClawdbotConfig,
accountId: string,
groupPolicy: "open" | "allowlist" | "disabled",
): ClawdbotConfig {
if (accountId === DEFAULT_ACCOUNT_ID) {
return {
...cfg,
channels: {
...cfg.channels,
discord: {
...cfg.channels?.discord,
enabled: true,
groupPolicy,
},
},
};
}
return {
...cfg,
channels: {
...cfg.channels,
discord: {
...cfg.channels?.discord,
enabled: true,
accounts: {
...cfg.channels?.discord?.accounts,
[accountId]: {
...cfg.channels?.discord?.accounts?.[accountId],
enabled: cfg.channels?.discord?.accounts?.[accountId]?.enabled ?? true,
groupPolicy,
},
},
},
},
};
}
function setDiscordGuildChannelAllowlist(
cfg: ClawdbotConfig,
accountId: string,
entries: Array<{
guildKey: string;
channelKey?: string;
}>,
): ClawdbotConfig {
const baseGuilds =
accountId === DEFAULT_ACCOUNT_ID
? (cfg.channels?.discord?.guilds ?? {})
: (cfg.channels?.discord?.accounts?.[accountId]?.guilds ?? {});
const guilds: Record<string, { channels?: Record<string, { allow: boolean }> }> = {
...baseGuilds,
};
for (const entry of entries) {
const guildKey = entry.guildKey || "*";
const existing = guilds[guildKey] ?? {};
if (entry.channelKey) {
const channels = { ...(existing.channels ?? {}) };
channels[entry.channelKey] = { allow: true };
guilds[guildKey] = { ...existing, channels };
} else {
guilds[guildKey] = existing;
}
}
if (accountId === DEFAULT_ACCOUNT_ID) {
return {
...cfg,
channels: {
...cfg.channels,
discord: {
...cfg.channels?.discord,
enabled: true,
guilds,
},
},
};
}
return {
...cfg,
channels: {
...cfg.channels,
discord: {
...cfg.channels?.discord,
enabled: true,
accounts: {
...cfg.channels?.discord?.accounts,
[accountId]: {
...cfg.channels?.discord?.accounts?.[accountId],
enabled: cfg.channels?.discord?.accounts?.[accountId]?.enabled ?? true,
guilds,
},
},
},
},
};
}
const dmPolicy: ChannelOnboardingDmPolicy = {
label: "Discord",
channel,
@@ -174,6 +274,91 @@ export const discordOnboardingAdapter: ChannelOnboardingAdapter = {
}
}
const currentEntries = Object.entries(resolvedAccount.config.guilds ?? {}).flatMap(
([guildKey, value]) => {
const channels = value?.channels ?? {};
const channelKeys = Object.keys(channels);
if (channelKeys.length === 0) return [guildKey];
return channelKeys.map((channelKey) => `${guildKey}/${channelKey}`);
},
);
const accessConfig = await promptChannelAccessConfig({
prompter,
label: "Discord channels",
currentPolicy: resolvedAccount.config.groupPolicy ?? "allowlist",
currentEntries,
placeholder: "My Server/#general, guildId/channelId, #support",
updatePrompt: Boolean(resolvedAccount.config.guilds),
});
if (accessConfig) {
if (accessConfig.policy !== "allowlist") {
next = setDiscordGroupPolicy(next, discordAccountId, accessConfig.policy);
} else {
const accountWithTokens = resolveDiscordAccount({
cfg: next,
accountId: discordAccountId,
});
let resolved = accessConfig.entries.map((input) => ({ input, resolved: false }));
if (accountWithTokens.token && accessConfig.entries.length > 0) {
try {
resolved = await resolveDiscordChannelAllowlist({
token: accountWithTokens.token,
entries: accessConfig.entries,
});
const resolvedChannels = resolved.filter(
(entry) => entry.resolved && entry.channelId,
);
const resolvedGuilds = resolved.filter(
(entry) => entry.resolved && entry.guildId && !entry.channelId,
);
const unresolved = resolved.filter((entry) => !entry.resolved).map((entry) => entry.input);
if (resolvedChannels.length > 0 || resolvedGuilds.length > 0 || unresolved.length > 0) {
const summary: string[] = [];
if (resolvedChannels.length > 0) {
summary.push(
`Resolved channels: ${resolvedChannels
.map((entry) => entry.channelId)
.filter(Boolean)
.join(", ")}`,
);
}
if (resolvedGuilds.length > 0) {
summary.push(
`Resolved guilds: ${resolvedGuilds
.map((entry) => entry.guildId)
.filter(Boolean)
.join(", ")}`,
);
}
if (unresolved.length > 0) {
summary.push(`Unresolved (kept as typed): ${unresolved.join(", ")}`);
}
await prompter.note(summary.join("\n"), "Discord channels");
}
} catch (err) {
await prompter.note(
`Channel lookup failed; keeping entries as typed. ${String(err)}`,
"Discord channels",
);
}
}
const allowlistEntries: Array<{ guildKey: string; channelKey?: string }> = [];
for (const entry of resolved) {
const guildKey =
entry.guildId ??
(entry.guildName ? normalizeDiscordSlug(entry.guildName) : undefined) ??
"*";
const channelKey =
entry.channelId ??
(entry.channelName ? normalizeDiscordSlug(entry.channelName) : undefined);
if (!channelKey && guildKey === "*") continue;
allowlistEntries.push({ guildKey, ...(channelKey ? { channelKey } : {}) });
}
next = setDiscordGroupPolicy(next, discordAccountId, "allowlist");
next = setDiscordGuildChannelAllowlist(next, discordAccountId, allowlistEntries);
}
}
return { cfg: next, accountId: discordAccountId };
},
dmPolicy,

View File

@@ -6,9 +6,11 @@ import {
resolveDefaultSlackAccountId,
resolveSlackAccount,
} from "../../../slack/accounts.js";
import { resolveSlackChannelAllowlist } from "../../../slack/resolve-channels.js";
import { formatDocsLink } from "../../../terminal/links.js";
import type { WizardPrompter } from "../../../wizard/prompts.js";
import type { ChannelOnboardingAdapter, ChannelOnboardingDmPolicy } from "../onboarding-types.js";
import { promptChannelAccessConfig } from "./channel-access.js";
import { addWildcardAllowFrom, promptAccountId } from "./helpers.js";
const channel = "slack" as const;
@@ -121,6 +123,85 @@ async function noteSlackTokenHelp(prompter: WizardPrompter, botName: string): Pr
);
}
function setSlackGroupPolicy(
cfg: ClawdbotConfig,
accountId: string,
groupPolicy: "open" | "allowlist" | "disabled",
): ClawdbotConfig {
if (accountId === DEFAULT_ACCOUNT_ID) {
return {
...cfg,
channels: {
...cfg.channels,
slack: {
...cfg.channels?.slack,
enabled: true,
groupPolicy,
},
},
};
}
return {
...cfg,
channels: {
...cfg.channels,
slack: {
...cfg.channels?.slack,
enabled: true,
accounts: {
...cfg.channels?.slack?.accounts,
[accountId]: {
...cfg.channels?.slack?.accounts?.[accountId],
enabled: cfg.channels?.slack?.accounts?.[accountId]?.enabled ?? true,
groupPolicy,
},
},
},
},
};
}
function setSlackChannelAllowlist(
cfg: ClawdbotConfig,
accountId: string,
channelKeys: string[],
): ClawdbotConfig {
const channels = Object.fromEntries(
channelKeys.map((key) => [key, { allow: true }]),
);
if (accountId === DEFAULT_ACCOUNT_ID) {
return {
...cfg,
channels: {
...cfg.channels,
slack: {
...cfg.channels?.slack,
enabled: true,
channels,
},
},
};
}
return {
...cfg,
channels: {
...cfg.channels,
slack: {
...cfg.channels?.slack,
enabled: true,
accounts: {
...cfg.channels?.slack?.accounts,
[accountId]: {
...cfg.channels?.slack?.accounts?.[accountId],
enabled: cfg.channels?.slack?.accounts?.[accountId]?.enabled ?? true,
channels,
},
},
},
},
};
}
const dmPolicy: ChannelOnboardingDmPolicy = {
label: "Slack",
channel,
@@ -284,6 +365,68 @@ export const slackOnboardingAdapter: ChannelOnboardingAdapter = {
}
}
const accessConfig = await promptChannelAccessConfig({
prompter,
label: "Slack channels",
currentPolicy: resolvedAccount.config.groupPolicy ?? "allowlist",
currentEntries: Object.entries(resolvedAccount.config.channels ?? {})
.filter(([, value]) => value?.allow !== false && value?.enabled !== false)
.map(([key]) => key),
placeholder: "#general, #private, C123",
updatePrompt: Boolean(resolvedAccount.config.channels),
});
if (accessConfig) {
if (accessConfig.policy !== "allowlist") {
next = setSlackGroupPolicy(next, slackAccountId, accessConfig.policy);
} else {
let keys = accessConfig.entries;
const accountWithTokens = resolveSlackAccount({
cfg: next,
accountId: slackAccountId,
});
if (accountWithTokens.botToken && accessConfig.entries.length > 0) {
try {
const resolved = await resolveSlackChannelAllowlist({
token: accountWithTokens.botToken,
entries: accessConfig.entries,
});
const resolvedKeys = resolved
.filter((entry) => entry.resolved && entry.id)
.map((entry) => entry.id as string);
const unresolved = resolved
.filter((entry) => !entry.resolved)
.map((entry) => entry.input);
keys = [
...resolvedKeys,
...unresolved.map((entry) => entry.trim()).filter(Boolean),
];
if (resolvedKeys.length > 0 || unresolved.length > 0) {
await prompter.note(
[
resolvedKeys.length > 0
? `Resolved: ${resolvedKeys.join(", ")}`
: undefined,
unresolved.length > 0
? `Unresolved (kept as typed): ${unresolved.join(", ")}`
: undefined,
]
.filter(Boolean)
.join("\n"),
"Slack channels",
);
}
} catch (err) {
await prompter.note(
`Channel lookup failed; keeping entries as typed. ${String(err)}`,
"Slack channels",
);
}
}
next = setSlackGroupPolicy(next, slackAccountId, "allowlist");
next = setSlackChannelAllowlist(next, slackAccountId, keys);
}
}
return { cfg: next, accountId: slackAccountId };
},
dmPolicy,

View File

@@ -108,8 +108,9 @@ export const signalPlugin: ChannelPlugin<ResolvedSignalAccount> = {
normalizeEntry: (raw) => normalizeE164(raw.replace(/^signal:/i, "").trim()),
};
},
collectWarnings: ({ account }) => {
const groupPolicy = account.config.groupPolicy ?? "allowlist";
collectWarnings: ({ account, cfg }) => {
const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy;
const groupPolicy = account.config.groupPolicy ?? defaultGroupPolicy ?? "allowlist";
if (groupPolicy !== "open") return [];
return [
`- Signal groups: groupPolicy="open" allows any member to trigger the bot. Set channels.signal.groupPolicy="allowlist" + channels.signal.groupAllowFrom to restrict senders.`,

View File

@@ -9,6 +9,8 @@ import {
resolveDefaultSlackAccountId,
resolveSlackAccount,
} from "../../slack/accounts.js";
import { resolveSlackChannelAllowlist } from "../../slack/resolve-channels.js";
import { resolveSlackUserAllowlist } from "../../slack/resolve-users.js";
import { probeSlack } from "../../slack/probe.js";
import { sendMessageSlack } from "../../slack/send.js";
import { getChatChannelMeta } from "../registry.js";
@@ -32,6 +34,10 @@ import {
listSlackDirectoryGroupsFromConfig,
listSlackDirectoryPeersFromConfig,
} from "./directory-config.js";
import {
listSlackDirectoryGroupsLive,
listSlackDirectoryPeersLive,
} from "../../slack/directory-live.js";
const meta = getChatChannelMeta("slack");
@@ -138,9 +144,10 @@ export const slackPlugin: ChannelPlugin<ResolvedSlackAccount> = {
normalizeEntry: (raw) => raw.replace(/^(slack|user):/i, ""),
};
},
collectWarnings: ({ account }) => {
collectWarnings: ({ account, cfg }) => {
const warnings: string[] = [];
const groupPolicy = account.config.groupPolicy ?? "allowlist";
const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy;
const groupPolicy = account.config.groupPolicy ?? defaultGroupPolicy ?? "open";
const channelAllowlistConfigured =
Boolean(account.config.channels) && Object.keys(account.config.channels ?? {}).length > 0;
@@ -190,6 +197,39 @@ export const slackPlugin: ChannelPlugin<ResolvedSlackAccount> = {
self: async () => null,
listPeers: async (params) => listSlackDirectoryPeersFromConfig(params),
listGroups: async (params) => listSlackDirectoryGroupsFromConfig(params),
listPeersLive: async (params) => listSlackDirectoryPeersLive(params),
listGroupsLive: async (params) => listSlackDirectoryGroupsLive(params),
},
resolver: {
resolveTargets: async ({ cfg, accountId, inputs, kind }) => {
const account = resolveSlackAccount({ cfg, accountId });
const token = account.config.userToken?.trim() || account.botToken?.trim();
if (!token) {
return inputs.map((input) => ({
input,
resolved: false,
note: "missing Slack token",
}));
}
if (kind === "group") {
const resolved = await resolveSlackChannelAllowlist({ token, entries: inputs });
return resolved.map((entry) => ({
input: entry.input,
resolved: entry.resolved,
id: entry.id,
name: entry.name,
note: entry.archived ? "archived" : undefined,
}));
}
const resolved = await resolveSlackUserAllowlist({ token, entries: inputs });
return resolved.map((entry) => ({
input: entry.input,
resolved: entry.resolved,
id: entry.id,
name: entry.name,
note: entry.note,
}));
},
},
actions: {
listActions: ({ cfg }) => {

View File

@@ -141,8 +141,9 @@ export const telegramPlugin: ChannelPlugin<ResolvedTelegramAccount> = {
normalizeEntry: (raw) => raw.replace(/^(telegram|tg):/i, ""),
};
},
collectWarnings: ({ account }) => {
const groupPolicy = account.config.groupPolicy ?? "allowlist";
collectWarnings: ({ account, cfg }) => {
const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy;
const groupPolicy = account.config.groupPolicy ?? defaultGroupPolicy ?? "allowlist";
if (groupPolicy !== "open") return [];
const groupAllowlistConfigured =
account.config.groups && Object.keys(account.config.groups).length > 0;

View File

@@ -263,6 +263,26 @@ export type ChannelDirectoryAdapter = {
}) => Promise<ChannelDirectoryEntry[]>;
};
export type ChannelResolveKind = "user" | "group";
export type ChannelResolveResult = {
input: string;
resolved: boolean;
id?: string;
name?: string;
note?: string;
};
export type ChannelResolverAdapter = {
resolveTargets: (params: {
cfg: ClawdbotConfig;
accountId?: string | null;
inputs: string[];
kind: ChannelResolveKind;
runtime: RuntimeEnv;
}) => Promise<ChannelResolveResult[]>;
};
export type ChannelElevatedAdapter = {
allowFromFallback?: (params: {
cfg: ClawdbotConfig;

View File

@@ -236,6 +236,7 @@ export type ChannelDirectoryEntry = {
name?: string;
handle?: string;
avatarUrl?: string;
rank?: number;
raw?: unknown;
};

View File

@@ -4,6 +4,7 @@ import type {
ChannelCommandAdapter,
ChannelConfigAdapter,
ChannelDirectoryAdapter,
ChannelResolverAdapter,
ChannelElevatedAdapter,
ChannelGatewayAdapter,
ChannelGroupAdapter,
@@ -68,6 +69,7 @@ export type ChannelPlugin<ResolvedAccount = any> = {
threading?: ChannelThreadingAdapter;
messaging?: ChannelMessagingAdapter;
directory?: ChannelDirectoryAdapter;
resolver?: ChannelResolverAdapter;
actions?: ChannelMessageActionAdapter;
heartbeat?: ChannelHeartbeatAdapter;
// Channel-owned agent tools (login flows, etc.).

View File

@@ -9,6 +9,9 @@ export type {
ChannelCommandAdapter,
ChannelConfigAdapter,
ChannelDirectoryAdapter,
ChannelResolveKind,
ChannelResolveResult,
ChannelResolverAdapter,
ChannelElevatedAdapter,
ChannelGatewayAdapter,
ChannelGatewayContext,

View File

@@ -149,8 +149,9 @@ export const whatsappPlugin: ChannelPlugin<ResolvedWhatsAppAccount> = {
normalizeEntry: (raw) => normalizeE164(raw),
};
},
collectWarnings: ({ account }) => {
const groupPolicy = account.groupPolicy ?? "allowlist";
collectWarnings: ({ account, cfg }) => {
const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy;
const groupPolicy = account.groupPolicy ?? defaultGroupPolicy ?? "allowlist";
if (groupPolicy !== "open") return [];
const groupAllowlistConfigured =
Boolean(account.groups) && Object.keys(account.groups ?? {}).length > 0;