refactor: compose shared channel security adapters

This commit is contained in:
Peter Steinberger
2026-03-22 21:23:19 +00:00
parent 87b2672126
commit 99462776d1
8 changed files with 161 additions and 94 deletions

View File

@@ -20,10 +20,9 @@ import {
import { getIMessageRuntime } from "./runtime.js";
import { imessageSetupAdapter } from "./setup-core.js";
import {
collectIMessageSecurityWarnings,
createIMessagePluginBase,
imessageConfigAdapter,
imessageResolveDmPolicy,
imessageSecurityAdapter,
imessageSetupWizard,
} from "./shared.js";
import {
@@ -124,10 +123,7 @@ export const imessagePlugin: ChannelPlugin<ResolvedIMessageAccount> = {
resolveDmPolicy: (account) => account.config.dmPolicy,
resolveGroupPolicy: (account) => account.config.groupPolicy,
}),
security: {
resolveDmPolicy: imessageResolveDmPolicy,
collectWarnings: collectIMessageSecurityWarnings,
},
security: imessageSecurityAdapter,
groups: {
resolveRequireMention: resolveIMessageGroupRequireMention,
resolveToolPolicy: resolveIMessageGroupToolPolicy,

View File

@@ -2,10 +2,9 @@ import { describeAccountSnapshot } from "openclaw/plugin-sdk/account-helpers";
import {
adaptScopedAccountAccessor,
createScopedChannelConfigAdapter,
createScopedDmSecurityResolver,
formatTrimmedAllowFromEntries,
} from "openclaw/plugin-sdk/channel-config-helpers";
import { createAllowlistProviderRestrictSendersWarningCollector } from "openclaw/plugin-sdk/channel-policy";
import { createRestrictSendersChannelSecurity } from "openclaw/plugin-sdk/channel-policy";
import { createChannelPluginBase } from "openclaw/plugin-sdk/core";
import {
buildChannelConfigSchema,
@@ -42,22 +41,18 @@ export const imessageConfigAdapter = createScopedChannelConfigAdapter<ResolvedIM
resolveDefaultTo: (account: ResolvedIMessageAccount) => account.config.defaultTo,
});
export const imessageResolveDmPolicy = createScopedDmSecurityResolver<ResolvedIMessageAccount>({
channelKey: IMESSAGE_CHANNEL,
resolvePolicy: (account) => account.config.dmPolicy,
resolveAllowFrom: (account) => account.config.allowFrom,
policyPathSuffix: "dmPolicy",
});
export const collectIMessageSecurityWarnings =
createAllowlistProviderRestrictSendersWarningCollector<ResolvedIMessageAccount>({
providerConfigPresent: (cfg) => cfg.channels?.imessage !== undefined,
export const imessageSecurityAdapter =
createRestrictSendersChannelSecurity<ResolvedIMessageAccount>({
channelKey: IMESSAGE_CHANNEL,
resolveDmPolicy: (account) => account.config.dmPolicy,
resolveDmAllowFrom: (account) => account.config.allowFrom,
resolveGroupPolicy: (account) => account.config.groupPolicy,
surface: "iMessage groups",
openScope: "any member",
groupPolicyPath: "channels.imessage.groupPolicy",
groupAllowFromPath: "channels.imessage.groupAllowFrom",
mentionGated: false,
policyPathSuffix: "dmPolicy",
});
export function createIMessagePluginBase(params: {
@@ -98,10 +93,7 @@ export function createIMessagePluginBase(params: {
configured: account.configured,
}),
},
security: {
resolveDmPolicy: imessageResolveDmPolicy,
collectWarnings: collectIMessageSecurityWarnings,
},
security: imessageSecurityAdapter,
setup: params.setup,
}) as Pick<
ChannelPlugin<ResolvedIMessageAccount>,

View File

@@ -1,9 +1,8 @@
import { createScopedDmSecurityResolver } from "openclaw/plugin-sdk/channel-config-helpers";
import {
createPairingPrefixStripper,
createTextPairingAdapter,
} from "openclaw/plugin-sdk/channel-pairing";
import { createAllowlistProviderRestrictSendersWarningCollector } from "openclaw/plugin-sdk/channel-policy";
import { createRestrictSendersChannelSecurity } from "openclaw/plugin-sdk/channel-policy";
import {
createAttachedChannelResultAdapter,
createEmptyChannelResult,
@@ -29,26 +28,21 @@ import { getLineRuntime } from "./runtime.js";
import { lineSetupAdapter } from "./setup-core.js";
import { lineSetupWizard } from "./setup-surface.js";
const resolveLineDmPolicy = createScopedDmSecurityResolver<ResolvedLineAccount>({
const lineSecurityAdapter = createRestrictSendersChannelSecurity<ResolvedLineAccount>({
channelKey: "line",
resolvePolicy: (account) => account.config.dmPolicy,
resolveAllowFrom: (account) => account.config.allowFrom,
resolveDmPolicy: (account) => account.config.dmPolicy,
resolveDmAllowFrom: (account) => account.config.allowFrom,
resolveGroupPolicy: (account) => account.config.groupPolicy,
surface: "LINE groups",
openScope: "any member in groups",
groupPolicyPath: "channels.line.groupPolicy",
groupAllowFromPath: "channels.line.groupAllowFrom",
mentionGated: false,
policyPathSuffix: "dmPolicy",
approveHint: "openclaw pairing approve line <code>",
normalizeEntry: (raw) => raw.replace(/^line:(?:user:)?/i, ""),
normalizeDmEntry: (raw) => raw.replace(/^line:(?:user:)?/i, ""),
});
const collectLineSecurityWarnings =
createAllowlistProviderRestrictSendersWarningCollector<ResolvedLineAccount>({
providerConfigPresent: (cfg) => cfg.channels?.line !== undefined,
resolveGroupPolicy: (account) => account.config.groupPolicy,
surface: "LINE groups",
openScope: "any member in groups",
groupPolicyPath: "channels.line.groupPolicy",
groupAllowFromPath: "channels.line.groupAllowFrom",
mentionGated: false,
});
export const linePlugin: ChannelPlugin<ResolvedLineAccount> = {
id: "line",
...lineChannelPluginCommon,
@@ -69,10 +63,7 @@ export const linePlugin: ChannelPlugin<ResolvedLineAccount> = {
},
}),
setupWizard: lineSetupWizard,
security: {
resolveDmPolicy: resolveLineDmPolicy,
collectWarnings: collectLineSecurityWarnings,
},
security: lineSecurityAdapter,
groups: {
resolveRequireMention: resolveLineGroupRequireMention,
},

View File

@@ -4,7 +4,6 @@ import { createMessageToolButtonsSchema } from "openclaw/plugin-sdk/channel-acti
import {
adaptScopedAccountAccessor,
createScopedChannelConfigAdapter,
createScopedDmSecurityResolver,
} from "openclaw/plugin-sdk/channel-config-helpers";
import type {
ChannelMessageActionAdapter,
@@ -12,7 +11,7 @@ import type {
ChannelMessageToolDiscovery,
} from "openclaw/plugin-sdk/channel-contract";
import { createLoggedPairingApprovalNotifier } from "openclaw/plugin-sdk/channel-pairing";
import { createAllowlistProviderRestrictSendersWarningCollector } from "openclaw/plugin-sdk/channel-policy";
import { createRestrictSendersChannelSecurity } from "openclaw/plugin-sdk/channel-policy";
import { createAttachedChannelResultAdapter } from "openclaw/plugin-sdk/channel-send-result";
import { createScopedAccountReplyToModeResolver } from "openclaw/plugin-sdk/conversation-runtime";
import { createChannelDirectoryAdapter } from "openclaw/plugin-sdk/directory-runtime";
@@ -50,15 +49,18 @@ import { resolveMattermostOutboundSessionRoute } from "./session-route.js";
import { mattermostSetupAdapter } from "./setup-core.js";
import { mattermostSetupWizard } from "./setup-surface.js";
const collectMattermostSecurityWarnings =
createAllowlistProviderRestrictSendersWarningCollector<ResolvedMattermostAccount>({
providerConfigPresent: (cfg) => cfg.channels?.mattermost !== undefined,
resolveGroupPolicy: (account) => account.config.groupPolicy,
surface: "Mattermost channels",
openScope: "any member",
groupPolicyPath: "channels.mattermost.groupPolicy",
groupAllowFromPath: "channels.mattermost.groupAllowFrom",
});
const mattermostSecurityAdapter = createRestrictSendersChannelSecurity<ResolvedMattermostAccount>({
channelKey: "mattermost",
resolveDmPolicy: (account) => account.config.dmPolicy,
resolveDmAllowFrom: (account) => account.config.allowFrom,
resolveGroupPolicy: (account) => account.config.groupPolicy,
surface: "Mattermost channels",
openScope: "any member",
groupPolicyPath: "channels.mattermost.groupPolicy",
groupAllowFromPath: "channels.mattermost.groupAllowFrom",
policyPathSuffix: "dmPolicy",
normalizeDmEntry: (raw) => normalizeAllowEntry(raw),
});
function describeMattermostMessageTool({
cfg,
@@ -279,14 +281,6 @@ const mattermostConfigAdapter = createScopedChannelConfigAdapter<ResolvedMatterm
}),
});
const resolveMattermostDmPolicy = createScopedDmSecurityResolver<ResolvedMattermostAccount>({
channelKey: "mattermost",
resolvePolicy: (account) => account.config.dmPolicy,
resolveAllowFrom: (account) => account.config.allowFrom,
policyPathSuffix: "dmPolicy",
normalizeEntry: (raw) => normalizeAllowEntry(raw),
});
export const mattermostPlugin: ChannelPlugin<ResolvedMattermostAccount> = {
id: "mattermost",
meta: {
@@ -339,10 +333,7 @@ export const mattermostPlugin: ChannelPlugin<ResolvedMattermostAccount> = {
},
}),
},
security: {
resolveDmPolicy: resolveMattermostDmPolicy,
collectWarnings: collectMattermostSecurityWarnings,
},
security: mattermostSecurityAdapter,
groups: {
resolveRequireMention: resolveMattermostGroupRequireMention,
},

View File

@@ -38,10 +38,9 @@ import {
import { getSignalRuntime } from "./runtime.js";
import { signalSetupAdapter } from "./setup-core.js";
import {
collectSignalSecurityWarnings,
signalConfigAdapter,
createSignalPluginBase,
signalResolveDmPolicy,
signalSecurityAdapter,
signalSetupWizard,
} from "./shared.js";
type SignalSendFn = ReturnType<typeof getSignalRuntime>["channel"]["signal"]["sendMessageSignal"];
@@ -295,10 +294,7 @@ export const signalPlugin: ChannelPlugin<ResolvedSignalAccount> = {
resolveDmPolicy: (account) => account.config.dmPolicy,
resolveGroupPolicy: (account) => account.config.groupPolicy,
}),
security: {
resolveDmPolicy: signalResolveDmPolicy,
collectWarnings: collectSignalSecurityWarnings,
},
security: signalSecurityAdapter,
messaging: {
normalizeTarget: normalizeSignalMessagingTarget,
parseExplicitTarget: ({ raw }) => parseSignalExplicitTarget(raw),

View File

@@ -2,9 +2,8 @@ import { describeAccountSnapshot } from "openclaw/plugin-sdk/account-helpers";
import {
adaptScopedAccountAccessor,
createScopedChannelConfigAdapter,
createScopedDmSecurityResolver,
} from "openclaw/plugin-sdk/channel-config-helpers";
import { createAllowlistProviderRestrictSendersWarningCollector } from "openclaw/plugin-sdk/channel-policy";
import { createRestrictSendersChannelSecurity } from "openclaw/plugin-sdk/channel-policy";
import { createChannelPluginBase } from "openclaw/plugin-sdk/core";
import {
listSignalAccountIds,
@@ -47,25 +46,20 @@ export const signalConfigAdapter = createScopedChannelConfigAdapter<ResolvedSign
resolveDefaultTo: (account: ResolvedSignalAccount) => account.config.defaultTo,
});
export const signalResolveDmPolicy = createScopedDmSecurityResolver<ResolvedSignalAccount>({
export const signalSecurityAdapter = createRestrictSendersChannelSecurity<ResolvedSignalAccount>({
channelKey: SIGNAL_CHANNEL,
resolvePolicy: (account) => account.config.dmPolicy,
resolveAllowFrom: (account) => account.config.allowFrom,
resolveDmPolicy: (account) => account.config.dmPolicy,
resolveDmAllowFrom: (account) => account.config.allowFrom,
resolveGroupPolicy: (account) => account.config.groupPolicy,
surface: "Signal groups",
openScope: "any member",
groupPolicyPath: "channels.signal.groupPolicy",
groupAllowFromPath: "channels.signal.groupAllowFrom",
mentionGated: false,
policyPathSuffix: "dmPolicy",
normalizeEntry: (raw) => normalizeE164(raw.replace(/^signal:/i, "").trim()),
normalizeDmEntry: (raw) => normalizeE164(raw.replace(/^signal:/i, "").trim()),
});
export const collectSignalSecurityWarnings =
createAllowlistProviderRestrictSendersWarningCollector<ResolvedSignalAccount>({
providerConfigPresent: (cfg) => cfg.channels?.signal !== undefined,
resolveGroupPolicy: (account) => account.config.groupPolicy,
surface: "Signal groups",
openScope: "any member",
groupPolicyPath: "channels.signal.groupPolicy",
groupAllowFromPath: "channels.signal.groupAllowFrom",
mentionGated: false,
});
export function createSignalPluginBase(params: {
setupWizard?: NonNullable<ChannelPlugin<ResolvedSignalAccount>["setupWizard"]>;
setup: NonNullable<ChannelPlugin<ResolvedSignalAccount>["setup"]>;
@@ -110,10 +104,7 @@ export function createSignalPluginBase(params: {
},
}),
},
security: {
resolveDmPolicy: signalResolveDmPolicy,
collectWarnings: collectSignalSecurityWarnings,
},
security: signalSecurityAdapter,
setup: params.setup,
}) as Pick<
ChannelPlugin<ResolvedSignalAccount>,

View File

@@ -0,0 +1,57 @@
import { describe, expect, it } from "vitest";
import type { GroupPolicy } from "../config/types.base.js";
import { createRestrictSendersChannelSecurity } from "./channel-policy.js";
describe("createRestrictSendersChannelSecurity", () => {
it("builds dm policy resolution and open-group warnings from one descriptor", async () => {
const security = createRestrictSendersChannelSecurity<{
accountId: string;
allowFrom?: string[];
dmPolicy?: string;
groupPolicy?: GroupPolicy;
}>({
channelKey: "line",
resolveDmPolicy: (account) => account.dmPolicy,
resolveDmAllowFrom: (account) => account.allowFrom,
resolveGroupPolicy: (account) => account.groupPolicy,
surface: "LINE groups",
openScope: "any member in groups",
groupPolicyPath: "channels.line.groupPolicy",
groupAllowFromPath: "channels.line.groupAllowFrom",
mentionGated: false,
policyPathSuffix: "dmPolicy",
});
expect(
security.resolveDmPolicy?.({
cfg: { channels: {} } as never,
accountId: "default",
account: {
accountId: "default",
dmPolicy: "allowlist",
allowFrom: ["line:user:abc"],
},
}),
).toEqual({
policy: "allowlist",
allowFrom: ["line:user:abc"],
policyPath: "channels.line.dmPolicy",
allowFromPath: "channels.line.",
approveHint: "Approve via: openclaw pairing list line / openclaw pairing approve line <code>",
normalizeEntry: undefined,
});
expect(
security.collectWarnings?.({
cfg: { channels: { line: {} } } as never,
accountId: "default",
account: {
accountId: "default",
groupPolicy: "open",
},
}),
).toEqual([
'- LINE groups: groupPolicy="open" allows any member in groups to trigger. Set channels.line.groupPolicy="allowlist" + channels.line.groupAllowFrom to restrict senders.',
]);
});
});

View File

@@ -1,3 +1,8 @@
import { createAllowlistProviderRestrictSendersWarningCollector } from "../channels/plugins/group-policy-warnings.js";
import type { ChannelSecurityAdapter } from "../channels/plugins/types.adapters.js";
import type { OpenClawConfig } from "../config/config.js";
import type { GroupPolicy } from "../config/types.base.js";
import { createScopedDmSecurityResolver } from "./channel-config-helpers.js";
/** Shared policy warnings and DM/group policy helpers for channel plugins. */
export type {
GroupToolPolicyBySenderConfig,
@@ -9,7 +14,6 @@ export {
createAllowlistProviderGroupPolicyWarningCollector,
createConditionalWarningCollector,
createAllowlistProviderOpenWarningCollector,
createAllowlistProviderRestrictSendersWarningCollector,
createAllowlistProviderRouteAllowlistWarningCollector,
createOpenGroupPolicyRestrictSendersWarningCollector,
createOpenProviderGroupPolicyWarningCollector,
@@ -35,3 +39,52 @@ export {
resolveDmGroupAccessWithLists,
resolveEffectiveAllowFromLists,
} from "../security/dm-policy-shared.js";
export { createAllowlistProviderRestrictSendersWarningCollector };
/** Compose the common DM policy resolver with restrict-senders group warnings. */
export function createRestrictSendersChannelSecurity<
ResolvedAccount extends { accountId?: string | null },
>(params: {
channelKey: string;
resolveDmPolicy: (account: ResolvedAccount) => string | null | undefined;
resolveDmAllowFrom: (account: ResolvedAccount) => Array<string | number> | null | undefined;
resolveGroupPolicy: (account: ResolvedAccount) => GroupPolicy | null | undefined;
surface: string;
openScope: string;
groupPolicyPath: string;
groupAllowFromPath: string;
mentionGated?: boolean;
providerConfigPresent?: (cfg: OpenClawConfig) => boolean;
resolveFallbackAccountId?: (account: ResolvedAccount) => string | null | undefined;
defaultDmPolicy?: string;
allowFromPathSuffix?: string;
policyPathSuffix?: string;
approveChannelId?: string;
approveHint?: string;
normalizeDmEntry?: (raw: string) => string;
}): ChannelSecurityAdapter<ResolvedAccount> {
return {
resolveDmPolicy: createScopedDmSecurityResolver<ResolvedAccount>({
channelKey: params.channelKey,
resolvePolicy: params.resolveDmPolicy,
resolveAllowFrom: params.resolveDmAllowFrom,
resolveFallbackAccountId: params.resolveFallbackAccountId,
defaultPolicy: params.defaultDmPolicy,
allowFromPathSuffix: params.allowFromPathSuffix,
policyPathSuffix: params.policyPathSuffix,
approveChannelId: params.approveChannelId,
approveHint: params.approveHint,
normalizeEntry: params.normalizeDmEntry,
}),
collectWarnings: createAllowlistProviderRestrictSendersWarningCollector<ResolvedAccount>({
providerConfigPresent:
params.providerConfigPresent ?? ((cfg) => cfg.channels?.[params.channelKey] !== undefined),
resolveGroupPolicy: params.resolveGroupPolicy,
surface: params.surface,
openScope: params.openScope,
groupPolicyPath: params.groupPolicyPath,
groupAllowFromPath: params.groupAllowFromPath,
mentionGated: params.mentionGated,
}),
};
}