openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
2,658 lines • 109 kB
JavaScript
import { c as normalizeOptionalString } from "./string-coerce-mnp54Vah.js";
import { n as isVitestRuntimeEnv, r as logAcceptedEnvOption, t as isTruthyEnvValue } from "./env-Di49gJwo.js";
import { D as resolveIntegerOption, b as parseStrictPositiveInteger } from "./number-coercion-CJQ8TR--.js";
import { a as normalizeStateDirEnv, i as isNixMode } from "./paths-mvMm5bYV.js";
import { _ as uniqueStrings } from "./string-normalization-WNUDCpXX.js";
import { _ as sleep } from "./utils-CCC-BEJH.js";
import { D as runWithDiagnosticTraceContext, h as setDiagnosticsEnabledForProcess, l as isDiagnosticsEnabled, v as createDiagnosticTraceContext } from "./diagnostic-events-Chmz2PSy.js";
import { r as runtimeForLogger, t as createSubsystemLogger } from "./subsystem-BzXSmsuh.js";
import { o as setCurrentPluginMetadataSnapshot } from "./current-plugin-metadata-snapshot-CfA_n2Nc.js";
import { t as clearPluginMetadataLifecycleCaches } from "./plugin-metadata-lifecycle-C3dWg4tn.js";
import { c as isDiagnosticsTimelineEnabled, s as emitDiagnosticsTimelineEvent } from "./plugin-metadata-snapshot-Ctk9Oarq.js";
import { t as loadBundledPluginPublicArtifactModuleSync } from "./public-surface-loader-CqBVRQ-t.js";
import { t as isContainerEnvironment } from "./container-environment-CNsJSTpY.js";
import { _ as resolveRequestClientIp, i as isLoopbackHost, m as resolveGatewayListenHosts } from "./net-DTe7AQiu.js";
import { s as createAuthRateLimiter } from "./auth-rate-limit-Bwr29t2Y.js";
import { o as isLocalDirectRequest, r as authorizeHttpGatewayConnect } from "./auth-DXSWhsnk.js";
import { n as resolveGatewayAuth } from "./auth-resolve-BSKl_nHz.js";
import { A as applyConfigOverrides, c as promoteConfigSnapshotToLastKnownGood, i as getRuntimeConfig, u as readConfigFileSnapshot, v as registerConfigWriteListener } from "./io-Gi7-pyU-.js";
import { v as setRuntimeConfigSnapshot } from "./runtime-snapshot-D93_HOsR.js";
import { n as isRestartEnabled } from "./commands.flags-Bx5KKfOp.js";
import { t as loadGatewayTlsRuntime$1 } from "./gateway-cyLi5C4L.js";
import { n as createCoreGatewayMethodDescriptors, r as isCoreGatewayMethodClassified, s as listCoreGatewayMethodNames, t as STARTUP_UNAVAILABLE_GATEWAY_METHODS } from "./core-descriptors-CqQ0q50E.js";
import { i as READ_SCOPE, n as APPROVALS_SCOPE, o as WRITE_SCOPE, r as PAIRING_SCOPE, t as ADMIN_SCOPE } from "./operator-scopes-CS3xdS-V.js";
import "./method-scopes-BtdN3y7s.js";
import { h as setPreRestartDeferralCheck, m as setGatewaySigusr1RestartPolicy } from "./restart-CiO1ILZU.js";
import { n as readGatewayRestartHandoffSync } from "./restart-handoff-B55foNq9.js";
import { f as pinActivePluginChannelRegistry, g as releasePinnedPluginHttpRouteRegistry, h as releasePinnedPluginChannelRegistry, p as pinActivePluginHttpRouteRegistry } from "./runtime-B2ROiq5l.js";
import { i as createPluginGatewayMethodDescriptors, n as createGatewayMethodRegistry, t as createGatewayMethodDescriptorsFromHandlers } from "./registry-CCu2s_6W.js";
import { i as resolveMainSessionKey } from "./main-session-Eahn-btj.js";
import { n as getLoadedChannelPluginEntryById, r as listLoadedChannelPlugins } from "./registry-loaded-C3DZQnLP.js";
import "./sessions-D6zZvoxX.js";
import { a as enqueueSystemEvent } from "./system-events-C5WI3S5a.js";
import { C as stopDiagnosticHeartbeat, S as startDiagnosticHeartbeat } from "./diagnostic-TmaJRyPq.js";
import { i as withDiagnosticPhase } from "./diagnostic-phase-LQperifR.js";
import { o as resolveCronJobsStorePath } from "./store-_jAmB352.js";
import { u as getActiveEmbeddedRunCount } from "./run-state-BYGFyjne.js";
import { u as getTotalQueueSize } from "./command-queue-DGvXlE4p.js";
import { n as clearSecretsRuntimeSnapshot, r as getActiveSecretsRuntimeConfigSnapshot } from "./runtime-state-B3MH_XLp.js";
import { s as setFallbackGatewayContextResolver } from "./server-plugins-DUe67FFK.js";
import { t as getTotalPendingReplies } from "./dispatcher-registry-CaTZukRA.js";
import { c as recordGatewayRestartTraceDetail, d as resumeGatewayRestartTraceFromHandoff, i as finishGatewayRestartTrace, l as recordGatewayRestartTraceSpan, n as collectGatewayProcessMemoryUsageMb, u as resumeGatewayRestartTraceFromEnv } from "./restart-trace-q_or-N4J.js";
import { t as ensureOpenClawCliOnPath } from "./path-env-CDAYsLgD.js";
import { c as resolveControlUiRootSync, n as isPackageProvenControlUiRootSync, s as resolveControlUiRootOverrideSync, t as ensureControlUiAssetsBuilt } from "./control-ui-assets-OWiFO3R_.js";
import { n as logRejectedLargePayload } from "./diagnostic-payload-DELJ7QXt.js";
import { i as MAX_BUFFERED_BYTES, o as MAX_PREAUTH_PAYLOAD_BYTES } from "./server-constants-BGwLM6XN.js";
import { s as normalizePluginNodeCapabilityScopedUrl } from "./plugin-node-capability-CQtFV9Fn.js";
import { i as summarizeAgentEventForWsLog, n as logWs, r as shouldLogWs } from "./ws-log-DlzuStZb.js";
import { t as createDefaultDeps } from "./deps-CWqGdIJS.js";
import { c as setDefaultSecurityHeaders, r as sendGatewayAuthFailure } from "./http-common-DunL6val.js";
import { t as GatewayLockError } from "./gateway-lock-BQehKmw6.js";
import { n as DEFAULT_CHANNEL_STALE_EVENT_THRESHOLD_MS, r as evaluateChannelHealth, t as DEFAULT_CHANNEL_CONNECT_GRACE_MS } from "./channel-health-policy-D_eDwUBm.js";
import { n as ensureControlUiAllowedOriginsForNonLoopbackBind } from "./gateway-control-ui-origins-DdvpUKq8.js";
import { n as resolveAssistantIdentity } from "./assistant-identity-BYpxelqc.js";
import { t as resolveSharedGatewaySessionGeneration } from "./ws-shared-generation-Bp5l7wzu.js";
import { i as getRequiredSharedGatewaySessionGeneration, r as enforceSharedGatewaySessionGenerationForConfigWrite } from "./server-shared-auth-generation-kMfFmuwl.js";
import { a as createToolEventRecipientRegistry, n as createChatRunState } from "./server-chat-state-CcwJF39T.js";
import { a as incrementPresenceVersion, i as getPresenceVersion, n as getHealthCache, o as refreshGatewayHealthSnapshot, r as getHealthVersion } from "./health-state-Cha0Tz7F.js";
import { t as resolveGatewayPluginConfig } from "./runtime-plugin-config-DPKmNeph.js";
import { n as applyGatewayLaneConcurrency, t as resolveHookClientIpConfig } from "./hook-client-ip-config-CWB_cWpU.js";
import { t as GATEWAY_EVENTS } from "./server-methods-list-BcMuybf7.js";
import { a as resolvePluginRoutePathContext, i as isProtectedPluginRoutePathFromContext } from "./route-match-DlTv7M6i.js";
import { n as shouldEnforceGatewayAuthForPluginPath } from "./route-auth-CuhIaW7_.js";
import { t as findMatchingPluginNodeCapabilityRoute } from "./route-capability-CofcQt2a.js";
import path from "node:path";
import { monitorEventLoopDelay, performance } from "node:perf_hooks";
import { WebSocketServer } from "ws";
import { createServer } from "node:http";
import { createServer as createServer$1 } from "node:https";
//#region src/gateway/plugin-channel-reload-targets.ts
function addNormalizedTarget(targets, value) {
const normalized = normalizeOptionalString(value);
if (normalized) targets.add(normalized);
}
/** Lists all config ids that should trigger reload for a channel plugin target. */
function listChannelPluginConfigTargetIds(target) {
const targets = /* @__PURE__ */ new Set();
addNormalizedTarget(targets, target.channelId);
addNormalizedTarget(targets, target.pluginId);
for (const alias of target.aliases ?? []) addNormalizedTarget(targets, alias);
return targets;
}
/** Returns true when changed config paths affect any target plugin/channel id. */
function pluginConfigTargetsChanged(targetIds, changedPaths) {
const prefixes = Array.from(targetIds, (id) => [`plugins.entries.${id}`, `plugins.installs.${id}`]).flat();
return changedPaths.some((path) => prefixes.some((prefix) => path === prefix || path.startsWith(`${prefix}.`)));
}
//#endregion
//#region src/gateway/server-control-ui-root.ts
function startControlUiAssetsBuild(params) {
ensureControlUiAssetsBuilt(params.gatewayRuntime).then((result) => {
if (!result.ok && result.message) params.log.warn(`gateway: ${result.message}`);
}).catch((error) => {
params.log.warn(`gateway: Control UI assets build failed: ${error instanceof Error ? error.message : String(error)}`);
});
}
/** Resolves the Control UI asset root state for gateway startup. */
async function resolveGatewayControlUiRootState(params) {
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}`);
return resolvedOverride ? {
kind: "resolved",
path: resolvedOverride
} : {
kind: "invalid",
path: resolvedOverridePath
};
}
if (!params.controlUiEnabled) return;
const resolveRoot = () => resolveControlUiRootSync({
moduleUrl: import.meta.url,
argv1: process.argv[1],
cwd: process.cwd()
});
const resolvedRoot = resolveRoot();
if (!resolvedRoot) {
startControlUiAssetsBuild({
gatewayRuntime: params.gatewayRuntime,
log: params.log
});
return;
}
return {
kind: isPackageProvenControlUiRootSync(resolvedRoot, {
moduleUrl: import.meta.url,
argv1: process.argv[1],
cwd: process.cwd()
}) ? "bundled" : "resolved",
path: resolvedRoot
};
}
//#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 storePath = resolveCronJobsStorePath(params.cfg.cron?.store);
const cronEnabled = process.env.OPENCLAW_SKIP_CRON !== "1" && params.cfg.cron?.enabled !== false;
let loaded = null;
let loading = null;
let stopped = false;
const load = async () => {
if (loaded) return loaded;
loading ??= import("./server-cron-CPp7aT0g.js").then(({ buildGatewayCronService }) => {
loaded = {
state: buildGatewayCronService(params),
started: false
};
return loaded;
});
return await loading;
};
return {
cron: {
async start() {
stopped = false;
const resolved = await load();
if (stopped) return;
if (resolved.started) return;
resolved.started = true;
await resolved.state.cron.start();
if (stopped && resolved.started) {
resolved.started = false;
resolved.state.cron.stop();
}
},
stop() {
stopped = true;
if (loaded) {
loaded.started = false;
loaded.state.cron.stop();
return;
}
if (loading) loading.then((resolved) => {
if (!stopped) return;
resolved.started = false;
resolved.state.cron.stop();
}).catch(() => {});
},
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) {
return await (await load()).state.cron.add(input);
},
async update(id, patch) {
return await (await load()).state.cron.update(id, patch);
},
async remove(id) {
return await (await load()).state.cron.remove(id);
},
async run(id, mode) {
return await (await load()).state.cron.run(id, mode);
},
async enqueueRun(id, mode) {
return await (await load()).state.cron.enqueueRun(id, mode);
},
getJob(id) {
if (!loaded) return;
return loaded.state.cron.getJob(id);
},
async readJob(id) {
return await (await load()).state.cron.readJob(id);
},
getDefaultAgentId() {
if (!loaded) return;
return loaded.state.cron.getDefaultAgentId();
},
wake(opts) {
if (!loaded) {
load();
return { ok: false };
}
return loaded.state.cron.wake(opts);
}
},
storePath,
cronEnabled
};
}
//#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() {
const noopInterval = () => {
const timer = setInterval(() => {}, 1 << 30);
timer.unref?.();
return timer;
};
return {
bonjourStop: null,
tickInterval: noopInterval(),
healthInterval: noopInterval(),
dedupeCleanup: noopInterval(),
mediaCleanup: null,
heartbeatRunner: {
stop: () => {},
updateConfig: (_cfg) => {}
},
stopGatewayUpdateCheck: () => {},
tailscaleCleanup: null,
postReadySidecars: [],
gatewayLifetimeSidecars: [],
skillsRefreshTimer: null,
skillsRefreshDelayMs: 3e4,
skillsChangeUnsub: () => {},
channelHealthMonitor: null,
stopModelPricingRefresh: () => {},
mcpServer: void 0,
configReloader: { stop: async () => {} },
agentUnsub: null,
heartbeatUnsub: null,
transcriptUnsub: null,
lifecycleUnsub: 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,
pluginServices: null,
gatewayMethods: params.gatewayMethods
};
}
//#endregion
//#region src/gateway/server-broadcast.ts
const EVENT_SCOPE_GUARDS = {
agent: [READ_SCOPE],
chat: [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],
heartbeat: [],
"plugin.approval.requested": [APPROVALS_SCOPE],
"plugin.approval.resolved": [APPROVALS_SCOPE],
presence: [],
shutdown: [],
tick: [],
"talk.event": [READ_SCOPE],
"talk.mode": [WRITE_SCOPE],
"update.available": [],
"voicewake.changed": [READ_SCOPE],
"voicewake.routing.changed": [READ_SCOPE],
"device.pair.requested": [PAIRING_SCOPE],
"device.pair.resolved": [PAIRING_SCOPE],
"node.pair.requested": [PAIRING_SCOPE],
"node.pair.resolved": [PAIRING_SCOPE],
"sessions.changed": [READ_SCOPE],
"session.message": [READ_SCOPE],
"session.operation": [READ_SCOPE],
"session.tool": [READ_SCOPE]
};
const NODE_ALLOWED_EVENTS = new Set(["voicewake.changed", "voicewake.routing.changed"]);
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 hasEventScope(client, event) {
const required = EVENT_SCOPE_GUARDS[event];
if (!required && event.startsWith("plugin.")) {
if ((client.connect.role ?? "operator") !== "operator") return false;
const scopes = Array.isArray(client.connect.scopes) ? client.connect.scopes : [];
return scopes.includes("operator.write") || scopes.includes("operator.admin");
}
if (!required) return false;
if (required.length === 0) return true;
const role = client.connect.role ?? "operator";
if (role !== "operator") return role === "node" && NODE_ALLOWED_EVENTS.has(event);
const scopes = Array.isArray(client.connect.scopes) ? client.connect.scopes : [];
if (scopes.includes("operator.admin")) return true;
if (required.includes("operator.read")) return scopes.includes("operator.read") || scopes.includes("operator.write");
return required.some((scope) => scopes.includes(scope));
}
function createGatewayBroadcaster(params) {
const clientSeq = /* @__PURE__ */ new WeakMap();
const reportedSlowPayloadClients = /* @__PURE__ */ new WeakSet();
const broadcastInternal = (event, payload, opts, targetConnIds) => {
if (params.clients.size === 0) return;
const isTargeted = Boolean(targetConnIds);
if (shouldLogWs()) {
const logMeta = {
event,
seq: isTargeted ? "targeted" : "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));
logWs("out", "event", logMeta);
}
let frameBase;
const getFrameBase = () => {
if (!frameBase) frameBase = {
eventJSON: JSON.stringify(event),
payloadFragment: serializeFrameField("payload", payload),
stateVersionFragment: opts?.stateVersion === void 0 ? "" : serializeFrameField("stateVersion", opts.stateVersion)
};
return frameBase;
};
for (const c of params.clients) {
if (targetConnIds && !targetConnIds.has(c.connId)) continue;
if (!hasEventScope(c, event)) continue;
const nextSeq = (clientSeq.get(c) ?? 0) + 1;
const slow = c.socket.bufferedAmount > MAX_BUFFERED_BYTES;
if (!slow) reportedSlowPayloadClients.delete(c);
else if (!reportedSlowPayloadClients.has(c)) {
reportedSlowPayloadClients.add(c);
logRejectedLargePayload({
surface: "gateway.ws.outbound_buffer",
bytes: c.socket.bufferedAmount,
limitBytes: MAX_BUFFERED_BYTES,
reason: opts?.dropIfSlow ? "ws_send_buffer_drop" : "ws_send_buffer_close"
});
}
if (slow && opts?.dropIfSlow) {
if (!isTargeted) clientSeq.set(c, nextSeq);
continue;
}
if (slow) {
try {
c.socket.close(1008, "slow consumer");
} catch {}
continue;
}
try {
const eventSeq = isTargeted ? void 0 : nextSeq;
if (!isTargeted) clientSeq.set(c, nextSeq);
const base = getFrameBase();
const seqFragment = eventSeq === void 0 ? "" : `,"seq":${eventSeq}`;
const frame = `{"type":"event","event":${base.eventJSON}${base.payloadFragment}${seqFragment}${base.stateVersionFragment}}`;
c.socket.send(frame);
} catch {}
}
};
const broadcast = (event, payload, opts) => broadcastInternal(event, payload, opts);
const broadcastToConnIds = (event, payload, connIds, opts) => {
if (connIds.size === 0) return;
broadcastInternal(event, payload, opts, connIds);
};
return {
broadcast,
broadcastToConnIds
};
}
//#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 ";
function loadBundledChannelGatewayAuthApi(channelId) {
try {
return loadBundledPluginPublicArtifactModuleSync({
dirName: channelId,
artifactBasename: GATEWAY_AUTH_API_ARTIFACT_BASENAME
});
} catch (error) {
if (error instanceof Error && error.message.startsWith(MISSING_PUBLIC_SURFACE_PREFIX)) return;
throw error;
}
}
/**
* Resolves configured gateway auth bypass paths from a bundled channel artifact.
*/
function resolveBundledChannelGatewayAuthBypassPaths(params) {
return (loadBundledChannelGatewayAuthApi(params.channelId)?.resolveGatewayAuthBypassPaths?.({ cfg: params.cfg }) ?? []).flatMap((path) => typeof path === "string" && path.trim() ? [path.trim()] : []);
}
//#endregion
//#region src/gateway/server-http.ts
let identityAvatarModulePromise;
let controlUiModulePromise;
let embeddingsHttpModulePromise;
let managedImageAttachmentsModulePromise;
let modelsHttpModulePromise;
let openAiHttpModulePromise;
let openResponsesHttpModulePromise;
let sessionHistoryHttpModulePromise;
let sessionKillHttpModulePromise;
let toolsInvokeHttpModulePromise;
let pluginNodeCapabilityAuthModulePromise;
let httpAuthUtilsModulePromise;
let pluginRouteRuntimeScopesModulePromise;
function getIdentityAvatarModule() {
identityAvatarModulePromise ??= import("./identity-avatar-C_4GqQbo.js");
return identityAvatarModulePromise;
}
function getControlUiModule() {
controlUiModulePromise ??= import("./control-ui-C74HTYCV.js");
return controlUiModulePromise;
}
function getEmbeddingsHttpModule() {
embeddingsHttpModulePromise ??= import("./embeddings-http-ByFASJA1.js");
return embeddingsHttpModulePromise;
}
function getManagedImageAttachmentsModule() {
managedImageAttachmentsModulePromise ??= import("./managed-image-attachments-Cc16ND01.js");
return managedImageAttachmentsModulePromise;
}
function getModelsHttpModule() {
modelsHttpModulePromise ??= import("./models-http-DEVnFdBd.js");
return modelsHttpModulePromise;
}
function getOpenAiHttpModule() {
openAiHttpModulePromise ??= import("./openai-http-DJ7nlIMd.js");
return openAiHttpModulePromise;
}
function getOpenResponsesHttpModule() {
openResponsesHttpModulePromise ??= import("./openresponses-http-CYkW69Pz.js");
return openResponsesHttpModulePromise;
}
function getSessionHistoryHttpModule() {
sessionHistoryHttpModulePromise ??= import("./sessions-history-http-DeTbXV_j.js");
return sessionHistoryHttpModulePromise;
}
function getSessionKillHttpModule() {
sessionKillHttpModulePromise ??= import("./session-kill-http-DaAM7htL.js");
return sessionKillHttpModulePromise;
}
function getToolsInvokeHttpModule() {
toolsInvokeHttpModulePromise ??= import("./tools-invoke-http-CHftJH8K.js");
return toolsInvokeHttpModulePromise;
}
function getPluginNodeCapabilityAuthModule() {
pluginNodeCapabilityAuthModulePromise ??= import("./plugin-node-capability-auth-Btf_u4k9.js");
return pluginNodeCapabilityAuthModulePromise;
}
function getHttpAuthUtilsModule() {
httpAuthUtilsModulePromise ??= import("./http-auth-utils-CzAL4aMA.js");
return httpAuthUtilsModulePromise;
}
function getPluginRouteRuntimeScopesModule() {
pluginRouteRuntimeScopesModulePromise ??= import("./plugin-route-runtime-scopes-BxEuC1N7.js");
return pluginRouteRuntimeScopesModulePromise;
}
const GATEWAY_PROBE_STATUS_BY_PATH = new Map([
["/health", "live"],
["/healthz", "live"],
["/ready", "ready"],
["/readyz", "ready"]
]);
const 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 resolveBundledChannelGatewayAuthBypassPaths({
channelId,
cfg: configSnapshot
})) paths.add(path);
return paths;
}
function getCachedPluginGatewayAuthBypassPaths(configSnapshot) {
const cached = pluginGatewayAuthBypassPathsCache.get(configSnapshot);
if (cached) return cached;
const resolved = resolvePluginGatewayAuthBypassPaths(configSnapshot).catch((error) => {
pluginGatewayAuthBypassPathsCache.delete(configSnapshot);
throw error;
});
pluginGatewayAuthBypassPathsCache.set(configSnapshot, resolved);
return resolved;
}
function isOpenAiModelsPath(pathname) {
return pathname === "/v1/models" || pathname.startsWith("/v1/models/");
}
function isEmbeddingsPath(pathname) {
return pathname === "/v1/embeddings";
}
function isOpenAiChatCompletionsPath(pathname) {
return pathname === "/v1/chat/completions";
}
function isOpenResponsesPath(pathname) {
return pathname === "/v1/responses";
}
function isToolsInvokePath(pathname) {
return pathname === "/tools/invoke";
}
function isManagedOutgoingImagePath(pathname) {
return pathname.startsWith("/api/chat/media/outgoing/");
}
function isSessionKillPath(pathname) {
return /^\/sessions\/[^/]+\/kill$/.test(pathname);
}
function isSessionHistoryPath(pathname) {
return /^\/sessions\/[^/]+\/history$/.test(pathname);
}
function shouldEnforceDefaultPluginGatewayAuth(pathContext) {
return pathContext.malformedEncoding || pathContext.decodePassLimitReached || isProtectedPluginRoutePathFromContext(pathContext);
}
async function canRevealReadinessDetails(params) {
if (isLocalDirectRequest(params.req, params.trustedProxies, params.allowRealIpFallback)) return true;
if (params.resolvedAuth.mode === "none") return false;
const { getBearerToken, resolveHttpBrowserOriginPolicy } = await getHttpAuthUtilsModule();
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,
browserOriginPolicy: resolveHttpBrowserOriginPolicy(params.req)
})).ok;
}
/** Handles live/ready probe endpoints before normal gateway routing. */
async function handleGatewayProbeRequest(req, res, requestPath, resolvedAuth, trustedProxies, allowRealIpFallback, getReadiness) {
const status = GATEWAY_PROBE_STATUS_BY_PATH.get(requestPath);
if (!status) 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 canRevealReadinessDetails({
req,
resolvedAuth,
trustedProxies,
allowRealIpFallback
});
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 {
statusCode = 200;
body = JSON.stringify({
ok: true,
status
});
}
res.statusCode = statusCode;
res.end(method === "HEAD" ? void 0 : body);
return true;
}
function writeUpgradeAuthFailure(socket, auth) {
if (auth.rateLimited) {
const retryAfterSeconds = auth.retryAfterMs && auth.retryAfterMs > 0 ? Math.ceil(auth.retryAfterMs / 1e3) : void 0;
socket.write([
"HTTP/1.1 429 Too Many Requests",
retryAfterSeconds ? `Retry-After: ${retryAfterSeconds}` : void 0,
"Content-Type: application/json; charset=utf-8",
"Connection: close",
"",
JSON.stringify({ error: {
message: "Too many failed authentication attempts. Please try again later.",
type: "rate_limited"
} })
].filter(Boolean).join("\r\n"));
return;
}
socket.write("HTTP/1.1 401 Unauthorized\r\nConnection: close\r\n\r\n");
}
function writeUpgradeServiceUnavailable(socket, body) {
socket.write(`HTTP/1.1 503 Service Unavailable\r
Connection: close\r
Content-Type: text/plain; charset=utf-8\r
Content-Length: ${Buffer.byteLength(body, "utf8")}\r\n\r
` + body);
}
function parseGatewayRequestPath(rawUrl) {
try {
return new URL(rawUrl ?? "/", "http://localhost").pathname;
} catch {
return;
}
}
async function runGatewayHttpRequestStages(stages) {
for (const stage of stages) try {
if (await stage.run()) return true;
} catch (err) {
if (!stage.continueOnError) throw err;
console.error(`[gateway-http] stage "${stage.name}" threw — skipping:`, err);
}
return false;
}
function buildPluginRequestStages(params) {
if (!params.handlePluginRequest) return [];
let pluginGatewayAuthSatisfied = false;
let pluginGatewayRequestAuth;
let pluginRequestOperatorScopes;
return [{
name: "plugin-auth",
run: async () => {
const pathContext = params.pluginPathContext ?? resolvePluginRoutePathContext(params.requestPath);
if (!(params.shouldEnforcePluginGatewayAuth ?? shouldEnforceDefaultPluginGatewayAuth)(pathContext)) return false;
if ((await params.getGatewayAuthBypassPaths()).has(params.requestPath)) return false;
const { authorizeGatewayHttpRequestOrReply } = await getHttpAuthUtilsModule();
const requestAuth = await authorizeGatewayHttpRequestOrReply({
req: params.req,
res: params.res,
auth: params.resolvedAuth,
trustedProxies: params.trustedProxies,
allowRealIpFallback: params.allowRealIpFallback,
rateLimiter: params.rateLimiter
});
if (!requestAuth) return true;
pluginGatewayAuthSatisfied = true;
pluginGatewayRequestAuth = requestAuth;
const { resolvePluginRouteRuntimeOperatorScopes } = await getPluginRouteRuntimeScopesModule();
pluginRequestOperatorScopes = resolvePluginRouteRuntimeOperatorScopes(params.req, requestAuth);
return false;
}
}, {
name: "plugin-http",
continueOnError: true,
run: () => {
const pathContext = params.pluginPathContext ?? resolvePluginRoutePathContext(params.requestPath);
return params.handlePluginRequest?.(params.req, params.res, pathContext, {
gatewayAuthSatisfied: pluginGatewayAuthSatisfied,
gatewayRequestAuth: pluginGatewayRequestAuth,
gatewayRequestOperatorScopes: pluginRequestOperatorScopes
}) ?? false;
}
}];
}
/** Creates the gateway HTTP/HTTPS server and ordered request-stage router. */
function createGatewayHttpServer(opts) {
const { clients, controlUiEnabled, controlUiBasePath, controlUiRoot, openAiChatCompletionsEnabled, openAiChatCompletionsConfig, openResponsesEnabled, openResponsesConfig, strictTransportSecurityHeader, handleHooksRequest, handlePluginRequest, shouldEnforcePluginGatewayAuth, resolvePluginNodeCapabilityRoute, resolvedAuth, rateLimiter, getReadiness } = opts;
const getResolvedAuth = opts.getResolvedAuth ?? (() => resolvedAuth);
const loadGatewayConfig = opts.getRuntimeConfig ?? getRuntimeConfig;
const openAiCompatEnabled = openAiChatCompletionsEnabled || openResponsesEnabled;
const httpServer = opts.tlsOptions ? createServer$1(opts.tlsOptions, (req, res) => {
handleRequestWithTrace(req, res);
}) : createServer((req, res) => {
handleRequestWithTrace(req, res);
});
function handleRequestWithTrace(req, res) {
return runWithDiagnosticTraceContext(createDiagnosticTraceContext(), () => handleRequest(req, res));
}
async function handleRequest(req, res) {
setDefaultSecurityHeaders(res, { strictTransportSecurity: strictTransportSecurityHeader });
if ((req.headers.upgrade ?? "").toLowerCase() === "websocket") return;
try {
const requestPath = parseGatewayRequestPath(req.url);
if (requestPath === void 0) {
sendGatewayAuthFailure(res, {
ok: false,
reason: "unauthorized"
});
return;
}
if (GATEWAY_PROBE_STATUS_BY_PATH.get(requestPath) === "live") {
await handleGatewayProbeRequest(req, res, requestPath, getResolvedAuth(), [], false, getReadiness);
return;
}
const configSnapshot = loadGatewayConfig();
const trustedProxies = configSnapshot.gateway?.trustedProxies ?? [];
const allowRealIpFallback = configSnapshot.gateway?.allowRealIpFallback === true;
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 = handlePluginRequest ? resolvePluginRoutePathContext(scopedRequestPath) : null;
const resolvedAuthValue = getResolvedAuth();
const requestStages = [{
name: "gateway-probes",
run: () => handleGatewayProbeRequest(req, res, scopedRequestPath, resolvedAuthValue, trustedProxies, allowRealIpFallback, getReadiness)
}, {
name: "hooks",
run: () => handleHooksRequest(req, res)
}];
if (openAiCompatEnabled && isOpenAiModelsPath(scopedRequestPath)) requestStages.push({
name: "models",
run: async () => (await getModelsHttpModule()).handleOpenAiModelsHttpRequest(req, res, {
auth: resolvedAuthValue,
trustedProxies,
allowRealIpFallback,
rateLimiter
})
});
if (openAiCompatEnabled && isEmbeddingsPath(scopedRequestPath)) requestStages.push({
name: "embeddings",
run: async () => (await getEmbeddingsHttpModule()).handleOpenAiEmbeddingsHttpRequest(req, res, {
auth: resolvedAuthValue,
trustedProxies,
allowRealIpFallback,
rateLimiter
})
});
if (isToolsInvokePath(scopedRequestPath)) requestStages.push({
name: "tools-invoke",
run: async () => (await getToolsInvokeHttpModule()).handleToolsInvokeHttpRequest(req, res, {
auth: resolvedAuthValue,
trustedProxies,
allowRealIpFallback,
rateLimiter
})
});
if (isSessionKillPath(scopedRequestPath)) requestStages.push({
name: "sessions-kill",
run: async () => (await getSessionKillHttpModule()).handleSessionKillHttpRequest(req, res, {
auth: resolvedAuthValue,
trustedProxies,
allowRealIpFallback,
rateLimiter
})
});
if (isSessionHistoryPath(scopedRequestPath)) requestStages.push({
name: "sessions-history",
run: async () => (await getSessionHistoryHttpModule()).handleSessionHistoryHttpRequest(req, res, {
auth: resolvedAuthValue,
getResolvedAuth,
trustedProxies,
allowRealIpFallback,
rateLimiter
})
});
if (openResponsesEnabled && isOpenResponsesPath(scopedRequestPath)) requestStages.push({
name: "openresponses",
run: async () => (await getOpenResponsesHttpModule()).handleOpenResponsesHttpRequest(req, res, {
auth: resolvedAuthValue,
config: openResponsesConfig,
trustedProxies,
allowRealIpFallback,
rateLimiter
})
});
if (openAiChatCompletionsEnabled && isOpenAiChatCompletionsPath(scopedRequestPath)) requestStages.push({
name: "openai",
run: async () => (await getOpenAiHttpModule()).handleOpenAiHttpRequest(req, res, {
auth: resolvedAuthValue,
config: openAiChatCompletionsConfig,
trustedProxies,
allowRealIpFallback,
rateLimiter
})
});
if (handlePluginRequest && pluginPathContext && resolvePluginNodeCapabilityRoute?.(pluginPathContext)) {
const nodeCapability = resolvePluginNodeCapabilityRoute(pluginPathContext);
requestStages.push({
name: "plugin-node-capability-auth",
run: async () => {
if (!nodeCapability) return false;
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;
}
});
}
requestStages.push(...buildPluginRequestStages({
req,
res,
requestPath: scopedRequestPath,
getGatewayAuthBypassPaths: () => getCachedPluginGatewayAuthBypassPaths(configSnapshot),
pluginPathContext,
handlePluginRequest,
shouldEnforcePluginGatewayAuth,
resolvedAuth: resolvedAuthValue,
trustedProxies,
allowRealIpFallback,
rateLimiter
}));
if (isManagedOutgoingImagePath(scopedRequestPath)) requestStages.push({
name: "chat-managed-image-media",
run: async () => (await getManagedImageAttachmentsModule()).handleManagedOutgoingImageHttpRequest(req, res, {
auth: resolvedAuthValue,
trustedProxies,
allowRealIpFallback,
rateLimiter
})
});
if (controlUiEnabled) {
requestStages.push({
name: "control-ui-assistant-media",
run: async () => (await getControlUiModule()).handleControlUiAssistantMediaRequest(req, res, {
basePath: controlUiBasePath,
config: configSnapshot,
agentId: resolveAssistantIdentity({ cfg: configSnapshot }).agentId,
auth: resolvedAuthValue,
trustedProxies,
allowRealIpFallback,
rateLimiter
})
});
requestStages.push({
name: "control-ui-avatar",
run: async () => {
const { handleControlUiAvatarRequest } = await getControlUiModule();
const { resolveAgentAvatar } = await getIdentityAvatarModule();
return handleControlUiAvatarRequest(req, res, {
basePath: controlUiBasePath,
auth: resolvedAuthValue,
trustedProxies,
allowRealIpFallback,
rateLimiter,
resolveAvatar: (agentId) => resolveAgentAvatar(configSnapshot, agentId, { includeUiOverride: true })
});
}
});
requestStages.push({
name: "control-ui-http",
run: async () => (await getControlUiModule()).handleControlUiHttpRequest(req, res, {
basePath: controlUiBasePath,
config: configSnapshot,
agentId: resolveAssistantIdentity({ cfg: configSnapshot }).agentId,
root: controlUiRoot,
auth: resolvedAuthValue,
trustedProxies,
allowRealIpFallback,
rateLimiter
})
});
}
if (await runGatewayHttpRequestStages(requestStages)) return;
res.statusCode = 404;
res.setHeader("Content-Type", "text/plain; charset=utf-8");
res.end("Not Found");
} catch (err) {
console.error("[gateway-http] unhandled error in request handler:", err);
res.statusCode = 500;
res.setHeader("Content-Type", "text/plain; charset=utf-8");
res.end("Internal Server Error");
}
}
return httpServer;
}
/** 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, log } = opts;
const getResolvedAuth = opts.getResolvedAuth ?? (() => resolvedAuth);
httpServer.on("upgrade", (req, socket, head) => {
runWithDiagnosticTraceContext(createDiagnosticTraceContext(), async () => {
const configSnapshot = getRuntimeConfig();
const trustedProxies = configSnapshot.gateway?.trustedProxies ?? [];
const allowRealIpFallback = configSnapshot.gateway?.allowRealIpFallback === true;
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);
const nodeCapability = resolvePluginNodeCapabilityRoute?.(pathContext);
if (nodeCapability) {
const { authorizePluginNodeCapabilityRequest } = await getPluginNodeCapabilityAuthModule();
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();
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();
pluginGatewayRequestOperatorScopes = resolvePluginRouteRuntimeOperatorScopes(req, authCheck.requestAuth);
}
if (await handlePluginUpgrade(req, socket, head, pathContext, {
gatewayAuthSatisfied: pluginGatewayAuthSatisfied,
gatewayRequestAuth: pluginGatewayRequestAuth,
gatewayRequestOperatorScopes: pluginGatewayRequestOperatorScopes
})) return;
}
const preauthBudgetKey = resolveRequestClientIp(req, trustedProxies, allowRealIpFallback);
if (wss.listenerCount("connection") === 0) {
writeUpgradeServiceUnavailable(socket, "Gateway websocket handlers unavailable");
socket.destroy();
return;
}
if (!preauthConnectionBudget.acquire(preauthBudgetKey)) {
writeUpgradeServiceUnavailable(socket, "Too many unauthenticated sockets");
socket.destroy();
return;
}
let budgetTransferred = false;
const releaseUpgradeBudget = () => {
if (budgetTransferred) return;
budgetTransferred = true;
preauthConnectionBudget.release(preauthBudgetKey);
};
socket.once("close", releaseUpgradeBudget);
try {
wss.handleUpgrade(req, socket, head, (ws) => {
ws["__openclawPreauthBudgetKey"] = preauthBudgetKey;
wss.emit("connection", ws, req);
if (Boolean(ws["__openclawPreauthBudgetClaimed"])) {
budgetTransferred = true;
socket.off("close", releaseUpgradeBudget);
}
});
} catch {
socket.off("close", releaseUpgradeBudget);
releaseUpgradeBudget();
throw new Error("gateway websocket upgrade failed");
}
}).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-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 } = params;
for (const attempt of Array.from({ length: 21 }, (_, 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 < EADDRINUSE_MAX_RETRIES) {
await closeServerQuietly(httpServer);
await sleep(EADDRINUSE_RETRY_INTERVAL_MS);
continue;
}
if (code === "EADDRINUSE") throw new GatewayLockError(`another gateway instance is already listening on ws://${bindHost}:${port}`, err);
throw new GatewayLockError(`failed to bind gateway socket on ws://${bindHost}:${port}: ${String(err)}`, err);
}
}
//#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 || env.VITEST && env.OPENCLAW_TEST_MAX_PREAUTH_CONNECTIONS_PER_IP;
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 loadGatewayPluginsHttpModule = async () => await import("./plugins-http-Co5KbUSt.js");
/** Creates the HTTP/WebSocket runtime state and pinned plugin registries for one gateway start. */
async function createGatewayRuntimeState(params) {
pinActivePluginHttpRouteRegistry(params.pluginRegistry);
if (params.pinChannelRegistry !== false) pinActivePluginChannelRegistry(params.pluginRegistry);
else releasePinnedPluginChannelRegistry();
try {
const resolvePluginRouteRegistry = () => params.getPluginRouteRegistry?.() ?? params.pluginRegistry;
const clients = /* @__PURE__ */ new Set();
const { broadcast, broadcastToConnIds } = createGatewayBroadcaster({ clients });
let loadedHooksRequestHandler = null;
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;
if (!loadedHooksRequestHandler) {
const { createGatewayHooksRequestHandler } = await import("./hooks-AiivSleC.js");
loadedHooksRequestHandler = createGatewayHooksRequestHandler({
deps: params.deps,
getHooksConfig: params.hooksConfig,
getClientIpConfig: params.getHookClientIpConfig,
bindHost: params.bindHost,
port: params.port,
logHooks: params.logHooks
});
}
return await loadedHooksRequestHandler(req, res);
};
let loadedPluginRequestHandler = null;
let loadedPluginUpgradeHandler = null;
const handlePluginRequest = async (req, res, pathContext, dispatchContext) => {
if ((resolvePluginRouteRegistry().httpRoutes ?? []).length === 0) return false;
if (!loadedPluginRequestHandler) {
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 ((resolvePluginRouteRegistry().httpRoutes ?? []).length === 0) return false;
if (!loadedPluginUpgradeHandler) {
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 resolvePluginNodeCapabilityRoute = (pathContext) => findMatchingPluginNodeCapabilityRoute(resolvePluginRouteRegistry(), pathContext)?.nodeCapability;
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 WebSocketServer({
noServer: true,
maxPayload: MAX_PREAUTH_PAYLOAD_BYTES
});
const preauthConnectionBudget = createPreauthConnectionBudget();
const httpServers = [];
const httpBindHosts = [];
for (const _ of bindHosts) {
const httpServer = createGatewayHttpServer({
clients,
controlUiEnabled: params.controlUiEnabled,
controlUiBasePath: params.controlUiBasePath,
controlUiRoot: params.controlUiRoot,
openAiChatCompletionsEnabled: params.openAiChatCompletionsEnabled,
openAiChatCompletionsConfig: params.openAiChatCompletionsConfig,
openResponsesEnabled: params.openResponsesEnabled,
openResponsesConfig: params.openResponsesConfig,
strictTransportSecurityHeader: params.strictTransportSecurityHeader,
handleHooksRequest,
handlePluginRequest,
shouldEnforcePluginGatewayAuth,
resolvePluginNodeCapabilityRoute,
resolvedAuth: params.resolvedAuth,
getResolvedAuth: params.getResolvedAuth,
rateLimiter: params.rateLimiter,
getReadiness: params.getReadiness,
tlsOptions: params.gatewayTls?.enabled ? params.gatewayTls.tlsOptions : void 0
});
attachGatewayUpgradeHandler({
httpServer,
wss,
handlePluginUpgrade,
shouldEnforcePluginGatewayAuth,
resolvePluginNodeCapabilityRoute,
clients,
preauthConnectionBudget,
resolvedAuth: params.resolvedAuth,
getResolvedAuth: params.getResolvedAuth,
rateLimiter: params.rateLimiter,
log: params.log
});
httpServers.push(httpServer);
}
const httpServer = httpServers[0];
if (!httpServer) throw new Error("Gateway HTTP server failed to start");
let startListeningPromise = null;
const startListening = async () => {
if (startListeningPromise) {
await startListeningPromise;
return;
}
startListeningPromise = (async () => {
for (const [index, host] of bindHosts.entries()) {
const server = httpServers[index];
if (!server) throw new Error(`Missing gateway HTTP server for bind host ${host}`);
try {
await listenGatewayHttpServer({
httpServer: server,
bindHost: host,
port: params.port
});
httpBindHosts.push(host);
} catch (err) {
if (host === bindHosts[0]) throw err;
params.log.warn(`gateway: failed to bind loopback alias ${host}:${params.port} (${String(err)})`);
}
}
if (httpBindHosts.length === 0) throw new Error("Gateway HTTP server failed to start");
})();
try {
await startListeningPromise;
} catch (err) {
startListeningPromise = null;
throw err;
}
};
const agentRunSeq = /* @__PURE__ */ new Map();
const dedupe = /* @__PURE__ */ new Map();
const chatRunState = createChatRunState();
const chatRunRegistry = chatRunState.registry;
return {
releasePluginRouteRegistry: () => {
releasePinnedPluginHttpRouteRegistry();
releasePinnedPluginChannelRegistry();
},
httpServer,
httpServers,
httpBindHosts,
startListening,
wss,
preauthConnectionBudget,
clients,
broadcast,
broadcastToConnIds,
agentRunSeq,
dedupe,
chatRunState,
chatRunBuffers: chatRunState.buffers,
chatDeltaSentAt: chatRunState.deltaSentAt,
chatDeltaLastBroadcastLen: chatRunState.deltaLastBroadcastLen,
addChatRun: chatRunRegistry.add,
removeChatRun: chatRunRegistry.remove,
chatAbortControllers: /* @__PURE__ */ new Map(),
toolEventRecipients: createToolEventRecipientRegistry()
};
} catch (err) {
releasePinnedPluginHttpRouteRegistry();
releasePinnedPluginChannelRegistry();
throw err;
}
}
//#endregion
//#region src/gateway/server-wizard-sessions.ts
/** Creates the in-memory tracker used for active Gateway wizard sessions. */
function createWizardSessionTracker() {
const wizardSessions = /* @__PURE__ */ new Map();
const findRunningWizard = () => {
for (const [id, session] of wizardSessions) if (session.getStatus() === "running") return id;
return null;
};
const purgeWizardSession = (id) => {
const session = wizardSessions.get(id);
if (!session) return;
if (session.getStatus() === "running") return;
wizardSessions.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 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 ?? Date.now;
const readCpuUsage = deps.cpuUsage ?? process.cpuUsage.bind(process);
const readEventLoopUtilization = deps.eventLoopUtilization ?? performance.eventLoopUtilization.bind(performance);
const createDelayMonitor = deps.createDelayMonitor ?? ((resolutionMs) => monitorEventLoopDelay({ resolution: resolutionMs }));
let monitor = null;
let lastWallAt = nowMs();
let lastCpuUsage = readCpuUsage();
let lastEventLoopUtilization = readEventLoopUtilization();
let lastSnapshot;
try {
monitor = createDelayMonitor(EVENT_LOOP_MONITOR_RESOLUTION_MS);
monitor.enable();
monitor.reset();
} catch {
monitor = null;
}
return {
snapshot: () => {
if (!monitor || !lastCpuUsage || !lastEventLoopUtilization || lastWallAt <= 0) return;
const now = nowMs();
const intervalMs = Math.max(1, now - lastWallAt);
const delayP99Ms = nanosecondsToMilliseconds(monitor.percentile(99));
const delayMaxMs = nanosecondsToMilliseconds(monitor.max);
if (!(delayP99Ms >= EVENT_LOOP_DELAY_WARN_MS || delayMaxMs >= EVENT_LOOP_DELAY_WARN_MS) && intervalMs < SUSTAINED_LOAD_SAMPLE_MIN_INTERVAL_MS) return lastSnapshot;
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 snapshot = {
degraded: reasons.length > 0,
reasons,
intervalMs,
delayP99Ms,
delayMaxMs,
utilization,
cpuCoreRatio
};
monitor.reset();
lastWallAt = now;
lastCpuUsage = readCpuUsage();
lastEventLoopUtilization = currentEventLoopUtilization;
lastSnapshot = snapshot;
return snapshot;
},
stop: () => {
monitor?.disable();
monitor = null;
lastWallAt = 0;
lastCpuUsage = null;
lastEventLoopUtilization = null;
lastSnapshot = void 0;
}
};
}
//#endregion
//#region src/gateway/server/readiness.ts
const DEFAULT_READINESS_CACHE_TTL_MS = 1e3;
function shouldIgnoreReadinessFailure(accountSnapshot, health) {
if (health.reason === "unmanaged" || health.reason === "stale-socket") return true;
return health.reason === "not-running" && accountSnapshot.restartPending === true;
}
/** Create a cached readiness checker over channel runtime health. */
function createReadinessChecker(deps) {
const { channelManager, startedAt } = deps;
const cacheTtlMs = Math.max(0, deps.cacheTtlMs ?? DEFAULT_READINESS_CACHE_TTL_MS);
let cachedAt = 0;
let cachedState = null;
return () => {
const now = Date.now();
const uptimeMs = now - startedAt;
if (deps.getStartupPending?.()) return withEventLoopHealth({
ready: false,
failing: [deps.getStartupPendingReason?.() ?? "startup-sidecars"],
uptimeMs
}, deps.getEventLoopHealth);
if (deps.shouldSkipChannelReadiness?.()) return withEventLoopHealth({
ready: true,
failing: [],
uptimeMs
}, deps.getEventLoopHealth);
if (cachedState && now - cachedAt < cacheTtlMs) return withEventLoopHealth({
...cachedState,
uptimeMs
}, deps.getEventLoopHealth);
const snapshot = channelManager.getRuntimeSnapshot();
const failing = [];
for (const [channelId, accounts] of Object.entries(snapshot.channelAccounts)) {
if (!accounts) continue;
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 && !shouldIgnoreReadinessFailure(accountSnapshot, health)) {
failing.push(channelId);
break;
}
}
}
cachedAt = now;
cachedState = {
ready: failing.length === 0,
failing
};
return withEventLoopHealth({
...cachedState,
uptimeMs
}, deps.getEventLoopHealth);
};
}
function withEventLoopHealth(result, getEventLoopHealth) {
const eventLoop = getEventLoopHealth?.();
return eventLoop ? {
...result,
eventLoop
} : result;
}
//#endregion
//#region src/gateway/server/tls.ts
/** Loads certificate/key material for the gateway listener from config. */
async function loadGatewayTlsRuntime(cfg, log) {
return await loadGatewayTlsRuntime$1(cfg, log);
}
//#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.impl.ts
let gatewayModelCatalogModulePromise = null;
const loadGatewayModelCatalogModule = async () => {
gatewayModelCatalogModulePromise ??= import("./server-model-catalog-DpjUx-ks.js");
return await gatewayModelCatalogModulePromise;
};
async function resetModelCatalogCacheForTest() {
const { resetModelCatalogCacheForTest: resetModelCatalogCacheForTestLocal } = await loadGatewayModelCatalogModule();
await resetModelCatalogCacheForTestLocal();
}
ensureOpenClawCliOnPath();
const MAX_MEDIA_TTL_HOURS = 168;
const POST_READY_MAINTENANCE_DELAY_MS = 250;
let gatewayStartupEarlyModulePromise = null;
let gatewayStartupPostAttachModulePromise = null;
function loadGatewayStartupEarlyModule() {
gatewayStartupEarlyModulePromise ??= import("./server-startup-early-Dnq8FXb4.js");
return gatewayStartupEarlyModulePromise;
}
function loadGatewayStartupPostAttachModule() {
gatewayStartupPostAttachModulePromise ??= import("./server-startup-post-attach-CNavhh7C.js");
return gatewayStartupPostAttachModulePromise;
}
function listGatewayStartupChannelPlugins() {
return listLoadedChannelPlugins();
}
function resolveMediaCleanupTtlMs(ttlHoursRaw) {
const ttlMs = Math.min(Math.max(ttlHoursRaw, 1), MAX_MEDIA_TTL_HOURS) * 60 * 6e4;
if (!Number.isFinite(ttlMs) || !Number.isSafeInteger(ttlMs)) throw new Error(`Invalid media.ttlHours: ${String(ttlHoursRaw)}`);
return ttlMs;
}
const log = createSubsystemLogger("gateway");
const logDiscovery = log.child("discovery");
const logTailscale = log.child("tailscale");
const logChannels = log.child("channels");
let cachedChannelRuntimePromise = null;
function getChannelRuntime() {
cachedChannelRuntimePromise ??= import("./runtime-channel-D9-IYkDu.js").then(({ createRuntimeChannel }) => createRuntimeChannel());
return cachedChannelRuntimePromise;
}
async function closeMcpLoopbackServerOnDemand() {
const { closeMcpLoopbackServer } = await import("./mcp-http-DXWAQNZ3.js");
await closeMcpLoopbackServer();
}
let gatewayCloseModulePromise = null;
function loadGatewayCloseModule() {
gatewayCloseModulePromise ??= import("./server-close.runtime.js");
return gatewayCloseModulePromise;
}
const loadGatewayModelCatalog = async (...args) => {
return (await loadGatewayModelCatalogModule()).loadGatewayModelCatalog(...args);
};
let gatewayPluginBootstrapModulePromise = null;
const loadGatewayPluginBootstrapModule = async () => {
gatewayPluginBootstrapModulePromise ??= import("./server-plugin-bootstrap-C1t0ZYOK.js");
return await gatewayPluginBootstrapModulePromise;
};
const logHealth = log.child("health");
const logCron = log.child("cron");
const logReload = log.child("reload");
const logHooks = log.child("hooks");
const logPlugins = log.child("plugins");
const logWsControl = log.child("ws");
const logSecrets = log.child("secrets");
const gatewayRuntime = runtimeForLogger(log);
function createGatewayStartupTrace() {
const logEnabled = isTruthyEnvValue(process.env.OPENCLAW_GATEWAY_STARTUP_TRACE);
let timelineConfig;
let eventLoopDelay;
const timelineOptions = () => ({
...timelineConfig ? { config: timelineConfig } : {},
env: process.env
});
const eventLoopTimelineEnabled = () => isDiagnosticsTimelineEnabled(timelineOptions()) && isTruthyEnvValue(process.env.OPENCLAW_DIAGNOSTICS_EVENT_LOOP);
const ensureEventLoopDelay = () => {
if (eventLoopDelay || !logEnabled && !eventLoopTimelineEnabled()) return;
eventLoopDelay = monitorEventLoopDelay({ resolution: 10 });
eventLoopDelay.enable();
};
ensureEventLoopDelay();
const started = performance.now();
let last = started;
let spanSequence = 0;
const formatMetric = (key, value) => `${key}=${typeof value === "number" ? value.toFixed(1) : value}`;
const mapTimelineName = (name) => {
switch (name) {
case "config.snapshot": return "config.load";
case "config.auth":
case "config.final-snapshot":
case "runtime.config": return "config.normalize";
case "plugins.bootstrap": return "plugins.load";
case "runtime.post-attach":
case "ready": return "gateway.ready";
default: return name;
}
};
const takeEventLoopSample = () => {
if (!eventLoopDelay) return;
const sample = {
p50Ms: eventLoopDelay.percentile(50) / 1e6,
p95Ms: eventLoopDelay.percentile(95) / 1e6,
p99Ms: eventLoopDelay.percentile(99) / 1e6,
maxMs: eventLoopDelay.max / 1e6
};
eventLoopDelay.reset();
return sample;
};
const emitEventLoopTimelineSample = (activeSpanName, sample) => {
if (!eventLoopTimelineEnabled()) return;
if (!sample) return;
emitDiagnosticsTimelineEvent({
type: "eventLoop.sample",
name: "eventLoop",
phase: "startup",
activeSpanName: mapTimelineName(activeSpanName),
attributes: activeSpanName === mapTimelineName(activeSpanName) ? void 0 : { traceName: activeSpanName },
...sample
}, timelineOptions());
};
const emit = (name, durationMs, totalMs, eventLoopSample, extras = []) => {
const metrics = [["eventLoopMax", `${(eventLoopSample?.maxMs ?? 0).toFixed(1)}ms`], ...extras];
recordGatewayRestartTraceSpan(`restart.ready.${name}`, durationMs, totalMs, metrics);
if (logEnabled) log.info(`startup trace: ${name} ${durationMs.toFixed(1)}ms total=${totalMs.toFixed(1)}ms ${metrics.map(([key, value]) => formatMetric(key, value)).join(" ")}`);
};
return {
setConfig(config) {
timelineConfig = config;
ensureEventLoopDelay();
},
mark(name) {
const now = performance.now();
const eventLoopSample = takeEventLoopSample();
emit(name, now - last, now - started, eventLoopSample);
emitDiagnosticsTimelineEvent({
type: "mark",
name: mapTimelineName(name),
phase: "startup",
durationMs: now - started,
attributes: name === mapTimelineName(name) ? void 0 : { traceName: name }
}, timelineOptions());
emitEventLoopTimelineSample(name, eventLoopSample);
last = now;
if (name === "ready") eventLoopDelay?.disable();
},
detail(name, metrics) {
const attributes = Object.fromEntries(metrics);
recordGatewayRestartTraceDetail(`restart.ready.${name}`, metrics);
if (logEnabled) log.info(`startup trace: ${name} ${metrics.map(([key, value]) => formatMetric(key, value)).join(" ")}`);
emitDiagnosticsTimelineEvent({
type: "mark",
name: mapTimelineName(name),
phase: "startup",
attributes: {
traceName: name,
...attributes
}
}, timelineOptions());
},
async measure(name, run, options = {}) {
const before = performance.now();
const spanId = `gateway-startup-${++spanSequence}`;
emitDiagnosticsTimelineEvent({
type: "span.start",
name: mapTimelineName(name),
phase: "startup",
spanId,
attributes: name === mapTimelineName(name) ? void 0 : { traceName: name }
}, timelineOptions());
try {
const result = await withDiagnosticPhase(mapTimelineName(name), run, { traceName: name });
const now = performance.now();
emitDiagnosticsTimelineEvent({
type: "span.end",
name: mapTimelineName(name),
phase: "startup",
spanId,
durationMs: now - before,
attributes: name === mapTimelineName(name) ? void 0 : { traceName: name }
}, timelineOptions());
return result;
} catch (error) {
const now = performance.now();
emitDiagnosticsTimelineEvent({
type: "span.error",
name: mapTimelineName(name),
phase: "startup",
spanId,
durationMs: now - before,
attributes: name === mapTimelineName(name) ? void 0 : { traceName: name },
errorName: error instanceof Error ? error.name : typeof error,
...options.omitErrorMessage ? {} : { errorMessage: error instanceof Error ? error.message : String(error) }
}, timelineOptions());
throw error;
} finally {
const now = performance.now();
const eventLoopSample = takeEventLoopSample();
emit(name, now - before, now - started, eventLoopSample);
emitEventLoopTimelineSample(name, eventLoopSample);
last = now;
}
}
};
}
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 stopTaskRegistryMaintenanceOnDemand() {
const { stopTaskRegistryMaintenance } = await import("./task-registry.maintenance-DciQfQc1.js");
stopTaskRegistryMaintenance();
}
function createGatewayAuthRateLimiters(rateLimitConfig) {
return {
rateLimiter: createAuthRateLimiter(rateLimitConfig ?? {}),
browserRateLimiter: createAuthRateLimiter({
...rateLimitConfig,
exemptLoopback: false
})
};
}
const runDefaultSetupWizard = async (...args) => {
const { runSetupWizard } = await import("./setup-D4KXTmWR.js");
return runSetupWizard(...args);
};
async function startGatewayServer(port = 18789, opts = {}) {
normalizeStateDirEnv(process.env);
const { bootstrapGatewayNetworkRuntime } = await import("./server-network-runtime-DaW2FvHU.js");
bootstrapGatewayNetworkRuntime();
const minimalTestGateway = isVitestRuntimeEnv() && process.env.OPENCLAW_TEST_MINIMAL_GATEWAY === "1";
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]
]);
}
const startupTrace = createGatewayStartupTrace();
const startupConfigModulePromise = import("./server-startup-config-oHbjlr7L.js");
let startupPluginsModulePromise = null;
const loadStartupPluginsModule = () => {
startupPluginsModulePromise ??= import("./server-startup-plugins-BbejNxaJ.js");
return startupPluginsModulePromise;
};
const { loadGatewayStartupConfigSnapshot } = await startupConfigModulePromise;
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 emitSecretsStateEvent = (code, message, cfg) => {
enqueueSystemEvent(`[${code}] ${message}`, {
sessionKey: resolveMainSessionKey(cfg),
contextKey: code
});
};
const { createRuntimeSecretsActivator } = await startupConfigModulePromise;
const activateRuntimeSecrets = createRuntimeSecretsActivator({
logSecrets,
emitStateEvent: emitSecretsStateEvent,
...startupConfigLoad.pluginMetadataSnapshot ? { pluginMetadataSnapshot: startupConfigLoad.pluginMetadataSnapshot } : {}
});
let cfgAtStart;
let startupInternalWriteHash = null;
let startupLastGoodSnapshot = configSnapshot;
const startupActivationSourceConfig = configSnapshot.sourceConfig;
const startupRuntimeConfig = applyConfigOverrides(configSnapshot.config);
startupTrace.setConfig(startupRuntimeConfig);
const { prepareGatewayStartupConfig } = await startupConfigModulePromise;
const authBootstrap = await startupTrace.measure("config.auth", () => prepareGatewayStartupConfig({
configSnapshot,
authOverride: opts.auth,
tailscaleOverride: opts.tailscale,
activateRuntimeSecrets,
log,
measure: (name, run, measureOptions) => startupTrace.measure(name, run, measureOptions)
}), { omitErrorMessage: true });
cfgAtStart = authBootstrap.cfg;
startupTrace.setConfig(cfgAtStart);
if (authBootstrap.generatedToken) log.warn(formatRuntimeGatewayAuthTokenWarning());
const diagnosticsEnabled = isDiagnosticsEnabled(cfgAtStart);
setDiagnosticsEnabledForProcess(diagnosticsEnabled);
if (diagnosticsEnabled) startDiagnosticHeartbeat(void 0, {
getConfig: getRuntimeConfig,
startupGraceMs: 6e4
});
setGatewaySigusr1RestartPolicy({ allowExternal: isRestartEnabled(cfgAtStart) });
let getActiveTaskCount = () => 0;
setPreRestartDeferralCheck(() => getTotalQueueSize() + getTotalPendingReplies() + getActiveEmbeddedRunCount() + getActiveTaskCount());
cfgAtStart = (minimalTestGateway ? {
config: cfgAtStart,
seededAllowedOrigins: false
} : await startupTrace.measure("control-ui.seed", () => maybeSeedControlUiAllowedOriginsAtStartup({
config: cfgAtStart,
log,
runtimeBind: opts.bind,
runtimePort: port
}))).config;
if (startupConfigLoad.wroteConfig || authBootstrap.persistedGeneratedToken) {
const startupSnapshot = await startupTrace.measure("config.final-snapshot", () => readConfigFileSnapshot());
startupInternalWriteHash = startupSnapshot.hash ?? null;
startupLastGoodSnapshot = startupSnapshot;
}
setRuntimeConfigSnapshot(cfgAtStart, startupLastGoodSnapshot.sourceConfig);
const { prepareGatewayPluginBootstrap } = await loadStartupPluginsModule();
const pluginBootstrap = await startupTrace.measure("plugins.bootstrap", () => prepareGatewayPluginBootstrap({
cfgAtStart,
activationSourceConfig: startupActivationSourceConfig,
startupRuntimeConfig,
pluginMetadataSnapshot: startupConfigLoad.pluginMetadataSnapshot,
minimalTestGateway,
log,
loadRuntimePlugins: false,
loadSetupRuntimePlugins: true
}));
const { gatewayPluginConfigAtStart, defaultWorkspaceDir, deferredConfiguredChannelPluginIds, startupPluginIds, pluginLookUpTable, baseMethods, runtimePluginsLoaded } = pluginBootstrap;
const coreGatewayMethodNames = listCoreGatewayMethodNames();
setCurrentPluginMetadataSnapshot(pluginLookUpTable, {
config: startupActivationSourceConfig,
compatibleConfigs: [
startupRuntimeConfig,
cfgAtStart,
gatewayPluginConfigAtStart
],
env: process.env,
workspaceDir: defaultWorkspaceDir
});
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],
["deferredChannelPlugins", String(metrics.deferredChannelPluginCount)],
["deferredChannelPluginCount", metrics.deferredChannelPluginCount]
]);
}
let { pluginRegistry, baseGatewayMethods } = pluginBootstrap;
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()]);
const runtimeConfig = await startupTrace.measure("runtime.config", async () => {
const { resolveGatewayRuntimeConfig } = await import("./server-runtime-config-BBjJ_azR.js");
return resolveGatewayRuntimeConfig({
cfg: cfgAtStart,
port,
bind: opts.bind,
host: opts.host,
controlUiEnabled: opts.controlUiEnabled,
openAiChatCompletionsEnabled: opts.openAiChatCompletionsEnabled,
openResponsesEnabled: opts.openResponsesEnabled,
auth: opts.auth,
tailscale: opts.tailscale
});
});
const { bindHost, controlUiEnabled, openAiChatCompletionsEnabled, openAiChatCompletionsConfig, openResponsesEnabled, openResponsesConfig, strictTransportSecurityHeader, controlUiBasePath, controlUiRoot: controlUiRootOverride, resolvedAuth, tailscaleConfig, tailscaleMode } = runtimeConfig;
const getResolvedAuth = () => resolveGatewayAuth({
authConfig: getActiveSecretsRuntimeConfigSnapshot()?.config.gateway?.auth ?? getRuntimeConfig().gateway?.auth,
authOverride: opts.auth,
env: process.env,
tailscaleMode
});
const resolveSharedGatewaySessionGenerationForConfig = (config) => resolveSharedGatewaySessionGeneration(resolveGatewayAuth({
authConfig: config.gateway?.auth,
authOverride: opts.auth,
env: process.env,
tailscaleMode
}), config.gateway?.trustedProxies);
const resolveCurrentSharedGatewaySessionGeneration = () => resolveSharedGatewaySessionGeneration(getResolvedAuth(), getRuntimeConfig().gateway?.trustedProxies);
const resolveSharedGatewaySessionGenerationForRuntimeSnapshot = () => resolveSharedGatewaySessionGeneration(resolveGatewayAuth({
authConfig: getRuntimeConfig().gateway?.auth,
authOverride: opts.auth,
env: process.env,
tailscaleMode
}), getRuntimeConfig().gateway?.trustedProxies);
const sharedGatewaySessionGenerationState = {
current: resolveCurrentSharedGatewaySessionGeneration(),
required: null
};
const preauthHandshakeTimeoutMs = cfgAtStart.gateway?.handshakeTimeoutMs ?? getRuntimeConfig().gateway?.handshakeTimeoutMs;
const initialHooksConfig = runtimeConfig.hooksConfig;
const initialHookClientIpConfig = resolveHookClientIpConfig(cfgAtStart);
const rateLimitConfig = cfgAtStart.gateway?.auth?.rateLimit;
const { rateLimiter: authRateLimiter, browserRateLimiter: browserAuthRateLimiter } = createGatewayAuthRateLimiters(rateLimitConfig);
const controlUiRootState = await startupTrace.measure("control-ui.root", () => resolveGatewayControlUiRootState({
controlUiRootOverride,
controlUiEnabled,
gatewayRuntime,
log
}));
const wizardRunner = opts.wizardRunner ?? runDefaultSetupWizard;
const { wizardSessions, findRunningWizard, purgeWizardSession } = createWizardSessionTracker();
const deps = createDefaultDeps();
let runtimeState = null;
let gatewayCronStartHandled = false;
const gatewayTls = await startupTrace.measure("tls.runtime", () => loadGatewayTlsRuntime(cfgAtStart.gateway?.tls, log.child("tls")));
if (cfgAtStart.gateway?.tls?.enabled && !gatewayTls.enabled) throw new Error(gatewayTls.error ?? "gateway tls: failed to enable");
const serverStartedAt = Date.now();
const readinessEventLoopHealth = createGatewayEventLoopHealthMonitor();
let startupSidecarsReady = minimalTestGateway;
let startupPendingReason = "startup-sidecars";
let releaseStartupAccountStarts = () => {};
const startupAccountStartsReady = new Promise((resolve) => {
releaseStartupAccountStarts = resolve;
});
const { createChannelManager } = await import("./server-channels-g1oRRKIH.js");
const channelManager = createChannelManager({
getRuntimeConfig: () => {
return resolveGatewayPluginConfig({ config: getRuntimeConfig() });
},
channelLogs,
channelRuntimeEnvs,
resolveChannelRuntime: getChannelRuntime,
getPluginHttpRouteRegistry: () => pluginRegistry,
startupTrace,
deferStartupAccountStartsUntil: startupAccountStartsReady
});
const deferStartupSidecars = opts.deferStartupSidecars === true;
const isGatewayStartupPending = () => !startupSidecarsReady && !deferStartupSidecars;
const getReadiness = createReadinessChecker({
channelManager,
startedAt: serverStartedAt,
getStartupPending: isGatewayStartupPending,
getStartupPendingReason: () => startupPendingReason,
getEventLoopHealth: readinessEventLoopHealth.snapshot,
shouldSkipChannelReadiness: () => isTruthyEnvValue(process.env.OPENCLAW_SKIP_CHANNELS) || isTruthyEnvValue(process.env.OPENCLAW_SKIP_PROVIDERS)
});
log.info("starting HTTP server...");
let currentPluginRegistryGatewayContext;
const { releasePluginRouteRegistry, httpServer, httpServers, httpBindHosts, startListening, wss, preauthConnectionBudget, clients, broadcast, broadcastToConnIds, agentRunSeq, dedupe, chatRunState, chatRunBuffers, chatDeltaSentAt, chatDeltaLastBroadcastLen, addChatRun, removeChatRun, chatAbortControllers, toolEventRecipients } = await startupTrace.measure("runtime.state", () => createGatewayRuntimeState({
cfg: cfgAtStart,
bindHost,
port,
controlUiEnabled,
controlUiBasePath,
controlUiRoot: controlUiRootState,
openAiChatCompletionsEnabled,
openAiChatCompletionsConfig,
openResponsesEnabled,
openResponsesConfig,
strictTransportSecurityHeader,
resolvedAuth,
rateLimiter: authRateLimiter,
gatewayTls,
getResolvedAuth,
hooksConfig: () => runtimeState?.hooksConfig ?? initialHooksConfig,
getHookClientIpConfig: () => runtimeState?.hookClientIpConfig ?? initialHookClientIpConfig,
pluginRegistry,
getPluginRouteRegistry: () => pluginRegistry,
getGatewayRequestContext: () => currentPluginRegistryGatewayContext,
pinChannelRegistry: !minimalTestGateway,
deps,
log,
logHooks,
logPlugins,
getReadiness
}));
const { createGatewayNodeSessionRuntime } = await import("./server-node-session-runtime-x3RvDdaP.js");
const { nodeRegistry, nodePresenceTimers, sessionEventSubscribers, sessionMessageSubscribers, nodeSendToSession, nodeSendToAllSubscribed, nodeSubscribe, nodeUnsubscribe, nodeUnsubscribeAll, broadcastVoiceWakeChanged, hasTalkNodeConnected } = createGatewayNodeSessionRuntime({ broadcast });
applyGatewayLaneConcurrency(cfgAtStart);
runtimeState = createGatewayServerLiveState({
hooksConfig: initialHooksConfig,
hookClientIpConfig: initialHookClientIpConfig,
cronState: createLazyGatewayCronState({
cfg: cfgAtStart,
deps,
broadcast
}),
gatewayMethods: listActiveGatewayMethods(baseGatewayMethods)
});
deps.cron = runtimeState.cronState.cron;
const pluginHostServices = { get cron() {
return runtimeState.cronState.cron;
} };
let closePreludeStarted = false;
let postReadyMaintenanceTimer = null;
const clearPostReadyMaintenanceTimer = () => {
if (!postReadyMaintenanceTimer) return;
clearTimeout(postReadyMaintenanceTimer);
postReadyMaintenanceTimer = null;
};
const markClosePreludeStarted = () => {
closePreludeStarted = true;
clearPostReadyMaintenanceTimer();
};
const runClosePrelude = async () => {
markClosePreludeStarted();
clearPluginMetadataLifecycleCaches();
const { runGatewayClosePrelude } = await loadGatewayCloseModule();
await runGatewayClosePrelude({
...diagnosticsEnabled ? { stopDiagnostics: stopDiagnosticHeartbeat } : {},
clearSkillsRefreshTimer: () => {
if (!runtimeState?.skillsRefreshTimer) return;
clearTimeout(runtimeState.skillsRefreshTimer);
runtimeState.skillsRefreshTimer = null;
},
skillsChangeUnsub: runtimeState.skillsChangeUnsub,
disposeAuthRateLimiter: () => authRateLimiter.dispose(),
disposeBrowserAuthRateLimiter: () => browserAuthRateLimiter.dispose(),
stopModelPricingRefresh: runtimeState.stopModelPricingRefresh,
stopChannelHealthMonitor: () => runtimeState?.channelHealthMonitor?.stop(),
stopReadinessEventLoopHealth: readinessEventLoopHealth.stop,
clearSecretsRuntimeSnapshot,
closeMcpServer: closeMcpLoopbackServerOnDemand
});
};
const { getRuntimeSnapshot, startChannels, startChannel, stopChannel, markChannelLoggedOut } = channelManager;
const refreshGatewayHealthSnapshotWithRuntime = (optsResult) => refreshGatewayHealthSnapshot({
...optsResult,
getRuntimeSnapshot,
getEventLoopHealth: readinessEventLoopHealth.snapshot
});
const stopRegisteredPostReadySidecars = async () => {
const postReadySidecars = runtimeState.postReadySidecars;
runtimeState.postReadySidecars = [];
for (const postReadySidecar of postReadySidecars) await postReadySidecar.stop();
};
const stopRegisteredGatewayLifetimeSidecars = async () => {
const gatewayLifetimeSidecars = runtimeState.gatewayLifetimeSidecars;
runtimeState.gatewayLifetimeSidecars = [];
for (const gatewayLifetimeSidecar of gatewayLifetimeSidecars) await gatewayLifetimeSidecar.stop();
};
const createCloseHandler = () => async (optsValue) => {
const channelIds = listLoadedChannelPlugins().map((plugin) => plugin.id);
const { createGatewayCloseHandler, drainActiveSessionsForShutdown } = await loadGatewayCloseModule();
await createGatewayCloseHandler({
bonjourStop: runtimeState.bonjourStop,
tailscaleCleanup: runtimeState.tailscaleCleanup,
releasePluginRouteRegistry,
channelIds,
stopChannel,
pluginServices: runtimeState.pluginServices,
postReadySidecars: runtimeState.postReadySidecars,
cron: runtimeState.cronState.cron,
heartbeatRunner: runtimeState.heartbeatRunner,
updateCheckStop: runtimeState.stopGatewayUpdateCheck,
stopTaskRegistryMaintenance: stopTaskRegistryMaintenanceOnDemand,
nodePresenceTimers,
broadcast,
tickInterval: runtimeState.tickInterval,
healthInterval: runtimeState.healthInterval,
dedupeCleanup: runtimeState.dedupeCleanup,
mediaCleanup: runtimeState.mediaCleanup,
agentUnsub: runtimeState.agentUnsub,
heartbeatUnsub: runtimeState.heartbeatUnsub,
transcriptUnsub: runtimeState.transcriptUnsub,
lifecycleUnsub: runtimeState.lifecycleUnsub,
chatRunState,
chatAbortControllers,
removeChatRun,
agentRunSeq,
nodeSendToSession,
getPendingReplyCount: getTotalPendingReplies,
clients,
configReloader: runtimeState.configReloader,
wss,
httpServer,
httpServers,
drainActiveSessionsForShutdown
})(optsValue);
};
let clearFallbackGatewayContextForServer = () => {};
const closeOnStartupFailure = async () => {
try {
await stopRegisteredGatewayLifetimeSidecars();
await stopRegisteredPostReadySidecars();
await runClosePrelude();
await createCloseHandler()({ reason: "gateway startup failed" });
} finally {
clearFallbackGatewayContextForServer();
}
};
const broadcastVoiceWakeRoutingChanged = (config) => {
broadcast("voicewake.routing.changed", { config }, { dropIfSlow: true });
};
try {
const earlyRuntime = await startupTrace.measure("runtime.early", () => loadGatewayStartupEarlyModule().then(({ startGatewayEarlyRuntime }) => startGatewayEarlyRuntime({
minimalTestGateway,
cfgAtStart,
port,
gatewayTls,
gatewayDirectReachable: !isLoopbackHost(bindHost),
tailscaleMode,
log,
logDiscovery,
nodeRegistry,
pluginRegistry,
broadcast,
nodeSendToAllSubscribed,
getPresenceVersion,
getHealthVersion,
refreshGatewayHealthSnapshot: refreshGatewayHealthSnapshotWithRuntime,
logHealth,
dedupe,
chatAbortControllers,
chatRunState,
chatRunBuffers,
chatDeltaSentAt,
chatDeltaLastBroadcastLen,
removeChatRun,
agentRunSeq,
nodeSendToSession,
...typeof cfgAtStart.media?.ttlHours === "number" ? { mediaCleanupTtlMs: resolveMediaCleanupTtlMs(cfgAtStart.media.ttlHours) } : {},
skillsRefreshDelayMs: runtimeState.skillsRefreshDelayMs,
getSkillsRefreshTimer: () => runtimeState.skillsRefreshTimer,
setSkillsRefreshTimer: (timer) => {
runtimeState.skillsRefreshTimer = timer;
},
getRuntimeConfig,
startupTrace
})));
runtimeState.bonjourStop = earlyRuntime.bonjourStop;
getActiveTaskCount = earlyRuntime.getActiveTaskCount;
runtimeState.skillsChangeUnsub = earlyRuntime.skillsChangeUnsub;
const [{ startGatewayEventSubscriptions }, { startGatewayRuntimeServices }] = await startupTrace.measure("runtime.post-early-imports", () => Promise.all([import("./server-runtime-subscriptions-CZL0ItaG.js"), import("./server-runtime-startup-services-Cf0Bd_2X.js")]));
const runtimeSubscriptions = await startupTrace.measure("runtime.subscriptions", () => startGatewayEventSubscriptions({
broadcast,
broadcastToConnIds,
nodeSendToSession,
agentRunSeq,
chatRunState,
toolEventRecipients,
sessionEventSubscribers,
sessionMessageSubscribers,
chatAbortControllers
}));
Object.assign(runtimeState, runtimeSubscriptions);
const runtimeServices = await startupTrace.measure("runtime.services", () => startGatewayRuntimeServices({
minimalTestGateway,
cfgAtStart,
channelManager,
log
}));
Object.assign(runtimeState, runtimeServices);
const { execApprovalManager, pluginApprovalManager, extraHandlers, coreGatewayHandlers } = await startupTrace.measure("gateway.handlers", async () => {
const [{ createGatewayAuxHandlers }, { coreGatewayHandlers: coreGatewayHandlersLocal }] = await Promise.all([import("./server-aux-handlers-BjeT4a8k.js"), import("./server-methods-xd1wrn22.js")]);
return {
...createGatewayAuxHandlers({
log,
activateRuntimeSecrets,
sharedGatewaySessionGenerationState,
resolveSharedGatewaySessionGenerationForConfig,
clients,
startChannel,
stopChannel,
logChannels
}),
coreGatewayHandlers: coreGatewayHandlersLocal
};
});
const attachedGatewayExtraHandlers = {
...pluginRegistry.gatewayHandlers,
...extraHandlers
};
let attachedPluginGatewayHandlerKeys = new Set(Object.keys(pluginRegistry.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;
return createGatewayMethodRegistry([
...createCoreGatewayMethodDescriptors(coreDescriptorHandlers),
...createPluginGatewayMethodDescriptors(nextPluginRegistry),
...createGatewayMethodDescriptorsFromHandlers({
handlers: auxHandlers,
owner: {
kind: "aux",
area: "gateway-extra"
},
defaultScope: ADMIN_SCOPE
})
]);
};
let attachedGatewayMethodRegistry = buildAttachedGatewayMethodRegistry(pluginRegistry);
const listAttachedGatewayMethods = () => {
const methods = attachedGatewayMethodRegistry.listAdvertisedMethods();
methods.push(...listStartupChannelGatewayMethods());
return uniqueStrings(methods);
};
runtimeState.gatewayMethods.splice(0, runtimeState.gatewayMethods.length, ...listAttachedGatewayMethods());
const replaceAttachedPluginRuntime = (loaded) => {
pluginRegistry = loaded.pluginRegistry;
baseGatewayMethods = loaded.gatewayMethods;
for (const key of attachedPluginGatewayHandlerKeys) delete attachedGatewayExtraHandlers[key];
Object.assign(attachedGatewayExtraHandlers, pluginRegistry.gatewayHandlers);
attachedPluginGatewayHandlerKeys = new Set(Object.keys(pluginRegistry.gatewayHandlers));
attachedGatewayMethodRegistry = buildAttachedGatewayMethodRegistry(pluginRegistry);
runtimeState.gatewayMethods.splice(0, runtimeState.gatewayMethods.length, ...listAttachedGatewayMethods());
pinActivePluginHttpRouteRegistry(pluginRegistry);
pinActivePluginChannelRegistry(pluginRegistry);
};
const refreshAttachedGatewayDiscovery = async (nextPluginRegistry) => {
if (minimalTestGateway) return;
try {
const stopPreviousDiscovery = runtimeState.bonjourStop;
runtimeState.bonjourStop = null;
if (stopPreviousDiscovery) try {
await stopPreviousDiscovery();
} catch (err) {
logDiscovery.warn(`gateway discovery stop failed before plugin refresh: ${String(err)}`);
}
const { startGatewayPluginDiscovery } = await loadGatewayStartupEarlyModule();
runtimeState.bonjourStop = await startGatewayPluginDiscovery({
minimalTestGateway,
cfgAtStart,
port,
gatewayTls,
gatewayDirectReachable: !isLoopbackHost(bindHost),
tailscaleMode,
logDiscovery,
pluginRegistry: nextPluginRegistry
});
} catch (err) {
logDiscovery.warn(`gateway discovery refresh failed after plugin load: ${String(err)}`);
}
};
const listAttachedChannelConfigTargets = () => new Map(listGatewayStartupChannelPlugins().map((plugin) => [plugin.id, listChannelPluginConfigTargetIds({
channelId: plugin.id,
pluginId: getLoadedChannelPluginEntryById(plugin.id)?.pluginId,
aliases: plugin.meta.aliases
})]));
const reloadAttachedGatewayPlugins = async (params) => {
const beforeChannelTargets = listAttachedChannelConfigTargets();
const beforeChannelIds = new Set(beforeChannelTargets.keys());
const [{ loadPluginLookUpTable }, { prepareGatewayPluginLoad }, { startPluginServices }] = await Promise.all([
import("./plugin-lookup-table-BEW_p_aD.js"),
loadGatewayPluginBootstrapModule(),
import("./services-Be8nnJ8N.js")
]);
const nextPluginLookUpTable = loadPluginLookUpTable({
config: params.nextConfig,
workspaceDir: defaultWorkspaceDir,
env: process.env,
activationSourceConfig: params.nextConfig
});
const nextStartupPluginIds = new Set(nextPluginLookUpTable.startup.pluginIds);
const nextStartupChannelIds = /* @__PURE__ */ new Set();
for (const plugin of nextPluginLookUpTable.manifestRegistry.plugins) {
if (!nextStartupPluginIds.has(plugin.id)) continue;
if (plugin.channels.length === 0) {
nextStartupChannelIds.add(plugin.id);
continue;
}
for (const channelId of plugin.channels) nextStartupChannelIds.add(channelId);
}
const channelsToStopBeforeReplace = /* @__PURE__ */ new Set();
for (const channelId of beforeChannelIds) {
const targetIds = beforeChannelTargets.get(channelId) ?? new Set([channelId]);
if (!nextStartupChannelIds.has(channelId) || pluginConfigTargetsChanged(targetIds, params.changedPaths)) channelsToStopBeforeReplace.add(channelId);
}
await params.beforeReplace(channelsToStopBeforeReplace);
setCurrentPluginMetadataSnapshot(nextPluginLookUpTable, {
config: params.nextConfig,
env: process.env,
workspaceDir: defaultWorkspaceDir
});
const loaded = prepareGatewayPluginLoad({
cfg: params.nextConfig,
workspaceDir: defaultWorkspaceDir,
log,
coreGatewayMethodNames,
hostServices: pluginHostServices,
baseMethods,
pluginLookUpTable: nextPluginLookUpTable
});
const previousPluginServices = runtimeState.pluginServices;
runtimeState.pluginServices = null;
if (previousPluginServices) await previousPluginServices.stop().catch((err) => {
log.warn(`plugin services stop failed during reload: ${String(err)}`);
});
replaceAttachedPluginRuntime(loaded);
await refreshAttachedGatewayDiscovery(loaded.pluginRegistry);
try {
runtimeState.pluginServices = await startPluginServices({
registry: loaded.pluginRegistry,
config: params.nextConfig,
workspaceDir: defaultWorkspaceDir
});
} catch (err) {
log.warn(`plugin services failed to start after reload: ${String(err)}`);
}
const afterChannelTargets = listAttachedChannelConfigTargets();
const afterChannelIds = new Set(afterChannelTargets.keys());
const restartChannels = /* @__PURE__ */ new Set();
for (const channelId of new Set([...beforeChannelIds, ...afterChannelIds])) {
const targetIds = afterChannelTargets.get(channelId) ?? beforeChannelTargets.get(channelId) ?? new Set([channelId]);
if (afterChannelIds.has(channelId) && (beforeChannelIds.has(channelId) !== afterChannelIds.has(channelId) || pluginConfigTargetsChanged(targetIds, params.changedPaths))) restartChannels.add(channelId);
}
return {
restartChannels,
activeChannels: afterChannelIds
};
};
const unavailableGatewayMethods = new Set(minimalTestGateway ? [] : STARTUP_UNAVAILABLE_GATEWAY_METHODS);
const gatewayRequestContext = await startupTrace.measure("gateway.request-context", async () => {
const { createGatewayRequestContext } = await import("./server-request-context-CioeFw35.js");
return createGatewayRequestContext({
deps,
runtimeState,
getRuntimeConfig,
execApprovalManager,
pluginApprovalManager,
loadGatewayModelCatalog,
getHealthCache,
refreshHealthSnapshot: refreshGatewayHealthSnapshotWithRuntime,
logHealth,
logGateway: log,
incrementPresenceVersion,
getHealthVersion,
broadcast,
broadcastToConnIds,
nodeSendToSession,
nodeSendToAllSubscribed,
nodeSubscribe,
nodeUnsubscribe,
nodeUnsubscribeAll,
hasConnectedTalkNode: hasTalkNodeConnected,
clients,
enforceSharedGatewayAuthGenerationForConfigWrite: (nextConfig) => {
enforceSharedGatewaySessionGenerationForConfigWrite({
state: sharedGatewaySessionGenerationState,
nextConfig,
resolveRuntimeSnapshotGeneration: resolveSharedGatewaySessionGenerationForRuntimeSnapshot,
clients
});
},
nodeRegistry,
agentRunSeq,
chatAbortControllers,
chatAbortedRuns: chatRunState.abortedRuns,
chatRunBuffers: chatRunState.buffers,
chatDeltaSentAt: chatRunState.deltaSentAt,
chatDeltaLastBroadcastLen: chatRunState.deltaLastBroadcastLen,
chatDeltaLastBroadcastText: chatRunState.deltaLastBroadcastText,
agentDeltaSentAt: chatRunState.agentDeltaSentAt,
bufferedAgentEvents: chatRunState.bufferedAgentEvents,
clearChatRunState: chatRunState.clearRun,
addChatRun,
removeChatRun,
subscribeSessionEvents: sessionEventSubscribers.subscribe,
unsubscribeSessionEvents: sessionEventSubscribers.unsubscribe,
subscribeSessionMessageEvents: sessionMessageSubscribers.subscribe,
unsubscribeSessionMessageEvents: sessionMessageSubscribers.unsubscribe,
unsubscribeAllSessionEvents: (connId) => {
sessionEventSubscribers.unsubscribe(connId);
sessionMessageSubscribers.unsubscribeAll(connId);
},
getSessionEventSubscriberConnIds: sessionEventSubscribers.getAll,
registerToolEventRecipient: toolEventRecipients.add,
dedupe,
wizardSessions,
findRunningWizard,
purgeWizardSession,
getRuntimeSnapshot,
getEventLoopHealth: readinessEventLoopHealth.snapshot,
startChannel,
stopChannel,
markChannelLoggedOut,
wizardRunner,
broadcastVoiceWakeChanged,
unavailableGatewayMethods,
broadcastVoiceWakeRoutingChanged
});
});
currentPluginRegistryGatewayContext = gatewayRequestContext;
const fallbackGatewayContextCleanup = setFallbackGatewayContextResolver(() => gatewayRequestContext);
clearFallbackGatewayContextForServer = typeof fallbackGatewayContextCleanup === "function" ? () => {
fallbackGatewayContextCleanup();
} : () => {};
if (!minimalTestGateway) {
if (runtimePluginsLoaded && deferredConfiguredChannelPluginIds.length > 0) {
const { reloadDeferredGatewayPlugins } = await loadGatewayPluginBootstrapModule();
const loaded = await startupTrace.measure("gateway.deferred-plugins", () => reloadDeferredGatewayPlugins({
cfg: gatewayPluginConfigAtStart,
activationSourceConfig: startupActivationSourceConfig,
workspaceDir: defaultWorkspaceDir,
log,
coreGatewayMethodNames,
hostServices: pluginHostServices,
baseMethods,
pluginIds: startupPluginIds,
pluginLookUpTable,
logDiagnostics: false
}));
replaceAttachedPluginRuntime(loaded);
await refreshAttachedGatewayDiscovery(loaded.pluginRegistry);
}
}
const [{ attachGatewayWsHandlers }, { listPluginNodeCapabilities }] = await startupTrace.measure("gateway.ws-imports", () => Promise.all([import("./server-ws-runtime-CyhobgV0.js"), import("./route-capability-9Y_C1THF.js")]));
const pluginSurfaceScheme = gatewayTls.enabled ? "https" : "http";
await startupTrace.measure("gateway.ws-attach", () => attachGatewayWsHandlers({
wss,
clients,
preauthConnectionBudget,
port,
gatewayHost: bindHost ?? void 0,
pluginSurfaceScheme,
getPluginNodeCapabilities: () => listPluginNodeCapabilities(pluginRegistry),
resolvedAuth,
getResolvedAuth,
getRequiredSharedGatewaySessionGeneration: () => getRequiredSharedGatewaySessionGeneration(sharedGatewaySessionGenerationState),
rateLimiter: authRateLimiter,
browserRateLimiter: browserAuthRateLimiter,
preauthHandshakeTimeoutMs,
isStartupPending: isGatewayStartupPending,
gatewayMethods: runtimeState.gatewayMethods,
events: GATEWAY_EVENTS,
logGateway: log,
logHealth,
logWsControl,
extraHandlers: attachedGatewayExtraHandlers,
getMethodRegistry: () => attachedGatewayMethodRegistry,
broadcast,
context: gatewayRequestContext
}));
await startupTrace.measure("http.listen", () => startListening());
startupTrace.mark("http.bound");
const sessionDeliveryRecoveryMaxEnqueuedAt = Date.now();
let postAttachRuntimeReturned = false;
let scheduledServicesActivated = false;
let scheduledServicesModulePromise = null;
const loadScheduledServicesModule = () => {
scheduledServicesModulePromise ??= import("./server-runtime-services-CjCm7trW.js");
return scheduledServicesModulePromise;
};
const activateScheduledServicesWhenReady = () => {
if (closePreludeStarted || !postAttachRuntimeReturned || !startupSidecarsReady || scheduledServicesActivated) return;
scheduledServicesActivated = true;
loadScheduledServicesModule().then((gatewayRuntimeServices) => {
if (closePreludeStarted) return;
const activated = gatewayRuntimeServices.activateGatewayScheduledServices({
minimalTestGateway,
cfgAtStart,
deps,
sessionDeliveryRecoveryMaxEnqueuedAt,
cron: runtimeState.cronState.cron,
startCron: false,
logCron,
log,
pluginLookUpTable
});
runtimeState.heartbeatRunner = activated.heartbeatRunner;
runtimeState.stopModelPricingRefresh = activated.stopModelPricingRefresh;
});
};
({stopGatewayUpdateCheck: runtimeState.stopGatewayUpdateCheck, tailscaleCleanup: runtimeState.tailscaleCleanup, pluginServices: runtimeState.pluginServices} = await startupTrace.measure("runtime.post-attach", () => loadGatewayStartupPostAttachModule().then(({ startGatewayPostAttachRuntime, stopPostReadySidecarsAfterCloseStarted }) => startGatewayPostAttachRuntime({
minimalTestGateway,
cfgAtStart,
bindHost,
bindHosts: httpBindHosts,
port,
tlsEnabled: gatewayTls.enabled,
log,
isNixMode,
startupStartedAt: opts.startupStartedAt,
broadcast,
tailscaleMode,
resetOnExit: tailscaleConfig.resetOnExit ?? false,
serviceName: tailscaleConfig.serviceName,
preserveFunnel: tailscaleConfig.preserveFunnel ?? false,
controlUiBasePath,
logTailscale,
gatewayPluginConfigAtStart,
pluginRegistry,
defaultWorkspaceDir,
deps,
startChannels,
logHooks,
logChannels,
unavailableGatewayMethods,
loadStartupPlugins: runtimePluginsLoaded ? void 0 : async () => {
const { loadGatewayStartupPluginRuntime } = await loadStartupPluginsModule();
return loadGatewayStartupPluginRuntime({
cfg: gatewayPluginConfigAtStart,
activationSourceConfig: startupActivationSourceConfig,
workspaceDir: defaultWorkspaceDir,
log,
baseMethods,
coreGatewayMethodNames,
hostServices: pluginHostServices,
startupPluginIds,
pluginLookUpTable,
startupTrace
});
},
onStartupPluginsLoading: () => {
startupPendingReason = "startup-sidecars";
},
onStartupPluginsLoaded: async (loaded) => {
replaceAttachedPluginRuntime(loaded);
startupPendingReason = "startup-sidecars";
await refreshAttachedGatewayDiscovery(loaded.pluginRegistry);
},
getCronService: () => runtimeState?.cronState.cron,
onChannelsStarted: () => {
releaseStartupAccountStarts();
},
onPluginServices: (pluginServices) => {
runtimeState.pluginServices = pluginServices;
},
onPostReadySidecars: (postReadySidecars) => {
runtimeState.postReadySidecars = postReadySidecars;
stopPostReadySidecarsAfterCloseStarted({
postReadySidecars,
closeStarted: closePreludeStarted
});
if (closePreludeStarted) runtimeState.postReadySidecars = [];
},
onGatewayLifetimeSidecars: (gatewayLifetimeSidecars) => {
runtimeState.gatewayLifetimeSidecars = gatewayLifetimeSidecars;
stopPostReadySidecarsAfterCloseStarted({
postReadySidecars: gatewayLifetimeSidecars,
closeStarted: closePreludeStarted
});
if (closePreludeStarted) runtimeState.gatewayLifetimeSidecars = [];
},
onSidecarsReady: () => {
startupSidecarsReady = true;
activateScheduledServicesWhenReady();
},
isClosing: () => closePreludeStarted,
startupTrace,
deferSidecars: deferStartupSidecars,
logReadyOnSidecars: !deferStartupSidecars,
providerAuthPrewarm: { getConfig: getRuntimeConfig }
}))));
startupTrace.detail("memory.ready", collectGatewayProcessMemoryUsageMb());
startupTrace.mark("ready");
if (deferStartupSidecars) log.info("gateway ready");
finishGatewayRestartTrace("restart.ready", collectGatewayProcessMemoryUsageMb());
postAttachRuntimeReturned = true;
activateScheduledServicesWhenReady();
const { startManagedGatewayConfigReloader } = await import("./server-reload-handlers-CTehjgXX.js");
runtimeState.configReloader = startManagedGatewayConfigReloader({
minimalTestGateway,
initialConfig: cfgAtStart,
initialCompareConfig: startupLastGoodSnapshot.sourceConfig,
initialInternalWriteHash: startupInternalWriteHash,
watchPath: configSnapshot.path,
readSnapshot: readConfigFileSnapshot,
promoteSnapshot: promoteConfigSnapshotToLastKnownGood,
subscribeToWrites: registerConfigWriteListener,
deps,
broadcast,
getState: () => ({
hooksConfig: runtimeState.hooksConfig,
hookClientIpConfig: runtimeState.hookClientIpConfig,
heartbeatRunner: runtimeState.heartbeatRunner,
cronState: runtimeState.cronState,
channelHealthMonitor: runtimeState.channelHealthMonitor
}),
setState: (nextState) => {
const cronStateChanged = nextState.cronState !== runtimeState.cronState;
runtimeState.hooksConfig = nextState.hooksConfig;
runtimeState.hookClientIpConfig = nextState.hookClientIpConfig;
runtimeState.heartbeatRunner = nextState.heartbeatRunner;
runtimeState.cronState = nextState.cronState;
deps.cron = runtimeState.cronState.cron;
runtimeState.channelHealthMonitor = nextState.channelHealthMonitor;
if (cronStateChanged) gatewayCronStartHandled = true;
},
startChannel,
stopChannel,
stopPostReadySidecars: stopRegisteredPostReadySidecars,
reloadPlugins: reloadAttachedGatewayPlugins,
logHooks,
logChannels,
logCron,
logReload,
onCronRestart: () => {
gatewayCronStartHandled = true;
},
channelManager,
activateRuntimeSecrets,
resolveSharedGatewaySessionGenerationForConfig,
sharedGatewaySessionGenerationState,
clients
});
await promoteConfigSnapshotToLastKnownGood(startupLastGoodSnapshot).catch((err) => {
log.warn(`gateway: failed to promote config last-known-good backup: ${String(err)}`);
});
if (!minimalTestGateway) postReadyMaintenanceTimer = (await loadScheduledServicesModule()).scheduleGatewayPostReadyMaintenance({
delayMs: POST_READY_MAINTENANCE_DELAY_MS,
isClosing: () => closePreludeStarted,
onStarted: () => {
postReadyMaintenanceTimer = null;
},
startMaintenance: async () => {
if (closePreludeStarted) return null;
return earlyRuntime.startMaintenance();
},
applyMaintenance: (maintenance) => {
if (closePreludeStarted) {
clearInterval(maintenance.tickInterval);
clearInterval(maintenance.healthInterval);
clearInterval(maintenance.dedupeCleanup);
if (maintenance.mediaCleanup) clearInterval(maintenance.mediaCleanup);
return;
}
runtimeState.tickInterval = maintenance.tickInterval;
runtimeState.healthInterval = maintenance.healthInterval;
runtimeState.dedupeCleanup = maintenance.dedupeCleanup;
runtimeState.mediaCleanup = maintenance.mediaCleanup;
},
shouldStartCron: () => !closePreludeStarted && !gatewayCronStartHandled,
markCronStartHandled: () => {
gatewayCronStartHandled = true;
},
cron: runtimeState.cronState.cron,
logCron,
log,
recordPostReadyMemory: () => {
startupTrace.detail("memory.post-ready", collectGatewayProcessMemoryUsageMb());
}
});
else startupTrace.detail("memory.post-ready", collectGatewayProcessMemoryUsageMb());
} catch (err) {
await closeOnStartupFailure();
throw err;
}
const close = createCloseHandler();
return { close: async (optsLocal) => {
try {
markClosePreludeStarted();
await stopRegisteredGatewayLifetimeSidecars();
await stopRegisteredPostReadySidecars();
const { runGlobalGatewayStopSafely } = await import("./plugins/hook-runner-global.js");
await runGlobalGatewayStopSafely({
event: { reason: optsLocal?.reason ?? "gateway stopping" },
ctx: { port },
onError: (err) => log.warn(`gateway_stop hook failed: ${String(err)}`)
});
await runClosePrelude();
await close(optsLocal);
} finally {
clearFallbackGatewayContextForServer();
}
} };
}
//#endregion
export { resetModelCatalogCacheForTest, startGatewayServer };