Files
openclaw/src/plugin-sdk/approval-delivery-helpers.ts
Gustavo Madeira Santana 0ef9383487 fix(approvals): make exec approval fallback guidance channel-specific (#61424)
Merged via squash.

Prepared head SHA: cb5d3c249c
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
2026-04-05 14:26:46 -04:00

218 lines
8.6 KiB
TypeScript

import type { ExecApprovalRequest } from "../infra/exec-approvals.js";
import type { PluginApprovalRequest } from "../infra/plugin-approvals.js";
import type { ChannelApprovalCapability } from "./channel-contract.js";
import type { OpenClawConfig } from "./config-runtime.js";
import { normalizeMessageChannel } from "./routing.js";
type ApprovalKind = "exec" | "plugin";
type NativeApprovalDeliveryMode = "dm" | "channel" | "both";
type NativeApprovalRequest = ExecApprovalRequest | PluginApprovalRequest;
type NativeApprovalTarget = { to: string; threadId?: string | number | null };
type NativeApprovalSurface = "origin" | "approver-dm";
type ApprovalAdapterParams = {
cfg: OpenClawConfig;
accountId?: string | null;
senderId?: string | null;
};
type DeliverySuppressionParams = {
cfg: OpenClawConfig;
approvalKind: ApprovalKind;
target: { channel: string; accountId?: string | null };
request: { request: { turnSourceChannel?: string | null; turnSourceAccountId?: string | null } };
};
type ApproverRestrictedNativeApprovalParams = {
channel: string;
channelLabel: string;
listAccountIds: (cfg: OpenClawConfig) => string[];
hasApprovers: (params: ApprovalAdapterParams) => boolean;
isExecAuthorizedSender: (params: ApprovalAdapterParams) => boolean;
isPluginAuthorizedSender?: (params: ApprovalAdapterParams) => boolean;
isNativeDeliveryEnabled: (params: { cfg: OpenClawConfig; accountId?: string | null }) => boolean;
resolveNativeDeliveryMode: (params: {
cfg: OpenClawConfig;
accountId?: string | null;
}) => NativeApprovalDeliveryMode;
requireMatchingTurnSourceChannel?: boolean;
resolveSuppressionAccountId?: (params: DeliverySuppressionParams) => string | undefined;
resolveOriginTarget?: (params: {
cfg: OpenClawConfig;
accountId?: string | null;
approvalKind: ApprovalKind;
request: NativeApprovalRequest;
}) => NativeApprovalTarget | null | Promise<NativeApprovalTarget | null>;
resolveApproverDmTargets?: (params: {
cfg: OpenClawConfig;
accountId?: string | null;
approvalKind: ApprovalKind;
request: NativeApprovalRequest;
}) => NativeApprovalTarget[] | Promise<NativeApprovalTarget[]>;
notifyOriginWhenDmOnly?: boolean;
describeExecApprovalSetup?: ChannelApprovalCapability["describeExecApprovalSetup"];
};
function buildApproverRestrictedNativeApprovalCapability(
params: ApproverRestrictedNativeApprovalParams,
): ChannelApprovalCapability {
const pluginSenderAuth = params.isPluginAuthorizedSender ?? params.isExecAuthorizedSender;
const normalizePreferredSurface = (
mode: NativeApprovalDeliveryMode,
): NativeApprovalSurface | "both" =>
mode === "channel" ? "origin" : mode === "dm" ? "approver-dm" : "both";
return createChannelApprovalCapability({
authorizeActorAction: ({
cfg,
accountId,
senderId,
approvalKind,
}: {
cfg: OpenClawConfig;
accountId?: string | null;
senderId?: string | null;
action: "approve";
approvalKind: ApprovalKind;
}) => {
const authorized =
approvalKind === "plugin"
? pluginSenderAuth({ cfg, accountId, senderId })
: params.isExecAuthorizedSender({ cfg, accountId, senderId });
return authorized
? { authorized: true }
: {
authorized: false,
reason: `❌ You are not authorized to approve ${approvalKind} requests on ${params.channelLabel}.`,
};
},
getActionAvailabilityState: ({
cfg,
accountId,
}: {
cfg: OpenClawConfig;
accountId?: string | null;
action: "approve";
}) =>
params.hasApprovers({ cfg, accountId })
? ({ kind: "enabled" } as const)
: ({ kind: "disabled" } as const),
describeExecApprovalSetup: params.describeExecApprovalSetup,
approvals: {
delivery: {
hasConfiguredDmRoute: ({ cfg }: { cfg: OpenClawConfig }) =>
params.listAccountIds(cfg).some((accountId) => {
if (!params.hasApprovers({ cfg, accountId })) {
return false;
}
if (!params.isNativeDeliveryEnabled({ cfg, accountId })) {
return false;
}
const target = params.resolveNativeDeliveryMode({ cfg, accountId });
return target === "dm" || target === "both";
}),
shouldSuppressForwardingFallback: (input: DeliverySuppressionParams) => {
const channel = normalizeMessageChannel(input.target.channel) ?? input.target.channel;
if (channel !== params.channel) {
return false;
}
if (params.requireMatchingTurnSourceChannel) {
const turnSourceChannel = normalizeMessageChannel(
input.request.request.turnSourceChannel,
);
if (turnSourceChannel !== params.channel) {
return false;
}
}
const resolvedAccountId = params.resolveSuppressionAccountId?.(input);
const accountId =
(resolvedAccountId === undefined
? input.target.accountId?.trim()
: resolvedAccountId.trim()) || undefined;
return params.isNativeDeliveryEnabled({ cfg: input.cfg, accountId });
},
},
native:
params.resolveOriginTarget || params.resolveApproverDmTargets
? {
describeDeliveryCapabilities: ({
cfg,
accountId,
}: {
cfg: OpenClawConfig;
accountId?: string | null;
approvalKind: ApprovalKind;
request: NativeApprovalRequest;
}) => ({
enabled:
params.hasApprovers({ cfg, accountId }) &&
params.isNativeDeliveryEnabled({ cfg, accountId }),
preferredSurface: normalizePreferredSurface(
params.resolveNativeDeliveryMode({ cfg, accountId }),
),
supportsOriginSurface: Boolean(params.resolveOriginTarget),
supportsApproverDmSurface: Boolean(params.resolveApproverDmTargets),
notifyOriginWhenDmOnly: params.notifyOriginWhenDmOnly ?? false,
}),
resolveOriginTarget: params.resolveOriginTarget,
resolveApproverDmTargets: params.resolveApproverDmTargets,
}
: undefined,
},
});
}
export function createApproverRestrictedNativeApprovalAdapter(
params: ApproverRestrictedNativeApprovalParams,
) {
return splitChannelApprovalCapability(buildApproverRestrictedNativeApprovalCapability(params));
}
export function createChannelApprovalCapability(params: {
authorizeActorAction?: ChannelApprovalCapability["authorizeActorAction"];
getActionAvailabilityState?: ChannelApprovalCapability["getActionAvailabilityState"];
resolveApproveCommandBehavior?: ChannelApprovalCapability["resolveApproveCommandBehavior"];
describeExecApprovalSetup?: ChannelApprovalCapability["describeExecApprovalSetup"];
approvals?: Pick<ChannelApprovalCapability, "delivery" | "render" | "native">;
}): ChannelApprovalCapability {
return {
authorizeActorAction: params.authorizeActorAction,
getActionAvailabilityState: params.getActionAvailabilityState,
resolveApproveCommandBehavior: params.resolveApproveCommandBehavior,
describeExecApprovalSetup: params.describeExecApprovalSetup,
delivery: params.approvals?.delivery,
render: params.approvals?.render,
native: params.approvals?.native,
};
}
export function splitChannelApprovalCapability(capability: ChannelApprovalCapability): {
auth: {
authorizeActorAction?: ChannelApprovalCapability["authorizeActorAction"];
getActionAvailabilityState?: ChannelApprovalCapability["getActionAvailabilityState"];
resolveApproveCommandBehavior?: ChannelApprovalCapability["resolveApproveCommandBehavior"];
};
delivery: ChannelApprovalCapability["delivery"];
render: ChannelApprovalCapability["render"];
native: ChannelApprovalCapability["native"];
describeExecApprovalSetup: ChannelApprovalCapability["describeExecApprovalSetup"];
} {
return {
auth: {
authorizeActorAction: capability.authorizeActorAction,
getActionAvailabilityState: capability.getActionAvailabilityState,
resolveApproveCommandBehavior: capability.resolveApproveCommandBehavior,
},
delivery: capability.delivery,
render: capability.render,
native: capability.native,
describeExecApprovalSetup: capability.describeExecApprovalSetup,
};
}
export function createApproverRestrictedNativeApprovalCapability(
params: ApproverRestrictedNativeApprovalParams,
): ChannelApprovalCapability {
return buildApproverRestrictedNativeApprovalCapability(params);
}