UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

597 lines (596 loc) 21 kB
import { t as formatErrorMessage } from "./errors-Db3Ymjlb.js"; import { t as createSubsystemLogger } from "./subsystem-Dy2tqXOS.js"; import { t as createDeferredCore } from "./deferred-D0La5CRk.js"; import { i as GATEWAY_CLIENT_NAMES, r as GATEWAY_CLIENT_MODES } from "./client-info-B1bPgeKr.js"; import { u as readConnectErrorDetailCode } from "./connect-error-details-DaOfGNN8.js"; import { t as startGatewayClientWhenEventLoopReady } from "./client-start-readiness-B1nULpha.js"; import { i as isApprovalMethod } from "./method-scopes-K6J_UQGL.js"; import "./message-channel-BQrhwUEA.js"; import { t as getGatewayNativeApprovalRuntime } from "./approval-gateway-runtime-context-BSAo6TQe.js"; import { n as createApprovalNativeRouteReporter } from "./approval-native-route-coordinator-dwijzZNW.js"; import { t as buildChannelApprovalNativeTargetKey } from "./approval-native-target-key-uR5nEUEh.js"; import { s as normalizeApprovalRequest, t as classifyApprovalRequestChannelRoute } from "./approval-request-account-binding-Sc-eGYO-.js"; import { t as createOperatorApprovalsGatewayClient } from "./operator-approvals-client-DM1DC0Bp.js"; import { t as createPendingApprovalRegistry } from "./pending-approval-registry-Ba8xUSFo.js"; import { n as isGatewayNativeApprovalMethod } from "./approval-gateway-runtime-methods-CZtHej9H.js"; //#region src/infra/approval-native-delivery.ts function dedupeTargets(targets) { const seen = /* @__PURE__ */ new Set(); const deduped = []; for (const target of targets) { const key = buildChannelApprovalNativeTargetKey(target.target); if (seen.has(key)) continue; seen.add(key); deduped.push(target); } return deduped; } /** Resolves the origin and approver-DM targets a channel should use for native approvals. */ async function resolveChannelNativeApprovalDeliveryPlan(params) { const adapter = params.adapter; if (!adapter) return { targets: [], originTarget: null, notifyOriginWhenDmOnly: false }; const capabilities = adapter.describeDeliveryCapabilities({ cfg: params.cfg, accountId: params.accountId, approvalKind: params.approvalKind, request: params.request }); if (!capabilities.enabled) return { targets: [], originTarget: null, notifyOriginWhenDmOnly: false }; const originTarget = capabilities.supportsOriginSurface && adapter.resolveOriginTarget ? await adapter.resolveOriginTarget({ cfg: params.cfg, accountId: params.accountId, approvalKind: params.approvalKind, request: params.request }) ?? null : null; const approverDmTargets = capabilities.supportsApproverDmSurface && adapter.resolveApproverDmTargets ? await adapter.resolveApproverDmTargets({ cfg: params.cfg, accountId: params.accountId, approvalKind: params.approvalKind, request: params.request }) : []; const plannedTargets = []; const preferOrigin = capabilities.preferredSurface === "origin" || capabilities.preferredSurface === "both"; const preferApproverDm = capabilities.preferredSurface === "approver-dm" || capabilities.preferredSurface === "both"; if (preferOrigin && originTarget) plannedTargets.push({ surface: "origin", target: originTarget, reason: "preferred" }); if (preferApproverDm) for (const target of approverDmTargets) plannedTargets.push({ surface: "approver-dm", target, reason: "preferred" }); else if (!originTarget) for (const target of approverDmTargets) plannedTargets.push({ surface: "approver-dm", target, reason: "fallback" }); return { targets: dedupeTargets(plannedTargets), originTarget, notifyOriginWhenDmOnly: capabilities.preferredSurface === "approver-dm" && capabilities.notifyOriginWhenDmOnly === true && originTarget !== null }; } //#endregion //#region src/infra/exec-approval-channel-runtime.ts /** Error raised when the gateway pauses approval reconnects after a terminal startup failure. */ var ExecApprovalChannelRuntimeTerminalStartError = class extends Error { constructor(info, cause) { super(`native approval gateway client paused reconnect after startup auth failure (${info.detailCode ?? "unknown"}): gateway closed (${info.code}): ${info.reason}`, cause === void 0 ? void 0 : { cause }); this.name = "ExecApprovalChannelRuntimeTerminalStartError"; this.detailCode = info.detailCode; } }; /** Narrows terminal approval runtime startup failures for bootstrap retry policy. */ function isExecApprovalChannelRuntimeTerminalStartError(error) { return error instanceof ExecApprovalChannelRuntimeTerminalStartError; } function resolveApprovalReplayMethods(eventKinds) { const methods = []; if (eventKinds.has("exec")) methods.push("exec.approval.list"); if (eventKinds.has("plugin")) methods.push("plugin.approval.list"); if (eventKinds.has("system-agent")) methods.push("openclaw.approval.list"); return methods; } function readGatewayConnectErrorDetailCode(error) { if (!error || typeof error !== "object") return null; return readConnectErrorDetailCode(error.details); } /** Creates the gateway-backed approval runtime that tracks pending requests and finalization. */ function createExecApprovalChannelRuntime(adapter) { const log = createSubsystemLogger(adapter.label); const nowMs = adapter.nowMs ?? Date.now; const eventKinds = new Set(adapter.eventKinds ?? ["exec"]); const configuredGatewayRuntime = getGatewayNativeApprovalRuntime(); const pending = createPendingApprovalRegistry(); let gatewayClient = null; let gatewayRuntime; let unsubscribeGatewayRuntime = null; let started = false; let shouldRun = false; let startPromise = null; let replayPromise = null; const shouldKeepRunning = () => shouldRun; const spawn = (label, promise) => { promise.catch((err) => { const message = formatErrorMessage(err); log.error(`${label}: ${message}`); }); }; const stopClientIfInactive = (client) => { if (shouldKeepRunning()) return false; gatewayClient = null; client.stop(); return true; }; const finalizeExpiredEntry = async (entry) => { log.debug(`expired ${entry.id}`); await adapter.finalizeExpired?.(entry.value); }; const handleExpired = async (approvalId) => { const entry = pending.remove(approvalId); if (!entry) return; await finalizeExpiredEntry(entry); }; const handleRequested = async (requestInput, opts) => { if (opts?.ignoreIfInactive && !shouldKeepRunning()) return; const request = normalizeApprovalRequest(requestInput); if (pending.has(request.id)) { log.debug(`ignored duplicate request ${request.id}`); return; } if (opts?.alreadyAccepted !== true && !adapter.shouldHandle(request)) return; log.debug(`received request ${request.id}`); const entry = pending.begin(request.id, { request, entries: [] }); let entries; try { entries = await adapter.deliverRequested(request); } catch (err) { pending.remove(request.id, entry); throw err; } if (!pending.isCurrent(entry)) return; if (!entries.length) { pending.remove(request.id, entry); return; } await pending.completeDelivery(entry, { request, entries }); if (!pending.isCurrent(entry)) return; const timeoutMs = Math.max(0, request.expiresAtMs - nowMs()); pending.scheduleExpiry(entry, timeoutMs, (expired) => { spawn("error handling approval expiration", finalizeExpiredEntry(expired)); }); }; const handleResolved = async (resolved) => { if ("terminalStatus" in resolved && resolved.terminalStatus === "expired") { await handleExpired(resolved.id); return; } const settled = pending.settle(resolved.id, async (entry) => { log.debug(`resolved ${resolved.id} with ${resolved.decision}`); await adapter.finalizeResolved({ request: entry.value.request, resolved, entries: entry.value.entries }); }); if (settled.status === "taken") await settled.terminal(settled.entry); }; const handleGatewayEvent = (evt) => { if (evt.event === "exec.approval.requested" && eventKinds.has("exec")) { spawn("error handling approval request", handleRequested(evt.payload, { ignoreIfInactive: true })); return; } if (evt.event === "plugin.approval.requested" && eventKinds.has("plugin")) { spawn("error handling approval request", handleRequested(evt.payload, { ignoreIfInactive: true })); return; } if (evt.event === "openclaw.approval.requested" && eventKinds.has("system-agent")) { spawn("error handling approval request", handleRequested(evt.payload, { ignoreIfInactive: true })); return; } if (evt.event === "exec.approval.resolved" && eventKinds.has("exec")) { spawn("error handling approval resolved", handleResolved(evt.payload)); return; } if (evt.event === "plugin.approval.resolved" && eventKinds.has("plugin")) { spawn("error handling approval resolved", handleResolved(evt.payload)); return; } if (evt.event === "openclaw.approval.resolved" && eventKinds.has("system-agent")) spawn("error handling approval resolved", handleResolved(evt.payload)); }; const replayPendingApprovals = async (client, externalClient) => { try { for (const method of resolveApprovalReplayMethods(eventKinds)) { if (externalClient && stopClientIfInactive(externalClient)) return; const pendingRequests = await client.request(method, {}); if (externalClient && stopClientIfInactive(externalClient)) return; for (const request of pendingRequests) { if (externalClient && stopClientIfInactive(externalClient)) return; await handleRequested(request, { ignoreIfInactive: true }); } } } catch (error) { if (!shouldKeepRunning()) return; throw error; } }; const startPendingApprovalReplay = (client, externalClient) => { const promise = replayPendingApprovals(client, externalClient).catch((err) => { const message = formatErrorMessage(err); log.error(`error replaying pending approvals: ${message}`); }).finally(() => { if (replayPromise === promise) replayPromise = null; }); replayPromise = promise; }; const waitForPendingApprovalReplay = async () => { const replay = replayPromise; if (!replay) return; await replay.catch(() => {}); }; return { async start() { if (started) return; if (startPromise) { await startPromise; return; } shouldRun = true; startPromise = (async () => { if (!adapter.isConfigured()) { log.debug("disabled"); return; } if (configuredGatewayRuntime) { await adapter.beforeGatewayClientStart?.(); gatewayRuntime = configuredGatewayRuntime; unsubscribeGatewayRuntime = gatewayRuntime.subscribe({ eventKinds, shouldHandle: (request) => shouldKeepRunning() && adapter.shouldHandle(request), onRequested: (request) => { spawn("error handling approval request", handleRequested(request, { ignoreIfInactive: true, alreadyAccepted: true })); }, onResolved: (resolved) => { spawn("error handling approval resolved", handleResolved(resolved)); } }); if (!shouldRun) { unsubscribeGatewayRuntime(); unsubscribeGatewayRuntime = null; gatewayRuntime = void 0; return; } started = true; startPendingApprovalReplay({ request: gatewayRuntime.request }); return; } const ready = createDeferredCore(); let lastConnectError = null; const client = await createOperatorApprovalsGatewayClient({ config: adapter.cfg, gatewayUrl: adapter.gatewayUrl, clientDisplayName: adapter.clientDisplayName, onEvent: handleGatewayEvent, onHelloOk: () => { log.debug("connected to gateway"); ready.resolve(); }, onConnectError: (err) => { log.error(`connect error: ${err.message}`); lastConnectError = err; if (readGatewayConnectErrorDetailCode(err)) return; ready.reject(err); }, onReconnectPaused: (info) => { ready.reject(new ExecApprovalChannelRuntimeTerminalStartError(info, lastConnectError)); }, onClose: (code, reason) => { log.debug(`gateway closed: ${code} ${reason}`); ready.reject(lastConnectError ?? /* @__PURE__ */ new Error(`gateway closed: ${code} ${reason}`)); } }); if (!shouldRun) { client.stop(); return; } await adapter.beforeGatewayClientStart?.(); gatewayClient = client; try { const readiness = await startGatewayClientWhenEventLoopReady(client, { clientOptions: {} }); if (!readiness.ready) throw new Error(readiness.aborted ? "gateway approval runtime start aborted before readiness" : "gateway readiness unavailable before exec approval runtime start"); await ready.promise; if (stopClientIfInactive(client)) return; started = true; startPendingApprovalReplay(client, client); } catch (error) { gatewayClient = null; started = false; client.stop(); throw error; } })().finally(() => { startPromise = null; }); await startPromise; }, async stop() { shouldRun = false; if (startPromise) await startPromise.catch(() => {}); const wasActive = started || gatewayClient !== null || replayPromise !== null; started = false; unsubscribeGatewayRuntime?.(); unsubscribeGatewayRuntime = null; gatewayRuntime = void 0; gatewayClient?.stop(); gatewayClient = null; await waitForPendingApprovalReplay(); if (!wasActive) { await adapter.onStopped?.(); return; } pending.clear(); await adapter.onStopped?.(); log.debug("stopped"); }, handleRequested, handleResolved, handleExpired, async request(method, params) { if (!isApprovalMethod(method)) throw new Error(`${adapter.label}: operator approvals runtime cannot dispatch ${method}; use a write-capable gateway client`); if (gatewayRuntime) { if (!isGatewayNativeApprovalMethod(method)) throw new Error(`${adapter.label}: Gateway-owned approval runtime cannot dispatch ${method}`); return await gatewayRuntime.request(method, params, { clientDisplayName: adapter.clientDisplayName }); } if (!gatewayClient) throw new Error(`${adapter.label}: gateway client not connected`); return await gatewayClient.request(method, params); } }; } //#endregion //#region src/infra/approval-native-runtime.ts /** Delivers an approval request to the adapter-planned native targets and returns pending entries. */ async function deliverApprovalRequestViaChannelNativePlan(params) { const deliveryPlan = await resolveChannelNativeApprovalDeliveryPlan({ cfg: params.cfg, accountId: params.accountId, approvalKind: params.approvalKind, request: params.request, adapter: params.adapter }); const deliveredKeys = /* @__PURE__ */ new Set(); const pendingEntries = []; const deliveredTargets = []; for (const plannedTarget of deliveryPlan.targets) try { const preparedTarget = await params.prepareTarget({ plannedTarget, request: params.request }); if (!preparedTarget) continue; if (deliveredKeys.has(preparedTarget.dedupeKey)) { params.onDuplicateSkipped?.({ plannedTarget, preparedTarget, request: params.request }); continue; } const entry = await params.deliverTarget({ plannedTarget, preparedTarget: preparedTarget.target, request: params.request }); if (!entry) continue; deliveredKeys.add(preparedTarget.dedupeKey); pendingEntries.push(entry); deliveredTargets.push(plannedTarget); params.onDelivered?.({ plannedTarget, preparedTarget, request: params.request, entry }); } catch (error) { params.onDeliveryError?.({ error, plannedTarget, request: params.request }); } return { entries: pendingEntries, deliveryPlan, deliveredTargets }; } /** Creates the shared gateway approval runtime backed by channel-native delivery hooks. */ function createChannelNativeApprovalRuntime(adapter) { const nowMs = adapter.nowMs ?? Date.now; const handledEventKinds = new Set(adapter.eventKinds ?? ["exec"]); const gatewayRuntime = getGatewayNativeApprovalRuntime(); const routeReporter = (gatewayRuntime?.routeCoordinator.createReporter ?? createApprovalNativeRouteReporter)({ handledKinds: handledEventKinds, channel: adapter.channel, channelLabel: adapter.channelLabel, accountId: adapter.accountId, shouldHandle: (request) => adapter.shouldHandle(request), classifyRoute: (request) => classifyApprovalRequestChannelRoute({ cfg: adapter.cfg, request, channel: adapter.channel ?? "" }), requestGateway: async (method, params) => { if (gatewayRuntime) { if (method !== "send") throw new Error(`native approval route cannot dispatch ${method}`); return await gatewayRuntime.requestRoute(method, params); } const { callGatewayLeastPrivilege } = await import("./call-Bo4Qx1ax.js"); return await callGatewayLeastPrivilege({ config: adapter.cfg, ...adapter.gatewayUrl ? { url: adapter.gatewayUrl } : {}, method, params, clientName: GATEWAY_CLIENT_NAMES.GATEWAY_CLIENT, mode: GATEWAY_CLIENT_MODES.BACKEND }); } }); const runtime = createExecApprovalChannelRuntime({ label: adapter.label, clientDisplayName: adapter.clientDisplayName, cfg: adapter.cfg, gatewayUrl: adapter.gatewayUrl, eventKinds: adapter.eventKinds, isConfigured: adapter.isConfigured, shouldHandle: (request) => { const approvalKind = adapter.resolveApprovalKind?.(request) ?? request.approvalKind; const selection = routeReporter.selectRequest({ approvalKind, request }); if (selection.kind === "selected") return true; if (selection.kind === "selector-error") { routeReporter.reportSkipped({ approvalKind, request, reason: "ineligible" }); throw selection.error; } routeReporter.reportSkipped({ approvalKind, request, reason: selection.kind }); return false; }, finalizeResolved: async (params) => { try { await adapter.finalizeResolved(params); } finally { routeReporter.completeRequest(params.request.id); } }, finalizeExpired: adapter.finalizeExpired ? async (params) => { try { await adapter.finalizeExpired?.(params); } finally { routeReporter.completeRequest(params.request.id); } } : void 0, onStopped: adapter.onStopped, beforeGatewayClientStart: () => { routeReporter.start(); }, nowMs, deliverRequested: async (request) => { const approvalKind = adapter.resolveApprovalKind?.(request) ?? request.approvalKind; let deliveryPlan = { targets: [], originTarget: null, notifyOriginWhenDmOnly: false }; let deliveredTargets = []; try { const pendingContent = await adapter.buildPendingContent({ request, approvalKind, nowMs: nowMs() }); const deliveryResult = await deliverApprovalRequestViaChannelNativePlan({ cfg: adapter.cfg, accountId: adapter.accountId, approvalKind, request, adapter: adapter.nativeAdapter, prepareTarget: async ({ plannedTarget, request: requestCandidate }) => await adapter.prepareTarget({ plannedTarget, request: requestCandidate, approvalKind, pendingContent }), deliverTarget: async ({ plannedTarget, preparedTarget, request: requestEntry }) => await adapter.deliverTarget({ plannedTarget, preparedTarget, request: requestEntry, approvalKind, pendingContent }), onDeliveryError: adapter.onDeliveryError ? ({ error, plannedTarget, request: requestResult }) => { adapter.onDeliveryError?.({ error, plannedTarget, request: requestResult, approvalKind, pendingContent }); } : void 0, onDuplicateSkipped: adapter.onDuplicateSkipped ? ({ plannedTarget, preparedTarget, request: requestValue }) => { adapter.onDuplicateSkipped?.({ plannedTarget, preparedTarget, request: requestValue, approvalKind, pendingContent }); } : void 0, onDelivered: adapter.onDelivered ? ({ plannedTarget, preparedTarget, request: requestLocal, entry }) => { adapter.onDelivered?.({ plannedTarget, preparedTarget, request: requestLocal, approvalKind, pendingContent, entry }); } : void 0 }); deliveryPlan = deliveryResult.deliveryPlan; deliveredTargets = deliveryResult.deliveredTargets; return deliveryResult.entries; } finally { await routeReporter.reportDelivery({ approvalKind, request, deliveryPlan, deliveredTargets }); } } }); return { ...runtime, async start() { try { await runtime.start(); } catch (error) { await routeReporter.stop(); throw error; } }, async stop() { await runtime.stop(); await routeReporter.stop(); } }; } //#endregion export { isExecApprovalChannelRuntimeTerminalStartError as a, createExecApprovalChannelRuntime as i, deliverApprovalRequestViaChannelNativePlan as n, resolveChannelNativeApprovalDeliveryPlan as o, ExecApprovalChannelRuntimeTerminalStartError as r, createChannelNativeApprovalRuntime as t };