refactor: unify account-scoped dm security policy resolver

This commit is contained in:
Peter Steinberger
2026-03-07 22:37:59 +00:00
parent 7230b96cc7
commit b456649974
18 changed files with 293 additions and 189 deletions

View File

@@ -1,4 +1,7 @@
import { buildOpenGroupPolicyRestrictSendersWarning } from "openclaw/plugin-sdk"; import {
buildAccountScopedDmSecurityPolicy,
buildOpenGroupPolicyRestrictSendersWarning,
} from "openclaw/plugin-sdk";
import type { import type {
ChannelAccountSnapshot, ChannelAccountSnapshot,
ChannelPlugin, ChannelPlugin,
@@ -12,7 +15,6 @@ import {
collectBlueBubblesStatusIssues, collectBlueBubblesStatusIssues,
DEFAULT_ACCOUNT_ID, DEFAULT_ACCOUNT_ID,
deleteAccountFromConfigSection, deleteAccountFromConfigSection,
formatPairingApproveHint,
migrateBaseNameToDefaultAccount, migrateBaseNameToDefaultAccount,
normalizeAccountId, normalizeAccountId,
PAIRING_APPROVED_MESSAGE, PAIRING_APPROVED_MESSAGE,
@@ -125,19 +127,16 @@ export const bluebubblesPlugin: ChannelPlugin<ResolvedBlueBubblesAccount> = {
actions: bluebubblesMessageActions, actions: bluebubblesMessageActions,
security: { security: {
resolveDmPolicy: ({ cfg, accountId, account }) => { resolveDmPolicy: ({ cfg, accountId, account }) => {
const resolvedAccountId = accountId ?? account.accountId ?? DEFAULT_ACCOUNT_ID; return buildAccountScopedDmSecurityPolicy({
const useAccountPath = Boolean(cfg.channels?.bluebubbles?.accounts?.[resolvedAccountId]); cfg,
const basePath = useAccountPath channelKey: "bluebubbles",
? `channels.bluebubbles.accounts.${resolvedAccountId}.` accountId,
: "channels.bluebubbles."; fallbackAccountId: account.accountId ?? DEFAULT_ACCOUNT_ID,
return { policy: account.config.dmPolicy,
policy: account.config.dmPolicy ?? "pairing",
allowFrom: account.config.allowFrom ?? [], allowFrom: account.config.allowFrom ?? [],
policyPath: `${basePath}dmPolicy`, policyPathSuffix: "dmPolicy",
allowFromPath: basePath,
approveHint: formatPairingApproveHint("bluebubbles"),
normalizeEntry: (raw) => normalizeBlueBubblesHandle(raw.replace(/^bluebubbles:/i, "")), normalizeEntry: (raw) => normalizeBlueBubblesHandle(raw.replace(/^bluebubbles:/i, "")),
}; });
}, },
collectWarnings: ({ account }) => { collectWarnings: ({ account }) => {
const groupPolicy = account.config.groupPolicy ?? "allowlist"; const groupPolicy = account.config.groupPolicy ?? "allowlist";

View File

@@ -1,4 +1,5 @@
import { import {
buildAccountScopedDmSecurityPolicy,
buildOpenGroupPolicyConfigureRouteAllowlistWarning, buildOpenGroupPolicyConfigureRouteAllowlistWarning,
buildOpenGroupPolicyWarning, buildOpenGroupPolicyWarning,
} from "openclaw/plugin-sdk"; } from "openclaw/plugin-sdk";
@@ -13,7 +14,6 @@ import {
deleteAccountFromConfigSection, deleteAccountFromConfigSection,
discordOnboardingAdapter, discordOnboardingAdapter,
DiscordConfigSchema, DiscordConfigSchema,
formatPairingApproveHint,
getChatChannelMeta, getChatChannelMeta,
inspectDiscordAccount, inspectDiscordAccount,
listDiscordAccountIds, listDiscordAccountIds,
@@ -127,18 +127,16 @@ export const discordPlugin: ChannelPlugin<ResolvedDiscordAccount> = {
}, },
security: { security: {
resolveDmPolicy: ({ cfg, accountId, account }) => { resolveDmPolicy: ({ cfg, accountId, account }) => {
const resolvedAccountId = accountId ?? account.accountId ?? DEFAULT_ACCOUNT_ID; return buildAccountScopedDmSecurityPolicy({
const useAccountPath = Boolean(cfg.channels?.discord?.accounts?.[resolvedAccountId]); cfg,
const allowFromPath = useAccountPath channelKey: "discord",
? `channels.discord.accounts.${resolvedAccountId}.dm.` accountId,
: "channels.discord.dm."; fallbackAccountId: account.accountId ?? DEFAULT_ACCOUNT_ID,
return { policy: account.config.dm?.policy,
policy: account.config.dm?.policy ?? "pairing",
allowFrom: account.config.dm?.allowFrom ?? [], allowFrom: account.config.dm?.allowFrom ?? [],
allowFromPath, allowFromPathSuffix: "dm.",
approveHint: formatPairingApproveHint("discord"),
normalizeEntry: (raw) => raw.replace(/^(discord|user):/i, "").replace(/^<@!?(\d+)>$/, "$1"), normalizeEntry: (raw) => raw.replace(/^(discord|user):/i, "").replace(/^<@!?(\d+)>$/, "$1"),
}; });
}, },
collectWarnings: ({ account, cfg }) => { collectWarnings: ({ account, cfg }) => {
const warnings: string[] = []; const warnings: string[] = [];

View File

@@ -1,4 +1,7 @@
import { buildOpenGroupPolicyConfigureRouteAllowlistWarning } from "openclaw/plugin-sdk"; import {
buildAccountScopedDmSecurityPolicy,
buildOpenGroupPolicyConfigureRouteAllowlistWarning,
} from "openclaw/plugin-sdk";
import { import {
applyAccountNameToChannelSection, applyAccountNameToChannelSection,
applySetupAccountConfigPatch, applySetupAccountConfigPatch,
@@ -6,7 +9,6 @@ import {
buildChannelConfigSchema, buildChannelConfigSchema,
DEFAULT_ACCOUNT_ID, DEFAULT_ACCOUNT_ID,
deleteAccountFromConfigSection, deleteAccountFromConfigSection,
formatPairingApproveHint,
getChatChannelMeta, getChatChannelMeta,
listDirectoryGroupEntriesFromMapKeys, listDirectoryGroupEntriesFromMapKeys,
listDirectoryUserEntriesFromAllowFrom, listDirectoryUserEntriesFromAllowFrom,
@@ -190,18 +192,16 @@ export const googlechatPlugin: ChannelPlugin<ResolvedGoogleChatAccount> = {
}, },
security: { security: {
resolveDmPolicy: ({ cfg, accountId, account }) => { resolveDmPolicy: ({ cfg, accountId, account }) => {
const resolvedAccountId = accountId ?? account.accountId ?? DEFAULT_ACCOUNT_ID; return buildAccountScopedDmSecurityPolicy({
const useAccountPath = Boolean(cfg.channels?.["googlechat"]?.accounts?.[resolvedAccountId]); cfg,
const allowFromPath = useAccountPath channelKey: "googlechat",
? `channels.googlechat.accounts.${resolvedAccountId}.dm.` accountId,
: "channels.googlechat.dm."; fallbackAccountId: account.accountId ?? DEFAULT_ACCOUNT_ID,
return { policy: account.config.dm?.policy,
policy: account.config.dm?.policy ?? "pairing",
allowFrom: account.config.dm?.allowFrom ?? [], allowFrom: account.config.dm?.allowFrom ?? [],
allowFromPath, allowFromPathSuffix: "dm.",
approveHint: formatPairingApproveHint("googlechat"),
normalizeEntry: (raw) => formatAllowFromEntry(raw), normalizeEntry: (raw) => formatAllowFromEntry(raw),
}; });
}, },
collectWarnings: ({ account, cfg }) => { collectWarnings: ({ account, cfg }) => {
const warnings: string[] = []; const warnings: string[] = [];

View File

@@ -1,11 +1,13 @@
import { buildOpenGroupPolicyRestrictSendersWarning } from "openclaw/plugin-sdk"; import {
buildAccountScopedDmSecurityPolicy,
buildOpenGroupPolicyRestrictSendersWarning,
} from "openclaw/plugin-sdk";
import { import {
applyAccountNameToChannelSection, applyAccountNameToChannelSection,
buildChannelConfigSchema, buildChannelConfigSchema,
collectStatusIssuesFromLastError, collectStatusIssuesFromLastError,
DEFAULT_ACCOUNT_ID, DEFAULT_ACCOUNT_ID,
deleteAccountFromConfigSection, deleteAccountFromConfigSection,
formatPairingApproveHint,
formatTrimmedAllowFromEntries, formatTrimmedAllowFromEntries,
getChatChannelMeta, getChatChannelMeta,
imessageOnboardingAdapter, imessageOnboardingAdapter,
@@ -132,18 +134,15 @@ export const imessagePlugin: ChannelPlugin<ResolvedIMessageAccount> = {
}, },
security: { security: {
resolveDmPolicy: ({ cfg, accountId, account }) => { resolveDmPolicy: ({ cfg, accountId, account }) => {
const resolvedAccountId = accountId ?? account.accountId ?? DEFAULT_ACCOUNT_ID; return buildAccountScopedDmSecurityPolicy({
const useAccountPath = Boolean(cfg.channels?.imessage?.accounts?.[resolvedAccountId]); cfg,
const basePath = useAccountPath channelKey: "imessage",
? `channels.imessage.accounts.${resolvedAccountId}.` accountId,
: "channels.imessage."; fallbackAccountId: account.accountId ?? DEFAULT_ACCOUNT_ID,
return { policy: account.config.dmPolicy,
policy: account.config.dmPolicy ?? "pairing",
allowFrom: account.config.allowFrom ?? [], allowFrom: account.config.allowFrom ?? [],
policyPath: `${basePath}dmPolicy`, policyPathSuffix: "dmPolicy",
allowFromPath: basePath, });
approveHint: formatPairingApproveHint("imessage"),
};
}, },
collectWarnings: ({ account, cfg }) => { collectWarnings: ({ account, cfg }) => {
const defaultGroupPolicy = resolveDefaultGroupPolicy(cfg); const defaultGroupPolicy = resolveDefaultGroupPolicy(cfg);

View File

@@ -1,11 +1,13 @@
import { buildOpenGroupPolicyWarning } from "openclaw/plugin-sdk"; import {
buildAccountScopedDmSecurityPolicy,
buildOpenGroupPolicyWarning,
} from "openclaw/plugin-sdk";
import { import {
buildBaseAccountStatusSnapshot, buildBaseAccountStatusSnapshot,
buildBaseChannelStatusSummary, buildBaseChannelStatusSummary,
buildChannelConfigSchema, buildChannelConfigSchema,
DEFAULT_ACCOUNT_ID, DEFAULT_ACCOUNT_ID,
deleteAccountFromConfigSection, deleteAccountFromConfigSection,
formatPairingApproveHint,
getChatChannelMeta, getChatChannelMeta,
PAIRING_APPROVED_MESSAGE, PAIRING_APPROVED_MESSAGE,
resolveAllowlistProviderRuntimeGroupPolicy, resolveAllowlistProviderRuntimeGroupPolicy,
@@ -123,19 +125,16 @@ export const ircPlugin: ChannelPlugin<ResolvedIrcAccount, IrcProbe> = {
}, },
security: { security: {
resolveDmPolicy: ({ cfg, accountId, account }) => { resolveDmPolicy: ({ cfg, accountId, account }) => {
const resolvedAccountId = accountId ?? account.accountId ?? DEFAULT_ACCOUNT_ID; return buildAccountScopedDmSecurityPolicy({
const useAccountPath = Boolean(cfg.channels?.irc?.accounts?.[resolvedAccountId]); cfg,
const basePath = useAccountPath channelKey: "irc",
? `channels.irc.accounts.${resolvedAccountId}.` accountId,
: "channels.irc."; fallbackAccountId: account.accountId ?? DEFAULT_ACCOUNT_ID,
return { policy: account.config.dmPolicy,
policy: account.config.dmPolicy ?? "pairing",
allowFrom: account.config.allowFrom ?? [], allowFrom: account.config.allowFrom ?? [],
policyPath: `${basePath}dmPolicy`, policyPathSuffix: "dmPolicy",
allowFromPath: `${basePath}allowFrom`,
approveHint: formatPairingApproveHint("irc"),
normalizeEntry: (raw) => normalizeIrcAllowEntry(raw), normalizeEntry: (raw) => normalizeIrcAllowEntry(raw),
}; });
}, },
collectWarnings: ({ account, cfg }) => { collectWarnings: ({ account, cfg }) => {
const warnings: string[] = []; const warnings: string[] = [];

View File

@@ -1,4 +1,7 @@
import { buildOpenGroupPolicyRestrictSendersWarning } from "openclaw/plugin-sdk"; import {
buildAccountScopedDmSecurityPolicy,
buildOpenGroupPolicyRestrictSendersWarning,
} from "openclaw/plugin-sdk";
import { import {
buildChannelConfigSchema, buildChannelConfigSchema,
buildComputedAccountStatusSnapshot, buildComputedAccountStatusSnapshot,
@@ -159,21 +162,17 @@ export const linePlugin: ChannelPlugin<ResolvedLineAccount> = {
}, },
security: { security: {
resolveDmPolicy: ({ cfg, accountId, account }) => { resolveDmPolicy: ({ cfg, accountId, account }) => {
const resolvedAccountId = accountId ?? account.accountId ?? DEFAULT_ACCOUNT_ID; return buildAccountScopedDmSecurityPolicy({
const useAccountPath = Boolean( cfg,
(cfg.channels?.line as LineConfig | undefined)?.accounts?.[resolvedAccountId], channelKey: "line",
); accountId,
const basePath = useAccountPath fallbackAccountId: account.accountId ?? DEFAULT_ACCOUNT_ID,
? `channels.line.accounts.${resolvedAccountId}.` policy: account.config.dmPolicy,
: "channels.line.";
return {
policy: account.config.dmPolicy ?? "pairing",
allowFrom: account.config.allowFrom ?? [], allowFrom: account.config.allowFrom ?? [],
policyPath: `${basePath}dmPolicy`, policyPathSuffix: "dmPolicy",
allowFromPath: basePath,
approveHint: "openclaw pairing approve line <code>", approveHint: "openclaw pairing approve line <code>",
normalizeEntry: (raw) => raw.replace(/^line:(?:user:)?/i, ""), normalizeEntry: (raw) => raw.replace(/^line:(?:user:)?/i, ""),
}; });
}, },
collectWarnings: ({ account, cfg }) => { collectWarnings: ({ account, cfg }) => {
const defaultGroupPolicy = resolveDefaultGroupPolicy(cfg); const defaultGroupPolicy = resolveDefaultGroupPolicy(cfg);

View File

@@ -1,4 +1,7 @@
import { buildOpenGroupPolicyWarning } from "openclaw/plugin-sdk"; import {
buildAccountScopedDmSecurityPolicy,
buildOpenGroupPolicyWarning,
} from "openclaw/plugin-sdk";
import { import {
applyAccountNameToChannelSection, applyAccountNameToChannelSection,
buildChannelConfigSchema, buildChannelConfigSchema,
@@ -6,7 +9,6 @@ import {
collectStatusIssuesFromLastError, collectStatusIssuesFromLastError,
DEFAULT_ACCOUNT_ID, DEFAULT_ACCOUNT_ID,
deleteAccountFromConfigSection, deleteAccountFromConfigSection,
formatPairingApproveHint,
normalizeAccountId, normalizeAccountId,
PAIRING_APPROVED_MESSAGE, PAIRING_APPROVED_MESSAGE,
resolveAllowlistProviderRuntimeGroupPolicy, resolveAllowlistProviderRuntimeGroupPolicy,
@@ -159,20 +161,17 @@ export const matrixPlugin: ChannelPlugin<ResolvedMatrixAccount> = {
formatAllowFrom: ({ allowFrom }) => normalizeMatrixAllowList(allowFrom), formatAllowFrom: ({ allowFrom }) => normalizeMatrixAllowList(allowFrom),
}, },
security: { security: {
resolveDmPolicy: ({ account }) => { resolveDmPolicy: ({ cfg, accountId, account }) => {
const accountId = account.accountId; return buildAccountScopedDmSecurityPolicy({
const prefix = cfg: cfg as CoreConfig,
accountId && accountId !== "default" channelKey: "matrix",
? `channels.matrix.accounts.${accountId}.dm` accountId,
: "channels.matrix.dm"; fallbackAccountId: account.accountId ?? DEFAULT_ACCOUNT_ID,
return { policy: account.config.dm?.policy,
policy: account.config.dm?.policy ?? "pairing",
allowFrom: account.config.dm?.allowFrom ?? [], allowFrom: account.config.dm?.allowFrom ?? [],
policyPath: `${prefix}.policy`, allowFromPathSuffix: "dm.",
allowFromPath: `${prefix}.allowFrom`,
approveHint: formatPairingApproveHint("matrix"),
normalizeEntry: (raw) => normalizeMatrixUserId(raw), normalizeEntry: (raw) => normalizeMatrixUserId(raw),
}; });
}, },
collectWarnings: ({ account, cfg }) => { collectWarnings: ({ account, cfg }) => {
const defaultGroupPolicy = resolveDefaultGroupPolicy(cfg as CoreConfig); const defaultGroupPolicy = resolveDefaultGroupPolicy(cfg as CoreConfig);

View File

@@ -1,4 +1,7 @@
import { buildOpenGroupPolicyRestrictSendersWarning } from "openclaw/plugin-sdk"; import {
buildAccountScopedDmSecurityPolicy,
buildOpenGroupPolicyRestrictSendersWarning,
} from "openclaw/plugin-sdk";
import { import {
applyAccountNameToChannelSection, applyAccountNameToChannelSection,
applySetupAccountConfigPatch, applySetupAccountConfigPatch,
@@ -6,7 +9,6 @@ import {
buildChannelConfigSchema, buildChannelConfigSchema,
DEFAULT_ACCOUNT_ID, DEFAULT_ACCOUNT_ID,
deleteAccountFromConfigSection, deleteAccountFromConfigSection,
formatPairingApproveHint,
migrateBaseNameToDefaultAccount, migrateBaseNameToDefaultAccount,
normalizeAccountId, normalizeAccountId,
resolveAllowlistProviderRuntimeGroupPolicy, resolveAllowlistProviderRuntimeGroupPolicy,
@@ -281,19 +283,16 @@ export const mattermostPlugin: ChannelPlugin<ResolvedMattermostAccount> = {
}, },
security: { security: {
resolveDmPolicy: ({ cfg, accountId, account }) => { resolveDmPolicy: ({ cfg, accountId, account }) => {
const resolvedAccountId = accountId ?? account.accountId ?? DEFAULT_ACCOUNT_ID; return buildAccountScopedDmSecurityPolicy({
const useAccountPath = Boolean(cfg.channels?.mattermost?.accounts?.[resolvedAccountId]); cfg,
const basePath = useAccountPath channelKey: "mattermost",
? `channels.mattermost.accounts.${resolvedAccountId}.` accountId,
: "channels.mattermost."; fallbackAccountId: account.accountId ?? DEFAULT_ACCOUNT_ID,
return { policy: account.config.dmPolicy,
policy: account.config.dmPolicy ?? "pairing",
allowFrom: account.config.allowFrom ?? [], allowFrom: account.config.allowFrom ?? [],
policyPath: `${basePath}dmPolicy`, policyPathSuffix: "dmPolicy",
allowFromPath: basePath,
approveHint: formatPairingApproveHint("mattermost"),
normalizeEntry: (raw) => normalizeAllowEntry(raw), normalizeEntry: (raw) => normalizeAllowEntry(raw),
}; });
}, },
collectWarnings: ({ account, cfg }) => { collectWarnings: ({ account, cfg }) => {
const defaultGroupPolicy = resolveDefaultGroupPolicy(cfg); const defaultGroupPolicy = resolveDefaultGroupPolicy(cfg);

View File

@@ -1,4 +1,5 @@
import { import {
buildAccountScopedDmSecurityPolicy,
buildOpenGroupPolicyNoRouteAllowlistWarning, buildOpenGroupPolicyNoRouteAllowlistWarning,
buildOpenGroupPolicyRestrictSendersWarning, buildOpenGroupPolicyRestrictSendersWarning,
} from "openclaw/plugin-sdk"; } from "openclaw/plugin-sdk";
@@ -10,7 +11,6 @@ import {
clearAccountEntryFields, clearAccountEntryFields,
DEFAULT_ACCOUNT_ID, DEFAULT_ACCOUNT_ID,
deleteAccountFromConfigSection, deleteAccountFromConfigSection,
formatPairingApproveHint,
normalizeAccountId, normalizeAccountId,
resolveAllowlistProviderRuntimeGroupPolicy, resolveAllowlistProviderRuntimeGroupPolicy,
resolveDefaultGroupPolicy, resolveDefaultGroupPolicy,
@@ -121,21 +121,16 @@ export const nextcloudTalkPlugin: ChannelPlugin<ResolvedNextcloudTalkAccount> =
}, },
security: { security: {
resolveDmPolicy: ({ cfg, accountId, account }) => { resolveDmPolicy: ({ cfg, accountId, account }) => {
const resolvedAccountId = accountId ?? account.accountId ?? DEFAULT_ACCOUNT_ID; return buildAccountScopedDmSecurityPolicy({
const useAccountPath = Boolean( cfg,
cfg.channels?.["nextcloud-talk"]?.accounts?.[resolvedAccountId], channelKey: "nextcloud-talk",
); accountId,
const basePath = useAccountPath fallbackAccountId: account.accountId ?? DEFAULT_ACCOUNT_ID,
? `channels.nextcloud-talk.accounts.${resolvedAccountId}.` policy: account.config.dmPolicy,
: "channels.nextcloud-talk.";
return {
policy: account.config.dmPolicy ?? "pairing",
allowFrom: account.config.allowFrom ?? [], allowFrom: account.config.allowFrom ?? [],
policyPath: `${basePath}dmPolicy`, policyPathSuffix: "dmPolicy",
allowFromPath: basePath,
approveHint: formatPairingApproveHint("nextcloud-talk"),
normalizeEntry: (raw) => raw.replace(/^(nextcloud-talk|nc-talk|nc):/i, "").toLowerCase(), normalizeEntry: (raw) => raw.replace(/^(nextcloud-talk|nc-talk|nc):/i, "").toLowerCase(),
}; });
}, },
collectWarnings: ({ account, cfg }) => { collectWarnings: ({ account, cfg }) => {
const defaultGroupPolicy = resolveDefaultGroupPolicy(cfg); const defaultGroupPolicy = resolveDefaultGroupPolicy(cfg);

View File

@@ -1,4 +1,7 @@
import { buildOpenGroupPolicyRestrictSendersWarning } from "openclaw/plugin-sdk"; import {
buildAccountScopedDmSecurityPolicy,
buildOpenGroupPolicyRestrictSendersWarning,
} from "openclaw/plugin-sdk";
import { import {
applyAccountNameToChannelSection, applyAccountNameToChannelSection,
buildBaseAccountStatusSnapshot, buildBaseAccountStatusSnapshot,
@@ -8,7 +11,6 @@ import {
createDefaultChannelRuntimeState, createDefaultChannelRuntimeState,
DEFAULT_ACCOUNT_ID, DEFAULT_ACCOUNT_ID,
deleteAccountFromConfigSection, deleteAccountFromConfigSection,
formatPairingApproveHint,
getChatChannelMeta, getChatChannelMeta,
listSignalAccountIds, listSignalAccountIds,
looksLikeSignalTargetId, looksLikeSignalTargetId,
@@ -155,19 +157,16 @@ export const signalPlugin: ChannelPlugin<ResolvedSignalAccount> = {
}, },
security: { security: {
resolveDmPolicy: ({ cfg, accountId, account }) => { resolveDmPolicy: ({ cfg, accountId, account }) => {
const resolvedAccountId = accountId ?? account.accountId ?? DEFAULT_ACCOUNT_ID; return buildAccountScopedDmSecurityPolicy({
const useAccountPath = Boolean(cfg.channels?.signal?.accounts?.[resolvedAccountId]); cfg,
const basePath = useAccountPath channelKey: "signal",
? `channels.signal.accounts.${resolvedAccountId}.` accountId,
: "channels.signal."; fallbackAccountId: account.accountId ?? DEFAULT_ACCOUNT_ID,
return { policy: account.config.dmPolicy,
policy: account.config.dmPolicy ?? "pairing",
allowFrom: account.config.allowFrom ?? [], allowFrom: account.config.allowFrom ?? [],
policyPath: `${basePath}dmPolicy`, policyPathSuffix: "dmPolicy",
allowFromPath: basePath,
approveHint: formatPairingApproveHint("signal"),
normalizeEntry: (raw) => normalizeE164(raw.replace(/^signal:/i, "").trim()), normalizeEntry: (raw) => normalizeE164(raw.replace(/^signal:/i, "").trim()),
}; });
}, },
collectWarnings: ({ account, cfg }) => { collectWarnings: ({ account, cfg }) => {
const defaultGroupPolicy = resolveDefaultGroupPolicy(cfg); const defaultGroupPolicy = resolveDefaultGroupPolicy(cfg);

View File

@@ -1,4 +1,5 @@
import { import {
buildAccountScopedDmSecurityPolicy,
buildOpenGroupPolicyConfigureRouteAllowlistWarning, buildOpenGroupPolicyConfigureRouteAllowlistWarning,
buildOpenGroupPolicyWarning, buildOpenGroupPolicyWarning,
} from "openclaw/plugin-sdk"; } from "openclaw/plugin-sdk";
@@ -9,7 +10,6 @@ import {
DEFAULT_ACCOUNT_ID, DEFAULT_ACCOUNT_ID,
deleteAccountFromConfigSection, deleteAccountFromConfigSection,
extractSlackToolSend, extractSlackToolSend,
formatPairingApproveHint,
getChatChannelMeta, getChatChannelMeta,
handleSlackMessageAction, handleSlackMessageAction,
inspectSlackAccount, inspectSlackAccount,
@@ -177,18 +177,16 @@ export const slackPlugin: ChannelPlugin<ResolvedSlackAccount> = {
}, },
security: { security: {
resolveDmPolicy: ({ cfg, accountId, account }) => { resolveDmPolicy: ({ cfg, accountId, account }) => {
const resolvedAccountId = accountId ?? account.accountId ?? DEFAULT_ACCOUNT_ID; return buildAccountScopedDmSecurityPolicy({
const useAccountPath = Boolean(cfg.channels?.slack?.accounts?.[resolvedAccountId]); cfg,
const allowFromPath = useAccountPath channelKey: "slack",
? `channels.slack.accounts.${resolvedAccountId}.dm.` accountId,
: "channels.slack.dm."; fallbackAccountId: account.accountId ?? DEFAULT_ACCOUNT_ID,
return { policy: account.dm?.policy,
policy: account.dm?.policy ?? "pairing",
allowFrom: account.dm?.allowFrom ?? [], allowFrom: account.dm?.allowFrom ?? [],
allowFromPath, allowFromPathSuffix: "dm.",
approveHint: formatPairingApproveHint("slack"),
normalizeEntry: (raw) => raw.replace(/^(slack|user):/i, ""), normalizeEntry: (raw) => raw.replace(/^(slack|user):/i, ""),
}; });
}, },
collectWarnings: ({ account, cfg }) => { collectWarnings: ({ account, cfg }) => {
const warnings: string[] = []; const warnings: string[] = [];

View File

@@ -1,4 +1,5 @@
import { import {
buildAccountScopedDmSecurityPolicy,
buildOpenGroupPolicyNoRouteAllowlistWarning, buildOpenGroupPolicyNoRouteAllowlistWarning,
buildOpenGroupPolicyRestrictSendersWarning, buildOpenGroupPolicyRestrictSendersWarning,
} from "openclaw/plugin-sdk"; } from "openclaw/plugin-sdk";
@@ -10,7 +11,6 @@ import {
collectTelegramStatusIssues, collectTelegramStatusIssues,
DEFAULT_ACCOUNT_ID, DEFAULT_ACCOUNT_ID,
deleteAccountFromConfigSection, deleteAccountFromConfigSection,
formatPairingApproveHint,
getChatChannelMeta, getChatChannelMeta,
inspectTelegramAccount, inspectTelegramAccount,
listTelegramAccountIds, listTelegramAccountIds,
@@ -192,19 +192,16 @@ export const telegramPlugin: ChannelPlugin<ResolvedTelegramAccount, TelegramProb
}, },
security: { security: {
resolveDmPolicy: ({ cfg, accountId, account }) => { resolveDmPolicy: ({ cfg, accountId, account }) => {
const resolvedAccountId = accountId ?? account.accountId ?? DEFAULT_ACCOUNT_ID; return buildAccountScopedDmSecurityPolicy({
const useAccountPath = Boolean(cfg.channels?.telegram?.accounts?.[resolvedAccountId]); cfg,
const basePath = useAccountPath channelKey: "telegram",
? `channels.telegram.accounts.${resolvedAccountId}.` accountId,
: "channels.telegram."; fallbackAccountId: account.accountId ?? DEFAULT_ACCOUNT_ID,
return { policy: account.config.dmPolicy,
policy: account.config.dmPolicy ?? "pairing",
allowFrom: account.config.allowFrom ?? [], allowFrom: account.config.allowFrom ?? [],
policyPath: `${basePath}dmPolicy`, policyPathSuffix: "dmPolicy",
allowFromPath: basePath,
approveHint: formatPairingApproveHint("telegram"),
normalizeEntry: (raw) => raw.replace(/^(telegram|tg):/i, ""), normalizeEntry: (raw) => raw.replace(/^(telegram|tg):/i, ""),
}; });
}, },
collectWarnings: ({ account, cfg }) => { collectWarnings: ({ account, cfg }) => {
const defaultGroupPolicy = resolveDefaultGroupPolicy(cfg); const defaultGroupPolicy = resolveDefaultGroupPolicy(cfg);

View File

@@ -1,4 +1,5 @@
import { import {
buildAccountScopedDmSecurityPolicy,
buildOpenGroupPolicyNoRouteAllowlistWarning, buildOpenGroupPolicyNoRouteAllowlistWarning,
buildOpenGroupPolicyRestrictSendersWarning, buildOpenGroupPolicyRestrictSendersWarning,
} from "openclaw/plugin-sdk"; } from "openclaw/plugin-sdk";
@@ -8,7 +9,6 @@ import {
collectWhatsAppStatusIssues, collectWhatsAppStatusIssues,
createActionGate, createActionGate,
DEFAULT_ACCOUNT_ID, DEFAULT_ACCOUNT_ID,
formatPairingApproveHint,
getChatChannelMeta, getChatChannelMeta,
listWhatsAppAccountIds, listWhatsAppAccountIds,
listWhatsAppDirectoryGroupsFromConfig, listWhatsAppDirectoryGroupsFromConfig,
@@ -125,19 +125,16 @@ export const whatsappPlugin: ChannelPlugin<ResolvedWhatsAppAccount> = {
}, },
security: { security: {
resolveDmPolicy: ({ cfg, accountId, account }) => { resolveDmPolicy: ({ cfg, accountId, account }) => {
const resolvedAccountId = accountId ?? account.accountId ?? DEFAULT_ACCOUNT_ID; return buildAccountScopedDmSecurityPolicy({
const useAccountPath = Boolean(cfg.channels?.whatsapp?.accounts?.[resolvedAccountId]); cfg,
const basePath = useAccountPath channelKey: "whatsapp",
? `channels.whatsapp.accounts.${resolvedAccountId}.` accountId,
: "channels.whatsapp."; fallbackAccountId: account.accountId ?? DEFAULT_ACCOUNT_ID,
return { policy: account.dmPolicy,
policy: account.dmPolicy ?? "pairing",
allowFrom: account.allowFrom ?? [], allowFrom: account.allowFrom ?? [],
policyPath: `${basePath}dmPolicy`, policyPathSuffix: "dmPolicy",
allowFromPath: basePath,
approveHint: formatPairingApproveHint("whatsapp"),
normalizeEntry: (raw) => normalizeE164(raw), normalizeEntry: (raw) => normalizeE164(raw),
}; });
}, },
collectWarnings: ({ account, cfg }) => { collectWarnings: ({ account, cfg }) => {
const defaultGroupPolicy = resolveDefaultGroupPolicy(cfg); const defaultGroupPolicy = resolveDefaultGroupPolicy(cfg);

View File

@@ -1,4 +1,5 @@
import { import {
buildAccountScopedDmSecurityPolicy,
buildOpenGroupPolicyRestrictSendersWarning, buildOpenGroupPolicyRestrictSendersWarning,
buildOpenGroupPolicyWarning, buildOpenGroupPolicyWarning,
} from "openclaw/plugin-sdk"; } from "openclaw/plugin-sdk";
@@ -19,7 +20,6 @@ import {
deleteAccountFromConfigSection, deleteAccountFromConfigSection,
chunkTextForOutbound, chunkTextForOutbound,
formatAllowFromLowercase, formatAllowFromLowercase,
formatPairingApproveHint,
migrateBaseNameToDefaultAccount, migrateBaseNameToDefaultAccount,
listDirectoryUserEntriesFromAllowFrom, listDirectoryUserEntriesFromAllowFrom,
normalizeAccountId, normalizeAccountId,
@@ -28,7 +28,6 @@ import {
resolveOutboundMediaUrls, resolveOutboundMediaUrls,
resolveDefaultGroupPolicy, resolveDefaultGroupPolicy,
resolveOpenProviderRuntimeGroupPolicy, resolveOpenProviderRuntimeGroupPolicy,
resolveChannelAccountConfigBasePath,
sendPayloadWithChunkedTextAndMedia, sendPayloadWithChunkedTextAndMedia,
setAccountEnabledInConfigSection, setAccountEnabledInConfigSection,
} from "openclaw/plugin-sdk/zalo"; } from "openclaw/plugin-sdk/zalo";
@@ -142,20 +141,16 @@ export const zaloPlugin: ChannelPlugin<ResolvedZaloAccount> = {
}, },
security: { security: {
resolveDmPolicy: ({ cfg, accountId, account }) => { resolveDmPolicy: ({ cfg, accountId, account }) => {
const resolvedAccountId = accountId ?? account.accountId ?? DEFAULT_ACCOUNT_ID; return buildAccountScopedDmSecurityPolicy({
const basePath = resolveChannelAccountConfigBasePath({
cfg, cfg,
channelKey: "zalo", channelKey: "zalo",
accountId: resolvedAccountId, accountId,
}); fallbackAccountId: account.accountId ?? DEFAULT_ACCOUNT_ID,
return { policy: account.config.dmPolicy,
policy: account.config.dmPolicy ?? "pairing",
allowFrom: account.config.allowFrom ?? [], allowFrom: account.config.allowFrom ?? [],
policyPath: `${basePath}dmPolicy`, policyPathSuffix: "dmPolicy",
allowFromPath: basePath,
approveHint: formatPairingApproveHint("zalo"),
normalizeEntry: (raw) => raw.replace(/^(zalo|zl):/i, ""), normalizeEntry: (raw) => raw.replace(/^(zalo|zl):/i, ""),
}; });
}, },
collectWarnings: ({ account, cfg }) => { collectWarnings: ({ account, cfg }) => {
const defaultGroupPolicy = resolveDefaultGroupPolicy(cfg); const defaultGroupPolicy = resolveDefaultGroupPolicy(cfg);

View File

@@ -1,3 +1,4 @@
import { buildAccountScopedDmSecurityPolicy } from "openclaw/plugin-sdk";
import type { import type {
ChannelAccountSnapshot, ChannelAccountSnapshot,
ChannelDirectoryEntry, ChannelDirectoryEntry,
@@ -18,11 +19,9 @@ import {
chunkTextForOutbound, chunkTextForOutbound,
deleteAccountFromConfigSection, deleteAccountFromConfigSection,
formatAllowFromLowercase, formatAllowFromLowercase,
formatPairingApproveHint,
isNumericTargetId, isNumericTargetId,
migrateBaseNameToDefaultAccount, migrateBaseNameToDefaultAccount,
normalizeAccountId, normalizeAccountId,
resolveChannelAccountConfigBasePath,
sendPayloadWithChunkedTextAndMedia, sendPayloadWithChunkedTextAndMedia,
setAccountEnabledInConfigSection, setAccountEnabledInConfigSection,
} from "openclaw/plugin-sdk/zalouser"; } from "openclaw/plugin-sdk/zalouser";
@@ -282,20 +281,16 @@ export const zalouserPlugin: ChannelPlugin<ResolvedZalouserAccount> = {
}, },
security: { security: {
resolveDmPolicy: ({ cfg, accountId, account }) => { resolveDmPolicy: ({ cfg, accountId, account }) => {
const resolvedAccountId = accountId ?? account.accountId ?? DEFAULT_ACCOUNT_ID; return buildAccountScopedDmSecurityPolicy({
const basePath = resolveChannelAccountConfigBasePath({
cfg, cfg,
channelKey: "zalouser", channelKey: "zalouser",
accountId: resolvedAccountId, accountId,
}); fallbackAccountId: account.accountId ?? DEFAULT_ACCOUNT_ID,
return { policy: account.config.dmPolicy,
policy: account.config.dmPolicy ?? "pairing",
allowFrom: account.config.allowFrom ?? [], allowFrom: account.config.allowFrom ?? [],
policyPath: `${basePath}dmPolicy`, policyPathSuffix: "dmPolicy",
allowFromPath: basePath,
approveHint: formatPairingApproveHint("zalouser"),
normalizeEntry: (raw) => raw.replace(/^(zalouser|zlu):/i, ""), normalizeEntry: (raw) => raw.replace(/^(zalouser|zlu):/i, ""),
}; });
}, },
}, },
groups: { groups: {

View File

@@ -0,0 +1,95 @@
import { describe, expect, it } from "vitest";
import type { OpenClawConfig } from "../../config/config.js";
import { buildAccountScopedDmSecurityPolicy, formatPairingApproveHint } from "./helpers.js";
function cfgWithChannel(channelKey: string, accounts?: Record<string, unknown>): OpenClawConfig {
return {
channels: {
[channelKey]: accounts ? { accounts } : {},
},
} as unknown as OpenClawConfig;
}
describe("buildAccountScopedDmSecurityPolicy", () => {
it("builds top-level dm policy paths when no account config exists", () => {
expect(
buildAccountScopedDmSecurityPolicy({
cfg: cfgWithChannel("telegram"),
channelKey: "telegram",
fallbackAccountId: "default",
policy: "pairing",
allowFrom: ["123"],
policyPathSuffix: "dmPolicy",
}),
).toEqual({
policy: "pairing",
allowFrom: ["123"],
policyPath: "channels.telegram.dmPolicy",
allowFromPath: "channels.telegram.",
approveHint: formatPairingApproveHint("telegram"),
normalizeEntry: undefined,
});
});
it("uses account-scoped paths when account config exists", () => {
expect(
buildAccountScopedDmSecurityPolicy({
cfg: cfgWithChannel("signal", { work: {} }),
channelKey: "signal",
accountId: "work",
fallbackAccountId: "default",
policy: "allowlist",
allowFrom: ["+12125551212"],
policyPathSuffix: "dmPolicy",
}),
).toEqual({
policy: "allowlist",
allowFrom: ["+12125551212"],
policyPath: "channels.signal.accounts.work.dmPolicy",
allowFromPath: "channels.signal.accounts.work.",
approveHint: formatPairingApproveHint("signal"),
normalizeEntry: undefined,
});
});
it("supports nested dm paths without explicit policyPath", () => {
expect(
buildAccountScopedDmSecurityPolicy({
cfg: cfgWithChannel("discord", { work: {} }),
channelKey: "discord",
accountId: "work",
policy: "pairing",
allowFrom: [],
allowFromPathSuffix: "dm.",
}),
).toEqual({
policy: "pairing",
allowFrom: [],
policyPath: undefined,
allowFromPath: "channels.discord.accounts.work.dm.",
approveHint: formatPairingApproveHint("discord"),
normalizeEntry: undefined,
});
});
it("supports custom defaults and approve hints", () => {
expect(
buildAccountScopedDmSecurityPolicy({
cfg: cfgWithChannel("synology-chat"),
channelKey: "synology-chat",
fallbackAccountId: "default",
allowFrom: ["user-1"],
defaultPolicy: "allowlist",
policyPathSuffix: "dmPolicy",
approveHint: "openclaw pairing approve synology-chat <code>",
}),
).toEqual({
policy: "allowlist",
allowFrom: ["user-1"],
policyPath: "channels.synology-chat.dmPolicy",
allowFromPath: "channels.synology-chat.",
approveHint: "openclaw pairing approve synology-chat <code>",
normalizeEntry: undefined,
});
});
});

View File

@@ -1,6 +1,7 @@
import { formatCliCommand } from "../../cli/command-format.js"; import { formatCliCommand } from "../../cli/command-format.js";
import type { OpenClawConfig } from "../../config/config.js"; import type { OpenClawConfig } from "../../config/config.js";
import { DEFAULT_ACCOUNT_ID } from "../../routing/session-key.js"; import { DEFAULT_ACCOUNT_ID } from "../../routing/session-key.js";
import type { ChannelSecurityDmPolicy } from "./types.core.js";
import type { ChannelPlugin } from "./types.js"; import type { ChannelPlugin } from "./types.js";
// Channel docking helper: use this when selecting the default account for a plugin. // Channel docking helper: use this when selecting the default account for a plugin.
@@ -18,3 +19,40 @@ export function formatPairingApproveHint(channelId: string): string {
const approveCmd = formatCliCommand(`openclaw pairing approve ${channelId} <code>`); const approveCmd = formatCliCommand(`openclaw pairing approve ${channelId} <code>`);
return `Approve via: ${listCmd} / ${approveCmd}`; return `Approve via: ${listCmd} / ${approveCmd}`;
} }
export function buildAccountScopedDmSecurityPolicy(params: {
cfg: OpenClawConfig;
channelKey: string;
accountId?: string | null;
fallbackAccountId?: string | null;
policy?: string | null;
allowFrom?: Array<string | number> | null;
defaultPolicy?: string;
allowFromPathSuffix?: string;
policyPathSuffix?: string;
approveChannelId?: string;
approveHint?: string;
normalizeEntry?: (raw: string) => string;
}): ChannelSecurityDmPolicy {
const resolvedAccountId = params.accountId ?? params.fallbackAccountId ?? DEFAULT_ACCOUNT_ID;
const channelConfig = (params.cfg.channels as Record<string, unknown> | undefined)?.[
params.channelKey
] as { accounts?: Record<string, unknown> } | undefined;
const useAccountPath = Boolean(channelConfig?.accounts?.[resolvedAccountId]);
const basePath = useAccountPath
? `channels.${params.channelKey}.accounts.${resolvedAccountId}.`
: `channels.${params.channelKey}.`;
const allowFromPath = `${basePath}${params.allowFromPathSuffix ?? ""}`;
const policyPath =
params.policyPathSuffix != null ? `${basePath}${params.policyPathSuffix}` : undefined;
return {
policy: params.policy ?? params.defaultPolicy ?? "pairing",
allowFrom: params.allowFrom ?? [],
policyPath,
allowFromPath,
approveHint:
params.approveHint ?? formatPairingApproveHint(params.approveChannelId ?? params.channelKey),
normalizeEntry: params.normalizeEntry,
};
}

View File

@@ -537,7 +537,10 @@ export {
buildOpenGroupPolicyRestrictSendersWarning, buildOpenGroupPolicyRestrictSendersWarning,
buildOpenGroupPolicyWarning, buildOpenGroupPolicyWarning,
} from "../channels/plugins/group-policy-warnings.js"; } from "../channels/plugins/group-policy-warnings.js";
export { formatPairingApproveHint } from "../channels/plugins/helpers.js"; export {
buildAccountScopedDmSecurityPolicy,
formatPairingApproveHint,
} from "../channels/plugins/helpers.js";
export { PAIRING_APPROVED_MESSAGE } from "../channels/plugins/pairing-message.js"; export { PAIRING_APPROVED_MESSAGE } from "../channels/plugins/pairing-message.js";
export type { export type {