diff --git a/extensions/imessage/src/approval-handler.runtime.ts b/extensions/imessage/src/approval-handler.runtime.ts index 050793d1561..537a5a4bc80 100644 --- a/extensions/imessage/src/approval-handler.runtime.ts +++ b/extensions/imessage/src/approval-handler.runtime.ts @@ -1,23 +1,18 @@ import { + buildChannelApprovalExpiredText, + buildChannelApprovalResolvedText, createChannelApprovalNativeRuntimeAdapter, - type ExpiredApprovalView, type PendingApprovalView, - type ResolvedApprovalView, + resolvePreparedApprovalAccountId, } from "openclaw/plugin-sdk/approval-handler-runtime"; import { buildChannelApprovalNativeTargetKey } from "openclaw/plugin-sdk/approval-native-runtime"; import { buildApprovalReactionPendingContent } from "openclaw/plugin-sdk/approval-reaction-runtime"; import type { ExecApprovalReplyDecision } from "openclaw/plugin-sdk/approval-reply-runtime"; import { - buildApprovalResolvedReplyPayload, - buildPluginApprovalExpiredMessage, - buildPluginApprovalResolvedMessage, type ExecApprovalRequest, - type ExecApprovalResolved, type PluginApprovalRequest, - type PluginApprovalResolved, } from "openclaw/plugin-sdk/approval-runtime"; import { createSubsystemLogger } from "openclaw/plugin-sdk/runtime-env"; -import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime"; import { registerIMessageApprovalReactionTarget, unregisterIMessageApprovalReactionTarget, @@ -30,7 +25,6 @@ import { normalizeIMessageHandle, parseIMessageTarget } from "./targets.js"; const log = createSubsystemLogger("imessage/approvals"); type ApprovalRequest = ExecApprovalRequest | PluginApprovalRequest; -type ApprovalResolved = ExecApprovalResolved | PluginApprovalResolved; type IMessagePendingDelivery = { text: string; allowedDecisions: readonly ExecApprovalReplyDecision[]; @@ -66,42 +60,6 @@ function buildPendingPayload(params: { }; } -function buildResolvedText(params: { - request: ApprovalRequest; - resolved: ApprovalResolved; - view: ResolvedApprovalView; -}): string { - if (params.view.approvalKind === "plugin") { - return buildPluginApprovalResolvedMessage(params.resolved as PluginApprovalResolved); - } - const resolvedByText = params.resolved.resolvedBy - ? ` Resolved by ${params.resolved.resolvedBy}.` - : ""; - const payload = buildApprovalResolvedReplyPayload({ - approvalId: params.request.id, - approvalSlug: params.request.id.slice(0, 8), - text: `✅ Exec approval ${params.resolved.decision}.${resolvedByText} ID: ${params.request.id}`, - }); - return payload.text ?? ""; -} - -function buildExpiredText(params: { request: ApprovalRequest; view: ExpiredApprovalView }): string { - if (params.view.approvalKind === "plugin") { - return buildPluginApprovalExpiredMessage(params.request as PluginApprovalRequest); - } - return `⏱️ Exec approval expired. ID: ${params.request.id}`; -} - -function resolvePreparedAccountId(params: { - plannedAccountId?: string | null; - contextAccountId?: string | null; -}): string | undefined { - return ( - normalizeOptionalString(params.plannedAccountId) ?? - normalizeOptionalString(params.contextAccountId) - ); -} - function buildConversationKeyForTarget(to: string): IMessageApprovalConversationKey | null { try { const parsed = parseIMessageTarget(to); @@ -138,11 +96,11 @@ export const imessageApprovalNativeRuntime = createChannelApprovalNativeRuntimeA buildPendingPayload({ request, approvalKind, nowMs, view }), buildResolvedResult: ({ request, resolved, view }) => ({ kind: "update", - payload: { text: buildResolvedText({ request, resolved, view }) }, + payload: { text: buildChannelApprovalResolvedText({ request, resolved, view }) }, }), buildExpiredResult: ({ request, view }) => ({ kind: "update", - payload: { text: buildExpiredText({ request, view }) }, + payload: { text: buildChannelApprovalExpiredText({ request, view }) }, }), }, transport: { @@ -153,7 +111,7 @@ export const imessageApprovalNativeRuntime = createChannelApprovalNativeRuntimeA } const prepared: PreparedIMessageApprovalTarget = { to, - accountId: resolvePreparedAccountId({ + accountId: resolvePreparedApprovalAccountId({ plannedAccountId: (plannedTarget.target as { accountId?: string | null }).accountId, contextAccountId: accountId, }), diff --git a/extensions/imessage/src/approval-native.ts b/extensions/imessage/src/approval-native.ts index 06994631c5b..8320dca51e2 100644 --- a/extensions/imessage/src/approval-native.ts +++ b/extensions/imessage/src/approval-native.ts @@ -1,4 +1,3 @@ -import { matchesApprovalRequestFilters } from "openclaw/plugin-sdk/approval-client-runtime"; import { createChannelApprovalCapability, splitChannelApprovalCapability, @@ -6,9 +5,11 @@ import { import { createLazyChannelApprovalNativeRuntimeAdapter } from "openclaw/plugin-sdk/approval-handler-adapter-runtime"; import type { ChannelApprovalNativeRuntimeAdapter } from "openclaw/plugin-sdk/approval-handler-runtime"; import { + createChannelApprovalForwardingEvaluator, createChannelApproverDmTargetResolver, createChannelNativeOriginTargetResolver, - doesApprovalRequestMatchChannelAccount, + createNativeApprovalForwardingFallbackSuppressor, + nativeApprovalTargetsMatch, resolveApprovalRequestSessionTarget, shouldSuppressLocalNativeExecApprovalPrompt, } from "openclaw/plugin-sdk/approval-native-runtime"; @@ -28,7 +29,6 @@ import type { ChannelApprovalCapability, ChannelOutboundPayloadHint, } from "openclaw/plugin-sdk/channel-contract"; -import { channelRouteTargetsMatchExact } from "openclaw/plugin-sdk/channel-route"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts"; import type { ReplyPayload } from "openclaw/plugin-sdk/reply-runtime"; import { normalizeAccountId, parseAgentSessionKey } from "openclaw/plugin-sdk/routing"; @@ -50,7 +50,6 @@ import { inferIMessageTargetChatType } from "./targets.js"; type ApprovalRequest = ExecApprovalRequest | PluginApprovalRequest; type ApprovalKind = "exec" | "plugin"; type ApprovalForwardingConfig = NonNullable["exec"]>; -type ApprovalForwardingMode = NonNullable; type ChannelApprovalForwardTarget = Parameters< NonNullable< NonNullable["shouldSuppressForwardingFallback"] @@ -62,7 +61,6 @@ type IMessageApprovalTarget = { threadId?: string | number | null; }; -const DEFAULT_APPROVAL_FORWARDING_MODE: ApprovalForwardingMode = "session"; const DEFAULT_PLUGIN_APPROVAL_DECISIONS: readonly ExecApprovalReplyDecision[] = [ "allow-once", "allow-always", @@ -76,48 +74,6 @@ function isIMessageApprovalTransportEnabled(params: { return resolveIMessageAccount({ cfg: params.cfg, accountId: params.accountId }).enabled; } -function resolveApprovalKind(request: ApprovalRequest, approvalKind?: ApprovalKind): ApprovalKind { - if (approvalKind) { - return approvalKind; - } - return "command" in request.request ? "exec" : "plugin"; -} - -function resolveApprovalForwardingConfig(params: { - cfg: OpenClawConfig; - approvalKind: ApprovalKind; -}): ApprovalForwardingConfig | undefined { - return params.approvalKind === "plugin" - ? params.cfg.approvals?.plugin - : params.cfg.approvals?.exec; -} - -function normalizeApprovalForwardingMode( - mode: ApprovalForwardingConfig["mode"] | undefined, -): ApprovalForwardingMode { - return mode ?? DEFAULT_APPROVAL_FORWARDING_MODE; -} - -function approvalModeIncludesSession(mode: ApprovalForwardingMode): boolean { - return mode === "session" || mode === "both"; -} - -function approvalModeIncludesTargets(mode: ApprovalForwardingMode): boolean { - return mode === "targets" || mode === "both"; -} - -function matchesForwardingFilters(params: { - config: ApprovalForwardingConfig; - request: ApprovalRequest; -}): boolean { - return matchesApprovalRequestFilters({ - request: params.request.request, - agentFilter: params.config.agentFilter, - sessionFilter: params.config.sessionFilter, - fallbackAgentIdFromSessionKey: true, - }); -} - function targetAccountMatchesIMessageAccount(params: { cfg: OpenClawConfig; targetAccountId?: string | null; @@ -164,26 +120,6 @@ function normalizeIMessageForwardTarget( }; } -function nativeApprovalTargetsMatch(params: { - left: IMessageApprovalTarget; - right: IMessageApprovalTarget; -}): boolean { - return channelRouteTargetsMatchExact({ - left: { - channel: "imessage", - to: params.left.to, - accountId: params.left.accountId, - threadId: params.left.threadId, - }, - right: { - channel: "imessage", - to: params.right.to, - accountId: params.right.accountId, - threadId: params.right.threadId, - }, - }); -} - function hasMatchingIMessageTarget(params: { cfg: OpenClawConfig; config: ApprovalForwardingConfig; @@ -208,7 +144,11 @@ function hasMatchingIMessageTarget(params: { if (!candidateTarget) { return true; } - return nativeApprovalTargetsMatch({ left: configuredTarget, right: candidateTarget }); + return nativeApprovalTargetsMatch({ + channel: "imessage", + left: configuredTarget, + right: candidateTarget, + }); }); } @@ -235,118 +175,17 @@ function hasIMessageOriginOrSessionTarget(params: { ); } -function canApprovalPotentiallyRouteToIMessage(params: { - cfg: OpenClawConfig; - accountId?: string | null; - approvalKind: ApprovalKind; - nativeSessionOnly?: boolean; -}): boolean { - if (!isIMessageApprovalTransportEnabled(params)) { - return false; - } - const config = resolveApprovalForwardingConfig(params); - if (!config?.enabled) { - return false; - } - const mode = normalizeApprovalForwardingMode(config.mode); - if (approvalModeIncludesSession(mode)) { - return true; - } - if (params.nativeSessionOnly) { - return false; - } - return ( - approvalModeIncludesTargets(mode) && - hasMatchingIMessageTarget({ - cfg: params.cfg, - config, - accountId: params.accountId, - }) - ); -} +const imessageApprovalForwarding = createChannelApprovalForwardingEvaluator({ + channel: "imessage", + isTransportEnabled: isIMessageApprovalTransportEnabled, + hasMatchingTarget: hasMatchingIMessageTarget, + hasOriginOrSessionTarget: hasIMessageOriginOrSessionTarget, +}); -function canAnyApprovalPotentiallyRouteToIMessage(params: { - cfg: OpenClawConfig; - accountId?: string | null; - nativeSessionOnly?: boolean; -}): boolean { - return ( - canApprovalPotentiallyRouteToIMessage({ - ...params, - approvalKind: "exec", - }) || - canApprovalPotentiallyRouteToIMessage({ - ...params, - approvalKind: "plugin", - }) - ); -} - -function isIMessageSessionApprovalEligible(params: { - cfg: OpenClawConfig; - accountId?: string | null; - approvalKind: ApprovalKind; - request: ApprovalRequest; -}): boolean { - if (!isIMessageApprovalTransportEnabled(params)) { - return false; - } - const config = resolveApprovalForwardingConfig(params); - if (!config?.enabled) { - return false; - } - const mode = normalizeApprovalForwardingMode(config.mode); - if (!approvalModeIncludesSession(mode)) { - return false; - } - if (!matchesForwardingFilters({ config, request: params.request })) { - return false; - } - if ( - !doesApprovalRequestMatchChannelAccount({ - cfg: params.cfg, - request: params.request, - channel: "imessage", - accountId: params.accountId, - }) - ) { - return false; - } - return hasIMessageOriginOrSessionTarget({ - cfg: params.cfg, - accountId: params.accountId, - request: params.request, - }); -} - -function isIMessageExplicitTargetEligible(params: { - cfg: OpenClawConfig; - accountId?: string | null; - approvalKind: ApprovalKind; - request: ApprovalRequest; - target: ChannelApprovalForwardTarget; -}): boolean { - if (!isIMessageApprovalTransportEnabled(params)) { - return false; - } - const config = resolveApprovalForwardingConfig(params); - if (!config?.enabled) { - return false; - } - const mode = normalizeApprovalForwardingMode(config.mode); - if (!approvalModeIncludesTargets(mode)) { - return false; - } - if (!matchesForwardingFilters({ config, request: params.request })) { - return false; - } - return hasMatchingIMessageTarget({ - cfg: params.cfg, - config, - accountId: params.accountId, - target: params.target, - }); -} +const canApprovalPotentiallyRouteToIMessage = imessageApprovalForwarding.isPotentialRoute; +const canAnyApprovalPotentiallyRouteToIMessage = imessageApprovalForwarding.canAnyPotentiallyRoute; +const isIMessageSessionApprovalEligible = imessageApprovalForwarding.isSessionEligible; +const isIMessageExplicitTargetEligible = imessageApprovalForwarding.isExplicitTargetEligible; function resolveTurnSourceIMessageOriginTarget( request: ApprovalRequest, @@ -475,10 +314,7 @@ function shouldHandleIMessageApprovalRequest(params: { approvalKind?: ApprovalKind; request: ApprovalRequest; }): boolean { - return isIMessageSessionApprovalEligible({ - ...params, - approvalKind: resolveApprovalKind(params.request, params.approvalKind), - }); + return imessageApprovalForwarding.shouldHandleRequest(params); } const resolveIMessageOriginTargetBase = createChannelNativeOriginTargetResolver({ @@ -528,6 +364,22 @@ const resolveIMessageApproverDmTargets = createChannelApproverDmTargetResolver({ }, }); +const shouldSuppressIMessageForwardingFallback = + createNativeApprovalForwardingFallbackSuppressor({ + channel: "imessage", + normalizeForwardTarget: normalizeIMessageForwardTarget, + resolveAccountId: ({ forwardingTarget, request }) => + forwardingTarget.accountId ?? normalizeOptionalString(request.request.turnSourceAccountId), + resolveForwardingTargetForMatch: ({ forwardingTarget, accountId }) => ({ + ...forwardingTarget, + accountId, + }), + isSessionRouteEligible: isIMessageSessionApprovalEligible, + isExplicitTargetEligible: isIMessageExplicitTargetEligible, + resolveOriginTarget: resolveIMessageOriginTarget, + resolveApproverDmTargets: resolveIMessageApproverDmTargets, + }); + function appendIMessageReactionHint(params: { text?: string; allowedDecisions: readonly ExecApprovalReplyDecision[]; @@ -625,58 +477,7 @@ export const imessageApprovalCapability: ChannelApprovalCapability = } return getIMessageApprovalApprovers({ cfg, accountId }).length > 0; }), - shouldSuppressForwardingFallback: ({ cfg, approvalKind, target, request }) => { - const forwardingTarget = normalizeIMessageForwardTarget(target); - if (!forwardingTarget) { - return false; - } - const accountId = - forwardingTarget.accountId ?? - normalizeOptionalString(request.request.turnSourceAccountId); - const forwardingTargetForMatch = { - ...forwardingTarget, - accountId, - }; - const kind = resolveApprovalKind(request, approvalKind); - const eligible = - target.source === "target" - ? isIMessageExplicitTargetEligible({ - cfg, - accountId, - approvalKind: kind, - request, - target, - }) - : isIMessageSessionApprovalEligible({ - cfg, - accountId, - approvalKind: kind, - request, - }); - if (!eligible) { - return false; - } - const originTarget = resolveIMessageOriginTarget({ - cfg, - accountId, - approvalKind: kind, - request, - }); - if ( - originTarget && - nativeApprovalTargetsMatch({ left: forwardingTargetForMatch, right: originTarget }) - ) { - return true; - } - return resolveIMessageApproverDmTargets({ - cfg, - accountId, - approvalKind: kind, - request, - }).some((approverTarget) => - nativeApprovalTargetsMatch({ left: forwardingTargetForMatch, right: approverTarget }), - ); - }, + shouldSuppressForwardingFallback: shouldSuppressIMessageForwardingFallback, }, render: { exec: { diff --git a/extensions/signal/src/approval-handler.runtime.ts b/extensions/signal/src/approval-handler.runtime.ts index 8dada172b5c..d038b2e93ea 100644 --- a/extensions/signal/src/approval-handler.runtime.ts +++ b/extensions/signal/src/approval-handler.runtime.ts @@ -1,9 +1,10 @@ import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/account-id"; import { + buildChannelApprovalExpiredText, + buildChannelApprovalResolvedText, createChannelApprovalNativeRuntimeAdapter, - type ExpiredApprovalView, type PendingApprovalView, - type ResolvedApprovalView, + resolvePreparedApprovalAccountId, } from "openclaw/plugin-sdk/approval-handler-runtime"; import { buildChannelApprovalNativeTargetKey } from "openclaw/plugin-sdk/approval-native-runtime"; import { @@ -11,13 +12,8 @@ import { type ApprovalReactionPendingContent, } from "openclaw/plugin-sdk/approval-reaction-runtime"; import { - buildApprovalResolvedReplyPayload, - buildPluginApprovalExpiredMessage, - buildPluginApprovalResolvedMessage, type ExecApprovalRequest, - type ExecApprovalResolved, type PluginApprovalRequest, - type PluginApprovalResolved, } from "openclaw/plugin-sdk/approval-runtime"; import { createSubsystemLogger } from "openclaw/plugin-sdk/runtime-env"; import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime"; @@ -34,7 +30,6 @@ import { sendMessageSignal, sendTypingSignal } from "./send.js"; const log = createSubsystemLogger("signal/approvals"); type ApprovalRequest = ExecApprovalRequest | PluginApprovalRequest; -type ApprovalResolved = ExecApprovalResolved | PluginApprovalResolved; type SignalPendingDelivery = ApprovalReactionPendingContent; type PreparedSignalApprovalTarget = { to: string; @@ -89,43 +84,6 @@ function buildPendingPayload(params: { return buildApprovalReactionPendingContent(params); } -function buildResolvedText(params: { - request: ApprovalRequest; - resolved: ApprovalResolved; - view: ResolvedApprovalView; -}): string { - if (params.view.approvalKind === "plugin") { - return buildPluginApprovalResolvedMessage(params.resolved as PluginApprovalResolved); - } - const resolvedByText = params.resolved.resolvedBy - ? ` Resolved by ${params.resolved.resolvedBy}.` - : ""; - const payload = buildApprovalResolvedReplyPayload({ - approvalId: params.request.id, - approvalSlug: params.request.id.slice(0, 8), - text: `✅ Exec approval ${params.resolved.decision}.${resolvedByText} ID: ${params.request.id}`, - }); - return payload.text ?? ""; -} - -function buildExpiredText(params: { request: ApprovalRequest; view: ExpiredApprovalView }): string { - if (params.view.approvalKind === "plugin") { - return buildPluginApprovalExpiredMessage(params.request as PluginApprovalRequest); - } - return `⏱️ Exec approval expired. ID: ${params.request.id}`; -} - -function resolvePreparedAccountId(params: { - plannedAccountId?: string | null; - contextAccountId?: string | null; -}): string { - return ( - normalizeOptionalString(params.plannedAccountId) ?? - normalizeOptionalString(params.contextAccountId) ?? - DEFAULT_ACCOUNT_ID - ); -} - export const signalApprovalNativeRuntime = createChannelApprovalNativeRuntimeAdapter< SignalPendingDelivery, PreparedSignalApprovalTarget, @@ -143,11 +101,11 @@ export const signalApprovalNativeRuntime = createChannelApprovalNativeRuntimeAda buildPendingPayload({ request, nowMs, view }), buildResolvedResult: ({ request, resolved, view }) => ({ kind: "update", - payload: { text: buildResolvedText({ request, resolved, view }) }, + payload: { text: buildChannelApprovalResolvedText({ request, resolved, view }) }, }), buildExpiredResult: ({ request, view }) => ({ kind: "update", - payload: { text: buildExpiredText({ request, view }) }, + payload: { text: buildChannelApprovalExpiredText({ request, view }) }, }), }, transport: { @@ -163,9 +121,10 @@ export const signalApprovalNativeRuntime = createChannelApprovalNativeRuntimeAda }); const prepared: PreparedSignalApprovalTarget = { to, - accountId: resolvePreparedAccountId({ + accountId: resolvePreparedApprovalAccountId({ plannedAccountId: (plannedTarget.target as { accountId?: string | null }).accountId, contextAccountId: accountId, + fallbackAccountId: DEFAULT_ACCOUNT_ID, }), ...(runtimeContext.baseUrl ? { baseUrl: runtimeContext.baseUrl } : {}), ...(runtimeContext.account ? { account: runtimeContext.account } : {}), diff --git a/extensions/signal/src/approval-native.ts b/extensions/signal/src/approval-native.ts index 0588801a214..b51e5413aeb 100644 --- a/extensions/signal/src/approval-native.ts +++ b/extensions/signal/src/approval-native.ts @@ -1,4 +1,3 @@ -import { matchesApprovalRequestFilters } from "openclaw/plugin-sdk/approval-client-runtime"; import { createChannelApprovalCapability, splitChannelApprovalCapability, @@ -6,10 +5,10 @@ import { import { createLazyChannelApprovalNativeRuntimeAdapter } from "openclaw/plugin-sdk/approval-handler-adapter-runtime"; import type { ChannelApprovalNativeRuntimeAdapter } from "openclaw/plugin-sdk/approval-handler-runtime"; import { + createChannelApprovalForwardingEvaluator, createChannelApproverDmTargetResolver, createChannelNativeOriginTargetResolver, createNativeApprovalForwardingFallbackSuppressor, - doesApprovalRequestMatchChannelAccount, nativeApprovalTargetsMatch, resolveApprovalRequestSessionTarget, shouldSuppressLocalNativeExecApprovalPrompt, @@ -41,7 +40,6 @@ import { normalizeSignalMessagingTarget } from "./normalize.js"; type ApprovalRequest = ExecApprovalRequest | PluginApprovalRequest; type ApprovalKind = "exec" | "plugin"; type ApprovalForwardingConfig = NonNullable["exec"]>; -type ApprovalForwardingMode = NonNullable; type ChannelApprovalForwardTarget = Parameters< NonNullable< NonNullable["shouldSuppressForwardingFallback"] @@ -53,8 +51,6 @@ type SignalApprovalTarget = { threadId?: string | number | null; }; -const DEFAULT_APPROVAL_FORWARDING_MODE: ApprovalForwardingMode = "session"; - function isSignalApprovalTransportEnabled(params: { cfg: OpenClawConfig; accountId?: string | null; @@ -62,48 +58,6 @@ function isSignalApprovalTransportEnabled(params: { return resolveSignalAccount({ cfg: params.cfg, accountId: params.accountId }).enabled; } -function resolveApprovalKind(request: ApprovalRequest, approvalKind?: ApprovalKind): ApprovalKind { - if (approvalKind) { - return approvalKind; - } - return "command" in request.request ? "exec" : "plugin"; -} - -function resolveApprovalForwardingConfig(params: { - cfg: OpenClawConfig; - approvalKind: ApprovalKind; -}): ApprovalForwardingConfig | undefined { - return params.approvalKind === "plugin" - ? params.cfg.approvals?.plugin - : params.cfg.approvals?.exec; -} - -function normalizeApprovalForwardingMode( - mode: ApprovalForwardingConfig["mode"] | undefined, -): ApprovalForwardingMode { - return mode ?? DEFAULT_APPROVAL_FORWARDING_MODE; -} - -function approvalModeIncludesSession(mode: ApprovalForwardingMode): boolean { - return mode === "session" || mode === "both"; -} - -function approvalModeIncludesTargets(mode: ApprovalForwardingMode): boolean { - return mode === "targets" || mode === "both"; -} - -function matchesForwardingFilters(params: { - config: ApprovalForwardingConfig; - request: ApprovalRequest; -}): boolean { - return matchesApprovalRequestFilters({ - request: params.request.request, - agentFilter: params.config.agentFilter, - sessionFilter: params.config.sessionFilter, - fallbackAgentIdFromSessionKey: true, - }); -} - function targetAccountMatchesSignalAccount(params: { cfg: OpenClawConfig; targetAccountId?: string | null; @@ -205,52 +159,16 @@ function hasSignalOriginOrSessionTarget(params: { ); } -function canApprovalPotentiallyRouteToSignal(params: { - cfg: OpenClawConfig; - accountId?: string | null; - approvalKind: ApprovalKind; - nativeSessionOnly?: boolean; -}): boolean { - if (!isSignalApprovalTransportEnabled(params)) { - return false; - } - const config = resolveApprovalForwardingConfig(params); - if (!config?.enabled) { - return false; - } - const mode = normalizeApprovalForwardingMode(config.mode); - if (approvalModeIncludesSession(mode)) { - return true; - } - if (params.nativeSessionOnly) { - return false; - } - return ( - approvalModeIncludesTargets(mode) && - hasMatchingSignalTarget({ - cfg: params.cfg, - config, - accountId: params.accountId, - }) - ); -} +const signalApprovalForwarding = createChannelApprovalForwardingEvaluator({ + channel: "signal", + isTransportEnabled: isSignalApprovalTransportEnabled, + hasMatchingTarget: hasMatchingSignalTarget, + hasOriginOrSessionTarget: hasSignalOriginOrSessionTarget, +}); -function canAnyApprovalPotentiallyRouteToSignal(params: { - cfg: OpenClawConfig; - accountId?: string | null; - nativeSessionOnly?: boolean; -}): boolean { - return ( - canApprovalPotentiallyRouteToSignal({ - ...params, - approvalKind: "exec", - }) || - canApprovalPotentiallyRouteToSignal({ - ...params, - approvalKind: "plugin", - }) - ); -} +const canApprovalPotentiallyRouteToSignal = signalApprovalForwarding.isPotentialRoute; +const canAnyApprovalPotentiallyRouteToSignal = signalApprovalForwarding.canAnyPotentiallyRoute; +const isSignalSessionApprovalEligible = signalApprovalForwarding.isSessionEligible; export function isSignalNativeApprovalHandlerConfigured(params: { cfg: OpenClawConfig; @@ -262,43 +180,6 @@ export function isSignalNativeApprovalHandlerConfigured(params: { }); } -function isSignalSessionApprovalEligible(params: { - cfg: OpenClawConfig; - accountId?: string | null; - approvalKind: ApprovalKind; - request: ApprovalRequest; -}): boolean { - if (!isSignalApprovalTransportEnabled(params)) { - return false; - } - const config = resolveApprovalForwardingConfig(params); - if (!config?.enabled) { - return false; - } - const mode = normalizeApprovalForwardingMode(config.mode); - if (!approvalModeIncludesSession(mode)) { - return false; - } - if (!matchesForwardingFilters({ config, request: params.request })) { - return false; - } - if ( - !doesApprovalRequestMatchChannelAccount({ - cfg: params.cfg, - request: params.request, - channel: "signal", - accountId: params.accountId, - }) - ) { - return false; - } - return hasSignalOriginOrSessionTarget({ - cfg: params.cfg, - accountId: params.accountId, - request: params.request, - }); -} - function resolveTurnSourceSignalOriginTarget( request: ApprovalRequest, ): SignalApprovalTarget | null { @@ -330,10 +211,7 @@ function shouldHandleSignalApprovalRequest(params: { approvalKind?: ApprovalKind; request: ApprovalRequest; }): boolean { - return isSignalSessionApprovalEligible({ - ...params, - approvalKind: resolveApprovalKind(params.request, params.approvalKind), - }); + return signalApprovalForwarding.shouldHandleRequest(params); } function resolveSignalSessionTargetFromSessionKey(sessionKey?: string | null): string | null { diff --git a/extensions/whatsapp/src/approval-handler.runtime.ts b/extensions/whatsapp/src/approval-handler.runtime.ts index 9cdf9d27723..658051033c4 100644 --- a/extensions/whatsapp/src/approval-handler.runtime.ts +++ b/extensions/whatsapp/src/approval-handler.runtime.ts @@ -1,8 +1,9 @@ import { + buildChannelApprovalExpiredText, + buildChannelApprovalResolvedText, createChannelApprovalNativeRuntimeAdapter, - type ExpiredApprovalView, type PendingApprovalView, - type ResolvedApprovalView, + resolvePreparedApprovalAccountId, } from "openclaw/plugin-sdk/approval-handler-runtime"; import { buildChannelApprovalNativeTargetKey } from "openclaw/plugin-sdk/approval-native-runtime"; import { @@ -10,16 +11,10 @@ import { type ApprovalReactionPendingContent, } from "openclaw/plugin-sdk/approval-reaction-runtime"; import { - buildApprovalResolvedReplyPayload, - buildPluginApprovalExpiredMessage, - buildPluginApprovalResolvedMessage, type ExecApprovalRequest, - type ExecApprovalResolved, type PluginApprovalRequest, - type PluginApprovalResolved, } from "openclaw/plugin-sdk/approval-runtime"; import { createSubsystemLogger } from "openclaw/plugin-sdk/runtime-env"; -import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime"; import { registerWhatsAppApprovalReactionTarget, unregisterWhatsAppApprovalReactionTarget, @@ -31,7 +26,6 @@ import { sendMessageWhatsApp, sendTypingWhatsApp } from "./send.js"; const log = createSubsystemLogger("whatsapp/approvals"); type ApprovalRequest = ExecApprovalRequest | PluginApprovalRequest; -type ApprovalResolved = ExecApprovalResolved | PluginApprovalResolved; type WhatsAppPendingDelivery = ApprovalReactionPendingContent; type PreparedWhatsAppApprovalTarget = { to: string; @@ -55,42 +49,6 @@ function buildPendingPayload(params: { return buildApprovalReactionPendingContent(params); } -function buildResolvedText(params: { - request: ApprovalRequest; - resolved: ApprovalResolved; - view: ResolvedApprovalView; -}): string { - if (params.view.approvalKind === "plugin") { - return buildPluginApprovalResolvedMessage(params.resolved as PluginApprovalResolved); - } - const resolvedByText = params.resolved.resolvedBy - ? ` Resolved by ${params.resolved.resolvedBy}.` - : ""; - const payload = buildApprovalResolvedReplyPayload({ - approvalId: params.request.id, - approvalSlug: params.request.id.slice(0, 8), - text: `✅ Exec approval ${params.resolved.decision}.${resolvedByText} ID: ${params.request.id}`, - }); - return payload.text ?? ""; -} - -function buildExpiredText(params: { request: ApprovalRequest; view: ExpiredApprovalView }): string { - if (params.view.approvalKind === "plugin") { - return buildPluginApprovalExpiredMessage(params.request as PluginApprovalRequest); - } - return `⏱️ Exec approval expired. ID: ${params.request.id}`; -} - -function resolvePreparedAccountId(params: { - plannedAccountId?: string | null; - contextAccountId?: string | null; -}): string | undefined { - return ( - normalizeOptionalString(params.plannedAccountId) ?? - normalizeOptionalString(params.contextAccountId) - ); -} - export const whatsappApprovalNativeRuntime = createChannelApprovalNativeRuntimeAdapter< WhatsAppPendingDelivery, PreparedWhatsAppApprovalTarget, @@ -108,11 +66,11 @@ export const whatsappApprovalNativeRuntime = createChannelApprovalNativeRuntimeA buildPendingPayload({ request, view, nowMs }), buildResolvedResult: ({ request, resolved, view }) => ({ kind: "update", - payload: { text: buildResolvedText({ request, resolved, view }) }, + payload: { text: buildChannelApprovalResolvedText({ request, resolved, view }) }, }), buildExpiredResult: ({ request, view }) => ({ kind: "update", - payload: { text: buildExpiredText({ request, view }) }, + payload: { text: buildChannelApprovalExpiredText({ request, view }) }, }), }, transport: { @@ -123,7 +81,7 @@ export const whatsappApprovalNativeRuntime = createChannelApprovalNativeRuntimeA } const prepared: PreparedWhatsAppApprovalTarget = { to, - accountId: resolvePreparedAccountId({ + accountId: resolvePreparedApprovalAccountId({ plannedAccountId: (plannedTarget.target as { accountId?: string | null }).accountId, contextAccountId: accountId, }), diff --git a/extensions/whatsapp/src/approval-native.ts b/extensions/whatsapp/src/approval-native.ts index cbf7c5121d3..ce7010f6023 100644 --- a/extensions/whatsapp/src/approval-native.ts +++ b/extensions/whatsapp/src/approval-native.ts @@ -1,4 +1,3 @@ -import { matchesApprovalRequestFilters } from "openclaw/plugin-sdk/approval-client-runtime"; import { createChannelApprovalCapability, splitChannelApprovalCapability, @@ -6,10 +5,10 @@ import { import { createLazyChannelApprovalNativeRuntimeAdapter } from "openclaw/plugin-sdk/approval-handler-adapter-runtime"; import type { ChannelApprovalNativeRuntimeAdapter } from "openclaw/plugin-sdk/approval-handler-runtime"; import { + createChannelApprovalForwardingEvaluator, createChannelApproverDmTargetResolver, createChannelNativeOriginTargetResolver, createNativeApprovalForwardingFallbackSuppressor, - doesApprovalRequestMatchChannelAccount, nativeApprovalTargetsMatch, resolveApprovalRequestSessionTarget, } from "openclaw/plugin-sdk/approval-native-runtime"; @@ -36,7 +35,6 @@ import { isWhatsAppGroupJid, normalizeWhatsAppMessagingTarget } from "./normaliz type ApprovalRequest = ExecApprovalRequest | PluginApprovalRequest; type ApprovalKind = "exec" | "plugin"; type ApprovalForwardingConfig = NonNullable["exec"]>; -type ApprovalForwardingMode = NonNullable; type ChannelApprovalForwardTarget = Parameters< NonNullable< NonNullable["shouldSuppressForwardingFallback"] @@ -48,8 +46,6 @@ type WhatsAppApprovalTarget = { threadId?: string | number | null; }; -const DEFAULT_APPROVAL_FORWARDING_MODE: ApprovalForwardingMode = "session"; - function isWhatsAppApprovalTransportEnabled(params: { cfg: OpenClawConfig; accountId?: string | null; @@ -57,48 +53,6 @@ function isWhatsAppApprovalTransportEnabled(params: { return resolveWhatsAppAccount({ cfg: params.cfg, accountId: params.accountId }).enabled; } -function resolveApprovalKind(request: ApprovalRequest, approvalKind?: ApprovalKind): ApprovalKind { - if (approvalKind) { - return approvalKind; - } - return "command" in request.request ? "exec" : "plugin"; -} - -function resolveApprovalForwardingConfig(params: { - cfg: OpenClawConfig; - approvalKind: ApprovalKind; -}): ApprovalForwardingConfig | undefined { - return params.approvalKind === "plugin" - ? params.cfg.approvals?.plugin - : params.cfg.approvals?.exec; -} - -function normalizeApprovalForwardingMode( - mode: ApprovalForwardingConfig["mode"] | undefined, -): ApprovalForwardingMode { - return mode ?? DEFAULT_APPROVAL_FORWARDING_MODE; -} - -function approvalModeIncludesSession(mode: ApprovalForwardingMode): boolean { - return mode === "session" || mode === "both"; -} - -function approvalModeIncludesTargets(mode: ApprovalForwardingMode): boolean { - return mode === "targets" || mode === "both"; -} - -function matchesForwardingFilters(params: { - config: ApprovalForwardingConfig; - request: ApprovalRequest; -}): boolean { - return matchesApprovalRequestFilters({ - request: params.request.request, - agentFilter: params.config.agentFilter, - sessionFilter: params.config.sessionFilter, - fallbackAgentIdFromSessionKey: true, - }); -} - function targetAccountMatchesWhatsAppAccount(params: { cfg: OpenClawConfig; targetAccountId?: string | null; @@ -200,118 +154,17 @@ function hasWhatsAppOriginOrSessionTarget(params: { ); } -function canApprovalPotentiallyRouteToWhatsApp(params: { - cfg: OpenClawConfig; - accountId?: string | null; - approvalKind: ApprovalKind; - nativeSessionOnly?: boolean; -}): boolean { - if (!isWhatsAppApprovalTransportEnabled(params)) { - return false; - } - const config = resolveApprovalForwardingConfig(params); - if (!config?.enabled) { - return false; - } - const mode = normalizeApprovalForwardingMode(config.mode); - if (approvalModeIncludesSession(mode)) { - return true; - } - if (params.nativeSessionOnly) { - return false; - } - return ( - approvalModeIncludesTargets(mode) && - hasMatchingWhatsAppTarget({ - cfg: params.cfg, - config, - accountId: params.accountId, - }) - ); -} +const whatsappApprovalForwarding = createChannelApprovalForwardingEvaluator({ + channel: "whatsapp", + isTransportEnabled: isWhatsAppApprovalTransportEnabled, + hasMatchingTarget: hasMatchingWhatsAppTarget, + hasOriginOrSessionTarget: hasWhatsAppOriginOrSessionTarget, +}); -function canAnyApprovalPotentiallyRouteToWhatsApp(params: { - cfg: OpenClawConfig; - accountId?: string | null; - nativeSessionOnly?: boolean; -}): boolean { - return ( - canApprovalPotentiallyRouteToWhatsApp({ - ...params, - approvalKind: "exec", - }) || - canApprovalPotentiallyRouteToWhatsApp({ - ...params, - approvalKind: "plugin", - }) - ); -} - -function isWhatsAppSessionApprovalEligible(params: { - cfg: OpenClawConfig; - accountId?: string | null; - approvalKind: ApprovalKind; - request: ApprovalRequest; -}): boolean { - if (!isWhatsAppApprovalTransportEnabled(params)) { - return false; - } - const config = resolveApprovalForwardingConfig(params); - if (!config?.enabled) { - return false; - } - const mode = normalizeApprovalForwardingMode(config.mode); - if (!approvalModeIncludesSession(mode)) { - return false; - } - if (!matchesForwardingFilters({ config, request: params.request })) { - return false; - } - if ( - !doesApprovalRequestMatchChannelAccount({ - cfg: params.cfg, - request: params.request, - channel: "whatsapp", - accountId: params.accountId, - }) - ) { - return false; - } - return hasWhatsAppOriginOrSessionTarget({ - cfg: params.cfg, - accountId: params.accountId, - request: params.request, - }); -} - -function isWhatsAppExplicitTargetEligible(params: { - cfg: OpenClawConfig; - accountId?: string | null; - approvalKind: ApprovalKind; - request: ApprovalRequest; - target: ChannelApprovalForwardTarget; -}): boolean { - if (!isWhatsAppApprovalTransportEnabled(params)) { - return false; - } - const config = resolveApprovalForwardingConfig(params); - if (!config?.enabled) { - return false; - } - const mode = normalizeApprovalForwardingMode(config.mode); - if (!approvalModeIncludesTargets(mode)) { - return false; - } - if (!matchesForwardingFilters({ config, request: params.request })) { - return false; - } - return hasMatchingWhatsAppTarget({ - cfg: params.cfg, - config, - accountId: params.accountId, - target: params.target, - }); -} +const canApprovalPotentiallyRouteToWhatsApp = whatsappApprovalForwarding.isPotentialRoute; +const canAnyApprovalPotentiallyRouteToWhatsApp = whatsappApprovalForwarding.canAnyPotentiallyRoute; +const isWhatsAppSessionApprovalEligible = whatsappApprovalForwarding.isSessionEligible; +const isWhatsAppExplicitTargetEligible = whatsappApprovalForwarding.isExplicitTargetEligible; function resolveTurnSourceWhatsAppOriginTarget( request: ApprovalRequest, @@ -344,10 +197,7 @@ function shouldHandleWhatsAppApprovalRequest(params: { approvalKind?: ApprovalKind; request: ApprovalRequest; }): boolean { - return isWhatsAppSessionApprovalEligible({ - ...params, - approvalKind: resolveApprovalKind(params.request, params.approvalKind), - }); + return whatsappApprovalForwarding.shouldHandleRequest(params); } const resolveWhatsAppOriginTargetBase = createChannelNativeOriginTargetResolver({ diff --git a/src/plugin-sdk/approval-handler-runtime.ts b/src/plugin-sdk/approval-handler-runtime.ts index 8c3f8d08324..a47db3dcab8 100644 --- a/src/plugin-sdk/approval-handler-runtime.ts +++ b/src/plugin-sdk/approval-handler-runtime.ts @@ -29,3 +29,70 @@ export { type ResolvedApprovalView, } from "../infra/approval-handler-runtime.js"; export { resolveApprovalOverGateway } from "./approval-gateway-runtime.js"; +import type { + ExpiredApprovalView, + ResolvedApprovalView, +} from "../infra/approval-view-model.types.js"; +import type { ExecApprovalRequest, ExecApprovalResolved } from "../infra/exec-approvals.js"; +import { + buildPluginApprovalExpiredMessage, + buildPluginApprovalResolvedMessage, + type PluginApprovalRequest, + type PluginApprovalResolved, +} from "../infra/plugin-approvals.js"; +import { normalizeOptionalString } from "../shared/string-coerce.js"; +import { buildApprovalResolvedReplyPayload } from "./approval-renderers.js"; + +type ApprovalRequest = ExecApprovalRequest | PluginApprovalRequest; +type ApprovalResolved = ExecApprovalResolved | PluginApprovalResolved; + +export function buildChannelApprovalResolvedText(params: { + request: ApprovalRequest; + resolved: ApprovalResolved; + view: ResolvedApprovalView; +}): string { + if (params.view.approvalKind === "plugin") { + return buildPluginApprovalResolvedMessage(params.resolved as PluginApprovalResolved); + } + const resolvedByText = params.resolved.resolvedBy + ? ` Resolved by ${params.resolved.resolvedBy}.` + : ""; + const payload = buildApprovalResolvedReplyPayload({ + approvalId: params.request.id, + approvalSlug: params.request.id.slice(0, 8), + text: `✅ Exec approval ${params.resolved.decision}.${resolvedByText} ID: ${params.request.id}`, + }); + return payload.text ?? ""; +} + +export function buildChannelApprovalExpiredText(params: { + request: ApprovalRequest; + view: ExpiredApprovalView; +}): string { + if (params.view.approvalKind === "plugin") { + return buildPluginApprovalExpiredMessage(params.request as PluginApprovalRequest); + } + return `⏱️ Exec approval expired. ID: ${params.request.id}`; +} + +export function resolvePreparedApprovalAccountId(params: { + plannedAccountId?: string | null; + contextAccountId?: string | null; + fallbackAccountId: string; +}): string; +export function resolvePreparedApprovalAccountId(params: { + plannedAccountId?: string | null; + contextAccountId?: string | null; + fallbackAccountId?: string | null; +}): string | undefined; +export function resolvePreparedApprovalAccountId(params: { + plannedAccountId?: string | null; + contextAccountId?: string | null; + fallbackAccountId?: string | null; +}): string | undefined { + return ( + normalizeOptionalString(params.plannedAccountId) ?? + normalizeOptionalString(params.contextAccountId) ?? + normalizeOptionalString(params.fallbackAccountId) + ); +} diff --git a/src/plugin-sdk/approval-native-helpers.ts b/src/plugin-sdk/approval-native-helpers.ts index 245693f3ac0..8f2cba905c6 100644 --- a/src/plugin-sdk/approval-native-helpers.ts +++ b/src/plugin-sdk/approval-native-helpers.ts @@ -1,3 +1,8 @@ +import type { + ExecApprovalForwardingConfig, + ExecApprovalForwardingMode, +} from "../config/types.approvals.js"; +import { doesApprovalRequestMatchChannelAccount } from "../infra/approval-request-account-binding.js"; import { matchesApprovalRequestFilters } from "../infra/approval-request-filters.js"; import { getExecApprovalReplyMetadata, @@ -26,6 +31,7 @@ type LocalNativeExecApprovalConfig = { agentFilter?: string[]; sessionFilter?: string[]; }; +type ChannelApprovalForwardTarget = DeliverySuppressionInput["target"]; type ApprovalResolverParams = { cfg: OpenClawConfig; @@ -34,6 +40,41 @@ type ApprovalResolverParams = { request: ApprovalRequest; }; +type ChannelApprovalForwardingEvaluatorParams = { + channel: string; + isTransportEnabled: (params: { cfg: OpenClawConfig; accountId?: string | null }) => boolean; + hasMatchingTarget: (params: { + cfg: OpenClawConfig; + config: ExecApprovalForwardingConfig; + accountId?: string | null; + target?: ChannelApprovalForwardTarget; + }) => boolean; + hasOriginOrSessionTarget: (params: { + cfg: OpenClawConfig; + accountId?: string | null; + request: ApprovalRequest; + }) => boolean; +}; + +export type ChannelApprovalForwardingEligibilityParams = { + cfg: OpenClawConfig; + accountId?: string | null; + approvalKind: ApprovalKind; + request: ApprovalRequest; +}; + +export type ChannelApprovalPotentialRouteParams = { + cfg: OpenClawConfig; + accountId?: string | null; + approvalKind: ApprovalKind; + nativeSessionOnly?: boolean; +}; + +export type ChannelApprovalExplicitTargetEligibilityParams = + ChannelApprovalForwardingEligibilityParams & { + target: ChannelApprovalForwardTarget; + }; + type NativeApprovalTargetNormalizer = ( target: TTarget, request: ApprovalRequest, @@ -230,13 +271,170 @@ function nativeApprovalTargetMatcher(channel: string): (left: unknown, right: un nativeApprovalTargetsMatch({ channel, left, right }); } -function resolveApprovalKind(request: ApprovalRequest, approvalKind?: ApprovalKind): ApprovalKind { +export function resolveApprovalKind( + request: ApprovalRequest, + approvalKind?: ApprovalKind, +): ApprovalKind { if (approvalKind) { return approvalKind; } return "command" in request.request ? "exec" : "plugin"; } +function resolveApprovalForwardingConfig(params: { + cfg: OpenClawConfig; + approvalKind: ApprovalKind; +}): ExecApprovalForwardingConfig | undefined { + return params.approvalKind === "plugin" + ? params.cfg.approvals?.plugin + : params.cfg.approvals?.exec; +} + +function normalizeApprovalForwardingMode( + mode: ExecApprovalForwardingConfig["mode"] | undefined, +): ExecApprovalForwardingMode { + return mode ?? "session"; +} + +function approvalModeIncludesSession(mode: ExecApprovalForwardingMode): boolean { + return mode === "session" || mode === "both"; +} + +function approvalModeIncludesTargets(mode: ExecApprovalForwardingMode): boolean { + return mode === "targets" || mode === "both"; +} + +function matchesForwardingFilters(params: { + config: ExecApprovalForwardingConfig; + request: ApprovalRequest; +}): boolean { + return matchesApprovalRequestFilters({ + request: params.request.request, + agentFilter: params.config.agentFilter, + sessionFilter: params.config.sessionFilter, + fallbackAgentIdFromSessionKey: true, + }); +} + +export function createChannelApprovalForwardingEvaluator( + params: ChannelApprovalForwardingEvaluatorParams, +) { + const isPotentialRoute = (input: ChannelApprovalPotentialRouteParams): boolean => { + if (!params.isTransportEnabled(input)) { + return false; + } + const config = resolveApprovalForwardingConfig(input); + if (!config?.enabled) { + return false; + } + const mode = normalizeApprovalForwardingMode(config.mode); + if (approvalModeIncludesSession(mode)) { + return true; + } + if (input.nativeSessionOnly) { + return false; + } + return ( + approvalModeIncludesTargets(mode) && + params.hasMatchingTarget({ + cfg: input.cfg, + config, + accountId: input.accountId, + }) + ); + }; + + const isSessionEligible = (input: ChannelApprovalForwardingEligibilityParams): boolean => { + if (!params.isTransportEnabled(input)) { + return false; + } + const config = resolveApprovalForwardingConfig(input); + if (!config?.enabled) { + return false; + } + const mode = normalizeApprovalForwardingMode(config.mode); + if (!approvalModeIncludesSession(mode)) { + return false; + } + if (!matchesForwardingFilters({ config, request: input.request })) { + return false; + } + if ( + !doesApprovalRequestMatchChannelAccount({ + cfg: input.cfg, + request: input.request, + channel: params.channel, + accountId: input.accountId, + }) + ) { + return false; + } + return params.hasOriginOrSessionTarget({ + cfg: input.cfg, + accountId: input.accountId, + request: input.request, + }); + }; + + const isExplicitTargetEligible = ( + input: ChannelApprovalExplicitTargetEligibilityParams, + ): boolean => { + if (!params.isTransportEnabled(input)) { + return false; + } + const config = resolveApprovalForwardingConfig(input); + if (!config?.enabled) { + return false; + } + const mode = normalizeApprovalForwardingMode(config.mode); + if (!approvalModeIncludesTargets(mode)) { + return false; + } + if (!matchesForwardingFilters({ config, request: input.request })) { + return false; + } + return params.hasMatchingTarget({ + cfg: input.cfg, + config, + accountId: input.accountId, + target: input.target, + }); + }; + + const canAnyPotentiallyRoute = (input: { + cfg: OpenClawConfig; + accountId?: string | null; + nativeSessionOnly?: boolean; + }): boolean => + isPotentialRoute({ + ...input, + approvalKind: "exec", + }) || + isPotentialRoute({ + ...input, + approvalKind: "plugin", + }); + + const shouldHandleRequest = (input: { + cfg: OpenClawConfig; + accountId?: string | null; + approvalKind?: ApprovalKind; + request: ApprovalRequest; + }): boolean => + isSessionEligible({ + ...input, + approvalKind: resolveApprovalKind(input.request, input.approvalKind), + }); + + return { + canAnyPotentiallyRoute, + isExplicitTargetEligible, + isPotentialRoute, + isSessionEligible, + shouldHandleRequest, + }; +} + function normalizeOptionalAccountId(value?: string | null): string | undefined { return value?.trim() || undefined; } diff --git a/src/plugin-sdk/approval-native-runtime.ts b/src/plugin-sdk/approval-native-runtime.ts index 4b9b5fca017..940d8d5ce78 100644 --- a/src/plugin-sdk/approval-native-runtime.ts +++ b/src/plugin-sdk/approval-native-runtime.ts @@ -1,9 +1,14 @@ export { + createChannelApprovalForwardingEvaluator, createChannelApproverDmTargetResolver, createChannelNativeOriginTargetResolver, createNativeApprovalForwardingFallbackSuppressor, nativeApprovalTargetsMatch, + resolveApprovalKind, shouldSuppressLocalNativeExecApprovalPrompt, + type ChannelApprovalExplicitTargetEligibilityParams, + type ChannelApprovalForwardingEligibilityParams, + type ChannelApprovalPotentialRouteParams, } from "./approval-native-helpers.js"; export { resolveApprovalRequestSessionConversation,