UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

388 lines (387 loc) 17.1 kB
import { l as normalizeOptionalString, o as normalizeLowercaseStringOrEmpty } from "./string-coerce-CIXf7egm.js"; import { v as sortUniqueStrings } from "./string-normalization-DsCfAx8q.js"; import { t as formatHumanList } from "./human-list-AC9kKekF.js"; import { t as buildChannelApprovalNativeTargetKey } from "./approval-native-target-key-uR5nEUEh.js"; //#region src/infra/approval-native-route-notice.ts /** Formats the human destination label for where native approval prompts were delivered. */ function describeApprovalDeliveryDestination(params) { const surfaces = new Set(params.deliveredTargets.map((target) => target.surface)); return surfaces.size === 1 && surfaces.has("approver-dm") ? `${params.channelLabel} DMs` : params.channelLabel; } /** Builds the notice shown in the current chat when approval was routed elsewhere. */ function resolveApprovalRoutedElsewhereNoticeText(destinations) { const uniqueDestinations = sortUniqueStrings(destinations.map((value) => value.trim())).filter(Boolean); if (uniqueDestinations.length === 0) return null; return `Approval required. I sent the approval request to ${formatHumanList(uniqueDestinations)}, not this chat.`; } /** Builds the recovery notice when no channel account uniquely owns the approval. */ function resolveAmbiguousApprovalRouteNoticeText() { return "Approval required, but multiple channel accounts can handle this request. Open the Control UI or terminal UI to approve it."; } /** Builds the fallback slash-command notice when native approval delivery fails. */ function resolveApprovalDeliveryFailedNoticeText(params) { return [ "Approval required. I could not deliver the native approval request.", `Reply with: /approve ${params.approvalKind === "exec" && params.approvalId.length > 8 ? params.approvalId.slice(0, 8) : params.approvalId} ${(params.allowedDecisions?.length ? params.allowedDecisions : [ "allow-once", "allow-always", "deny" ]).join("|")}`, "If the short code is ambiguous, use the full id in /approve." ].join("\n"); } //#endregion //#region src/infra/approval-native-route-coordinator.ts function createApprovalNativeRouteCoordinatorState() { return { activeRuntimes: /* @__PURE__ */ new Map(), pendingNotices: /* @__PURE__ */ new Map(), selections: /* @__PURE__ */ new Map(), runtimeSeq: 0, closed: false }; } function clearApprovalRouteSelection(state, approvalId) { const selection = state.selections.get(approvalId); if (!selection) return; state.selections.delete(approvalId); clearTimeout(selection.cleanupTimeout); } function routeGroupKey(runtime) { return normalizeChannel(runtime.channel) || runtime.runtimeId; } function createApprovalRouteSelection(state, params) { const runtimes = Array.from(state.activeRuntimes.values()).filter((runtime) => runtime.handledKinds.has(params.approvalKind)); const verdicts = /* @__PURE__ */ new Map(); const groups = /* @__PURE__ */ new Map(); for (const runtime of runtimes) { const key = routeGroupKey(runtime); groups.set(key, [...groups.get(key) ?? [], runtime]); } const selectedRuntimeIds = /* @__PURE__ */ new Set(); for (const group of groups.values()) { const candidates = []; for (const runtime of group) try { if (runtime.shouldHandle(params.request)) candidates.push(runtime); } catch (error) { verdicts.set(runtime.runtimeId, { kind: "selector-error", error }); } let routeClass; try { routeClass = group[0]?.classifyRoute(params.request) ?? "unbound"; } catch (error) { for (const runtime of group) verdicts.set(runtime.runtimeId, { kind: "selector-error", error }); continue; } if (routeClass === "bound-or-explicit") { if (candidates.length === 0) { for (const runtime of group) if (!verdicts.has(runtime.runtimeId)) verdicts.set(runtime.runtimeId, { kind: "owner-unavailable" }); continue; } for (const runtime of candidates) selectedRuntimeIds.add(runtime.runtimeId); } else if (routeClass === "unbound" && candidates.length === 1) { const [candidate] = candidates; if (candidate) selectedRuntimeIds.add(candidate.runtimeId); } else if (routeClass === "unbound" && candidates.length > 1) for (const runtime of candidates) verdicts.set(runtime.runtimeId, { kind: "ambiguous-owner" }); } for (const runtime of runtimes) if (selectedRuntimeIds.has(runtime.runtimeId)) verdicts.set(runtime.runtimeId, { kind: "selected" }); else if (!verdicts.has(runtime.runtimeId)) verdicts.set(runtime.runtimeId, { kind: "ineligible" }); const timeoutMs = Math.min(Math.max(0, params.request.expiresAtMs - Date.now()), 2147483647); const cleanupTimeout = setTimeout(() => { clearApprovalRouteSelection(state, params.request.id); }, timeoutMs); cleanupTimeout.unref?.(); const selection = { verdicts, cleanupTimeout }; state.selections.set(params.request.id, selection); return selection; } function resolveApprovalRouteSelection(state, params) { return state.selections.get(params.request.id) ?? createApprovalRouteSelection(state, params); } const defaultCoordinatorState = createApprovalNativeRouteCoordinatorState(); const MAX_APPROVAL_ROUTE_NOTICE_TTL_MS = 3e5; function normalizeChannel(value) { return normalizeLowercaseStringOrEmpty(value); } function clearPendingApprovalRouteNotice(state, approvalId) { const entry = state.pendingNotices.get(approvalId); if (!entry) return; state.pendingNotices.delete(approvalId); clearTimeout(entry.cleanupTimeout); } function createPendingApprovalRouteNotice(state, params) { const timeoutMs = Math.min(Math.max(0, params.request.expiresAtMs - Date.now()), MAX_APPROVAL_ROUTE_NOTICE_TTL_MS); const cleanupTimeout = setTimeout(() => { maybeFinalizeApprovalRouteNotice(state, params.request.id, { force: true }); }, timeoutMs); cleanupTimeout.unref?.(); return { request: params.request, approvalKind: params.approvalKind, reports: /* @__PURE__ */ new Map(), cleanupTimeout }; } function resolveRouteNoticeTargetFromRequest(request) { const channel = request.request.turnSourceChannel?.trim(); const to = request.request.turnSourceTo?.trim(); if (!channel || !to) return null; return { channel, to, accountId: request.request.turnSourceAccountId ?? void 0, threadId: request.request.turnSourceThreadId ?? void 0 }; } function resolveFallbackRouteNoticeTarget(report) { const channel = report.channel?.trim(); const to = report.deliveryPlan.originTarget?.to?.trim(); if (!channel || !to) return null; return { channel, to, accountId: report.accountId ?? void 0, threadId: report.deliveryPlan.originTarget?.threadId ?? void 0 }; } function didReportDeliverToOrigin(report, originAccountId) { const originTarget = report.deliveryPlan.originTarget; if (!originTarget) return false; const reportAccountId = normalizeOptionalString(report.accountId); if (originAccountId !== void 0 && reportAccountId !== void 0 && reportAccountId !== originAccountId) return false; const originKey = buildChannelApprovalNativeTargetKey(originTarget); return report.deliveredTargets.some((plannedTarget) => buildChannelApprovalNativeTargetKey(plannedTarget.target) === originKey); } function hasPlannedNativeTargets(report) { return report.deliveryPlan.targets.length > 0; } function readAllowedDecisionStrings(request) { const allowedDecisions = "allowedDecisions" in request.request ? request.request.allowedDecisions : void 0; if (!Array.isArray(allowedDecisions)) return; return allowedDecisions.filter((value) => typeof value === "string"); } function resolveApprovalRouteNotice(params) { const explicitTarget = resolveRouteNoticeTargetFromRequest(params.request); const originChannel = normalizeChannel(explicitTarget?.channel ?? params.request.request.turnSourceChannel); const fallbackTarget = params.reports.filter((report) => normalizeChannel(report.channel) === originChannel || !originChannel).map(resolveFallbackRouteNoticeTarget).find((target) => target !== null) ?? null; const target = explicitTarget ? { ...fallbackTarget, ...explicitTarget, accountId: explicitTarget.accountId ?? fallbackTarget?.accountId, threadId: explicitTarget.threadId ?? fallbackTarget?.threadId } : fallbackTarget; if (!target) return null; const originAccountId = normalizeOptionalString(target.accountId); const deliveredAnyTarget = params.reports.some((report) => report.deliveredTargets.length > 0); const ambiguousOwner = params.reports.some((report) => report.skipReason === "ambiguous-owner"); const requiresManualFallback = ambiguousOwner || params.reports.some((report) => report.skipReason === "owner-unavailable"); if (!deliveredAnyTarget && (params.reports.some(hasPlannedNativeTargets) || requiresManualFallback || params.missingSelectedRuntime)) { const requestGateway = params.reports.find((report) => params.state.activeRuntimes.has(report.runtimeId))?.requestGateway ?? params.reports[0]?.requestGateway ?? Array.from(params.state.activeRuntimes.values())[0]?.requestGateway; if (!requestGateway) return null; return { requestGateway, target, text: ambiguousOwner ? resolveAmbiguousApprovalRouteNoticeText() : resolveApprovalDeliveryFailedNoticeText({ approvalId: params.request.id, approvalKind: params.approvalKind, allowedDecisions: readAllowedDecisionStrings(params.request) }) }; } if (params.reports.some((report) => { if (originChannel && normalizeChannel(report.channel) !== originChannel) return false; return didReportDeliverToOrigin(report, originAccountId); })) return null; const text = resolveApprovalRoutedElsewhereNoticeText(params.reports.flatMap((report) => { if (!report.channelLabel || report.deliveredTargets.length === 0) return []; const reportChannel = normalizeChannel(report.channel); if (originChannel && reportChannel === originChannel && !report.deliveryPlan.notifyOriginWhenDmOnly) return []; const reportAccountId = normalizeOptionalString(report.accountId); if (originChannel && reportChannel === originChannel && originAccountId !== void 0 && reportAccountId !== void 0 && reportAccountId !== originAccountId) return []; return [describeApprovalDeliveryDestination({ channelLabel: report.channelLabel, deliveredTargets: report.deliveredTargets })]; })); if (!text) return null; const requestGateway = params.reports.find((report) => params.state.activeRuntimes.has(report.runtimeId))?.requestGateway ?? params.reports[0]?.requestGateway; if (!requestGateway) return null; return { requestGateway, target, text }; } /** Returns whether a native approval runtime is active for the requested channel/account scope. */ function hasActiveApprovalNativeRouteRuntime(params) { return hasActiveApprovalNativeRouteRuntimeForState(defaultCoordinatorState, params); } function hasActiveApprovalNativeRouteRuntimeForState(state, params) { const channel = normalizeChannel(params.channel); const accountId = normalizeOptionalString(params.accountId); const matchingRuntimes = Array.from(state.activeRuntimes.values()).filter((runtime) => { if (!runtime.handledKinds.has(params.approvalKind)) return false; if (channel && normalizeChannel(runtime.channel) !== channel) return false; const runtimeAccountId = normalizeOptionalString(runtime.accountId); return accountId === void 0 || runtimeAccountId === void 0 || runtimeAccountId === accountId; }); return accountId === void 0 ? matchingRuntimes.length === 1 : matchingRuntimes.length > 0; } async function maybeFinalizeApprovalRouteNotice(state, approvalId, options) { const entry = state.pendingNotices.get(approvalId); if (!entry) return; const selection = state.selections.get(approvalId); if (!selection) return; if (!options?.force) { for (const runtimeId of selection.verdicts.keys()) if (!entry.reports.has(runtimeId)) return; } const missingSelectedRuntime = Array.from(selection.verdicts).some(([runtimeId, verdict]) => verdict.kind === "selected" && !entry.reports.has(runtimeId)); if (!options?.force && missingSelectedRuntime) return; const reports = Array.from(entry.reports.values()); const notice = resolveApprovalRouteNotice({ state, approvalKind: entry.approvalKind, request: entry.request, reports, missingSelectedRuntime }); clearPendingApprovalRouteNotice(state, approvalId); if (!notice) return; try { await notice.requestGateway("send", { channel: notice.target.channel, to: notice.target.to, accountId: notice.target.accountId ?? void 0, threadId: notice.target.threadId ?? void 0, message: notice.text, idempotencyKey: `approval-route-notice:${approvalId}` }); } catch {} } /** Tracks native approval deliveries and sends origin-chat notices after all observed runtimes report. */ function createApprovalNativeRouteReporter(params) { return createApprovalNativeRouteReporterForState(defaultCoordinatorState, params); } function createApprovalNativeRouteReporterForState(state, params) { const runtimeId = `native-approval-route:${++state.runtimeSeq}`; let registered = false; const report = async (payload) => { if (state.closed || !registered || !params.handledKinds.has(payload.approvalKind)) return; if (!resolveApprovalRouteSelection(state, payload).verdicts.has(runtimeId)) return; const entry = state.pendingNotices.get(payload.request.id) ?? createPendingApprovalRouteNotice(state, { request: payload.request, approvalKind: payload.approvalKind }); entry.reports.set(runtimeId, { runtimeId, request: payload.request, channel: params.channel, channelLabel: params.channelLabel, accountId: params.accountId, deliveryPlan: payload.deliveryPlan, deliveredTargets: payload.deliveredTargets, requestGateway: params.requestGateway, skipReason: payload.skipReason }); state.pendingNotices.set(payload.request.id, entry); await maybeFinalizeApprovalRouteNotice(state, payload.request.id); }; return { selectRequest(payload) { if (state.closed || !params.handledKinds.has(payload.approvalKind)) return { kind: "ineligible" }; if (!registered) try { return params.shouldHandle(payload.request) ? { kind: "selected" } : { kind: "ineligible" }; } catch (error) { return { kind: "selector-error", error }; } const selection = resolveApprovalRouteSelection(state, payload); const entry = state.pendingNotices.get(payload.request.id) ?? createPendingApprovalRouteNotice(state, { request: payload.request, approvalKind: payload.approvalKind }); state.pendingNotices.set(payload.request.id, entry); return selection.verdicts.get(runtimeId) ?? { kind: "ineligible" }; }, start() { if (state.closed || registered) return; state.activeRuntimes.set(runtimeId, { runtimeId, handledKinds: params.handledKinds, channel: params.channel, channelLabel: params.channelLabel, accountId: params.accountId, requestGateway: params.requestGateway, shouldHandle: params.shouldHandle, classifyRoute: params.classifyRoute }); registered = true; }, async reportSkipped(paramsValue) { await report({ approvalKind: paramsValue.approvalKind, request: paramsValue.request, deliveryPlan: { targets: [], originTarget: null, notifyOriginWhenDmOnly: false }, deliveredTargets: [], skipReason: paramsValue.reason }); }, async reportDelivery(paramsLocal) { await report(paramsLocal); }, completeRequest(approvalId) { clearApprovalRouteSelection(state, approvalId); clearPendingApprovalRouteNotice(state, approvalId); }, async stop() { if (!registered) return; for (const entry of Array.from(state.pendingNotices.values())) { const selection = state.selections.get(entry.request.id); if (selection?.verdicts.has(runtimeId) && !entry.reports.has(runtimeId)) await report({ request: entry.request, approvalKind: entry.approvalKind, deliveryPlan: { targets: [], originTarget: null, notifyOriginWhenDmOnly: false }, deliveredTargets: [], skipReason: selection.verdicts.get(runtimeId)?.kind === "selected" ? "owner-unavailable" : "ineligible" }); } registered = false; state.activeRuntimes.delete(runtimeId); } }; } /** Creates an instance-local route coordinator so Gateway runtimes cannot share account state. */ function createApprovalNativeRouteCoordinator() { const state = createApprovalNativeRouteCoordinatorState(); return { createReporter: (params) => createApprovalNativeRouteReporterForState(state, params), hasActiveRuntime: (params) => hasActiveApprovalNativeRouteRuntimeForState(state, params), close: () => { state.closed = true; for (const approvalId of Array.from(state.pendingNotices.keys())) clearPendingApprovalRouteNotice(state, approvalId); for (const approvalId of Array.from(state.selections.keys())) clearApprovalRouteSelection(state, approvalId); state.activeRuntimes.clear(); } }; } //#endregion export { createApprovalNativeRouteReporter as n, hasActiveApprovalNativeRouteRuntime as r, createApprovalNativeRouteCoordinator as t };