2026-02-03 13:56:20 -05:00
import path from "node:path" ;
2026-01-14 14:31:43 +00:00
import { resolveAgentWorkspaceDir , resolveDefaultAgentId } from "../agents/agent-scope.js" ;
2026-02-13 15:29:29 -08:00
import { getActiveEmbeddedRunCount } from "../agents/pi-embedded-runner/runs.js" ;
2026-01-16 03:45:03 +00:00
import { registerSkillsChangeListener } from "../agents/skills/refresh.js" ;
2026-02-01 10:03:47 +09:00
import { initSubagentRegistry } from "../agents/subagent-registry.js" ;
2026-02-13 15:29:29 -08:00
import { getTotalPendingReplies } from "../auto-reply/reply/dispatcher-registry.js" ;
2026-02-18 01:34:35 +00:00
import type { CanvasHostServer } from "../canvas-host/server.js" ;
2026-01-14 14:31:43 +00:00
import { type ChannelId , listChannelPlugins } from "../channels/plugins/index.js" ;
2026-01-20 07:42:21 +00:00
import { formatCliCommand } from "../cli/command-format.js" ;
2026-02-01 10:03:47 +09:00
import { createDefaultDeps } from "../cli/deps.js" ;
2026-02-19 10:00:27 +01:00
import { isRestartEnabled } from "../config/commands.js" ;
2026-01-14 01:08:15 +00:00
import {
2026-01-27 12:19:58 +00:00
CONFIG_PATH ,
2026-02-21 11:13:25 -08:00
type OpenClawConfig ,
2026-01-14 01:08:15 +00:00
isNixMode ,
loadConfig ,
migrateLegacyConfig ,
readConfigFileSnapshot ,
writeConfigFile ,
} from "../config/config.js" ;
2026-03-02 20:05:12 -05:00
import { formatConfigIssueLines } from "../config/issue-format.js" ;
2026-01-20 16:37:34 +00:00
import { applyPluginAutoEnable } from "../config/plugin-auto-enable.js" ;
2026-02-22 14:37:20 -08:00
import { resolveMainSessionKey } from "../config/sessions.js" ;
2026-01-14 09:11:21 +00:00
import { clearAgentRunContext , onAgentEvent } from "../infra/agent-events.js" ;
2026-02-03 13:56:20 -05:00
import {
ensureControlUiAssetsBuilt ,
resolveControlUiRootOverrideSync ,
resolveControlUiRootSync ,
} from "../infra/control-ui-assets.js" ;
2026-02-01 10:03:47 +09:00
import { isDiagnosticsEnabled } from "../infra/diagnostic-events.js" ;
import { logAcceptedEnvOption } from "../infra/env.js" ;
import { createExecApprovalForwarder } from "../infra/exec-approval-forwarder.js" ;
2026-01-14 01:08:15 +00:00
import { onHeartbeatEvent } from "../infra/heartbeat-events.js" ;
2026-02-14 05:09:07 +00:00
import { startHeartbeatRunner , type HeartbeatRunner } from "../infra/heartbeat-runner.js" ;
2026-01-14 01:08:15 +00:00
import { getMachineDisplayName } from "../infra/machine-name.js" ;
2026-01-30 03:15:10 +01:00
import { ensureOpenClawCliOnPath } from "../infra/path-env.js" ;
2026-02-13 15:29:29 -08:00
import { setGatewaySigusr1RestartPolicy , setPreRestartDeferralCheck } from "../infra/restart.js" ;
2026-01-16 03:45:03 +00:00
import {
primeRemoteSkillsCache ,
refreshRemoteBinsForConnectedNodes ,
2026-01-19 04:50:07 +00:00
setSkillsRemoteRegistry ,
2026-01-16 03:45:03 +00:00
} from "../infra/skills-remote.js" ;
2026-02-22 14:37:20 -08:00
import { enqueueSystemEvent } from "../infra/system-events.js" ;
2026-01-17 12:07:14 +00:00
import { scheduleGatewayUpdateCheck } from "../infra/update-startup.js" ;
2026-01-21 00:29:42 +00:00
import { startDiagnosticHeartbeat , stopDiagnosticHeartbeat } from "../logging/diagnostic.js" ;
2026-01-18 23:25:04 +00:00
import { createSubsystemLogger , runtimeForLogger } from "../logging/subsystem.js" ;
2026-02-14 17:33:08 -05:00
import { getGlobalHookRunner , runGlobalGatewayStopSafely } from "../plugins/hook-runner-global.js" ;
2026-02-15 19:05:00 +00:00
import { createEmptyPluginRegistry } from "../plugins/registry.js" ;
2026-02-24 21:51:41 +08:00
import { createPluginRuntime } from "../plugins/runtime/index.js" ;
2026-02-18 01:34:35 +00:00
import type { PluginServicesHandle } from "../plugins/services.js" ;
2026-02-13 15:29:29 -08:00
import { getTotalQueueSize } from "../process/command-queue.js" ;
2026-02-18 01:34:35 +00:00
import type { RuntimeEnv } from "../runtime.js" ;
2026-03-02 20:58:20 -06:00
import type { CommandSecretAssignment } from "../secrets/command-config.js" ;
import {
GATEWAY_AUTH_SURFACE_PATHS ,
evaluateGatewayAuthSurfaceStates ,
} from "../secrets/runtime-gateway-auth-surfaces.js" ;
2026-02-21 11:13:25 -08:00
import {
activateSecretsRuntimeSnapshot ,
clearSecretsRuntimeSnapshot ,
getActiveSecretsRuntimeSnapshot ,
prepareSecretsRuntimeSnapshot ,
2026-03-02 20:58:20 -06:00
resolveCommandSecretsFromActiveRuntimeSnapshot ,
2026-02-21 11:13:25 -08:00
} from "../secrets/runtime.js" ;
2026-01-14 01:08:15 +00:00
import { runOnboardingWizard } from "../wizard/onboarding.js" ;
2026-02-13 15:32:38 +01:00
import { createAuthRateLimiter , type AuthRateLimiter } from "./auth-rate-limit.js" ;
2026-02-12 11:47:26 +07:00
import { startChannelHealthMonitor } from "./channel-health-monitor.js" ;
2026-01-14 09:11:21 +00:00
import { startGatewayConfigReloader } from "./config-reload.js" ;
2026-02-18 01:34:35 +00:00
import type { ControlUiRootState } from "./control-ui.js" ;
2026-02-19 10:00:27 +01:00
import {
GATEWAY_EVENT_UPDATE_AVAILABLE ,
type GatewayUpdateAvailableEventPayload ,
} from "./events.js" ;
2026-01-19 02:31:18 +00:00
import { ExecApprovalManager } from "./exec-approval-manager.js" ;
2026-02-01 10:03:47 +09:00
import { NodeRegistry } from "./node-registry.js" ;
2026-02-18 01:34:35 +00:00
import type { startBrowserControlServerIfEnabled } from "./server-browser.js" ;
2026-01-14 01:08:15 +00:00
import { createChannelManager } from "./server-channels.js" ;
2026-01-14 09:11:21 +00:00
import { createAgentEventHandler } from "./server-chat.js" ;
import { createGatewayCloseHandler } from "./server-close.js" ;
import { buildGatewayCronService } from "./server-cron.js" ;
2026-02-01 10:03:47 +09:00
import { startGatewayDiscovery } from "./server-discovery-runtime.js" ;
2026-01-14 09:11:21 +00:00
import { applyGatewayLaneConcurrency } from "./server-lanes.js" ;
import { startGatewayMaintenanceTimers } from "./server-maintenance.js" ;
2026-01-15 02:42:41 +00:00
import { GATEWAY_EVENTS , listGatewayMethods } from "./server-methods-list.js" ;
2026-02-01 10:03:47 +09:00
import { coreGatewayHandlers } from "./server-methods.js" ;
import { createExecApprovalHandlers } from "./server-methods/exec-approval.js" ;
import { safeParseJson } from "./server-methods/nodes.helpers.js" ;
2026-02-21 13:57:49 -08:00
import { createSecretsHandlers } from "./server-methods/secrets.js" ;
2026-02-01 10:03:47 +09:00
import { hasConnectedMobileNode } from "./server-mobile-nodes.js" ;
2026-01-14 09:11:21 +00:00
import { loadGatewayModelCatalog } from "./server-model-catalog.js" ;
2026-01-19 04:50:07 +00:00
import { createNodeSubscriptionManager } from "./server-node-subscriptions.js" ;
2026-01-14 09:11:21 +00:00
import { loadGatewayPlugins } from "./server-plugins.js" ;
import { createGatewayReloadHandlers } from "./server-reload-handlers.js" ;
import { resolveGatewayRuntimeConfig } from "./server-runtime-config.js" ;
import { createGatewayRuntimeState } from "./server-runtime-state.js" ;
import { resolveSessionKeyForRun } from "./server-session-key.js" ;
import { logGatewayStartup } from "./server-startup-log.js" ;
2026-02-01 10:03:47 +09:00
import { startGatewaySidecars } from "./server-startup.js" ;
2026-01-14 09:11:21 +00:00
import { startGatewayTailscaleExposure } from "./server-tailscale.js" ;
import { createWizardSessionTracker } from "./server-wizard-sessions.js" ;
import { attachGatewayWsHandlers } from "./server-ws-runtime.js" ;
2026-02-01 10:03:47 +09:00
import {
getHealthCache ,
getHealthVersion ,
getPresenceVersion ,
incrementPresenceVersion ,
refreshGatewayHealthSnapshot ,
} from "./server/health-state.js" ;
import { loadGatewayTlsRuntime } from "./server/tls.js" ;
2026-02-19 02:35:50 -05:00
import { ensureGatewayStartupAuth } from "./startup-auth.js" ;
2026-03-02 00:42:15 +00:00
import { maybeSeedControlUiAllowedOriginsAtStartup } from "./startup-control-ui-origins.js" ;
2026-01-14 09:11:21 +00:00
export { __resetModelCatalogCacheForTest } from "./server-model-catalog.js" ;
2026-01-14 01:08:15 +00:00
2026-01-30 03:15:10 +01:00
ensureOpenClawCliOnPath ( ) ;
2026-01-14 01:08:15 +00:00
const log = createSubsystemLogger ( "gateway" ) ;
const logCanvas = log . child ( "canvas" ) ;
const logDiscovery = log . child ( "discovery" ) ;
const logTailscale = log . child ( "tailscale" ) ;
const logChannels = log . child ( "channels" ) ;
const logBrowser = log . child ( "browser" ) ;
const logHealth = log . child ( "health" ) ;
const logCron = log . child ( "cron" ) ;
const logReload = log . child ( "reload" ) ;
const logHooks = log . child ( "hooks" ) ;
2026-01-15 05:03:50 +00:00
const logPlugins = log . child ( "plugins" ) ;
2026-01-14 01:08:15 +00:00
const logWsControl = log . child ( "ws" ) ;
2026-02-21 11:13:25 -08:00
const logSecrets = log . child ( "secrets" ) ;
2026-02-03 13:56:20 -05:00
const gatewayRuntime = runtimeForLogger ( log ) ;
2026-01-14 01:08:15 +00:00
const canvasRuntime = runtimeForLogger ( logCanvas ) ;
2026-02-26 01:36:52 +01:00
type AuthRateLimitConfig = Parameters < typeof createAuthRateLimiter > [ 0 ] ;
function createGatewayAuthRateLimiters ( rateLimitConfig : AuthRateLimitConfig | undefined ) : {
rateLimiter? : AuthRateLimiter ;
browserRateLimiter : AuthRateLimiter ;
} {
const rateLimiter = rateLimitConfig ? createAuthRateLimiter ( rateLimitConfig ) : undefined ;
// Browser-origin WS auth attempts always use loopback-non-exempt throttling.
const browserRateLimiter = createAuthRateLimiter ( {
. . . rateLimitConfig ,
exemptLoopback : false ,
} ) ;
return { rateLimiter , browserRateLimiter } ;
}
2026-03-02 20:58:20 -06:00
function logGatewayAuthSurfaceDiagnostics ( prepared : {
sourceConfig : OpenClawConfig ;
warnings : Array < { code : string ; path : string ; message : string } > ;
} ) : void {
const states = evaluateGatewayAuthSurfaceStates ( {
config : prepared.sourceConfig ,
defaults : prepared.sourceConfig.secrets?.defaults ,
env : process.env ,
} ) ;
const inactiveWarnings = new Map < string , string > ( ) ;
for ( const warning of prepared . warnings ) {
if ( warning . code !== "SECRETS_REF_IGNORED_INACTIVE_SURFACE" ) {
continue ;
}
inactiveWarnings . set ( warning . path , warning . message ) ;
}
for ( const path of GATEWAY_AUTH_SURFACE_PATHS ) {
const state = states [ path ] ;
if ( ! state . hasSecretRef ) {
continue ;
}
const stateLabel = state . active ? "active" : "inactive" ;
const inactiveDetails =
! state . active && inactiveWarnings . get ( path ) ? inactiveWarnings . get ( path ) : undefined ;
const details = inactiveDetails ? ? state . reason ;
logSecrets . info ( ` [SECRETS_GATEWAY_AUTH_SURFACE] ${ path } is ${ stateLabel } . ${ details } ` ) ;
}
}
2026-01-14 01:08:15 +00:00
export type GatewayServer = {
2026-01-14 14:31:43 +00:00
close : ( opts ? : { reason? : string ; restartExpectedMs? : number | null } ) = > Promise < void > ;
2026-01-14 01:08:15 +00:00
} ;
export type GatewayServerOptions = {
/ * *
* Bind address policy for the Gateway WebSocket / HTTP server .
* - loopback : 127.0.0.1
* - lan : 0.0.0.0
* - tailnet : bind only to the Tailscale IPv4 address ( 100.64 . 0.0 / 10 )
2026-01-21 20:35:39 +00:00
* - auto : prefer loopback , else LAN
2026-01-14 01:08:15 +00:00
* /
2026-01-19 04:50:07 +00:00
bind? : import ( "../config/config.js" ) . GatewayBindMode ;
2026-01-14 01:08:15 +00:00
/ * *
* Advanced override for the bind host , bypassing bind resolution .
* Prefer ` bind ` unless you really need a specific address .
* /
host? : string ;
/ * *
* If false , do not serve the browser Control UI .
* Default : config ` gateway.controlUi.enabled ` ( or true when absent ) .
* /
controlUiEnabled? : boolean ;
/ * *
* If false , do not serve ` POST /v1/chat/completions ` .
* Default : config ` gateway.http.endpoints.chatCompletions.enabled ` ( or false when absent ) .
* /
openAiChatCompletionsEnabled? : boolean ;
2026-01-19 10:44:48 +01:00
/ * *
* If false , do not serve ` POST /v1/responses ` ( OpenResponses API ) .
* Default : config ` gateway.http.endpoints.responses.enabled ` ( or false when absent ) .
* /
openResponsesEnabled? : boolean ;
2026-01-14 01:08:15 +00:00
/ * *
* Override gateway auth configuration ( merges with config ) .
* /
auth? : import ( "../config/config.js" ) . GatewayAuthConfig ;
/ * *
* Override gateway Tailscale exposure configuration ( merges with config ) .
* /
tailscale? : import ( "../config/config.js" ) . GatewayTailscaleConfig ;
/ * *
* Test - only : allow canvas host startup even when NODE_ENV / VITEST would disable it .
* /
allowCanvasHostInTests? : boolean ;
/ * *
* Test - only : override the onboarding wizard runner .
* /
wizardRunner ? : (
opts : import ( "../commands/onboard-types.js" ) . OnboardOptions ,
runtime : import ( "../runtime.js" ) . RuntimeEnv ,
prompter : import ( "../wizard/prompts.js" ) . WizardPrompter ,
) = > Promise < void > ;
} ;
export async function startGatewayServer (
port = 18789 ,
opts : GatewayServerOptions = { } ,
) : Promise < GatewayServer > {
2026-02-14 05:09:07 +00:00
const minimalTestGateway =
process . env . VITEST === "1" && process . env . OPENCLAW_TEST_MINIMAL_GATEWAY === "1" ;
2026-01-19 04:50:07 +00:00
// Ensure all default port derivations (browser/canvas) see the actual runtime port.
2026-01-30 03:15:10 +01:00
process . env . OPENCLAW_GATEWAY_PORT = String ( port ) ;
2026-01-25 10:22:47 +00:00
logAcceptedEnvOption ( {
2026-01-30 03:15:10 +01:00
key : "OPENCLAW_RAW_STREAM" ,
2026-01-25 10:22:47 +00:00
description : "raw stream logging enabled" ,
} ) ;
logAcceptedEnvOption ( {
2026-01-30 03:15:10 +01:00
key : "OPENCLAW_RAW_STREAM_PATH" ,
2026-01-25 10:22:47 +00:00
description : "raw stream log path override" ,
} ) ;
2026-01-14 01:08:15 +00:00
2026-01-17 10:25:24 +00:00
let configSnapshot = await readConfigFileSnapshot ( ) ;
2026-01-14 01:08:15 +00:00
if ( configSnapshot . legacyIssues . length > 0 ) {
if ( isNixMode ) {
throw new Error (
"Legacy config entries detected while running in Nix mode. Update your Nix config to the latest schema and restart." ,
) ;
}
2026-01-14 14:31:43 +00:00
const { config : migrated , changes } = migrateLegacyConfig ( configSnapshot . parsed ) ;
2026-01-14 01:08:15 +00:00
if ( ! migrated ) {
throw new Error (
2026-01-30 03:15:10 +01:00
` Legacy config entries detected but auto-migration failed. Run " ${ formatCliCommand ( "openclaw doctor" ) } " to migrate. ` ,
2026-01-14 01:08:15 +00:00
) ;
}
await writeConfigFile ( migrated ) ;
if ( changes . length > 0 ) {
log . info (
` gateway: migrated legacy config entries: \ n ${ changes
. map ( ( entry ) = > ` - ${ entry } ` )
. join ( "\n" ) } ` ,
) ;
}
}
2026-01-17 10:25:24 +00:00
configSnapshot = await readConfigFileSnapshot ( ) ;
if ( configSnapshot . exists && ! configSnapshot . valid ) {
const issues =
configSnapshot . issues . length > 0
2026-03-02 20:05:12 -05:00
? formatConfigIssueLines ( configSnapshot . issues , "" , { normalizeRoot : true } ) . join ( "\n" )
2026-01-17 10:25:24 +00:00
: "Unknown validation issue." ;
throw new Error (
2026-01-30 03:15:10 +01:00
` Invalid config at ${ configSnapshot . path } . \ n ${ issues } \ nRun " ${ formatCliCommand ( "openclaw doctor" ) } " to repair, then retry. ` ,
2026-01-17 10:25:24 +00:00
) ;
}
2026-01-20 16:37:34 +00:00
const autoEnable = applyPluginAutoEnable ( { config : configSnapshot.config , env : process.env } ) ;
if ( autoEnable . changes . length > 0 ) {
try {
await writeConfigFile ( autoEnable . config ) ;
log . info (
` gateway: auto-enabled plugins: \ n ${ autoEnable . changes
. map ( ( entry ) = > ` - ${ entry } ` )
. join ( "\n" ) } ` ,
) ;
} catch ( err ) {
log . warn ( ` gateway: failed to persist plugin auto-enable changes: ${ String ( err ) } ` ) ;
}
}
2026-02-21 11:13:25 -08:00
let secretsDegraded = false ;
2026-02-22 14:37:20 -08:00
const emitSecretsStateEvent = (
code : "SECRETS_RELOADER_DEGRADED" | "SECRETS_RELOADER_RECOVERED" ,
message : string ,
cfg : OpenClawConfig ,
) = > {
enqueueSystemEvent ( ` [ ${ code } ] ${ message } ` , {
sessionKey : resolveMainSessionKey ( cfg ) ,
contextKey : code ,
} ) ;
} ;
2026-02-22 14:41:26 -08:00
let secretsActivationTail : Promise < void > = Promise . resolve ( ) ;
const runWithSecretsActivationLock = async < T > ( operation : ( ) = > Promise < T > ) : Promise < T > = > {
const run = secretsActivationTail . then ( operation , operation ) ;
secretsActivationTail = run . then (
( ) = > undefined ,
( ) = > undefined ,
) ;
return await run ;
} ;
2026-02-21 11:13:25 -08:00
const activateRuntimeSecrets = async (
config : OpenClawConfig ,
params : { reason : "startup" | "reload" | "restart-check" ; activate : boolean } ,
2026-02-22 14:41:26 -08:00
) = >
await runWithSecretsActivationLock ( async ( ) = > {
try {
const prepared = await prepareSecretsRuntimeSnapshot ( { config } ) ;
if ( params . activate ) {
activateSecretsRuntimeSnapshot ( prepared ) ;
2026-03-02 20:58:20 -06:00
logGatewayAuthSurfaceDiagnostics ( prepared ) ;
2026-02-22 14:37:20 -08:00
}
2026-02-22 14:41:26 -08:00
for ( const warning of prepared . warnings ) {
logSecrets . warn ( ` [ ${ warning . code } ] ${ warning . message } ` ) ;
}
if ( secretsDegraded ) {
const recoveredMessage =
"Secret resolution recovered; runtime remained on last-known-good during the outage." ;
logSecrets . info ( ` [SECRETS_RELOADER_RECOVERED] ${ recoveredMessage } ` ) ;
emitSecretsStateEvent ( "SECRETS_RELOADER_RECOVERED" , recoveredMessage , prepared . config ) ;
}
secretsDegraded = false ;
return prepared ;
} catch ( err ) {
const details = String ( err ) ;
if ( ! secretsDegraded ) {
logSecrets . error ( ` [SECRETS_RELOADER_DEGRADED] ${ details } ` ) ;
if ( params . reason !== "startup" ) {
emitSecretsStateEvent (
"SECRETS_RELOADER_DEGRADED" ,
` Secret resolution failed; runtime remains on last-known-good snapshot. ${ details } ` ,
config ,
) ;
}
} else {
logSecrets . warn ( ` [SECRETS_RELOADER_DEGRADED] ${ details } ` ) ;
}
secretsDegraded = true ;
if ( params . reason === "startup" ) {
throw new Error ( ` Startup failed: required secrets are unavailable. ${ details } ` , {
cause : err ,
} ) ;
}
throw err ;
2026-02-21 11:13:25 -08:00
}
2026-02-22 14:41:26 -08:00
} ) ;
2026-02-21 11:13:25 -08:00
// Fail fast before startup if required refs are unresolved.
let cfgAtStart : OpenClawConfig ;
{
const freshSnapshot = await readConfigFileSnapshot ( ) ;
if ( ! freshSnapshot . valid ) {
const issues =
freshSnapshot . issues . length > 0
2026-03-02 20:05:12 -05:00
? formatConfigIssueLines ( freshSnapshot . issues , "" , { normalizeRoot : true } ) . join ( "\n" )
2026-02-21 11:13:25 -08:00
: "Unknown validation issue." ;
throw new Error ( ` Invalid config at ${ freshSnapshot . path } . \ n ${ issues } ` ) ;
}
2026-02-21 13:23:29 -08:00
await activateRuntimeSecrets ( freshSnapshot . config , {
2026-02-21 11:13:25 -08:00
reason : "startup" ,
2026-02-21 13:23:29 -08:00
activate : false ,
2026-02-21 11:13:25 -08:00
} ) ;
}
cfgAtStart = loadConfig ( ) ;
2026-02-19 02:35:50 -05:00
const authBootstrap = await ensureGatewayStartupAuth ( {
cfg : cfgAtStart ,
env : process.env ,
authOverride : opts.auth ,
tailscaleOverride : opts.tailscale ,
persist : true ,
} ) ;
cfgAtStart = authBootstrap . cfg ;
if ( authBootstrap . generatedToken ) {
if ( authBootstrap . persistedGeneratedToken ) {
log . info (
"Gateway auth token was missing. Generated a new token and saved it to config (gateway.auth.token)." ,
) ;
} else {
log . warn (
"Gateway auth token was missing. Generated a runtime token for this startup without changing config; restart will generate a different token. Persist one with `openclaw config set gateway.auth.mode token` and `openclaw config set gateway.auth.token <token>`." ,
) ;
}
}
2026-02-21 11:13:25 -08:00
cfgAtStart = (
await activateRuntimeSecrets ( cfgAtStart , {
reason : "startup" ,
activate : true ,
} )
) . config ;
2026-01-21 00:29:42 +00:00
const diagnosticsEnabled = isDiagnosticsEnabled ( cfgAtStart ) ;
if ( diagnosticsEnabled ) {
2026-03-02 00:32:21 +00:00
startDiagnosticHeartbeat ( ) ;
2026-01-21 00:29:42 +00:00
}
2026-02-19 10:00:27 +01:00
setGatewaySigusr1RestartPolicy ( { allowExternal : isRestartEnabled ( cfgAtStart ) } ) ;
2026-02-13 15:29:29 -08:00
setPreRestartDeferralCheck (
( ) = > getTotalQueueSize ( ) + getTotalPendingReplies ( ) + getActiveEmbeddedRunCount ( ) ,
) ;
2026-03-02 00:05:48 +00:00
// Unconditional startup migration: seed gateway.controlUi.allowedOrigins for existing
2026-03-02 00:42:15 +00:00
// non-loopback installs that upgraded to v2026.2.26+ without required origins.
cfgAtStart = await maybeSeedControlUiAllowedOriginsAtStartup ( {
config : cfgAtStart ,
writeConfig : writeConfigFile ,
log ,
} ) ;
2026-03-02 00:05:48 +00:00
2026-01-14 01:08:15 +00:00
initSubagentRegistry ( ) ;
const defaultAgentId = resolveDefaultAgentId ( cfgAtStart ) ;
2026-01-14 14:31:43 +00:00
const defaultWorkspaceDir = resolveAgentWorkspaceDir ( cfgAtStart , defaultAgentId ) ;
2026-01-15 02:42:41 +00:00
const baseMethods = listGatewayMethods ( ) ;
2026-02-15 19:05:00 +00:00
const emptyPluginRegistry = createEmptyPluginRegistry ( ) ;
2026-02-14 05:09:07 +00:00
const { pluginRegistry , gatewayMethods : baseGatewayMethods } = minimalTestGateway
? { pluginRegistry : emptyPluginRegistry , gatewayMethods : baseMethods }
: loadGatewayPlugins ( {
cfg : cfgAtStart ,
workspaceDir : defaultWorkspaceDir ,
log ,
coreGatewayHandlers ,
baseMethods ,
} ) ;
2026-01-15 02:42:41 +00:00
const channelLogs = Object . fromEntries (
listChannelPlugins ( ) . map ( ( plugin ) = > [ plugin . id , logChannels . child ( plugin . id ) ] ) ,
) as Record < ChannelId , ReturnType < typeof createSubsystemLogger > > ;
const channelRuntimeEnvs = Object . fromEntries (
Object . entries ( channelLogs ) . map ( ( [ id , logger ] ) = > [ id , runtimeForLogger ( logger ) ] ) ,
) as Record < ChannelId , RuntimeEnv > ;
const channelMethods = listChannelPlugins ( ) . flatMap ( ( plugin ) = > plugin . gatewayMethods ? ? [ ] ) ;
const gatewayMethods = Array . from ( new Set ( [ . . . baseGatewayMethods , . . . channelMethods ] ) ) ;
2026-01-14 01:08:15 +00:00
let pluginServices : PluginServicesHandle | null = null ;
2026-01-14 09:11:21 +00:00
const runtimeConfig = await resolveGatewayRuntimeConfig ( {
cfg : cfgAtStart ,
port ,
bind : opts.bind ,
host : opts.host ,
controlUiEnabled : opts.controlUiEnabled ,
openAiChatCompletionsEnabled : opts.openAiChatCompletionsEnabled ,
2026-01-19 10:44:48 +01:00
openResponsesEnabled : opts.openResponsesEnabled ,
2026-01-14 09:11:21 +00:00
auth : opts.auth ,
tailscale : opts.tailscale ,
2026-01-14 01:08:15 +00:00
} ) ;
2026-01-14 09:11:21 +00:00
const {
bindHost ,
controlUiEnabled ,
openAiChatCompletionsEnabled ,
2026-01-19 10:44:48 +01:00
openResponsesEnabled ,
2026-01-20 07:35:29 +00:00
openResponsesConfig ,
2026-02-23 19:47:09 +00:00
strictTransportSecurityHeader ,
2026-01-14 09:11:21 +00:00
controlUiBasePath ,
2026-02-03 13:56:20 -05:00
controlUiRoot : controlUiRootOverride ,
2026-01-14 09:11:21 +00:00
resolvedAuth ,
tailscaleConfig ,
tailscaleMode ,
} = runtimeConfig ;
let hooksConfig = runtimeConfig . hooksConfig ;
const canvasHostEnabled = runtimeConfig . canvasHostEnabled ;
2026-01-14 01:08:15 +00:00
2026-02-26 01:36:52 +01:00
// Create auth rate limiters used by connect/auth flows.
2026-02-13 15:32:38 +01:00
const rateLimitConfig = cfgAtStart . gateway ? . auth ? . rateLimit ;
2026-02-26 01:36:52 +01:00
const { rateLimiter : authRateLimiter , browserRateLimiter : browserAuthRateLimiter } =
createGatewayAuthRateLimiters ( rateLimitConfig ) ;
2026-02-13 15:32:38 +01:00
2026-02-03 13:56:20 -05:00
let controlUiRootState : ControlUiRootState | undefined ;
if ( controlUiRootOverride ) {
const resolvedOverride = resolveControlUiRootOverrideSync ( controlUiRootOverride ) ;
const resolvedOverridePath = path . resolve ( controlUiRootOverride ) ;
controlUiRootState = resolvedOverride
? { kind : "resolved" , path : resolvedOverride }
: { kind : "invalid" , path : resolvedOverridePath } ;
if ( ! resolvedOverride ) {
log . warn ( ` gateway: controlUi.root not found at ${ resolvedOverridePath } ` ) ;
}
} else if ( controlUiEnabled ) {
let resolvedRoot = resolveControlUiRootSync ( {
moduleUrl : import.meta.url ,
argv1 : process.argv [ 1 ] ,
cwd : process.cwd ( ) ,
} ) ;
if ( ! resolvedRoot ) {
const ensureResult = await ensureControlUiAssetsBuilt ( gatewayRuntime ) ;
if ( ! ensureResult . ok && ensureResult . message ) {
log . warn ( ` gateway: ${ ensureResult . message } ` ) ;
}
resolvedRoot = resolveControlUiRootSync ( {
moduleUrl : import.meta.url ,
argv1 : process.argv [ 1 ] ,
cwd : process.cwd ( ) ,
} ) ;
}
controlUiRootState = resolvedRoot
? { kind : "resolved" , path : resolvedRoot }
: { kind : "missing" } ;
}
2026-01-14 01:08:15 +00:00
const wizardRunner = opts . wizardRunner ? ? runOnboardingWizard ;
2026-01-14 14:31:43 +00:00
const { wizardSessions , findRunningWizard , purgeWizardSession } = createWizardSessionTracker ( ) ;
2026-01-14 01:08:15 +00:00
const deps = createDefaultDeps ( ) ;
let canvasHostServer : CanvasHostServer | null = null ;
2026-01-19 02:46:07 +00:00
const gatewayTls = await loadGatewayTlsRuntime ( cfgAtStart . gateway ? . tls , log . child ( "tls" ) ) ;
if ( cfgAtStart . gateway ? . tls ? . enabled && ! gatewayTls . enabled ) {
throw new Error ( gatewayTls . error ? ? "gateway tls: failed to enable" ) ;
}
2026-01-14 09:11:21 +00:00
const {
canvasHost ,
httpServer ,
2026-01-25 05:48:40 +00:00
httpServers ,
httpBindHosts ,
2026-01-14 09:11:21 +00:00
wss ,
clients ,
broadcast ,
2026-02-04 17:12:16 -05:00
broadcastToConnIds ,
2026-01-14 09:11:21 +00:00
agentRunSeq ,
dedupe ,
chatRunState ,
chatRunBuffers ,
chatDeltaSentAt ,
addChatRun ,
removeChatRun ,
chatAbortControllers ,
2026-02-04 17:12:16 -05:00
toolEventRecipients ,
2026-01-14 09:11:21 +00:00
} = await createGatewayRuntimeState ( {
cfg : cfgAtStart ,
2026-01-14 01:08:15 +00:00
bindHost ,
port ,
controlUiEnabled ,
controlUiBasePath ,
2026-02-03 13:56:20 -05:00
controlUiRoot : controlUiRootState ,
2026-01-14 01:08:15 +00:00
openAiChatCompletionsEnabled ,
2026-01-19 10:44:48 +01:00
openResponsesEnabled ,
2026-01-20 07:35:29 +00:00
openResponsesConfig ,
2026-02-23 19:47:09 +00:00
strictTransportSecurityHeader ,
2026-01-14 01:08:15 +00:00
resolvedAuth ,
2026-02-13 15:32:38 +01:00
rateLimiter : authRateLimiter ,
2026-01-19 02:46:07 +00:00
gatewayTls ,
2026-01-14 09:11:21 +00:00
hooksConfig : ( ) = > hooksConfig ,
2026-01-15 05:03:50 +00:00
pluginRegistry ,
2026-01-14 09:11:21 +00:00
deps ,
canvasRuntime ,
canvasHostEnabled ,
allowCanvasHostInTests : opts.allowCanvasHostInTests ,
logCanvas ,
2026-01-25 05:48:40 +00:00
log ,
2026-01-14 09:11:21 +00:00
logHooks ,
2026-01-15 05:03:50 +00:00
logPlugins ,
2026-01-14 01:08:15 +00:00
} ) ;
let bonjourStop : ( ( ) = > Promise < void > ) | null = null ;
2026-01-19 04:50:07 +00:00
const nodeRegistry = new NodeRegistry ( ) ;
const nodePresenceTimers = new Map < string , ReturnType < typeof setInterval > > ( ) ;
const nodeSubscriptions = createNodeSubscriptionManager ( ) ;
const nodeSendEvent = ( opts : { nodeId : string ; event : string ; payloadJSON? : string | null } ) = > {
const payload = safeParseJson ( opts . payloadJSON ? ? null ) ;
nodeRegistry . sendEvent ( opts . nodeId , opts . event , payload ) ;
} ;
const nodeSendToSession = ( sessionKey : string , event : string , payload : unknown ) = >
nodeSubscriptions . sendToSession ( sessionKey , event , payload , nodeSendEvent ) ;
const nodeSendToAllSubscribed = ( event : string , payload : unknown ) = >
nodeSubscriptions . sendToAllSubscribed ( event , payload , nodeSendEvent ) ;
const nodeSubscribe = nodeSubscriptions . subscribe ;
const nodeUnsubscribe = nodeSubscriptions . unsubscribe ;
const nodeUnsubscribeAll = nodeSubscriptions . unsubscribeAll ;
const broadcastVoiceWakeChanged = ( triggers : string [ ] ) = > {
broadcast ( "voicewake.changed" , { triggers } , { dropIfSlow : true } ) ;
} ;
const hasMobileNodeConnected = ( ) = > hasConnectedMobileNode ( nodeRegistry ) ;
2026-01-14 09:11:21 +00:00
applyGatewayLaneConcurrency ( cfgAtStart ) ;
2026-01-14 01:08:15 +00:00
2026-01-14 09:11:21 +00:00
let cronState = buildGatewayCronService ( {
cfg : cfgAtStart ,
deps ,
broadcast ,
2026-01-14 01:08:15 +00:00
} ) ;
2026-01-14 09:11:21 +00:00
let { cron , storePath : cronStorePath } = cronState ;
2026-01-14 01:08:15 +00:00
const channelManager = createChannelManager ( {
loadConfig ,
channelLogs ,
channelRuntimeEnvs ,
2026-02-24 21:51:41 +08:00
channelRuntime : createPluginRuntime ( ) . channel ,
2026-01-14 01:08:15 +00:00
} ) ;
2026-01-14 14:31:43 +00:00
const { getRuntimeSnapshot , startChannels , startChannel , stopChannel , markChannelLoggedOut } =
channelManager ;
2026-01-14 01:08:15 +00:00
2026-02-14 05:09:07 +00:00
if ( ! minimalTestGateway ) {
const machineDisplayName = await getMachineDisplayName ( ) ;
const discovery = await startGatewayDiscovery ( {
machineDisplayName ,
port ,
gatewayTls : gatewayTls.enabled
? { enabled : true , fingerprintSha256 : gatewayTls.fingerprintSha256 }
: undefined ,
wideAreaDiscoveryEnabled : cfgAtStart.discovery?.wideArea?.enabled === true ,
wideAreaDiscoveryDomain : cfgAtStart.discovery?.wideArea?.domain ,
tailscaleMode ,
mdnsMode : cfgAtStart.discovery?.mdns?.mode ,
logDiscovery ,
} ) ;
bonjourStop = discovery . bonjourStop ;
}
2026-01-14 09:11:21 +00:00
2026-02-14 05:09:07 +00:00
if ( ! minimalTestGateway ) {
setSkillsRemoteRegistry ( nodeRegistry ) ;
void primeRemoteSkillsCache ( ) ;
}
2026-01-24 21:05:41 +01:00
// Debounce skills-triggered node probes to avoid feedback loops and rapid-fire invokes.
// Skills changes can happen in bursts (e.g., file watcher events), and each probe
// takes time to complete. A 30-second delay ensures we batch changes together.
let skillsRefreshTimer : ReturnType < typeof setTimeout > | null = null ;
const skillsRefreshDelayMs = 30 _000 ;
2026-02-14 05:09:07 +00:00
const skillsChangeUnsub = minimalTestGateway
? ( ) = > { }
: registerSkillsChangeListener ( ( event ) = > {
if ( event . reason === "remote-node" ) {
return ;
}
if ( skillsRefreshTimer ) {
clearTimeout ( skillsRefreshTimer ) ;
}
skillsRefreshTimer = setTimeout ( ( ) = > {
skillsRefreshTimer = null ;
const latest = loadConfig ( ) ;
void refreshRemoteBinsForConnectedNodes ( latest ) ;
} , skillsRefreshDelayMs ) ;
} ) ;
2026-01-14 01:08:15 +00:00
2026-02-14 05:09:07 +00:00
const noopInterval = ( ) = > setInterval ( ( ) = > { } , 1 << 30 ) ;
let tickInterval = noopInterval ( ) ;
let healthInterval = noopInterval ( ) ;
let dedupeCleanup = noopInterval ( ) ;
if ( ! minimalTestGateway ) {
( { tickInterval , healthInterval , dedupeCleanup } = startGatewayMaintenanceTimers ( {
2026-01-14 01:08:15 +00:00
broadcast ,
2026-02-14 05:09:07 +00:00
nodeSendToAllSubscribed ,
getPresenceVersion ,
getHealthVersion ,
refreshGatewayHealthSnapshot ,
logHealth ,
dedupe ,
chatAbortControllers ,
2026-01-14 01:08:15 +00:00
chatRunState ,
2026-02-14 05:09:07 +00:00
chatRunBuffers ,
chatDeltaSentAt ,
removeChatRun ,
agentRunSeq ,
nodeSendToSession ,
} ) ) ;
}
2026-01-14 01:08:15 +00:00
2026-02-14 05:09:07 +00:00
const agentUnsub = minimalTestGateway
? null
: onAgentEvent (
createAgentEventHandler ( {
broadcast ,
broadcastToConnIds ,
nodeSendToSession ,
agentRunSeq ,
chatRunState ,
resolveSessionKeyForRun ,
clearAgentRunContext ,
toolEventRecipients ,
} ) ,
) ;
2026-01-14 01:08:15 +00:00
2026-02-14 05:09:07 +00:00
const heartbeatUnsub = minimalTestGateway
? null
: onHeartbeatEvent ( ( evt ) = > {
broadcast ( "heartbeat" , evt , { dropIfSlow : true } ) ;
} ) ;
let heartbeatRunner : HeartbeatRunner = minimalTestGateway
? {
stop : ( ) = > { } ,
updateConfig : ( ) = > { } ,
}
: startHeartbeatRunner ( { cfg : cfgAtStart } ) ;
2026-01-14 01:08:15 +00:00
2026-02-12 11:47:26 +07:00
const healthCheckMinutes = cfgAtStart . gateway ? . channelHealthCheckMinutes ;
const healthCheckDisabled = healthCheckMinutes === 0 ;
2026-03-02 12:47:55 -08:00
let channelHealthMonitor = healthCheckDisabled
2026-02-12 11:47:26 +07:00
? null
: startChannelHealthMonitor ( {
channelManager ,
checkIntervalMs : ( healthCheckMinutes ? ? 5 ) * 60 _000 ,
} ) ;
2026-02-14 05:09:07 +00:00
if ( ! minimalTestGateway ) {
void cron . start ( ) . catch ( ( err ) = > logCron . error ( ` failed to start: ${ String ( err ) } ` ) ) ;
}
2026-01-14 01:08:15 +00:00
2026-02-13 15:54:07 -06:00
// Recover pending outbound deliveries from previous crash/restart.
2026-02-14 05:09:07 +00:00
if ( ! minimalTestGateway ) {
void ( async ( ) = > {
const { recoverPendingDeliveries } = await import ( "../infra/outbound/delivery-queue.js" ) ;
const { deliverOutboundPayloads } = await import ( "../infra/outbound/deliver.js" ) ;
const logRecovery = log . child ( "delivery-recovery" ) ;
await recoverPendingDeliveries ( {
deliver : deliverOutboundPayloads ,
log : logRecovery ,
cfg : cfgAtStart ,
} ) ;
} ) ( ) . catch ( ( err ) = > log . error ( ` Delivery recovery failed: ${ String ( err ) } ` ) ) ;
}
2026-02-13 15:54:07 -06:00
2026-01-19 02:31:18 +00:00
const execApprovalManager = new ExecApprovalManager ( ) ;
2026-01-24 12:56:40 -08:00
const execApprovalForwarder = createExecApprovalForwarder ( ) ;
const execApprovalHandlers = createExecApprovalHandlers ( execApprovalManager , {
forwarder : execApprovalForwarder ,
} ) ;
2026-02-21 13:57:49 -08:00
const secretsHandlers = createSecretsHandlers ( {
reloadSecrets : async ( ) = > {
const active = getActiveSecretsRuntimeSnapshot ( ) ;
if ( ! active ) {
throw new Error ( "Secrets runtime snapshot is not active." ) ;
}
const prepared = await activateRuntimeSecrets ( active . sourceConfig , {
reason : "reload" ,
activate : true ,
} ) ;
return { warningCount : prepared.warnings.length } ;
} ,
2026-03-02 20:58:20 -06:00
resolveSecrets : async ( { commandName , targetIds } ) = > {
const { assignments , diagnostics , inactiveRefPaths } =
resolveCommandSecretsFromActiveRuntimeSnapshot ( {
commandName ,
targetIds : new Set ( targetIds ) ,
} ) ;
if ( assignments . length === 0 ) {
return { assignments : [ ] as CommandSecretAssignment [ ] , diagnostics , inactiveRefPaths } ;
}
return { assignments , diagnostics , inactiveRefPaths } ;
} ,
2026-02-21 13:57:49 -08:00
} ) ;
2026-01-19 02:31:18 +00:00
2026-01-19 06:22:01 +00:00
const canvasHostServerPort = ( canvasHostServer as CanvasHostServer | null ) ? . port ;
2026-01-14 09:11:21 +00:00
attachGatewayWsHandlers ( {
2026-01-14 01:08:15 +00:00
wss ,
clients ,
port ,
2026-01-19 04:50:07 +00:00
gatewayHost : bindHost ? ? undefined ,
2026-01-14 01:08:15 +00:00
canvasHostEnabled : Boolean ( canvasHost ) ,
2026-01-19 06:22:01 +00:00
canvasHostServerPort ,
2026-01-14 01:08:15 +00:00
resolvedAuth ,
2026-02-13 15:32:38 +01:00
rateLimiter : authRateLimiter ,
2026-02-26 01:22:28 +01:00
browserRateLimiter : browserAuthRateLimiter ,
2026-01-14 01:08:15 +00:00
gatewayMethods ,
2026-01-14 09:11:21 +00:00
events : GATEWAY_EVENTS ,
2026-01-14 01:08:15 +00:00
logGateway : log ,
logHealth ,
logWsControl ,
2026-01-19 02:31:18 +00:00
extraHandlers : {
. . . pluginRegistry . gatewayHandlers ,
. . . execApprovalHandlers ,
2026-02-21 13:57:49 -08:00
. . . secretsHandlers ,
2026-01-19 02:31:18 +00:00
} ,
2026-01-14 01:08:15 +00:00
broadcast ,
2026-01-14 09:11:21 +00:00
context : {
2026-01-14 01:08:15 +00:00
deps ,
cron ,
cronStorePath ,
2026-02-14 13:02:48 +01:00
execApprovalManager ,
2026-01-14 01:08:15 +00:00
loadGatewayModelCatalog ,
getHealthCache ,
refreshHealthSnapshot : refreshGatewayHealthSnapshot ,
logHealth ,
logGateway : log ,
incrementPresenceVersion ,
getHealthVersion ,
broadcast ,
2026-02-04 17:12:16 -05:00
broadcastToConnIds ,
2026-01-19 04:50:07 +00:00
nodeSendToSession ,
nodeSendToAllSubscribed ,
nodeSubscribe ,
nodeUnsubscribe ,
nodeUnsubscribeAll ,
hasConnectedMobileNode : hasMobileNodeConnected ,
2026-02-22 22:13:40 +01:00
hasExecApprovalClients : ( ) = > {
for ( const gatewayClient of clients ) {
const scopes = Array . isArray ( gatewayClient . connect . scopes )
? gatewayClient . connect . scopes
: [ ] ;
if ( scopes . includes ( "operator.admin" ) || scopes . includes ( "operator.approvals" ) ) {
return true ;
}
}
return false ;
} ,
2026-01-19 04:50:07 +00:00
nodeRegistry ,
2026-01-14 01:08:15 +00:00
agentRunSeq ,
chatAbortControllers ,
chatAbortedRuns : chatRunState.abortedRuns ,
2026-01-14 09:11:21 +00:00
chatRunBuffers : chatRunState.buffers ,
chatDeltaSentAt : chatRunState.deltaSentAt ,
2026-01-14 01:08:15 +00:00
addChatRun ,
removeChatRun ,
2026-02-04 17:12:16 -05:00
registerToolEventRecipient : toolEventRecipients.add ,
2026-01-14 01:08:15 +00:00
dedupe ,
wizardSessions ,
findRunningWizard ,
purgeWizardSession ,
getRuntimeSnapshot ,
startChannel ,
stopChannel ,
markChannelLoggedOut ,
wizardRunner ,
broadcastVoiceWakeChanged ,
2026-01-14 09:11:21 +00:00
} ,
2026-01-14 01:08:15 +00:00
} ) ;
2026-01-14 09:11:21 +00:00
logGatewayStartup ( {
cfg : cfgAtStart ,
bindHost ,
2026-01-25 05:48:40 +00:00
bindHosts : httpBindHosts ,
2026-01-14 09:11:21 +00:00
port ,
2026-01-19 02:46:07 +00:00
tlsEnabled : gatewayTls.enabled ,
2026-01-14 09:11:21 +00:00
log ,
isNixMode ,
} ) ;
2026-02-22 17:11:24 +01:00
const stopGatewayUpdateCheck = minimalTestGateway
? ( ) = > { }
: scheduleGatewayUpdateCheck ( {
cfg : cfgAtStart ,
log ,
isNixMode ,
onUpdateAvailableChange : ( updateAvailable ) = > {
const payload : GatewayUpdateAvailableEventPayload = { updateAvailable } ;
broadcast ( GATEWAY_EVENT_UPDATE_AVAILABLE , payload , { dropIfSlow : true } ) ;
} ,
} ) ;
2026-02-14 05:09:07 +00:00
const tailscaleCleanup = minimalTestGateway
? null
: await startGatewayTailscaleExposure ( {
tailscaleMode ,
resetOnExit : tailscaleConfig.resetOnExit ,
port ,
controlUiBasePath ,
logTailscale ,
} ) ;
2026-01-14 01:08:15 +00:00
2026-01-14 14:31:43 +00:00
let browserControl : Awaited < ReturnType < typeof startBrowserControlServerIfEnabled > > = null ;
2026-02-14 05:09:07 +00:00
if ( ! minimalTestGateway ) {
( { browserControl , pluginServices } = await startGatewaySidecars ( {
cfg : cfgAtStart ,
pluginRegistry ,
defaultWorkspaceDir ,
deps ,
startChannels ,
log ,
logHooks ,
logChannels ,
logBrowser ,
} ) ) ;
}
2026-01-14 01:08:15 +00:00
2026-02-13 00:14:14 +00:00
// Run gateway_start plugin hook (fire-and-forget)
2026-02-14 05:09:07 +00:00
if ( ! minimalTestGateway ) {
2026-02-13 00:14:14 +00:00
const hookRunner = getGlobalHookRunner ( ) ;
if ( hookRunner ? . hasHooks ( "gateway_start" ) ) {
void hookRunner . runGatewayStart ( { port } , { port } ) . catch ( ( err ) = > {
log . warn ( ` gateway_start hook failed: ${ String ( err ) } ` ) ;
} ) ;
}
}
2026-02-14 05:09:07 +00:00
const configReloader = minimalTestGateway
? { stop : async ( ) = > { } }
: ( ( ) = > {
const { applyHotReload , requestGatewayRestart } = createGatewayReloadHandlers ( {
deps ,
broadcast ,
getState : ( ) = > ( {
hooksConfig ,
heartbeatRunner ,
cronState ,
browserControl ,
2026-03-02 12:47:55 -08:00
channelHealthMonitor ,
2026-02-14 05:09:07 +00:00
} ) ,
setState : ( nextState ) = > {
hooksConfig = nextState . hooksConfig ;
heartbeatRunner = nextState . heartbeatRunner ;
cronState = nextState . cronState ;
cron = cronState . cron ;
cronStorePath = cronState . storePath ;
browserControl = nextState . browserControl ;
2026-03-02 12:47:55 -08:00
channelHealthMonitor = nextState . channelHealthMonitor ;
2026-02-14 05:09:07 +00:00
} ,
startChannel ,
stopChannel ,
logHooks ,
logBrowser ,
logChannels ,
logCron ,
logReload ,
2026-03-02 12:47:55 -08:00
createHealthMonitor : ( checkIntervalMs : number ) = >
startChannelHealthMonitor ( { channelManager , checkIntervalMs } ) ,
2026-02-14 05:09:07 +00:00
} ) ;
return startGatewayConfigReloader ( {
initialConfig : cfgAtStart ,
readSnapshot : readConfigFileSnapshot ,
2026-02-21 11:13:25 -08:00
onHotReload : async ( plan , nextConfig ) = > {
const previousSnapshot = getActiveSecretsRuntimeSnapshot ( ) ;
const prepared = await activateRuntimeSecrets ( nextConfig , {
reason : "reload" ,
activate : true ,
} ) ;
try {
await applyHotReload ( plan , prepared . config ) ;
} catch ( err ) {
if ( previousSnapshot ) {
activateSecretsRuntimeSnapshot ( previousSnapshot ) ;
} else {
clearSecretsRuntimeSnapshot ( ) ;
}
throw err ;
}
} ,
onRestart : async ( plan , nextConfig ) = > {
await activateRuntimeSecrets ( nextConfig , { reason : "restart-check" , activate : false } ) ;
requestGatewayRestart ( plan , nextConfig ) ;
} ,
2026-02-14 05:09:07 +00:00
log : {
info : ( msg ) = > logReload . info ( msg ) ,
warn : ( msg ) = > logReload . warn ( msg ) ,
error : ( msg ) = > logReload . error ( msg ) ,
} ,
watchPath : CONFIG_PATH ,
} ) ;
} ) ( ) ;
2026-01-14 01:08:15 +00:00
2026-01-14 09:11:21 +00:00
const close = createGatewayCloseHandler ( {
bonjourStop ,
tailscaleCleanup ,
canvasHost ,
canvasHostServer ,
stopChannel ,
pluginServices ,
cron ,
heartbeatRunner ,
2026-02-22 17:11:24 +01:00
updateCheckStop : stopGatewayUpdateCheck ,
2026-01-14 09:11:21 +00:00
nodePresenceTimers ,
broadcast ,
tickInterval ,
healthInterval ,
dedupeCleanup ,
agentUnsub ,
heartbeatUnsub ,
chatRunState ,
clients ,
configReloader ,
browserControl ,
wss ,
httpServer ,
2026-01-25 05:48:40 +00:00
httpServers ,
2026-01-14 09:11:21 +00:00
} ) ;
2026-01-21 00:29:42 +00:00
return {
close : async ( opts ) = > {
2026-02-13 00:14:14 +00:00
// Run gateway_stop plugin hook before shutdown
2026-02-14 17:33:08 -05:00
await runGlobalGatewayStopSafely ( {
event : { reason : opts?.reason ? ? "gateway stopping" } ,
ctx : { port } ,
onError : ( err ) = > log . warn ( ` gateway_stop hook failed: ${ String ( err ) } ` ) ,
} ) ;
2026-01-21 00:29:42 +00:00
if ( diagnosticsEnabled ) {
stopDiagnosticHeartbeat ( ) ;
}
2026-01-24 21:05:41 +01:00
if ( skillsRefreshTimer ) {
clearTimeout ( skillsRefreshTimer ) ;
skillsRefreshTimer = null ;
}
skillsChangeUnsub ( ) ;
2026-02-13 15:32:38 +01:00
authRateLimiter ? . dispose ( ) ;
2026-02-26 01:22:28 +01:00
browserAuthRateLimiter . dispose ( ) ;
2026-02-12 11:47:26 +07:00
channelHealthMonitor ? . stop ( ) ;
2026-02-21 11:13:25 -08:00
clearSecretsRuntimeSnapshot ( ) ;
2026-01-21 00:29:42 +00:00
await close ( opts ) ;
} ,
} ;
2026-01-14 01:08:15 +00:00
}