UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

227 lines (226 loc) 8.64 kB
import "./src-vebZIeLe.js"; import { t as expectDefined } from "./expect-CyE8FADM.js"; import { o as normalizeLowercaseStringOrEmpty } from "./string-coerce-CIXf7egm.js"; import { t as formatErrorMessage } from "./errors-Db3Ymjlb.js"; import { r as logVerbose } from "./globals-CTaGxEqj.js"; import { n as normalizeMessageChannel } from "./message-channel-core-AaEWic-A.js"; import "./message-channel-BQrhwUEA.js"; import { t as getChannelPlugin } from "./registry-Cz6cv4VC.js"; import { n as resolveChannelApprovalCapability } from "./plugins-Cozj1Enf.js"; import { t as isApprovalNotFoundError } from "./approval-errors-Bzw_-cAg.js"; import { l as requireGatewayClientScope } from "./command-gates-B4tlStPI.js"; import { r as isImplicitSameChatApprovalAuthorization } from "./approval-auth-helpers-D5WyQ1Bo.js"; import { t as resolveApprovalOverGateway } from "./approval-gateway-resolver-PqvGdq98.js"; import { t as resolveChannelAccountId } from "./channel-context-DRwyIHwz.js"; //#region src/infra/channel-approval-auth.ts /** Resolves whether a chat `/approve` command is authorized by channel-specific approval policy. */ function resolveApprovalCommandAuthorization(params) { const channel = normalizeMessageChannel(params.channel); if (!channel) return { authorized: true, explicit: false }; const approvalCapability = resolveChannelApprovalCapability(getChannelPlugin(channel)); const resolved = approvalCapability?.authorizeActorAction?.({ cfg: params.cfg, accountId: params.accountId, senderId: params.senderId, action: "approve", approvalKind: params.kind }); if (!resolved) return { authorized: true, explicit: false }; const implicitSameChatAuthorization = isImplicitSameChatApprovalAuthorization(resolved); const availability = approvalCapability?.getActionAvailabilityState?.({ cfg: params.cfg, accountId: params.accountId, action: "approve", approvalKind: params.kind }); return { authorized: resolved.authorized, reason: resolved.reason, explicit: resolved.authorized ? !implicitSameChatAuthorization && availability?.kind !== "disabled" : true }; } //#endregion //#region src/auto-reply/reply/commands-approve.ts const COMMAND_REGEX = /^\/?approve(?:\s|$)/i; const FOREIGN_COMMAND_MENTION_REGEX = /^\/approve@([^\s]+)(?:\s|$)/i; const DECISION_ALIASES = { allow: "allow-once", once: "allow-once", "allow-once": "allow-once", allowonce: "allow-once", always: "allow-always", "allow-always": "allow-always", allowalways: "allow-always", deny: "deny", reject: "deny", block: "deny" }; const APPROVE_USAGE_TEXT = "Usage: /approve <id> <decision> (see the pending approval message for available decisions)"; function parseApproveCommand(raw) { const trimmed = raw.trim(); if (FOREIGN_COMMAND_MENTION_REGEX.test(trimmed)) return { ok: false, error: "❌ This /approve command targets a different Telegram bot." }; const commandMatch = trimmed.match(COMMAND_REGEX); if (!commandMatch) return null; const rest = trimmed.slice(commandMatch[0].length).trim(); if (!rest) return { ok: false, error: APPROVE_USAGE_TEXT }; const tokens = rest.split(/\s+/).filter(Boolean); if (tokens.length < 2) return { ok: false, error: APPROVE_USAGE_TEXT }; const first = normalizeLowercaseStringOrEmpty(tokens[0]); const second = normalizeLowercaseStringOrEmpty(tokens[1]); if (DECISION_ALIASES[first]) return { ok: true, decision: DECISION_ALIASES[first], id: tokens.slice(1).join(" ").trim() }; if (DECISION_ALIASES[second]) return { ok: true, decision: DECISION_ALIASES[second], id: expectDefined(tokens[0], "tokens entry at 0") }; return { ok: false, error: APPROVE_USAGE_TEXT }; } function buildResolvedByLabel(params) { return `${params.command.channel}:${params.command.senderId ?? "unknown"}`; } function formatApprovalSubmitError(error) { return formatErrorMessage(error); } function resolveAuthorizedApprovalKinds(params) { return [...params.execAuthorization.authorized ? ["exec"] : [], ...params.pluginAuthorization.authorized ? ["plugin"] : []]; } function resolveApprovalAuthorizationError(params) { return params.execAuthorization.reason ?? params.pluginAuthorization.reason ?? "❌ You are not authorized to approve this request."; } async function handleApproveCommandFromContext(params, allowTextCommands) { if (!allowTextCommands) return null; const normalized = params.command.commandBodyNormalized; const parsed = parseApproveCommand(normalized); if (!parsed) return null; if (!parsed.ok) return { shouldContinue: false, reply: { text: parsed.error } }; const effectiveAccountId = resolveChannelAccountId({ cfg: params.cfg, ctx: params.ctx, command: params.command }); const execApprovalAuthorization = resolveApprovalCommandAuthorization({ cfg: params.cfg, channel: params.command.channel, accountId: effectiveAccountId, senderId: params.command.senderId, kind: "exec" }); const pluginApprovalAuthorization = resolveApprovalCommandAuthorization({ cfg: params.cfg, channel: params.command.channel, accountId: effectiveAccountId, senderId: params.command.senderId, kind: "plugin" }); const hasExplicitApprovalAuthorization = execApprovalAuthorization.explicit && execApprovalAuthorization.authorized || pluginApprovalAuthorization.explicit && pluginApprovalAuthorization.authorized; if (!params.command.isAuthorizedSender && !hasExplicitApprovalAuthorization) { logVerbose(`Ignoring /approve from unauthorized sender: ${params.command.senderId || "<unknown>"}`); return { shouldContinue: false }; } const missingScope = requireGatewayClientScope(params, { label: "/approve", allowedScopes: ["operator.approvals", "operator.admin"], missingText: "❌ /approve requires operator.approvals for gateway clients." }); if (missingScope) return missingScope; const approvalCapability = resolveChannelApprovalCapability(getChannelPlugin(params.command.channel)); const commandBehaviors = /* @__PURE__ */ new Map(); for (const approvalKind of ["exec", "plugin"]) commandBehaviors.set(approvalKind, approvalCapability?.resolveApproveCommandBehavior?.({ cfg: params.cfg, accountId: effectiveAccountId, senderId: params.command.senderId, approvalKind })); const blockedCommandResult = () => { const replyBehavior = Array.from(commandBehaviors.values()).find((behavior) => behavior?.kind === "reply"); if (replyBehavior?.kind === "reply") return { shouldContinue: false, reply: { text: replyBehavior.text } }; if (Array.from(commandBehaviors.values()).some((behavior) => behavior?.kind === "ignore")) return { shouldContinue: false }; return null; }; const resolvedBy = buildResolvedByLabel(params); const callApprovalMethod = async (resolveMethod) => { await resolveApprovalOverGateway({ cfg: params.cfg, approvalId: parsed.id, decision: parsed.decision, ...approvalCapability?.authorizeActorAction ? { channel: params.command.channel, accountId: effectiveAccountId, senderId: params.command.senderId } : {}, resolveMethod, clientDisplayName: `Chat approval (${resolvedBy})` }); }; const methods = resolveAuthorizedApprovalKinds({ execAuthorization: execApprovalAuthorization, pluginAuthorization: pluginApprovalAuthorization }).filter((approvalKind) => { const behavior = commandBehaviors.get(approvalKind); return !behavior || behavior.kind === "allow"; }); if (methods.length === 0) { const blocked = blockedCommandResult(); if (blocked) return blocked; return { shouldContinue: false, reply: { text: resolveApprovalAuthorizationError({ execAuthorization: execApprovalAuthorization, pluginAuthorization: pluginApprovalAuthorization }) } }; } for (const [index, method] of methods.entries()) try { await callApprovalMethod(method); break; } catch (error) { const isLastMethod = index === methods.length - 1; if (!isApprovalNotFoundError(error)) return { shouldContinue: false, reply: { text: `❌ Failed to submit approval: ${formatApprovalSubmitError(error)}` } }; if (isLastMethod) { const blocked = blockedCommandResult(); if (blocked) return blocked; return { shouldContinue: false, reply: { text: `❌ Failed to submit approval: ${formatApprovalSubmitError(error)}` } }; } } return { shouldContinue: false, reply: { text: `✅ Approval ${parsed.decision} submitted for ${parsed.id}.` } }; } const handleApproveCommand = async (params, allowTextCommands) => await handleApproveCommandFromContext(params, allowTextCommands); //#endregion export { handleApproveCommandFromContext as n, handleApproveCommand as t };