openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
370 lines (369 loc) • 17 kB
JavaScript
import { t as createDeferredCore } from "./deferred-D0La5CRk.js";
import { a as hasGatewayClientCap, n as GATEWAY_CLIENT_IDS, t as GATEWAY_CLIENT_CAPS } from "./client-info-B1bPgeKr.js";
import { r as roleScopesAllow } from "./operator-scope-compat-iV7_Lmth.js";
import { i as trackAsyncWork, r as getAsyncWorkSignal, t as AsyncWorkScope } from "./async-work-scope-CMQS2uTf.js";
import { c as resolveUserProfileId } from "./user-profiles-4AB7AmiH.js";
import { n as getPairedDevice, r as getPendingDevicePairing } from "./device-pairing-DFIQp3ZY.js";
import { r as disconnectAllSharedGatewayAuthClients } from "./server-shared-auth-generation-BzQsL2E3.js";
import { t as NODE_DESKTOP_SERVICE_CONTEXT } from "./node-source-context-Csxf7qYw.js";
import { t as broadcastPresenceSnapshot } from "./presence-events-DoThnKcO.js";
import { n as refreshClientPresence, t as recordClientPresenceActivity } from "./client-presence-BbKoA8kz.js";
//#region src/gateway/device-scope-upgrade.ts
const TERMINAL_GRACE_MS = 15e3;
const DURABLE_RECONCILE_INTERVAL_MS = 250;
function sameOwner(left, right) {
return left.deviceId === right.deviceId && left.publicKey === right.publicKey;
}
function scheduleUnref(callback, delayMs) {
const timer = setTimeout(callback, delayMs);
timer.unref?.();
return timer;
}
/** Coordinates live device scope-upgrade waiters with the durable pairing store. */
var ScopeUpgradeCoordinator = class {
constructor() {
this.entries = /* @__PURE__ */ new Map();
this.work = new AsyncWorkScope();
this.lifetimeBound = false;
}
bindGatewayLifetime() {
if (this.lifetimeBound || this.work.isClosing) return;
this.lifetimeBound = true;
const gatewaySignal = getAsyncWorkSignal();
if (!gatewaySignal) return;
if (gatewaySignal.aborted) {
this.close();
return;
}
const signal = AbortSignal.any([gatewaySignal, this.work.signal]);
trackAsyncWork(() => new Promise((resolve, reject) => {
signal.addEventListener("abort", () => {
this.close().then(resolve, reject);
}, { once: true });
}));
}
async close() {
this.work.beginClose();
for (const entry of this.entries.values()) {
clearTimeout(entry.cleanupTimer);
entry.wake.resolve();
}
this.entries.clear();
await this.work.drain();
}
register(params) {
this.bindGatewayLifetime();
if (this.work.isClosing) return false;
const existing = this.entries.get(params.requestId);
if (existing && !sameOwner(existing.owner, params.owner)) return false;
const entry = existing ?? {
requestId: params.requestId,
owner: params.owner,
requestedScopes: [...params.requestedScopes],
initialToken: params.initialToken,
initialApprovedAtMs: params.initialApprovedAtMs,
expiresAtMs: 0,
wake: createDeferredCore()
};
entry.requestedScopes = [...params.requestedScopes];
entry.expiresAtMs = params.expiresAtMs;
if (entry.cleanupTimer) clearTimeout(entry.cleanupTimer);
entry.cleanupTimer = scheduleUnref(() => this.entries.delete(entry.requestId), Math.max(0, entry.expiresAtMs + TERMINAL_GRACE_MS - Date.now()));
this.entries.set(entry.requestId, entry);
return true;
}
notify(requestId, resolution) {
const entry = this.entries.get(requestId);
if (!entry) return;
entry.resolutionHint = resolution;
const wake = entry.wake;
entry.wake = createDeferredCore();
wake.resolve();
}
async wait(requestId, owner) {
const entry = this.entries.get(requestId);
if (!entry || !sameOwner(entry.owner, owner)) return null;
if (!entry.resultPromise) {
const pending = this.work.track(() => this.waitForResult(entry));
entry.resultPromise = pending;
pending.catch(() => {
if (entry.resultPromise === pending) entry.resultPromise = void 0;
});
}
return await entry.resultPromise;
}
async waitForResult(entry) {
while (!this.work.isClosing) {
if (Date.now() >= entry.expiresAtMs) {
this.retainTerminal(entry);
return {
status: "expired",
requestId: entry.requestId
};
}
const wake = entry.wake;
const result = await this.readDurableResult(entry);
if (this.work.isClosing) break;
if (result) {
this.retainTerminal(entry);
return result;
}
const delayMs = Math.min(DURABLE_RECONCILE_INTERVAL_MS, Math.max(0, entry.expiresAtMs - Date.now()));
const timer = scheduleUnref(wake.resolve, delayMs);
try {
await wake.promise;
} finally {
clearTimeout(timer);
if (entry.wake === wake) entry.wake = createDeferredCore();
}
}
return null;
}
async readDurableResult(entry) {
const pending = await getPendingDevicePairing(entry.requestId);
if (this.work.isClosing || pending) return null;
if (entry.resolutionHint === "rejected") return {
status: "rejected",
requestId: entry.requestId
};
const paired = await getPairedDevice(entry.owner.deviceId);
if (this.work.isClosing) return null;
const token = paired?.tokens?.operator;
const approvedEvidence = entry.resolutionHint === "approved" || token?.token !== entry.initialToken && paired?.approvedAtMs !== entry.initialApprovedAtMs;
return paired?.publicKey === entry.owner.publicKey && token !== void 0 && token.revokedAtMs === void 0 && approvedEvidence && roleScopesAllow({
role: "operator",
requestedScopes: entry.requestedScopes,
allowedScopes: token.scopes
}) ? {
status: "approved",
requestId: entry.requestId,
deviceToken: token.token,
scopes: token.scopes
} : {
status: "rejected",
requestId: entry.requestId
};
}
retainTerminal(entry) {
if (entry.cleanupTimer) clearTimeout(entry.cleanupTimer);
entry.cleanupTimer = scheduleUnref(() => this.entries.delete(entry.requestId), TERMINAL_GRACE_MS);
}
};
//#endregion
//#region src/gateway/server-request-context.ts
const ALL_APPROVAL_CLIENT_IDS = /* @__PURE__ */ new Set([GATEWAY_CLIENT_IDS.CONTROL_UI]);
const EXEC_APPROVAL_CLIENT_IDS = /* @__PURE__ */ new Set([
GATEWAY_CLIENT_IDS.MACOS_APP,
GATEWAY_CLIENT_IDS.IOS_APP,
GATEWAY_CLIENT_IDS.ANDROID_APP
]);
const PLUGIN_APPROVAL_CLIENT_IDS = /* @__PURE__ */ new Set([GATEWAY_CLIENT_IDS.TUI]);
function canDeliverApprovals(gatewayClient, approvalKind) {
if (gatewayClient.invalidated) return false;
const scopes = Array.isArray(gatewayClient.connect.scopes) ? gatewayClient.connect.scopes : [];
if (!(scopes.includes("operator.admin") || scopes.includes("operator.approvals"))) return false;
return gatewayClient.internal?.approvalRuntime === true || ALL_APPROVAL_CLIENT_IDS.has(gatewayClient.connect.client.id) || hasGatewayClientCap(gatewayClient.connect.caps, GATEWAY_CLIENT_CAPS.APPROVALS) || approvalKind === "exec" && (EXEC_APPROVAL_CLIENT_IDS.has(gatewayClient.connect.client.id) || hasGatewayClientCap(gatewayClient.connect.caps, GATEWAY_CLIENT_CAPS.EXEC_APPROVALS)) || approvalKind === "plugin" && (PLUGIN_APPROVAL_CLIENT_IDS.has(gatewayClient.connect.client.id) || hasGatewayClientCap(gatewayClient.connect.caps, GATEWAY_CLIENT_CAPS.PLUGIN_APPROVALS));
}
function createGatewayRequestContext(params) {
const scopeUpgradeCoordinator = new ScopeUpgradeCoordinator();
const context = {
trackExecution: params.trackExecution,
deps: params.deps,
configRevisionProjector: params.configRevisionProjector,
get cron() {
return params.runtimeState.cronState.cron;
},
get cronStorePath() {
return params.runtimeState.cronState.storePath;
},
getRuntimeConfig: params.getRuntimeConfig,
isConfigReloadSettled: params.isConfigReloadSettled,
getGatewayMethodRegistry: params.getGatewayMethodRegistry,
gatewayTlsFingerprint: params.gatewayTlsFingerprint,
controlUiSessionPullRequests: params.runtimeState.controlUiSessionPullRequests,
sessionViewerPresence: params.runtimeState.sessionViewerPresence,
sessionCompanion: params.sessionCompanion,
sessionObserver: params.sessionObserver,
mentionInbox: params.mentionInbox,
notifyPluginMetadataChanged: params.notifyPluginMetadataChanged,
getMcpAppSandboxPort: params.getMcpAppSandboxPort,
ensureSandboxHostPort: params.ensureSandboxHostPort,
get portalService() {
return params.getPortalService?.();
},
resolveTerminalLaunchPolicy: params.resolveTerminalLaunchPolicy,
isTerminalEnabled: params.isTerminalEnabled,
execApprovalManager: params.execApprovalManager,
questionManager: params.questionManager,
scopeUpgradeCoordinator,
cancelRunBoundApprovals: params.cancelRunBoundApprovals ? (runId) => params.cancelRunBoundApprovals(runId, context) : void 0,
forwardPluginApprovalRequest: params.forwardPluginApprovalRequest,
approvalWebPushDelivery: params.approvalWebPushDelivery,
pluginApprovalIosPushDelivery: params.pluginApprovalIosPushDelivery,
pluginApprovalManager: params.pluginApprovalManager,
placementStandingGrants: params.placementStandingGrants,
systemAgentApprovalManager: params.systemAgentApprovalManager,
listSessionPendingApprovals: params.listSessionPendingApprovals,
loadGatewayModelCatalog: params.loadGatewayModelCatalog,
loadGatewayModelCatalogSnapshot: params.loadGatewayModelCatalogSnapshot,
...params.readPreparedGatewayModelCatalog ? { readPreparedGatewayModelCatalog: params.readPreparedGatewayModelCatalog } : {},
readChatMetadata: params.readChatMetadata,
...params.readChatStartupProjection ? { readChatStartupProjection: params.readChatStartupProjection } : {},
getHealthCache: params.getHealthCache,
refreshHealthSnapshot: params.refreshHealthSnapshot,
logHealth: params.logHealth,
logGateway: params.logGateway,
incrementPresenceVersion: params.incrementPresenceVersion,
getHealthVersion: params.getHealthVersion,
broadcast: params.broadcast,
broadcastToConnIds: params.broadcastToConnIds,
nodeSendToSession: params.nodeSendToSession,
nodeSendToAllSubscribed: params.nodeSendToAllSubscribed,
nodeSubscribe: params.nodeSubscribe,
nodeUnsubscribe: params.nodeUnsubscribe,
nodeUnsubscribeAll: params.nodeUnsubscribeAll,
hasConnectedTalkNode: params.hasConnectedTalkNode,
isConnectionActive: params.isConnectionActive,
recordClientActivity: (client) => {
if (recordClientPresenceActivity(params.clients, client)) broadcastPresenceSnapshot(params);
},
hasExecApprovalClients: (excludeConnId) => {
for (const gatewayClient of params.clients) {
if (excludeConnId && gatewayClient.connId === excludeConnId) continue;
if (canDeliverApprovals(gatewayClient, "exec")) return true;
}
return false;
},
getApprovalClientConnIds: (opts = {}) => {
const connIds = /* @__PURE__ */ new Set();
for (const gatewayClient of params.clients) {
if (!gatewayClient.connId) continue;
if (opts.excludeConnId && gatewayClient.connId === opts.excludeConnId) continue;
if (!canDeliverApprovals(gatewayClient, opts.approvalKind ?? "exec")) continue;
if (opts.filter && !opts.filter(gatewayClient, opts.record)) continue;
connIds.add(gatewayClient.connId);
}
return connIds;
},
getClientConnIds: (filter) => {
const connIds = /* @__PURE__ */ new Set();
for (const gatewayClient of params.clients) {
if (!gatewayClient.connId || gatewayClient.invalidated) continue;
if (filter && !filter(gatewayClient)) continue;
connIds.add(gatewayClient.connId);
}
return connIds;
},
hasConnectedClientsForDevice: (deviceId) => {
for (const gatewayClient of params.clients) if (gatewayClient.connect.device?.id === deviceId && !gatewayClient.invalidated) return true;
return false;
},
refreshConnectedUserProfile: (profile) => {
let presenceChanged = false;
for (const gatewayClient of params.clients) {
if (gatewayClient.invalidated || gatewayClient.socket.readyState !== 1) continue;
const authenticatedUserProfile = gatewayClient.authenticatedUserProfile;
if (!authenticatedUserProfile) continue;
const canonicalProfileId = authenticatedUserProfile.profileId === profile.id ? profile.id : resolveUserProfileId(authenticatedUserProfile.profileId);
if (canonicalProfileId !== profile.id) continue;
Object.assign(authenticatedUserProfile, {
profileId: canonicalProfileId,
displayName: profile.displayName,
avatarRevision: profile.avatarRevision,
hasAvatar: profile.hasAvatar,
updatedAt: profile.updatedAt
});
presenceChanged = refreshClientPresence(params.clients, gatewayClient) || presenceChanged;
}
if (presenceChanged) broadcastPresenceSnapshot({
broadcast: params.broadcast,
incrementPresenceVersion: params.incrementPresenceVersion,
getHealthVersion: params.getHealthVersion
});
},
invalidateClientsForDevice: (deviceId, opts) => {
const reason = opts?.reason ?? "device-invalidated";
for (const gatewayClient of params.clients) {
if (gatewayClient.connect.device?.id !== deviceId) continue;
if (opts?.role && gatewayClient.connect.role !== opts.role) continue;
if (gatewayClient.connId) params.nodeRegistry.invalidateConnectionForPairingChange(gatewayClient.connId, reason);
gatewayClient.invalidated = true;
gatewayClient.invalidatedReason = reason;
}
params.invalidateDeviceTransports?.(deviceId, opts);
},
disconnectClientsForDevice: (deviceId, opts) => {
for (const gatewayClient of params.clients) {
if (gatewayClient.connect.device?.id !== deviceId) continue;
if (opts?.role && gatewayClient.connect.role !== opts.role) continue;
gatewayClient.invalidated = true;
gatewayClient.invalidatedReason ??= "device-removed";
try {
gatewayClient.socket.close(4001, "device removed");
} catch {}
}
params.disconnectDeviceTransports?.(deviceId, opts);
},
disconnectClientsForUserProfile: (profileId) => {
for (const gatewayClient of params.clients) {
if (gatewayClient.authenticatedUserProfile?.profileId !== profileId) continue;
gatewayClient.invalidated = true;
gatewayClient.invalidatedReason = "operator-role-changed";
try {
gatewayClient.socket.close(4001, "operator role changed");
} catch {}
}
},
disconnectClientsUsingSharedGatewayAuth: () => {
disconnectAllSharedGatewayAuthClients(params.clients);
},
enforceSharedGatewayAuthGenerationForConfigWrite: params.enforceSharedGatewayAuthGenerationForConfigWrite,
nodeRegistry: params.nodeRegistry,
...params.nodeDesktopService ? { [NODE_DESKTOP_SERVICE_CONTEXT]: params.nodeDesktopService } : {},
...params.workerEnvironmentService ? { workerEnvironmentService: params.workerEnvironmentService } : {},
...params.hostDesktopService ? { hostDesktopService: params.hostDesktopService } : {},
...params.workerSessionPlacementService ? { workerSessionPlacementService: params.workerSessionPlacementService } : {},
...params.workerPlacementDiskSpaceReader ? { workerPlacementDiskSpaceReader: params.workerPlacementDiskSpaceReader } : {},
...params.workerPlacementRunnerAvailabilityReader ? { workerPlacementRunnerAvailabilityReader: params.workerPlacementRunnerAvailabilityReader } : {},
validateAgentRuntimeApprovalAuthority: params.validateAgentRuntimeApprovalAuthority,
...params.workerPlacementDispatchService ? { workerPlacementDispatchService: params.workerPlacementDispatchService } : {},
...params.githubPublicationService ? { githubPublicationService: params.githubPublicationService } : {},
terminalSessions: params.terminalSessions,
agentRunSeq: params.agentRunSeq,
chatAbortControllers: params.chatAbortControllers,
chatQueuedTurns: params.chatQueuedTurns,
chatRunState: params.chatRunState,
addChatRun: params.addChatRun,
removeChatRun: params.removeChatRun,
subscribeSessionEvents: params.subscribeSessionEvents,
unsubscribeSessionEvents: params.unsubscribeSessionEvents,
subscribeSessionMessageEvents: params.subscribeSessionMessageEvents,
unsubscribeSessionMessageEvents: params.unsubscribeSessionMessageEvents,
unsubscribeAllSessionEvents: (connId) => {
params.unsubscribeAllSessionEvents(connId);
params.runtimeState.controlUiSessionPullRequests?.unsubscribe(connId);
params.runtimeState.sessionViewerPresence?.unsubscribe(connId);
},
getSessionEventSubscriberConnIds: params.getSessionEventSubscriberConnIds,
registerToolEventRecipient: params.registerToolEventRecipient,
dedupe: params.dedupe,
wizardSessions: params.wizardSessions,
systemAgentSessions: params.systemAgentSessions,
findRunningWizard: params.findRunningWizard,
purgeWizardSession: params.purgeWizardSession,
getRuntimeSnapshot: params.getRuntimeSnapshot,
getEventLoopHealth: params.getEventLoopHealth,
getConfigReloaderHotReloadStatus: params.getConfigReloaderHotReloadStatus,
startChannel: params.startChannel,
stopChannel: params.stopChannel,
markChannelLoggedOut: params.markChannelLoggedOut,
wizardRunner: params.wizardRunner,
channelWizardRunner: params.channelWizardRunner,
broadcastVoiceWakeChanged: params.broadcastVoiceWakeChanged,
broadcastVoiceWakeRoutingChanged: params.broadcastVoiceWakeRoutingChanged,
unavailableGatewayMethods: params.unavailableGatewayMethods
};
return context;
}
//#endregion
export { createGatewayRequestContext };