openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
7,240 lines • 312 kB
JavaScript
import { g as isFutureDateTimestampMs, j as resolveIntegerOption, w as parseStrictPositiveInteger } from "./number-coercion-CLj0HTDM.js";
import "./src-vebZIeLe.js";
import { c as isRecord } from "./record-coerce-DItp3I4t.js";
import { l as normalizeOptionalString } from "./string-coerce-CIXf7egm.js";
import { y as uniqueStrings } from "./string-normalization-DsCfAx8q.js";
import { r as truncateUtf16Safe } from "./utf16-slice-D_ngcYKd.js";
import { n as createLazyPromise, r as createLazyPromiseLoader } from "./lazy-promise-DGqyc4Y4.js";
import { r as createLazyRuntimeModule } from "./lazy-runtime-CgCh8H_K.js";
import { n as isVitestRuntimeEnv } from "./test-runtime-env-DQDRzsLt.js";
import { n as isTruthyEnvValue, r as logAcceptedEnvOption } from "./env-M3R40TOb.js";
import { c as readConfigFileSnapshotForRuntimeTransaction, g as registerConfigWriteListener, i as promoteConfigSnapshotToLastKnownGood, n as getRuntimeConfig, s as readConfigFileSnapshot, w as captureConfigOverrideApplier } from "./io.runtime-B9iJRs3w.js";
import { n as isErrno } from "./errno-CkbDOfLk.js";
import { a as isWithinDir } from "./path-safety-Bi0ppMWC.js";
import "./utils-P__uGsPB.js";
import { t as sleep } from "./sleep-D7nua6TP.js";
import { n as ok, t as err } from "./result-BQGgYouL.js";
import { c as isNixMode, l as normalizeStateDirEnv, w as resolveStateDir } from "./paths-D2sRr1a_.js";
import "./session-key-BnWWjqNc.js";
import { t as isIncognitoSessionKey } from "./incognito-session-key-BwpD1Lwd.js";
import { d as isSecretRef } from "./types.secrets-kC0nOetj.js";
import { t as formatErrorMessage } from "./errors-Db3Ymjlb.js";
import { r as applyLoggingConfig } from "./logger-DK-iouVT.js";
import { B as hasInternalDiagnosticEventInterest, N as runWithDiagnosticTraceContext, a as emitInternalDiagnosticEvent, f as isDiagnosticsEnabled, t as areDiagnosticsEnabledForProcess, w as createDiagnosticTraceContext, y as setDiagnosticsEnabledForProcess } from "./diagnostic-events-Cwe92uV3.js";
import { r as runtimeForLogger, t as createSubsystemLogger } from "./subsystem-Dy2tqXOS.js";
import { i as getGatewayPluginMetadataSnapshot } from "./current-plugin-metadata-state-B1UoAr4G.js";
import { n as registerPluginMetadataProcessMemoLifecycleClear, r as retainGatewayPluginMetadata } from "./plugin-metadata-lifecycle-C6m-q5Au.js";
import { g as resolveDatabasePath, o as openClawStateDatabaseCache } from "./openclaw-state-db-cache-C7ljO0xP.js";
import { l as resolveRuntimeServiceVersion } from "./version-v1kuAkGj.js";
import { s as resolveOpenClawStateSqlitePath } from "./openclaw-state-db-schema-version-c1ZL6JGz.js";
import { m as assertOpenClawStateWriteAllowedAtPath } from "./openclaw-state-db-BRTnL-D8.js";
import { o as sha256HexPrefixCore } from "./crypto-digest-C4hqTb_e.js";
import { t as createDeferredCore } from "./deferred-D0La5CRk.js";
import { f as runHttpConnectionRequest } from "./http-body-D3IMwTJJ.js";
import { a as setGatewayPluginMetadataSnapshot } from "./current-plugin-metadata-snapshot-CmSX4G3W.js";
import { n as ensureControlUiAllowedOriginsForNonLoopbackBind } from "./gateway-control-ui-origins-dIyw_yYs.js";
import { d as prepareConfigRuntimeEnv, l as initializePublishedConfigRuntimeEnv, r as collectConfigRuntimeEnvOwnership } from "./config-env-vars-DUfQlcAk.js";
import { n as copyConfigResolutionFacts, r as copyConfigResolutionFactsExcept } from "./resolution-facts-Dks1tbik.js";
import { l as getRuntimeConfigSourceSnapshot, o as getRuntimeConfigSnapshot, w as setAppliedRuntimeConfigSnapshot } from "./runtime-snapshot-BaQikjTR.js";
import { C as assertGatewayConfigEnvSelectionUnchanged } from "./io.read-helpers-ZKp-UiGx.js";
import { a as READ_SCOPE, i as QUESTIONS_SCOPE, n as APPROVALS_SCOPE, o as TALK_SCOPE, r as PAIRING_SCOPE, t as ADMIN_SCOPE } from "./operator-scopes-Dw7Gu2cA.js";
import { a as hasGatewayClientCap, t as GATEWAY_CLIENT_CAPS } from "./client-info-B1bPgeKr.js";
import { t as KeyedAsyncQueue } from "./keyed-async-queue-CTreGrmR.js";
import "./io-bdCzpGWJ.js";
import "./config-Cs0XXL3x.js";
import { t as GatewayLockError } from "./gateway-lock-B0QIxQaj.js";
import { o as purgeExpiredSecretStoreEntries } from "./secret-store-CC1e6gjb.js";
import { t as isContainerEnvironment } from "./container-environment-CNsJSTpY.js";
import { _ as resolveGatewayListenHosts, r as isLocalDirectRequest, s as isLoopbackHost } from "./net-DbNPs6Xm.js";
import { f as buildRateLimitIdentityKey, p as createAuthRateLimiter, s as AUTH_RATE_LIMIT_SCOPE_NODE_REAPPROVAL, u as AUTH_RATE_LIMIT_SCOPE_WORKER_ADMISSION } from "./auth-rate-limit-C2k5aTEA.js";
import { o as readTailscaleWhoisIdentity } from "./tailscale-Boe4Jx-9.js";
import { a as prepareGatewayIngressAttribution, i as markGatewayIngressTransport, n as PROXY_ATTRIBUTION_REQUIRED_REASON, o as readPreparedGatewayIngressAttribution, r as createGatewayUnattributableProxyReporter, t as PROXY_ATTRIBUTION_GUIDANCE } from "./ingress-attribution-C3JDfhmX.js";
import { n as resolveGatewayAuth } from "./auth-resolve-O5AKX-sb.js";
import { r as authorizeHttpGatewayConnect } from "./auth-CyN_wFeb.js";
import { n as isRestartEnabled } from "./commands.flags-CZN5Wwe1.js";
import { t as ErrorCodes } from "./gateway-error-details-w0nAGBBp.js";
import { r as roleScopesAllow } from "./operator-scope-compat-iV7_Lmth.js";
import { n as resolveControlUiWebPushUrl, t as normalizeControlUiBasePath } from "./control-ui-shared-BiO6QP54.js";
import { n as loadGatewayTlsServerRuntime } from "./gateway-BtIixcdW.js";
import { r as capturePluginRegistryLifecycleEpoch, s as isPluginRegistryLifecycleEpochActive } from "./registry-lifecycle-BozndFXl.js";
import { a as listLoadedChannelPluginsForRegistry } from "./registry-loaded-Bh7xuMJh.js";
import { t as bindGatewayContextResolver } from "./gateway-request-scope-BCMYlsDI.js";
import "./src-BiL5aQto.js";
import { d as isCoreGatewayMethodClassified, l as STARTUP_UNAVAILABLE_GATEWAY_METHODS, m as listCoreGatewayMethodNames, u as createCoreGatewayMethodDescriptors } from "./method-scopes-K6J_UQGL.js";
import { m as setPreRestartDeferralCheck, p as setGatewaySigusr1RestartPolicy } from "./restart-DwCats8x.js";
import { a as getActiveGatewayRootWorkCount, c as getGatewaySuspendAdmissionPhase, f as isGatewayWorkAdmissionClosed, u as isGatewayRestartDraining, y as runOutsideGatewayRootWorkAdmission } from "./gateway-work-admission-R1IpuDim.js";
import { t as isBrowserCopilotClient } from "./message-channel-BQrhwUEA.js";
import { d as errorShape } from "./error-codes-Bo8q2D1o.js";
import { r as readGatewayRestartHandoffSync } from "./restart-handoff-V55GddB4.js";
import { t as AsyncWorkScope } from "./async-work-scope-CMQS2uTf.js";
import { i as tryLoadActivatedBundledPluginPublicSurfaceModule } from "./facade-runtime-BjmVS_9G.js";
import { a as enqueueSystemEvent, h as withSystemEventOwner } from "./system-events-C03jsM0j.js";
import { a as decodeSandboxHostCsp, i as buildSandboxHostProxyHtml, n as buildSandboxHostContentSecurityPolicy, s as resolveSandboxHostPort } from "./sandbox-host-BttkEpCR.js";
import { p as onSessionIdentityMutation } from "./session-history-eviction-C4srftLJ.js";
import { i as MAX_BUFFERED_BYTES, l as WS_COMPRESSION_THRESHOLD_BYTES, o as MAX_PREAUTH_PAYLOAD_BYTES } from "./server-constants-BrVEC7RW.js";
import { s as isTranscriptOnlyOpenClawAssistantMessage } from "./transcript-only-openclaw-assistant-CVgy4bjA.js";
import { c as resolveSystemMainSessionTarget } from "./main-session-Br0F9dzh.js";
import { C as getUserPreferences, c as resolveUserProfileId, h as listProfiles } from "./user-profiles-4AB7AmiH.js";
import { i as onUserProfilesChanged, s as readUserProfileVersion } from "./user-profiles-owner-DdMEsuGt.js";
import { m as stopDiagnosticHeartbeat, p as startDiagnosticHeartbeat } from "./diagnostic-ryAZMr5N.js";
import { f as resolveCronJobsStorePathFromConfig } from "./store-K9I-tbar.js";
import { a as isPackageProvenControlUiRootSync, c as resolveControlUiRootOverrideSync, i as isControlUiStartupAssetsReady, l as resolveControlUiRootSync, n as ensureControlUiAssetsBuilt } from "./control-ui-assets-Ia-uwq-Z.js";
import { s as getActiveBackgroundExecSessionCount } from "./bash-process-registry-DvUU9bL3.js";
import { i as resolveActiveEmbeddedRunSessionId, t as getActiveEmbeddedRunCount } from "./active-run-projections-C4mdt8WG.js";
import { t as getTotalPendingReplies } from "./dispatcher-registry-B2AzyUtN.js";
import { a as getActiveCronJobCount } from "./active-jobs-C_biqiZG.js";
import { s as getTotalQueueSize, u as isGatewayDraining } from "./command-queue-C3Fv2rcU.js";
import "./sessions-9nxpeTwt.js";
import { t as createAgentRuntimeApprovalAuthorityValidator } from "./agent-runtime-identity-token-DoLEaEV9.js";
import { i as retireQuestionChannelGateway } from "./question-channel-runtime-Ds-274X0.js";
import { a as CONTROL_UI_BUILD_ID_ATTRIBUTE, i as isControlUiPluginManagerRequest, n as isControlUiApprovalDocumentPath, r as isControlUiFocusDocumentPath, t as classifyControlUiRequest } from "./control-ui-routing-CjtUNaXH.js";
import { i as buildControlUiSessionPath } from "./src-DqwLld49.js";
import { n as fenceSessionSuspensionWritesForGatewayShutdown } from "./session-suspension-DlBl5K-b.js";
import { f as resolveOperatorSessionCreation, l as resolveOperatorRolePolicyForProfile, p as authenticatedProfileUnavailableError, t as authorizeGatewaySessionCreation } from "./operator-role-policy-wsr1DeJv.js";
import "./node-desktop-stream-BZM2AiRA.js";
import { S as resolveCurrentUserProfileDisplay } from "./session-transcript-readers-CYDRQsH5.js";
import { t as flattenMarkdownToPlainText } from "./markdown-plain-text-BIBtRgN0.js";
import { a as parseControlUiResourcePath, o as parseControlUiUserAvatarPath, s as resolveAssistantMediaRoutePath } from "./control-ui-contract-zYW4RcpK.js";
import { O as deriveSessionTitle } from "./session-utils-list-B0k8KJn5.js";
import { a as resolveAssistantAgentId } from "./assistant-avatar-B0tiesfb.js";
import { n as resolveRequestedSessionAgentId } from "./session-request-agent-CCRSEGCB.js";
import { i as withCoreCanvasNodeCapability, n as isCanvasDocumentHttpPath, r as resolveCanvasNodeCapability } from "./constants-Cm4bJJ1Q.js";
import { r as prepareGatewayAgentCliShim, t as clearGatewayAgentCliShim } from "./openclaw-cli-shim-DSn93Ndv.js";
import { a as getActiveSecretsRuntimeConfigSnapshot, r as clearSecretsRuntimeSnapshotState } from "./runtime-state-C4aJ8Hzz.js";
import { t as isCoreCanvasHostEnabled } from "./config-BdhaCXwT.js";
import { d as resumeGatewayRestartTraceFromHandoff, i as finishGatewayRestartTrace, n as collectGatewayProcessMemoryUsageMb, u as resumeGatewayRestartTraceFromEnv } from "./restart-trace-DlhJ9XxP.js";
import { i as upsertPresence } from "./system-presence-Y_GcutUC.js";
import { a as createSessionMessageSubscriberRegistry, i as createSessionEventSubscriberRegistry, r as createChatRunState } from "./server-chat-state-CwKZZaYd.js";
import { a as rethrowGatewayStartupError, i as resolveGatewayShutdownNotice, o as runGatewayShutdownSteps } from "./server-shutdown-Djoi94QR.js";
import { m as reusePendingNodePairingForReconnect, p as requestNodePairing, r as finalizeNodePairingCleanupClaim } from "./device-pairing-node-DiTk_P80.js";
import { c as removeRemoteNodeInfoForConnection, i as recordRemoteNodeInfo, s as removeRemoteNodeInfo } from "./remote-D-j1AEXQ.js";
import { t as createDefaultDeps } from "./deps-CSNtqiEp.js";
import { n as logRejectedLargePayload } from "./diagnostic-payload-B51qzY4j.js";
import { r as registerGatewayModelCatalogPrivateAccess } from "./server-model-catalog-auth-d5Ty5VGR.js";
import { c as normalizePluginNodeCapabilityScopedUrl, o as indexPluginNodeCapabilitySurfaces, p as resolvePluginNodeCapabilityTtlMs, u as reconcileClientPluginNodeCapabilities } from "./plugin-node-capability-CIHQ6nIw.js";
import { n as logWs, r as summarizeAgentEventForWsLog } from "./ws-log-Dkg9wKNU.js";
import { n as mergeGatewayAuthConfig, r as mergeGatewayTailscaleConfig } from "./startup-auth-D92BVWdq.js";
import { t as adoptPluginHttpRouteHandoffs } from "./http-registry-BDq8iS_8.js";
import { t as ensureOpenClawCliOnPath } from "./path-env-6L3Z8lTZ.js";
import { u as revokeAttachGrantsForSession } from "./mcp-grant-store-aCFS2cFj.js";
import { a as controlUiPluginAssetRoot, o as isProtectedPluginRoutePathFromContext, s as resolvePluginRoutePathContext, t as findMatchingPluginHttpRoutes } from "./route-match-BkI3BCZw.js";
import { a as sendGatewayAuthFailure, f as setDefaultSecurityHeaders, n as finishFailedGatewayHttpResponse, v as respondNotFound, y as respondPlainText } from "./http-common-BaZaosnr.js";
import { t as resolveSharedGatewaySessionGeneration } from "./ws-shared-generation-DZTjxt6J.js";
import { L as isSessionVisibilityAllowed, V as resolveSessionVisibility, i as prepareSessionSharing, n as createProfileSessionEntryFilter, t as canReceiveSessionEvent, z as resolveSessionSharingTarget } from "./session-sharing-B7MI8hNo.js";
import { r as createModelAccountConnectService } from "./model-account-connect-BqlZBB8r.js";
import { n as queuePluginSessionsChanged } from "./gateway-events-niGuJ0eS.js";
import { n as installActiveGitHubOAuthLifecycle, t as createGitHubOAuthLifecycle } from "./github-oauth-lifecycle-DMB5peav.js";
import { a as classifyMcpAppStandalonePath, c as classifyWorkerBootstrapArtifactTransferPath, i as classifyGatewayProbePath, l as classifyWorkerGatewayPath, o as classifyNodeWorkerBundleTransferPath, s as classifyNodeWorkspaceTransferPath } from "./gateway-http-route-contracts-jBtizxKU.js";
import { n as diffGatewayReloadPaths } from "./config-diff-D4CS9rJs.js";
import { r as listConfigReloadRefinementPrefixes, t as buildGatewayReloadPlan } from "./config-reload-plan-DUT3qlnP.js";
import { n as parseDevicePairingJoinRequestPath } from "./join-code-B_OfdZ-j.js";
import { n as DEFAULT_CHANNEL_STALE_EVENT_THRESHOLD_MS, r as evaluateChannelHealth, t as DEFAULT_CHANNEL_CONNECT_GRACE_MS } from "./channel-health-policy-D20awG2O.js";
import { n as createGatewayMethodRegistry, r as createPluginGatewayMethodDescriptors, t as createGatewayMethodDescriptorsFromHandlers } from "./registry-CC22si3g.js";
import { t as isTerminalConfigEnabled } from "./enabled-BSjeiWpO.js";
import { n as resolveGatewayReloadPluginActivationCandidate, r as resolveGatewayStartupPluginActivationConfig, t as mergeActivationSectionsIntoRuntimeConfig } from "./plugin-activation-runtime-config-BOxF5R5M.js";
import { A as resolveEffectiveWebPushPreferences, E as isWebPushQuietHours, M as webPushCategoryEnabled, O as normalizeWebPushDisplayLabel, T as WEB_PUSH_USER_PREFERENCES_KEY, j as webPushAgentAllowed, m as listBoundWebPushSubscriptions } from "./push-web-store-DllT5THN.js";
import { r as prepareWebPushNotificationSender } from "./push-web-Q11I0BtA.js";
import { n as webPushTargetClient, t as listCurrentWebPushTargets } from "./web-push-authority-ggw8QaCn.js";
import { a as enforceSharedGatewaySessionGenerationForConfigWrite, s as getRequiredSharedGatewaySessionGeneration } from "./server-shared-auth-generation-BzQsL2E3.js";
import { r as waitForMediaCleanupDrains, t as MEDIA_CLEANUP_STOP_TIMEOUT_MS } from "./server-media-cleanup-lifecycle-dOPPbnLL.js";
import { i as GATEWAY_EVENT_UPDATE_RUN_CHANGED, n as GATEWAY_EVENT_NODE_RUNNER_INVENTORY_CHANGED, t as GATEWAY_EVENT_DEVICE_PAIR_CHANGED } from "./events-DFzVQFw-.js";
import { t as createPresenceRecipientProjection } from "./presence-projection-BjIM8edy.js";
import { a as incrementPresenceVersion, i as getPresenceVersion, n as getHealthCache, o as refreshGatewayHealthSnapshot, r as getHealthVersion } from "./health-state-BQ5pY3AF.js";
import { t as resolveGatewayPluginConfig } from "./runtime-plugin-config-CKZ9Aehs.js";
import { t as createControlUiSessionPullRequestSubscriptions } from "./control-ui-session-pr-subscriptions-B_EFG2hk.js";
import { i as clearNodeWakeState } from "./node-wake-state-CWVR-GCk.js";
import { t as resolveGrantExpiryDaysConfig } from "./standing-grant-expiry-config-CqUe6ux8.js";
import { n as refreshConnectedNodeSurfaceCaches } from "./nodes.read-CKOVJ-gR.js";
import { t as broadcastPresenceSnapshot } from "./presence-events-DoThnKcO.js";
import { n as createGatewayChatMetadataLifecycle, t as broadcastChatMetadataChanged } from "./server-chat-metadata-lifecycle-C0eM0JlH.js";
import { n as applyGatewayLaneConcurrency, r as resolveGatewayLaneConcurrency, t as resolveHookClientIpConfig } from "./hook-client-ip-config-BRBMpW13.js";
import { t as createNoopHeartbeatRunner } from "./server-runtime-service-shared-iwns63ly.js";
import { t as recordClientPresenceActivity } from "./client-presence-BbKoA8kz.js";
import { t as assertGatewayRuntimeSecurityConfig } from "./server-runtime-config-B8g8sWUH.js";
import { i as disposeNodeConnectionNotifications, n as disconnectDisallowedGatewayBrowserOriginClients, o as retireDeviceTokenClients } from "./ws-origin-policy-DhLBST96.js";
import { r as createDesktopSessionRegistry } from "./session-registry-un13apYG.js";
import { t as createGatewayStartupTrace } from "./server-startup-trace-DfOgyIYF.js";
import { i as runWithGatewayHttpWorkAdmission, o as writeGatewayUpgradeServiceUnavailable, r as shouldEnforceGatewayAuthForPluginPath, t as isPluginAuthenticatedRoutePath } from "./route-auth-Bp_S_Wob.js";
import { n as handleNodeWorkerBundleTransferHttpRequest } from "./node-worker-bundle-transfer-http-BP0-lXgy.js";
import { n as handleNodeWorkspaceTransferHttpRequest } from "./node-workspace-transfer-http-DTKaOjMs.js";
import { n as handleWorkerBootstrapArtifactTransferHttpRequest } from "./worker-bootstrap-artifact-transfer-http-DjUAf6hp.js";
import { i as markPublicWorkerIngress, t as GATEWAY_WS_CONNECTION_KIND_PROPERTY } from "./ws-types-099MIJxx.js";
import { t as GATEWAY_EVENTS } from "./server-methods-list-BTWMuzF7.js";
import "./session-limits-BqiIRDwa.js";
import { t as beginMacOSSystemCaWarmupOnce } from "./system-ca-warmup-dlHipq-P.js";
import { createRequire } from "node:module";
import fs, { constants } from "node:fs";
import path from "node:path";
import fs$1 from "node:fs/promises";
import net from "node:net";
import { createHash, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
import { createHistogram, performance } from "node:perf_hooks";
import { createServer as createServer$1, request } from "node:http";
import { createServer as createServer$2 } from "node:https";
//#region src/gateway/channel-thaw-restart.ts
function snapshotRunningTargets(manager) {
return Object.entries(manager.getRuntimeSnapshot().channelAccounts).flatMap(([channelId, accounts]) => Object.entries(accounts ?? {}).filter(([, status]) => status?.running === true).map(([accountId]) => ({
channelId,
accountId
})));
}
function dedupeTargets(targets) {
const seen = /* @__PURE__ */ new Set();
return targets.filter((target) => {
const key = `${target.channelId}:${target.accountId}`;
if (seen.has(key)) return false;
seen.add(key);
return true;
});
}
/**
* Restarts every running, non-manually-stopped channel account after a host
* thaw. Dead sockets from a freeze otherwise wait for the slow health sweep.
*/
async function restartRunningChannelAccounts(manager, opts, selection = { kind: "new-thaw" }) {
const targets = selection.kind === "new-thaw" ? dedupeTargets([...selection.pendingTargets ?? [], ...snapshotRunningTargets(manager)]) : [...selection.targets];
const failedTargets = [];
for (const [index, target] of targets.entries()) {
const { channelId, accountId } = target;
if (manager.isManuallyStopped(channelId, accountId)) continue;
if (!opts.shouldContinue()) return [...failedTargets, ...targets.slice(index)];
try {
let current = manager.getRuntimeSnapshot().channelAccounts[channelId]?.[accountId];
if (!current) continue;
await manager.stopChannel(channelId, accountId, { manual: false });
if (!opts.shouldContinue()) return [
...failedTargets,
target,
...targets.slice(index + 1)
];
current = manager.getRuntimeSnapshot().channelAccounts[channelId]?.[accountId];
if (!current) continue;
let startOutcomes = await manager.startChannel(channelId, accountId, { preserveManualStop: true });
let startOutcome = startOutcomes.get(accountId);
let restarted = manager.getRuntimeSnapshot().channelAccounts[channelId]?.[accountId];
if (startOutcome?.status === "retry" && restarted?.restartPending === true) {
startOutcomes = await manager.startChannel(channelId, accountId, { preserveManualStop: true });
startOutcome = startOutcomes.get(accountId);
restarted = manager.getRuntimeSnapshot().channelAccounts[channelId]?.[accountId];
}
if (startOutcome?.status === "retry") {
failedTargets.push(target);
opts.onError(`[${channelId}:${accountId}] host-thaw restart failed: replacement was not handed off (${startOutcome.reason})${restarted?.lastError ? `: ${restarted.lastError}` : ""}`);
}
} catch (error) {
failedTargets.push(target);
opts.onError(`[${channelId}:${accountId}] host-thaw restart failed: ${String(error)}`);
}
if (!opts.shouldContinue()) return [...failedTargets, ...targets.slice(index + 1)];
}
return failedTargets;
}
//#endregion
//#region src/gateway/server/plugins-http/route-capability.ts
function hasNodeCapabilityRoute(route) {
return Boolean(route.nodeCapability?.surface?.trim());
}
function resolvePluginNodeCapabilityRouteSurface(route) {
const surface = route.nodeCapability.surface.trim();
const owner = route.pluginId?.trim() || route.source?.trim();
return {
...route.nodeCapability,
surface,
...owner ? { scopeKey: `${owner}:${surface}` } : {}
};
}
/** Lists all node-capability routes matching the already canonicalized path context. */
function findMatchingPluginNodeCapabilityRoutes(registry, context) {
return findMatchingPluginHttpRoutes(registry, context).filter(hasNodeCapabilityRoute).map((route) => Object.assign({}, route, { nodeCapability: resolvePluginNodeCapabilityRouteSurface(route) }));
}
/** Returns the highest-priority node-capability route for a plugin HTTP path. */
function findMatchingPluginNodeCapabilityRoute(registry, context) {
return findMatchingPluginNodeCapabilityRoutes(registry, context)[0];
}
/** Lists unique node-capability surfaces, preferring the shortest TTL per surface. */
function listPluginNodeCapabilities(registry) {
const surfaces = /* @__PURE__ */ new Map();
for (const route of registry.httpRoutes ?? []) {
const surface = route.nodeCapability?.surface?.trim();
if (surface) {
const next = resolvePluginNodeCapabilityRouteSurface(route);
const existing = surfaces.get(surface);
if (!existing || resolveTtlMs(next) < resolveTtlMs(existing)) surfaces.set(surface, next);
}
}
return [...surfaces.values()].toSorted((a, b) => a.surface.localeCompare(b.surface));
}
function resolveTtlMs(surface) {
return resolvePluginNodeCapabilityTtlMs(surface);
}
//#endregion
//#region src/gateway/server-core-runtime.ts
function approvalRequestTargetsSession(request, sessionKeys, sessionId) {
if (typeof request !== "object" || request === null) return false;
const record = request;
return typeof record.sessionId === "string" && record.sessionId === sessionId || typeof record.sessionKey === "string" && sessionKeys.has(record.sessionKey);
}
async function startGatewayCoreRuntime(input) {
const { lifecycleRuntime: runtime, port, log, logDiscovery, logHealth, logChannels, loadGatewayStartupEarlyModule, loadGatewayPluginBootstrapModule, loadGatewayModelCatalog, loadGatewayModelCatalogSnapshot, readPreparedGatewayModelCatalog } = input;
const { minimalTestGateway, cfgAtStart, gatewayTls, bindHost, tailscaleMode, nodeRegistry, pluginRuntime, broadcast, nodeSendToAllSubscribed, refreshGatewayHealthSnapshotWithRuntime, dedupe, chatAbortControllers, chatQueuedTurns, restartRecoveryCandidates, chatRunState, removeChatRun, agentRunSeq, nodeSendToSession, runtimeState, kernel, startupTrace, channelManager, readinessEventLoopHealth, workerDispatchAuthority, clients, sharedGatewaySessionGenerationState, resolveSharedGatewaySessionGenerationForConfig, sessionMessageSubscribers, sessionEventSubscribers, toolEventRecipients, broadcastToConnIds, terminalSessions, controlUiBasePath, workerEnvironmentService, workerPlacementDispatchAvailable, workerPlacementControlAvailable, workerDesktopObserveAvailable, desktopSessionRegistry, listStartupChannelGatewayMethods, coreGatewayMethodNames, pluginHostServices, baseMethods, pluginWorkspaceDir, ambientEnvTriggers, resolvePluginGatewayContext, workerEnvironmentStartup, broadcastPluginEvent, activateRuntimeSecrets } = runtime;
const pluginMetadataSnapshot = runtime.pluginMetadataSnapshot;
kernel.addGatewayLifetimeSidecar({ stop: () => desktopSessionRegistry.stopAll() });
const secretEgressProxy = cfgAtStart.secrets?.egressProxy?.enabled === true ? await import("./runtime-91f5fVLk.js").then((egressRuntime) => egressRuntime.startGatewaySecretEgressProxy({
...cfgAtStart.secrets?.egressProxy?.allowedHosts !== void 0 ? { allowedHosts: cfgAtStart.secrets.egressProxy.allowedHosts } : {},
...cfgAtStart.secrets?.egressProxy?.bypassHosts ? { bypassHosts: cfgAtStart.secrets.egressProxy.bypassHosts } : {}
})) : void 0;
if (secretEgressProxy) kernel.addGatewayLifetimeSidecar(secretEgressProxy);
let pendingThawRestartTargets;
let earlyRuntimePromise;
const startEarlyRuntime = () => earlyRuntimePromise ??= startupTrace.measure("runtime.early", () => loadGatewayStartupEarlyModule().then(({ startGatewayEarlyRuntime }) => startGatewayEarlyRuntime({
minimalTestGateway,
cfgAtStart,
port,
gatewayTls,
gatewayDirectReachable: !isLoopbackHost(bindHost),
tailscaleMode,
log,
logDiscovery,
nodeRegistry,
swapDiscovery: kernel.swapDiscovery,
pluginRegistry: pluginRuntime.registry,
pluginRuntimeClaim: kernel.pluginRuntimeGeneration.currentClaim(),
broadcast,
nodeSendToAllSubscribed,
getPresenceVersion,
getHealthVersion,
refreshGatewayHealthSnapshot: refreshGatewayHealthSnapshotWithRuntime,
restartRunningChannels: async (mode, shouldContinue = () => !isGatewayWorkAdmissionClosed()) => {
const failedTargets = await restartRunningChannelAccounts(channelManager, {
shouldContinue,
onError: (message) => logHealth.error(message)
}, mode === "new-thaw" || pendingThawRestartTargets === void 0 ? {
kind: "new-thaw",
pendingTargets: pendingThawRestartTargets
} : {
kind: "deferred-retry",
targets: pendingThawRestartTargets
});
pendingThawRestartTargets = failedTargets.length > 0 ? failedTargets : void 0;
return failedTargets.length === 0;
},
refreshPresence: () => broadcastPresenceSnapshot({
broadcast,
incrementPresenceVersion,
getHealthVersion
}),
resetEventLoopHealth: readinessEventLoopHealth.reset,
logHealth,
dedupe,
chatAbortControllers,
chatQueuedTurns,
restartRecoveryCandidates,
chatRunState,
removeChatRun,
agentRunSeq,
nodeSendToSession,
skillsRefreshDelayMs: runtimeState.skillsRefreshDelayMs,
getSkillsRefreshTimer: () => runtimeState.skillsRefreshTimer,
setSkillsRefreshTimer: (timer) => {
runtimeState.skillsRefreshTimer = timer;
},
getRuntimeConfig,
startupTrace
}))).then((earlyRuntime) => {
kernel.setEarlyRuntimeHandles(earlyRuntime);
return earlyRuntime;
});
const [{ startGatewayEventSubscriptions }, { startGatewayChannelHealthMonitor }] = await startupTrace.measure("runtime.post-early-imports", () => Promise.all([import("./server-runtime-subscriptions-DthvEO6_.js"), import("./server-runtime-startup-services-DXv1F8dE.js")]));
const { sessionCompanion, sessionObserver, ...runtimeSubscriptionUnsubs } = await startupTrace.measure("runtime.subscriptions", () => startGatewayEventSubscriptions({
log,
broadcast,
broadcastToConnIds,
nodeSendToSession,
agentRunSeq,
chatRunState,
toolEventRecipients,
sessionEventSubscribers,
sessionMessageSubscribers,
chatAbortControllers,
restartRecoveryCandidates,
terminalSessions
}));
Object.assign(runtimeState, runtimeSubscriptionUnsubs);
await startupTrace.measure("runtime.services", () => kernel.setChannelHealthMonitor(startGatewayChannelHealthMonitor({ channelManager })));
const { createOperatorApprovalSessionEventRuntime } = await import("./operator-approval-session-events-BZbWGVT2.js");
const approvalManagersForReplay = /* @__PURE__ */ new Map();
const approvalSessionEvents = createOperatorApprovalSessionEventRuntime({
clients,
sessionMessageSubscribers,
broadcastToConnIds,
controlUiBasePath,
reconcileTerminal: (record) => {
return approvalManagersForReplay.get(record.kind)?.reconcileDurableTerminal(record) ?? false;
}
});
const validateAgentRuntimeApprovalAuthority = createAgentRuntimeApprovalAuthorityValidator(workerEnvironmentStartup?.placementStore);
const { execApprovalManager, questionManager, cancelRunBoundApprovals, forwardPluginApprovalRequest, approvalWebPushDelivery, pluginApprovalIosPushDelivery, pluginApprovalManager, placementStandingGrants, systemAgentApprovalManager, bindApprovalPublicationContext, beginCloseApprovalObservers, stopOperatorInteractions, extraHandlers, coreGatewayHandlers } = await startupTrace.measure("gateway.handlers", async () => {
const [{ createGatewayAuxHandlers }, { coreGatewayHandlers: coreGatewayHandlersLocal }] = await Promise.all([import("./server-aux-handlers-DdYlMVVn.js"), import("./server-methods-CRX3McIT.js")]);
return {
...createGatewayAuxHandlers({
log,
chatAbortControllers,
hasRunAbortMarker: (runId) => chatRunState.hasAbortMarker(runId),
resolveGrantDefaultExpiresAtMs: (nowMs) => {
const days = resolveGrantExpiryDaysConfig(getRuntimeConfig());
return days !== null ? nowMs + days * 864e5 : null;
},
activateRuntimeSecrets,
sharedGatewaySessionGenerationState,
resolveSharedGatewaySessionGenerationForConfig,
clients,
channelManager,
getChannelAutostartSuppression: channelManager.getAutostartSuppression,
logChannels,
registerWorkerTurnClaimClosedHandler: workerEnvironmentStartup?.placementStore ? (handler) => workerEnvironmentStartup.placementStore.registerTurnClaimClosedHandler(handler) : void 0,
validateAgentRuntimeDelegatedAuthority: (authority) => validateAgentRuntimeApprovalAuthority({
kind: "agentRuntime",
agentId: "approval-manager",
sessionKey: "approval-manager",
operationalRunInstance: authority.operationalRunInstance,
delegatedAuthority: authority
}),
onApprovalLifecycle: approvalSessionEvents.publish,
onAgentRunAuthorityClosed: (authority) => {
secretEgressProxy?.revokeRun(authority.operationalRunInstance);
}
}),
coreGatewayHandlers: coreGatewayHandlersLocal
};
});
const requestLifetime = runtime.connectionWork.signal;
requestLifetime.addEventListener("abort", beginCloseApprovalObservers, { once: true });
if (requestLifetime.aborted) beginCloseApprovalObservers();
kernel.addGatewayLifetimeSidecar({ stop: async () => {
requestLifetime.removeEventListener("abort", beginCloseApprovalObservers);
await stopOperatorInteractions();
} });
approvalManagersForReplay.set("exec", execApprovalManager);
approvalManagersForReplay.set("plugin", pluginApprovalManager);
approvalManagersForReplay.set("system-agent", systemAgentApprovalManager);
workerDispatchAuthority.revoke = ({ sessionId, sessionKeys }) => {
const keys = new Set(sessionKeys);
for (const sessionKey of keys) revokeAttachGrantsForSession(sessionKey);
const fenceResolver = {
kind: "system",
id: "worker-dispatch"
};
for (const manager of [execApprovalManager, pluginApprovalManager]) for (const record of manager.listPendingRecords()) if (approvalRequestTargetsSession(record.request, keys, sessionId)) manager.forceDenyDetailed(record.id, "run-aborted", fenceResolver, "cancelled");
};
const attachedGatewayExtraHandlers = {
...pluginRuntime.registry.gatewayHandlers,
...extraHandlers
};
let attachedPluginGatewayHandlerKeys = new Set(Object.keys(pluginRuntime.registry.gatewayHandlers));
const buildAttachedGatewayMethodRegistry = (nextPluginRegistry) => {
const coreDescriptorHandlers = { ...coreGatewayHandlers };
const auxHandlers = {};
for (const [method, handler] of Object.entries(extraHandlers)) if (isCoreGatewayMethodClassified(method)) coreDescriptorHandlers[method] = handler;
else auxHandlers[method] = handler;
const coreDescriptors = createCoreGatewayMethodDescriptors(coreDescriptorHandlers).filter((descriptor) => (workerEnvironmentService || descriptor.name !== "environments.create" && descriptor.name !== "environments.destroy") && (workerPlacementDispatchAvailable || descriptor.name !== "sessions.dispatch") && (workerPlacementControlAvailable || descriptor.name !== "sessions.reclaim" && descriptor.name !== "sessions.move") && (workerDesktopObserveAvailable || descriptor.name !== "desktop.launch" && descriptor.name !== "worker.desktop.observe" && descriptor.name !== "worker.desktop.launch"));
return createGatewayMethodRegistry([
...coreDescriptors,
...createPluginGatewayMethodDescriptors(nextPluginRegistry),
...createGatewayMethodDescriptorsFromHandlers({
handlers: auxHandlers,
owner: {
kind: "aux",
area: "gateway-extra"
},
defaultScope: ADMIN_SCOPE
})
], nextPluginRegistry);
};
let attachedGatewayMethodRegistry = buildAttachedGatewayMethodRegistry(pluginRuntime.registry);
let retireAttachedPluginRuntimeBindings = () => {};
kernel.addGatewayLifetimeSidecar({ stop: async () => retireAttachedPluginRuntimeBindings() });
const listAttachedGatewayMethods = () => {
const methods = attachedGatewayMethodRegistry.listAdvertisedMethods();
methods.push(...listStartupChannelGatewayMethods());
return uniqueStrings(methods);
};
kernel.publishMethodSurface(listAttachedGatewayMethods());
const getPluginNodeCapabilities = () => withCoreCanvasNodeCapability(listPluginNodeCapabilities(pluginRuntime.registry), isCoreCanvasHostEnabled(getRuntimeConfig()));
const replaceAttachedPluginRuntime = (loaded) => {
adoptPluginHttpRouteHandoffs(pluginRuntime.registry, loaded.pluginRegistry);
const retirePreviousBindings = retireAttachedPluginRuntimeBindings;
retireAttachedPluginRuntimeBindings = loaded.retireGatewayRuntimeBindings ?? (() => {});
retirePreviousBindings();
pluginRuntime.registry = loaded.pluginRegistry;
pluginRuntime.baseGatewayMethods = loaded.gatewayMethods;
for (const key of attachedPluginGatewayHandlerKeys) delete attachedGatewayExtraHandlers[key];
Object.assign(attachedGatewayExtraHandlers, pluginRuntime.registry.gatewayHandlers);
attachedPluginGatewayHandlerKeys = new Set(Object.keys(pluginRuntime.registry.gatewayHandlers));
attachedGatewayMethodRegistry = buildAttachedGatewayMethodRegistry(pluginRuntime.registry);
kernel.publishMethodSurface(listAttachedGatewayMethods());
nodeRegistry.refreshRuntimePolicy();
const surfaces = indexPluginNodeCapabilitySurfaces(getPluginNodeCapabilities());
for (const client of clients) reconcileClientPluginNodeCapabilities(client, surfaces);
};
const refreshAttachedGatewayDiscovery = async (nextPluginRegistry, claim) => {
if (minimalTestGateway) return;
try {
if (!await claim.waitForUnblocked()) return;
await runtimeState.discovery?.update({ gatewayDiscoveryServices: nextPluginRegistry.gatewayDiscoveryServices }, claim);
} catch (err) {
logDiscovery.warn(`gateway discovery refresh failed after plugin load: ${String(err)}`);
}
};
const reloadAttachedGatewayPlugins = async (params) => {
const [{ loadPluginLookUpTable }, { listAmbientOnlyConfiguredChannelIds }, { prepareGatewayPluginLoad }, { startPluginServices, PLUGIN_SERVICE_REPLACEMENT_STOP_TIMEOUT_MS }] = await Promise.all([
import("./plugin-lookup-table-D5daxN5m.js"),
import("./channel-presence-policy-DnjyRWee.js"),
loadGatewayPluginBootstrapModule(),
import("./services-wg5DeUW5.js")
]);
const cancelledReload = (activeChannels) => ({
activeChannels: new Set(activeChannels),
cancelled: true
});
const listAttachedChannelIds = () => new Set(listLoadedChannelPluginsForRegistry(pluginRuntime.registry).map((plugin) => plugin.id));
const beforeChannelIds = listAttachedChannelIds();
const nextPluginLookUpTable = loadPluginLookUpTable({
config: resolveGatewayStartupPluginActivationConfig({
runtimeConfig: params.nextConfig,
activationSourceConfig: params.sourceConfig,
env: params.env,
manifestRegistry: pluginMetadataSnapshot?.manifestRegistry,
discovery: pluginMetadataSnapshot?.discovery,
ambientEnvTriggers
}),
workspaceDir: pluginWorkspaceDir,
env: params.env,
activationSourceConfig: params.sourceConfig,
metadataSnapshot: pluginMetadataSnapshot,
workerProviderIds: workerEnvironmentStartup?.listDurableProviderIds() ?? [],
ambientEnvTriggers
});
const nextAmbientAutostartSuppressedChannelIds = ambientEnvTriggers === "suppress" ? new Set(listAmbientOnlyConfiguredChannelIds({
config: params.nextConfig,
activationSourceConfig: params.sourceConfig,
env: params.env,
includePersistedAuthState: false,
manifestRecords: nextPluginLookUpTable.manifestRegistry.plugins
})) : /* @__PURE__ */ new Set();
const pluginRuntimeGeneration = kernel.pluginRuntimeGeneration;
const replacement = pluginRuntimeGeneration.reserve();
const releaseChannelStarts = channelManager.pauseChannelStarts();
let restoreChannelStarts = true;
let recoverFromReplacementTeardown;
try {
await params.beforeReplace(beforeChannelIds);
if (params.isAborted?.()) {
replacement.reject();
return cancelledReload(beforeChannelIds);
}
const previousServices = pluginRuntimeGeneration.currentServices();
if (previousServices) {
recoverFromReplacementTeardown = params.onReplacementTeardownFailure;
restoreChannelStarts = false;
replacement.retirePrevious();
await previousServices.stop({
strict: true,
deadlineAtMs: Date.now() + PLUGIN_SERVICE_REPLACEMENT_STOP_TIMEOUT_MS
});
if (params.isAborted?.()) throw new Error("Gateway plugin runtime replacement was superseded after service teardown");
}
await params.commitRuntime(() => {
restoreChannelStarts = false;
replacement.commit();
pluginRuntimeGeneration.publishServices(replacement.claim, null);
recoverFromReplacementTeardown = void 0;
});
if (!await replacement.claim.waitForUnblocked()) return cancelledReload(beforeChannelIds);
let loaded;
if (!replacement.claim.publish(() => {
channelManager.setAmbientAutostartSuppressedChannelIds(nextAmbientAutostartSuppressedChannelIds);
loaded = prepareGatewayPluginLoad({
cfg: params.nextConfig,
activationSourceConfig: params.sourceConfig,
workspaceDir: pluginWorkspaceDir,
log,
coreGatewayMethodNames,
hostServices: pluginHostServices,
baseMethods,
pluginLookUpTable: nextPluginLookUpTable,
pluginMetadataSnapshot,
ambientEnvTriggers,
resolveGatewayContext: resolvePluginGatewayContext
});
replaceAttachedPluginRuntime(loaded);
releaseChannelStarts("published");
}) || !loaded) return cancelledReload(listAttachedChannelIds());
await refreshAttachedGatewayDiscovery(loaded.pluginRegistry, replacement.claim);
if (!await replacement.claim.waitForUnblocked()) return cancelledReload(listAttachedChannelIds());
const nextServices = await startPluginServices({
registry: loaded.pluginRegistry,
config: params.nextConfig,
workspaceDir: pluginWorkspaceDir,
broadcastPluginEvent,
getCronService: () => runtimeState.cronState.cron,
onHandle: (handle) => pluginRuntimeGeneration.publishServices(replacement.claim, handle)
});
if (!await replacement.claim.waitForUnblocked() || !pluginRuntimeGeneration.publishServices(replacement.claim, nextServices)) await nextServices.stop({
strict: true,
deadlineAtMs: Date.now() + PLUGIN_SERVICE_REPLACEMENT_STOP_TIMEOUT_MS
});
} catch (error) {
replacement.reject();
recoverFromReplacementTeardown?.(error);
throw error;
} finally {
if (restoreChannelStarts) releaseChannelStarts("rollback");
}
return { activeChannels: listAttachedChannelIds() };
};
return {
...runtime,
kernel: {
...kernel,
reloadPlugins: reloadAttachedGatewayPlugins
},
startEarlyRuntime,
sessionCompanion,
sessionObserver,
approvalSessionEvents,
execApprovalManager,
questionManager,
cancelRunBoundApprovals,
forwardPluginApprovalRequest,
approvalWebPushDelivery,
pluginApprovalIosPushDelivery,
pluginApprovalManager,
placementStandingGrants,
systemAgentApprovalManager,
bindApprovalPublicationContext,
validateAgentRuntimeApprovalAuthority,
attachedGatewayExtraHandlers,
getAttachedGatewayMethodRegistry: () => attachedGatewayMethodRegistry,
getPluginNodeCapabilities,
replaceAttachedPluginRuntime,
refreshAttachedGatewayDiscovery,
loadGatewayModelCatalog,
loadGatewayModelCatalogSnapshot,
readPreparedGatewayModelCatalog,
getPluginMetadataSnapshot: () => pluginMetadataSnapshot
};
}
//#endregion
//#region src/gateway/server-lifetime-sidecars.ts
const SECRET_STORE_EXPIRY_INTERVAL_MS = 6e4;
const GITHUB_PUBLICATION_RECONCILE_INTERVAL_MS = 6e4;
function startGitHubPublicationMaintenance(reconcile, logWarning) {
let current;
let stopped = false;
const run = () => {
if (stopped || current) return;
const operation = reconcile().catch(() => logWarning("GitHub publication recovery failed; will retry.")).finally(() => {
if (current === operation) current = void 0;
});
current = operation;
};
run();
const interval = setInterval(run, GITHUB_PUBLICATION_RECONCILE_INTERVAL_MS);
interval.unref?.();
return { stop: async () => {
stopped = true;
clearInterval(interval);
await current;
} };
}
function startSecretStoreExpiryMaintenance(logWarning) {
let warned = false;
const purge = () => {
try {
purgeExpiredSecretStoreEntries();
warned = false;
} catch {
if (!warned) {
logWarning("Secret store expiry cleanup failed; will retry.");
warned = true;
}
}
};
purge();
const interval = setInterval(purge, SECRET_STORE_EXPIRY_INTERVAL_MS);
interval.unref?.();
return { stop: () => clearInterval(interval) };
}
async function attachInitialGatewayLifetimeSidecars(params) {
await params.chatMetadataLifecycle.attachContext(params.gatewayRequestContext, params.sidecars);
const modelAccountConnect = createModelAccountConnectService({
getConfig: params.gatewayRequestContext.getRuntimeConfig,
onChanged: () => broadcastChatMetadataChanged(params.gatewayRequestContext)
});
params.gatewayRequestContext.modelAccountConnectService = modelAccountConnect;
params.sidecars.push({ stop: async () => {
await modelAccountConnect.stop();
if (params.gatewayRequestContext.modelAccountConnectService === modelAccountConnect) delete params.gatewayRequestContext.modelAccountConnectService;
} });
const githubOAuth = createGitHubOAuthLifecycle({
getConfig: params.gatewayRequestContext.getRuntimeConfig,
getPersistedConfig: () => getRuntimeConfig({ pin: false }),
warn: params.logWarning
});
params.gatewayRequestContext.githubOAuthService = githubOAuth;
const uninstallGitHubOAuth = installActiveGitHubOAuthLifecycle(githubOAuth);
if (!params.minimalTestGateway) githubOAuth.start();
params.sidecars.push({ stop: async () => {
uninstallGitHubOAuth();
await githubOAuth.stop();
if (params.gatewayRequestContext.githubOAuthService === githubOAuth) delete params.gatewayRequestContext.githubOAuthService;
} });
if (!params.minimalTestGateway) params.sidecars.push(startSecretStoreExpiryMaintenance(params.logWarning));
if (params.reconcileGitHubPublications) params.sidecars.push(startGitHubPublicationMaintenance(params.reconcileGitHubPublications, params.logWarning));
params.sidecars.push({ stop: () => {
params.flushPendingSessionsChangedEvents(params.gatewayRequestContext);
} });
}
//#endregion
//#region src/gateway/server-kernel-request-runtime.ts
/** Completes the socket-free request and internal-dispatch half of Gateway startup. */
async function prepareGatewayKernelRequestRuntime(params) {
const { coreRuntime: runtime, log, logHealth } = params;
const { minimalTestGateway, deps, runtimeState, unavailableGatewayMethods, sessionCompanion, sessionObserver, mentionInbox, getMcpAppSandboxPort, ensureSandboxHostPort, getPortalService, terminalLaunchPolicy, execApprovalManager, questionManager, cancelRunBoundApprovals, forwardPluginApprovalRequest, approvalWebPushDelivery, pluginApprovalIosPushDelivery, pluginApprovalManager, placementStandingGrants, systemAgentApprovalManager, bindApprovalPublicationContext, validateAgentRuntimeApprovalAuthority, approvalSessionEvents, startupTrace, loadGatewayModelCatalog, loadGatewayModelCatalogSnapshot, readPreparedGatewayModelCatalog, refreshGatewayHealthSnapshotWithRuntime, getRuntimeSnapshot, broadcast, broadcastToConnIds, nodeSendToSession, nodeSendToAllSubscribed, nodeSubscribe, nodeUnsubscribe, nodeUnsubscribeAll, hasTalkNodeConnected, clients, isConnectionActive, watchNodeHttpRuntime, sharedGatewaySessionGenerationState, resolveSharedGatewaySessionGenerationForRuntimeSnapshot, nodeRegistry, nodeDesktopService, workerEnvironmentService, hostDesktopService, workerEnvironmentStartup, workerPlacementRuntime, workerPlacementControlAvailable, githubPublicationRuntime, githubPublicationService, terminalSessions, agentRunSeq, chatAbortControllers, chatQueuedTurns, chatRunState, addChatRun, removeChatRun, subscribeSessionMessageEvents, unsubscribeSessionMessageEvents, sessionEventSubscribers, sessionMessageSubscribers, toolEventRecipients, dedupe, wizardSessions, systemAgentSessions, findRunningWizard, purgeWizardSession, readinessEventLoopHealth, startChannel, stopChannel, markChannelLoggedOut, wizardRunner, channelWizardRunner, broadcastVoiceWakeChanged, broadcastVoiceWakeRoutingChanged, pluginGatewayContext, getAttachedGatewayMethodRegistry, gatewayInstanceRuntimeRef, gatewayTls, lifecycle, startupState, kernel, shutdownRuntime } = runtime;
const chatMetadataLifecycle = await createGatewayChatMetadataLifecycle({
getConfig: getRuntimeConfig,
minimalTestGateway,
log
});
const configRevisionProjector = await startupTrace.measure("gateway.config-revision-key", async () => {
const { loadGatewayConfigRevisionProjector } = await import("./config-revision-token-Cj_YN6eC.js");
return loadGatewayConfigRevisionProjector({ env: process.env });
});
const gatewayRequestContext = await startupTrace.measure("gateway.request-context", async () => {
const { createGatewayRequestContext } = await import("./server-request-context-BW-f3z4W.js");
return createGatewayRequestContext({
trackExecution: (run) => runtime.connectionWork.track(run),
deps,
configRevisionProjector,
runtimeState,
sessionCompanion,
getRuntimeConfig,
isConfigReloadSettled: () => !lifecycle.closePreludeStarted && runtimeState.configReloader.isConfigReloadSettled(),
getGatewayMethodRegistry: getAttachedGatewayMethodRegistry,
gatewayTlsFingerprint: gatewayTls.enabled ? gatewayTls.fingerprintSha256 : void 0,
sessionObserver,
mentionInbox,
getMcpAppSandboxPort,
ensureSandboxHostPort,
getPortalService,
resolveTerminalLaunchPolicy: terminalLaunchPolicy.resolve,
isTerminalEnabled: terminalLaunchPolicy.isEnabled,
execApprovalManager,
questionManager,
cancelRunBoundApprovals,
forwardPluginApprovalRequest,
approvalWebPushDelivery,
pluginApprovalIosPushDelivery,
pluginApprovalManager,
placementStandingGrants,
systemAgentApprovalManager,
listSessionPendingApprovals: approvalSessionEvents.replay,
loadGatewayModelCatalog,
loadGatewayModelCatalogSnapshot,
readPreparedGatewayModelCatalog,
readChatMetadata: chatMetadataLifecycle.read,
readChatStartupProjection: chatMetadataLifecycle.readStartup,
getHealthCache,
refreshHealthSnapshot: refreshGatewayHealthSnapshotWithRuntime,
logHealth,
logGateway: log,
incrementPresenceVersion,
getHealthVersion,
broadcast,
broadcastToConnIds,
nodeSendToSession,
nodeSendToAllSubscribed,
nodeSubscribe,
nodeUnsubscribe,
nodeUnsubscribeAll,
hasConnectedTalkNode: hasTalkNodeConnected,
clients,
isConnectionActive,
invalidateDeviceTransports: watchNodeHttpRuntime.invalidateSessionsForDevice,
disconnectDeviceTransports: watchNodeHttpRuntime.disconnectSessionsForDevice,
enforceSharedGatewayAuthGenerationForConfigWrite: (nextConfig) => {
enforceSharedGatewaySessionGenerationForConfigWrite({
state: sharedGatewaySessionGenerationState,
nextConfig,
resolveRuntimeSnapshotGeneration: resolveSharedGatewaySessionGenerationForRuntimeSnapshot,
clients
});
},
nodeRegistry,
...nodeDesktopService ? { nodeDesktopService } : {},
...workerEnvironmentService ? { workerEnvironmentService } : {},
...hostDesktopService ? { hostDesktopService } : {},
...workerEnvironmentStartup ? { workerSessionPlacementService: workerEnvironmentStartup.placementStore } : {},
...workerPlacementRuntime ? {
workerPlacementDiskSpaceReader: workerPlacementRuntime.diskSpace,
workerPlacementRunnerAvailabilityReader: workerPlacementRuntime.runnerAvailability
} : {},
...workerPlacementControlAvailable ? { workerPlacementDispatchService: workerPlacementControlAvailable } : {},
...githubPublicationService ? { githubPublicationService } : {},
validateAgentRuntimeApprovalAuthority,
terminalSessions,
agentRunSeq,
chatAbortControllers,
chatQueuedTurns,
chatRunState,
addChatRun,
removeChatRun,
subscribeSessionEvents: sessionEventSubscribers.subscribe,
unsubscribeSessionEvents: sessionEventSubscribers.unsubscribe,
subscribeSessionMessageEvents,
unsubscribeSessionMessageEvents,
unsubscribeAllSessionEvents: (connId) => {
sessionEventSubscribers.unsubscribe(connId);
sessionMessageSubscribers.unsubscribeAll(connId);
sessionObserver.removeConnection(connId);
},
getSessionEventSubscriberConnIds: sessionEventSubscribers.getAll,
registerToolEventRecipient: toolEventRecipients.add,
dedupe,
wizardSessions,
systemAgentSessions,
findRunningWizard,
purgeWizardSession,
getRuntimeSnapshot,
getEventLoopHealth: readinessEventLoopHealth.snapshot,
startChannel,
stopChannel,
markChannelLoggedOut,
wizardRunner,
channelWizardRunner,
broadcastVoiceWakeChanged,
unavailableGatewayMethods,
broadcastVoiceWakeRoutingChanged,
notifyPluginMetadataChanged: kernel.notifyPluginMetadataChanged,
getConfigReloaderHotReloadStatus: kernel.getConfigReloaderHotReloadStatus
});
});
kernel.addGatewayLifetimeSidecar({ stop: async () => {
retireQuestionChannelGateway(runtime.connectionWork.signal);
await gatewayRequestContext.scopeUpgradeCoordinator?.close();
} });
gatewayRequestContext.requestEntryLifetime = runtime.requestEntryLifetime;
bindApprovalPublicationContext(gatewayRequestContext);
await attachInitialGatewayLifetimeSidecars({
chatMetadataLifecycle,
gatewayRequestContext,
flushPendingSessionsChangedEvents: shutdownRuntime.flushPendingSessionsChangedEvents,
minimalTestGateway,
logWarning: (message) => log.warn(message),
...!workerPlacementRuntime && githubPublicationRuntime ? { reconcileGitHubPublications: githubPublicationRuntime.reconcilePublications } : {},
sidecars: runtimeState.gatewayLifetimeSidecars
});
pluginGatewayContext.current = gatewayRequestContext;
gatewayRequestContext.dispatchHookAgentTurn = async (pluginId, hookParams) => {
const transport = runtime.transportBridge.current();
if (!transport) throw new Error("Gateway listener must start before plugin hook dispatch");
return await transport.dispatchHookAgentTurn(pluginId, hookParams);
};
const { createGatewayInstanceRuntime } = await import("./server-instance-runtime-DThAZuxQ.js");
const gatewayInstanceRuntime = createGatewayInstanceRuntime({
getContext: () => gatewayRequestContext,
getMethodRegistry: () => getAttachedGatewayMethodRegistry(),
isDispatchAvailable: () => startupState.dispatchReady && !lifecycle.closePreludeStarted,
logError: (message) => log.error(message)
});
gatewayInstanceRuntimeRef.current = gatewayInstanceRuntime;
gatewayRequestContext.resolveGatewayContext = () => gatewayInstanceRuntime.isAvailable() ? gatewayRequestContext : void 0;
bindGatewayContextResolver(gatewayRequestContext.resolveGatewayContext, runtime.resolvePluginGatewayContext);
const hostLifecycle = params.hostLifecycle;
if (hostLifecycle) gatewayRequestContext.hostLifecycle = {
externalRestart: hostLifecycle.externalRestart,
request: (action, assertCaller) => hostLifecycle.request(action, () => {
if (!gatewayInstanceRuntime.isAvailable()) throw new Error("Gateway lifecycle is unavailable for this closed instance. Reconnect and retry.");
assertCaller();
})
};
gatewayRequestContext.approvalEvents = gatewayInstanceRuntime.approvalEvents;
gatewayRequestContext.recoveryRuntime = gatewayInstanceRuntime.recovery;
gatewayRequestContext.createAgentTurnFacade = gatewayInstanceRuntime.createAgentTurnFacade;
return {
...runtime,
chatMetadataLifecycle,
gatewayRequestContext,
gatewayInstanceRuntime
};
}
//#endregion
//#region src/gateway/server-cron-lazy.ts
/** Creates a cron state proxy that imports the real cron service on first use. */
function createLazyGatewayCronState(params) {
const env = params.env ?? process.env;
const storePath = resolveCronJobsStorePathFromConfig(params.cfg, env);
const cronEnabled = env.OPENCLAW_SKIP_CRON !== "1" && params.cfg.cron?.enabled !== false;
let loaded = null;
let stopped = false;
let exitWatcherHandoff;
let exitWatcherHandoffStop;
let lifecycleGeneration = 0;
let schedulingPaused = false;
const schedulingResumeWaiters = /* @__PURE__ */ new Set();
const releaseSchedulingResumeWaiters = () => {
const waiters = Array.from(schedulingResumeWaiters);
schedulingResumeWaiters.clear();
for (const resolve of waiters) resolve();
};
const waitForSchedulingResume = async () => {
if (!schedulingPaused) return;
await new Promise((resolve) => {
schedulingResumeWaiters.add(resolve);
});
};
const cronStateLoader = createLazyPromiseLoader(() => import("./server-cron-XQIdEx2h.js").then(({ buildGatewayCronService }) => {
loaded = {
state: buildGatewayCronService(params),
phase: "idle",
startPromise: null,
startGeneration: null,
schedulingPaused: false,
underlyingStartInFlight: false,
underlyingStarted: false
};
if (schedulingPaused) {
loaded.state.cron.pauseScheduling();
loaded.schedulingPaused = true;
}
return loaded;
}), { cacheRejections: true });
const load = async () => {
if (loaded) return loaded;
return await cronStateLoader.load();
};
const stopResolvedCron = async (resolved) => {
resolved.phase = "stopped";
resolved.underlyingStarted = false;
if (exitWatcherHandoff) await (exitWatcherHandoffStop ??= exitWatcherHandoff.stopOwner());
else if (resolved.state.cron.stopAndDrain) await resolved.state.cron.stopAndDrain();
else {
resolved.state.cron.stop();
await resolved.state.stopStreamWatchers();
}
};
const stopLoadedCronAndDrain = async (handoff) => {
stopped = true;
exitWatcherHandoff ??= handoff;
lifecycleGeneration += 1;
releaseSchedulingResumeWaiters();
const loading = cronStateLoader.peek();
const resolved = loaded ?? (loading ? await loading : null);
if (resolved) await stopResolvedCron(resolved);
};
const cron = {
async start() {
stopped = false;
const generation = lifecycleGeneration;
const startCancelled = () => stopped || generation !== lifecycleGeneration;
const resolved = await load();
const hasStarted = () => resolved.phase === "started";
if (startCancelled()) return;
if (hasStarted()) return;
if (resolved.startPromise) {
const pendingGeneration = resolved.startGeneration;
try {
await resolved.startPromise;
} catch (err) {
if (pendingGeneration === generation) throw err;
}
if (startCancelled() || hasStarted()) return;
if (pendingGeneration !== generation) {
await cron.start();
return;
}
}
resolved.phase = "starting";
resolved.startGeneration = generation;
const startPromise = (async () => {
await waitForSchedulingResume();
if (startCancelled()) {
resolved.phase = "stopped";
return;
}
if (resolved.schedulingPaused) {
resolved.state.cron.resumeScheduling();
resolved.schedulingPaused = false;
}
resolved.underlyingStartInFlight = true;
try {
await resolved.state.cron.start();
resolved.underlyingStarted = true;
} catch (err) {
resolved.underlyingStarted = false;
resolved.phase = startCancelled() ? "stopped" : "idle";
throw err;
} finally {
resolved.underlyingStartInFlight = false;
}
if (startCancelled()) {
await stopResolvedCron(resolved);
return;
}
if (schedulingPaused) {
resolved.state.cron.pauseScheduling();
resolved.schedulingPaused = true;
}
try {
if (resolved.state.cronEnabled) await Promise.all([resolved.state.reconcileExitWatchers(), resolved.state.reconcileStreamWatchers()]);
} catch (err) {
resolved.phase = startCancelled() ? "stopped" : "started";
throw err;
}
if (startCancelled()) {
await stopResolvedCron(resolved);
return;
}
resolved.phase = "started";
})();
resolved.startPromise = startPromise;
try {
await startPromise;
} finally {
if (resolved.startPromise === startPromise) {
resolved.startPromise = null;
resolved.startGeneration = null;
}
}
},
stop() {
stopped = true;
lifecycleGeneration += 1;
releaseSchedulingResumeWaiters();
if (loaded) {
loaded.phase = "stopped";
loaded.underlyingStarted = false;
loaded.state.cron.stop();
return;
}
const loading = cronStateLoader.peek();
if (loading) loading.then((resolved) => {
if (!stopped) return;
resolved.phase = "stopped";
resolved.underlyingStarted = false;
resolved.state.cron.stop();
}).catch(() => {});
},
async stopAndDrain() {
await stopLoadedCronAndDrain();
},
pauseScheduling() {
schedulingPaused = true;
if (loaded) {
loaded.state.cron.pauseScheduling();
loaded.schedulingPaused = true;
}
},
resumeScheduling() {
schedulingPaused = false;
releaseSchedulingResumeWaiters();
if (loaded && loaded.schedulingPaused && (loaded.underlyingStarted || loaded.underlyingStartInFlight)) {
loaded.state.cron.resumeScheduling();
loaded.schedulingPaused = false;
}
},
getSuspensionBlockerCount() {
const loadedBlockers = loaded?.state.cron.getSuspensionBlockerCount?.() ?? 0;
return loaded?.phase === "starting" ? Math.max(1, loadedBlockers) : loadedBlockers;
},
async status() {
return await (await load()).state.cron.status();
},
async list(opts) {
return await (await load()).state.cron.list(opts);
},
async listPage(opts) {
return await (await load()).state.cron.listPage(opts);
},
async add(input, opts) {
return await (await load()).state.cron.add(input, opts);
},
async update(id, patch, opts) {
return await (await load()).state.cron.update(id, patch, opts);
},
async updateWithPrecondition(id, patch, precondition, opts) {
return await (await load()).state.cron.updateWithPrecondition(id, patch, precondition, opts);
},
async remove(id, opts) {
return await (await load()).state.cron.remove(id, opts);
},
async removeStaleJobFamily(family, opts) {
return await (await load()).state.cron.removeStaleJobFamily(family, opts);
},
async removeAgentJobsTransactional(agentId, commit) {
return await (await load()).state.cron.removeAgentJobsTransactional(agentId, commit);
},
async run(id, mode, opts) {
return await (await load()).state.cron.run(id, mode, opts);
},
async enqueueRun(id, mode, opts) {
return await (await load()).state.cron.enqueueRun(id, mode, opts);
},
getJob(id) {
if (!loaded) return;
return loaded.state.cron.getJob(id);
},
async readJob(id) {
return await (await load()).state.cron.readJob(id);
},
async readScratch(id) {
return await (await load()).state.cron.readScratch(id);
},
async writeScratch(id, write) {
return await (await load()).state.cron.writeScratch(id, write);
},
getDefaultAgentId() {
if (!loaded) return;
return loaded.state.cron.getDefaultAgentId();
},
async prepareWake() {
await load();
},
wake(opts) {
if (!loaded) {
load();
return { ok: false };
}
return loaded.state.cron.wake(opts);
}
};
return {
cron,
storePath,
cronEnabled,
prepareExitWatcherHandoff: async () => {
const loading = cronStateLoader.peek();
const handoff = await (loaded ?? (loading ? await loading : null))?.state.prepareExitWatcherHandoff?.();
if (!handoff) return;
return {
...handoff,
stopOwner: async () => {
await stopLoadedCronAndDrain(handoff);
}
};
},
async reconcileExitWatchers() {
await (await load()).state.reconcileExitWatchers();
},
async reconcileStreamWatchers() {
await (await load()).state.reconcileStreamWatchers();
},
async stopStreamWatchers() {
await loaded?.state.stopStreamWatchers();
},
async reconcileSystemJobs(cfg) {
return await (await load()).state.reconcileSystemJobs(cfg);
}
};
}
//#endregion
//#region src/gateway/server-cron-reconciled.ts
function createGatewayCronReconciliation(params) {
let lifecycleGeneration = 0;
let activeAbortController;
const supersedeActive = () => {
lifecycleGeneration += 1;
activeAbortController?.abort();
activeAbortController = void 0;
};
return {
arm: ({ reason, config, cronState }) => {
supersedeActive();
const generation = lifecycleGeneration;
const abortController = new AbortController();
activeAbortController = abortController;
const cron = cronState.cron;
const event = {
reason,
enabled: cronState.cronEnabled
};
let completed = false;
return { complete: async () => {
if (completed) return;
completed = true;
if (params.isClosing() || generation !== lifecycleGeneration || abortController.signal.aborted) return;
await params.runHook(event, {
port: params.port,
config,
workspaceDir: params.workspaceDir,
getCron: () => cron,
abortSignal: abortController.signal
});
} };
},
invalidate: supersedeActive
};
}
//#endregion
//#region src/gateway/server-runtime-handles.ts
/** Creates gateway mutable state with inert handles that are safe to stop before startup finishes. */
function createGatewayServerMutableState() {
return {
discovery: null,
maintenance: null,
stopMediaCleanup: () => waitForMediaCleanupDrains({ timeoutMs: MEDIA_CLEANUP_STOP_TIMEOUT_MS }),
heartbeatRunner: createNoopHeartbeatRunner(),
stopDeliveryRecovery: async () => {},
stopGatewayUpdateCheck: async () => {},
tailscaleCleanup: null,
postReadySidecars: [],
gatewayLifetimeSidecars: [],
skillsRefreshTimer: null,
skillsRefreshDelayMs: 3e4,
skillsChangeUnsub: async () => {},
channelHealthMonitor: null,
configReloader: {
stop: async () => {},
notifyPluginMetadataChanged: () => {},
isConfigReloadSettled: () => false
},
agentUnsub: null,
heartbeatUnsub: null,
transcriptUnsub: null,
lifecycleUnsub: null,
taskUnsub: null
};
}
//#endregion
//#region src/gateway/server-live-state.ts
/** Creates gateway live state with fresh mutable runtime handles. */
function createGatewayServerLiveState(params) {
return {
...createGatewayServerMutableState(),
hooksConfig: params.hooksConfig,
hookClientIpConfig: params.hookClientIpConfig,
cronState: params.cronState,
controlUiSessionPullRequests: void 0,
sessionViewerPresence: void 0,
pluginServices: null,
gatewayMethods: params.gatewayMethods
};
}
//#endregion
//#region src/gateway/server-plugin-runtime-generation.ts
/** One Gateway owner fences every plugin publication across startup and hot replacement. */
function createGatewayPluginRuntimeGeneration(params) {
let current;
let retired = false;
let pending;
const createClaim = () => {
const claim = Object.freeze({
isCurrent: () => current === claim && pending === void 0 && !retired,
waitForUnblocked: async () => {
for (;;) {
const reservation = pending;
if (current !== claim || !reservation) return claim.isCurrent();
await reservation.settled.promise;
}
},
publish: (publication) => {
if (!claim.isCurrent()) return false;
publication();
return true;
}
});
return claim;
};
current = createClaim();
return {
currentClaim: () => current,
currentServices: () => params.getServices(),
publishServices: (claim, services) => claim.publish(() => params.setServices(services)),
reserve: () => {
if (pending) throw new Error("a Gateway plugin runtime replacement is already pending");
const reservation = {
claim: createClaim(),
settled: createDeferredCore()
};
pending = reservation;
const settle = (accepted) => {
if (pending !== reservation) return;
if (accepted) {
current = reservation.claim;
retired = false;
}
pending = void 0;
reservation.settled.resolve();
};
return Object.freeze({
claim: reservation.claim,
retirePrevious: () => {
if (pending === reservation) retired = true;
},
commit: () => settle(true),
reject: () => settle(false)
});
}
};
}
//#endregion
//#region src/gateway/server-request-entry.ts
function isPendingNodeCompletion({ req, client, context }) {
if (client?.connect.role !== "node" || !client.connId || !isRecord(req.params)) return false;
const invokeId = req.method === "node.invoke.progress" ? req.params.invokeId : req.method === "node.invoke.result" ? req.params.id : void 0;
return typeof invokeId === "string" && typeof req.params.nodeId === "string" && context.nodeRegistry.isInvokeCurrent(invokeId, req.params.nodeId, client.connId);
}
/** One Gateway's preparation leases; handler execution belongs to its existing runtime owner. */
var GatewayRequestEntryLifetime = class {
constructor() {
this.stopping = new AbortController();
this.active = /* @__PURE__ */ new Set();
this.sealed = false;
this.signal = this.stopping.signal;
}
enter(options) {
let released = false;
const assertOpen = () => {
if (released || this.sealed || this.signal.aborted && !isPendingNodeCompletion(options)) throw new Error("Gateway request entry is closed");
};
assertOpen();
const settled = createDeferredCore();
this.active.add(settled.promise);
return {
assertOpen,
release: () => {
if (released) return;
released = true;
this.active.delete(settled.promise);
settled.resolve();
}
};
}
beginClose() {
this.stopping.abort();
}
async waitForPendingEntries() {
await Promise.all(this.active);
}
async sealAndJoin() {
this.sealed = true;
await this.waitForPendingEntries();
}
};
//#endregion
//#region src/gateway/server-sidecar-owners.ts
function createGatewaySidecarStopOwner(params) {
let activeStop = null;
let phase = "open";
const publish = (sidecars) => {
if (phase === "sealed") throw new Error("cannot publish a Gateway sidecar after shutdown sealed its owner");
params.setRegistered(mergeGatewaySidecarOwners({
registered: params.getRegistered(),
published: sidecars
}));
if (phase === "closing") stop().catch(() => {});
};
const beginClose = () => {
if (phase === "open") phase = "closing";
};
const stop = () => {
beginClose();
if (activeStop) return activeStop;
const stopping = Promise.resolve().then(async () => {
const failedSidecars = /* @__PURE__ */ new Set();
let failure;
try {
while (params.getRegistered().some((sidecar) => !failedSidecars.has(sidecar))) {
const sidecars = [...new Set(params.getRegistered().filter((sidecar) => !failedSidecars.has(sidecar)))];
const ownedSidecars = new Set(sidecars);
params.setRegistered(params.getRegistered().filter((sidecar) => !ownedSidecars.has(sidecar)));
let pending = sidecars;
let results = [];
for (let attempt = 0; attempt < 2; attempt += 1) {
results = await Promise.allSettled(pending.map(async (sidecar) => await sidecar.stop()));
pending = pending.filter((_sidecar, index) => results[index]?.status === "rejected");
if (pending.length === 0) break;
}
params.setRegistered(params.getRegistered().filter((sidecar) => !ownedSidecars.has(sidecar)));
if (pending.length > 0) {
const rejected = results.find((result) => result.status === "rejected");
failure ??= rejected?.reason;
for (const sidecar of pending) failedSidecars.add(sidecar);
}
}
if (failedSidecars.size > 0) {
params.setRegistered([...failedSidecars, ...params.getRegistered()]);
throw failure;
}
} finally {
activeStop = null;
}
});
activeStop = stopping;
stopping.catch(() => {});
return stopping;
};
const sealAndJoin = async () => {
let failure;
while (true) {
const stopping = activeStop;
if (!stopping) {
phase = "sealed";
break;
}
try {
await stopping;
} catch (error) {
failure ??= error instanceof Error ? error : new Error(String(error));
}
}
if (failure) throw failure;
};
return {
publish,
beginClose,
stop,
sealAndJoin
};
}
function mergeGatewaySidecarOwners(params) {
return [.../* @__PURE__ */ new Set([...params.registered, ...params.published])];
}
//#endregion
//#region src/gateway/session-viewer-presence.ts
function normalizedSessionKeys(sessionKeys) {
return [...new Set(sessionKeys.map((key) => key.trim()).filter(Boolean))].toSorted();
}
function sameKeys(left, right) {
return left !== void 0 && left.length === right.length && left.every((key, index) => key === right[index]);
}
/** Owns one replace-set per websocket connection until empty declaration or disconnect. */
function createSessionViewerPresenceDeclarations(deps) {
const declarations = /* @__PURE__ */ new Map();
let stopped = false;
const replace = (connId, sessionKeys) => {
if (stopped) return [];
const normalizedConnId = connId.trim();
const client = deps.clients.getByConnectionId(normalizedConnId);
if (!client || client.invalidated || client.socket.readyState !== 1) return [];
const next = normalizedSessionKeys(sessionKeys);
const previous = declarations.get(normalizedConnId);
if (sameKeys(previous, next) || previous === void 0 && next.length === 0) return next;
if (next.length === 0) declarations.delete(normalizedConnId);
else declarations.set(normalizedConnId, next);
if (client.presenceKey) {
upsertPresence(client.presenceKey, { watchedSessions: next.length > 0 ? [...next] : void 0 });
if (next.length > 0) recordClientPresenceActivity(deps.clients, client);
broadcastPresenceSnapshot(deps);
}
return next;
};
const unsubscribe = (connId) => {
const normalizedConnId = connId.trim();
if (normalizedConnId) declarations.delete(normalizedConnId);
};
const stop = () => {
stopped = true;
declarations.clear();
};
return {
replace,
unsubscribe,
stop
};
}
//#endregion
//#region src/gateway/server-lifecycle.ts
async function prepareGatewayLifecycle(params) {
const { runtime, port, log, logCron, shutdownRuntime } = params;
const requestEntryLifetime = new GatewayRequestEntryLifetime();
const { minimalTestGateway, transportBridge, sessionMessageSubscribers, isConnectionActive, clients, mentionInbox, broadcast, cfgAtStart, pluginRuntime, authRateLimiter, nodeReapprovalCoordinator, channelManager, deps, initialHooksConfig, initialHookClientIpConfig, runtimeStateRef, gatewayInstanceRuntimeRef, startupState, readinessEventLoopHealth, browserAuthRateLimiter, chatRunState, chatAbortControllers, chatQueuedTurns, removeChatRun, agentRunSeq, listActiveGatewayMethods, broadcastToConnIds, getBufferedAmount, sessionEventSubscribers, watchNodeRequestHandler, defaultWorkspaceDir, activeTaskCount, desktopSessionRegistry, nodeDesktopStreamBroker, bindDeviceNodeControl, bindWorkerNodeDesktopControl, workerPlacementRuntime, lifecycle } = runtime;
const subscribeSessionMessageEvents = (connId, sessionKey, options) => sessionMessageSubscribers.subscribe(connId, sessionKey, options);
const unsubscribeSessionMessageEvents = (connId, sessionKey) => sessionMessageSubscribers.unsubscribe(connId, sessionKey);
const restartRecoveryCandidates = /* @__PURE__ */ new Map();
const nodeDesktopServiceRef = {};
const { createGatewayNodeSessionRuntime } = await import("./server-node-session-runtime-Dlo3NeMC.js");
const { nodeRegistry, nodeWorkerSupervisorTransport, nodePresenceTimers, nodeSendToSession, nodeSendToAllSubscribed, nodeSubscribe, nodeUnsubscribe, nodeUnsubscribeAll, broadcastVoiceWakeChanged, broadcastVoiceWakeRoutingChanged, hasTalkNodeConnected } = createGatewayNodeSessionRuntime({
broadcast,
sessionEventSubscribers,
sessionMessageSubscribers,
listRegisteredNodePluginToolCommands: () => pluginRuntime.registry.nodeHostCommands,
getConfig: getRuntimeConfig,
onRunnerStateChanged: (nodeId, change) => {
if (change.availabilityChanged) workerPlacementRuntime?.runnerAvailability.markChanged();
if (change.inventoryChanged) workerPlacementRuntime?.scheduleNodeWorkspaceRetention(nodeId);
},
onPairingInvalidated: ({ nodeId, connId }) => {
nodeDesktopServiceRef.current?.stopNode(nodeId);
upsertPresence(nodeId, { reason: "disconnect" });
broadcastPresenceSnapshot({
broadcast,
incrementPresenceVersion,
getHealthVersion
});
removeRemoteNodeInfoForConnection(nodeId, connId);
},
onPairingGenerationChanged: ({ nodeId }) => {
nodeDesktopServiceRef.current?.stopNode(nodeId);
}
});
const nodeDesktopService = (await import("./node-source-BDb0jkF6.js")).createNodeDesktopService({
getConfig: getRuntimeConfig,
nodeRegistry,
desktopRegistry: desktopSessionRegistry,
streamBroker: nodeDesktopStreamBroker
});
nodeDesktopServiceRef.current = nodeDesktopService;
bindDeviceNodeControl?.(nodeWorkerSupervisorTransport);
bindWorkerNodeDesktopControl?.(nodeWorkerSupervisorTransport);
const { createWatchNodeHttpRuntime } = await import("./watch-node-http-DZUX16Xq.js");
const watchNodeHttpRuntime = createWatchNodeHttpRuntime({
nodeRegistry,
getConfig: getRuntimeConfig,
broadcast,
rateLimiter: authRateLimiter,
nodeReapprovalCoordinator,
onDeviceTokensReplaced: (deviceId, roles) => {
const context = runtime.resolvePluginGatewayContext();
if (!context) throw new Error("Gateway request context is unavailable during device setup");
retireDeviceTokenClients(context, deviceId, roles, "device-token-rotated");
},
onNodeConnected: (session) => {
upsertPresence(session.nodeId, {
host: session.displayName ?? session.clientId ?? session.nodeId,
ip: session.remoteIp,
version: session.version,
platform: session.platform,
deviceFamily: session.deviceFamily,
modelIdentifier: session.modelIdentifier,
mode: session.clientMode,
deviceId: session.nodeId,
roles: ["node"],
scopes: [],
instanceId: session.nodeId,
reason: "connect"
});
broadcastPresenceSnapshot({
broadcast,
incrementPresenceVersion,
getHealthVersion
});
recordRemoteNodeInfo({
nodeId: session.nodeId,
connId: session.connId,
displayName: session.displayName,
platform: session.platform,
deviceFamily: session.deviceFamily,
commands: session.commands,
remoteIp: session.remoteIp,
pairingGeneration: session.pairingGeneration
});
},
onNodeDisconnected: (nodeId) => {
upsertPresence(nodeId, { reason: "disconnect" });
broadcastPresenceSnapshot({
broadcast,
incrementPresenceVersion,
getHealthVersion
});
removeRemoteNodeInfo(nodeId);
nodeUnsubscribeAll(nodeId);
clearNodeWakeState(nodeId);
},
onError: (message, error) => log.warn(`${message}: ${String(error)}`)
});
watchNodeRequestHandler.current = watchNodeHttpRuntime.handleRequest;
const { TerminalSessionManager, DEFAULT_TERMINAL_DETACH_SECONDS } = await import("./session-manager-WyX2j7zt.js");
const { createTerminalSessionTransport } = await import("./gateway-transport-CCDhR7aF.js");
const terminalSessions = new TerminalSessionManager({
...createTerminalSessionTransport(broadcastToConnIds, getBufferedAmount),
detachGraceMs: (cfgAtStart.gateway?.terminal?.detachedSessionTimeoutSeconds ?? DEFAULT_TERMINAL_DETACH_SECONDS) * 1e3
});
applyGatewayLaneConcurrency(resolveGatewayLaneConcurrency(cfgAtStart), { gatewayStart: true });
runtimeStateRef.current = createGatewayServerLiveState({
hooksConfig: initialHooksConfig,
hookClientIpConfig: initialHookClientIpConfig,
cronState: createLazyGatewayCronState({
cfg: cfgAtStart,
deps,
broadcast,
resolveGatewayContext: runtime.resolvePluginGatewayContext
}),
gatewayMethods: listActiveGatewayMethods(pluginRuntime.baseGatewayMethods)
});
const runtimeState = runtimeStateRef.current;
const pluginRuntimeGeneration = createGatewayPluginRuntimeGeneration({
getServices: () => runtimeState.pluginServices,
setServices: (services) => {
runtimeState.pluginServices = services;
}
});
const unavailableGatewayMethods = new Set(minimalTestGateway ? [] : STARTUP_UNAVAILABLE_GATEWAY_METHODS);
const kernel = {
pluginRuntimeGeneration,
setDispatchReady: (ready) => {
startupState.dispatchReady = ready;
},
markSidecarsReady: () => {
startupState.sidecarsReady = true;
},
unlockStartupMethods: () => {
for (const method of STARTUP_UNAVAILABLE_GATEWAY_METHODS) unavailableGatewayMethods.delete(method);
},
publishMethodSurface: (methods) => {
runtimeState.gatewayMethods.splice(0, runtimeState.gatewayMethods.length, ...methods);
},
setEarlyRuntimeHandles: (handles) => {
activeTaskCount.get = handles.getActiveTaskCount;
runtimeState.skillsChangeUnsub = handles.skillsChangeUnsub;
},
swapDiscovery: (next) => {
const previous = runtimeState.discovery;
runtimeState.discovery = next;
return previous;
},
setScheduledServiceHandles: (handles) => {
runtimeState.heartbeatRunner = handles.heartbeatRunner;
runtimeState.stopDeliveryRecovery = handles.stopDeliveryRecovery;
},
setPostAttachHandles: (handles, claim) => {
runtimeState.stopGatewayUpdateCheck = handles.stopGatewayUpdateCheck;
pluginRuntimeGeneration.publishServices(claim, handles.pluginServices);
},
setTailscaleCleanup: (cleanup) => {
runtimeState.tailscaleCleanup = cleanup;
},
setConfigReloaderHandle: (configReloader) => {
runtimeState.configReloader = configReloader;
},
getReloadState: () => ({
hooksConfig: runtimeState.hooksConfig,
hookClientIpConfig: runtimeState.hookClientIpConfig,
heartbeatRunner: runtimeState.heartbeatRunner,
cronState: runtimeState.cronState
}),
setReloadHookState: (next) => {
runtimeState.hooksConfig = next.hooksConfig;
runtimeState.hookClientIpConfig = next.hookClientIpConfig;
},
swapHeartbeatRunner: (next) => {
const previous = runtimeState.heartbeatRunner;
runtimeState.heartbeatRunner = next;
return previous;
},
swapCronState: (next) => {
const previous = runtimeState.cronState;
runtimeState.cronState = next;
deps.cron = next.cron;
return previous;
},
setChannelHealthMonitor: (next) => {
runtimeState.channelHealthMonitor = next;
},
notifyPluginMetadataChanged: () => {
runtimeState.configReloader.notifyPluginMetadataChanged();
},
getConfigReloaderHotReloadStatus: () => runtimeState.configReloader.hotReloadStatus?.(),
setPostReadySidecars: (sidecars) => {
runtimeState.postReadySidecars = sidecars;
},
setGatewayLifetimeSidecars: (sidecars) => {
runtimeState.gatewayLifetimeSidecars = sidecars;
},
addGatewayLifetimeSidecar: (sidecar) => {
runtimeState.gatewayLifetimeSidecars.push(sidecar);
},
setMaintenanceHandles: (handles) => {
runtimeState.maintenance = handles;
runtimeState.stopMediaCleanup = handles.stopMediaCleanup;
}
};
runtimeState.controlUiSessionPullRequests = createControlUiSessionPullRequestSubscriptions({
broadcastToConnIds,
isConnectionActive
});
runtimeState.sessionViewerPresence = createSessionViewerPresenceDeclarations({
clients,
broadcast,
incrementPresenceVersion,
getHealthVersion
});
deps.cron = runtimeState.cronState.cron;
const pluginHostServices = { get cron() {
return runtimeState.cronState.cron;
} };
const cronReconciliation = createGatewayCronReconciliation({
port,
workspaceDir: defaultWorkspaceDir,
isClosing: () => lifecycle.closePreludeStarted,
runHook: async (event, ctx) => {
try {
const hookRunner = (await import("./plugins/hook-runner-global.js")).getGlobalHookRunner();
if (hookRunner?.hasHooks("cron_reconciled")) await hookRunner.runCronReconciled(event, ctx);
} catch (err) {
logCron.error(`cron_reconciled hook failed: ${String(err)}`);
}
}
});
const postReadyState = { maintenanceTimer: null };
const clearPostReadyMaintenanceTimer = () => {
if (!postReadyState.maintenanceTimer) return;
clearTimeout(postReadyState.maintenanceTimer);
postReadyState.maintenanceTimer = null;
};
let deliveryRecoveryStopPromise = null;
const stopDeliveryRecoveryForClose = () => {
deliveryRecoveryStopPromise ??= runtimeState.stopDeliveryRecovery();
return deliveryRecoveryStopPromise;
};
let mediaCleanupStopPromise = null;
const stopMediaCleanupForClose = () => {
mediaCleanupStopPromise ??= runtimeState.stopMediaCleanup();
return mediaCleanupStopPromise;
};
const healthWork = new AsyncWorkScope();
const markClosePreludeStarted = (options) => {
if (lifecycle.closePreludeStarted) return;
lifecycle.closePreludeStarted = true;
requestEntryLifetime.beginClose();
mentionInbox.dispose();
healthWork.beginClose();
broadcast("shutdown", resolveGatewayShutdownNotice(options));
runtime.connectionWork.beginClose();
connectionDependentSidecarStopOwner.beginClose();
stopDeliveryRecoveryForClose();
stopMediaCleanupForClose();
runtimeState.stopGatewayUpdateCheck().catch(() => {});
runtimeState.controlUiSessionPullRequests?.stop();
runtimeState.sessionViewerPresence?.stop();
kernel.setDispatchReady(false);
gatewayInstanceRuntimeRef.current?.close();
cronReconciliation.invalidate();
clearPostReadyMaintenanceTimer();
};
let configReloaderStopPromise = null;
const stopConfigReloaderForClose = () => {
configReloaderStopPromise ??= runtimeState.configReloader.stop();
return configReloaderStopPromise;
};
const beginClosePrelude = async (options) => {
fenceSessionSuspensionWritesForGatewayShutdown();
markClosePreludeStarted(options);
await Promise.all([
requestEntryLifetime.waitForPendingEntries(),
stopDeliveryRecoveryForClose(),
stopMediaCleanupForClose(),
runtimeState.stopGatewayUpdateCheck(),
stopConfigReloaderForClose().catch(() => {}),
runtimeState.controlUiSessionPullRequests?.stop(),
healthWork.drain()
]);
};
const runClosePrelude = async () => {
await beginClosePrelude();
disposeNodeConnectionNotifications(nodeRegistry);
watchNodeHttpRuntime.close();
await shutdownRuntime.runGatewayClosePrelude({
stopDiagnostics: stopDiagnosticHeartbeat,
clearSkillsRefreshTimer: () => {
if (!runtimeState?.skillsRefreshTimer) return;
clearTimeout(runtimeState.skillsRefreshTimer);
runtimeState.skillsRefreshTimer = null;
},
skillsChangeUnsub: runtimeState.skillsChangeUnsub,
disposeAuthRateLimiter: () => {
authRateLimiter.dispose();
nodeReapprovalCoordinator.dispose();
},
disposeBrowserAuthRateLimiter: () => browserAuthRateLimiter.dispose(),
stopChannelHealthMonitor: async () => {
const monitor = runtimeState?.channelHealthMonitor;
monitor?.shutdown();
await monitor?.waitForIdle();
},
stopReadinessEventLoopHealth: readinessEventLoopHealth.stop,
closeMcpServer: shutdownRuntime.closeMcpLoopbackServer
});
};
const { getRuntimeSnapshot, startChannels, startChannel, stopChannel, markChannelLoggedOut } = channelManager;
const refreshGatewayHealthSnapshotWithRuntime = (optsResult) => {
if (healthWork.isClosing) return Promise.reject(/* @__PURE__ */ new Error("Gateway health refresh owner is closed"));
return healthWork.track(() => refreshGatewayHealthSnapshot({
...optsResult,
getRuntimeSnapshot,
getEventLoopHealth: readinessEventLoopHealth.snapshot,
getConfigReloaderHotReloadStatus: kernel.getConfigReloaderHotReloadStatus
}));
};
let connectionDependentSidecars = [];
const connectionDependentSidecarStopOwner = createGatewaySidecarStopOwner({
getRegistered: () => connectionDependentSidecars,
setRegistered: (sidecars) => {
connectionDependentSidecars = sidecars;
}
});
const stopConnectionDependentSidecars = async () => {
try {
await connectionDependentSidecarStopOwner.stop();
} finally {
await connectionDependentSidecarStopOwner.sealAndJoin();
}
};
const postReadySidecarStopOwner = createGatewaySidecarStopOwner({
getRegistered: () => runtimeState.postReadySidecars,
setRegistered: (sidecars) => {
runtimeState.postReadySidecars = sidecars;
}
});
const gatewayLifetimeSidecarStopOwner = createGatewaySidecarStopOwner({
getRegistered: () => runtimeState.gatewayLifetimeSidecars,
setRegistered: (sidecars) => {
runtimeState.gatewayLifetimeSidecars = sidecars;
}
});
const stopRegisteredPostReadySidecars = postReadySidecarStopOwner.stop;
const stopRegisteredGatewayLifetimeSidecars = gatewayLifetimeSidecarStopOwner.stop;
const sealAndJoinRegisteredSidecarStops = async () => {
const failure = (await Promise.allSettled([postReadySidecarStopOwner.sealAndJoin(), gatewayLifetimeSidecarStopOwner.sealAndJoin()])).find((result) => result.status === "rejected");
if (failure) throw failure.reason;
};
const prepareClose = async (optsValue) => {
await beginClosePrelude(optsValue);
const preparation = await shutdownRuntime.prepareGatewayClose({
resolveGatewayContext: runtime.resolvePluginGatewayContext,
chatRunState,
chatAbortControllers,
chatQueuedTurns,
restartRecoveryCandidates,
removeChatRun,
agentRunSeq,
broadcast,
nodeSendToSession,
resolveActiveSessionIdForKey: resolveActiveEmbeddedRunSessionId,
markMainSessionsAbortedForRestart: async (restart) => {
await shutdownRuntime.markRestartAbortedMainSessions({
cfg: getRuntimeConfig(),
...restart
});
},
getPendingReplyCount: getTotalPendingReplies,
updateCheckStop: runtimeState.stopGatewayUpdateCheck,
configReloader: { stop: stopConfigReloaderForClose }
}, optsValue);
return async () => {
const channelIds = listLoadedChannelPluginsForRegistry(pluginRuntime.registry).map((plugin) => plugin.id);
const transport = transportBridge.current();
await transport?.portalService.closeAll();
await shutdownRuntime.completeGatewayClose({
bonjourStop: kernel.swapDiscovery(null)?.stop ?? null,
tailscaleCleanup: runtimeState.tailscaleCleanup,
clearSecretsRuntimeSnapshot: clearSecretsRuntimeSnapshotState,
channelIds,
stopChannel,
pluginServices: runtimeState.pluginServices,
cron: runtimeState.cronState.cron,
heartbeatRunner: runtimeState.heartbeatRunner,
stopTaskRegistryMaintenance: shutdownRuntime.stopTaskRegistryMaintenance,
nodePresenceTimers,
maintenance: runtimeState.maintenance,
stopMediaCleanup: stopMediaCleanupForClose,
agentUnsub: runtimeState.agentUnsub,
heartbeatUnsub: runtimeState.heartbeatUnsub,
transcriptUnsub: runtimeState.transcriptUnsub,
lifecycleUnsub: runtimeState.lifecycleUnsub,
taskUnsub: runtimeState.taskUnsub,
chatRunState,
clients,
finishRequestEntries: () => requestEntryLifetime.sealAndJoin(),
...transport ? {
wss: transport.wss,
httpServer: transport.httpServer,
httpServers: transport.httpServers
} : {},
drainActiveSessionsForShutdown: shutdownRuntime.drainActiveSessionsForShutdown,
disposeAllBundleLspRuntimes: shutdownRuntime.disposeAllBundleLspRuntimes,
drainRetainedOpenAiEmbeddingProviders: shutdownRuntime.drainRetainedOpenAiEmbeddingProviders,
stopGmailWatcher: shutdownRuntime.stopGmailWatcher,
disposeAllCodeModeRuns: shutdownRuntime.disposeAllCodeModeRuns,
closeProviderTransportDispatcherPool: shutdownRuntime.closeProviderTransportDispatcherPool
}, preparation);
await requestEntryLifetime.sealAndJoin();
params.releasePluginMetadata();
};
};
const closeOnStartupFailure = async () => {
const close = await prepareClose({ reason: "gateway startup failed" });
await runGatewayShutdownSteps({
steps: [
{
name: "connection-dependent sidecars",
run: stopConnectionDependentSidecars,
required: true
},
{
name: "received connection work",
run: () => runtime.connectionWork.drain(),
required: true
},
{
name: "gateway lifetime sidecars",
run: stopRegisteredGatewayLifetimeSidecars
},
{
name: "post-ready sidecars",
run: stopRegisteredPostReadySidecars
},
{
name: "gateway close prelude",
run: runClosePrelude
},
{
name: "late sidecar cleanup",
run: sealAndJoinRegisteredSidecarStops
},
{
name: "gateway close",
run: close
}
],
onError: (message) => log.error(message)
});
};
const configureDiagnostics = (config) => {
if (lifecycle.closePreludeStarted) return;
const enabled = isDiagnosticsEnabled(config);
setDiagnosticsEnabledForProcess(enabled);
if (!enabled) {
stopDiagnosticHeartbeat();
return;
}
startDiagnosticHeartbeat(void 0, {
getConfig: getRuntimeConfig,
startupGraceMs: 6e4,
sampleLiveness: () => {
const sample = readinessEventLoopHealth.persistentDegradationSnapshot();
if (!sample || sample.degradedSinceMs == null) return null;
return {
reasons: sample.reasons,
intervalMs: sample.intervalMs,
degradedSinceMs: sample.degradedSinceMs,
eventLoopDelayP99Ms: sample.delayP99Ms,
eventLoopDelayMaxMs: sample.delayMaxMs,
eventLoopUtilization: sample.utilization,
cpuCoreRatio: sample.cpuCoreRatio
};
}
});
};
configureDiagnostics(cfgAtStart);
return {
...runtime,
configureDiagnostics,
requestEntryLifetime,
subscribeSessionMessageEvents,
unsubscribeSessionMessageEvents,
restartRecoveryCandidates,
nodeRegistry,
nodeDesktopService,
nodePresenceTimers,
nodeSendToSession,
nodeSendToAllSubscribed,
nodeSubscribe,
nodeUnsubscribe,
nodeUnsubscribeAll,
broadcastVoiceWakeChanged,
broadcastVoiceWakeRoutingChanged,
hasTalkNodeConnected,
watchNodeHttpRuntime,
terminalSessions,
runtimeState,
unavailableGatewayMethods,
kernel,
pluginHostServices,
shutdownRuntime,
lifecycle,
postReadyState,
cronReconciliation,
beginClosePrelude,
runClosePrelude,
getRuntimeSnapshot,
startChannels,
startChannel,
stopChannel,
markChannelLoggedOut,
refreshGatewayHealthSnapshotWithRuntime,
stopRegisteredPostReadySidecars,
stopRegisteredGatewayLifetimeSidecars,
stopConnectionDependentSidecars,
registerConnectionDependentSidecars: connectionDependentSidecarStopOwner.publish,
unregisterConnectionDependentSidecar: (sidecar) => {
connectionDependentSidecars = connectionDependentSidecars.filter((registered) => registered !== sidecar);
},
registerPostReadySidecars: postReadySidecarStopOwner.publish,
registerGatewayLifetimeSidecars: gatewayLifetimeSidecarStopOwner.publish,
sealAndJoinRegisteredSidecarStops,
prepareClose,
closeOnStartupFailure
};
}
//#endregion
//#region src/gateway/node-reapproval-coordinator.ts
const pendingNodeReapprovalAttempts = new KeyedAsyncQueue();
function normalizeFingerprintList(value) {
return value ? [...new Set(value.map((entry) => entry.trim()).filter((entry) => entry.length > 0))].toSorted() : void 0;
}
function buildRequestFingerprint(input) {
const permissions = input.permissions ? Object.fromEntries(Object.entries(input.permissions).toSorted(([left], [right]) => left.localeCompare(right))) : void 0;
return JSON.stringify({
nodeId: input.nodeId.trim(),
clientId: input.clientId,
clientMode: input.clientMode,
displayName: input.displayName,
platform: input.platform,
version: input.version,
coreVersion: input.coreVersion,
uiVersion: input.uiVersion,
deviceFamily: input.deviceFamily,
modelIdentifier: input.modelIdentifier,
caps: normalizeFingerprintList(input.caps),
commands: normalizeFingerprintList(input.commands),
permissions,
remoteIp: input.remoteIp,
silent: Boolean(input.silent)
});
}
/** Creates the gateway-lifetime owner for paired-node reapproval write limits. */
function createNodeReapprovalCoordinator(config) {
const limiter = createAuthRateLimiter({
...config,
exemptLoopback: false
});
const requestStates = /* @__PURE__ */ new Map();
let disposed = false;
const executeRequest = async ({ input, cleanupClaim, baseDir }) => {
if (disposed) return null;
const reused = await reusePendingNodePairingForReconnect(input, cleanupClaim, baseDir);
if (reused) return reused;
const nodeId = input.nodeId.trim();
const identityKey = buildRateLimitIdentityKey("node", nodeId);
if (!limiter.check(identityKey, "node-reapproval").allowed) return null;
const result = await requestNodePairing(input, baseDir);
limiter.recordFailure(identityKey, AUTH_RATE_LIMIT_SCOPE_NODE_REAPPROVAL);
return result;
};
const enqueueRequest = (nodeId, state, initial) => {
pendingNodeReapprovalAttempts.enqueue(`node-reapproval:${nodeId}`, async () => {
const queued = initial ?? state.queued;
if (!initial) state.queued = void 0;
if (!queued) return;
try {
queued.deferred.resolve(await executeRequest(queued.params));
for (const follower of queued.followers) follower.resolve(null);
} catch (error) {
queued.deferred.reject(error);
for (const follower of queued.followers) follower.reject(error);
} finally {
if (requestStates.get(nodeId) === state && !state.queued) requestStates.delete(nodeId);
}
});
};
return {
updateConfig: (next) => limiter.updateConfig({
...next,
exemptLoopback: false
}),
request(params) {
if (disposed) return Promise.resolve(null);
const nodeId = params.input.nodeId.trim();
const fingerprint = buildRequestFingerprint(params.input);
const state = requestStates.get(nodeId);
if (!state) {
const deferred = createDeferredCore();
const nextState = {};
requestStates.set(nodeId, nextState);
enqueueRequest(nodeId, nextState, {
fingerprint,
params,
deferred,
followers: []
});
return deferred.promise;
}
if (state.queued?.fingerprint === fingerprint) {
const follower = createDeferredCore();
state.queued.params = params;
state.queued.followers.push(follower);
return follower.promise;
}
const deferred = createDeferredCore();
if (state.queued) {
state.queued.deferred.resolve(null);
for (const follower of state.queued.followers) follower.resolve(null);
state.queued = {
fingerprint,
params,
deferred,
followers: []
};
} else {
state.queued = {
fingerprint,
params,
deferred,
followers: []
};
enqueueRequest(nodeId, state);
}
return deferred.promise;
},
async finalizeCleanup(claim) {
return await pendingNodeReapprovalAttempts.enqueue(`node-reapproval:${claim.nodeId}`, async () => await finalizeNodePairingCleanupClaim(claim));
},
dispose() {
disposed = true;
for (const state of requestStates.values()) {
state.queued?.deferred.resolve(null);
for (const follower of state.queued?.followers ?? []) follower.resolve(null);
}
requestStates.clear();
limiter.dispose();
}
};
}
//#endregion
//#region src/gateway/event-web-push.ts
const EVENT_PUSH_TTL_SECONDS = 300;
const defaultLog = createSubsystemLogger("gateway/web-push");
function resolveEventWebPushNotification(event, payload) {
const value = isRecord(payload) ? payload : null;
if (!value) return null;
if (event === "question.requested") return {
category: "agent-question",
title: "OpenClaw needs an answer",
body: "An agent has a question for you.",
tag: `openclaw-question-${normalizeWebPushDisplayLabel(value.id) ?? "pending"}`
};
if (event === "chat" && value.state === "final" && !isTranscriptOnlyOpenClawAssistantMessage(value.message)) return {
category: "agent-finished",
title: "OpenClaw agent finished",
body: "An agent completed its response.",
tag: `openclaw-agent-finished-${normalizeWebPushDisplayLabel(value.runId) ?? "finished"}`
};
if (event === "task" && value.action === "upserted") {
const task = isRecord(value.task) ? value.task : null;
if (task?.status !== "failed" && task?.status !== "timed_out") return null;
const taskId = normalizeWebPushDisplayLabel(task.id) ?? "failed";
const taskTitle = normalizeWebPushDisplayLabel(task.title);
return {
category: "background-task-failed",
title: "OpenClaw background task failed",
body: "A background task needs attention.",
...taskTitle ? { identifiedBody: `${taskTitle} needs attention.` } : {},
tag: `openclaw-task-failed-${taskId}`
};
}
if (event === "cron" && value.action === "finished" && value.status === "error") {
const job = isRecord(value.job) ? value.job : null;
const jobId = normalizeWebPushDisplayLabel(value.jobId) ?? "failed";
const jobName = normalizeWebPushDisplayLabel(job?.name);
return {
category: "scheduled-task-failed",
title: "OpenClaw scheduled task failed",
body: "A scheduled task needs attention.",
...jobName ? { identifiedBody: `${jobName} needs attention.` } : {},
tag: `openclaw-cron-failed-${jobId}`
};
}
return null;
}
function preferenceFor(target, stateDir) {
const profileId = target.userProfileId;
const user = profileId ? getUserPreferences(profileId, [WEB_PUSH_USER_PREFERENCES_KEY], stateDir ? { env: {
...process.env,
OPENCLAW_STATE_DIR: stateDir
} } : {})[WEB_PUSH_USER_PREFERENCES_KEY] : void 0;
return resolveEffectiveWebPushPreferences({
user,
device: target.subscription.devicePreferences
});
}
/** Routes attention events to offline browsers without expanding live session visibility. */
function createEventWebPushDelivery(params) {
const log = params.log ?? defaultLog;
const deliver = (notification, event, payload, opts, mention) => {
(async () => {
if (listBoundWebPushSubscriptions(params.stateDir).length === 0) return;
const sender = await prepareWebPushNotificationSender(params.stateDir);
const cfg = params.getRuntimeConfig();
const recipientProfileId = mention && resolveUserProfileId(mention.recipientProfileId);
if (mention && !recipientProfileId) return;
const sessionPath = mention ? buildControlUiSessionPath({
namespace: "chat",
sessionKey: mention.sessionKey,
fallbackAgentId: mention.agentId,
mainKey: cfg.session?.mainKey,
exactKey: true
}) : void 0;
if (mention && !sessionPath) return;
const url = sessionPath ? resolveControlUiWebPushUrl(cfg, sessionPath.slice(1)) : void 0;
const targets = listCurrentWebPushTargets({
cfg,
requiredScopes: notification.category === "agent-question" ? [READ_SCOPE, QUESTIONS_SCOPE] : [READ_SCOPE],
...mention ? { visibilityScopes: [ADMIN_SCOPE] } : {},
stateDir: params.stateDir
});
const agentId = normalizeOptionalString(opts?.agentId ?? (isRecord(payload) ? payload.agentId : void 0));
const agentLabel = normalizeWebPushDisplayLabel(agentId);
const groups = /* @__PURE__ */ new Map();
for (const target of targets) {
if (mention && target.userProfileId !== recipientProfileId) continue;
const preferences = preferenceFor(target, params.stateDir);
if (!webPushCategoryEnabled(preferences, notification.category) || isWebPushQuietHours(preferences) || !webPushAgentAllowed(preferences, agentId)) continue;
const sessionKeys = opts?.sessionKeys ?? [];
if (sessionKeys.length > 0 && !canReceiveSessionEvent({
cfg,
client: webPushTargetClient(target),
sessionKeys,
...agentId ? { agentId } : {},
event,
payload
})) continue;
if (cfg.gateway?.roles && sessionKeys.length === 0) continue;
const title = `${preferences.label ? `${preferences.label} · ` : ""}${notification.title}`;
const body = preferences.detailLevel === "private" ? notification.body : notification.identifiedBody ?? (agentLabel ? `${agentLabel}: ${notification.body}` : notification.body);
const key = JSON.stringify({
title,
body
});
const group = groups.get(key) ?? {
title,
body,
subscriptions: []
};
group.subscriptions.push(target.subscription);
groups.set(key, group);
}
if (mention && !mention.isCurrent()) return;
const topic = createHash("sha256").update(notification.tag).digest("base64url").slice(0, 32);
const results = (await Promise.all([...groups.values()].map((group) => sender({
subscriptions: group.subscriptions,
payload: {
title: group.title,
body: group.body,
tag: notification.tag,
renotify: false,
...url ? { url } : {}
},
deliveryOptions: {
TTL: EVENT_PUSH_TTL_SECONDS,
urgency: notification.category.includes("failed") ? "high" : "normal",
topic
}
})))).flat();
const failed = results.filter((result) => !result.ok).length;
if (failed > 0) log.warn("event Web Push delivery failed", {
category: notification.category,
attempted: results.length,
failed
});
})().catch(() => {
log.warn("event Web Push delivery could not complete", { category: notification.category });
});
};
return {
handleEvent(event, payload, opts) {
const notification = resolveEventWebPushNotification(event, payload);
if (notification) deliver(notification, event, payload, opts);
},
deliverMention(mention) {
const senderLabel = normalizeWebPushDisplayLabel(mention.senderLabel) ?? "Someone";
const sessionTitle = normalizeWebPushDisplayLabel(mention.sessionTitle);
const id = createHash("sha256").update(mention.id).digest("base64url");
deliver({
category: "human-mentioned",
title: "OpenClaw mention",
body: "Someone mentioned you in a conversation.",
identifiedBody: `${senderLabel} mentioned you${sessionTitle ? ` in ${sessionTitle}` : ""}.`,
tag: `openclaw-mention-${id}`
}, "human-mentioned", void 0, {
agentId: mention.agentId,
sessionKeys: [mention.sessionKey]
}, mention);
}
};
}
//#endregion
//#region src/gateway/human-mention-policy.ts
const MAX_DIRECTORY_PROFILES = 1e4;
function scopesAllowRead(scopes) {
return roleScopesAllow({
role: "operator",
requestedScopes: [READ_SCOPE],
allowedScopes: scopes
});
}
/** UI labels are text, never identity or an email-address fallback. */
function humanMentionDisplayLabel(label, profileId) {
const text = label?.replace(/[\p{Cc}\p{Cf}]/gu, " ").replace(/\s+/gu, " ").trim();
return truncateUtf16Safe(text || `Person ${profileId.slice(0, 8)}`, 256);
}
function createHumanMentionPolicy(params) {
let profileVersion = -1;
const displays = /* @__PURE__ */ new Map();
let directory;
let eligibleDirectory;
function readProfile(profileId) {
const version = readUserProfileVersion();
if (profileVersion !== version) {
profileVersion = version;
displays.clear();
directory = void 0;
eligibleDirectory = void 0;
}
let profile = displays.get(profileId);
if (!profile) {
profile = resolveCurrentUserProfileDisplay(profileId);
if (displays.size >= MAX_DIRECTORY_PROFILES) {
const oldest = displays.keys().next().value;
if (oldest !== void 0) displays.delete(oldest);
}
displays.set(profileId, profile);
}
return profile.kind === "resolved" ? profile : void 0;
}
function identify(client, cfg) {
if (!client?.connect || client.invalidated === true || client.internal?.syntheticClient || (client.connect.role ?? "operator") !== "operator" || !scopesAllowRead(client.connect.scopes ?? [])) return err(errorShape(ErrorCodes.FORBIDDEN, "Human mentions require a signed-in operator."));
const verifiedProfile = client.authenticatedUserProfile;
if (!verifiedProfile?.profileId) return err(client.authenticatedGitHubIdentitySync ? authenticatedProfileUnavailableError() : errorShape(ErrorCodes.FORBIDDEN, "Human mentions require a verified user profile. Sign in to use mentions."));
const profile = readProfile(verifiedProfile.profileId);
if (!profile) return err(authenticatedProfileUnavailableError());
const policy = resolveOperatorRolePolicyForProfile(profile.profileId, cfg);
if (policy && !scopesAllowRead(policy.scopes)) return err(errorShape(ErrorCodes.FORBIDDEN, "Your operator role cannot read mentions."));
const admin = client.connect.scopes?.includes("operator.admin") && (!policy || policy.scopes.includes("operator.admin"));
const { entryFilter } = prepareSessionSharing({
cfg,
client: {
connect: {
...client.connect,
scopes: admin ? [ADMIN_SCOPE] : [READ_SCOPE]
},
internal: { operatorRoleActor: {
kind: "operator",
profileId: profile.profileId
} }
}
});
return ok({
profile,
canRead: (target) => entryFilter?.(target.sessionKey, target.entry) ?? true
});
}
function recipientProfile(profileId, target, cfg) {
const profile = readProfile(profileId);
if (!profile || target.entry.incognito === true || isIncognitoSessionKey(target.sessionKey)) return;
const policy = resolveOperatorRolePolicyForProfile(profile.profileId, cfg);
const scopes = policy?.scopes ?? ["operator.read"];
if (!scopesAllowRead(scopes)) return;
if (scopes.includes("operator.admin")) return profile;
return createProfileSessionEntryFilter({
profileId: profile.profileId,
sessionCap: policy?.sessions.others
})(target.sessionKey, target.entry) ? profile : void 0;
}
function resolveContext(client, input, cfg) {
const identified = identify(client, cfg);
if (!identified.ok) return identified;
const requester = identified.value;
if ("sessionKey" in input) {
const agent = resolveRequestedSessionAgentId(cfg, input.sessionKey, input.agentId);
if (!agent.ok) return err(agent.error);
const resolved = resolveSessionSharingTarget({
cfg,
sessionKey: input.sessionKey,
agentId: agent.agentId
});
const target = resolved && {
agentId: resolved.agentId,
sessionKey: resolved.canonicalKey,
entry: {
createdActor: resolved.entry.createdActor,
visibility: resolved.entry.visibility,
incognito: resolved.entry.incognito
}
};
if (!target || !requester.canRead(target)) return err(errorShape(ErrorCodes.INVALID_REQUEST, "Session was not found."));
return ok({
target,
profile: requester.profile
});
}
const agent = resolveRequestedSessionAgentId(cfg, void 0, input.agentId);
if (!agent.ok) return err(agent.error);
const creationError = authorizeGatewaySessionCreation({
cfg,
profileId: requester.profile.profileId,
agentId: agent.agentId
});
if (creationError) return err(creationError);
const visibility = resolveSessionVisibility({ visibility: input.visibility });
if (!isSessionVisibilityAllowed(cfg, visibility)) return err(errorShape(ErrorCodes.INVALID_REQUEST, "This session visibility is disabled."));
return ok({
profile: requester.profile,
target: {
agentId: agent.agentId,
entry: {
visibility,
createdActor: resolveOperatorSessionCreation({ authenticatedUserProfile: requester.profile }).actor
}
}
});
}
return {
identify,
readProfile,
recipientProfile,
invalidateDirectory() {
eligibleDirectory = void 0;
},
dispose() {
displays.clear();
directory = void 0;
eligibleDirectory = void 0;
},
mentionable(client, input) {
const cfg = params.getRuntimeConfig();
const context = resolveContext(client, input, cfg);
if (!context.ok) return context;
const { target, profile } = context.value;
if (!directory) {
const profiles = listProfiles().filter((candidate) => candidate.mergedInto === null);
directory = {
ids: profiles.slice(0, MAX_DIRECTORY_PROFILES).map((candidate) => candidate.id),
truncated: profiles.length > MAX_DIRECTORY_PROFILES
};
}
const key = JSON.stringify([
profileVersion,
target,
cfg.gateway?.roles
]);
if (eligibleDirectory?.key !== key) eligibleDirectory = {
key,
users: directory.ids.flatMap((id) => {
const candidate = recipientProfile(id, target, cfg);
return candidate ? [{
profileId: candidate.profileId,
displayName: humanMentionDisplayLabel(candidate.label, candidate.profileId),
avatarUrl: candidate.avatarUrl,
online: false
}] : [];
}),
truncated: directory.truncated
};
const query = input.query?.trim().toLocaleLowerCase() ?? "";
const users = eligibleDirectory.users.filter((candidate) => candidate.profileId !== profile.profileId && (!query || candidate.displayName.toLocaleLowerCase().includes(query)));
const names = /* @__PURE__ */ new Map();
for (const candidate of users) names.set(candidate.displayName, (names.get(candidate.displayName) ?? 0) + 1);
const online = /* @__PURE__ */ new Set();
for (const connected of params.getClients()) {
const id = connected.authenticatedUserProfile?.profileId;
if (id && !connected.internal?.syntheticClient) {
const current = readProfile(id);
if (current) online.add(current.profileId);
}
}
const projected = users.map((candidate) => ({
profileId: candidate.profileId,
displayName: (names.get(candidate.displayName) ?? 0) > 1 ? `${truncateUtf16Safe(candidate.displayName, 244)} (${candidate.profileId.slice(0, 8)})` : candidate.displayName,
avatarUrl: candidate.avatarUrl,
online: online.has(candidate.profileId)
}));
projected.sort((left, right) => Number(right.online) - Number(left.online) || left.displayName.localeCompare(right.displayName) || left.profileId.localeCompare(right.profileId));
return ok({
users: projected.slice(0, 100),
truncated: eligibleDirectory.truncated || projected.length > 100
});
},
validateRecipients(client, input, profileIds) {
if (profileIds.length === 0) return ok([]);
const cfg = params.getRuntimeConfig();
const context = resolveContext(client, input, cfg);
if (!context.ok) return context;
const { target, profile } = context.value;
const recipients = /* @__PURE__ */ new Set();
for (const id of profileIds) {
const candidate = recipientProfile(id, target, cfg);
if (!candidate || candidate.profileId === profile.profileId || profileIds.length > 10) return err(errorShape(ErrorCodes.INVALID_REQUEST, "One or more mentioned people are unavailable. Select the recipients again."));
recipients.add(candidate.profileId);
}
return ok([...recipients]);
}
};
}
//#endregion
//#region src/gateway/mention-inbox.ts
const RETENTION_MS = 6048e5;
const MAX_GLOBAL_ITEMS = 1e4;
const MAX_PROCESSED_SOURCES = 1e4;
const log$3 = createSubsystemLogger("gateway/mentions");
/** One Gateway lifetime owns temporary mention retention, acknowledgement, and replay suppression. */
function createMentionInbox(params) {
const policy = createHumanMentionPolicy(params);
const items = /* @__PURE__ */ new Map();
const itemsByProfile = /* @__PURE__ */ new Map();
const processed = /* @__PURE__ */ new Map();
const views = /* @__PURE__ */ new WeakMap();
let active = true;
let profileVersion = readUserProfileVersion();
let expiryTimer;
let capacityReported = false;
let profileInvalidationPending = false;
let nextExpiryAt = Infinity;
function removeItem(item) {
if (!item || !items.delete(item.id)) return false;
const profileItems = itemsByProfile.get(item.recipientProfileId);
profileItems?.delete(item);
if (profileItems?.size === 0) itemsByProfile.delete(item.recipientProfileId);
item.source.recipients.set(item.recipientProfileId, null);
return true;
}
function trimItems(retained, limit) {
const oldest = retained.values();
while (retained.size > limit) removeItem(oldest.next().value);
}
function indexItem(item) {
const retained = itemsByProfile.get(item.recipientProfileId) ?? /* @__PURE__ */ new Set();
retained.add(item);
itemsByProfile.set(item.recipientProfileId, retained);
trimItems(retained, 100);
}
function expireItems() {
const now = Date.now();
if (now < nextExpiryAt) return false;
let changed = false;
let next = Infinity;
for (const [key, source] of processed) {
if (source.expiresAt > now) {
next = Math.min(next, source.expiresAt);
continue;
}
for (const item of source.recipients.values()) changed = removeItem(item) || changed;
processed.delete(key);
}
nextExpiryAt = next;
if (processed.size < MAX_PROCESSED_SOURCES) capacityReported = false;
return changed;
}
function reconcileProfiles() {
const version = readUserProfileVersion();
if (version === profileVersion) return;
profileVersion = version;
for (const source of processed.values()) {
const recipients = /* @__PURE__ */ new Map();
for (const [profileId, item] of source.recipients) {
const canonical = policy.readProfile(profileId)?.profileId ?? profileId;
if (!recipients.has(canonical)) {
recipients.set(canonical, item);
if (item) item.recipientProfileId = canonical;
continue;
}
const previous = recipients.get(canonical);
if (item === null && previous) {
items.delete(previous.id);
recipients.set(canonical, null);
} else if (item) items.delete(item.id);
}
source.recipients = recipients;
}
itemsByProfile.clear();
for (const item of items.values()) if (policy.readProfile(item.recipientProfileId)) indexItem(item);
else removeItem(item);
}
function currentTarget(item, cfg, targets) {
const { source, message } = item;
const { agentId, sessionKey, senderProfileId } = message.content;
if (!active || items.get(item.id) !== item || source.expiresAt <= Date.now()) return;
const key = JSON.stringify([agentId, sessionKey]);
let resolved = targets?.get(key);
if (resolved === void 0) {
resolved = resolveSessionSharingTarget({
cfg,
sessionKey,
agentId
});
targets?.set(key, resolved);
}
if (!resolved || resolved.entry.sessionId !== message.sessionId) return;
const target = {
agentId: resolved.agentId,
sessionKey: resolved.canonicalKey,
entry: resolved.entry
};
const recipient = policy.recipientProfile(item.recipientProfileId, target, cfg);
const sender = policy.readProfile(senderProfileId);
return recipient && recipient.profileId !== sender?.profileId ? {
target,
recipient,
sender
} : void 0;
}
function projectItem(item, current) {
const { content } = item.message;
return {
...content,
id: item.id,
expiresAt: item.source.expiresAt,
senderProfileId: current.sender?.profileId ?? content.senderProfileId,
senderLabel: humanMentionDisplayLabel(current.sender?.label, content.senderProfileId),
...current.sender ? { senderAvatarUrl: current.sender.avatarUrl } : {},
sessionTitle: truncateUtf16Safe((deriveSessionTitle(current.target.entry) ?? "Conversation").replace(/[\p{Cc}\p{Cf}]/gu, " ").replace(/\s+/gu, " ").trim(), 256) || "Conversation"
};
}
function readView(client, cfg = params.getRuntimeConfig()) {
const identified = policy.identify(client, cfg);
if (!identified.ok) return identified;
const requester = identified.value;
const visible = [];
const targets = /* @__PURE__ */ new Map();
const profileItems = itemsByProfile.get(requester.profile.profileId);
for (const item of [...profileItems ?? []].toReversed()) {
const current = currentTarget(item, cfg, targets);
if (current && requester.canRead(current.target)) visible.push(projectItem(item, current));
}
const signature = createHash("sha256").update(JSON.stringify([requester.profile.profileId, visible])).digest("hex");
const previous = client && views.get(client);
const revision = previous ? previous.revision + Number(signature !== previous.signature) : 0;
if (client) views.set(client, {
signature,
revision
});
return ok({
gatewayInstanceId: params.gatewayInstanceId,
revision,
items: visible
});
}
function refreshConnectedViews() {
const cfg = params.getRuntimeConfig();
for (const client of params.getClients()) {
if (!client.connId) continue;
const previous = views.get(client);
const result = readView(client, cfg);
if (!result.ok || (previous ? previous.revision === result.value.revision : result.value.items.length === 0)) continue;
params.broadcastToConnIds("mentions.changed", {
gatewayInstanceId: params.gatewayInstanceId,
revision: result.value.revision
}, /* @__PURE__ */ new Set([client.connId]));
}
}
function scheduleExpiry() {
if (expiryTimer || processed.size === 0 || !active) return;
expiryTimer = setTimeout(() => {
expiryTimer = void 0;
refresh();
}, Math.max(1, nextExpiryAt - Date.now()));
expiryTimer.unref?.();
}
function refresh() {
if (!active) return;
try {
expireItems();
reconcileProfiles();
refreshConnectedViews();
scheduleExpiry();
} catch {
log$3.warn("Unable to refresh the temporary mention Inbox; current reads will retry.");
}
}
function invalidate() {
policy.invalidateDirectory();
refresh();
}
const stopProfiles = onUserProfilesChanged(() => {
if (profileInvalidationPending) return;
profileInvalidationPending = true;
queueMicrotask(() => {
profileInvalidationPending = false;
invalidate();
});
});
const stopSessions = onSessionIdentityMutation(() => invalidate());
function readOperation(operation) {
if (active) try {
return operation();
} catch {
log$3.warn("The temporary mention Inbox could not read its current authorization.");
}
return err(errorShape(ErrorCodes.UNAVAILABLE, "The mention Inbox is unavailable. Reconnect to retry.", { retryable: true }));
}
return {
mentionable: (...args) => readOperation(() => policy.mentionable(...args)),
validateRecipients: (...args) => readOperation(() => policy.validateRecipients(...args)),
list(client) {
return readOperation(() => {
reconcileProfiles();
if (expireItems()) refreshConnectedViews();
return readView(client);
});
},
dismiss(client, ids) {
return readOperation(() => {
reconcileProfiles();
expireItems();
const current = readView(client);
if (!current.ok) return current;
const owned = new Set(current.value.items.map((item) => item.id));
for (const id of ids) if (owned.has(id)) removeItem(items.get(id));
refresh();
return readView(client);
});
},
recordCommittedInput(input) {
try {
if (!active || input.recipientProfileIds.length === 0) return;
const references = [
input.sourceId,
input.sessionId,
input.messageId,
input.senderProfileId,
...input.recipientProfileIds
];
if (input.recipientProfileIds.length > 10 || input.sessionKey.length > 512 || references.some((value) => !value || value.length > 256)) {
log$3.warn("Skipped mention delivery with invalid committed references.");
return;
}
expireItems();
reconcileProfiles();
const cfg = params.getRuntimeConfig();
const resolved = resolveSessionSharingTarget({
cfg,
sessionKey: input.sessionKey,
agentId: input.agentId
});
if (!resolved || resolved.entry.sessionId !== input.sessionId) {
log$3.debug("Skipped mention delivery because its committed session changed.");
return;
}
const sourceKey = createHash("sha256").update(JSON.stringify([
resolved.agentId,
resolved.canonicalKey,
input.sessionId,
input.sourceId
])).digest("hex");
if (processed.has(sourceKey)) return;
if (processed.size >= MAX_PROCESSED_SOURCES) {
if (!capacityReported) {
log$3.warn("Temporary mention retention reached its replay budget; new mention alerts are skipped until retained sources expire.");
capacityReported = true;
}
return;
}
const now = Date.now();
const source = {
expiresAt: now + RETENTION_MS,
recipients: /* @__PURE__ */ new Map()
};
processed.set(sourceKey, source);
nextExpiryAt = Math.min(nextExpiryAt, source.expiresAt);
const sender = policy.readProfile(input.senderProfileId);
const target = {
agentId: resolved.agentId,
sessionKey: resolved.canonicalKey,
entry: resolved.entry
};
const excerpt = input.excerpt ? truncateUtf16Safe(flattenMarkdownToPlainText(truncateUtf16Safe(input.excerpt, 2048)).replace(/[\p{Cc}\p{Cf}]/gu, " ").replace(/\s+/gu, " ").trim(), 280) : void 0;
const message = {
sessionId: input.sessionId,
content: {
senderProfileId: sender?.profileId ?? input.senderProfileId,
sessionKey: target.sessionKey,
agentId: target.agentId,
messageId: input.messageId,
createdAt: now,
...excerpt ? { excerpt } : {}
}
};
const created = [];
let unavailableRecipients = 0;
for (const profileId of input.recipientProfileIds) {
const recipient = policy.recipientProfile(profileId, target, cfg);
const canonicalId = recipient?.profileId ?? profileId;
if (source.recipients.has(canonicalId)) continue;
source.recipients.set(canonicalId, null);
if (!sender || !recipient || sender.profileId === recipient.profileId) {
unavailableRecipients += 1;
continue;
}
const item = {
id: randomUUID(),
recipientProfileId: recipient.profileId,
source,
message
};
items.set(item.id, item);
source.recipients.set(recipient.profileId, item);
indexItem(item);
trimItems(items, MAX_GLOBAL_ITEMS);
created.push(item);
}
if (unavailableRecipients > 0) log$3.debug(`Skipped ${unavailableRecipients} unavailable mention recipients for committed input.`);
refresh();
if (!params.onMentionCreated) return;
for (const item of created) {
const current = currentTarget(item, params.getRuntimeConfig());
if (!current) continue;
const projected = projectItem(item, current);
params.onMentionCreated({
id: item.id,
recipientProfileId: current.recipient.profileId,
sessionKey: projected.sessionKey,
agentId: projected.agentId,
senderLabel: projected.senderLabel,
sessionTitle: projected.sessionTitle,
isCurrent: () => {
try {
return Boolean(currentTarget(item, params.getRuntimeConfig()));
} catch {
return false;
}
}
});
}
} catch {
log$3.warn("Mention delivery could not be completed; the posted message is unchanged.");
}
},
invalidate,
dispose() {
active = false;
stopProfiles();
stopSessions();
policy.dispose();
if (expiryTimer) {
clearTimeout(expiryTimer);
expiryTimer = void 0;
}
items.clear();
itemsByProfile.clear();
processed.clear();
}
};
}
//#endregion
//#region src/gateway/server-broadcast.ts
const EVENT_SCOPE_GUARDS = {
agent: [READ_SCOPE],
chat: [READ_SCOPE],
"chat.metadata.changed": [READ_SCOPE],
"board.changed": [READ_SCOPE],
"board.command": [READ_SCOPE],
"progressCard.changed": [READ_SCOPE],
"ui.command": [READ_SCOPE],
"chat.send_timing": [READ_SCOPE],
"chat.side_result": [READ_SCOPE],
cron: [READ_SCOPE],
health: [],
"exec.approval.requested": [APPROVALS_SCOPE],
"exec.approval.resolved": [APPROVALS_SCOPE],
"question.requested": [QUESTIONS_SCOPE],
"question.resolved": [QUESTIONS_SCOPE],
heartbeat: [],
"plugin.approval.requested": [APPROVALS_SCOPE],
"plugin.approval.resolved": [APPROVALS_SCOPE],
"openclaw.approval.requested": [APPROVALS_SCOPE],
"openclaw.approval.resolved": [APPROVALS_SCOPE],
presence: [READ_SCOPE],
shutdown: [],
"gateway.suspension": [],
tick: [],
"talk.event": [READ_SCOPE],
"talk.mode": [TALK_SCOPE],
task: [READ_SCOPE],
"task.suggestion": [READ_SCOPE],
"update.available": [],
[GATEWAY_EVENT_UPDATE_RUN_CHANGED]: [ADMIN_SCOPE],
"config.changed": [READ_SCOPE],
"users.prefs.changed": [READ_SCOPE],
"mentions.changed": [READ_SCOPE],
"skills.changed": [READ_SCOPE],
"voicewake.changed": [READ_SCOPE],
"voicewake.routing.changed": [READ_SCOPE],
[GATEWAY_EVENT_DEVICE_PAIR_CHANGED]: [PAIRING_SCOPE],
"device.pair.requested": [PAIRING_SCOPE],
"device.pair.resolved": [PAIRING_SCOPE],
"device.pair.setup.completed": [PAIRING_SCOPE],
"device.pair.setup.deliveryUncertain": [PAIRING_SCOPE],
"node.pair.requested": [PAIRING_SCOPE],
"node.pair.resolved": [PAIRING_SCOPE],
"node.presence": [READ_SCOPE],
"node.hostStats": [READ_SCOPE],
[GATEWAY_EVENT_NODE_RUNNER_INVENTORY_CHANGED]: [READ_SCOPE],
"sessions.catalog.host": [READ_SCOPE],
"sessions.changed": [READ_SCOPE],
"controlUi.sessionPullRequests.changed": [READ_SCOPE],
"plugins.controlUi.changed": [READ_SCOPE],
"session.approval": [APPROVALS_SCOPE],
"session.message": [READ_SCOPE],
"session.observer": [READ_SCOPE],
"session.operation": [READ_SCOPE],
"session.sharing": [READ_SCOPE],
"session.sharing.evidence": [READ_SCOPE],
"session.suggestion": [READ_SCOPE],
"session.typing": [READ_SCOPE],
"session.tool": [READ_SCOPE],
"terminal.data": [ADMIN_SCOPE],
"terminal.exit": [ADMIN_SCOPE],
"portal.changed": [READ_SCOPE]
};
const log$2 = createSubsystemLogger("gateway/broadcast");
const SESSION_SUBSCRIPTION_EVENTS = /* @__PURE__ */ new Set([
"agent",
"chat",
"chat.side_result",
"session.observer",
"session.tool"
]);
function serializeFrameField(name, value) {
const fieldJSON = JSON.stringify({ [name]: value });
const keyJSON = JSON.stringify(name);
const prefix = `{${keyJSON}:`;
return fieldJSON.startsWith(prefix) ? `,${keyJSON}:${fieldJSON.slice(prefix.length, -1)}` : "";
}
function resolveBroadcastSessionScope(payload, explicit, explicitAgentId) {
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return {
sessionKeys: explicit ?? [],
...explicitAgentId ? { agentId: explicitAgentId } : {}
};
const record = payload;
const source = [
record,
record.suggestion,
record.request
].find((candidate) => typeof candidate?.sessionKey === "string" && candidate.sessionKey.trim());
const sessionKey = typeof source?.sessionKey === "string" ? source.sessionKey.trim() : "";
const agentId = explicitAgentId ?? (typeof source?.agentId === "string" ? source.agentId.trim() || void 0 : void 0);
return {
sessionKeys: explicit?.length ? explicit : sessionKey ? [sessionKey] : [],
...agentId ? { agentId } : {}
};
}
function hasEventScope(client, event, explicitPluginScope) {
if (client.connectionKind === "worker") return false;
const role = client.connect.role ?? "operator";
const scopes = Array.isArray(client.connect.scopes) ? client.connect.scopes : [];
if (explicitPluginScope) {
if (role !== "operator") return false;
if (scopes.includes("operator.admin")) return true;
return explicitPluginScope === "operator.read" ? scopes.includes("operator.read") || scopes.includes("operator.write") : explicitPluginScope === "operator.write" && scopes.includes("operator.write");
}
const required = EVENT_SCOPE_GUARDS[event];
if (!required && event.startsWith("plugin.")) {
if (role !== "operator") return false;
return scopes.includes("operator.write") || scopes.includes("operator.admin");
}
if (!required) return false;
if (required.length === 0) return true;
if (role !== "operator") return false;
if (scopes.includes("operator.admin")) return true;
if (required.includes("operator.read")) return scopes.includes("operator.read") || scopes.includes("operator.write");
if (required.includes("operator.talk")) return scopes.includes("operator.talk") || scopes.includes("operator.write");
return required.some((scope) => scopes.includes(scope));
}
const MAX_SERVER_FRAME_HEADER_BYTES = 10;
function frameWithSequence(base, seq, payload = base.payloadFragment) {
return `{"type":"event","event":${base.eventJSON}${payload},"seq":${seq}${base.stateVersionFragment}}`;
}
function createGatewayBroadcaster(params) {
const clientSeq = /* @__PURE__ */ new WeakMap();
const reportedSlowPayloadClients = /* @__PURE__ */ new WeakSet();
const deliveries = /* @__PURE__ */ new WeakMap();
const deliveryFor = (client) => {
let state = deliveries.get(client);
if (!state || state.socket !== client.socket) {
if (state) clearPending(state);
state = {
socket: client.socket,
retired: false,
inFlight: 0,
draining: false,
bytes: 0,
groups: /* @__PURE__ */ new Map(),
pending: /* @__PURE__ */ new Set()
};
deliveries.set(client, state);
}
return state;
};
const bufferedBytes = (state) => state.socket.bufferedAmount + state.bytes;
const takePending = (state, entry) => {
state.pending.delete(entry);
const group = state.groups.get(entry.group);
group.entries.delete(entry.key);
if (!group.entries.size) {
entry.group.removeEventListener("abort", group.retire);
state.groups.delete(entry.group);
}
state.bytes -= entry.bytes;
};
const clearPending = (state) => {
for (const entry of state.pending) takePending(state, entry);
};
const isCurrent = (predicate) => {
try {
return predicate?.() !== false;
} catch {
return false;
}
};
const drain = (state, group) => {
if (state.retired || state.draining) return;
state.draining = true;
try {
for (const entry of state.pending) {
if (group ? entry.group !== group : state.inFlight !== 0) {
if (group) continue;
break;
}
takePending(state, entry);
try {
entry.send();
} catch (err) {
log$2.error(`broadcast pending send failed: ${formatErrorMessage(err)}`);
}
}
} finally {
state.draining = false;
}
};
const broadcastInternal = (event, payload, opts, targetConnIds, explicitPluginScope, retained) => {
if (!retained && event === "sessions.changed") queuePluginSessionsChanged(payload);
const live = opts?.liveText;
if (params.clients.size === 0) return;
const { sessionKeys, agentId } = resolveBroadcastSessionScope(payload, opts?.sessionKeys, opts?.agentId);
const isTargeted = Boolean(targetConnIds);
const presencePayload = event === "presence" ? payload : void 0;
let projectPresence;
let outboundEventLogged = false;
let frameBase = retained?.base;
let frameFields;
const frameBaseFor = (value) => {
frameFields ??= {
eventJSON: JSON.stringify(event),
stateVersionFragment: opts?.stateVersion === void 0 ? "" : serializeFrameField("stateVersion", opts.stateVersion)
};
return {
...frameFields,
payloadFragment: presencePayload ? "" : serializeFrameField("payload", value)
};
};
const getFrameBase = () => {
return frameBase ??= frameBaseFor(payload);
};
const sessionSubscriptionVerified = opts?.sessionSubscriptionVerified === true;
const isSessionSubscriptionEvent = SESSION_SUBSCRIPTION_EVENTS.has(event);
const sessionMessageSubscribers = params.sessionMessageSubscribers;
let sessionSubscriberConnIdsByKey;
const recipients = retained ? [retained.client] : targetConnIds ? params.clients.getByConnectionIds(targetConnIds) : params.clients;
for (const c of recipients) {
if (!params.clients.has(c) || retained && c.socket !== retained.socket || c.invalidated === true || c.socket.readyState !== 1) continue;
if (!hasEventScope(c, event, explicitPluginScope)) continue;
if (sessionKeys.length > 0 && params.canReceiveSessionEvent && !params.canReceiveSessionEvent(c, sessionKeys, agentId, event, payload)) continue;
if ((event === "session.typing" || sessionSubscriptionVerified || (isBrowserCopilotClient(c.connect.client) || hasGatewayClientCap(c.connect.caps, GATEWAY_CLIENT_CAPS.SESSION_SCOPED_EVENTS)) && isSessionSubscriptionEvent) && !(isTargeted && sessionSubscriptionVerified && !retained)) {
if (!sessionKeys.length || !sessionMessageSubscribers) continue;
sessionSubscriberConnIdsByKey ??= [];
let subscribed = false;
let sessionKeyIndex = 0;
for (const sessionKey of sessionKeys) {
if ((sessionSubscriberConnIdsByKey[sessionKeyIndex] ??= sessionMessageSubscribers.get(sessionKey)).has(c.connId)) {
subscribed = true;
break;
}
sessionKeyIndex += 1;
}
if (!subscribed) continue;
}
if (retained && !isCurrent(live?.isCurrent) || live?.coalesce && live.group.aborted) continue;
if (!outboundEventLogged) {
outboundEventLogged = true;
logWs("out", "event", () => {
const logMeta = {
event,
seq: "per-client",
clients: params.clients.size,
targets: targetConnIds ? targetConnIds.size : void 0,
dropIfSlow: opts?.dropIfSlow,
presenceVersion: opts?.stateVersion?.presence,
healthVersion: opts?.stateVersion?.health
};
if (event === "agent") Object.assign(logMeta, summarizeAgentEventForWsLog(payload));
return logMeta;
});
}
const state = deliveryFor(c);
if (live && !live.coalesce) drain(state, live.group);
if (state.retired) continue;
const nextSeq = (clientSeq.get(c) ?? 0) + 1;
const bufferedAmount = bufferedBytes(state);
const slow = bufferedAmount > MAX_BUFFERED_BYTES;
if (!slow) reportedSlowPayloadClients.delete(c);
else if (!reportedSlowPayloadClients.has(c)) {
reportedSlowPayloadClients.add(c);
logRejectedLargePayload({
surface: "gateway.ws.outbound_buffer",
bytes: bufferedAmount,
limitBytes: MAX_BUFFERED_BYTES,
reason: opts?.dropIfSlow ? "ws_send_buffer_drop" : "ws_send_buffer_close"
});
}
if (slow && opts?.dropIfSlow) {
clientSeq.set(c, nextSeq);
continue;
}
if (slow) {
state.retired = true;
clearPending(state);
try {
c.socket.close(1008, "slow consumer");
} catch {}
c.socket.terminate();
continue;
}
if (!retained && live?.coalesce && state.inFlight > 0) {
let previous = state.groups.get(live.group)?.entries.get(live.coalesce.key);
if (previous && !isCurrent(previous.isCurrent)) {
takePending(state, previous);
previous = void 0;
}
try {
const nextPayload = previous ? live.coalesce.merge(previous.payload, payload) : payload;
const base = previous ? frameBaseFor(nextPayload) : getFrameBase();
const bytes = Buffer.byteLength(frameWithSequence(base, Number.MAX_SAFE_INTEGER)) + MAX_SERVER_FRAME_HEADER_BYTES;
if (bufferedBytes(state) - (previous?.bytes ?? 0) + bytes <= 52428800) {
if (previous) takePending(state, previous);
const socket = c.socket;
const entry = {
group: live.group,
key: live.coalesce.key,
payload: nextPayload,
bytes,
isCurrent: live.isCurrent,
send: () => broadcastInternal(event, nextPayload, opts, targetConnIds, explicitPluginScope, {
client: c,
socket,
base
})
};
let group = state.groups.get(live.group);
if (!group) {
const entries = /* @__PURE__ */ new Map();
const retire = () => {
for (const pending of entries.values()) takePending(state, pending);
};
group = {
entries,
retire
};
state.groups.set(live.group, group);
live.group.addEventListener("abort", retire, { once: true });
}
group.entries.set(entry.key, entry);
state.pending.add(entry);
state.bytes += bytes;
continue;
}
} catch (err) {
log$2.error(`broadcast serialization failed for event ${event}: ${formatErrorMessage(err)}`);
return;
}
drain(state, live.group);
broadcastInternal(event, payload, opts, targetConnIds, explicitPluginScope, {
client: c,
socket: c.socket,
base: getFrameBase()
});
continue;
}
let frame;
try {
const base = getFrameBase();
let payloadFragment = base.payloadFragment;
if (presencePayload) {
if (!params.preparePresenceProjection) throw new Error("presence recipient projection unavailable");
projectPresence ??= params.preparePresenceProjection(presencePayload.presence);
payloadFragment = serializeFrameField("payload", {
...presencePayload,
presence: projectPresence(c)
});
}
frame = frameWithSequence(base, nextSeq, payloadFragment);
} catch (err) {
log$2.error(`broadcast serialization failed for event ${event}: ${formatErrorMessage(err)}`);
return;
}
clientSeq.set(c, nextSeq);
state.inFlight += 1;
let finished = false;
const sent = (err) => {
if (finished) return;
finished = true;
state.inFlight -= 1;
if (state.retired) return;
if (err) {
state.retired = true;
clearPending(state);
log$2.error(`broadcast send failed conn=${c.connId}: ${formatErrorMessage(err)}`, { event });
state.socket.terminate();
} else drain(state);
};
try {
state.socket.send(frame, sent);
} catch (err) {
sent(err instanceof Error ? err : new Error(String(err)));
}
}
};
const broadcast = (event, payload, opts) => {
params.onBroadcast?.(event, payload, opts);
broadcastInternal(event, payload, opts);
};
const broadcastToConnIds = (event, payload, connIds, opts) => {
broadcastInternal(event, payload, opts, connIds);
};
const getBufferedAmount = (connId) => {
const client = params.clients.getByConnectionId(connId);
if (!client || client.invalidated || client.socket.readyState !== 1) return;
const state = deliveryFor(client);
return state.retired ? void 0 : bufferedBytes(state);
};
const broadcastPluginEvent = (event, payload, scope) => {
if (!event.startsWith("plugin.") || event.startsWith("plugin.approval.")) throw new Error(`invalid plugin gateway event: ${event}`);
if (scope !== "operator.read" && scope !== "operator.write" && scope !== "operator.admin") throw new Error("invalid plugin gateway event scope");
broadcastInternal(event, payload, void 0, void 0, scope);
};
return {
broadcast,
broadcastToConnIds,
broadcastPluginEvent,
getBufferedAmount
};
}
//#endregion
//#region src/gateway/server-connection-work.ts
/** Owns received work and connection cleanup until this Gateway generation settles. */
var GatewayConnectionWork = class extends AsyncWorkScope {
constructor(..._args) {
super(..._args);
this.connections = /* @__PURE__ */ new Set();
}
trackCleanup(run) {
return this.track(async () => {
try {
await run();
} catch (error) {
this.failure ??= { error };
throw error;
}
});
}
registerConnection(close) {
const closed = createDeferredCore();
this.connections.add(close);
this.track(() => closed.promise);
return () => {
this.connections.delete(close);
closed.resolve();
};
}
async drain() {
this.beginClose();
for (const close of this.connections) {
this.connections.delete(close);
try {
close();
} catch (error) {
this.failure ??= { error };
}
}
await super.drain();
if (this.failure) throw new Error("Gateway connection work failed to close cleanly", { cause: this.failure.error });
}
};
//#endregion
//#region src/gateway/server/client-registry.ts
var GatewayClientRegistry = class extends Set {
#byConnectionId = /* @__PURE__ */ new Map();
#nextOrder = 0;
constructor(clients) {
super();
for (const client of clients ?? []) this.add(client);
}
add(client) {
if (!this.has(client)) this.#byConnectionId.set(client.connId, {
client,
order: this.#nextOrder++
});
return super.add(client);
}
delete(client) {
if (!super.delete(client)) return false;
if (this.#byConnectionId.get(client.connId)?.client === client) this.#byConnectionId.delete(client.connId);
return true;
}
clear() {
super.clear();
this.#byConnectionId.clear();
}
getByConnectionId(connId) {
return this.#byConnectionId.get(connId)?.client;
}
getByConnectionIds(connIds) {
const indexed = [];
for (const connId of connIds) {
const entry = this.#byConnectionId.get(connId);
if (entry) indexed.push(entry);
}
if (indexed.length > 1) indexed.sort((a, b) => a.order - b.order);
return indexed.map((entry) => entry.client);
}
};
//#endregion
//#region src/gateway/server-connection-state.ts
/** Creates transport-independent connection, subscription, and run state. */
function createGatewayConnectionState(params) {
const loadRuntimeConfig = params.getRuntimeConfig ?? (() => params.cfg);
const clients = new GatewayClientRegistry();
const isConnectionActive = (connId) => {
const client = clients.getByConnectionId(connId);
return Boolean(client && !client.invalidated);
};
const sessionEventSubscribers = createSessionEventSubscriberRegistry(isConnectionActive);
const sessionMessageSubscribers = createSessionMessageSubscriberRegistry(isConnectionActive);
const eventWebPush = createEventWebPushDelivery({ getRuntimeConfig: loadRuntimeConfig });
const gatewayBroadcaster = createGatewayBroadcaster({
clients,
preparePresenceProjection: (presence) => createPresenceRecipientProjection({
cfg: loadRuntimeConfig(),
presence
}),
sessionMessageSubscribers,
canReceiveSessionEvent: (client, sessionKeys, agentId, event, payload) => canReceiveSessionEvent({
cfg: loadRuntimeConfig(),
client,
sessionKeys,
agentId,
event,
payload
}),
onBroadcast: (event, payload, opts) => eventWebPush.handleEvent(event, payload, opts)
});
const mentionInbox = createMentionInbox({
gatewayInstanceId: params.bootId,
getRuntimeConfig: loadRuntimeConfig,
*getClients() {
for (const client of clients) if (!client.invalidated && client.socket.readyState === 1 && (client.connect.role ?? "operator") === "operator") yield client;
},
broadcastToConnIds: gatewayBroadcaster.broadcastToConnIds,
onMentionCreated: eventWebPush.deliverMention
});
const agentRunSeq = /* @__PURE__ */ new Map();
const dedupe = /* @__PURE__ */ new Map();
const chatRunState = createChatRunState();
const chatRunRegistry = chatRunState.registry;
const addChatRun = chatRunRegistry.add;
const removeChatRun = chatRunRegistry.remove;
const chatAbortControllers = /* @__PURE__ */ new Map();
const chatQueuedTurns = /* @__PURE__ */ new Map();
const toolEventRecipients = chatRunState.toolEventRecipients;
return {
clients,
connectionWork: new GatewayConnectionWork(),
mentionInbox,
isConnectionActive,
...gatewayBroadcaster,
agentRunSeq,
dedupe,
chatRunState,
addChatRun,
removeChatRun,
chatAbortControllers,
chatQueuedTurns,
toolEventRecipients,
sessionEventSubscribers,
sessionMessageSubscribers
};
}
//#endregion
//#region src/gateway/control-ui-asset-manifest.ts
const CONTROL_UI_ASSET_MANIFEST_FILENAME = "asset-manifest.json";
function hashControlUiAssetManifestEntries(entries) {
const hash = createHash("sha256");
for (const entry of entries) {
hash.update(entry.path);
hash.update("\0");
hash.update(String(entry.size));
hash.update("\0");
hash.update(entry.sha256);
hash.update("\n");
}
return hash.digest("hex");
}
//#endregion
//#region src/gateway/control-ui-asset-manifest-parse.ts
const CONTROL_UI_ASSET_SHA256_PATTERN = /^[a-f0-9]{64}$/u;
const CONTROL_UI_ASSET_MANIFEST_MAX_ENTRIES = 8192;
const CONTROL_UI_ASSET_MANIFEST_MAX_FILE_BYTES = 67108864;
const CONTROL_UI_ASSET_MANIFEST_MAX_TOTAL_BYTES = 536870912;
function hasExactKeys(record, keys) {
const actual = Object.keys(record);
return actual.length === keys.length && actual.every((key) => keys.includes(key));
}
function isControlUiAssetManifestPath(value) {
if (!value.startsWith("assets/") || value.includes("\\") || value.includes("\0")) return false;
const normalized = path.posix.normalize(value);
return normalized === value && !normalized.endsWith("/");
}
function parseControlUiAssetManifest(value) {
if (!isRecord(value) || !hasExactKeys(value, [
"assets",
"generation",
"version"
])) return null;
if (value.version !== 1 || typeof value.generation !== "string" || !CONTROL_UI_ASSET_SHA256_PATTERN.test(value.generation) || !Array.isArray(value.assets) || value.assets.length === 0 || value.assets.length > CONTROL_UI_ASSET_MANIFEST_MAX_ENTRIES) return null;
const assets = [];
const paths = /* @__PURE__ */ new Set();
let totalBytes = 0;
for (const candidate of value.assets) {
if (!isRecord(candidate) || !hasExactKeys(candidate, [
"path",
"sha256",
"size"
])) return null;
const assetPath = candidate.path;
const size = candidate.size;
const sha256 = candidate.sha256;
if (typeof assetPath !== "string" || !isControlUiAssetManifestPath(assetPath) || paths.has(assetPath) || typeof size !== "number" || !Number.isSafeInteger(size) || size < 0 || size > CONTROL_UI_ASSET_MANIFEST_MAX_FILE_BYTES || typeof sha256 !== "string" || !CONTROL_UI_ASSET_SHA256_PATTERN.test(sha256)) return null;
totalBytes += size;
if (totalBytes > CONTROL_UI_ASSET_MANIFEST_MAX_TOTAL_BYTES) return null;
paths.add(assetPath);
assets.push({
path: assetPath,
sha256,
size
});
}
for (let index = 1; index < assets.length; index += 1) if (assets[index - 1].path.localeCompare(assets[index].path) > 0) return null;
if (hashControlUiAssetManifestEntries(assets) !== value.generation) return null;
return {
version: 1,
generation: value.generation,
assets
};
}
//#endregion
//#region src/gateway/control-ui-asset-retention.ts
const CONTROL_UI_RETAINED_GENERATION_LIMIT = 3;
const CONTROL_UI_RETAINED_ASSET_MAX_BYTES = 100663296;
const CONTROL_UI_GENERATION_PATTERN = /^[a-f0-9]{64}$/u;
const CONTROL_UI_STAGING_PATTERN = /^\.staging-[0-9]+-[a-f0-9-]+$/u;
const CONTROL_UI_STAGING_MAX_AGE_MS = 36e5;
const CONTROL_UI_MANIFEST_MAX_BYTES = 4194304;
const CONTROL_UI_ASSET_IO_BUFFER_BYTES = 65536;
function resolveControlUiAssetCacheDir() {
return path.join(resolveStateDir(), "cache", "control-ui-assets");
}
function throwIfCancelled(operation) {
operation?.signal?.throwIfAborted();
if (operation?.isCancelled?.()) throw new DOMException("Control UI asset retention cancelled", "AbortError");
}
async function readCachedGeneration(directory, operation) {
try {
throwIfCancelled(operation);
const stats = await fs$1.lstat(directory);
if (stats.isSymbolicLink() || !stats.isDirectory()) return null;
const realPath = await fs$1.realpath(directory);
if (!isWithinDir(path.dirname(directory), realPath)) return null;
const manifest = await readAssetManifest(realPath, operation);
if (manifest.generation !== path.basename(directory)) return null;
for (const asset of manifest.assets) await verifyAsset({
entry: asset,
operation,
root: realPath,
rootRealPath: realPath
});
const currentStats = await fs$1.lstat(directory);
throwIfCancelled(operation);
if (!sameDirectory(stats, currentStats)) return null;
return {
assetPaths: new Set(manifest.assets.map((asset) => asset.path)),
bytes: manifest.assets.reduce((total, asset) => total + asset.size, 0),
directory,
generation: manifest.generation,
stats: currentStats,
realPath
};
} catch {
throwIfCancelled(operation);
return null;
}
}
function sameDirectory(left, right) {
return right.isDirectory() && !right.isSymbolicLink() && left.dev === right.dev && left.ino === right.ino;
}
function compareGenerations(left, right) {
return right.stats.mtimeMs - left.stats.mtimeMs || left.generation.localeCompare(right.generation);
}
async function readCacheInventory(cacheDir, operation, verified = []) {
const generations = [];
const directories = /* @__PURE__ */ new Map();
let cacheRealPath;
let entries;
try {
throwIfCancelled(operation);
cacheRealPath = await fs$1.realpath(cacheDir);
entries = await fs$1.readdir(cacheRealPath, { withFileTypes: true });
} catch {
throwIfCancelled(operation);
return {
generations,
directories
};
}
const known = new Map(verified.map((generation) => [generation.generation, generation]));
for (const entry of entries) {
throwIfCancelled(operation);
if (!entry.isDirectory() || entry.isSymbolicLink()) continue;
const directory = path.join(cacheRealPath, entry.name);
const stats = await fs$1.lstat(directory).catch(() => null);
if (!stats || !stats.isDirectory() || stats.isSymbolicLink()) continue;
directories.set(directory, stats);
if (!CONTROL_UI_GENERATION_PATTERN.test(entry.name)) continue;
const previous = known.get(entry.name);
const generation = previous && sameDirectory(previous.stats, stats) ? {
...previous,
stats
} : await readCachedGeneration(directory, operation);
if (generation) generations.push(generation);
}
throwIfCancelled(operation);
return {
generations: generations.toSorted(compareGenerations),
directories
};
}
async function readAssetManifest(root, operation) {
const manifestPath = path.join(root, CONTROL_UI_ASSET_MANIFEST_FILENAME);
throwIfCancelled(operation);
const stats = await fs$1.lstat(manifestPath);
throwIfCancelled(operation);
if (stats.isSymbolicLink() || !stats.isFile() || stats.size > CONTROL_UI_MANIFEST_MAX_BYTES) throw new Error(`Invalid Control UI asset manifest: ${manifestPath}`);
const manifest = parseControlUiAssetManifest(JSON.parse(await fs$1.readFile(manifestPath, {
encoding: "utf8",
signal: operation?.signal
})));
throwIfCancelled(operation);
if (!manifest) throw new Error(`Invalid Control UI asset manifest: ${manifestPath}`);
return manifest;
}
async function verifyAsset(params) {
throwIfCancelled(params.operation);
const sourcePath = path.resolve(params.root, params.entry.path);
if (!isWithinDir(params.root, sourcePath)) throw new Error(`Unsafe Control UI asset path: ${params.entry.path}`);
const expectedRealPath = await fs$1.realpath(sourcePath);
if (!isWithinDir(params.rootRealPath, expectedRealPath)) throw new Error(`Unsafe Control UI asset path: ${params.entry.path}`);
const initialStats = await fs$1.lstat(sourcePath);
if (initialStats.isSymbolicLink() || !initialStats.isFile()) throw new Error(`Unsafe Control UI asset: ${params.entry.path}`);
const source = await fs$1.open(sourcePath, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
let destination;
try {
if (params.destination) destination = await fs$1.open(params.destination, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW ?? 0), 384);
const hash = createHash("sha256");
let offset = 0;
for (;;) {
throwIfCancelled(params.operation);
const { bytesRead } = await source.read(params.operation.scratch, 0, params.operation.scratch.length, offset);
throwIfCancelled(params.operation);
if (bytesRead === 0) break;
offset += bytesRead;
if (offset > params.entry.size) throw new Error(`Control UI asset changed while being retained: ${params.entry.path}`);
hash.update(params.operation.scratch.subarray(0, bytesRead));
if (!destination) continue;
for (let written = 0; written < bytesRead;) {
throwIfCancelled(params.operation);
const { bytesWritten } = await destination.write(params.operation.scratch, written, bytesRead - written, offset - bytesRead + written);
throwIfCancelled(params.operation);
if (bytesWritten === 0) throw new Error(`Control UI asset write made no progress: ${params.entry.path}`);
written += bytesWritten;
}
}
const openedStats = await source.stat();
const currentStats = await fs$1.lstat(sourcePath);
const currentRealPath = await fs$1.realpath(sourcePath);
if (!openedStats.isFile() || openedStats.size !== params.entry.size || currentStats.isSymbolicLink() || !currentStats.isFile() || currentRealPath !== expectedRealPath || currentStats.dev !== openedStats.dev || currentStats.ino !== openedStats.ino || offset !== params.entry.size || hash.digest("hex") !== params.entry.sha256) throw new Error(`Control UI asset changed while being retained: ${params.entry.path}`);
throwIfCancelled(params.operation);
} finally {
await Promise.allSettled([source.close(), destination?.close()]);
}
}
async function publishGeneration(params) {
const target = path.join(await fs$1.realpath(params.cacheDir), params.manifest.generation);
const stats = await fs$1.lstat(target).catch((error) => {
if (!isErrno(error) || error.code !== "ENOENT") throw error;
return null;
});
const verified = stats && (params.verified && sameDirectory(params.verified.stats, stats) ? params.verified : await readCachedGeneration(target, params.operation));
if (verified) {
throwIfCancelled(params.operation);
await fs$1.utimes(target, /* @__PURE__ */ new Date(), /* @__PURE__ */ new Date());
return verified;
}
const staging = path.join(params.cacheDir, `.staging-${process.pid}-${randomUUID()}`);
throwIfCancelled(params.operation);
await fs$1.mkdir(staging, {
recursive: false,
mode: 448
});
try {
const rootRealPath = await fs$1.realpath(params.root);
let preparedDirectory;
for (const entry of params.manifest.assets) {
throwIfCancelled(params.operation);
const destination = path.join(staging, entry.path);
const directory = path.dirname(destination);
if (directory !== preparedDirectory) {
await fs$1.mkdir(directory, {
recursive: true,
mode: 448
});
preparedDirectory = directory;
}
await verifyAsset({
destination,
entry,
operation: params.operation,
root: params.root,
rootRealPath
});
}
throwIfCancelled(params.operation);
await fs$1.writeFile(path.join(staging, CONTROL_UI_ASSET_MANIFEST_FILENAME), `${JSON.stringify(params.manifest)}\n`, {
mode: 384,
signal: params.operation?.signal
});
throwIfCancelled(params.operation);
let collision;
try {
await fs$1.rename(staging, target);
} catch (error) {
if (!isErrno(error) || error.code !== "EEXIST" && error.code !== "ENOTEMPTY") throw error;
collision = error;
}
const published = await readCachedGeneration(target, params.operation);
if (!published) throw collision ?? /* @__PURE__ */ new Error(`Invalid retained Control UI generation: ${target}`);
throwIfCancelled(params.operation);
await fs$1.utimes(target, /* @__PURE__ */ new Date(), /* @__PURE__ */ new Date());
return published;
} finally {
await fs$1.rm(staging, {
recursive: true,
force: true
});
}
}
async function pruneRetainedGenerations(params) {
const inventory = await readCacheInventory(params.cacheDir, params.operation, params.verified);
const generations = inventory.generations.toSorted((left, right) => {
if (left.generation === params.currentGeneration) return -1;
if (right.generation === params.currentGeneration) return 1;
return compareGenerations(left, right);
});
const retained = /* @__PURE__ */ new Set();
let retainedBytes = 0;
for (const generation of generations) if (retained.size < CONTROL_UI_RETAINED_GENERATION_LIMIT && retainedBytes + generation.bytes <= CONTROL_UI_RETAINED_ASSET_MAX_BYTES) {
retained.add(generation.generation);
retainedBytes += generation.bytes;
}
for (const [target, stats] of inventory.directories) {
throwIfCancelled(params.operation);
const name = path.basename(target);
const generation = CONTROL_UI_GENERATION_PATTERN.test(name);
const staleStaging = CONTROL_UI_STAGING_PATTERN.test(name) && params.now - stats.mtimeMs >= CONTROL_UI_STAGING_MAX_AGE_MS;
if ((!generation || retained.has(name)) && !staleStaging) continue;
const currentStats = await fs$1.lstat(target).catch(() => null);
throwIfCancelled(params.operation);
if (!currentStats || !sameDirectory(stats, currentStats) || stats.mtimeMs !== currentStats.mtimeMs) continue;
await fs$1.rm(target, {
recursive: true,
force: true
});
}
const survivors = [];
for (const generation of inventory.generations) {
if (!retained.has(generation.generation)) continue;
const stats = await fs$1.lstat(generation.directory).catch(() => null);
throwIfCancelled(params.operation);
if (stats && sameDirectory(generation.stats, stats)) survivors.push({
...generation,
stats
});
}
return survivors.toSorted(compareGenerations);
}
function createControlUiAssetRetention(root) {
const cacheDir = resolveControlUiAssetCacheDir();
let generations = [];
let preparing;
return {
prepare(operation) {
preparing ??= (async () => {
throwIfCancelled(operation);
await fs$1.mkdir(cacheDir, {
recursive: true,
mode: 448
});
await fs$1.chmod(cacheDir, 448);
const io = {
...operation,
scratch: Buffer.allocUnsafe(CONTROL_UI_ASSET_IO_BUFFER_BYTES)
};
const inventory = await readCacheInventory(cacheDir, io);
throwIfCancelled(operation);
generations = inventory.generations;
const verified = [...generations];
const manifest = await readAssetManifest(root, operation);
const manifestBytes = manifest.assets.reduce((total, asset) => total + asset.size, 0);
if (manifestBytes <= CONTROL_UI_RETAINED_ASSET_MAX_BYTES) {
const published = await publishGeneration({
cacheDir,
manifest,
operation: io,
root,
verified: verified.find((entry) => entry.generation === manifest.generation)
});
verified.push(published);
}
const survivors = await pruneRetainedGenerations({
cacheDir,
verified,
currentGeneration: manifestBytes <= CONTROL_UI_RETAINED_ASSET_MAX_BYTES ? manifest.generation : void 0,
now: Date.now(),
operation: io
});
throwIfCancelled(operation);
generations = survivors;
})().catch((error) => {
preparing = void 0;
throw error;
});
return preparing;
},
resolveAsset(assetPath) {
for (const generation of generations) {
if (!generation.assetPaths.has(assetPath)) continue;
return {
filePath: path.join(generation.directory, assetPath),
rootPath: generation.directory,
rootRealPath: generation.realPath
};
}
return null;
}
};
}
//#endregion
//#region src/gateway/server-control-ui-root.ts
function resolveAutoRoot() {
return resolveControlUiRootSync({
moduleUrl: import.meta.url,
argv1: process.argv[1],
cwd: process.cwd()
});
}
function createResolvedRootState(root, configured = false) {
return !configured && isPackageProvenControlUiRootSync(root, {
moduleUrl: import.meta.url,
argv1: process.argv[1],
cwd: process.cwd()
}) ? {
kind: "bundled",
path: root,
realPath: fs.realpathSync(root),
publicAssetBuildId: new RegExp(`${CONTROL_UI_BUILD_ID_ATTRIBUTE}="([a-zA-Z0-9._-]{1,161})"`).exec(fs.readFileSync(path.join(root, "index.html"), "utf8"))?.[1],
retainedAssets: createControlUiAssetRetention(root)
} : {
kind: "resolved",
path: root,
realPath: fs.realpathSync(root)
};
}
function prepareResolvedRootState(params) {
try {
return createResolvedRootState(params.root, params.configured);
} catch (error) {
const detail = error instanceof Error ? error.message : String(error);
const message = `Control UI assets are unavailable at ${params.root}: ${detail}`;
params.log.warn(`gateway: ${message}`);
return params.configured ? {
kind: "invalid",
path: path.resolve(params.root)
} : { kind: "failed" };
}
}
/** Prepare the stable root reference shared by every HTTP listener. */
function createGatewayControlUiRootLifecycle(params) {
let state = { kind: "preparing" };
if (params.controlUiRootOverride) {
const resolvedOverride = resolveControlUiRootOverrideSync(params.controlUiRootOverride);
const resolvedOverridePath = path.resolve(params.controlUiRootOverride);
if (!resolvedOverride) {
params.log.warn(`gateway: controlUi.root not found at ${resolvedOverridePath}`);
state = {
kind: "invalid",
path: resolvedOverridePath
};
} else state = prepareResolvedRootState({
root: resolvedOverride,
configured: true,
log: params.log
});
} else if (params.controlUiEnabled) {
const resolvedRoot = resolveAutoRoot();
state = resolvedRoot && isControlUiStartupAssetsReady(resolvedRoot) ? prepareResolvedRootState({
root: resolvedRoot,
log: params.log
}) : { kind: "preparing" };
}
let enabled = params.controlUiEnabled;
let stopped = false;
let preparation;
const prepare = async (signal) => {
const isStopped = () => stopped || signal.aborted;
if (isStopped()) return;
try {
if (state.kind === "preparing") {
let resolvedRoot = resolveAutoRoot();
if (!resolvedRoot || !isControlUiStartupAssetsReady(resolvedRoot)) {
const result = await ensureControlUiAssetsBuilt(params.gatewayRuntime, { signal });
if (isStopped()) return;
if (!result.ok) {
Object.assign(state, { kind: "failed" });
params.log.warn(`gateway: ${result.message ?? "Control UI assets could not be built."}`);
return;
}
resolvedRoot = resolveAutoRoot();
}
if (!resolvedRoot || !isControlUiStartupAssetsReady(resolvedRoot)) {
const message = resolvedRoot ? `Control UI assets at ${resolvedRoot} remain incomplete.` : "Control UI build completed, but its assets are still unavailable.";
Object.assign(state, { kind: "failed" });
params.log.warn(`gateway: ${message} Run \`openclaw doctor --fix\` or reinstall OpenClaw.`);
return;
}
Object.assign(state, createResolvedRootState(resolvedRoot));
}
} catch (error) {
if (!isStopped()) {
Object.assign(state, { kind: "failed" });
const detail = error instanceof Error ? error.message : String(error);
params.log.warn(`gateway: Control UI assets build failed: ${detail}`);
}
return;
}
if (state.kind === "bundled") await state.retainedAssets?.prepare({
isCancelled: isStopped,
signal
}).catch((error) => {
if (isStopped()) return;
const detail = error instanceof Error ? error.message : String(error);
params.log.warn(`gateway: Control UI asset retention failed: ${detail}`);
});
};
const start = () => {
if (!enabled || stopped) return Promise.resolve();
if (preparation) return preparation.controller.signal.aborted ? preparation.promise.then(start) : preparation.promise;
const controller = new AbortController();
const promise = runOutsideGatewayRootWorkAdmission(() => Promise.resolve().then(() => prepare(controller.signal))).finally(() => {
preparation = void 0;
});
preparation = {
controller,
promise
};
return promise;
};
return {
state,
start,
setEnabled: (nextEnabled) => {
if (stopped || enabled === nextEnabled) return;
enabled = nextEnabled;
if (enabled) {
if (state.kind === "failed") Object.assign(state, { kind: "preparing" });
start();
} else preparation?.controller.abort();
},
stop: async () => {
stopped = true;
preparation?.controller.abort();
await preparation?.promise;
}
};
}
//#endregion
//#region src/gateway/server-transport-bridge.ts
/** Late-bound transport facts consumed by the socket-free Gateway kernel. */
function createGatewayTransportBridge() {
let current;
return {
attach: (transport) => {
current = transport;
},
current: () => current,
getPortalService: () => current?.portalService,
getTailscaleIngressEndpoint: () => current?.getTailscaleIngressEndpoint(),
getMcpAppSandboxPort: () => current?.getMcpAppSandboxPort(),
ensureSandboxHostPort: async () => {
if (!current) throw new Error("Gateway listener must start before the sandbox host");
return await current.ensureSandboxHostPort();
}
};
}
//#endregion
//#region src/gateway/server-wizard-sessions.ts
const UNCOLLECTED_TERMINAL_RETENTION_MS = 3e5;
/** Creates the in-memory tracker used for active Gateway wizard sessions. */
function createWizardSessionTracker(options) {
const wizardSessions = /* @__PURE__ */ new Map();
const terminalSince = /* @__PURE__ */ new Map();
const now = options?.now ?? Date.now;
const findRunningWizard = () => {
for (const [id, session] of wizardSessions) {
if (!session.isSettled()) {
terminalSince.delete(id);
return id;
}
const observedAt = terminalSince.get(id);
if (observedAt === void 0) terminalSince.set(id, now());
else if (now() - observedAt >= UNCOLLECTED_TERMINAL_RETENTION_MS) {
wizardSessions.delete(id);
terminalSince.delete(id);
}
}
return null;
};
const purgeWizardSession = (id) => {
const session = wizardSessions.get(id);
if (!session) return;
if (!session.isSettled()) return;
wizardSessions.delete(id);
terminalSince.delete(id);
};
return {
wizardSessions,
findRunningWizard,
purgeWizardSession
};
}
//#endregion
//#region src/gateway/server/event-loop-health.ts
const EVENT_LOOP_MONITOR_RESOLUTION_MS = 20;
const EVENT_LOOP_DELAY_WARN_MS = 1e3;
const EVENT_LOOP_UTILIZATION_WARN = .95;
const CPU_CORE_RATIO_WARN = .9;
const PERSISTENT_DEGRADATION_WARN_AFTER_MS = 6e4;
const LOAD_DEGRADATION_DELAY_COEVIDENCE_MS = 25;
const SUSTAINED_LOAD_SAMPLE_MIN_INTERVAL_MS = 1e3;
function roundMetric(value, digits = 3) {
if (!Number.isFinite(value)) return 0;
const factor = 10 ** digits;
return Math.round(value * factor) / factor;
}
function nanosecondsToMilliseconds(value) {
return roundMetric(value / 1e6, 1);
}
function classifyGatewayEventLoopHealthReasons(metrics) {
const reasons = [];
if (metrics.delayP99Ms >= EVENT_LOOP_DELAY_WARN_MS || metrics.delayMaxMs >= EVENT_LOOP_DELAY_WARN_MS) reasons.push("event_loop_delay");
if (metrics.intervalMs < SUSTAINED_LOAD_SAMPLE_MIN_INTERVAL_MS) return reasons;
if (!(metrics.delayP99Ms >= LOAD_DEGRADATION_DELAY_COEVIDENCE_MS || metrics.delayMaxMs >= LOAD_DEGRADATION_DELAY_COEVIDENCE_MS)) return reasons;
if (metrics.utilization >= EVENT_LOOP_UTILIZATION_WARN) reasons.push("event_loop_utilization");
if (metrics.cpuCoreRatio >= CPU_CORE_RATIO_WARN) reasons.push("cpu");
return reasons;
}
function createGatewayEventLoopHealthMonitor(deps = {}) {
const nowMs = deps.now ?? performance.now.bind(performance);
const readCpuUsage = deps.cpuUsage ?? process.cpuUsage.bind(process);
const readEventLoopUtilization = deps.eventLoopUtilization ?? performance.eventLoopUtilization.bind(performance);
let histogram = null;
let lastSampleAt = nowMs();
let lastWallAt = lastSampleAt;
let lastCpuUsage = readCpuUsage();
let lastEventLoopUtilization = readEventLoopUtilization();
let lastSnapshot;
let firstDegradedAtMs = null;
try {
histogram = createHistogram({
lowest: 1000n,
highest: 2n ** 63n - 1n,
figures: 3
});
} catch {
histogram = null;
}
const sample = () => {
if (!histogram) return;
const now = nowMs();
histogram.record(BigInt(Math.max(1, Math.round((now - lastSampleAt) * 1e6))));
lastSampleAt = now;
const intervalMs = Math.max(1, now - lastWallAt);
const delayMaxMs = nanosecondsToMilliseconds(histogram.max);
if (delayMaxMs < EVENT_LOOP_DELAY_WARN_MS && intervalMs < SUSTAINED_LOAD_SAMPLE_MIN_INTERVAL_MS) return;
const delayP99Ms = nanosecondsToMilliseconds(histogram.percentile(99));
const cpuUsage = readCpuUsage(lastCpuUsage);
const currentEventLoopUtilization = readEventLoopUtilization();
const utilization = roundMetric(readEventLoopUtilization(currentEventLoopUtilization, lastEventLoopUtilization).utilization);
const cpuCoreRatio = roundMetric(roundMetric((cpuUsage.user + cpuUsage.system) / 1e3, 1) / intervalMs);
const reasons = classifyGatewayEventLoopHealthReasons({
intervalMs,
delayP99Ms,
delayMaxMs,
utilization,
cpuCoreRatio
});
const degraded = reasons.length > 0;
if (degraded) firstDegradedAtMs ??= now;
else firstDegradedAtMs = null;
const health = {
degraded,
degradedSinceMs: firstDegradedAtMs === null ? null : Math.max(0, Math.round(now - firstDegradedAtMs)),
reasons,
intervalMs,
delayP99Ms,
delayMaxMs,
utilization,
cpuCoreRatio
};
histogram.reset();
lastWallAt = now;
lastCpuUsage = readCpuUsage();
lastEventLoopUtilization = currentEventLoopUtilization;
lastSnapshot = health;
if (areDiagnosticsEnabledForProcess() && hasInternalDiagnosticEventInterest("gateway.event_loop.sample")) runWithDiagnosticTraceContext(void 0, () => emitInternalDiagnosticEvent({
type: "gateway.event_loop.sample",
intervalMs,
delayMaxMs
}));
};
const timer = histogram ? setInterval(sample, EVENT_LOOP_MONITOR_RESOLUTION_MS) : void 0;
timer?.unref();
const reset = () => {
histogram?.reset();
lastSampleAt = nowMs();
lastWallAt = lastSampleAt;
lastCpuUsage = readCpuUsage();
lastEventLoopUtilization = readEventLoopUtilization();
lastSnapshot = void 0;
firstDegradedAtMs = null;
};
return {
snapshot: () => lastSnapshot,
persistentDegradationSnapshot: () => {
const current = lastSnapshot;
return current?.degradedSinceMs != null && current.degradedSinceMs >= PERSISTENT_DEGRADATION_WARN_AFTER_MS ? current : void 0;
},
reset,
stop: () => {
clearInterval(timer);
histogram = null;
lastSnapshot = void 0;
firstDegradedAtMs = null;
}
};
}
//#endregion
//#region src/gateway/server/readiness.ts
const DEFAULT_READINESS_CACHE_TTL_MS = 1e3;
/** Create a startup checker that excludes downstream channel health. */
function createStartupChecker(deps) {
return () => {
const uptimeMs = Date.now() - deps.startedAt;
if (deps.getGatewayDraining?.()) return {
ok: false,
status: "draining",
uptimeMs
};
if (deps.getStartupPending?.()) return {
ok: false,
status: "starting",
uptimeMs,
pendingReason: deps.getStartupPendingReason?.() ?? "startup-sidecars"
};
return {
ok: true,
status: "started",
uptimeMs
};
};
}
function shouldIgnoreReadinessFailure(accountSnapshot, health, autostartSuppressed) {
if (health.reason === "unmanaged" || health.reason === "stale-socket") return true;
if (autostartSuppressed && health.reason === "not-running") return true;
const restartableReason = health.reason === "not-running" || health.reason === "ingress-unavailable";
const inRestartHandoff = accountSnapshot.restartPending === true && accountSnapshot.running !== true;
return restartableReason && inRestartHandoff;
}
/** Create a cached readiness checker over channel runtime health. */
function createReadinessChecker(deps) {
const { channelManager, startedAt } = deps;
const getStartup = createStartupChecker(deps);
const cacheTtlMs = Math.max(0, deps.cacheTtlMs ?? DEFAULT_READINESS_CACHE_TTL_MS);
let cachedAt = 0;
let cachedState = null;
return () => {
const startup = getStartup();
const uptimeMs = startup.uptimeMs;
const now = startedAt + uptimeMs;
if (startup.status === "starting") return withEventLoopHealth({
ready: false,
failing: [startup.pendingReason],
uptimeMs
}, deps.getEventLoopHealth);
if (startup.status === "draining") return withEventLoopHealth({
ready: false,
failing: ["gateway-draining"],
uptimeMs
}, deps.getEventLoopHealth);
if (cachedState && !isFutureDateTimestampMs(cachedAt, { nowMs: now }) && now - cachedAt < cacheTtlMs) return withEventLoopHealth({
...cachedState,
uptimeMs
}, deps.getEventLoopHealth);
if (deps.getStateDatabaseFailure?.()) return withEventLoopHealth({
ready: false,
failing: ["state-database"],
uptimeMs
}, deps.getEventLoopHealth);
if (deps.shouldSkipChannelReadiness?.()) return withEventLoopHealth({
ready: true,
failing: [],
uptimeMs
}, deps.getEventLoopHealth);
const snapshot = channelManager.getRuntimeSnapshot();
const globallyAutostartSuppressed = channelManager.getAutostartSuppression() !== null;
const failing = [];
const suppressed = [];
for (const [channelId, accounts] of Object.entries(snapshot.channelAccounts)) {
if (!accounts) continue;
const autostartSuppressed = globallyAutostartSuppressed || channelManager.isAmbientAutostartSuppressed(channelId);
for (const accountSnapshot of Object.values(accounts)) {
if (!accountSnapshot) continue;
const health = evaluateChannelHealth(accountSnapshot, {
now,
staleEventThresholdMs: DEFAULT_CHANNEL_STALE_EVENT_THRESHOLD_MS,
channelConnectGraceMs: DEFAULT_CHANNEL_CONNECT_GRACE_MS,
channelId
});
if (!health.healthy && autostartSuppressed && health.reason === "not-running") {
if (!suppressed.includes(channelId)) suppressed.push(channelId);
continue;
}
if (!health.healthy && !shouldIgnoreReadinessFailure(accountSnapshot, health, autostartSuppressed)) {
failing.push(channelId);
break;
}
}
}
cachedAt = now;
cachedState = {
ready: failing.length === 0,
failing,
...suppressed.length > 0 ? { suppressed } : {}
};
return withEventLoopHealth({
...cachedState,
uptimeMs
}, deps.getEventLoopHealth);
};
}
function withEventLoopHealth(result, getEventLoopHealth) {
const eventLoop = getEventLoopHealth?.();
return eventLoop ? {
...result,
eventLoop
} : result;
}
//#endregion
//#region src/gateway/server-runtime-state-prepare.ts
async function prepareGatewayKernelState(params) {
const { bootstrap, bootId, port, opts, log, logChannels, logHooks, logPlugins, gatewayRuntime, resolveChannelRuntime: getChannelRuntime, loadWorkerEnvironmentStartupModule, loadWorkerPlacementStartupModule } = params;
const { pluginBootstrap, gatewayPluginConfigAtStart, workerEnvironmentStartup, startupTrace, cfgAtStart, resolvedStartupAuthOverride, startupTailscaleOverride, ambientAutostartSuppressedChannelIds, minimalTestGateway, pluginGatewayContext, resolvePluginGatewayContext } = bootstrap;
const pluginRuntime = {
registry: pluginBootstrap.pluginRegistry,
baseGatewayMethods: pluginBootstrap.baseGatewayMethods
};
const listGatewayStartupChannelPlugins = () => listLoadedChannelPluginsForRegistry(pluginRuntime.registry);
const shouldStartWorkerEnvironmentService = Boolean(workerEnvironmentStartup);
const hostDesktopConfig = gatewayPluginConfigAtStart.desktop?.host;
const hostDesktopEnabled = hostDesktopConfig?.enabled === true;
const workerDesktopObserveAvailable = shouldStartWorkerEnvironmentService && gatewayPluginConfigAtStart.cloudWorkers?.desktop === true;
const desktopSessionRegistry = createDesktopSessionRegistry();
const nodeDesktopStreamBroker = (await startupTrace.measure("node-desktop.runtime-import", () => import("./node-stream-broker-B5rI2tk_.js"))).createNodeDesktopStreamBroker();
const hostDesktopService = hostDesktopConfig && hostDesktopEnabled ? (await startupTrace.measure("host-desktop.runtime-import", () => import("./host-source-DeKFKIMi.js"))).createHostDesktopService({
config: hostDesktopConfig,
registry: desktopSessionRegistry
}) : void 0;
const workerEnvironmentRuntime = workerEnvironmentStartup ? await startupTrace.measure("worker-environments.runtime-imports", async () => {
return await (await loadWorkerEnvironmentStartupModule()).createGatewayWorkerEnvironmentRuntime({
getPluginRegistry: () => pluginRuntime.registry,
getPortalRuntime: () => pluginGatewayContext.current,
resolveGatewayContext: resolvePluginGatewayContext,
desktopSessionRegistry,
nodeDesktopStreamBroker,
startup: workerEnvironmentStartup,
log
});
}) : {};
const { workerEnvironmentService, workerLiveEvents, nodeWorkerGatewayNamespace, bindDeviceNodeControl, bindWorkerNodeDesktopControl, bindNodeWorkspaceBindingResolver, handleNodeWorkerBundleTransferRequest, handleWorkerBootstrapArtifactTransferRequest, handleNodeWorkspaceTransferRequest } = workerEnvironmentRuntime;
const workerDispatchAuthority = { revoke: (_params) => {
throw new Error("Worker dispatch authority revocation is not ready");
} };
const workerPlacementModule = workerEnvironmentStartup ? await startupTrace.measure("worker-environments.placement-module", loadWorkerPlacementStartupModule) : void 0;
const githubPublicationRuntime = workerEnvironmentStartup && workerPlacementModule ? workerPlacementModule.createGatewayGitHubPublicationRuntime({
placements: workerEnvironmentStartup.placementStore,
warn: (message) => log.warn(message)
}) : void 0;
const workerPlacementRuntime = workerEnvironmentService && workerEnvironmentStartup && nodeWorkerGatewayNamespace && workerPlacementModule ? await startupTrace.measure("worker-environments.placement-runtime", async () => workerPlacementModule.createGatewayWorkerPlacementRuntime({
placements: workerEnvironmentStartup.placementStore,
environments: workerEnvironmentService,
gatewayNamespace: nodeWorkerGatewayNamespace,
getSessionChangeContext: () => pluginGatewayContext.current,
persistAbandonedPartial: async ({ sessionId, sessionKey, agentId, runId }) => {
const text = connectionState.chatRunState.resolveBuffer(runId, { final: true }).text;
if (!text.trim()) return;
const { captureAbortedPartial, persistAbortedPartials } = await import("./chat-transcript-persistence.runtime.js");
await persistAbortedPartials({
context: { logGateway: log },
snapshots: [captureAbortedPartial({
sessionKey,
sessionId,
agentId,
runId,
text,
abortOrigin: "placement-abandon"
})]
});
},
cancelSessionWork: async (request) => {
const context = pluginGatewayContext.current;
if (!context) throw new Error("Worker session cancellation is not ready");
const { cancelGatewayWorkerSessionWork } = await import("./server-worker-placement-cancel-B06xTrFv.js");
await cancelGatewayWorkerSessionWork(context, request);
},
revokeSessionAuthority: (request) => workerDispatchAuthority.revoke(request),
info: (message) => log.info(message),
warn: (message) => log.warn(message),
...githubPublicationRuntime ? { githubPublicationRuntime } : {}
})) : void 0;
if (workerPlacementRuntime) {
bindNodeWorkspaceBindingResolver?.(workerPlacementRuntime.resolveNodeWorkspaceBinding);
workerEnvironmentRuntime.bindWorkerSessionDispatch?.(workerPlacementRuntime.dispatchService.dispatch);
}
const bindDeviceNodeRuntime = bindDeviceNodeControl ? (transport) => {
bindDeviceNodeControl(transport);
workerPlacementRuntime?.bindNodeWorkerSupervisorTransport(transport);
} : void 0;
const workerPlacementControlAvailable = workerPlacementRuntime?.dispatchService;
const workerPlacementDispatchAvailable = workerPlacementControlAvailable;
const channelLogs = Object.fromEntries(listGatewayStartupChannelPlugins().map((plugin) => [plugin.id, logChannels.child(plugin.id)]));
const channelRuntimeEnvs = Object.fromEntries(Object.entries(channelLogs).map(([id, logger]) => [id, runtimeForLogger(logger)]));
const listStartupChannelGatewayMethods = () => {
const methods = [];
for (const plugin of listGatewayStartupChannelPlugins()) {
methods.push(...plugin.gatewayMethods ?? []);
for (const descriptor of plugin.gatewayMethodDescriptors ?? []) methods.push(descriptor.name);
}
return methods;
};
const listActiveGatewayMethods = (nextBaseGatewayMethods) => uniqueStrings([...nextBaseGatewayMethods, ...listStartupChannelGatewayMethods()]).filter((method) => (workerPlacementDispatchAvailable || method !== "sessions.dispatch") && (workerPlacementControlAvailable || method !== "sessions.reclaim" && method !== "sessions.move") && (workerDesktopObserveAvailable || method !== "desktop.launch" && method !== "worker.desktop.observe" && method !== "worker.desktop.launch"));
const runtimeConfig = await startupTrace.measure("runtime.config", async () => {
const { resolveGatewayRuntimeConfig } = await import("./server-runtime-config-CSWRIeVw.js");
return resolveGatewayRuntimeConfig({
cfg: cfgAtStart,
port,
bind: opts.bind,
host: opts.host,
controlUiEnabled: opts.controlUiEnabled,
auth: resolvedStartupAuthOverride,
tailscale: startupTailscaleOverride
});
});
const { bindHost, controlUiEnabled, controlUiBasePath, controlUiRoot: controlUiRootOverride, resolvedAuth, tailscaleConfig, tailscaleMode } = runtimeConfig;
if (bootstrap.generatedStartupAuthToken && isLoopbackHost(bindHost)) {
const { ensureStartupLocalCliPairing } = await startupTrace.measure("runtime.local-cli-pairing-import", () => import("./startup-local-cli-pairing-DSnCofoR.js"));
const pairingResult = await startupTrace.measure("runtime.local-cli-pairing", () => ensureStartupLocalCliPairing());
if (pairingResult === "created") log.info("runtime-only gateway auth paired the local CLI device before readiness");
else if (pairingResult === "unavailable") log.warn("runtime-only gateway auth could not prepare local CLI device credentials; configure gateway.auth.token or gateway.auth.password for CLI access");
}
const getResolvedAuth = () => resolveGatewayAuth({
authConfig: getActiveSecretsRuntimeConfigSnapshot()?.config.gateway?.auth ?? getRuntimeConfig().gateway?.auth,
authOverride: resolvedStartupAuthOverride,
env: process.env,
tailscaleMode
});
const resolveSharedGatewaySessionGenerationForConfig = (config) => resolveSharedGatewaySessionGeneration(resolveGatewayAuth({
authConfig: config.gateway?.auth,
authOverride: resolvedStartupAuthOverride,
env: process.env,
tailscaleMode
}), config.gateway?.trustedProxies);
const resolveCurrentSharedGatewaySessionGeneration = () => resolveSharedGatewaySessionGeneration(getResolvedAuth(), getRuntimeConfig().gateway?.trustedProxies);
const resolveSharedGatewaySessionGenerationForRuntimeSnapshot = () => resolveSharedGatewaySessionGenerationForConfig(getRuntimeConfig());
const sharedGatewaySessionGenerationState = {
current: resolveCurrentSharedGatewaySessionGeneration(),
required: null
};
const preauthHandshakeTimeoutMs = void 0;
const initialHooksConfig = runtimeConfig.hooksConfig;
const initialHookClientIpConfig = resolveHookClientIpConfig(cfgAtStart);
const rateLimitConfig = cfgAtStart.gateway?.auth?.rateLimit;
const authRateLimiter = createAuthRateLimiter(rateLimitConfig);
const browserAuthRateLimiter = createAuthRateLimiter({
...rateLimitConfig,
exemptLoopback: false
});
const nodeReapprovalCoordinator = createNodeReapprovalCoordinator(rateLimitConfig);
const controlUiRootLifecycle = await startupTrace.measure("control-ui.root", () => createGatewayControlUiRootLifecycle({
controlUiRootOverride,
controlUiEnabled,
gatewayRuntime,
log
}));
const { createTerminalLaunchPolicy } = await startupTrace.measure("terminal.launch-import", () => import("./launch-C70uwd1Z.js"));
const terminalLaunchPolicy = createTerminalLaunchPolicy(cfgAtStart);
const { runDefaultChannelSetupWizard, runDefaultSetupWizard } = await startupTrace.measure("gateway.wizard-imports", () => import("./wizard-Ba9Q3tcy.js"));
const wizardRunner = opts.wizardRunner ?? runDefaultSetupWizard;
const channelWizardRunner = opts.channelWizardRunner ?? runDefaultChannelSetupWizard;
const { wizardSessions, findRunningWizard, purgeWizardSession } = createWizardSessionTracker();
const systemAgentSessions = /* @__PURE__ */ new Map();
const deps = createDefaultDeps();
const runtimeStateRef = { current: null };
const cronStartState = { handled: false };
const gatewayTls = await startupTrace.measure("tls.runtime", () => loadGatewayTlsServerRuntime(cfgAtStart.gateway?.tls, log.child("tls")));
const serverStartedAt = Date.now();
const readinessEventLoopHealth = createGatewayEventLoopHealthMonitor();
const startupState = {
sidecarsReady: minimalTestGateway,
pendingReason: "startup-sidecars",
dispatchReady: false
};
const lifecycle = { closePreludeStarted: false };
let releaseStartupAccountStarts = () => {};
const startupAccountStartsReady = new Promise((resolve) => {
releaseStartupAccountStarts = resolve;
});
const gatewayInstanceRuntimeRef = { current: void 0 };
const { createChannelManager } = await startupTrace.measure("gateway.channel-manager-import", () => import("./server-channels-D39cmOPC.js"));
const channelManager = createChannelManager({
getRuntimeConfig: () => {
const runtimeConfigLocal = getRuntimeConfig();
return resolveGatewayPluginConfig({ config: runtimeConfigLocal });
},
channelLogs,
channelRuntimeEnvs,
resolveChannelRuntime: getChannelRuntime,
getPluginRegistry: () => pluginRuntime.registry,
startupTrace,
deferStartupAccountStartsUntil: startupAccountStartsReady,
getNativeApprovalRuntime: () => gatewayInstanceRuntimeRef.current?.nativeApprovals,
ambientAutostartSuppressedChannelIds,
...opts.tryRecoverChannelAutostartSuppression ? { tryRecoverAutostartSuppression: opts.tryRecoverChannelAutostartSuppression } : {},
isClosing: () => lifecycle.closePreludeStarted
});
channelManager.setAutostartSuppression(opts.channelAutostartSuppression ?? null);
const sidecarStartup = opts.sidecarStartup ?? "start";
const isGatewayStartupPending = () => !startupState.sidecarsReady && !lifecycle.closePreludeStarted;
const startupCheckerDeps = {
startedAt: serverStartedAt,
getStartupPending: isGatewayStartupPending,
getStartupPendingReason: () => startupState.pendingReason,
getGatewayDraining: () => lifecycle.closePreludeStarted || isGatewayDraining()
};
const getStartup = createStartupChecker(startupCheckerDeps);
const getReadiness = createReadinessChecker({
channelManager,
...startupCheckerDeps,
getEventLoopHealth: readinessEventLoopHealth.snapshot,
getStateDatabaseFailure: () => openClawStateDatabaseCache.getOpenClawStateDatabaseRuntimeFailure(resolveDatabasePath()),
shouldSkipChannelReadiness: () => isTruthyEnvValue(process.env.OPENCLAW_SKIP_CHANNELS) || isTruthyEnvValue(process.env.OPENCLAW_SKIP_PROVIDERS)
});
const watchNodeRequestHandler = {};
log.info("starting HTTP server...");
const connectionState = await startupTrace.measure("runtime.state", () => createGatewayConnectionState({
bootId,
cfg: cfgAtStart,
getRuntimeConfig
}));
const transportBridge = createGatewayTransportBridge();
const createHttpTransportOptions = () => ({
cfg: cfgAtStart,
getRuntimeConfig,
bindHost,
port,
controlUiEnabled: opts.controlUiEnabled,
controlUiBasePath,
controlUiRoot: controlUiRootLifecycle.state,
openAiChatCompletionsEnabled: opts.openAiChatCompletionsEnabled,
openResponsesEnabled: opts.openResponsesEnabled,
resolvedAuth,
rateLimiter: authRateLimiter,
joinRateLimiter: browserAuthRateLimiter,
isTerminalEnabled: terminalLaunchPolicy.isEnabled,
gatewayTls,
getResolvedAuth,
hooksConfig: () => runtimeStateRef.current?.hooksConfig ?? initialHooksConfig,
getHookClientIpConfig: () => runtimeStateRef.current?.hookClientIpConfig ?? initialHookClientIpConfig,
pluginRegistry: pluginRuntime.registry,
getPluginRouteRegistry: () => pluginRuntime.registry,
isStartupPluginRuntimeReady: () => startupState.sidecarsReady,
getGatewayRequestContext: resolvePluginGatewayContext,
deps,
log,
logHooks,
logPlugins,
getReadiness,
getStartup,
isStartupPending: isGatewayStartupPending,
handleWatchNodeRequest: async (req, res) => await watchNodeRequestHandler.current?.(req, res) ?? false,
handleNodeWorkerBundleTransferRequest,
handleWorkerBootstrapArtifactTransferRequest,
handleNodeWorkspaceTransferRequest,
workerIngressEnabled: Boolean(workerEnvironmentService),
desktopSessionRegistry,
nodeDesktopStreamBroker,
clients: connectionState.clients,
tailscaleMode
});
const { clients, mentionInbox, broadcast, broadcastToConnIds, broadcastPluginEvent, getBufferedAmount, agentRunSeq, dedupe, chatRunState, addChatRun, removeChatRun, chatAbortControllers, chatQueuedTurns, toolEventRecipients, sessionEventSubscribers, sessionMessageSubscribers, isConnectionActive } = connectionState;
return {
...bootstrap,
bootId,
pluginRuntime,
workerEnvironmentService,
workerLiveEvents,
bindDeviceNodeControl: bindDeviceNodeRuntime,
bindWorkerNodeDesktopControl,
workerDispatchAuthority,
workerPlacementRuntime,
githubPublicationRuntime,
githubPublicationService: githubPublicationRuntime?.coordinator,
workerPlacementControlAvailable,
workerPlacementDispatchAvailable,
workerDesktopObserveAvailable,
desktopSessionRegistry,
nodeDesktopStreamBroker,
hostDesktopService,
channelLogs,
channelRuntimeEnvs,
listStartupChannelGatewayMethods,
listActiveGatewayMethods,
bindHost,
controlUiRootLifecycle,
controlUiBasePath,
resolvedAuth,
tailscaleConfig,
tailscaleMode,
getResolvedAuth,
resolveSharedGatewaySessionGenerationForConfig,
resolveSharedGatewaySessionGenerationForRuntimeSnapshot,
sharedGatewaySessionGenerationState,
preauthHandshakeTimeoutMs,
initialHooksConfig,
initialHookClientIpConfig,
authRateLimiter,
browserAuthRateLimiter,
nodeReapprovalCoordinator,
terminalLaunchPolicy,
wizardRunner,
channelWizardRunner,
wizardSessions,
findRunningWizard,
purgeWizardSession,
systemAgentSessions,
deps,
runtimeStateRef,
cronStartState,
gatewayTls,
readinessEventLoopHealth,
startupState,
lifecycle,
releaseStartupAccountStarts,
gatewayInstanceRuntimeRef,
channelManager,
sidecarStartup,
isGatewayStartupPending,
pluginGatewayContext,
watchNodeRequestHandler,
createHttpTransportOptions,
transportBridge,
connectionWork: connectionState.connectionWork,
clients,
mentionInbox,
broadcast,
broadcastToConnIds,
broadcastPluginEvent,
getBufferedAmount,
agentRunSeq,
dedupe,
chatRunState,
addChatRun,
removeChatRun,
chatAbortControllers,
chatQueuedTurns,
toolEventRecipients,
sessionEventSubscribers,
sessionMessageSubscribers,
isConnectionActive,
getTailscaleIngressEndpoint: transportBridge.getTailscaleIngressEndpoint,
getMcpAppSandboxPort: transportBridge.getMcpAppSandboxPort,
ensureSandboxHostPort: transportBridge.ensureSandboxHostPort,
getPortalService: transportBridge.getPortalService
};
}
//#endregion
//#region src/gateway/startup-control-ui-origins.ts
/**
* Seeds runtime-only Control UI origins when a non-loopback gateway bind would
* otherwise reject the browser that just opened the local UI.
*/
async function maybeSeedControlUiAllowedOriginsAtStartup(params) {
const seeded = ensureControlUiAllowedOriginsForNonLoopbackBind(params.config, {
isContainerEnvironment,
runtimeBind: params.runtimeBind,
runtimePort: params.runtimePort
});
if (!seeded.seededOrigins || !seeded.bind) return {
config: params.config,
seededAllowedOrigins: false
};
params.log.info(buildSeededOriginsInfoLog(seeded.seededOrigins, seeded.bind));
return {
config: seeded.config,
seededAllowedOrigins: true
};
}
function buildSeededOriginsInfoLog(origins, bind) {
return `gateway: seeded gateway.controlUi.allowedOrigins ${JSON.stringify(origins)} for bind=${bind} (required since v2026.2.26; see issue #29385). Applied for this runtime without writing config; add other origins to gateway.controlUi.allowedOrigins if needed.`;
}
//#endregion
//#region src/gateway/server-startup-bootstrap.ts
function publishGatewayPluginRuntimeConfigAtStartup(params) {
setAppliedRuntimeConfigSnapshot(params.runtimeConfig, params.sourceConfig);
}
async function prepareGatewayServerBootstrap(input) {
const { port, opts, log, logSecrets, loadWorkerEnvironmentStartupModule } = input;
const formatRuntimeGatewayAuthTokenWarning = input.formatRuntimeGatewayAuthTokenWarning;
const traceOriginAt = opts.processStartedAt ?? opts.startupStartedAt;
const startupElapsedMs = typeof traceOriginAt === "number" ? Math.max(0, Date.now() - traceOriginAt) : 0;
const startupTrace = createGatewayStartupTrace(log, performance.now() - startupElapsedMs);
if (startupElapsedMs > 0) startupTrace.mark("process.bootstrap");
await startupTrace.measure("state.ownership", async () => {
normalizeStateDirEnv(process.env);
await assertOpenClawStateWriteAllowedAtPath({
databasePath: resolveOpenClawStateSqlitePath(process.env),
env: process.env
});
});
const [{ OPENCLAW_DATABASE_SCHEMA_DOCS_URL, OpenClawDatabaseSchemaPreflightError, preflightOpenClawDatabaseSchemas }, agentDatabase, stateDatabase] = await startupTrace.measure("state.runtime-imports", () => Promise.all([
import("./openclaw-database-preflight-of3VjD09.js"),
import("./openclaw-agent-db-Bfc4Wmxr.js"),
import("./openclaw-state-db-contract-Ciz9ElQz.js")
]));
const databaseSchemas = await startupTrace.measure("state.schema-preflight", () => preflightOpenClawDatabaseSchemas({
env: process.env,
supportedVersions: {
state: stateDatabase.OPENCLAW_STATE_SCHEMA_VERSION,
agent: agentDatabase.OPENCLAW_AGENT_SCHEMA_VERSION
}
}));
if (databaseSchemas.incompatible.length > 0) {
for (const database of databaseSchemas.incompatible) log.error("database schema preflight rejected newer schema", {
kind: database.kind,
path: database.path,
...database.agentId ? { agentId: database.agentId } : {},
foundVersion: database.foundVersion,
supportedVersion: database.supportedVersion,
writerAppVersion: database.writerAppVersion ?? "unknown",
docsUrl: OPENCLAW_DATABASE_SCHEMA_DOCS_URL
});
throw new OpenClawDatabaseSchemaPreflightError(databaseSchemas.incompatible);
}
for (const database of databaseSchemas.indeterminate) log.warn("database schema preflight could not inspect database; continuing to real open", {
kind: database.kind,
path: database.path,
reason: database.reason,
docsUrl: OPENCLAW_DATABASE_SCHEMA_DOCS_URL
});
const { bootstrapGatewayNetworkRuntime } = await startupTrace.measure("runtime.network-imports", () => import("./server-network-runtime-BQnoyI4o.js"));
await startupTrace.measure("runtime.network-bootstrap", () => bootstrapGatewayNetworkRuntime());
const minimalTestGateway = isVitestRuntimeEnv() && process.env.OPENCLAW_TEST_MINIMAL_GATEWAY === "1";
const ambientEnvTriggers = opts.ambientEnvTriggers ?? "suppress";
process.env.OPENCLAW_GATEWAY_PORT = String(port);
logAcceptedEnvOption({
key: "OPENCLAW_RAW_STREAM",
description: "raw stream logging enabled"
});
logAcceptedEnvOption({
key: "OPENCLAW_RAW_STREAM_PATH",
description: "raw stream log path override"
});
if (!resumeGatewayRestartTraceFromEnv(process.env, [["source", "env"]])) {
const restartHandoff = readGatewayRestartHandoffSync();
resumeGatewayRestartTraceFromHandoff(restartHandoff?.restartTrace, [
["source", restartHandoff?.source],
["restartKind", restartHandoff?.restartKind],
["supervisorMode", restartHandoff?.supervisorMode]
]);
}
if (!minimalTestGateway) await startupTrace.measure("runtime.agent-cli", () => prepareGatewayAgentCliShim());
const startupConfigModulePromise = startupTrace.measure("config.runtime-imports", () => import("./server-startup-config-D1FuvNQL.js"));
const loadStartupPluginsModule = createLazyPromise(() => import("./server-startup-plugins-C7BGmsZG.js"), { cacheRejections: true });
const { loadGatewayStartupConfigSnapshot } = await startupConfigModulePromise;
const envBeforeStartupConfigLoad = { ...process.env };
const startupConfigLoad = await startupTrace.measure("config.snapshot", () => loadGatewayStartupConfigSnapshot({
minimalTestGateway,
log,
measure: (name, run) => startupTrace.measure(name, run),
...opts.startupConfigSnapshotRead ? { initialSnapshotRead: opts.startupConfigSnapshotRead } : {}
}));
const configSnapshot = startupConfigLoad.snapshot;
const startupAuthOverride = opts.auth ? structuredClone(opts.auth) : void 0;
const startupTailscaleOverride = opts.tailscale ? structuredClone(opts.tailscale) : void 0;
const controlUiSeed = minimalTestGateway ? {
config: configSnapshot.config,
seededAllowedOrigins: false
} : await startupTrace.measure("control-ui.seed", () => maybeSeedControlUiAllowedOriginsAtStartup({
config: configSnapshot.config,
log,
runtimeBind: opts.bind,
runtimePort: port
}));
if (controlUiSeed.seededAllowedOrigins) copyConfigResolutionFacts(configSnapshot.config, controlUiSeed.config);
const startupConfigSnapshot = controlUiSeed.seededAllowedOrigins ? {
...configSnapshot,
runtimeConfig: controlUiSeed.config,
config: controlUiSeed.config
} : configSnapshot;
const emitSecretsStateEvent = (code, message, cfg) => {
const text = `[${code}] ${message}`;
try {
const target = resolveSystemMainSessionTarget(cfg);
enqueueSystemEvent(text, withSystemEventOwner({
sessionKey: target.sessionKey,
contextKey: code
}, target.agentId));
} catch (error) {
logSecrets.warn(`${text} not delivered: ${formatErrorMessage(error)}`);
}
};
const { createRuntimeSecretsActivator } = await startupConfigModulePromise;
const activateRuntimeSecrets = createRuntimeSecretsActivator({
logSecrets,
emitStateEvent: emitSecretsStateEvent,
...startupConfigLoad.pluginMetadataSnapshot ? { pluginMetadataSnapshot: startupConfigLoad.pluginMetadataSnapshot } : {}
});
let startupInternalWriteHash = null;
let startupLastGoodSnapshot = configSnapshot;
const startupActivationSourceConfig = configSnapshot.sourceConfig;
const startupRuntimeConfig = captureConfigOverrideApplier()(startupConfigSnapshot.config);
startupTrace.setConfig(startupRuntimeConfig);
const { prepareGatewayStartupConfig } = await startupConfigModulePromise;
const authBootstrap = await startupTrace.measure("config.auth", () => prepareGatewayStartupConfig({
configSnapshot: startupConfigSnapshot,
authOverride: startupAuthOverride,
tailscaleOverride: startupTailscaleOverride,
activateRuntimeSecrets,
log,
measure: (name, run, measureOptions) => startupTrace.measure(name, run, measureOptions)
}), { omitErrorMessage: true });
const cfgAtStart = authBootstrap.cfg;
startupTrace.setConfig(cfgAtStart);
try {
const cleanup = await startupTrace.measure("agents.github-profile-cleanup", async () => {
const { cleanupRetiredManagedGitHubProfiles } = await import("./github-tool-profile-cleanup-BjrTfM9-.js");
return await cleanupRetiredManagedGitHubProfiles({
config: cfgAtStart,
env: process.env
});
});
for (const warning of cleanup.warnings) log.warn(`managed GitHub profile cleanup: ${warning}`);
} catch (error) {
log.warn(`managed GitHub profile cleanup failed: ${formatErrorMessage(error)}`);
}
if (authBootstrap.generatedToken) log.warn(formatRuntimeGatewayAuthTokenWarning());
const trustedProxyDeviceAutoApprove = cfgAtStart.gateway?.auth?.trustedProxy?.deviceAutoApprove;
if (cfgAtStart.gateway?.auth?.mode === "trusted-proxy" && trustedProxyDeviceAutoApprove?.enabled === true && trustedProxyDeviceAutoApprove.scopes?.some((scope) => scope.trim() === "operator.admin")) log.warn("SECURITY WARNING: gateway.auth.trustedProxy.deviceAutoApprove.scopes includes operator.admin; every proxy-authenticated user can auto-approve a new operator device with full admin, and requests without scopes receive full admin automatically. Remove operator.admin and grant admin per identity via gateway.auth.identityScopes instead.");
const resolvedStartupAuthOverride = startupAuthOverride ? Object.fromEntries([
"mode",
"token",
"password",
"allowTailscale",
"rateLimit",
"trustedProxy"
].flatMap((key) => {
if (startupAuthOverride[key] === void 0) return [];
if ((key === "token" || key === "password") && isSecretRef(startupAuthOverride[key])) return [];
const resolvedValue = cfgAtStart.gateway?.auth?.[key];
return resolvedValue === void 0 ? [] : [[key, structuredClone(resolvedValue)]];
})) : void 0;
const startupAuthSecretRefOverride = startupAuthOverride ? {
...isSecretRef(startupAuthOverride.token) ? { token: structuredClone(startupAuthOverride.token) } : {},
...isSecretRef(startupAuthOverride.password) ? { password: structuredClone(startupAuthOverride.password) } : {}
} : void 0;
const reloadAuthOverride = authBootstrap.generatedToken ? mergeGatewayAuthConfig(resolvedStartupAuthOverride, { token: authBootstrap.generatedToken }) : resolvedStartupAuthOverride;
setDiagnosticsEnabledForProcess(isDiagnosticsEnabled(cfgAtStart));
setGatewaySigusr1RestartPolicy({ allowExternal: isRestartEnabled(cfgAtStart) });
const activeTaskCount = { get: () => 0 };
setPreRestartDeferralCheck(() => getTotalQueueSize() + getTotalPendingReplies() + getActiveEmbeddedRunCount() + getActiveCronJobCount() + getActiveBackgroundExecSessionCount() + getActiveGatewayRootWorkCount({ excludeCurrent: true }) + activeTaskCount.get());
const seededControlUiAllowedOrigins = controlUiSeed.seededAllowedOrigins ? cfgAtStart.gateway?.controlUi?.allowedOrigins : void 0;
const applyFixedGatewayOverlays = (config) => {
let runtimeConfig = config;
if (reloadAuthOverride || startupTailscaleOverride) runtimeConfig = {
...runtimeConfig,
gateway: {
...runtimeConfig.gateway,
...reloadAuthOverride ? { auth: mergeGatewayAuthConfig(runtimeConfig.gateway?.auth, reloadAuthOverride) } : {},
...startupTailscaleOverride ? { tailscale: mergeGatewayTailscaleConfig(runtimeConfig.gateway?.tailscale, startupTailscaleOverride) } : {}
}
};
if (seededControlUiAllowedOrigins && runtimeConfig.gateway?.controlUi?.allowedOrigins === void 0) runtimeConfig = {
...runtimeConfig,
gateway: {
...runtimeConfig.gateway,
controlUi: {
...runtimeConfig.gateway?.controlUi,
allowedOrigins: seededControlUiAllowedOrigins
}
}
};
copyConfigResolutionFactsExcept(config, runtimeConfig, [...reloadAuthOverride?.token !== void 0 ? ["gateway.auth.token"] : [], ...reloadAuthOverride?.password !== void 0 ? ["gateway.auth.password"] : []]);
return runtimeConfig;
};
const applyReloadableGatewayAuthRefs = (config) => {
if (!startupAuthSecretRefOverride?.token && !startupAuthSecretRefOverride?.password) return config;
const next = {
...config,
gateway: {
...config.gateway,
auth: mergeGatewayAuthConfig(config.gateway?.auth, startupAuthSecretRefOverride)
}
};
copyConfigResolutionFactsExcept(config, next, [...startupAuthSecretRefOverride.token !== void 0 ? ["gateway.auth.token"] : [], ...startupAuthSecretRefOverride.password !== void 0 ? ["gateway.auth.password"] : []]);
return next;
};
const { assertConfiguredWorkspaceStateReady } = await import("./workspace-state-dirs-B8xySjIm.js");
const prepareReloadCandidate = (params) => {
const previousSourceConfig = params.previousSourceConfig ?? getRuntimeConfigSourceSnapshot() ?? startupLastGoodSnapshot.sourceConfig;
assertGatewayConfigEnvSelectionUnchanged(previousSourceConfig, params.sourceConfig);
const runtimeEnv = prepareConfigRuntimeEnv({
previousConfig: previousSourceConfig,
nextConfig: params.sourceConfig
});
const metadata = startupConfigLoad.pluginMetadataSnapshot;
const pluginCandidate = minimalTestGateway ? {
runtimeConfig: params.runtimeConfig,
compareConfig: params.sourceConfig
} : resolveGatewayReloadPluginActivationCandidate({
...params,
env: runtimeEnv.env,
...metadata?.manifestRegistry ? { manifestRegistry: metadata.manifestRegistry } : {},
discovery: metadata?.discovery,
ambientEnvTriggers
});
const applyCandidateOverrides = captureConfigOverrideApplier();
const reapplyCompareOverlays = (config) => {
const applied = applyCandidateOverrides(mergeActivationSectionsIntoRuntimeConfig({
runtimeConfig: config,
activationConfig: pluginCandidate.compareConfig
}));
copyConfigResolutionFacts(config, applied);
return applied;
};
const reapplyRuntimeOverlays = (config) => applyFixedGatewayOverlays(applyReloadableGatewayAuthRefs(reapplyCompareOverlays(config)));
const runtimeConfig = reapplyRuntimeOverlays(params.runtimeConfig);
assertConfiguredWorkspaceStateReady({
cfg: runtimeConfig,
env: runtimeEnv.env
});
return {
runtimeConfig,
compareConfig: reapplyCompareOverlays(params.sourceConfig),
runtimeEnv,
reapplyRuntimeOverlays,
reapplyCompareOverlays
};
};
if (startupConfigLoad.wroteConfig || authBootstrap.persistedGeneratedToken) {
const startupSnapshot = await startupTrace.measure("config.final-snapshot", () => readConfigFileSnapshot());
startupInternalWriteHash = startupSnapshot.hash ?? null;
startupLastGoodSnapshot = startupSnapshot;
}
setAppliedRuntimeConfigSnapshot(cfgAtStart, startupLastGoodSnapshot.sourceConfig);
applyLoggingConfig(cfgAtStart.logging);
initializePublishedConfigRuntimeEnv(startupLastGoodSnapshot.sourceConfig, {
ownedEnv: collectConfigRuntimeEnvOwnership(startupLastGoodSnapshot.sourceConfig, envBeforeStartupConfigLoad, process.env),
preserveExistingOwnership: true
});
const workerEnvironmentStartup = minimalTestGateway ? void 0 : await startupTrace.measure("worker-environments.store-import", async () => {
return await (await loadWorkerEnvironmentStartupModule()).loadGatewayWorkerEnvironmentStartupState();
});
const { prepareGatewayPluginBootstrap, runGatewayStartupMaintenance } = await startupTrace.measure("plugins.bootstrap-imports", loadStartupPluginsModule);
const pluginGatewayContext = { current: void 0 };
const resolvePluginGatewayContext = () => pluginGatewayContext.current;
await startupTrace.measure("startup.maintenance", () => runGatewayStartupMaintenance({
cfgAtStart,
startupRuntimeConfig,
minimalTestGateway,
log
}));
const pluginBootstrap = await startupTrace.measure("plugins.bootstrap", () => prepareGatewayPluginBootstrap({
cfgAtStart,
activationSourceConfig: startupActivationSourceConfig,
pluginMetadataSnapshot: startupConfigLoad.pluginMetadataSnapshot,
workerProviderIds: workerEnvironmentStartup?.durableProviderIds ?? [],
minimalTestGateway,
ambientEnvTriggers,
log
}));
const { gatewayPluginConfigAtStart, defaultWorkspaceDir, pluginWorkspaceDir, startupPluginIds, pluginManifestRecords, pluginMetadataSnapshot, pluginLookUpTable, baseMethods, ambientAutostartSuppressedChannelIds } = pluginBootstrap;
copyConfigResolutionFacts(cfgAtStart, gatewayPluginConfigAtStart);
publishGatewayPluginRuntimeConfigAtStartup({
runtimeConfig: gatewayPluginConfigAtStart,
sourceConfig: startupLastGoodSnapshot.sourceConfig
});
const coreGatewayMethodNames = listCoreGatewayMethodNames();
const existingPluginMetadataSnapshot = getGatewayPluginMetadataSnapshot();
const currentPluginMetadataSnapshot = existingPluginMetadataSnapshot ?? pluginMetadataSnapshot;
if (!existingPluginMetadataSnapshot) setGatewayPluginMetadataSnapshot(currentPluginMetadataSnapshot, {
config: startupActivationSourceConfig,
compatibleConfigs: [
startupRuntimeConfig,
cfgAtStart,
gatewayPluginConfigAtStart
],
env: process.env,
workspaceDir: pluginWorkspaceDir
});
if (pluginLookUpTable) {
const metrics = pluginLookUpTable.metrics;
startupTrace.detail("plugins.lookup-table", [
["registrySnapshotMs", metrics.registrySnapshotMs],
["manifestRegistryMs", metrics.manifestRegistryMs],
["startupPlanMs", metrics.startupPlanMs],
["ownerMapsMs", metrics.ownerMapsMs],
["totalMs", metrics.totalMs],
["indexPlugins", String(metrics.indexPluginCount)],
["indexPluginCount", metrics.indexPluginCount],
["manifestPlugins", String(metrics.manifestPluginCount)],
["manifestPluginCount", metrics.manifestPluginCount],
["startupPlugins", String(metrics.startupPluginCount)],
["startupPluginCount", metrics.startupPluginCount]
]);
}
return {
opts,
minimalTestGateway,
ambientEnvTriggers,
startupTrace,
loadStartupPluginsModule,
configSnapshot,
startupConfigLoad,
startupActivationSourceConfig,
startupRuntimeConfig,
cfgAtStart,
generatedStartupAuthToken: authBootstrap.generatedToken !== void 0,
resolvedStartupAuthOverride,
startupTailscaleOverride,
activeTaskCount,
applyFixedGatewayOverlays,
prepareReloadCandidate,
startupInternalWriteHash,
startupLastGoodSnapshot,
workerEnvironmentStartup,
pluginGatewayContext,
resolvePluginGatewayContext,
pluginBootstrap,
gatewayPluginConfigAtStart,
defaultWorkspaceDir,
pluginWorkspaceDir,
startupPluginIds,
pluginManifestRecords,
pluginMetadataSnapshot: currentPluginMetadataSnapshot,
pluginLookUpTable,
baseMethods,
ambientAutostartSuppressedChannelIds,
coreGatewayMethodNames,
activateRuntimeSecrets
};
}
//#endregion
//#region src/gateway/server-kernel.ts
const loadGatewayModelCatalogModule = createLazyRuntimeModule(() => import("./server-model-catalog-4q-8QpiP.js"));
const loadWorkerEnvironmentStartupModule = createLazyRuntimeModule(() => import("./server-worker-environment-startup-BsPx_X1k.js"));
const loadWorkerPlacementStartupModule = createLazyRuntimeModule(() => import("./server-worker-placement-startup-D03EYLw5.js"));
const loadGatewayStartupEarlyModule = createLazyRuntimeModule(() => import("./server-startup-early-Bm7PqYdy.js"));
const loadGatewayPluginBootstrapModule = createLazyRuntimeModule(() => import("./server-plugin-bootstrap-Dkzu2wb7.js"));
const loadGatewayShutdownModule = createLazyRuntimeModule(() => import("./server-shutdown.runtime.js"));
const log$1 = createSubsystemLogger("gateway");
const logDiscovery = log$1.child("discovery");
const logTailscale$1 = log$1.child("tailscale");
const logChannels$1 = log$1.child("channels");
const logHealth$1 = log$1.child("health");
const logCron$1 = log$1.child("cron");
const logReload$1 = log$1.child("reload");
const logHooks$1 = log$1.child("hooks");
const logPlugins = log$1.child("plugins");
const logWsControl$1 = log$1.child("ws");
const logSecrets = log$1.child("secrets");
const gatewayKernelLogs = {
log: log$1,
logTailscale: logTailscale$1,
logChannels: logChannels$1,
logHealth: logHealth$1,
logCron: logCron$1,
logReload: logReload$1,
logHooks: logHooks$1,
logWsControl: logWsControl$1
};
const gatewayRuntime = runtimeForLogger(log$1);
const getChannelRuntime = createLazyRuntimeModule(() => import("./runtime-channel-BIP1BwO5.js").then(({ createRuntimeChannel }) => createRuntimeChannel()));
const loadGatewayModelCatalog = async (...args) => {
return (await loadGatewayModelCatalogModule()).loadGatewayModelCatalog(...args);
};
const loadGatewayModelCatalogSnapshot = async (...args) => {
return (await loadGatewayModelCatalogModule()).loadGatewayModelCatalogSnapshot(...args);
};
const readPreparedGatewayModelCatalog = async (...args) => {
return (await loadGatewayModelCatalogModule()).readPreparedGatewayModelCatalog(...args);
};
const loadPreparedGatewayModelCatalogSnapshot = async (...args) => {
return (await loadGatewayModelCatalogModule()).loadPreparedGatewayModelCatalogSnapshot(...args);
};
const readPreparedGatewayModelCatalogOwnerSnapshot = async (...args) => {
return (await loadGatewayModelCatalogModule()).readPreparedGatewayModelCatalogOwnerSnapshot(...args);
};
registerGatewayModelCatalogPrivateAccess(loadGatewayModelCatalogSnapshot, {
loadDeferred: (params) => loadPreparedGatewayModelCatalogSnapshot(params),
readPrepared: readPreparedGatewayModelCatalogOwnerSnapshot
});
function formatRuntimeGatewayAuthTokenWarning() {
const base = "Gateway auth token was missing. Generated a runtime token for this startup without changing config; restart will generate a different token.";
if (!isNixMode) return `${base} Persist one with \`openclaw config set gateway.auth.mode token\` and \`openclaw config set gateway.auth.token <token>\`.`;
return [
base,
"In Nix mode, set gateway.auth.token in your Nix-managed OpenClaw config and rebuild.",
"For the first-party Nix flow, see https://github.com/openclaw/nix-openclaw#quick-start and https://docs.openclaw.ai/install/nix."
].join(" ");
}
async function resetPreparedModelCatalogForTestCore() {
const { resetPreparedModelCatalogStateForTest } = await loadGatewayModelCatalogModule();
await resetPreparedModelCatalogStateForTest();
}
/** Builds the Gateway kernel and internal dispatch surface without creating HTTP servers. */
async function createGatewayKernel(port = 18789, opts = {}, options = {}) {
const suppliedBootId = opts.bootId;
if (suppliedBootId !== void 0 && (suppliedBootId.trim() !== suppliedBootId || !suppliedBootId || suppliedBootId.length > 96)) throw new Error("Gateway boot ID must contain 1 to 96 characters");
const bootId = suppliedBootId ?? randomUUID();
ensureOpenClawCliOnPath();
const releasePluginMetadata = retainGatewayPluginMetadata();
let lifecycleRuntime;
let kernelState;
try {
const bootstrap = await prepareGatewayServerBootstrap({
port,
opts,
log: log$1,
logSecrets,
loadWorkerEnvironmentStartupModule,
formatRuntimeGatewayAuthTokenWarning
});
const runtime = await bootstrap.startupTrace.measure("gateway.kernel-state", () => prepareGatewayKernelState({
bootstrap,
bootId,
port,
opts,
log: log$1,
logChannels: logChannels$1,
logHooks: logHooks$1,
logPlugins,
gatewayRuntime,
resolveChannelRuntime: getChannelRuntime,
loadWorkerEnvironmentStartupModule,
loadWorkerPlacementStartupModule
}));
kernelState = runtime;
const shutdownRuntime = await runtime.startupTrace.measure("gateway.shutdown-runtime-import", async () => (await loadGatewayShutdownModule()).prepareGatewayShutdownRuntime());
const preparedLifecycleRuntime = await runtime.startupTrace.measure("gateway.lifecycle", () => prepareGatewayLifecycle({
runtime,
releasePluginMetadata,
port,
log: log$1,
logCron: logCron$1,
shutdownRuntime
}));
lifecycleRuntime = preparedLifecycleRuntime;
if (bootstrap.cfgAtStart.gateway?.tls?.enabled && !runtime.gatewayTls.enabled) throw new Error(runtime.gatewayTls.error ?? "gateway tls: failed to enable");
const coreRuntime = await runtime.startupTrace.measure("gateway.core-runtime", () => startGatewayCoreRuntime({
lifecycleRuntime: preparedLifecycleRuntime,
port,
log: log$1,
logDiscovery,
logHealth: logHealth$1,
logChannels: logChannels$1,
loadGatewayStartupEarlyModule,
loadGatewayPluginBootstrapModule,
loadGatewayModelCatalog,
loadGatewayModelCatalogSnapshot,
readPreparedGatewayModelCatalog
}));
if (!options.deferEarlyRuntime) await coreRuntime.startEarlyRuntime();
return await runtime.startupTrace.measure("gateway.request-runtime", () => prepareGatewayKernelRequestRuntime({
coreRuntime,
log: log$1,
logHealth: logHealth$1,
hostLifecycle: opts.hostLifecycle
}));
} catch (error) {
return await rethrowGatewayStartupError(error, async () => {
if (lifecycleRuntime) await lifecycleRuntime.closeOnStartupFailure();
else {
kernelState?.mentionInbox.dispose();
clearGatewayAgentCliShim();
clearSecretsRuntimeSnapshotState();
releasePluginMetadata();
}
});
}
}
//#endregion
//#region src/gateway/mcp-app-sandbox-http.ts
const MCP_APP_PERMISSIONS_POLICY = "camera=(), microphone=(), geolocation=(), clipboard-write=()";
function handleMcpAppSandboxHttpRequest(req, res) {
let url;
try {
url = new URL(req.url ?? "/", "http://localhost");
} catch {
respondPlainText(res, 400, "Bad Request");
return true;
}
if (url.pathname !== "/mcp-app-sandbox" || req.method !== "GET" && req.method !== "HEAD") return false;
let csp;
try {
csp = decodeSandboxHostCsp(url.searchParams.get("csp"));
} catch {
respondPlainText(res, 400, "invalid MCP App sandbox policy");
return true;
}
res.statusCode = 200;
res.setHeader("Content-Type", "text/html; charset=utf-8");
res.setHeader("Cache-Control", "no-store");
res.setHeader("Content-Security-Policy", buildSandboxHostContentSecurityPolicy(csp));
res.setHeader("Permissions-Policy", MCP_APP_PERMISSIONS_POLICY);
res.setHeader("Cross-Origin-Resource-Policy", "cross-origin");
res.setHeader("Origin-Agent-Cluster", "?1");
res.setHeader("Referrer-Policy", "no-referrer");
res.setHeader("X-Content-Type-Options", "nosniff");
const html = buildSandboxHostProxyHtml(csp);
res.setHeader("Content-Length", String(Buffer.byteLength(html)));
res.end(req.method === "HEAD" ? void 0 : html);
return true;
}
/** Dedicated listener: only the proxy and explicitly public renderer assets, never Gateway data. */
function createSandboxHostHttpServer(tlsOptions, resolvePluginRegistry) {
const readersByEpoch = /* @__PURE__ */ new WeakMap();
const serveResource = async (req, res) => {
const registry = resolvePluginRegistry?.();
const epoch = registry ? capturePluginRegistryLifecycleEpoch(registry) : void 0;
if (!registry || !epoch || req.method !== "GET" && req.method !== "HEAD") {
respondPlainText(res, 404, "Not Found");
return;
}
let readers = readersByEpoch.get(epoch);
if (!readers) {
readers = /* @__PURE__ */ new Map();
for (const { definition } of registry.boardWidgetContentKinds.values()) {
const read = definition.resources.readPublicResource;
if (read) for (const resourcePath of definition.resources.paths) readers.set(resourcePath, read);
}
readersByEpoch.set(epoch, readers);
}
const pathname = new URL(req.url ?? "/", "http://localhost").pathname;
const reader = readers.get(pathname);
const resource = reader ? await reader(pathname) : void 0;
if (!resource || resolvePluginRegistry?.() !== registry || !isPluginRegistryLifecycleEpochActive(registry, epoch)) {
respondPlainText(res, 404, "Not Found");
return;
}
res.statusCode = 200;
res.setHeader("Content-Type", resource.contentType);
res.setHeader("Content-Length", String(resource.body.byteLength));
res.setHeader("Cache-Control", "no-cache");
res.setHeader("Cross-Origin-Resource-Policy", "cross-origin");
res.setHeader("X-Content-Type-Options", "nosniff");
res.end(req.method === "HEAD" ? void 0 : resource.body);
};
const handler = (req, res) => {
if (handleMcpAppSandboxHttpRequest(req, res)) return;
if (!resolvePluginRegistry) {
respondPlainText(res, 404, "Not Found");
return;
}
serveResource(req, res).catch(() => respondPlainText(res, 503, "Renderer resource unavailable"));
};
return tlsOptions ? createServer$2(tlsOptions, handler) : createServer$1(handler);
}
//#endregion
//#region src/gateway/server/http-listen.ts
const EADDRINUSE_MAX_RETRIES = 20;
const EADDRINUSE_RETRY_INTERVAL_MS = 500;
async function closeServerQuietly(httpServer) {
await new Promise((resolve) => {
try {
httpServer.close(() => resolve());
} catch {
resolve();
}
});
}
/** Listen on the configured gateway host/port, retrying transient EADDRINUSE windows. */
async function listenGatewayHttpServer(params) {
const { httpServer, bindHost, port, retryEaddrinuse = true, serviceName = "gateway", endpointScheme = "ws" } = params;
const maxRetries = retryEaddrinuse ? EADDRINUSE_MAX_RETRIES : 0;
for (const attempt of Array.from({ length: maxRetries + 1 }, (_, index) => index)) try {
await new Promise((resolve, reject) => {
const onError = (err) => {
httpServer.off("listening", onListening);
reject(err);
};
const onListening = () => {
httpServer.off("error", onError);
resolve();
};
httpServer.once("error", onError);
httpServer.once("listening", onListening);
httpServer.listen(port, bindHost);
});
return;
} catch (err) {
const code = err.code;
if (code === "EADDRINUSE" && attempt < maxRetries) {
await closeServerQuietly(httpServer);
await sleep(EADDRINUSE_RETRY_INTERVAL_MS);
continue;
}
if (code === "EADDRINUSE") throw new GatewayLockError(`another ${serviceName} instance is already listening on ${endpointScheme}://${bindHost}:${port}`, err);
throw new GatewayLockError(`failed to bind ${serviceName} socket on ${endpointScheme}://${bindHost}:${port}: ${String(err)}`, err);
}
}
//#endregion
//#region src/gateway/portals/portal-http-proxy.ts
const PORTAL_AUTH_NAME = "openclaw_portal";
function portalAuthCookieName(listenPort) {
return `${PORTAL_AUTH_NAME}_${listenPort}`;
}
const PORTAL_COOKIE_PREFIX = "oc_portal_";
const PORTAL_REFERRER_POLICY = "no-referrer";
const MAX_WEBSOCKET_RESPONSE_HEADER_BYTES = 65536;
const HOP_BY_HOP_HEADERS = /* @__PURE__ */ new Set([
"connection",
"keep-alive",
"proxy-authenticate",
"proxy-authorization",
"proxy-connection",
"te",
"trailer",
"transfer-encoding",
"upgrade"
]);
function tokensEqual(candidate, expected) {
if (!candidate) return false;
const candidateBytes = Buffer.from(candidate);
const expectedBytes = Buffer.from(expected);
return candidateBytes.length === expectedBytes.length && timingSafeEqual(candidateBytes, expectedBytes);
}
function readPortalCookie(cookieHeader, listenPort) {
const authCookieName = portalAuthCookieName(listenPort);
for (const segment of cookieHeader?.split(";") ?? []) {
const separator = segment.indexOf("=");
if (separator < 0 || segment.slice(0, separator).trim() !== authCookieName) continue;
return segment.slice(separator + 1).trim();
}
}
function portalCookiePrefix(cookieNamespace) {
return `${PORTAL_COOKIE_PREFIX}${cookieNamespace}_`;
}
function readTargetCookies(cookieHeader, cookieNamespace) {
const prefix = portalCookiePrefix(cookieNamespace);
return (cookieHeader?.split(";") ?? []).flatMap((segment) => {
const separator = segment.indexOf("=");
if (separator <= 0) return [];
const name = segment.slice(0, separator).trim();
if (!name.startsWith(prefix) || name.length === prefix.length) return [];
return [`${name.slice(prefix.length)}=${segment.slice(separator + 1).trim()}`];
}).join("; ") || void 0;
}
function rewriteTargetCookie(cookie, cookieNamespace) {
const [cookiePair, ...attributes] = cookie.split(";");
const separator = cookiePair?.indexOf("=") ?? -1;
if (!cookiePair || separator <= 0) return;
const name = cookiePair.slice(0, separator).trim();
if (!name) return;
const retainedAttributes = attributes.filter((attribute) => !/^\s*domain\s*=/iu.test(attribute));
const suffix = retainedAttributes.length > 0 ? `;${retainedAttributes.join(";")}` : "";
return `${portalCookiePrefix(cookieNamespace)}${name}=${cookiePair.slice(separator + 1)}${suffix}`;
}
function parsePortalUrl(req) {
try {
return new URL(req.url ?? "/", "http://openclaw.invalid");
} catch {
return;
}
}
function authorizePortalRequest(req, target) {
const url = parsePortalUrl(req);
if (tokensEqual(url?.searchParams.get(PORTAL_AUTH_NAME) ?? void 0, target.token)) {
url?.searchParams.delete(PORTAL_AUTH_NAME);
return {
kind: "authorized",
requestPath: `${url?.pathname ?? "/"}${url?.search ?? ""}`,
setCookie: true
};
}
if (tokensEqual(readPortalCookie(req.headers.cookie, target.listenPort), target.token)) {
url?.searchParams.delete(PORTAL_AUTH_NAME);
return {
kind: "authorized",
requestPath: `${url?.pathname ?? "/"}${url?.search ?? ""}`,
setCookie: false
};
}
return { kind: "unauthorized" };
}
function portalCookie(target, tls) {
return `${portalAuthCookieName(target.listenPort)}=${target.token}; HttpOnly; SameSite=Lax; Path=/${tls ? "; Secure" : ""}`;
}
function setProxyResponseHeader(res, name, value, cookieNamespace) {
if (name !== "set-cookie") {
res.setHeader(name, value);
return;
}
const existing = res.getHeader("Set-Cookie");
const existingCookies = existing === void 0 ? [] : Array.isArray(existing) ? existing : [existing];
const rewrittenCookies = (Array.isArray(value) ? value : [String(value)]).flatMap((cookie) => {
const rewritten = rewriteTargetCookie(cookie, cookieNamespace);
return rewritten ? [rewritten] : [];
});
const cookies = [...existingCookies.map(String), ...rewrittenCookies];
if (cookies.length > 0) res.setHeader("Set-Cookie", cookies);
}
function htmlResponse(res, statusCode, html, headOnly) {
res.statusCode = statusCode;
res.setHeader("Content-Type", "text/html; charset=utf-8");
res.setHeader("Cache-Control", "no-store");
res.setHeader("X-Content-Type-Options", "nosniff");
res.setHeader("Referrer-Policy", PORTAL_REFERRER_POLICY);
res.setHeader("Content-Length", String(Buffer.byteLength(html)));
res.end(headOnly ? void 0 : html);
}
function respondPortalUnauthorized(req, res) {
htmlResponse(res, 401, "<!doctype html><meta charset=utf-8><title>Private portal</title><p>This portal is private. Open it from the OpenClaw Control UI.</p>", req.method === "HEAD");
}
function portalWaitingHtml(targetPort) {
return `<!doctype html><meta charset=utf-8><meta http-equiv="refresh" content="2"><title>Waiting for app</title><p>Waiting for the app on port ${targetPort}…</p>`;
}
function respondPortalWaiting(req, res, targetPort) {
htmlResponse(res, 502, portalWaitingHtml(targetPort), req.method === "HEAD");
}
async function connectPortalTarget(target) {
if (target.kind === "worker") return await target.connect();
return net.connect({
host: "localhost",
autoSelectFamily: true,
port: target.port
});
}
function connectionHeaderTokens(headers) {
const value = headers.connection;
const joined = Array.isArray(value) ? value.join(",") : value;
return new Set((joined ?? "").split(",").map((token) => token.trim().toLowerCase()).filter(Boolean));
}
function proxyHeaders(headers, cookieNamespace) {
const result = {};
const connectionTokens = connectionHeaderTokens(headers);
for (const [name, value] of Object.entries(headers)) {
const normalized = name.toLowerCase();
if (value === void 0 || HOP_BY_HOP_HEADERS.has(normalized) || connectionTokens.has(normalized)) continue;
if (normalized === "cookie" && cookieNamespace !== void 0) {
const cookie = readTargetCookies(Array.isArray(value) ? value.join("; ") : value, cookieNamespace);
if (cookie) result.cookie = cookie;
continue;
}
if (normalized === "referer" && String(value).includes(`${PORTAL_AUTH_NAME}=`)) continue;
result[normalized] = value;
}
return result;
}
/** Proxies one authorized portal request to its local or worker target. */
function handlePortalProxyRequest(params) {
const { req, res, target, tls } = params;
const authorization = authorizePortalRequest(req, target);
if (authorization.kind === "unauthorized") {
respondPortalUnauthorized(req, res);
return;
}
if (authorization.setCookie) res.setHeader("Set-Cookie", portalCookie(target, tls));
const headers = proxyHeaders(req.headers, target.cookieNamespace);
const originalHost = req.headers.host;
const targetPort = target.target.kind === "local" ? target.target.port : target.target.remotePort;
headers.host = `localhost:${targetPort}`;
headers["x-forwarded-for"] = req.socket.remoteAddress ?? "";
headers["x-forwarded-proto"] = tls ? "https" : "http";
if (originalHost) headers["x-forwarded-host"] = originalHost;
connectPortalTarget(target.target).then((targetSocket) => {
if (req.aborted || res.destroyed) {
targetSocket.destroy();
return;
}
const proxyReq = request({
hostname: "localhost",
createConnection: () => targetSocket,
port: targetPort,
method: req.method,
path: authorization.requestPath,
headers
});
proxyReq.once("response", (proxyRes) => {
for (const [name, value] of Object.entries(proxyHeaders(proxyRes.headers))) if (value !== void 0) setProxyResponseHeader(res, name, value, target.cookieNamespace);
res.setHeader("Referrer-Policy", PORTAL_REFERRER_POLICY);
res.statusCode = proxyRes.statusCode ?? 502;
proxyRes.once("error", () => res.destroy());
res.flushHeaders();
proxyRes.pipe(res);
});
proxyReq.once("error", () => {
if (!res.headersSent) respondPortalWaiting(req, res, targetPort);
else res.destroy();
});
proxyReq.once("close", () => {
if (target.target.kind === "worker" && !res.headersSent && !res.writableEnded) respondPortalWaiting(req, res, targetPort);
});
res.once("close", () => proxyReq.destroy());
req.pipe(proxyReq);
}, () => {
if (!res.headersSent && !res.writableEnded && !res.destroyed) respondPortalWaiting(req, res, targetPort);
});
}
function websocketHeaders(req, targetPort, cookieNamespace, requestPath) {
const lines = [`${req.method ?? "GET"} ${requestPath} HTTP/1.1`];
for (const [name, value] of Object.entries(req.headers)) {
const normalized = name.toLowerCase();
if (value === void 0 || normalized === "host" || HOP_BY_HOP_HEADERS.has(normalized) && normalized !== "connection" && normalized !== "upgrade") continue;
if (normalized === "cookie") {
const cookie = readTargetCookies(Array.isArray(value) ? value.join("; ") : value, cookieNamespace);
if (cookie) lines.push(`cookie: ${cookie}`);
continue;
}
if (normalized === "referer" && String(value).includes(`${PORTAL_AUTH_NAME}=`)) continue;
for (const item of Array.isArray(value) ? value : [value]) lines.push(`${normalized}: ${item}`);
}
lines.push(`host: localhost:${targetPort}`, "", "");
return lines.join("\r\n");
}
function rejectPortalUpgrade(socket) {
socket.end("HTTP/1.1 401 Unauthorized\r\nContent-Type: text/plain; charset=utf-8\r\nContent-Length: 12\r\nConnection: close\r\n\r\nUnauthorized");
}
function respondUpgradeWaiting(socket, targetPort) {
const html = portalWaitingHtml(targetPort);
socket.end(`HTTP/1.1 502 Bad Gateway\r
Content-Type: text/html; charset=utf-8\r
Cache-Control: no-store\r\nReferrer-Policy: ${PORTAL_REFERRER_POLICY}\r\nContent-Length: ${Buffer.byteLength(html)}\r\nConnection: close\r\n\r\n${html}`);
}
function forwardWebSocketResponse(targetSocket, browserSocket, cookieNamespace, onResponse) {
let pending = Buffer.alloc(0);
const onData = (chunk) => {
pending = Buffer.concat([pending, chunk]);
const headerEnd = pending.indexOf("\r\n\r\n");
if (headerEnd < 0) {
if (pending.length > MAX_WEBSOCKET_RESPONSE_HEADER_BYTES) {
targetSocket.destroy();
browserSocket.destroy();
}
return;
}
targetSocket.off("data", onData);
const rewrittenLines = pending.subarray(0, headerEnd).toString("latin1").split("\r\n").flatMap((line) => {
const separator = line.indexOf(":");
if (separator <= 0 || line.slice(0, separator).trim().toLowerCase() !== "set-cookie") return [line];
const rewritten = rewriteTargetCookie(line.slice(separator + 1).trimStart(), cookieNamespace);
return rewritten ? [`${line.slice(0, separator)}: ${rewritten}`] : [];
});
onResponse();
browserSocket.write(`${rewrittenLines.join("\r\n")}\r\n\r\n`);
const remainder = pending.subarray(headerEnd + 4);
if (remainder.length > 0) browserSocket.write(remainder);
targetSocket.pipe(browserSocket);
};
targetSocket.on("data", onData);
}
/** Splices an authorized portal WebSocket upgrade into its local or worker target. */
function handlePortalProxyUpgrade(params) {
const { req, socket, head, target, upgradedSockets } = params;
const authorization = authorizePortalRequest(req, target);
if (authorization.kind !== "authorized") {
rejectPortalUpgrade(socket);
return;
}
const targetPort = target.target.kind === "local" ? target.target.port : target.target.remotePort;
upgradedSockets.add(socket);
socket.once("close", () => upgradedSockets.delete(socket));
connectPortalTarget(target.target).then((targetSocket) => {
if (socket.destroyed) {
targetSocket.destroy();
return;
}
upgradedSockets.add(targetSocket);
let responseStarted = false;
let waitingResponseSent = false;
const closeUpgrade = () => {
if (target.target.kind === "worker" && !responseStarted && !socket.destroyed) {
if (!waitingResponseSent) {
waitingResponseSent = true;
respondUpgradeWaiting(socket, targetPort);
}
return;
}
socket.destroy();
};
socket.once("close", () => targetSocket.destroy());
targetSocket.once("close", () => {
upgradedSockets.delete(targetSocket);
closeUpgrade();
});
targetSocket.once("end", closeUpgrade);
socket.once("error", () => targetSocket.destroy());
targetSocket.once("error", closeUpgrade);
const spliceUpgrade = () => {
forwardWebSocketResponse(targetSocket, socket, target.cookieNamespace, () => {
responseStarted = true;
});
targetSocket.write(websocketHeaders(req, targetPort, target.cookieNamespace, authorization.requestPath));
if (head.length > 0) targetSocket.write(head);
socket.pipe(targetSocket);
};
if (target.target.kind === "worker") spliceUpgrade();
else targetSocket.once("connect", spliceUpgrade);
}, () => {
if (!socket.destroyed) {
if (target.target.kind === "worker") respondUpgradeWaiting(socket, targetPort);
else socket.destroy();
}
});
}
//#endregion
//#region src/gateway/portals/portal-service.ts
const PORTAL_PORT_ALLOCATION_ATTEMPTS = 10;
function removeServers(shared, owned) {
for (const server of owned) {
const index = shared.indexOf(server);
if (index >= 0) shared.splice(index, 1);
}
}
async function closeServers(servers) {
await Promise.all(servers.map((server) => new Promise((resolve) => {
if (!server.listening) {
resolve();
return;
}
server.close(() => resolve());
server.closeAllConnections();
})));
}
function formatPortalHost(host) {
const openableHost = host === "0.0.0.0" ? "127.0.0.1" : host === "::" ? "::1" : host;
return openableHost.includes(":") ? `[${openableHost}]` : openableHost;
}
/** Creates the gateway-lifetime registry and per-portal transport listeners. */
function createGatewayPortalService(params) {
const entries = /* @__PURE__ */ new Map();
const operations = /* @__PURE__ */ new Map();
let closed = false;
const summarize = (portal) => {
const host = params.httpBindHosts[0];
if (!host) throw new Error("Gateway listener must start before opening a portal");
const scheme = params.tlsOptions ? "https" : "http";
const tokenQuery = `openclaw_portal=${portal.token}`;
const publicUrl = `${scheme}://${formatPortalHost(host)}:${portal.listenPort}${portal.path ?? "/"}`;
const openableUrl = new URL(publicUrl);
openableUrl.searchParams.set("openclaw_portal", portal.token);
return {
id: portal.id,
title: portal.title,
port: portal.target.kind === "local" ? portal.target.port : portal.target.remotePort,
listenPort: portal.listenPort,
tokenQuery,
url: openableUrl.toString(),
publicUrl,
...portal.path ? { path: portal.path } : {},
...portal.description ? { description: portal.description } : {},
...portal.origin ? { origin: portal.origin } : {},
createdAtMs: portal.createdAtMs
};
};
const serialize = async (id, operation) => {
const result = (operations.get(id) ?? Promise.resolve()).then(operation, operation);
const completion = result.then(() => void 0, () => void 0);
operations.set(id, completion);
try {
return await result;
} finally {
if (operations.get(id) === completion) operations.delete(id);
}
};
const closeEntry = async (id) => {
const runtime = entries.get(id);
if (!runtime) return;
entries.delete(id);
removeServers(params.httpServers, runtime.servers);
for (const socket of runtime.upgradedSockets) socket.destroy();
runtime.upgradedSockets.clear();
await closeServers(runtime.servers);
await runtime.onClose?.();
};
const summarizeEntries = (selected) => Array.from(selected, ({ portal }) => summarize(portal)).toSorted((left, right) => left.createdAtMs - right.createdAtMs || left.id.localeCompare(right.id));
return {
open: async (input) => {
const target = input.target ?? {
kind: "local",
port: input.targetPort
};
const targetPort = target.kind === "local" ? target.port : target.remotePort;
const id = target.kind === "local" ? `p${targetPort}` : `p${targetPort}-worker-${sha256HexPrefixCore(target.environmentId, 32)}-${target.ownerEpoch}`;
return await serialize(id, async () => {
let releaseTarget = input.onClose;
try {
if (closed) throw new Error("portals unavailable");
input.assertCurrent?.();
const existing = entries.get(id);
if (existing) {
existing.portal.title = input.title?.trim() || existing.portal.title;
if (input.description !== void 0) existing.portal.description = input.description;
if (input.path !== void 0) existing.portal.path = input.path;
if (input.origin !== void 0) existing.portal.origin = input.origin;
return summarize(existing.portal);
}
if (params.httpBindHosts.length === 0) throw new Error("Gateway listener must start before opening a portal");
const portal = {
id,
title: input.title?.trim() || `Port ${targetPort}`,
...input.description ? { description: input.description } : {},
...input.path ? { path: input.path } : {},
...input.origin ? { origin: input.origin } : {},
target,
token: randomBytes(32).toString("hex"),
cookieNamespace: randomBytes(16).toString("hex"),
listenPort: 0,
createdAtMs: Date.now()
};
const upgradedSockets = /* @__PURE__ */ new Set();
const handler = (req, res) => handlePortalProxyRequest({
req,
res,
target: portal,
tls: Boolean(params.tlsOptions)
});
const servers = params.httpBindHosts.map(() => params.tlsOptions ? createServer$2(params.tlsOptions, handler) : createServer$1(handler));
for (const server of servers) server.on("upgrade", (req, socket, head) => handlePortalProxyUpgrade({
req,
socket,
head,
target: portal,
upgradedSockets
}));
params.httpServers.push(...servers);
try {
const primaryServer = servers[0];
const primaryHost = params.httpBindHosts[0];
if (!primaryServer || !primaryHost) throw new Error("Missing primary portal HTTP server");
for (let attempt = 0; attempt < PORTAL_PORT_ALLOCATION_ATTEMPTS; attempt += 1) {
await listenGatewayHttpServer({
httpServer: primaryServer,
bindHost: primaryHost,
port: 0,
retryEaddrinuse: false,
serviceName: "portal",
endpointScheme: params.tlsOptions ? "https" : "http"
});
const address = primaryServer.address();
if (!address || typeof address === "string") throw new Error("Portal listener failed to resolve its port");
if (target.kind === "worker" || address.port !== targetPort) {
portal.listenPort = address.port;
break;
}
await closeServers([primaryServer]);
}
if (portal.listenPort === 0) throw new Error(`Portal listener repeatedly allocated target port ${targetPort}`);
for (const [index, host] of params.httpBindHosts.entries()) {
if (index === 0) continue;
const server = servers[index];
if (!server) throw new Error(`Missing portal HTTP server for bind host ${host}`);
await listenGatewayHttpServer({
httpServer: server,
bindHost: host,
port: portal.listenPort,
retryEaddrinuse: false,
serviceName: "portal",
endpointScheme: params.tlsOptions ? "https" : "http"
});
}
input.assertCurrent?.();
} catch (error) {
removeServers(params.httpServers, servers);
await closeServers(servers);
throw error;
}
entries.set(id, {
portal,
servers,
upgradedSockets,
...input.onClose ? { onClose: input.onClose } : {}
});
releaseTarget = void 0;
return summarize(portal);
} finally {
await releaseTarget?.();
}
});
},
list: () => summarizeEntries(entries.values()),
listWorkerPortals: (environmentId, ownerEpoch) => summarizeEntries([...entries.values()].filter(({ portal }) => portal.target.kind === "worker" && portal.target.environmentId === environmentId && portal.target.ownerEpoch === ownerEpoch)),
close: async (id, assertCurrent) => {
await serialize(id, () => {
assertCurrent?.();
return closeEntry(id);
});
},
closeWorkerPortals: async (environmentId, ownerEpoch) => {
const environmentSuffix = `-worker-${sha256HexPrefixCore(environmentId, 32)}-`;
const ids = [.../* @__PURE__ */ new Set([...entries.keys(), ...operations.keys()])].filter((id) => {
const separator = id.indexOf(environmentSuffix);
return separator >= 0 && (ownerEpoch === void 0 || id.slice(separator + environmentSuffix.length) === String(ownerEpoch));
});
await Promise.all(ids.map((id) => serialize(id, () => closeEntry(id))));
},
closeAll: async () => {
closed = true;
const ids = /* @__PURE__ */ new Set([...entries.keys(), ...operations.keys()]);
await Promise.all([...ids].map((id) => serialize(id, () => closeEntry(id))));
}
};
}
//#endregion
//#region src/channels/plugins/gateway-auth-bypass.ts
const GATEWAY_AUTH_API_ARTIFACT_BASENAME = "gateway-auth-api.js";
const MISSING_PUBLIC_SURFACE_PREFIX = "Unable to resolve bundled plugin public surface ";
/** Resolves to null when the plugin is not activated or ships no gateway auth artifact. */
async function loadChannelGatewayAuthApi(channelId) {
try {
return await tryLoadActivatedBundledPluginPublicSurfaceModule({
dirName: channelId,
artifactBasename: GATEWAY_AUTH_API_ARTIFACT_BASENAME
});
} catch (error) {
if (error instanceof Error && error.message.startsWith(MISSING_PUBLIC_SURFACE_PREFIX)) return null;
throw error;
}
}
/**
* Resolves configured gateway auth bypass paths from a channel plugin artifact.
*/
async function resolveBundledChannelGatewayAuthBypassPaths(params) {
return ((await loadChannelGatewayAuthApi(params.channelId))?.resolveGatewayAuthBypassPaths?.({ cfg: params.cfg }) ?? []).flatMap((path) => typeof path === "string" && path.trim() ? [path.trim()] : []);
}
//#endregion
//#region src/gateway/server-http-plugin-auth.ts
let pluginGatewayAuthBypassPathsCache = /* @__PURE__ */ new WeakMap();
registerPluginMetadataProcessMemoLifecycleClear(() => {
pluginGatewayAuthBypassPathsCache = /* @__PURE__ */ new WeakMap();
});
async function resolvePluginGatewayAuthBypassPaths(configSnapshot) {
const paths = /* @__PURE__ */ new Set();
const configuredChannels = configSnapshot.channels;
if (!configuredChannels || Object.keys(configuredChannels).length === 0) return paths;
for (const channelId of Object.keys(configuredChannels)) for (const path of await resolveBundledChannelGatewayAuthBypassPaths({
channelId,
cfg: configSnapshot
})) paths.add(path);
return paths;
}
function getCachedPluginGatewayAuthBypassPaths(configSnapshot) {
const cache = pluginGatewayAuthBypassPathsCache;
const cached = cache.get(configSnapshot);
if (cached) return cached;
const resolved = resolvePluginGatewayAuthBypassPaths(configSnapshot).catch((error) => {
cache.delete(configSnapshot);
throw error;
});
cache.set(configSnapshot, resolved);
return resolved;
}
function shouldEnforceDefaultPluginGatewayAuth(pathContext) {
return pathContext.malformedEncoding || pathContext.decodePassLimitReached || isProtectedPluginRoutePathFromContext(pathContext);
}
//#endregion
//#region src/gateway/server-http-probes.ts
const getHttpAuthUtilsModule$2 = createLazyRuntimeModule(() => import("./http-auth-utils-BKXWn2Eo.js"));
async function shouldIncludeGatewayProbeDetails(params) {
if (readPreparedGatewayIngressAttribution(params.req)?.kind === "direct-local" || !readPreparedGatewayIngressAttribution(params.req) && isLocalDirectRequest(params.req, params.trustedProxies, params.allowRealIpFallback)) return true;
if (params.resolvedAuth.mode === "none") return false;
const { getBearerToken, resolveHttpBrowserOriginPolicy } = await getHttpAuthUtilsModule$2();
const bearerToken = getBearerToken(params.req);
return (await authorizeHttpGatewayConnect({
auth: params.resolvedAuth,
connectAuth: bearerToken ? {
token: bearerToken,
password: bearerToken
} : null,
req: params.req,
trustedProxies: params.trustedProxies,
allowRealIpFallback: params.allowRealIpFallback,
rateLimiter: params.rateLimiter,
browserOriginPolicy: resolveHttpBrowserOriginPolicy(params.req)
})).ok;
}
function startupProbeBody(result, includeDetails) {
if (!includeDetails) return JSON.stringify({
ok: result.ok,
status: result.status
});
return JSON.stringify({
ok: result.ok,
status: result.status,
version: resolveRuntimeServiceVersion(process.env),
uptimeMs: result.uptimeMs,
...result.status === "starting" ? { pendingReason: result.pendingReason } : {}
});
}
/** Handles live/ready/startup probe endpoints before normal gateway routing. */
async function handleGatewayProbeRequest(req, res, requestPath, resolvedAuth, trustedProxies, allowRealIpFallback, rateLimiter, getReadiness, getStartup) {
const status = classifyGatewayProbePath(requestPath);
if (status === "namespace" || status === "outside") return false;
const method = (req.method ?? "GET").toUpperCase();
if (method !== "GET" && method !== "HEAD") {
res.statusCode = 405;
res.setHeader("Allow", "GET, HEAD");
res.setHeader("Content-Type", "text/plain; charset=utf-8");
res.end("Method Not Allowed");
return true;
}
res.setHeader("Content-Type", "application/json; charset=utf-8");
res.setHeader("Cache-Control", "no-store");
let statusCode;
let body;
if (status === "ready" && getReadiness) {
const includeDetails = await shouldIncludeGatewayProbeDetails({
req,
resolvedAuth,
trustedProxies,
allowRealIpFallback,
rateLimiter
});
try {
const result = getReadiness();
statusCode = result.ready ? 200 : 503;
body = JSON.stringify(includeDetails ? result : { ready: result.ready });
} catch {
statusCode = 503;
body = JSON.stringify(includeDetails ? {
ready: false,
failing: ["internal"],
uptimeMs: 0
} : { ready: false });
}
} else if (status === "startup") {
const includeDetails = await shouldIncludeGatewayProbeDetails({
req,
resolvedAuth,
trustedProxies,
allowRealIpFallback,
rateLimiter
});
try {
const result = getStartup?.() ?? {
ok: true,
status: "started",
uptimeMs: 0
};
statusCode = result.ok ? 200 : 503;
body = startupProbeBody(result, includeDetails);
} catch {
const result = {
ok: false,
status: "starting",
uptimeMs: 0,
pendingReason: "internal"
};
statusCode = 503;
body = startupProbeBody(result, includeDetails);
}
} else {
statusCode = 200;
body = JSON.stringify({
ok: true,
status
});
}
res.statusCode = statusCode;
res.setHeader("Content-Length", String(Buffer.byteLength(body)));
res.end(method === "HEAD" ? void 0 : body);
return true;
}
//#endregion
//#region src/gateway/server-http-upgrades.ts
const getPluginNodeCapabilityAuthModule$1 = createLazyRuntimeModule(() => import("./plugin-node-capability-auth-BP5OVXJc.js"));
const getHttpAuthUtilsModule$1 = createLazyRuntimeModule(() => import("./http-auth-utils-BKXWn2Eo.js"));
const getPluginRouteRuntimeScopesModule$1 = createLazyRuntimeModule(() => import("./plugin-route-runtime-scopes-CDEO-C-s.js"));
function writeUpgradeAuthFailure(socket, auth) {
if (auth.rateLimited) {
const retryAfterSeconds = auth.retryAfterMs && auth.retryAfterMs > 0 ? Math.ceil(auth.retryAfterMs / 1e3) : void 0;
const body = JSON.stringify({ error: {
message: "Too many failed authentication attempts. Please try again later.",
type: "rate_limited"
} });
socket.write([
"HTTP/1.1 429 Too Many Requests",
...retryAfterSeconds ? [`Retry-After: ${retryAfterSeconds}`] : [],
"Content-Type: application/json; charset=utf-8",
`Content-Length: ${Buffer.byteLength(body, "utf8")}`,
"Connection: close",
"",
body
].join("\r\n"));
return;
}
if (auth.reason === "proxy_attribution_required") {
const body = JSON.stringify({ error: {
message: `Proxy client attribution is required. ${PROXY_ATTRIBUTION_GUIDANCE}`,
type: PROXY_ATTRIBUTION_REQUIRED_REASON
} });
socket.write([
"HTTP/1.1 403 Forbidden",
"Content-Type: application/json; charset=utf-8",
`Content-Length: ${Buffer.byteLength(body, "utf8")}`,
"Connection: close",
"",
body
].join("\r\n"));
return;
}
socket.write("HTTP/1.1 401 Unauthorized\r\nConnection: close\r\n\r\n");
}
function handleBudgetedGatewayWebSocketUpgrade(params) {
const { req, socket, head, wss, preauthConnectionBudget, preauthBudgetKey, ingressName } = params;
const allowsRestartStartupPreauth = ingressName === "Gateway" && isGatewayRestartDraining() && getGatewaySuspendAdmissionPhase() === "accepting" && params.isStartupPending?.() === true;
if (isGatewayWorkAdmissionClosed() && !allowsRestartStartupPreauth && (ingressName === "Worker" || isGatewayRestartDraining() || getGatewaySuspendAdmissionPhase() !== "draining" && getGatewaySuspendAdmissionPhase() !== "prepared")) {
writeGatewayUpgradeServiceUnavailable(socket, `${ingressName} websocket admission closed`);
socket.destroy();
return;
}
if (wss.listenerCount("connection") === 0) {
writeGatewayUpgradeServiceUnavailable(socket, `${ingressName} websocket handlers unavailable`);
socket.destroy();
return;
}
if (!preauthConnectionBudget.acquire(preauthBudgetKey)) {
writeGatewayUpgradeServiceUnavailable(socket, "Too many unauthenticated sockets");
socket.destroy();
return;
}
let budgetTransferred = false;
const releaseUpgradeBudget = () => {
if (!budgetTransferred) {
budgetTransferred = true;
preauthConnectionBudget.release(preauthBudgetKey);
}
};
socket.once("close", releaseUpgradeBudget);
try {
wss.handleUpgrade(req, socket, head, (ws) => {
const ingressSocket = ws;
ingressSocket["__openclawPreauthBudgetKey"] = preauthBudgetKey;
params.prepareSocket?.(ingressSocket);
wss.emit("connection", ws, req);
if (ingressSocket["__openclawPreauthBudgetClaimed"]) {
budgetTransferred = true;
socket.off("close", releaseUpgradeBudget);
}
});
} catch (error) {
socket.off("close", releaseUpgradeBudget);
releaseUpgradeBudget();
throw error;
}
}
/** Attaches WebSocket and plugin-upgrade routing to an already-created HTTP server. */
function attachGatewayUpgradeHandler(opts) {
const { httpServer, wss, handlePluginUpgrade, shouldEnforcePluginGatewayAuth, resolvePluginNodeCapabilityRoute, clients, preauthConnectionBudget, resolvedAuth, rateLimiter, publicRateLimiter, workerIngressEnabled, log } = opts;
const getResolvedAuth = opts.getResolvedAuth ?? (() => resolvedAuth);
httpServer.on("upgrade", (req, socket, head) => {
markGatewayIngressTransport(req, opts.ingressTransport ?? { kind: "ordinary" });
const handleUpgrade = async () => {
const configSnapshot = getRuntimeConfig();
const trustedProxies = configSnapshot.gateway?.trustedProxies ?? [];
const allowRealIpFallback = configSnapshot.gateway?.allowRealIpFallback === true;
const ingressAttribution = prepareGatewayIngressAttribution({
req,
trustedProxies,
allowRealIpFallback
});
const requestClientIp = ingressAttribution.kind === "unattributable-proxy" ? ingressAttribution.remoteAddress : ingressAttribution.clientIp;
const originalRequestPath = URL.parse(req.url ?? "/", "http://localhost")?.pathname;
const originalWorkerGatewayRoute = originalRequestPath ? classifyWorkerGatewayPath(originalRequestPath) : "outside";
if (originalWorkerGatewayRoute !== "outside" && ingressAttribution.kind === "unattributable-proxy") {
opts.reportUnattributableProxy?.(ingressAttribution);
writeUpgradeAuthFailure(socket, {
ok: false,
reason: ingressAttribution.reason
});
socket.destroy();
return;
}
if (originalWorkerGatewayRoute === "worker" && !workerIngressEnabled) {
writeGatewayUpgradeServiceUnavailable(socket, "Worker websocket ingress unavailable");
socket.destroy();
return;
}
if (originalWorkerGatewayRoute === "worker") {
const rateCheck = publicRateLimiter?.check(requestClientIp, AUTH_RATE_LIMIT_SCOPE_WORKER_ADMISSION);
if (rateCheck && !rateCheck.allowed) {
writeUpgradeAuthFailure(socket, {
ok: false,
reason: "rate_limited",
rateLimited: true,
retryAfterMs: rateCheck.retryAfterMs
});
socket.destroy();
return;
}
try {
handleBudgetedGatewayWebSocketUpgrade({
req,
socket,
head,
wss,
preauthConnectionBudget,
preauthBudgetKey: requestClientIp,
ingressName: "Worker",
prepareSocket: (workerSocket) => {
workerSocket[GATEWAY_WS_CONNECTION_KIND_PROPERTY] = "worker";
markPublicWorkerIngress(workerSocket, {
clientIp: requestClientIp,
rateLimiter: publicRateLimiter
});
}
});
} catch {
throw new Error("public worker websocket upgrade failed");
}
return;
}
if (originalWorkerGatewayRoute !== "outside") {
socket.write("HTTP/1.1 404 Not Found\r\nConnection: close\r\n\r\n");
socket.destroy();
return;
}
const scopedNodeCapability = normalizePluginNodeCapabilityScopedUrl(req.url ?? "/");
if (scopedNodeCapability.malformedScopedPath) {
writeUpgradeAuthFailure(socket, {
ok: false,
reason: "unauthorized"
});
socket.destroy();
return;
}
if (scopedNodeCapability.rewrittenUrl) req.url = scopedNodeCapability.rewrittenUrl;
const resolvedAuthLocal = getResolvedAuth();
const requestPath = scopedNodeCapability.pathname;
const pathContext = resolvePluginRoutePathContext(requestPath);
if (classifyWorkerGatewayPath(requestPath) !== "outside") {
socket.write("HTTP/1.1 404 Not Found\r\nConnection: close\r\n\r\n");
socket.destroy();
return;
}
const nodeCapability = resolvePluginNodeCapabilityRoute?.(pathContext);
if (ingressAttribution.kind === "unattributable-proxy") {
opts.reportUnattributableProxy?.(ingressAttribution);
if (nodeCapability || !opts.isPluginAuthenticatedRoute?.(pathContext)) {
writeUpgradeAuthFailure(socket, {
ok: false,
reason: ingressAttribution.reason
});
socket.destroy();
return;
}
}
if (nodeCapability) {
const { authorizePluginNodeCapabilityRequest } = await getPluginNodeCapabilityAuthModule$1();
const ok = await authorizePluginNodeCapabilityRequest({
req,
auth: resolvedAuthLocal,
trustedProxies,
allowRealIpFallback,
clients,
nodeCapability,
capability: scopedNodeCapability.capability,
malformedScopedPath: scopedNodeCapability.malformedScopedPath,
rateLimiter
});
if (!ok.ok) {
writeUpgradeAuthFailure(socket, ok);
socket.destroy();
return;
}
}
if (handlePluginUpgrade) {
let pluginGatewayAuthSatisfied = false;
let pluginGatewayRequestAuth;
let pluginGatewayRequestOperatorScopes;
if ((shouldEnforcePluginGatewayAuth ?? shouldEnforceDefaultPluginGatewayAuth)(pathContext) && !(await getCachedPluginGatewayAuthBypassPaths(configSnapshot)).has(requestPath)) {
const { checkGatewayHttpRequestAuth } = await getHttpAuthUtilsModule$1();
const authCheck = await checkGatewayHttpRequestAuth({
req,
auth: resolvedAuthLocal,
trustedProxies,
allowRealIpFallback,
rateLimiter,
cfg: configSnapshot
});
if (!authCheck.ok) {
writeUpgradeAuthFailure(socket, authCheck.authResult);
socket.destroy();
return;
}
pluginGatewayAuthSatisfied = true;
pluginGatewayRequestAuth = authCheck.requestAuth;
const { resolvePluginRouteRuntimeOperatorScopes } = await getPluginRouteRuntimeScopesModule$1();
pluginGatewayRequestOperatorScopes = resolvePluginRouteRuntimeOperatorScopes(req, authCheck.requestAuth);
}
if (await handlePluginUpgrade(req, socket, head, pathContext, {
gatewayAuthSatisfied: pluginGatewayAuthSatisfied,
gatewayRequestAuth: pluginGatewayRequestAuth,
gatewayRequestOperatorScopes: pluginGatewayRequestOperatorScopes,
gatewayRequestClientIp: requestClientIp
})) return;
}
if (ingressAttribution.kind === "unattributable-proxy") {
writeUpgradeAuthFailure(socket, {
ok: false,
reason: ingressAttribution.reason
});
socket.destroy();
return;
}
if (requestPath === "/desktop/observe") {
if (!opts.desktopSessionRegistry) {
writeGatewayUpgradeServiceUnavailable(socket, "desktop observe unavailable");
socket.destroy();
return;
}
if (isGatewayWorkAdmissionClosed()) {
writeGatewayUpgradeServiceUnavailable(socket, "Gateway websocket admission closed");
socket.destroy();
return;
}
const { handleDesktopObserveUpgrade } = await import("./observe-bridge-BoVfcTPP.js");
handleDesktopObserveUpgrade(req, socket, head, { registry: opts.desktopSessionRegistry });
return;
}
if (requestPath === "/node-desktop/attach" || requestPath === "/node-portal/attach") {
const context = opts.getGatewayRequestContext?.();
if (!opts.nodeDesktopStreamBroker || !context) {
writeGatewayUpgradeServiceUnavailable(socket, `node ${requestPath === "/node-desktop/attach" ? "desktop" : "portal"} attach unavailable`);
socket.destroy();
return;
}
if (isGatewayWorkAdmissionClosed()) {
writeGatewayUpgradeServiceUnavailable(socket, "Gateway websocket admission closed");
socket.destroy();
return;
}
await opts.nodeDesktopStreamBroker.handleUpgrade(req, socket, head, context.nodeRegistry);
return;
}
try {
handleBudgetedGatewayWebSocketUpgrade({
req,
socket,
head,
wss,
preauthConnectionBudget,
preauthBudgetKey: requestClientIp,
ingressName: "Gateway",
isStartupPending: opts.isStartupPending
});
} catch {
throw new Error("gateway websocket upgrade failed");
}
};
runHttpConnectionRequest(req, () => runWithDiagnosticTraceContext(createDiagnosticTraceContext(), handleUpgrade), "upgrade").catch((err) => {
const remoteAddress = socket.remoteAddress ?? "unknown";
const errorMessage = err instanceof Error ? err.message : String(err);
log?.warn(`ws upgrade error from ${remoteAddress}: ${errorMessage}`);
socket.destroy();
});
});
}
//#endregion
//#region src/gateway/server-http.ts
const getControlUiModule = createLazyRuntimeModule(() => import("./control-ui-ajhI4ZuP.js"));
const getControlUiPluginAssetsModule = createLazyRuntimeModule(() => import("./control-ui-plugin-assets-BaOOoRTY.js"));
const getCanvasServeModule = createLazyRuntimeModule(() => import("./serve.runtime.js"));
const getBoardHttpModule = createLazyRuntimeModule(() => import("./board-http-CAiCYH6_.js"));
const getEmbeddingsHttpModule = createLazyRuntimeModule(() => import("./embeddings-http-6VBNpqLB.js"));
const getManagedMediaAttachmentsModule = createLazyRuntimeModule(() => import("./managed-image-attachments-D18sTbBJ.js"));
const getMcpAppStandaloneModule = createLazyRuntimeModule(() => import("./mcp-app-standalone-BAHoqKYV.js"));
const getPluginIconHttpModule = createLazyRuntimeModule(() => import("./plugin-icon-http-DnDWI1je.js"));
const getWorkspaceIconHttpModule = createLazyRuntimeModule(() => import("./workspace-icon-http-LYZG428T.js"));
const getChannelAvatarHttpModule = createLazyRuntimeModule(() => import("./channel-avatar-http-BOoAASPR.js"));
const getModelsHttpModule = createLazyRuntimeModule(() => import("./models-http-CdH1Xag-.js"));
const getOpenAiHttpModule = createLazyRuntimeModule(() => import("./openai-http-BuR2TEnw.js"));
const getOpenResponsesHttpModule = createLazyRuntimeModule(() => import("./openresponses-http-CVrv8kHG.js"));
const getSessionHistoryHttpModule = createLazyRuntimeModule(() => import("./sessions-history-http-CE816_yS.js"));
const getSessionKillHttpModule = createLazyRuntimeModule(() => import("./session-kill-http-B_tQBtq0.js"));
const getToolsInvokeHttpModule = createLazyRuntimeModule(() => import("./tools-invoke-http-DvbrG1DU.js"));
const getUserProfilesHttpModule = createLazyRuntimeModule(() => import("./user-profiles-http-D79qHoLw.js"));
const getDevicePairingJoinHttpModule = createLazyRuntimeModule(() => import("./device-pairing-join-http-Dbk3U2Ag.js"));
const getPluginNodeCapabilityAuthModule = createLazyRuntimeModule(() => import("./plugin-node-capability-auth-BP5OVXJc.js"));
const getHttpAuthUtilsModule = createLazyRuntimeModule(() => import("./http-auth-utils-BKXWn2Eo.js"));
const getPluginRouteRuntimeScopesModule = createLazyRuntimeModule(() => import("./plugin-route-runtime-scopes-CDEO-C-s.js"));
function isWebSocketUpgradeRequest(req) {
const headerContains = (value, token) => (typeof value === "string" ? [value] : value ?? []).some((entry) => entry.toLowerCase().split(",").some((part) => part.trim() === token));
return headerContains(req.headers.upgrade, "websocket") && headerContains(req.headers.connection, "upgrade");
}
/** Creates the gateway HTTP/HTTPS server and ordered request-stage router. */
function createGatewayHttpServer(opts) {
const { clients, controlUiBasePath, controlUiRoot, handleHooksRequest, handlePluginRequest, shouldEnforcePluginGatewayAuth, resolvePluginNodeCapabilityRoute, resolvedAuth, rateLimiter, joinRateLimiter, getReadiness, getStartup } = opts;
const getResolvedAuth = opts.getResolvedAuth ?? (() => resolvedAuth);
const loadGatewayConfig = opts.getRuntimeConfig ?? getRuntimeConfig;
const controlUiRouteBasePath = controlUiBasePath && controlUiBasePath !== "/" ? controlUiBasePath.replace(/\/$/, "") : "";
const pluginAssetRoot = controlUiPluginAssetRoot(controlUiRouteBasePath);
const handleServerRequest = (req, res, expectation) => {
markGatewayIngressTransport(req, opts.ingressTransport ?? { kind: "ordinary" });
runHttpConnectionRequest(req, () => runWithDiagnosticTraceContext(createDiagnosticTraceContext(), () => handleRequest(req, res, expectation)), res).catch((error) => {
console.error("[gateway-http] failed to finalize request:", error);
if (!res.destroyed) res.destroy(error instanceof Error ? error : void 0);
});
};
const httpServer = opts.tlsOptions ? createServer$2(opts.tlsOptions, handleServerRequest) : createServer$1(handleServerRequest);
httpServer.on("checkContinue", (req, res) => handleServerRequest(req, res, "continue"));
httpServer.on("checkExpectation", (req, res) => handleServerRequest(req, res, "reject"));
httpServer.on("connect", (req, socket) => {
runHttpConnectionRequest(req, async () => {
socket.destroy();
}, "upgrade");
});
async function handleRequest(req, res, expectation) {
setDefaultSecurityHeaders(res, getRuntimeConfigSnapshot()?.gateway?.http?.securityHeaders);
if (expectation === "reject") {
res.writeHead(417);
res.end();
return;
}
if (expectation === "continue") res.writeContinue();
if (isWebSocketUpgradeRequest(req)) return;
if (req.headers.upgrade !== void 0) {
res.statusCode = 400;
res.setHeader("Connection", "close");
res.setHeader("Content-Type", "text/plain; charset=utf-8");
res.end("Bad Request");
return;
}
try {
const requestPath = URL.parse(req.url ?? "/", "http://localhost")?.pathname;
if (requestPath === void 0) {
sendGatewayAuthFailure(res, {
ok: false,
reason: "unauthorized"
});
return;
}
if (classifyGatewayProbePath(requestPath) === "live") {
await handleGatewayProbeRequest(req, res, requestPath, resolvedAuth, [], false, rateLimiter, getReadiness, getStartup);
return;
}
const configSnapshot = loadGatewayConfig();
const controlUiEnabled = opts.controlUiEnabled ?? configSnapshot.gateway?.controlUi?.enabled ?? true;
const openAiChatCompletionsConfig = configSnapshot.gateway?.http?.endpoints?.chatCompletions;
const openResponsesConfig = configSnapshot.gateway?.http?.endpoints?.responses;
const openAiChatCompletionsEnabled = opts.openAiChatCompletionsEnabled ?? openAiChatCompletionsConfig?.enabled ?? false;
const openResponsesEnabled = opts.openResponsesEnabled ?? openResponsesConfig?.enabled ?? false;
const openAiCompatEnabled = openAiChatCompletionsEnabled || openResponsesEnabled;
const trustedProxies = configSnapshot.gateway?.trustedProxies ?? [];
const allowRealIpFallback = configSnapshot.gateway?.allowRealIpFallback === true;
const ingressAttribution = prepareGatewayIngressAttribution({
req,
trustedProxies,
allowRealIpFallback,
tailscaleWhois: (ip) => readTailscaleWhoisIdentity(ip, void 0, {
cacheTtlMs: 0,
errorTtlMs: 0
})
});
const scopedNodeCapability = normalizePluginNodeCapabilityScopedUrl(req.url ?? "/");
if (scopedNodeCapability.malformedScopedPath) {
sendGatewayAuthFailure(res, {
ok: false,
reason: "unauthorized"
});
return;
}
if (scopedNodeCapability.rewrittenUrl) req.url = scopedNodeCapability.rewrittenUrl;
const scopedRequestPath = scopedNodeCapability.pathname;
const pluginPathContext = resolvePluginRoutePathContext(scopedRequestPath);
const nodeCapability = resolvePluginNodeCapabilityRoute?.(pluginPathContext);
if (ingressAttribution.kind === "unattributable-proxy") {
opts.reportUnattributableProxy?.(ingressAttribution);
if (!nodeCapability && handlePluginRequest && opts.isPluginAuthenticatedRoute?.(pluginPathContext) && await handlePluginRequest(req, res, pluginPathContext, { gatewayRequestClientIp: ingressAttribution.remoteAddress })) return;
sendGatewayAuthFailure(res, {
ok: false,
reason: ingressAttribution.reason
});
return;
}
const requestClientIp = ingressAttribution.clientIp;
const resolvedAuthValue = getResolvedAuth();
const routeAuth = {
auth: resolvedAuthValue,
trustedProxies,
allowRealIpFallback,
rateLimiter
};
const controlUiRouteOptions = {
basePath: controlUiBasePath,
config: configSnapshot,
...routeAuth
};
const loadControlUi = () => {
const url = req.url ? new URL(req.url, "http://localhost") : void 0;
return url && (url.pathname === resolveAssistantMediaRoutePath(controlUiBasePath) || classifyControlUiRequest({
basePath: normalizeControlUiBasePath(controlUiBasePath),
pathname: url.pathname,
search: url.search,
method: req.method,
accept: req.headers.accept
}).kind !== "not-control-ui") ? getControlUiModule() : void 0;
};
const handleControlUiRequest = async () => (await loadControlUi())?.handleControlUiHttpRequest(req, res, {
...controlUiRouteOptions,
terminalEnabled: opts.isTerminalEnabled?.() ?? isTerminalConfigEnabled(configSnapshot),
agentId: resolveAssistantAgentId(configSnapshot),
root: controlUiRoot
}) ?? false;
const handleStandaloneControlUiRequest = async () => {
if (!controlUiEnabled) {
respondNotFound(res);
return true;
}
if (await handleControlUiRequest()) return true;
respondNotFound(res);
return true;
};
const requestStages = [() => handleGatewayProbeRequest(req, res, scopedRequestPath, resolvedAuthValue, trustedProxies, allowRealIpFallback, rateLimiter, getReadiness, getStartup)];
const addRequestStage = (enabled, stage, admitted = false) => {
if (enabled) requestStages.push(admitted ? () => runWithGatewayHttpWorkAdmission(res, stage) : stage);
};
const addAdmittedStage = (enabled, stage) => addRequestStage(enabled, stage, true);
addRequestStage(classifyWorkerGatewayPath(scopedRequestPath) !== "outside", () => {
respondNotFound(res);
return true;
});
addAdmittedStage(classifyWorkerBootstrapArtifactTransferPath(scopedRequestPath) !== "outside", () => handleWorkerBootstrapArtifactTransferHttpRequest({
req,
res,
clientIp: ingressAttribution.rateLimit.subject.key,
rateLimiter: joinRateLimiter,
callback: opts.handleWorkerBootstrapArtifactTransferRequest
}));
addAdmittedStage(classifyNodeWorkerBundleTransferPath(scopedRequestPath) !== "outside", () => handleNodeWorkerBundleTransferHttpRequest({
req,
res,
clientIp: ingressAttribution.rateLimit.subject.key,
rateLimiter: joinRateLimiter,
callback: opts.handleNodeWorkerBundleTransferRequest
}));
addAdmittedStage(classifyNodeWorkspaceTransferPath(scopedRequestPath) !== "outside", () => handleNodeWorkspaceTransferHttpRequest({
req,
res,
clientIp: ingressAttribution.rateLimit.subject.key,
rateLimiter: joinRateLimiter,
callback: opts.handleNodeWorkspaceTransferRequest
}));
const devicePairingJoinShortcode = parseDevicePairingJoinRequestPath(scopedRequestPath);
if (devicePairingJoinShortcode !== null) addAdmittedStage(true, async () => (await getDevicePairingJoinHttpModule()).handleDevicePairingJoinHttpRequest({
req,
res,
shortcode: devicePairingJoinShortcode,
clientIp: ingressAttribution.rateLimit.subject.key,
rateLimiter: joinRateLimiter
}));
addAdmittedStage(req.method === "GET" && scopedRequestPath === "/oauth/mcp/callback" && Boolean(opts.handleMcpOAuthCallbackRequest), () => opts.handleMcpOAuthCallbackRequest?.(req, res) ?? false);
addRequestStage(true, () => handleHooksRequest(req, res));
addAdmittedStage(Boolean(opts.handleWatchNodeRequest) && scopedRequestPath.startsWith("/api/nodes/watch/"), () => opts.handleWatchNodeRequest?.(req, res) ?? false);
addAdmittedStage(openAiCompatEnabled && (scopedRequestPath === "/v1/models" || scopedRequestPath.startsWith("/v1/models/")), async () => (await getModelsHttpModule()).handleOpenAiModelsHttpRequest(req, res, routeAuth));
addAdmittedStage(openAiCompatEnabled && scopedRequestPath === "/v1/embeddings", async () => (await getEmbeddingsHttpModule()).handleOpenAiEmbeddingsHttpRequest(req, res, routeAuth));
addAdmittedStage(scopedRequestPath === "/tools/invoke", async () => (await getToolsInvokeHttpModule()).handleToolsInvokeHttpRequest(req, res, routeAuth));
addAdmittedStage(/^\/sessions\/[^/]+\/kill$/.test(scopedRequestPath), async () => (await getSessionKillHttpModule()).handleSessionKillHttpRequest(req, res, routeAuth));
addAdmittedStage(/^\/sessions\/[^/]+\/history$/.test(scopedRequestPath), async () => (await getSessionHistoryHttpModule()).handleSessionHistoryHttpRequest(req, res, {
...routeAuth,
getResolvedAuth
}));
addAdmittedStage(scopedRequestPath.startsWith("/__openclaw__/board/"), async () => (await getBoardHttpModule()).handleBoardHttpRequest(req, res, { resolveGatewayContext: opts.getGatewayRequestContext?.()?.resolveGatewayContext }));
addAdmittedStage(scopedRequestPath.startsWith(pluginAssetRoot), async () => {
if (!controlUiEnabled) {
respondNotFound(res);
return true;
}
return await (await getControlUiPluginAssetsModule()).handleControlUiPluginAssetRequest(req, res, controlUiRouteOptions);
});
addAdmittedStage(parseControlUiUserAvatarPath(scopedRequestPath, controlUiRouteBasePath).matched, async () => (await getUserProfilesHttpModule()).handleUserProfileAvatarHttpRequest(req, res, scopedRequestPath, {
...routeAuth,
basePath: controlUiRouteBasePath
}));
addAdmittedStage(openResponsesEnabled && scopedRequestPath === "/v1/responses", async () => (await getOpenResponsesHttpModule()).handleOpenResponsesHttpRequest(req, res, {
...routeAuth,
config: openResponsesConfig,
resolveGatewayContext: opts.getGatewayRequestContext?.()?.resolveGatewayContext
}));
addAdmittedStage(openAiChatCompletionsEnabled && scopedRequestPath === "/v1/chat/completions", async () => (await getOpenAiHttpModule()).handleOpenAiHttpRequest(req, res, {
...routeAuth,
config: openAiChatCompletionsConfig,
resolveGatewayContext: opts.getGatewayRequestContext?.()?.resolveGatewayContext
}));
const approvalDocument = isControlUiApprovalDocumentPath({
basePath: controlUiBasePath,
pathname: scopedRequestPath
});
const focusDocument = isControlUiFocusDocumentPath({
basePath: controlUiBasePath,
pathname: scopedRequestPath
});
addRequestStage(approvalDocument, handleStandaloneControlUiRequest);
addRequestStage(Boolean(nodeCapability), async () => {
const { authorizePluginNodeCapabilityRequest } = await getPluginNodeCapabilityAuthModule();
const ok = await authorizePluginNodeCapabilityRequest({
req,
auth: resolvedAuthValue,
trustedProxies,
allowRealIpFallback,
clients,
nodeCapability,
capability: scopedNodeCapability.capability,
malformedScopedPath: scopedNodeCapability.malformedScopedPath,
rateLimiter
});
if (!ok.ok) {
sendGatewayAuthFailure(res, ok);
return true;
}
return false;
});
addRequestStage(Boolean(nodeCapability) && isCoreCanvasHostEnabled(configSnapshot) && isCanvasDocumentHttpPath(scopedRequestPath), async () => (await getCanvasServeModule()).handleCanvasDocumentHttpRequest(req, res));
addRequestStage(controlUiEnabled && isControlUiPluginManagerRequest({
basePath: controlUiBasePath,
pathname: scopedRequestPath,
method: req.method
}), handleControlUiRequest);
const mcpAppRoute = classifyMcpAppStandalonePath(scopedRequestPath);
if (configSnapshot.mcp?.apps?.enabled === true && (mcpAppRoute === "shell" || mcpAppRoute === "view")) requestStages.push(async () => await runWithGatewayHttpWorkAdmission(res, async () => {
return await (await getMcpAppStandaloneModule()).handleMcpAppStandaloneHttpRequest(req, res, {
sandboxPort: configSnapshot.mcp?.apps?.sandboxPort,
sandboxOrigin: configSnapshot.mcp?.apps?.sandboxOrigin
});
}));
if (handlePluginRequest) {
let pluginGatewayAuthSatisfied = false;
let pluginGatewayRequestAuth;
let pluginRequestOperatorScopes;
requestStages.push(async () => {
if (!(shouldEnforcePluginGatewayAuth ?? shouldEnforceDefaultPluginGatewayAuth)(pluginPathContext) || (await getCachedPluginGatewayAuthBypassPaths(configSnapshot)).has(scopedRequestPath)) return false;
const { authorizePluginGatewayHttpRequestOrReply } = await getHttpAuthUtilsModule();
const { resolvePluginRouteRuntimeOperatorScopes } = await getPluginRouteRuntimeScopesModule();
const authResult = await authorizePluginGatewayHttpRequestOrReply({
req,
res,
...routeAuth,
requestPath: scopedRequestPath,
resolveOperatorScopes: resolvePluginRouteRuntimeOperatorScopes
});
if (!authResult) return true;
pluginGatewayAuthSatisfied = true;
pluginGatewayRequestAuth = authResult.requestAuth;
pluginRequestOperatorScopes = authResult.operatorScopes;
return false;
}, () => handlePluginRequest(req, res, pluginPathContext, {
gatewayAuthSatisfied: pluginGatewayAuthSatisfied,
gatewayRequestAuth: pluginGatewayRequestAuth,
gatewayRequestOperatorScopes: pluginRequestOperatorScopes,
gatewayRequestClientIp: requestClientIp
}));
}
addRequestStage(focusDocument, handleStandaloneControlUiRequest);
addRequestStage(scopedRequestPath.startsWith("/api/chat/media/outgoing/") || controlUiRouteBasePath.length > 0 && scopedRequestPath.startsWith(`${controlUiRouteBasePath}/api/chat/media/outgoing/`), async () => (await getManagedMediaAttachmentsModule()).handleManagedOutgoingMediaHttpRequest(req, res, {
...routeAuth,
basePath: controlUiRouteBasePath
}));
for (const [routes, loadHandler] of [
[[
"pluginIcon",
"catalogIcon",
"linkFavicon"
], async () => (await getPluginIconHttpModule()).handlePluginIconHttpRequest],
[["workspaceIcon"], async () => (await getWorkspaceIconHttpModule()).handleWorkspaceIconHttpRequest],
[["channelAvatar"], async () => (await getChannelAvatarHttpModule()).handleChannelAvatarHttpRequest]
]) addRequestStage(controlUiEnabled && routes.some((route) => parseControlUiResourcePath(route, scopedRequestPath, controlUiRouteBasePath).matched), async () => (await loadHandler())(req, res, controlUiRouteOptions));
addRequestStage(controlUiEnabled, async () => (await loadControlUi())?.handleControlUiAssistantMediaRequest(req, res, {
...controlUiRouteOptions,
agentId: resolveAssistantAgentId(configSnapshot)
}) ?? false);
addRequestStage(controlUiEnabled, async () => (await loadControlUi())?.handleControlUiAvatarRequest(req, res, controlUiRouteOptions) ?? false);
addRequestStage(controlUiEnabled, handleControlUiRequest);
for (const stage of requestStages) if (await stage() || res.destroyed || res.writableEnded) return;
if (opts.isStartupPluginRuntimeReady?.() === false) {
res.setHeader("Cache-Control", "no-store");
res.setHeader("Retry-After", "1");
respondPlainText(res, 503, "Plugin runtime is starting");
return;
}
respondNotFound(res);
} catch (err) {
console.error("[gateway-http] unhandled error in request handler:", err);
finishFailedGatewayHttpResponse(res);
}
}
return httpServer;
}
//#endregion
//#region src/gateway/server/preauth-connection-budget.ts
const DEFAULT_MAX_PREAUTH_CONNECTIONS_PER_IP = 32;
const UNKNOWN_CLIENT_IP_BUDGET_KEY = "__openclaw_unknown_client_ip__";
function getMaxPreauthConnectionsPerIpFromEnv(env = process.env) {
const configured = env.OPENCLAW_MAX_PREAUTH_CONNECTIONS_PER_IP || (isVitestRuntimeEnv(env) ? env.OPENCLAW_TEST_MAX_PREAUTH_CONNECTIONS_PER_IP : void 0);
if (!configured) return DEFAULT_MAX_PREAUTH_CONNECTIONS_PER_IP;
const parsed = parseStrictPositiveInteger(configured);
if (parsed === void 0) return DEFAULT_MAX_PREAUTH_CONNECTIONS_PER_IP;
return parsed;
}
function createPreauthConnectionBudget(limit = getMaxPreauthConnectionsPerIpFromEnv()) {
const maxConnectionsPerIp = resolveIntegerOption(limit, getMaxPreauthConnectionsPerIpFromEnv(), { min: 1 });
const counts = /* @__PURE__ */ new Map();
const normalizeBudgetKey = (clientIp) => {
return clientIp?.trim() || UNKNOWN_CLIENT_IP_BUDGET_KEY;
};
return {
acquire(clientIp) {
const ip = normalizeBudgetKey(clientIp);
const next = (counts.get(ip) ?? 0) + 1;
if (next > maxConnectionsPerIp) return false;
counts.set(ip, next);
return true;
},
release(clientIp) {
const ip = normalizeBudgetKey(clientIp);
const current = counts.get(ip);
if (current === void 0) return;
if (current <= 1) {
counts.delete(ip);
return;
}
counts.set(ip, current - 1);
}
};
}
//#endregion
//#region src/gateway/server-runtime-state.ts
const require = createRequire(import.meta.url);
const { WebSocketServer: NpmWebSocketServer } = require(path.join(path.dirname(require.resolve("ws/package.json")), "index.js"));
const loadGatewayPluginsHttpModule = async () => await import("./plugins-http-C4fx92IG.js");
function hasMatchingGatewayPluginRoute(registry, pathContext, requiresUpgrade) {
if (!pathContext) return (registry.httpRoutes ?? []).length > 0;
const matchingRoutes = findMatchingPluginHttpRoutes(registry, pathContext);
return requiresUpgrade ? matchingRoutes.some((route) => typeof route.handleUpgrade === "function") : matchingRoutes.length > 0;
}
/** Creates the HTTP/WebSocket transport for one gateway start. */
async function createGatewayHttpTransport(params) {
const loadRuntimeConfig = params.getRuntimeConfig ?? (() => params.cfg);
const resolvePluginRouteRegistry = () => params.getPluginRouteRegistry?.() ?? params.pluginRegistry;
let loadedHooksRequestHandler = null;
let loadedHookDispatcher;
const getHookDispatcher = async () => {
const { createGatewayHookDispatcher } = await import("./hooks-A6fI_S6B.js");
return loadedHookDispatcher ??= createGatewayHookDispatcher({
deps: params.deps,
logHooks: params.logHooks,
...params.getGatewayRequestContext ? { resolveGatewayContext: params.getGatewayRequestContext } : {}
});
};
const handleHooksRequest = async (req, res) => {
const hooksConfig = params.hooksConfig();
if (!hooksConfig) return false;
const url = new URL(req.url ?? "/", "http://localhost");
const basePath = hooksConfig.basePath;
if (url.pathname !== basePath && !url.pathname.startsWith(`${basePath}/`)) return false;
return await runWithGatewayHttpWorkAdmission(res, async () => {
if (!loadedHooksRequestHandler) {
const { createGatewayHooksRequestHandler } = await import("./hooks-A6fI_S6B.js");
loadedHooksRequestHandler = createGatewayHooksRequestHandler({
deps: params.deps,
dispatcher: await getHookDispatcher(),
getHooksConfig: params.hooksConfig,
getClientIpConfig: params.getHookClientIpConfig,
bindHost: params.bindHost,
port: params.port,
logHooks: params.logHooks,
...params.getGatewayRequestContext ? { resolveGatewayContext: params.getGatewayRequestContext } : {}
});
}
return await loadedHooksRequestHandler(req, res);
});
};
const handleMcpOAuthCallbackRequest = async (req, res) => {
const { handleMcpOAuthCallback } = await import("./mcp-oauth-callback-DGln3eIO.js");
return await handleMcpOAuthCallback(req, res, {
config: loadRuntimeConfig(),
log: params.log
});
};
let loadedPluginRequestHandler = null;
let loadedPluginUpgradeHandler = null;
const handlePluginRequest = async (req, res, pathContext, dispatchContext) => {
if (loadedPluginRequestHandler) return await loadedPluginRequestHandler(req, res, pathContext, dispatchContext);
if (!hasMatchingGatewayPluginRoute(resolvePluginRouteRegistry(), pathContext, false)) return false;
const { createGatewayPluginRequestHandler } = await loadGatewayPluginsHttpModule();
loadedPluginRequestHandler = createGatewayPluginRequestHandler({
registry: params.pluginRegistry,
getRouteRegistry: resolvePluginRouteRegistry,
log: params.logPlugins,
getGatewayRequestContext: params.getGatewayRequestContext
});
return await loadedPluginRequestHandler(req, res, pathContext, dispatchContext);
};
const handlePluginUpgrade = async (req, socket, head, pathContext, dispatchContext) => {
if (loadedPluginUpgradeHandler) return await loadedPluginUpgradeHandler(req, socket, head, pathContext, dispatchContext);
if (!hasMatchingGatewayPluginRoute(resolvePluginRouteRegistry(), pathContext, true)) return false;
const { createGatewayPluginUpgradeHandler } = await loadGatewayPluginsHttpModule();
loadedPluginUpgradeHandler = createGatewayPluginUpgradeHandler({
registry: params.pluginRegistry,
getRouteRegistry: resolvePluginRouteRegistry,
log: params.logPlugins,
getGatewayRequestContext: params.getGatewayRequestContext
});
return await loadedPluginUpgradeHandler(req, socket, head, pathContext, dispatchContext);
};
const shouldEnforcePluginGatewayAuth = (pathContext) => {
return shouldEnforceGatewayAuthForPluginPath(resolvePluginRouteRegistry(), pathContext);
};
const isPluginAuthenticatedRoute = (pathContext) => {
return isPluginAuthenticatedRoutePath(resolvePluginRouteRegistry(), pathContext);
};
const resolvePluginNodeCapabilityRoute = (pathContext) => {
const coreCanvasCapability = isCoreCanvasHostEnabled(loadRuntimeConfig()) ? resolveCanvasNodeCapability(pathContext.candidates) : void 0;
if (coreCanvasCapability) return coreCanvasCapability;
return findMatchingPluginNodeCapabilityRoute(resolvePluginRouteRegistry(), pathContext)?.nodeCapability;
};
const managedTailscaleMode = params.tailscaleMode && params.tailscaleMode !== "off" ? params.tailscaleMode : void 0;
const bindHosts = await resolveGatewayListenHosts(params.bindHost);
if (!isLoopbackHost(params.bindHost)) params.log.warn("⚠️ Gateway is binding to a non-loopback address. Ensure authentication is configured before exposing to public networks.");
if (params.cfg.gateway?.controlUi?.dangerouslyAllowHostHeaderOriginFallback === true) params.log.warn("⚠️ gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback=true is enabled. Host-header origin fallback weakens origin checks and should only be used as break-glass.");
const wss = new NpmWebSocketServer({
noServer: true,
maxPayload: MAX_PREAUTH_PAYLOAD_BYTES,
allowSynchronousEvents: false,
perMessageDeflate: {
serverNoContextTakeover: true,
clientNoContextTakeover: true,
threshold: WS_COMPRESSION_THRESHOLD_BYTES
}
});
const preauthConnectionBudget = createPreauthConnectionBudget();
const httpServers = [];
const gatewayHttpServers = [];
const httpBindHosts = [];
const portalService = createGatewayPortalService({
httpBindHosts,
httpServers,
...params.gatewayTls?.enabled ? { tlsOptions: params.gatewayTls.tlsOptions } : {}
});
const reportUnattributableProxy = createGatewayUnattributableProxyReporter(params.log);
const createGatewayListener = (ingressTransport, tlsOptions) => {
const httpServer = createGatewayHttpServer({
clients: params.clients,
controlUiEnabled: params.controlUiEnabled,
controlUiBasePath: params.controlUiBasePath,
controlUiRoot: params.controlUiRoot,
openAiChatCompletionsEnabled: params.openAiChatCompletionsEnabled,
openResponsesEnabled: params.openResponsesEnabled,
handleWatchNodeRequest: params.handleWatchNodeRequest,
handleHooksRequest,
handleMcpOAuthCallbackRequest,
handlePluginRequest,
shouldEnforcePluginGatewayAuth,
isPluginAuthenticatedRoute,
resolvePluginNodeCapabilityRoute,
resolvedAuth: params.resolvedAuth,
getResolvedAuth: params.getResolvedAuth,
rateLimiter: params.rateLimiter,
joinRateLimiter: params.joinRateLimiter,
handleNodeWorkerBundleTransferRequest: params.handleNodeWorkerBundleTransferRequest,
handleWorkerBootstrapArtifactTransferRequest: params.handleWorkerBootstrapArtifactTransferRequest,
handleNodeWorkspaceTransferRequest: params.handleNodeWorkspaceTransferRequest,
getReadiness: params.getReadiness,
getStartup: params.getStartup,
getRuntimeConfig: loadRuntimeConfig,
getGatewayRequestContext: params.getGatewayRequestContext,
isStartupPluginRuntimeReady: params.isStartupPluginRuntimeReady,
isTerminalEnabled: params.isTerminalEnabled,
tlsOptions,
ingressTransport,
reportUnattributableProxy
});
attachGatewayUpgradeHandler({
httpServer,
wss,
handlePluginUpgrade,
shouldEnforcePluginGatewayAuth,
isPluginAuthenticatedRoute,
resolvePluginNodeCapabilityRoute,
clients: params.clients,
preauthConnectionBudget,
resolvedAuth: params.resolvedAuth,
getResolvedAuth: params.getResolvedAuth,
rateLimiter: params.rateLimiter,
publicRateLimiter: params.joinRateLimiter,
workerIngressEnabled: params.workerIngressEnabled,
log: params.log,
desktopSessionRegistry: params.desktopSessionRegistry,
nodeDesktopStreamBroker: params.nodeDesktopStreamBroker,
getGatewayRequestContext: params.getGatewayRequestContext,
isStartupPending: params.isStartupPending,
ingressTransport,
reportUnattributableProxy
});
return httpServer;
};
for (const _ of bindHosts) {
const httpServer = createGatewayListener({ kind: "ordinary" }, params.gatewayTls?.enabled ? params.gatewayTls.tlsOptions : void 0);
gatewayHttpServers.push(httpServer);
httpServers.push(httpServer);
}
const tailscaleHttpServer = managedTailscaleMode ? createGatewayListener({
kind: "managed-tailscale",
mode: managedTailscaleMode
}, void 0) : void 0;
if (tailscaleHttpServer) httpServers.push(tailscaleHttpServer);
let tailscaleIngressEndpoint;
const httpServer = gatewayHttpServers[0];
if (!httpServer) throw new Error("Gateway HTTP server failed to start");
let mcpAppSandboxPort;
let sandboxHostStartPromise = null;
let startListeningPromise = null;
let startListeningComplete = false;
const startSandboxHost = async () => {
if (sandboxHostStartPromise) return await sandboxHostStartPromise;
sandboxHostStartPromise = (async () => {
if (httpBindHosts.length === 0) throw new Error("Gateway listener must start before the sandbox host");
const sandboxPort = resolveSandboxHostPort(params.port, params.cfg.mcp?.apps?.sandboxPort);
const sandboxServers = bindHosts.map(() => createSandboxHostHttpServer(params.gatewayTls?.enabled ? params.gatewayTls.tlsOptions : void 0, resolvePluginRouteRegistry));
httpServers.push(...sandboxServers);
try {
for (const host of httpBindHosts) {
const server = sandboxServers[bindHosts.indexOf(host)];
if (!server) throw new Error(`Missing sandbox host HTTP server for bind host ${host}`);
await listenGatewayHttpServer({
httpServer: server,
bindHost: host,
port: sandboxPort,
retryEaddrinuse: false,
serviceName: "MCP App sandbox",
endpointScheme: params.gatewayTls?.enabled ? "https" : "http"
});
}
} catch (error) {
await Promise.all(sandboxServers.map((server) => new Promise((resolve) => {
if (!server.listening) {
resolve();
return;
}
server.close(() => resolve());
})));
for (const server of sandboxServers) {
const index = httpServers.indexOf(server);
if (index >= 0) httpServers.splice(index, 1);
}
throw error;
}
mcpAppSandboxPort = sandboxPort;
return sandboxPort;
})();
const startAttempt = sandboxHostStartPromise;
startAttempt.catch(() => {
if (sandboxHostStartPromise === startAttempt) sandboxHostStartPromise = null;
});
return await startAttempt;
};
const ensureSandboxHostPort = async () => {
if (!startListeningComplete) {
if (!startListeningPromise) throw new Error("Gateway listener must start before the sandbox host");
await startListeningPromise;
}
return await startSandboxHost();
};
const startListening = async () => {
if (startListeningPromise) {
await startListeningPromise;
return;
}
startListeningPromise = (async () => {
if (tailscaleHttpServer) {
await listenGatewayHttpServer({
httpServer: tailscaleHttpServer,
bindHost: "127.0.0.1",
port: 0,
retryEaddrinuse: false,
serviceName: "Tailscale gateway ingress"
});
const address = tailscaleHttpServer.address();
if (!address || typeof address === "string") throw new Error("Tailscale gateway ingress failed to resolve its loopback port");
tailscaleIngressEndpoint = {
host: "127.0.0.1",
port: address.port
};
await params.prepareManagedTailscaleIngress?.(tailscaleIngressEndpoint);
}
const requiredAlias = params.bindHost !== "127.0.0.1" && bindHosts.includes("127.0.0.1") ? "127.0.0.1" : void 0;
const listenOrder = requiredAlias ? [requiredAlias, ...bindHosts.filter((host) => host !== requiredAlias)] : bindHosts;
const boundHosts = /* @__PURE__ */ new Set();
for (const host of listenOrder) {
const index = bindHosts.indexOf(host);
const server = gatewayHttpServers[index];
if (!server) throw new Error(`Missing gateway HTTP server for bind host ${host}`);
const requiredLoopbackAlias = host === requiredAlias;
try {
await listenGatewayHttpServer({
httpServer: server,
bindHost: host,
port: params.port,
retryEaddrinuse: !requiredLoopbackAlias
});
boundHosts.add(host);
} catch (err) {
if (host === bindHosts[0] || requiredLoopbackAlias) throw err;
params.log.warn(`gateway: failed to bind loopback alias ${host}:${params.port} (${String(err)})`);
}
}
httpBindHosts.push(...bindHosts.filter((host) => boundHosts.has(host)));
if (httpBindHosts.length === 0) throw new Error("Gateway HTTP server failed to start");
if (params.cfg.mcp?.apps?.enabled === true) await startSandboxHost();
startListeningComplete = true;
})();
await startListeningPromise;
};
return {
httpServer,
httpServers,
httpBindHosts,
startListening,
wss,
preauthConnectionBudget,
portalService,
getTailscaleIngressEndpoint: () => tailscaleIngressEndpoint,
getMcpAppSandboxPort: () => mcpAppSandboxPort,
ensureSandboxHostPort,
dispatchHookAgentTurn: async (pluginId, hookParams) => await (await getHookDispatcher()).dispatchHookAgentTurn(hookParams, pluginId)
};
}
//#endregion
//#region src/gateway/server-startup-finish.ts
const [POST_READY_MAINTENANCE_DELAY_MS, RETAINED_PLUGIN_CLEANUP_DELAY_MS] = [250, 3e4];
async function finishGatewayStartup(params) {
const { kernelRuntime: runtime, port, bootId, opts, log, logHealth, logWsControl, logHooks, logChannels, logCron, logReload, loadGatewayStartupPostAttachModule } = params;
const { minimalTestGateway, deps, runtimeState, kernel, startupTrace, broadcast, broadcastToConnIds, clients, sharedGatewaySessionGenerationState, workerEnvironmentService, workerPlacementRuntime, terminalLaunchPolicy, terminalSessions, nodeRegistry, nodeDesktopService, startChannel, stopChannel, getAttachedGatewayMethodRegistry, lifecycle, startupState, pluginRuntime, resolvePluginGatewayContext, gatewayTls, bindHost, getResolvedAuth, authRateLimiter, browserAuthRateLimiter, nodeReapprovalCoordinator, preauthHandshakeTimeoutMs, isGatewayStartupPending, attachedGatewayExtraHandlers, startListening, loadStartupPluginsModule, gatewayPluginConfigAtStart, startupActivationSourceConfig, defaultWorkspaceDir, coreGatewayMethodNames, pluginHostServices, baseMethods, startupPluginIds, pluginManifestRecords, pluginMetadataSnapshot, pluginLookUpTable, ambientEnvTriggers, replaceAttachedPluginRuntime, refreshAttachedGatewayDiscovery, wss, httpBindHosts, startChannels, broadcastPluginEvent, controlUiBasePath, controlUiRootLifecycle, sidecarStartup, workerLiveEvents, startEarlyRuntime, cfgAtStart, preauthConnectionBudget, releaseStartupAccountStarts, cronReconciliation, postReadyState, cronStartState, prepareReloadCandidate, startupLastGoodSnapshot, startupInternalWriteHash, configSnapshot, channelManager, activateRuntimeSecrets, applyFixedGatewayOverlays, resolveSharedGatewaySessionGenerationForConfig, stopRegisteredPostReadySidecars, registerPostReadySidecars, registerGatewayLifetimeSidecars, registerConnectionDependentSidecars, unregisterConnectionDependentSidecar, chatMetadataLifecycle, gatewayRequestContext, gatewayInstanceRuntime, getPluginMetadataSnapshot, getPluginNodeCapabilities } = runtime;
const startupPluginRuntimeClaim = kernel.pluginRuntimeGeneration.currentClaim();
const { attachGatewayWsHandlers } = await startupTrace.measure("gateway.ws-imports", () => import("./server-ws-runtime-CF5d6UbO.js"));
await startupTrace.measure("gateway.ws-attach", () => attachGatewayWsHandlers({
wss,
clients,
connectionWork: runtime.connectionWork,
bootId,
preauthConnectionBudget,
port,
gatewayHost: bindHost ?? void 0,
pluginSurfaceScheme: gatewayTls.enabled ? "https" : "http",
getPluginNodeCapabilities,
getResolvedAuth,
getRequiredSharedGatewaySessionGeneration: () => getRequiredSharedGatewaySessionGeneration(sharedGatewaySessionGenerationState),
rateLimiter: authRateLimiter,
browserRateLimiter: browserAuthRateLimiter,
nodeReapprovalCoordinator,
preauthHandshakeTimeoutMs,
isStartupPending: isGatewayStartupPending,
isPendingWorkerNodeSetup: workerEnvironmentService?.hasPendingNodeEnrollmentSetup,
gatewayMethods: runtimeState.gatewayMethods,
events: GATEWAY_EVENTS,
logGateway: log,
logHealth,
logWsControl,
extraHandlers: attachedGatewayExtraHandlers,
getMethodRegistry: () => getAttachedGatewayMethodRegistry(),
...workerEnvironmentService ? { workerConnectionService: workerEnvironmentService } : {},
broadcast,
context: gatewayRequestContext
}));
await startupTrace.measure("http.listen", () => startListening());
kernel.setDispatchReady(true);
startupTrace.mark("http.bound");
const earlyRuntime = await startEarlyRuntime();
const sessionDeliveryRecoveryMaxEnqueuedAt = Date.now();
let postAttachRuntimeReturned = false;
let scheduledServicesActivated = false;
const loadScheduledServicesModule = createLazyPromise(() => import("./server-runtime-services-BOej8U-G.js"), { cacheRejections: true });
const activateScheduledServicesWhenReady = () => {
if (lifecycle.closePreludeStarted || !postAttachRuntimeReturned || !startupState.sidecarsReady || scheduledServicesActivated) return;
scheduledServicesActivated = true;
loadScheduledServicesModule().then((gatewayRuntimeServices) => {
if (lifecycle.closePreludeStarted) return;
const activated = gatewayRuntimeServices.activateGatewayScheduledServices({
minimalTestGateway,
cfgAtStart,
deps,
sessionDeliveryRecoveryMaxEnqueuedAt,
cronState: runtimeState.cronState,
cronReconciliation,
startCron: false,
logCron,
log,
resolveGatewayContext: resolvePluginGatewayContext
});
kernel.setScheduledServiceHandles(activated);
});
};
const { createGatewayServerActiveWorkInspectors } = await startupTrace.measure("gateway.active-work-import", () => import("./server-active-work-VHXbc9rT.js"));
const activeWorkInspectors = createGatewayServerActiveWorkInspectors(gatewayRequestContext);
const trackStartupWork = (run) => {
const operation = Promise.resolve().then(() => run(runtime.connectionWork.signal));
return runtime.connectionWork.track(() => operation);
};
const postAttachHandles = await trackStartupWork(() => startupTrace.measure("runtime.post-attach", () => loadGatewayStartupPostAttachModule().then(({ startGatewayPostAttachRuntime }) => startGatewayPostAttachRuntime({
minimalTestGateway,
cfgAtStart,
getConfig: getRuntimeConfig,
bindHost,
bindHosts: httpBindHosts,
port,
tlsEnabled: gatewayTls.enabled,
log,
isNixMode,
startupStartedAt: opts.startupStartedAt,
broadcastToConnIds,
getClientConnIds: gatewayRequestContext.getClientConnIds,
broadcastPluginEvent,
controlUiBasePath,
controlUiRootLifecycle,
gatewayPluginConfigAtStart,
activationSourceConfig: startupActivationSourceConfig,
pluginManifestRecords,
...pluginMetadataSnapshot ? { pluginMetadataSnapshot } : {},
pluginRuntimeClaim: startupPluginRuntimeClaim,
getCurrentPluginRegistry: () => pluginRuntime.registry,
getCurrentPluginMetadataSnapshot: getPluginMetadataSnapshot,
ambientEnvTriggers,
pluginRegistry: pluginRuntime.registry,
defaultWorkspaceDir,
deps,
startChannels,
recoveryRuntime: gatewayInstanceRuntime.recovery,
resolveGatewayContext: gatewayRequestContext.resolveGatewayContext,
logHooks,
logChannels,
unlockStartupMethods: kernel.unlockStartupMethods,
refreshChatMetadata: chatMetadataLifecycle.refresh,
loadStartupPlugins: async () => {
const { loadGatewayStartupPluginRuntime } = await loadStartupPluginsModule();
return loadGatewayStartupPluginRuntime({
cfg: gatewayPluginConfigAtStart,
activationSourceConfig: startupActivationSourceConfig,
workspaceDir: runtime.pluginWorkspaceDir,
log,
baseMethods,
coreGatewayMethodNames,
hostServices: pluginHostServices,
startupPluginIds,
pluginLookUpTable,
startupTrace,
ambientEnvTriggers,
resolveGatewayContext: resolvePluginGatewayContext,
pluginRuntimeClaim: startupPluginRuntimeClaim,
getCurrentPluginRegistry: () => pluginRuntime.registry
});
},
onStartupPluginsLoading: () => {
startupState.pendingReason = "startup-sidecars";
},
onStartupPluginsLoaded: async (loaded) => {
if (!startupPluginRuntimeClaim.publish(() => replaceAttachedPluginRuntime(loaded))) {
loaded.retireGatewayRuntimeBindings?.();
return;
}
startupState.pendingReason = "startup-sidecars";
await refreshAttachedGatewayDiscovery(loaded.pluginRegistry, startupPluginRuntimeClaim);
},
getCronService: () => runtimeState.cronState.cron,
onChannelsStarted: () => {
releaseStartupAccountStarts();
},
onPluginServices: (pluginServices) => {
kernel.pluginRuntimeGeneration.publishServices(startupPluginRuntimeClaim, pluginServices);
},
onPostReadySidecars: registerPostReadySidecars,
onGatewayLifetimeSidecars: registerGatewayLifetimeSidecars,
trackStartupWork,
unregisterConnectionDependentSidecar,
...workerPlacementRuntime ? { startWorkerEnvironmentRuntime: async () => {
if (lifecycle.closePreludeStarted) return null;
return await workerPlacementRuntime.startRuntime({
isClosePreludeStarted: () => lifecycle.closePreludeStarted,
registerSidecar: (sidecar) => {
registerConnectionDependentSidecars([sidecar]);
},
unregisterSidecar: unregisterConnectionDependentSidecar
});
} } : {},
onSidecarsReady: () => {
kernel.markSidecarsReady();
activateScheduledServicesWhenReady();
},
isClosing: () => lifecycle.closePreludeStarted,
startupTrace,
sidecarStartup,
waitForPostReadyWork: params.waitForPostReadyWork,
activeWorkInspectors,
providerAuthPrewarm: { getConfig: getRuntimeConfig }
}))));
kernel.setPostAttachHandles(postAttachHandles, startupPluginRuntimeClaim);
startupTrace.detail("memory.ready", collectGatewayProcessMemoryUsageMb());
startupTrace.mark("ready");
if (sidecarStartup === "defer") log.info("gateway ready");
finishGatewayRestartTrace("restart.ready", collectGatewayProcessMemoryUsageMb());
if (!minimalTestGateway) {
const { startOpenClawDatabaseIntegrityVerifier } = await import("./openclaw-database-verify-DmU_WGMj.js");
kernel.addGatewayLifetimeSidecar(startOpenClawDatabaseIntegrityVerifier({ env: process.env }));
}
postAttachRuntimeReturned = true;
activateScheduledServicesWhenReady();
const { startManagedGatewayConfigReloader } = await import("./server-reload-managed-iuGIwoJm.js");
const assertRuntimeSecurityConfig = (cfg, env) => {
assertGatewayRuntimeSecurityConfig({
cfg,
port,
bindHost,
controlUiEnabled: opts.controlUiEnabled ?? cfg.gateway?.controlUi?.enabled ?? true,
tailscaleMode: runtime.tailscaleMode,
resolvedAuth: resolveGatewayAuth({
authConfig: cfg.gateway?.auth,
tailscaleMode: runtime.tailscaleMode,
env
})
});
};
const configReloaderParams = {
configRevisionProjector: gatewayRequestContext.configRevisionProjector,
resolveGatewayContext: resolvePluginGatewayContext,
minimalTestGateway,
initialConfig: cfgAtStart,
initialCompareConfig: startupLastGoodSnapshot.sourceConfig,
initialSnapshotRawHash: startupLastGoodSnapshot.exists ? startupLastGoodSnapshot.hash ?? null : null,
initialAuthoredConfig: startupLastGoodSnapshot.parsed,
initialIncludedPaths: startupLastGoodSnapshot.includedPaths ?? [],
initialSnapshotValid: startupLastGoodSnapshot.valid,
initialSnapshotIssues: startupLastGoodSnapshot.issues,
initialInternalWriteHash: startupInternalWriteHash,
watchPath: configSnapshot.path,
readSnapshot: readConfigFileSnapshotForRuntimeTransaction,
promoteSnapshot: promoteConfigSnapshotToLastKnownGood,
subscribeToWrites: (listener) => registerConfigWriteListener(listener, {
ownsRuntimeActivationFor: configSnapshot.path,
preCommitRuntimePreflight: async (sourceConfig, runtimeRefresh) => {
const candidate = prepareReloadCandidate({
runtimeConfig: sourceConfig,
sourceConfig
});
const prepared = await activateRuntimeSecrets(candidate.runtimeConfig, {
reason: "reload",
activate: false,
env: candidate.runtimeEnv.env,
includeAuthStoreRefs: runtimeRefresh?.includeAuthStoreRefs
});
const previousConfig = getRuntimeConfig();
if (!buildGatewayReloadPlan(diffGatewayReloadPaths(getRuntimeConfigSourceSnapshot() ?? startupLastGoodSnapshot.sourceConfig, sourceConfig, listConfigReloadRefinementPrefixes()), {
previousConfig,
candidateConfig: prepared.config
}).restartGateway) assertRuntimeSecurityConfig(prepared.config, candidate.runtimeEnv.env);
return candidate;
}
}),
deps,
broadcast,
getState: kernel.getReloadState,
setState: (nextState) => {
kernel.setReloadHookState(nextState);
kernel.swapHeartbeatRunner(nextState.heartbeatRunner);
if (kernel.swapCronState(nextState.cronState) !== nextState.cronState) cronStartState.handled = true;
},
getPluginMetadataSnapshot,
getPluginRegistry: () => runtime.pluginRuntime.registry,
startChannel,
stopChannel,
getChannelAutostartSuppression: channelManager.getAutostartSuppression,
stopPostReadySidecars: stopRegisteredPostReadySidecars,
reloadPlugins: kernel.reloadPlugins,
reloadPluginServices: async (config, serviceIds) => {
const services = runtimeState.pluginServices;
if (!services) throw new Error("Plugin services are not attached");
await services.reload(config, serviceIds);
},
logHooks,
logChannels,
logCron,
logReload,
cronReconciliation,
onCronRestart: () => {
cronStartState.handled = true;
},
prepareTerminalConfig: (plan, nextConfig) => {
terminalLaunchPolicy.prepareConfig(nextConfig, { restartPending: plan.restartGateway });
},
reconcileRuntimePolicy: async (nextConfig, phase) => {
terminalSessions.closeDisallowedAgents((agentId) => terminalLaunchPolicy.resolve(agentId).ok);
if (phase !== "committed") return;
terminalSessions.updateDetachGraceMs((nextConfig.gateway?.terminal?.detachedSessionTimeoutSeconds ?? 300) * 1e3);
disconnectDisallowedGatewayBrowserOriginClients(clients, nextConfig);
for (const nodeSession of nodeRegistry.refreshRuntimePolicy(nextConfig)) refreshConnectedNodeSurfaceCaches({
context: gatewayRequestContext,
nodeSession
});
await Promise.all([nodeDesktopService.reconcileRuntimePolicy(), runtimeState.discovery?.update({ mdnsMode: nextConfig.discovery?.mdns?.mode })]);
},
commitRuntimePolicy: (nextConfig) => {
controlUiRootLifecycle.setEnabled(opts.controlUiEnabled ?? nextConfig.gateway?.controlUi?.enabled ?? true);
runtime.configureDiagnostics(nextConfig);
const rateLimit = nextConfig.gateway?.auth?.rateLimit;
authRateLimiter.updateConfig(rateLimit);
browserAuthRateLimiter.updateConfig({
...rateLimit,
exemptLoopback: false
});
nodeReapprovalCoordinator.updateConfig(rateLimit);
terminalLaunchPolicy.commitConfig();
workerLiveEvents?.rebindAll(nextConfig);
},
acceptTerminalConfig: terminalLaunchPolicy.acceptConfig,
channelManager,
activateRuntimeSecrets,
assertRuntimeSecurityConfig,
prepareConfigCandidate: prepareReloadCandidate,
applyRuntimeConfigOverrides: applyFixedGatewayOverlays,
resolveSharedGatewaySessionGenerationForConfig,
sharedGatewaySessionGenerationState,
clients,
...opts.hotReloadRecovery ? { requestRecoveryRestart: opts.hotReloadRecovery } : {},
restartRecoveryAvailable: opts.hotReloadRecovery !== void 0
};
kernel.setConfigReloaderHandle(startManagedGatewayConfigReloader(configReloaderParams));
await promoteConfigSnapshotToLastKnownGood(startupLastGoodSnapshot).catch((err) => {
log.warn(`gateway: failed to promote config last-known-good backup: ${String(err)}`);
});
if (!minimalTestGateway) {
const gatewayRuntimeServices = await loadScheduledServicesModule();
postReadyState.maintenanceTimer = gatewayRuntimeServices.scheduleGatewayPostReadyMaintenance({
delayMs: POST_READY_MAINTENANCE_DELAY_MS,
isClosing: () => lifecycle.closePreludeStarted,
onStarted: () => {
postReadyState.maintenanceTimer = null;
},
startMaintenance: async () => {
if (lifecycle.closePreludeStarted) return null;
return earlyRuntime.startMaintenance(activeWorkInspectors);
},
applyMaintenance: async (maintenance) => {
if (lifecycle.closePreludeStarted) {
await gatewayRuntimeServices.clearGatewayMaintenanceHandles(maintenance);
return;
}
kernel.setMaintenanceHandles(maintenance);
maintenance.startMediaCleanup();
},
shouldStartCron: () => !lifecycle.closePreludeStarted && !cronStartState.handled,
markCronStartHandled: () => {
cronStartState.handled = true;
},
cronState: runtimeState.cronState,
cronReconciliation,
cronConfig: cfgAtStart,
logCron,
log,
recordPostReadyMemory: () => {
startupTrace.detail("memory.post-ready", collectGatewayProcessMemoryUsageMb());
}
});
const startupInstallPaths = [...Object.values(pluginMetadataSnapshot?.index.installRecords ?? {}).flatMap((record) => record.installPath ? [record.installPath] : []), ...pluginMetadataSnapshot?.plugins.flatMap((record) => record.setupSource ? [
record.rootDir,
record.source,
record.setupSource
] : [record.rootDir, record.source]) ?? []];
registerGatewayLifetimeSidecars([gatewayRuntimeServices.scheduleGatewayIdleTask({
delayMs: RETAINED_PLUGIN_CLEANUP_DELAY_MS,
retryDelayMs: RETAINED_PLUGIN_CLEANUP_DELAY_MS,
isClosing: () => lifecycle.closePreludeStarted,
isBusy: () => getActiveGatewayRootWorkCount({ excludeCurrent: true }) > 0,
run: async () => {
const { cleanupRetainedPluginInstallGenerations } = await import("./server-retained-plugin-cleanup-DSzsOO27.js");
await cleanupRetainedPluginInstallGenerations({
log,
startupInstallPaths
});
},
log,
errorMessage: "retained npm generation cleanup failed"
})]);
} else startupTrace.detail("memory.post-ready", collectGatewayProcessMemoryUsageMb());
return { startupSettled: postAttachHandles.startupSettled };
}
//#endregion
//#region src/gateway/server-start.ts
const loadGatewayStartupPostAttachModule = createLazyRuntimeModule(() => import("./server-startup-post-attach-C4GJFili.js"));
const { log, logTailscale, logChannels, logHealth, logCron, logReload, logHooks, logWsControl } = gatewayKernelLogs;
const POST_READY_WORK_START_DELAY_MS = 500;
async function startGatewayServerCore(port = 18789, opts = {}) {
let releasePostReadyWork = () => {};
const postReadyWorkBarrier = new Promise((resolve) => {
releasePostReadyWork = resolve;
});
const gatewayKernel = await createGatewayKernel(port, opts, { deferEarlyRuntime: true });
if (!gatewayKernel.minimalTestGateway) beginMacOSSystemCaWarmupOnce({ log });
let startupSettled;
const { beginClosePrelude, closeOnStartupFailure, prepareClose, sealAndJoinRegisteredSidecarStops, runClosePrelude, stopRegisteredGatewayLifetimeSidecars, stopRegisteredPostReadySidecars, stopConnectionDependentSidecars, terminalSessions, shutdownRuntime } = gatewayKernel;
try {
const transport = await createGatewayHttpTransport({
...gatewayKernel.createHttpTransportOptions(),
...!gatewayKernel.minimalTestGateway && gatewayKernel.tailscaleMode !== "off" ? { prepareManagedTailscaleIngress: async (backend) => {
const { startGatewayTailscaleExposure } = await import("./server-tailscale-BG1-pIYc.js");
const cleanup = await startGatewayTailscaleExposure({
tailscaleMode: gatewayKernel.tailscaleMode,
preserveFunnel: gatewayKernel.tailscaleConfig.preserveFunnel ?? false,
port,
backend,
controlUiBasePath: gatewayKernel.controlUiBasePath,
logTailscale
});
gatewayKernel.kernel.setTailscaleCleanup(cleanup);
} } : {}
});
gatewayKernel.transportBridge.attach(transport);
startupSettled = (await finishGatewayStartup({
kernelRuntime: {
...gatewayKernel,
...transport
},
port,
opts,
bootId: gatewayKernel.bootId,
log,
logHealth,
logWsControl,
logHooks,
logChannels,
logCron,
logReload,
loadGatewayStartupPostAttachModule,
waitForPostReadyWork: () => postReadyWorkBarrier
})).startupSettled;
} catch (err) {
releasePostReadyWork();
return await rethrowGatewayStartupError(err, closeOnStartupFailure);
}
const postReadyWorkTimer = setTimeout(releasePostReadyWork, POST_READY_WORK_START_DELAY_MS);
postReadyWorkTimer.unref?.();
let closePromise;
return {
startupSettled,
getTailscaleIngressEndpoint: gatewayKernel.transportBridge.getTailscaleIngressEndpoint,
close: (optsLocal) => {
if (!closePromise) {
const prelude = beginClosePrelude(optsLocal);
clearTimeout(postReadyWorkTimer);
releasePostReadyWork();
closePromise = (async () => {
await prelude;
const close = await prepareClose(optsLocal);
await runGatewayShutdownSteps({
steps: [
{
name: "connection-dependent sidecars",
run: stopConnectionDependentSidecars,
required: true
},
{
name: "received connection work",
run: () => gatewayKernel.connectionWork.drain(),
required: true
},
{
name: "terminal sessions",
run: () => terminalSessions.disposeAll()
},
{
name: "gateway lifetime sidecars",
run: stopRegisteredGatewayLifetimeSidecars
},
{
name: "post-ready sidecars",
run: stopRegisteredPostReadySidecars
},
{
name: "gateway_stop plugin hooks",
run: async () => {
await shutdownRuntime.runGlobalGatewayStopSafely({
event: { reason: optsLocal?.reason ?? "gateway stopping" },
ctx: { port },
onError: (error) => log.warn(`gateway_stop hook failed: ${formatErrorMessage(error)}`)
});
}
},
{
name: "gateway close prelude",
run: runClosePrelude
},
{
name: "late sidecar cleanup",
run: sealAndJoinRegisteredSidecarStops
},
{
name: "gateway close",
run: close
}
],
onError: (message) => log.error(message)
});
})();
}
return closePromise;
}
};
}
//#endregion
export { resetPreparedModelCatalogForTestCore, startGatewayServerCore };