UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

370 lines (369 loc) 17 kB
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 };