mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-28 13:26:04 -06:00
99a02bf115
* feat(approvals): typed approval scope summaries on channel cards Approval owners can attach a closed ApprovalScope union (message-send, payment, external-post) describing an action's blast radius. The gateway sanitizes it once at the producer boundary, the core view model renders a Scope metadata row so Slack/Discord/Google Chat cards show it unchanged, shared text builders cover Telegram/WhatsApp/Signal/iMessage/Matrix, and the durable presentation carries it additively for operator surfaces. Scope is display-only, never authorization; missing scope keeps today's cards. * fix(approvals): emit native ApprovalScope union and clamp recipient previews Name the three scope variants as registered protocol schemas so the Swift generator emits the ApprovalScope discriminated union the presentation structs reference, and commit the regenerated GatewayModels.swift. Clamp recipient previews to the declared recipientCount at the sanitize boundary so a count of 1 with 2 previews can no longer render inconsistently. Addresses both ClawSweeper findings on #130116. * refactor(approvals): extract text sanitizer to break the exec-approvals import cycle check:architecture flagged approval-scope joining the exec-approvals SCC through exec-approval-command-display. Move the self-contained display sanitizer into a leaf module (exec-approval-text-sanitize) with no exec-approvals imports and migrate all sanitize importers; command-display keeps only the payload-typed command/preview resolver. * chore(plugin-sdk): ratchet public surface budgets down after sanitizer extraction The approval display sanitizers left the publicly reachable SDK graph when they moved to the exec-approval-text-sanitize leaf: exports 4343 -> 4338, callable exports 2582 -> 2578. Shrink-only budget pin.
234 lines
8.1 KiB
TypeScript
234 lines
8.1 KiB
TypeScript
// Telegram plugin module implements approval handler behavior.
|
|
import type {
|
|
ChannelApprovalCapabilityHandlerContext,
|
|
ChannelApprovalKind,
|
|
PendingApprovalView,
|
|
} from "openclaw/plugin-sdk/approval-handler-runtime";
|
|
import { createChannelApprovalNativeRuntimeAdapter } from "openclaw/plugin-sdk/approval-handler-runtime";
|
|
import { buildChannelApprovalNativeTargetKey } from "openclaw/plugin-sdk/approval-native-runtime";
|
|
import {
|
|
buildPluginApprovalPendingReplyPayload,
|
|
buildApprovalPresentationFromActionDescriptors,
|
|
buildExecApprovalPendingReplyPayload,
|
|
} from "openclaw/plugin-sdk/approval-reply-runtime";
|
|
import type { ExecApprovalPendingReplyParams } from "openclaw/plugin-sdk/approval-reply-runtime";
|
|
import type {
|
|
ExecApprovalRequest,
|
|
PluginApprovalRequest,
|
|
} from "openclaw/plugin-sdk/approval-runtime";
|
|
import { createSubsystemLogger } from "openclaw/plugin-sdk/runtime-env";
|
|
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
import {
|
|
buildTelegramNativeExpiredApprovalText,
|
|
buildTelegramNativeResolvedApprovalText,
|
|
} from "./approval-terminal.js";
|
|
import { resolveTelegramInlineButtons } from "./button-types.js";
|
|
import {
|
|
isTelegramExecApprovalHandlerConfigured,
|
|
shouldHandleTelegramExecApprovalRequest,
|
|
} from "./exec-approvals.js";
|
|
import { escapeTelegramHtml } from "./format.js";
|
|
import {
|
|
editMessageReplyMarkupTelegram,
|
|
editMessageTelegram,
|
|
sendMessageTelegram,
|
|
sendTypingTelegram,
|
|
} from "./send.js";
|
|
|
|
const log = createSubsystemLogger("telegram/approvals");
|
|
|
|
type ApprovalRequest = ExecApprovalRequest | PluginApprovalRequest;
|
|
type PendingMessage = {
|
|
chatId: string;
|
|
messageId: string;
|
|
};
|
|
type TelegramPendingDelivery = {
|
|
text: string;
|
|
buttons: ReturnType<typeof resolveTelegramInlineButtons>;
|
|
};
|
|
type TelegramFinalDelivery = {
|
|
text: string;
|
|
};
|
|
|
|
type TelegramExecApprovalHandlerDeps = {
|
|
nowMs?: () => number;
|
|
sendTyping?: typeof sendTypingTelegram;
|
|
sendMessage?: typeof sendMessageTelegram;
|
|
editMessage?: typeof editMessageTelegram;
|
|
editReplyMarkup?: typeof editMessageReplyMarkupTelegram;
|
|
};
|
|
|
|
type TelegramApprovalHandlerContext = {
|
|
token: string;
|
|
deps?: TelegramExecApprovalHandlerDeps;
|
|
};
|
|
|
|
function resolveHandlerContext(params: ChannelApprovalCapabilityHandlerContext): {
|
|
accountId: string;
|
|
context: TelegramApprovalHandlerContext;
|
|
} | null {
|
|
const context = params.context as TelegramApprovalHandlerContext | undefined;
|
|
const accountId = normalizeOptionalString(params.accountId) ?? "";
|
|
if (!context?.token || !accountId) {
|
|
return null;
|
|
}
|
|
return { accountId, context };
|
|
}
|
|
|
|
function buildPendingPayload(params: {
|
|
request: ApprovalRequest;
|
|
approvalKind: ChannelApprovalKind;
|
|
nowMs: number;
|
|
view: PendingApprovalView;
|
|
}): TelegramPendingDelivery {
|
|
const payload =
|
|
params.approvalKind === "plugin"
|
|
? buildPluginApprovalPendingReplyPayload({
|
|
request: params.request as PluginApprovalRequest,
|
|
nowMs: params.nowMs,
|
|
})
|
|
: buildExecApprovalPendingReplyPayload({
|
|
approvalId: params.request.id,
|
|
approvalSlug: params.request.id.slice(0, 8),
|
|
approvalCommandId: params.request.id,
|
|
warningText:
|
|
params.view.approvalKind === "exec"
|
|
? (params.view.warningText ?? undefined)
|
|
: undefined,
|
|
command: params.view.approvalKind === "exec" ? params.view.commandText : "",
|
|
cwd: params.view.approvalKind === "exec" ? (params.view.cwd ?? undefined) : undefined,
|
|
host:
|
|
params.view.approvalKind === "exec" && params.view.host === "node" ? "node" : "gateway",
|
|
nodeId:
|
|
params.view.approvalKind === "exec" ? (params.view.nodeId ?? undefined) : undefined,
|
|
scope: params.view.approvalKind === "exec" ? (params.view.scope ?? undefined) : undefined,
|
|
allowedDecisions: params.view.actions.map((action) => action.decision),
|
|
expiresAtMs: params.request.expiresAtMs,
|
|
nowMs: params.nowMs,
|
|
} satisfies ExecApprovalPendingReplyParams);
|
|
return {
|
|
text: payload.text ?? "",
|
|
buttons: resolveTelegramInlineButtons({
|
|
presentation: buildApprovalPresentationFromActionDescriptors(params.view.actions),
|
|
}),
|
|
};
|
|
}
|
|
|
|
export const telegramApprovalNativeRuntime = createChannelApprovalNativeRuntimeAdapter<
|
|
TelegramPendingDelivery,
|
|
{ chatId: string; messageThreadId?: number },
|
|
PendingMessage,
|
|
never,
|
|
TelegramFinalDelivery
|
|
>({
|
|
eventKinds: ["exec", "plugin"],
|
|
availability: {
|
|
isConfigured: (params) => {
|
|
const resolved = resolveHandlerContext(params);
|
|
return resolved
|
|
? isTelegramExecApprovalHandlerConfigured({
|
|
cfg: params.cfg,
|
|
accountId: resolved.accountId,
|
|
})
|
|
: false;
|
|
},
|
|
shouldHandle: (params) => {
|
|
const resolved = resolveHandlerContext(params);
|
|
return resolved
|
|
? shouldHandleTelegramExecApprovalRequest({
|
|
cfg: params.cfg,
|
|
accountId: resolved.accountId,
|
|
request: params.request,
|
|
})
|
|
: false;
|
|
},
|
|
},
|
|
presentation: {
|
|
buildPendingPayload: ({ request, approvalKind, nowMs, view }) =>
|
|
buildPendingPayload({ request, approvalKind, nowMs, view }),
|
|
buildResolvedResult: ({ view }) => ({
|
|
kind: "update",
|
|
payload: { text: buildTelegramNativeResolvedApprovalText(view) },
|
|
}),
|
|
buildExpiredResult: ({ view }) => ({
|
|
kind: "update",
|
|
payload: { text: buildTelegramNativeExpiredApprovalText(view) },
|
|
}),
|
|
},
|
|
transport: {
|
|
prepareTarget: ({ plannedTarget }) => ({
|
|
dedupeKey: buildChannelApprovalNativeTargetKey(plannedTarget.target),
|
|
target: {
|
|
chatId: plannedTarget.target.to,
|
|
messageThreadId:
|
|
typeof plannedTarget.target.threadId === "number"
|
|
? plannedTarget.target.threadId
|
|
: undefined,
|
|
},
|
|
}),
|
|
deliverPending: async ({ cfg, accountId, context, preparedTarget, pendingPayload }) => {
|
|
const resolved = resolveHandlerContext({ cfg, accountId, context });
|
|
if (!resolved) {
|
|
return null;
|
|
}
|
|
const sendTyping = resolved.context.deps?.sendTyping ?? sendTypingTelegram;
|
|
const sendMessage = resolved.context.deps?.sendMessage ?? sendMessageTelegram;
|
|
await sendTyping(preparedTarget.chatId, {
|
|
cfg,
|
|
token: resolved.context.token,
|
|
accountId: resolved.accountId,
|
|
...(preparedTarget.messageThreadId != null
|
|
? { messageThreadId: preparedTarget.messageThreadId }
|
|
: {}),
|
|
}).catch(() => {});
|
|
const result = await sendMessage(preparedTarget.chatId, pendingPayload.text, {
|
|
cfg,
|
|
token: resolved.context.token,
|
|
accountId: resolved.accountId,
|
|
buttons: pendingPayload.buttons,
|
|
...(preparedTarget.messageThreadId != null
|
|
? { messageThreadId: preparedTarget.messageThreadId }
|
|
: {}),
|
|
});
|
|
return {
|
|
chatId: result.chatId,
|
|
messageId: result.messageId,
|
|
};
|
|
},
|
|
updateEntry: async ({ cfg, accountId, context, entry, payload }) => {
|
|
const resolved = resolveHandlerContext({ cfg, accountId, context });
|
|
if (!resolved) {
|
|
return;
|
|
}
|
|
const editMessage = resolved.context.deps?.editMessage ?? editMessageTelegram;
|
|
await editMessage(entry.chatId, entry.messageId, escapeTelegramHtml(payload.text), {
|
|
cfg,
|
|
token: resolved.context.token,
|
|
accountId: resolved.accountId,
|
|
textMode: "html",
|
|
buttons: [],
|
|
});
|
|
},
|
|
},
|
|
interactions: {
|
|
clearPendingActions: async ({ cfg, accountId, context, entry }) => {
|
|
const resolved = resolveHandlerContext({ cfg, accountId, context });
|
|
if (!resolved) {
|
|
return;
|
|
}
|
|
const editReplyMarkup =
|
|
resolved.context.deps?.editReplyMarkup ?? editMessageReplyMarkupTelegram;
|
|
await editReplyMarkup(entry.chatId, entry.messageId, [], {
|
|
cfg,
|
|
token: resolved.context.token,
|
|
accountId: resolved.accountId,
|
|
});
|
|
},
|
|
},
|
|
observe: {
|
|
onDeliveryError: ({ error, request }) => {
|
|
log.error(`telegram approvals: failed to send request ${request.id}: ${String(error)}`);
|
|
},
|
|
},
|
|
});
|