diff --git a/extensions/bluebubbles/src/monitor-processing.ts b/extensions/bluebubbles/src/monitor-processing.ts index 4a9f75b25..486864fa4 100644 --- a/extensions/bluebubbles/src/monitor-processing.ts +++ b/extensions/bluebubbles/src/monitor-processing.ts @@ -1,6 +1,7 @@ import type { OpenClawConfig } from "openclaw/plugin-sdk"; import { DM_GROUP_ACCESS_REASON, + createScopedPairingAccess, createReplyPrefixOptions, evictOldHistoryKeys, logAckFailure, @@ -421,6 +422,11 @@ export async function processMessage( target: WebhookTarget, ): Promise { const { account, config, runtime, core, statusSink } = target; + const pairing = createScopedPairingAccess({ + core, + channel: "bluebubbles", + accountId: account.accountId, + }); const privateApiEnabled = isBlueBubblesPrivateApiEnabled(account.accountId); const groupFlag = resolveGroupFlagFromChatGuid(message.chatGuid); @@ -505,8 +511,9 @@ export async function processMessage( const configuredAllowFrom = (account.config.allowFrom ?? []).map((entry) => String(entry)); const storeAllowFrom = await readStoreAllowFromForDmPolicy({ provider: "bluebubbles", + accountId: account.accountId, dmPolicy, - readStore: (provider) => core.channel.pairing.readAllowFromStore(provider), + readStore: pairing.readStoreForDmPolicy, }); const accessDecision = resolveDmGroupAccessWithLists({ isGroup, @@ -587,8 +594,7 @@ export async function processMessage( } if (accessDecision.decision === "pairing") { - const { code, created } = await core.channel.pairing.upsertPairingRequest({ - channel: "bluebubbles", + const { code, created } = await pairing.upsertPairingRequest({ id: message.senderId, meta: { name: message.senderName }, }); @@ -1381,6 +1387,11 @@ export async function processReaction( target: WebhookTarget, ): Promise { const { account, config, runtime, core } = target; + const pairing = createScopedPairingAccess({ + core, + channel: "bluebubbles", + accountId: account.accountId, + }); if (reaction.fromMe) { return; } @@ -1389,8 +1400,9 @@ export async function processReaction( const groupPolicy = account.config.groupPolicy ?? "allowlist"; const storeAllowFrom = await readStoreAllowFromForDmPolicy({ provider: "bluebubbles", + accountId: account.accountId, dmPolicy, - readStore: (provider) => core.channel.pairing.readAllowFromStore(provider), + readStore: pairing.readStoreForDmPolicy, }); const accessDecision = resolveDmGroupAccessWithLists({ isGroup: reaction.isGroup, diff --git a/extensions/feishu/src/bot.ts b/extensions/feishu/src/bot.ts index 37c22da25..61c659737 100644 --- a/extensions/feishu/src/bot.ts +++ b/extensions/feishu/src/bot.ts @@ -3,6 +3,7 @@ import { buildAgentMediaPayload, buildPendingHistoryContextFromMap, clearHistoryEntriesIfEnabled, + createScopedPairingAccess, DEFAULT_GROUP_HISTORY_LIMIT, type HistoryEntry, recordPendingHistoryEntryIfEnabled, @@ -675,6 +676,11 @@ export async function handleFeishuMessage(params: { try { const core = getFeishuRuntime(); + const pairing = createScopedPairingAccess({ + core, + channel: "feishu", + accountId: account.accountId, + }); const shouldComputeCommandAuthorized = core.channel.commands.shouldComputeCommandAuthorized( ctx.content, cfg, @@ -683,7 +689,7 @@ export async function handleFeishuMessage(params: { !isGroup && dmPolicy !== "allowlist" && (dmPolicy !== "open" || shouldComputeCommandAuthorized) - ? await core.channel.pairing.readAllowFromStore("feishu").catch(() => []) + ? await pairing.readAllowFromStore().catch(() => []) : []; const effectiveDmAllowFrom = [...configAllowFrom, ...storeAllowFrom]; const dmAllowed = resolveFeishuAllowlistMatch({ @@ -695,8 +701,7 @@ export async function handleFeishuMessage(params: { if (!isGroup && dmPolicy !== "open" && !dmAllowed) { if (dmPolicy === "pairing") { - const { code, created } = await core.channel.pairing.upsertPairingRequest({ - channel: "feishu", + const { code, created } = await pairing.upsertPairingRequest({ id: ctx.senderOpenId, meta: { name: ctx.senderName }, }); diff --git a/extensions/googlechat/src/monitor.ts b/extensions/googlechat/src/monitor.ts index 8756f36e2..e31905a55 100644 --- a/extensions/googlechat/src/monitor.ts +++ b/extensions/googlechat/src/monitor.ts @@ -2,6 +2,7 @@ import type { IncomingMessage, ServerResponse } from "node:http"; import type { OpenClawConfig } from "openclaw/plugin-sdk"; import { GROUP_POLICY_BLOCKED_LABEL, + createScopedPairingAccess, createReplyPrefixOptions, readJsonBodyWithLimit, registerWebhookTarget, @@ -396,6 +397,11 @@ async function processMessageWithPipeline(params: { mediaMaxMb: number; }): Promise { const { event, account, config, runtime, core, statusSink, mediaMaxMb } = params; + const pairing = createScopedPairingAccess({ + core, + channel: "googlechat", + accountId: account.accountId, + }); const space = event.space; const message = event.message; if (!space || !message) { @@ -514,7 +520,7 @@ async function processMessageWithPipeline(params: { const shouldComputeAuth = core.channel.commands.shouldComputeCommandAuthorized(rawBody, config); const storeAllowFrom = !isGroup && dmPolicy !== "allowlist" && (dmPolicy !== "open" || shouldComputeAuth) - ? await core.channel.pairing.readAllowFromStore("googlechat").catch(() => []) + ? await pairing.readAllowFromStore().catch(() => []) : []; const access = resolveDmGroupAccessWithLists({ isGroup, @@ -590,8 +596,7 @@ async function processMessageWithPipeline(params: { if (access.decision !== "allow") { if (access.decision === "pairing") { - const { code, created } = await core.channel.pairing.upsertPairingRequest({ - channel: "googlechat", + const { code, created } = await pairing.upsertPairingRequest({ id: senderId, meta: { name: senderName || undefined, email: senderEmail }, }); diff --git a/extensions/irc/src/inbound.ts b/extensions/irc/src/inbound.ts index 29d232711..cb21b92c3 100644 --- a/extensions/irc/src/inbound.ts +++ b/extensions/irc/src/inbound.ts @@ -1,5 +1,6 @@ import { GROUP_POLICY_BLOCKED_LABEL, + createScopedPairingAccess, createNormalizedOutboundDeliverer, createReplyPrefixOptions, formatTextWithAttachmentLinks, @@ -90,6 +91,11 @@ export async function handleIrcInbound(params: { }): Promise { const { message, account, config, runtime, connectedNick, statusSink } = params; const core = getIrcRuntime(); + const pairing = createScopedPairingAccess({ + core, + channel: CHANNEL_ID, + accountId: account.accountId, + }); const rawBody = message.text?.trim() ?? ""; if (!rawBody) { @@ -123,8 +129,9 @@ export async function handleIrcInbound(params: { const configGroupAllowFrom = normalizeIrcAllowlist(account.config.groupAllowFrom); const storeAllowFrom = await readStoreAllowFromForDmPolicy({ provider: CHANNEL_ID, + accountId: account.accountId, dmPolicy, - readStore: (provider) => core.channel.pairing.readAllowFromStore(provider), + readStore: pairing.readStoreForDmPolicy, }); const storeAllowList = normalizeIrcAllowlist(storeAllowFrom); @@ -202,8 +209,7 @@ export async function handleIrcInbound(params: { }).allowed; if (!dmAllowed) { if (dmPolicy === "pairing") { - const { code, created } = await core.channel.pairing.upsertPairingRequest({ - channel: CHANNEL_ID, + const { code, created } = await pairing.upsertPairingRequest({ id: senderDisplay.toLowerCase(), meta: { name: message.senderNick || undefined }, }); diff --git a/extensions/matrix/src/matrix/monitor/handler.ts b/extensions/matrix/src/matrix/monitor/handler.ts index 8682e707a..fd1e96971 100644 --- a/extensions/matrix/src/matrix/monitor/handler.ts +++ b/extensions/matrix/src/matrix/monitor/handler.ts @@ -1,5 +1,7 @@ import type { LocationMessageEventContent, MatrixClient } from "@vector-im/matrix-bot-sdk"; import { + DEFAULT_ACCOUNT_ID, + createScopedPairingAccess, createReplyPrefixOptions, createTypingCallbacks, formatAllowlistMatchMeta, @@ -98,6 +100,12 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam getMemberDisplayName, accountId, } = params; + const resolvedAccountId = accountId?.trim() || DEFAULT_ACCOUNT_ID; + const pairing = createScopedPairingAccess({ + core, + channel: "matrix", + accountId: resolvedAccountId, + }); return async (roomId: string, event: MatrixRawEvent) => { try { @@ -229,8 +237,9 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam const storeAllowFrom = isDirectMessage ? await readStoreAllowFromForDmPolicy({ provider: "matrix", + accountId: resolvedAccountId, dmPolicy, - readStore: (provider) => core.channel.pairing.readAllowFromStore(provider), + readStore: pairing.readStoreForDmPolicy, }) : []; const groupAllowFrom = cfg.channels?.matrix?.groupAllowFrom ?? []; @@ -270,8 +279,7 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam }); const allowMatchMeta = formatAllowlistMatchMeta(allowMatch); if (access.decision === "pairing") { - const { code, created } = await core.channel.pairing.upsertPairingRequest({ - channel: "matrix", + const { code, created } = await pairing.upsertPairingRequest({ id: senderId, meta: { name: senderName }, }); diff --git a/extensions/mattermost/src/mattermost/monitor.ts b/extensions/mattermost/src/mattermost/monitor.ts index 54169f0d1..b66c15812 100644 --- a/extensions/mattermost/src/mattermost/monitor.ts +++ b/extensions/mattermost/src/mattermost/monitor.ts @@ -8,6 +8,7 @@ import type { import { buildAgentMediaPayload, DM_GROUP_ACCESS_REASON, + createScopedPairingAccess, createReplyPrefixOptions, createTypingCallbacks, logInboundDrop, @@ -171,6 +172,11 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {} cfg, accountId: opts.accountId, }); + const pairing = createScopedPairingAccess({ + core, + channel: "mattermost", + accountId: account.accountId, + }); const allowNameMatching = isDangerousNameMatchingEnabled(account.config); const botToken = opts.botToken?.trim() || account.botToken?.trim(); if (!botToken) { @@ -362,8 +368,9 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {} const storeAllowFrom = normalizeMattermostAllowList( await readStoreAllowFromForDmPolicy({ provider: "mattermost", + accountId: account.accountId, dmPolicy, - readStore: (provider) => core.channel.pairing.readAllowFromStore(provider), + readStore: pairing.readStoreForDmPolicy, }), ); const accessDecision = resolveDmGroupAccessWithLists({ @@ -424,8 +431,7 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {} return; } if (accessDecision.decision === "pairing") { - const { code, created } = await core.channel.pairing.upsertPairingRequest({ - channel: "mattermost", + const { code, created } = await pairing.upsertPairingRequest({ id: senderId, meta: { name: senderName }, }); @@ -862,8 +868,9 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {} const storeAllowFrom = normalizeMattermostAllowList( await readStoreAllowFromForDmPolicy({ provider: "mattermost", + accountId: account.accountId, dmPolicy, - readStore: (provider) => core.channel.pairing.readAllowFromStore(provider), + readStore: pairing.readStoreForDmPolicy, }), ); const reactionAccess = resolveDmGroupAccessWithLists({ diff --git a/extensions/msteams/src/monitor-handler/message-handler.ts b/extensions/msteams/src/monitor-handler/message-handler.ts index f3f517cd4..520a15832 100644 --- a/extensions/msteams/src/monitor-handler/message-handler.ts +++ b/extensions/msteams/src/monitor-handler/message-handler.ts @@ -1,7 +1,9 @@ import { + DEFAULT_ACCOUNT_ID, buildPendingHistoryContextFromMap, clearHistoryEntriesIfEnabled, DEFAULT_GROUP_HISTORY_LIMIT, + createScopedPairingAccess, logInboundDrop, recordPendingHistoryEntryIfEnabled, resolveControlCommandGate, @@ -57,6 +59,11 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) { log, } = deps; const core = getMSTeamsRuntime(); + const pairing = createScopedPairingAccess({ + core, + channel: "msteams", + accountId: DEFAULT_ACCOUNT_ID, + }); const logVerboseMessage = (message: string) => { if (core.logging.shouldLogVerbose()) { log.debug?.(message); @@ -132,8 +139,9 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) { const dmPolicy = msteamsCfg?.dmPolicy ?? "pairing"; const storedAllowFrom = await readStoreAllowFromForDmPolicy({ provider: "msteams", + accountId: pairing.accountId, dmPolicy, - readStore: (provider) => core.channel.pairing.readAllowFromStore(provider), + readStore: pairing.readStoreForDmPolicy, }); const useAccessGroups = cfg.commands?.useAccessGroups !== false; @@ -200,8 +208,7 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) { allowNameMatching: isDangerousNameMatchingEnabled(msteamsCfg), }); if (access.decision === "pairing") { - const request = await core.channel.pairing.upsertPairingRequest({ - channel: "msteams", + const request = await pairing.upsertPairingRequest({ id: senderId, meta: { name: senderName }, }); diff --git a/extensions/nextcloud-talk/src/inbound.ts b/extensions/nextcloud-talk/src/inbound.ts index 006bc4cff..69b983b68 100644 --- a/extensions/nextcloud-talk/src/inbound.ts +++ b/extensions/nextcloud-talk/src/inbound.ts @@ -1,5 +1,6 @@ import { GROUP_POLICY_BLOCKED_LABEL, + createScopedPairingAccess, createNormalizedOutboundDeliverer, createReplyPrefixOptions, formatTextWithAttachmentLinks, @@ -58,6 +59,11 @@ export async function handleNextcloudTalkInbound(params: { }): Promise { const { message, account, config, runtime, statusSink } = params; const core = getNextcloudTalkRuntime(); + const pairing = createScopedPairingAccess({ + core, + channel: CHANNEL_ID, + accountId: account.accountId, + }); const rawBody = message.text?.trim() ?? ""; if (!rawBody) { @@ -99,8 +105,9 @@ export async function handleNextcloudTalkInbound(params: { const configGroupAllowFrom = normalizeNextcloudTalkAllowlist(account.config.groupAllowFrom); const storeAllowFrom = await readStoreAllowFromForDmPolicy({ provider: CHANNEL_ID, + accountId: account.accountId, dmPolicy, - readStore: (provider) => core.channel.pairing.readAllowFromStore(provider), + readStore: pairing.readStoreForDmPolicy, }); const storeAllowList = normalizeNextcloudTalkAllowlist(storeAllowFrom); @@ -167,8 +174,7 @@ export async function handleNextcloudTalkInbound(params: { } else { if (access.decision !== "allow") { if (access.decision === "pairing") { - const { code, created } = await core.channel.pairing.upsertPairingRequest({ - channel: CHANNEL_ID, + const { code, created } = await pairing.upsertPairingRequest({ id: senderId, meta: { name: senderName || undefined }, }); diff --git a/extensions/zalo/src/monitor.ts b/extensions/zalo/src/monitor.ts index d1d5a91de..3063e231a 100644 --- a/extensions/zalo/src/monitor.ts +++ b/extensions/zalo/src/monitor.ts @@ -1,6 +1,7 @@ import type { IncomingMessage, ServerResponse } from "node:http"; import type { MarkdownTableMode, OpenClawConfig, OutboundReplyPayload } from "openclaw/plugin-sdk"; import { + createScopedPairingAccess, createReplyPrefixOptions, resolveSenderCommandAuthorization, resolveOutboundMediaUrls, @@ -303,6 +304,11 @@ async function processMessageWithPipeline(params: { statusSink, fetcher, } = params; + const pairing = createScopedPairingAccess({ + core, + channel: "zalo", + accountId: account.accountId, + }); const { from, chat, message_id, date } = message; const isGroup = chat.chat_type === "GROUP"; @@ -358,7 +364,7 @@ async function processMessageWithPipeline(params: { configuredGroupAllowFrom: groupAllowFrom, senderId, isSenderAllowed: isZaloSenderAllowed, - readAllowFromStore: () => core.channel.pairing.readAllowFromStore("zalo"), + readAllowFromStore: pairing.readAllowFromStore, shouldComputeCommandAuthorized: (body, cfg) => core.channel.commands.shouldComputeCommandAuthorized(body, cfg), resolveCommandAuthorizedFromAuthorizers: (params) => @@ -376,8 +382,7 @@ async function processMessageWithPipeline(params: { if (!allowed) { if (dmPolicy === "pairing") { - const { code, created } = await core.channel.pairing.upsertPairingRequest({ - channel: "zalo", + const { code, created } = await pairing.upsertPairingRequest({ id: senderId, meta: { name: senderName ?? undefined }, }); diff --git a/extensions/zalouser/src/monitor.ts b/extensions/zalouser/src/monitor.ts index 7e2ff850d..c6aee6adc 100644 --- a/extensions/zalouser/src/monitor.ts +++ b/extensions/zalouser/src/monitor.ts @@ -6,6 +6,7 @@ import type { RuntimeEnv, } from "openclaw/plugin-sdk"; import { + createScopedPairingAccess, createReplyPrefixOptions, resolveOutboundMediaUrls, mergeAllowlist, @@ -177,6 +178,11 @@ async function processMessage( statusSink?: (patch: { lastInboundAt?: number; lastOutboundAt?: number }) => void, ): Promise { const { threadId, content, timestamp, metadata } = message; + const pairing = createScopedPairingAccess({ + core, + channel: "zalouser", + accountId: account.accountId, + }); if (!content?.trim()) { return; } @@ -225,7 +231,7 @@ async function processMessage( configuredAllowFrom: configAllowFrom, senderId, isSenderAllowed, - readAllowFromStore: () => core.channel.pairing.readAllowFromStore("zalouser"), + readAllowFromStore: pairing.readAllowFromStore, shouldComputeCommandAuthorized: (body, cfg) => core.channel.commands.shouldComputeCommandAuthorized(body, cfg), resolveCommandAuthorizedFromAuthorizers: (params) => @@ -243,8 +249,7 @@ async function processMessage( if (!allowed) { if (dmPolicy === "pairing") { - const { code, created } = await core.channel.pairing.upsertPairingRequest({ - channel: "zalouser", + const { code, created } = await pairing.upsertPairingRequest({ id: senderId, meta: { name: senderName || undefined }, }); diff --git a/src/plugin-sdk/index.ts b/src/plugin-sdk/index.ts index 6dcff06a9..a4b32b182 100644 --- a/src/plugin-sdk/index.ts +++ b/src/plugin-sdk/index.ts @@ -216,6 +216,7 @@ export { type SenderGroupAccessReason, } from "./group-access.js"; export { resolveSenderCommandAuthorization } from "./command-auth.js"; +export { createScopedPairingAccess } from "./pairing-access.js"; export { handleSlackMessageAction } from "./slack-message-actions.js"; export { extractToolSend } from "./tool-send.js"; export { diff --git a/src/plugin-sdk/pairing-access.ts b/src/plugin-sdk/pairing-access.ts new file mode 100644 index 000000000..31f0cd4d3 --- /dev/null +++ b/src/plugin-sdk/pairing-access.ts @@ -0,0 +1,36 @@ +import type { ChannelId } from "../channels/plugins/types.js"; +import type { PluginRuntime } from "../plugins/runtime/types.js"; +import { normalizeAccountId } from "../routing/session-key.js"; + +type PairingApi = PluginRuntime["channel"]["pairing"]; +type ScopedUpsertInput = Omit< + Parameters[0], + "channel" | "accountId" +>; + +export function createScopedPairingAccess(params: { + core: PluginRuntime; + channel: ChannelId; + accountId: string; +}) { + const resolvedAccountId = normalizeAccountId(params.accountId); + return { + accountId: resolvedAccountId, + readAllowFromStore: () => + params.core.channel.pairing.readAllowFromStore({ + channel: params.channel, + accountId: resolvedAccountId, + }), + readStoreForDmPolicy: (provider: ChannelId, accountId: string) => + params.core.channel.pairing.readAllowFromStore({ + channel: provider, + accountId: normalizeAccountId(accountId), + }), + upsertPairingRequest: (input: ScopedUpsertInput) => + params.core.channel.pairing.upsertPairingRequest({ + channel: params.channel, + accountId: resolvedAccountId, + ...input, + }), + }; +}