openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
551 lines (550 loc) • 18.3 kB
JavaScript
import { i as formatErrorMessage } from "./errors-BXgSefBE.js";
import { t as createSubsystemLogger } from "./subsystem-BzXSmsuh.js";
import { t as startGatewayClientWhenEventLoopReady } from "./client-start-readiness-BIoVQZze.js";
import { l as readConnectErrorDetailCode } from "./connect-error-details-BXqba0zp.js";
import { t as createApprovalNativeRouteReporter } from "./approval-native-route-coordinator-Bs0VdpS8.js";
import { t as buildChannelApprovalNativeTargetKey } from "./approval-native-target-key-BgRxMHOp.js";
import { t as createOperatorApprovalsGatewayClient } from "./operator-approvals-client-4O2dZK2Q.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");
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 pending = /* @__PURE__ */ new Map();
let gatewayClient = 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 clearPendingEntry = (approvalId) => {
const entry = pending.get(approvalId);
if (!entry) return null;
pending.delete(approvalId);
if (entry.timeoutId) clearTimeout(entry.timeoutId);
return entry;
};
const handleExpired = async (approvalId) => {
const entry = clearPendingEntry(approvalId);
if (!entry) return;
log.debug(`expired ${approvalId}`);
await adapter.finalizeExpired?.({
request: entry.request,
entries: entry.entries
});
};
const handleRequested = async (request, opts) => {
if (opts?.ignoreIfInactive && !shouldKeepRunning()) return;
if (!adapter.shouldHandle(request)) return;
if (pending.has(request.id)) {
log.debug(`ignored duplicate request ${request.id}`);
return;
}
log.debug(`received request ${request.id}`);
const entry = {
request,
entries: [],
timeoutId: null,
delivering: true,
pendingResolution: null
};
pending.set(request.id, entry);
let entries;
try {
entries = await adapter.deliverRequested(request);
} catch (err) {
if (pending.get(request.id) === entry) clearPendingEntry(request.id);
throw err;
}
if (pending.get(request.id) !== entry) return;
if (!entries.length) {
pending.delete(request.id);
return;
}
entry.entries = entries;
entry.delivering = false;
if (entry.pendingResolution) {
pending.delete(request.id);
log.debug(`resolved ${entry.pendingResolution.id} with ${entry.pendingResolution.decision}`);
await adapter.finalizeResolved({
request: entry.request,
resolved: entry.pendingResolution,
entries: entry.entries
});
return;
}
const timeoutMs = Math.max(0, request.expiresAtMs - nowMs());
const timeoutId = setTimeout(() => {
spawn("error handling approval expiration", handleExpired(request.id));
}, timeoutMs);
timeoutId.unref?.();
entry.timeoutId = timeoutId;
};
const handleResolved = async (resolved) => {
const entry = pending.get(resolved.id);
if (!entry) return;
if (entry.delivering) {
entry.pendingResolution = resolved;
return;
}
const finalizedEntry = clearPendingEntry(resolved.id);
if (!finalizedEntry) return;
log.debug(`resolved ${resolved.id} with ${resolved.decision}`);
await adapter.finalizeResolved({
request: finalizedEntry.request,
resolved,
entries: finalizedEntry.entries
});
};
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 === "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));
};
const replayPendingApprovals = async (client) => {
try {
for (const method of resolveApprovalReplayMethods(eventKinds)) {
if (stopClientIfInactive(client)) return;
const pendingRequests = await client.request(method, {});
if (stopClientIfInactive(client)) return;
for (const request of pendingRequests) {
if (stopClientIfInactive(client)) return;
await handleRequested(request, { ignoreIfInactive: true });
}
}
} catch (error) {
if (!shouldKeepRunning()) return;
throw error;
}
};
const startPendingApprovalReplay = (client) => {
const promise = replayPendingApprovals(client).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;
}
let readySettled = false;
let resolveReady;
let rejectReady;
const ready = new Promise((resolve, reject) => {
resolveReady = resolve;
rejectReady = reject;
});
let lastConnectError = null;
const settleReady = (fn) => {
if (readySettled) return;
readySettled = true;
fn();
};
const client = await createOperatorApprovalsGatewayClient({
config: adapter.cfg,
gatewayUrl: adapter.gatewayUrl,
clientDisplayName: adapter.clientDisplayName,
onEvent: handleGatewayEvent,
onHelloOk: () => {
log.debug("connected to gateway");
settleReady(resolveReady);
},
onConnectError: (err) => {
log.error(`connect error: ${err.message}`);
lastConnectError = err;
if (readGatewayConnectErrorDetailCode(err)) return;
settleReady(() => rejectReady(err));
},
onReconnectPaused: (info) => {
settleReady(() => rejectReady(new ExecApprovalChannelRuntimeTerminalStartError(info, lastConnectError)));
},
onClose: (code, reason) => {
log.debug(`gateway closed: ${code} ${reason}`);
settleReady(() => rejectReady(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: { preauthHandshakeTimeoutMs: adapter.cfg.gateway?.handshakeTimeoutMs } });
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;
if (stopClientIfInactive(client)) return;
started = true;
startPendingApprovalReplay(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;
gatewayClient?.stop();
gatewayClient = null;
await waitForPendingApprovalReplay();
if (!wasActive) {
await adapter.onStopped?.();
return;
}
for (const entry of pending.values()) if (entry.timeoutId) clearTimeout(entry.timeoutId);
pending.clear();
await adapter.onStopped?.();
log.debug("stopped");
},
handleRequested,
handleResolved,
handleExpired,
async request(method, params) {
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
};
}
function defaultResolveApprovalKind(request) {
return request.id.startsWith("plugin:") ? "plugin" : "exec";
}
/** Creates the shared gateway approval runtime backed by channel-native delivery hooks. */
function createChannelNativeApprovalRuntime(adapter) {
const nowMs = adapter.nowMs ?? Date.now;
const resolveApprovalKind = adapter.resolveApprovalKind ?? ((request) => defaultResolveApprovalKind(request));
let runtimeRequest = null;
const routeReporter = createApprovalNativeRouteReporter({
handledKinds: new Set(adapter.eventKinds ?? ["exec"]),
channel: adapter.channel,
channelLabel: adapter.channelLabel,
accountId: adapter.accountId,
requestGateway: async (method, params) => {
if (!runtimeRequest) throw new Error(`${adapter.label}: gateway client not connected`);
return await runtimeRequest(method, params);
}
});
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 = resolveApprovalKind(request);
routeReporter.observeRequest({
approvalKind,
request
});
let shouldHandle;
try {
shouldHandle = adapter.shouldHandle(request);
} catch (error) {
routeReporter.reportSkipped({
approvalKind,
request
});
throw error;
}
if (shouldHandle) return shouldHandle;
routeReporter.reportSkipped({
approvalKind,
request
});
return false;
},
finalizeResolved: adapter.finalizeResolved,
finalizeExpired: adapter.finalizeExpired,
onStopped: adapter.onStopped,
beforeGatewayClientStart: () => {
routeReporter.start();
},
nowMs,
deliverRequested: async (request) => {
const approvalKind = resolveApprovalKind(request);
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
});
}
}
});
runtimeRequest = (method, params) => runtime.request(method, params);
return {
...runtime,
async start() {
try {
await runtime.start();
} catch (error) {
await routeReporter.stop();
throw error;
}
},
async stop() {
await routeReporter.stop();
await runtime.stop();
}
};
}
//#endregion
export { isExecApprovalChannelRuntimeTerminalStartError as a, createExecApprovalChannelRuntime as i, deliverApprovalRequestViaChannelNativePlan as n, resolveChannelNativeApprovalDeliveryPlan as o, ExecApprovalChannelRuntimeTerminalStartError as r, createChannelNativeApprovalRuntime as t };