| |
| import fs from "node:fs"; |
| import { expectDefined } from "@openclaw/normalization-core"; |
| import { parseStrictPositiveInteger } from "@openclaw/normalization-core/number-coercion"; |
| import { |
| normalizeOptionalLowercaseString, |
| normalizeOptionalString, |
| } from "@openclaw/normalization-core/string-coerce"; |
| import { triageAfterFailure } from "../../commands/triage-failure.js"; |
| import type { |
| ConfigFileSnapshot, |
| GatewayAuthMode, |
| GatewayBindMode, |
| GatewayTailscaleMode, |
| ReadConfigFileSnapshotWithPluginMetadataResult, |
| } from "../../config/config.js"; |
| import { ALLOW_OLDER_BINARY_DESTRUCTIVE_ACTIONS_ENV } from "../../config/future-version-guard.js"; |
| import { |
| isDoctorRecoverableInvalidConfigError, |
| isInvalidConfigError, |
| } from "../../config/io.invalid-config.js"; |
| import { CONFIG_PATH, normalizeStateDirEnv, resolveGatewayPort } from "../../config/paths.js"; |
| import type { OpenClawConfig } from "../../config/types.openclaw.js"; |
| import { hasConfiguredSecretInput } from "../../config/types.secrets.js"; |
| import { GATEWAY_SERVICE_RUNTIME_PID_ENV } from "../../daemon/constants.js"; |
| import { |
| createConfiguredGatewayLocalProbe, |
| normalizeGatewayHttpProbeHost, |
| requestGatewayLocalHttpProbe, |
| } from "../../gateway/local-http-probe.js"; |
| import { |
| defaultGatewayBindMode, |
| isContainerEnvironment, |
| isLoopbackHost, |
| resolveGatewayBindHost, |
| } from "../../gateway/net.js"; |
| import { isGatewayEffectiveConfigConflictError } from "../../gateway/server-runtime-config.js"; |
| import { GatewayStartupCleanupError } from "../../gateway/server-shutdown.js"; |
| import type { GatewayWsLogStyle } from "../../gateway/ws-logging.js"; |
| import { setGatewayWsLogStyle } from "../../gateway/ws-logging.js"; |
| import { setVerbose } from "../../globals.js"; |
| import { isAbortError } from "../../infra/abort-signal.js"; |
| import { isTruthyEnvValue } from "../../infra/env.js"; |
| import { collectNestedErrorCandidates } from "../../infra/error-graph-internal.js"; |
| import { formatErrorMessage } from "../../infra/errors.js"; |
| import { |
| completeGatewayBootLifecycle, |
| GATEWAY_CRASH_LOOP_BREAKER_REASON, |
| formatGatewayCrashLoopManualChannelStartHint, |
| GATEWAY_CRASH_LOOP_RECOVERED_REASON, |
| inspectGatewayCrashLoopBreaker, |
| recordGatewayBootStart, |
| recordGatewayCrashLoopRecovery, |
| type GatewayCrashLoopBreakerDecision, |
| type GatewayBootLifecycleCompletion, |
| } from "../../infra/gateway-boot-lifecycle.js"; |
| import { |
| GATEWAY_LIFECYCLE_LOCK_TIMEOUT_MS, |
| GatewayLockError, |
| isGatewayLifecycleContentionError, |
| } from "../../infra/gateway-lock.js"; |
| import { |
| findVerifiedGatewayListenerPidsOnPortSync, |
| formatGatewayPidList, |
| } from "../../infra/gateway-processes.js"; |
| import type { RespawnSupervisor } from "../../infra/supervisor-markers.js"; |
| import { isTailscaleRouteOwnershipConflictError } from "../../infra/tailscale-route-ownership-error.js"; |
| import { setConsoleSubsystemFilter, setConsoleTimestampPrefix } from "../../logging/console.js"; |
| import { withDiagnosticPhase } from "../../logging/diagnostic-phase.js"; |
| import { createSubsystemLogger } from "../../logging/subsystem.js"; |
| import { defaultRuntime } from "../../runtime.js"; |
| import { printClawBanner, type ClawBannerResult } from "../claw-banner.js"; |
| import { formatCliCommand } from "../command-format.js"; |
| import { formatInvalidConfigPort, formatInvalidPortOption } from "../error-format.js"; |
| import type { InvalidConfigRecoveryDeps } from "../invalid-config-recovery.js"; |
| import { withProgress } from "../progress.js"; |
| import { parsePort } from "../shared/parse-port.js"; |
| import { |
| isTerminalInteractive, |
| NON_INTERACTIVE_GATEWAY_RUN_FORCE_MESSAGE, |
| } from "../terminal-interactivity.js"; |
| import { enforceGatewayRunFutureConfigGuard } from "./future-config-guard.js"; |
| import { getGatewayStartGuardErrors } from "./pre-bootstrap.js"; |
| import { installQaParentWatchdog } from "./qa-parent-watchdog.js"; |
| import { runGatewayLoop } from "./run-loop.js"; |
| import type { GatewayRunOpts } from "./run-options.js"; |
| import type { GatewayRunRuntimeHooks } from "./runtime-hooks.js"; |
| import { resolveGatewayStartupMaintenanceReason } from "./startup-maintenance.js"; |
|
|
| const gatewayLog = createSubsystemLogger("gateway"); |
|
|
| const SUPERVISED_GATEWAY_LOCK_RETRY_MS = 5000; |
| const SUPERVISED_GATEWAY_HEALTH_PROBE_TIMEOUT_MS = 1000; |
| const GATEWAY_SHELL_ENV_CONVERGENCE_MAX_READS = 4; |
|
|
| type Awaitable<T> = T | Promise<T>; |
| type GatewayRunLogger = Pick<ReturnType<typeof createSubsystemLogger>, "info" | "warn">; |
|
|
| |
| |
| |
| |
| |
| const EXIT_CONFIG_ERROR = 78; |
|
|
| const GATEWAY_AUTH_MODES: readonly GatewayAuthMode[] = [ |
| "none", |
| "token", |
| "password", |
| "trusted-proxy", |
| ]; |
| const GATEWAY_TAILSCALE_MODES: readonly GatewayTailscaleMode[] = ["off", "serve", "funnel"]; |
|
|
| const toOptionString = (value: unknown): string | undefined => { |
| if (typeof value === "string") { |
| return value; |
| } |
| if (typeof value === "number" || typeof value === "bigint") { |
| return value.toString(); |
| } |
| return undefined; |
| }; |
|
|
| function extractGatewayMiskeys(parsed: unknown): { |
| hasGatewayToken: boolean; |
| hasRemoteToken: boolean; |
| } { |
| |
| if (!parsed || typeof parsed !== "object") { |
| return { hasGatewayToken: false, hasRemoteToken: false }; |
| } |
| const gateway = (parsed as Record<string, unknown>).gateway; |
| if (!gateway || typeof gateway !== "object") { |
| return { hasGatewayToken: false, hasRemoteToken: false }; |
| } |
| const hasGatewayToken = "token" in (gateway as Record<string, unknown>); |
| const remote = (gateway as Record<string, unknown>).remote; |
| const hasRemoteToken = |
| remote && typeof remote === "object" ? "token" in (remote as Record<string, unknown>) : false; |
| return { hasGatewayToken, hasRemoteToken }; |
| } |
|
|
| function createGatewayCliStartupTrace() { |
| const enabled = isTruthyEnvValue(process.env.OPENCLAW_GATEWAY_STARTUP_TRACE); |
| const started = performance.now(); |
| let last = started; |
| const emit = (name: string, durationMs: number, totalMs: number) => { |
| if (enabled) { |
| gatewayLog.info( |
| `startup trace: ${name} ${durationMs.toFixed(1)}ms total=${totalMs.toFixed(1)}ms`, |
| ); |
| } |
| }; |
| const startMeasure = <T>(name: string, run: () => Awaitable<T>) => { |
| const before = performance.now(); |
| let completedAt = before; |
| let emitted = false; |
| const result = withDiagnosticPhase(name, run).finally(() => { |
| completedAt = performance.now(); |
| }); |
| |
| |
| const settled = result.then( |
| () => {}, |
| () => {}, |
| ); |
| return { |
| result, |
| settled, |
| emit() { |
| if (emitted) { |
| return; |
| } |
| emitted = true; |
| emit(name, completedAt - before, completedAt - started); |
| last = completedAt; |
| }, |
| }; |
| }; |
| return { |
| mark(name: string) { |
| const now = performance.now(); |
| emit(name, now - last, now - started); |
| last = now; |
| }, |
| startMeasure, |
| async measure<T>(name: string, run: () => Awaitable<T>): Promise<T> { |
| const measurement = startMeasure(name, run); |
| try { |
| return await measurement.result; |
| } finally { |
| await measurement.settled; |
| measurement.emit(); |
| } |
| }, |
| }; |
| } |
|
|
| function warnInlinePasswordFlag() { |
| defaultRuntime.error( |
| "Warning: --password can be exposed via process listings. Prefer --password-file or OPENCLAW_GATEWAY_PASSWORD.", |
| ); |
| } |
|
|
| async function resolveGatewayPasswordOption(opts: GatewayRunOpts): Promise<string | undefined> { |
| const direct = toOptionString(opts.password); |
| const file = toOptionString(opts.passwordFile); |
| if (direct && file) { |
| throw new Error("Use either --password or --password-file."); |
| } |
| if (file) { |
| const { readSecretFromFile } = await import("../../acp/secret-file.js"); |
| return readSecretFromFile(file, "Gateway password"); |
| } |
| return direct; |
| } |
|
|
| function parseEnumOption<T extends string>( |
| raw: string | undefined, |
| allowed: readonly T[], |
| ): T | null { |
| if (!raw) { |
| return null; |
| } |
| return (allowed as readonly string[]).includes(raw) ? (raw as T) : null; |
| } |
|
|
| function formatModeErrorList(modes: readonly string[]): string { |
| const quoted = modes.map((mode) => `"${mode}"`); |
| if (quoted.length === 0) { |
| return ""; |
| } |
| if (quoted.length === 1) { |
| return expectDefined(quoted[0], "quoted entry at 0"); |
| } |
| if (quoted.length === 2) { |
| return `${quoted[0]} or ${quoted[1]}`; |
| } |
| return `${quoted.slice(0, -1).join(", ")}, or ${quoted[quoted.length - 1]}`; |
| } |
|
|
| function shouldBlockGatewayBindWithoutExplicitAuth(params: { |
| bindHost: string; |
| hasSharedSecret: boolean; |
| resolvedAuthMode: GatewayAuthMode; |
| }): boolean { |
| return ( |
| !isLoopbackHost(params.bindHost) && |
| !params.hasSharedSecret && |
| params.resolvedAuthMode !== "trusted-proxy" |
| ); |
| } |
|
|
| async function readGatewayStartupConfig(params: { |
| lowerPrecedenceEnv: Readonly<Record<string, string>>; |
| startupTrace: ReturnType<typeof createGatewayCliStartupTrace>; |
| }): Promise<{ |
| cfg: OpenClawConfig; |
| snapshot: ConfigFileSnapshot | null; |
| startupConfigSnapshotRead?: ReadConfigFileSnapshotWithPluginMetadataResult; |
| }> { |
| const { readConfigFileSnapshotWithPluginMetadata } = await import("../../config/config.js"); |
| const snapshotRead: ReadConfigFileSnapshotWithPluginMetadataResult | null = |
| await params.startupTrace.measure("cli.config-snapshot", () => |
| readConfigFileSnapshotWithPluginMetadata({ |
| isolateEnv: true, |
| observe: false, |
| ...(Object.keys(params.lowerPrecedenceEnv).length > 0 |
| ? { lowerPrecedenceEnv: params.lowerPrecedenceEnv } |
| : {}), |
| }).catch(() => null), |
| ); |
| const snapshot: ConfigFileSnapshot | null = snapshotRead?.snapshot ?? null; |
| const cfg = snapshot?.config ?? {}; |
| return { |
| cfg, |
| snapshot, |
| ...(snapshotRead ? { startupConfigSnapshotRead: snapshotRead } : {}), |
| }; |
| } |
|
|
| type GatewayRunShellEnvFallbackPlan = |
| | { enabled: false } |
| | { |
| enabled: true; |
| expectedKeys: string[]; |
| timeoutMs: number; |
| }; |
|
|
| async function resolveGatewayRunShellEnvFallbackPlan( |
| cfg: OpenClawConfig, |
| ): Promise<GatewayRunShellEnvFallbackPlan> { |
| const { createConfigRuntimeEnv } = await import("../../config/env-vars.js"); |
| const { |
| resolveShellEnvFallbackTimeoutMs, |
| shouldDeferShellEnvFallback, |
| shouldEnableShellEnvFallback, |
| } = await import("../../infra/shell-env.js"); |
| const planEnv = createConfigRuntimeEnv(cfg, process.env); |
| const enabled = |
| (shouldEnableShellEnvFallback(planEnv) || cfg.env?.shellEnv?.enabled === true) && |
| !shouldDeferShellEnvFallback(planEnv); |
| if (!enabled) { |
| return { enabled: false }; |
| } |
| const { resolveShellEnvExpectedKeys } = await import("../../config/shell-env-expected-keys.js"); |
| return { |
| enabled: true, |
| expectedKeys: resolveShellEnvExpectedKeys(planEnv, cfg), |
| timeoutMs: cfg.env?.shellEnv?.timeoutMs ?? resolveShellEnvFallbackTimeoutMs(planEnv), |
| }; |
| } |
|
|
| async function loadGatewayRunShellEnvFallback( |
| plan: Extract<GatewayRunShellEnvFallbackPlan, { enabled: true }>, |
| ): Promise<Record<string, string>> { |
| const { loadShellEnvFallback } = await import("../../infra/shell-env.js"); |
| const valuesBeforeLoad = new Map(plan.expectedKeys.map((key) => [key, process.env[key]])); |
| loadShellEnvFallback({ |
| enabled: true, |
| env: process.env, |
| expectedKeys: plan.expectedKeys, |
| logger: gatewayLog, |
| timeoutMs: plan.timeoutMs, |
| }); |
| return Object.fromEntries( |
| plan.expectedKeys.flatMap((key) => { |
| const value = process.env[key]; |
| return value !== undefined && value !== valuesBeforeLoad.get(key) ? [[key, value]] : []; |
| }), |
| ); |
| } |
|
|
| async function clearGatewayRunShellEnvFallback( |
| values: Readonly<Record<string, string>>, |
| ): Promise<void> { |
| const keys = Object.keys(values); |
| if (keys.length === 0) { |
| return; |
| } |
| for (const [key, value] of Object.entries(values)) { |
| if (process.env[key] === value) { |
| delete process.env[key]; |
| } |
| } |
| const { clearShellEnvAppliedKeys } = await import("../../infra/shell-env.js"); |
| clearShellEnvAppliedKeys(keys); |
| } |
|
|
| function gatewayRunShellEnvFallbackPlanSignature(plan: GatewayRunShellEnvFallbackPlan): string { |
| return JSON.stringify(plan); |
| } |
|
|
| async function readGatewayStartupConfigWithShellEnv(params: { |
| startupTrace: ReturnType<typeof createGatewayCliStartupTrace>; |
| }): Promise< |
| Awaited<ReturnType<typeof readGatewayStartupConfig>> & { |
| lowerPrecedenceEnv: Readonly<Record<string, string>>; |
| } |
| > { |
| let lowerPrecedenceEnv: Record<string, string> = {}; |
| let loadedPlanSignature: string | undefined; |
| try { |
| for (let readCount = 0; readCount < GATEWAY_SHELL_ENV_CONVERGENCE_MAX_READS; readCount += 1) { |
| const startupConfig = await readGatewayStartupConfig({ |
| lowerPrecedenceEnv, |
| startupTrace: params.startupTrace, |
| }); |
| const plan = await resolveGatewayRunShellEnvFallbackPlan( |
| startupConfig.snapshot?.valid === true ? startupConfig.cfg : {}, |
| ); |
| const planSignature = gatewayRunShellEnvFallbackPlanSignature(plan); |
| if (!plan.enabled) { |
| if (Object.keys(lowerPrecedenceEnv).length === 0) { |
| return { ...startupConfig, lowerPrecedenceEnv }; |
| } |
| await clearGatewayRunShellEnvFallback(lowerPrecedenceEnv); |
| lowerPrecedenceEnv = {}; |
| loadedPlanSignature = undefined; |
| continue; |
| } |
| if (loadedPlanSignature === planSignature) { |
| return { ...startupConfig, lowerPrecedenceEnv }; |
| } |
| await clearGatewayRunShellEnvFallback(lowerPrecedenceEnv); |
| lowerPrecedenceEnv = await loadGatewayRunShellEnvFallback(plan); |
| loadedPlanSignature = planSignature; |
| } |
| } catch (err) { |
| await clearGatewayRunShellEnvFallback(lowerPrecedenceEnv); |
| throw err; |
| } |
| await clearGatewayRunShellEnvFallback(lowerPrecedenceEnv); |
| throw new Error( |
| "Gateway shell environment fallback settings changed repeatedly during startup. Retry startup.", |
| ); |
| } |
|
|
| function isGatewayLockError(err: unknown): err is GatewayLockError { |
| return ( |
| err instanceof GatewayLockError || |
| (Boolean(err) && |
| typeof err === "object" && |
| (err as { name?: string }).name === "GatewayLockError") |
| ); |
| } |
|
|
| function isGatewayRetryableLockError(err: unknown): boolean { |
| if (!isGatewayLockError(err) || typeof err.message !== "string") { |
| return false; |
| } |
| return ( |
| isGatewayLifecycleContentionError(err) || |
| err.message.includes("gateway already running") || |
| err.message.includes("another gateway instance is already listening") |
| ); |
| } |
|
|
| class SupervisedGatewayLockError extends GatewayLockError { |
| constructor( |
| message: string, |
| cause: unknown, |
| readonly exitCode: 1 | typeof EXIT_CONFIG_ERROR, |
| ) { |
| super(message, cause); |
| } |
| } |
|
|
| function resolveGatewayLockErrorExitCode(err: unknown): number { |
| return err instanceof SupervisedGatewayLockError ? err.exitCode : 1; |
| } |
|
|
| function resolveGatewayStartupFailureExitCode(err: unknown): number { |
| return isInvalidConfigError(err) || |
| isTailscaleRouteOwnershipConflictError(err) || |
| isGatewayEffectiveConfigConflictError(err) || |
| resolveGatewayStartupMaintenanceReason(err) |
| ? EXIT_CONFIG_ERROR |
| : 1; |
| } |
|
|
| const normalizeGatewayHealthProbeHost = normalizeGatewayHttpProbeHost; |
|
|
| function isGatewayHealthzResponse(statusCode: number | undefined, body: string): boolean { |
| if (statusCode !== 200) { |
| return false; |
| } |
| try { |
| const payload = JSON.parse(body) as { ok?: unknown; status?: unknown }; |
| return payload.ok === true && payload.status === "live"; |
| } catch { |
| return false; |
| } |
| } |
|
|
| async function probeGatewayHealthz(params: { |
| host: string; |
| port: number; |
| timeoutMs?: number; |
| tlsFingerprint?: string; |
| }): Promise<boolean> { |
| const timeoutMs = params.timeoutMs ?? SUPERVISED_GATEWAY_HEALTH_PROBE_TIMEOUT_MS; |
| const result = await requestGatewayLocalHttpProbe({ |
| ...params, |
| pathname: "/healthz", |
| timeoutMs, |
| }); |
| return isGatewayHealthzResponse(result?.statusCode, result?.body ?? ""); |
| } |
|
|
| function createConfiguredGatewayHealthProbe(cfg: OpenClawConfig) { |
| const probe = createConfiguredGatewayLocalProbe(cfg); |
| return async (params: { host: string; port: number }): Promise<boolean> => { |
| const result = await probe.requestHttp({ |
| ...params, |
| pathname: "/healthz", |
| timeoutMs: SUPERVISED_GATEWAY_HEALTH_PROBE_TIMEOUT_MS, |
| }); |
| return isGatewayHealthzResponse(result?.statusCode, result?.body ?? ""); |
| }; |
| } |
|
|
| async function runGatewayLoopWithSupervisedLockRecovery(params: { |
| startLoop: (lifecycleDeadlineMs?: number) => Promise<void>; |
| supervisor: RespawnSupervisor | null; |
| port: number; |
| healthHost: string; |
| log: GatewayRunLogger; |
| now?: () => number; |
| sleep?: (ms: number) => Promise<void>; |
| probeHealth?: (params: { host: string; port: number }) => Promise<boolean>; |
| retryMs?: number; |
| timeoutMs?: number; |
| }) { |
| const supervisor = params.supervisor; |
| if (!supervisor) { |
| await params.startLoop(); |
| return; |
| } |
|
|
| const now = params.now ?? performance.now.bind(performance); |
| const sleep = |
| params.sleep ?? |
| (async (ms: number) => |
| await new Promise((resolve) => { |
| setTimeout(resolve, ms); |
| })); |
| const probeHealth = params.probeHealth ?? ((probeParams) => probeGatewayHealthz(probeParams)); |
| const retryMs = params.retryMs ?? SUPERVISED_GATEWAY_LOCK_RETRY_MS; |
| const timeoutMs = params.timeoutMs ?? GATEWAY_LIFECYCLE_LOCK_TIMEOUT_MS; |
| const startedAt = now(); |
|
|
| for (;;) { |
| try { |
| |
| await params.startLoop(startedAt + timeoutMs); |
| return; |
| } catch (err) { |
| if (!isGatewayRetryableLockError(err)) { |
| throw err; |
| } |
|
|
| const lifecycleContention = isGatewayLifecycleContentionError(err); |
| if ( |
| !lifecycleContention && |
| (await probeHealth({ host: params.healthHost, port: params.port })) |
| ) { |
| if (supervisor === "systemd") { |
| throw new SupervisedGatewayLockError( |
| "gateway already running under systemd; existing gateway is healthy, exiting with code 78 to prevent a systemd Restart=always loop", |
| err, |
| EXIT_CONFIG_ERROR, |
| ); |
| } |
| params.log.info( |
| `gateway already running under ${supervisor}; existing gateway is healthy, leaving it in control`, |
| ); |
| return; |
| } |
|
|
| const elapsedMs = now() - startedAt; |
| if (elapsedMs >= timeoutMs) { |
| if (lifecycleContention) { |
| throw err; |
| } |
| throw new SupervisedGatewayLockError( |
| `gateway already running under ${supervisor}; existing gateway did not become healthy after ${timeoutMs}ms`, |
| err, |
| 1, |
| ); |
| } |
|
|
| const waitMs = Math.min(retryMs, Math.max(0, timeoutMs - elapsedMs)); |
| params.log.warn( |
| `${lifecycleContention ? "gateway-lifecycle ownership held by another OpenClaw process" : "gateway already running"} under ${supervisor}; waiting ${waitMs}ms before retrying startup`, |
| ); |
| await sleep(waitMs); |
| } |
| } |
| } |
|
|
| async function maybeWriteGatewayStartupFailureBundle( |
| err: unknown, |
| reason = "gateway.startup_failed", |
| ): Promise<void> { |
| const { writeDiagnosticStabilityBundleForFailureSync } = |
| await import("../../logging/diagnostic-stability-bundle.js"); |
| const result = writeDiagnosticStabilityBundleForFailureSync(reason, err); |
| if ("message" in result) { |
| gatewayLog.warn(result.message); |
| } |
| } |
|
|
| async function runGatewayCommandOnce(opts: GatewayRunOpts, hooks: GatewayRunRuntimeHooks = {}) { |
| |
| |
| const inheritedGatewayServicePid = parseStrictPositiveInteger( |
| process.env[GATEWAY_SERVICE_RUNTIME_PID_ENV], |
| ); |
| normalizeStateDirEnv(process.env); |
| const { clearGatewayRunConfigEnvironment } = await import("./pre-bootstrap.js"); |
| clearGatewayRunConfigEnvironment(); |
| installQaParentWatchdog(); |
| const isDevProfile = normalizeOptionalLowercaseString(process.env.OPENCLAW_PROFILE) === "dev"; |
| const devMode = Boolean(opts.dev) || isDevProfile; |
| |
| |
| const ambientEnvTriggers = opts.ambientChannels || opts.devAmbientChannels ? "allow" : "suppress"; |
| if (opts.reset && !devMode) { |
| defaultRuntime.error("Use --reset with --dev."); |
| defaultRuntime.exit(1); |
| return; |
| } |
| setVerbose(Boolean(opts.verbose)); |
| if (opts.cliBackendLogs || opts.claudeCliLogs) { |
| setConsoleSubsystemFilter(["agent/cli-backend"]); |
| process.env.OPENCLAW_CLI_BACKEND_LOG_OUTPUT = "1"; |
| } |
| const wsLogRaw = (opts.compact ? "compact" : opts.wsLog) as string | undefined; |
| const wsLogStyle: GatewayWsLogStyle = |
| wsLogRaw === "compact" ? "compact" : wsLogRaw === "full" ? "full" : "auto"; |
| if ( |
| wsLogRaw !== undefined && |
| wsLogRaw !== "auto" && |
| wsLogRaw !== "compact" && |
| wsLogRaw !== "full" |
| ) { |
| defaultRuntime.error('Invalid --ws-log. Use "auto", "full", or "compact".'); |
| defaultRuntime.exit(1); |
| } |
| setGatewayWsLogStyle(wsLogStyle); |
|
|
| if (opts.rawStream) { |
| process.env.OPENCLAW_RAW_STREAM = "1"; |
| } |
| const rawStreamPath = toOptionString(opts.rawStreamPath); |
| if (rawStreamPath) { |
| process.env.OPENCLAW_RAW_STREAM_PATH = rawStreamPath; |
| } |
|
|
| const startupTrace = createGatewayCliStartupTrace(); |
|
|
| |
| |
| |
| |
| const serverImportMeasurement = startupTrace.startMeasure( |
| "cli.server-import", |
| () => import("../../gateway/server.js"), |
| ); |
| const rawServerImport = serverImportMeasurement.result; |
| const bannerDone: Promise<ClawBannerResult> = process.stdout.isTTY |
| ? printClawBanner(defaultRuntime, { settleWhen: rawServerImport }) |
| : Promise.resolve("static"); |
| const loadServerModule = async () => { |
| try { |
| const bannerResult = await bannerDone; |
| return bannerResult === "settled" |
| ? await rawServerImport |
| : await withProgress( |
| { label: "Loading gateway modules…", indeterminate: true }, |
| async () => rawServerImport, |
| ); |
| } finally { |
| |
| await serverImportMeasurement.settled; |
| serverImportMeasurement.emit(); |
| } |
| }; |
| const { startGatewayServer } = await loadServerModule(); |
|
|
| setConsoleTimestampPrefix(true); |
|
|
| if (devMode) { |
| if (opts.reset) { |
| |
| const { recheckGatewayRunReset } = await import("./pre-bootstrap.js"); |
| if (!(await recheckGatewayRunReset({ opts, runtime: defaultRuntime }))) { |
| return; |
| } |
| } |
| const { ensureDevGatewayConfig } = await import("./dev.js"); |
| await startupTrace.measure("cli.dev-config", () => |
| ensureDevGatewayConfig({ reset: Boolean(opts.reset) }), |
| ); |
| if (opts.reset) { |
| const { reloadTrustedGatewayRunEnvironment } = await import("./pre-bootstrap.js"); |
| if (!(await reloadTrustedGatewayRunEnvironment({ runtime: defaultRuntime }))) { |
| return; |
| } |
| } |
| } |
|
|
| gatewayLog.info("loading configuration…"); |
| const { cfg, lowerPrecedenceEnv, snapshot, startupConfigSnapshotRead } = |
| await readGatewayStartupConfigWithShellEnv({ |
| startupTrace, |
| }); |
| if ( |
| !enforceGatewayRunFutureConfigGuard({ |
| opts, |
| runtime: defaultRuntime, |
| snapshot, |
| }) |
| ) { |
| return; |
| } |
| if (snapshot) { |
| const { applyFinalGatewayRunConfigEnv } = await import("./pre-bootstrap.js"); |
| if ( |
| !(await applyFinalGatewayRunConfigEnv({ |
| lowerPrecedenceEnv, |
| runtime: defaultRuntime, |
| snapshot, |
| })) |
| ) { |
| return; |
| } |
| const finalConfigEnteredServiceMode = Boolean(process.env.OPENCLAW_SERVICE_MARKER?.trim()); |
| const clearRejectedFinalConfigEnv = () => { |
| clearGatewayRunConfigEnvironment(); |
| if (finalConfigEnteredServiceMode) { |
| delete process.env[ALLOW_OLDER_BINARY_DESTRUCTIVE_ACTIONS_ENV]; |
| } |
| }; |
| let finalConfigAllowed: boolean; |
| try { |
| finalConfigAllowed = enforceGatewayRunFutureConfigGuard({ |
| opts, |
| runtime: defaultRuntime, |
| snapshot, |
| }); |
| } catch (err) { |
| clearRejectedFinalConfigEnv(); |
| throw err; |
| } |
| if (!finalConfigAllowed) { |
| clearRejectedFinalConfigEnv(); |
| return; |
| } |
| } |
| if (process.env.OPENCLAW_SERVICE_MARKER?.trim()) { |
| process.env[GATEWAY_SERVICE_RUNTIME_PID_ENV] = String(process.pid); |
| if (process.platform === "darwin") { |
| const { warnAboutGatewayRestartStorm } = await import("../../daemon/restart-storm.js"); |
| await warnAboutGatewayRestartStorm(process.env, (message) => gatewayLog.warn(message)); |
| } |
| } |
| await hooks.refreshManagedProxy?.(cfg.proxy); |
| const portOverride = parsePort(opts.port); |
| if (opts.port !== undefined && portOverride === null) { |
| defaultRuntime.error(formatInvalidPortOption("--port")); |
| defaultRuntime.exit(1); |
| return; |
| } |
| const port = portOverride ?? resolveGatewayPort(cfg); |
| if (!Number.isFinite(port) || port <= 0 || port > 65_535) { |
| defaultRuntime.error(formatInvalidConfigPort("gateway.port")); |
| defaultRuntime.exit(EXIT_CONFIG_ERROR); |
| return; |
| } |
| |
| |
| |
| const VALID_BIND_MODES = new Set<string>(["loopback", "lan", "auto", "custom", "tailnet"]); |
| const bindExplicitRawStr = normalizeOptionalString( |
| toOptionString(opts.bind) ?? cfg.gateway?.bind, |
| ); |
| if (bindExplicitRawStr !== undefined && !VALID_BIND_MODES.has(bindExplicitRawStr)) { |
| defaultRuntime.error('Invalid --bind. Use "loopback", "lan", "tailnet", "auto", or "custom".'); |
| defaultRuntime.exit(1); |
| return; |
| } |
| const bindExplicitRaw = bindExplicitRawStr as GatewayBindMode | undefined; |
| if (process.env.OPENCLAW_SERVICE_MARKER?.trim()) { |
| const { cleanStaleGatewayProcessesSync } = await import("../../infra/restart-stale-pids.js"); |
| const stale = cleanStaleGatewayProcessesSync(port, { |
| protectedPid: inheritedGatewayServicePid, |
| }); |
| if (stale.length > 0) { |
| gatewayLog.info( |
| `service-mode: cleared ${stale.length} stale gateway pid(s) before bind on port ${port}`, |
| ); |
| |
| |
| |
| |
| |
| if (process.platform === "linux") { |
| const { findSystemdGatewayInstallation, formatDuelingScopesWarning } = |
| await import("../../daemon/systemd.js"); |
| const installation = await findSystemdGatewayInstallation(process.env).catch(() => null); |
| const warning = installation ? formatDuelingScopesWarning(installation, port) : null; |
| if (warning) { |
| gatewayLog.warn(`service-mode: ${warning}`); |
| } |
| } |
| } |
| } |
| if (opts.force) { |
| const interactive = isTerminalInteractive(); |
| const describeNonInteractiveGatewayOwner = () => { |
| const gatewayPids = findVerifiedGatewayListenerPidsOnPortSync(port); |
| if (gatewayPids.length === 0) { |
| return undefined; |
| } |
| return `${NON_INTERACTIVE_GATEWAY_RUN_FORCE_MESSAGE} Existing gateway listener pid${gatewayPids.length === 1 ? "" : "s"}: ${formatGatewayPidList(gatewayPids)}.`; |
| }; |
| if (!interactive) { |
| const refusal = describeNonInteractiveGatewayOwner(); |
| if (refusal) { |
| defaultRuntime.error(refusal); |
| defaultRuntime.exit(1); |
| return; |
| } |
| } |
| try { |
| const { forceFreePortAndWait, waitForPortBindable } = await import("../ports.js"); |
| const { killed, waitedMs, escalatedToSigkill } = await forceFreePortAndWait(port, { |
| timeoutMs: 2000, |
| intervalMs: 100, |
| sigtermTimeoutMs: 700, |
| ...(interactive |
| ? {} |
| : { |
| beforeSignal: () => { |
| const refusal = describeNonInteractiveGatewayOwner(); |
| if (refusal) { |
| throw new Error(refusal); |
| } |
| }, |
| }), |
| }); |
| if (killed.length === 0) { |
| |
| gatewayLog.debug(`force: no listeners on port ${port}`); |
| } else { |
| for (const proc of killed) { |
| gatewayLog.info( |
| `force: killed pid ${proc.pid}${proc.command ? ` (${proc.command})` : ""} on port ${port}`, |
| ); |
| } |
| if (escalatedToSigkill) { |
| gatewayLog.info(`force: escalated to SIGKILL while freeing port ${port}`); |
| } |
| if (waitedMs > 0) { |
| gatewayLog.info(`force: waited ${waitedMs}ms for port ${port} to free`); |
| } |
| } |
| |
| const bindProbeHost = |
| bindExplicitRaw === "loopback" |
| ? "127.0.0.1" |
| : bindExplicitRaw === "lan" |
| ? "0.0.0.0" |
| : bindExplicitRaw === "custom" |
| ? toOptionString(cfg.gateway?.customBindHost) |
| : undefined; |
| const bindWaitMs = await waitForPortBindable(port, { |
| timeoutMs: 3000, |
| intervalMs: 150, |
| host: bindProbeHost, |
| }); |
| if (bindWaitMs > 0) { |
| gatewayLog.info(`force: waited ${bindWaitMs}ms for port ${port} to become bindable`); |
| } |
| } catch (err) { |
| defaultRuntime.error( |
| `Could not free port ${port}: ${formatErrorMessage(err)}. Run ${formatCliCommand("openclaw gateway status --deep")} to inspect the listener.`, |
| ); |
| defaultRuntime.exit(1); |
| return; |
| } |
| } |
| if (opts.token) { |
| const token = toOptionString(opts.token); |
| if (token) { |
| process.env.OPENCLAW_GATEWAY_TOKEN = token; |
| } |
| } |
| const authModeRaw = toOptionString(opts.auth); |
| const authMode = parseEnumOption(authModeRaw, GATEWAY_AUTH_MODES); |
| if (authModeRaw && !authMode) { |
| defaultRuntime.error(`Invalid --auth. Use ${formatModeErrorList(GATEWAY_AUTH_MODES)}.`); |
| defaultRuntime.exit(1); |
| return; |
| } |
| const tailscaleRaw = toOptionString(opts.tailscale); |
| const tailscaleMode = parseEnumOption(tailscaleRaw, GATEWAY_TAILSCALE_MODES); |
| if (tailscaleRaw && !tailscaleMode) { |
| defaultRuntime.error( |
| `Invalid --tailscale. Use ${formatModeErrorList(GATEWAY_TAILSCALE_MODES)}.`, |
| ); |
| defaultRuntime.exit(1); |
| return; |
| } |
| |
| const effectiveTailscaleMode = tailscaleMode ?? cfg.gateway?.tailscale?.mode ?? "off"; |
| const bind = (bindExplicitRaw ?? defaultGatewayBindMode(effectiveTailscaleMode)) as |
| | "loopback" |
| | "lan" |
| | "auto" |
| | "custom" |
| | "tailnet"; |
|
|
| let passwordRaw: string | undefined; |
| try { |
| passwordRaw = await resolveGatewayPasswordOption(opts); |
| } catch (err) { |
| defaultRuntime.error(formatErrorMessage(err)); |
| defaultRuntime.exit(1); |
| return; |
| } |
| if (toOptionString(opts.password)) { |
| warnInlinePasswordFlag(); |
| } |
| const tokenRaw = toOptionString(opts.token); |
|
|
| gatewayLog.info("resolving authentication…"); |
| const configExists = snapshot?.exists ?? fs.existsSync(CONFIG_PATH); |
| const effectiveCfg = snapshot?.valid ? snapshot.config : cfg; |
| const mode = effectiveCfg.gateway?.mode; |
| const guardErrors = getGatewayStartGuardErrors({ |
| allowUnconfigured: opts.allowUnconfigured, |
| configExists, |
| mode, |
| }); |
| if (guardErrors.length > 0) { |
| for (const error of guardErrors) { |
| defaultRuntime.error(error); |
| } |
| defaultRuntime.exit(EXIT_CONFIG_ERROR); |
| return; |
| } |
| const miskeys = extractGatewayMiskeys(snapshot?.parsed); |
| const authOverride = |
| authMode || passwordRaw || tokenRaw || authModeRaw |
| ? { |
| ...(authMode ? { mode: authMode } : {}), |
| ...(tokenRaw ? { token: tokenRaw } : {}), |
| ...(passwordRaw ? { password: passwordRaw } : {}), |
| } |
| : undefined; |
| const { resolveGatewayAuth } = await import("../../gateway/auth.js"); |
| const resolvedAuth = await startupTrace.measure("cli.auth-resolve", () => |
| resolveGatewayAuth({ |
| authConfig: cfg.gateway?.auth, |
| authOverride, |
| env: process.env, |
| tailscaleMode: tailscaleMode ?? cfg.gateway?.tailscale?.mode ?? "off", |
| }), |
| ); |
| const resolvedAuthMode = resolvedAuth.mode; |
| const tokenValue = resolvedAuth.token; |
| const passwordValue = resolvedAuth.password; |
| const hasToken = typeof tokenValue === "string" && tokenValue.trim().length > 0; |
| const hasPassword = typeof passwordValue === "string" && passwordValue.trim().length > 0; |
| const tokenConfigured = |
| hasToken || |
| hasConfiguredSecretInput( |
| authOverride?.token ?? cfg.gateway?.auth?.token, |
| cfg.secrets?.defaults, |
| ); |
| const passwordConfigured = |
| hasPassword || |
| hasConfiguredSecretInput( |
| authOverride?.password ?? cfg.gateway?.auth?.password, |
| cfg.secrets?.defaults, |
| ); |
| const hasSharedSecret = |
| (resolvedAuthMode === "token" && tokenConfigured) || |
| (resolvedAuthMode === "password" && passwordConfigured); |
| const authHints: string[] = []; |
| if (miskeys.hasGatewayToken) { |
| authHints.push('Found "gateway.token" in config. Use "gateway.auth.token" instead.'); |
| } |
| if (miskeys.hasRemoteToken) { |
| authHints.push( |
| '"gateway.remote.token" is for remote CLI calls; it does not enable local gateway auth.', |
| ); |
| } |
| if (resolvedAuthMode === "password" && !passwordConfigured) { |
| defaultRuntime.error( |
| [ |
| "Gateway auth is set to password, but no password is configured.", |
| "Set gateway.auth.password (or OPENCLAW_GATEWAY_PASSWORD), or pass --password.", |
| ...authHints, |
| ] |
| .filter(Boolean) |
| .join("\n"), |
| ); |
| defaultRuntime.exit(EXIT_CONFIG_ERROR); |
| return; |
| } |
| if (resolvedAuthMode === "none") { |
| gatewayLog.warn( |
| "Gateway auth mode=none explicitly configured; all gateway connections are unauthenticated.", |
| ); |
| } |
| const healthHost = await resolveGatewayBindHost(bind, cfg.gateway?.customBindHost); |
| if ( |
| shouldBlockGatewayBindWithoutExplicitAuth({ |
| bindHost: healthHost, |
| hasSharedSecret, |
| resolvedAuthMode, |
| }) |
| ) { |
| defaultRuntime.error( |
| [ |
| `Refusing to bind gateway to ${bind} without auth.`, |
| ...(isContainerEnvironment() |
| ? [ |
| "Container environment detected \u2014 the gateway defaults to bind=auto (0.0.0.0) for port-forwarding compatibility.", |
| "Set OPENCLAW_GATEWAY_TOKEN or OPENCLAW_GATEWAY_PASSWORD, or pass --token/--password to start with auth.", |
| ] |
| : [ |
| "Set gateway.auth.token/password (or OPENCLAW_GATEWAY_TOKEN/OPENCLAW_GATEWAY_PASSWORD) or pass --token/--password.", |
| ]), |
| ...authHints, |
| ] |
| .filter(Boolean) |
| .join("\n"), |
| ); |
| defaultRuntime.exit(EXIT_CONFIG_ERROR); |
| return; |
| } |
| const tailscaleOverride = tailscaleMode ? { mode: tailscaleMode } : undefined; |
|
|
| gatewayLog.info("starting..."); |
| startupTrace.mark("cli.gateway-loop"); |
| let startupConfigSnapshotReadForNextStart = startupConfigSnapshotRead; |
| const envSidecarStartupMode = |
| isTruthyEnvValue(process.env.OPENCLAW_SKIP_CHANNELS) || |
| isTruthyEnvValue(process.env.OPENCLAW_SKIP_PROVIDERS) |
| ? "defer" |
| : "start"; |
| let crashLoopDecision: GatewayCrashLoopBreakerDecision | undefined; |
| let channelAutostartSuppression: { reason: "crash-loop-breaker"; message: string } | undefined; |
| let tryRecoverChannelAutostartSuppression: (() => boolean) | undefined; |
| let activeBootId: string | undefined; |
| let bootRecorded = false; |
| let triageAttempted = false; |
| const triageStartupFailure = async (error: unknown, signal?: AbortSignal) => { |
| if ( |
| triageAttempted || |
| !bootRecorded || |
| signal?.aborted || |
| isAbortError(error) || |
| isGatewayLockError(error) || |
| isInvalidConfigError(error) || |
| isTailscaleRouteOwnershipConflictError(error) || |
| isGatewayEffectiveConfigConflictError(error) || |
| collectNestedErrorCandidates(error).some( |
| (candidate) => candidate instanceof GatewayStartupCleanupError, |
| ) || |
| resolveGatewayStartupMaintenanceReason(error) |
| ) { |
| return; |
| } |
| |
| |
| if ( |
| (supervisor || process.env.OPENCLAW_SERVICE_MARKER) && |
| !crashLoopDecision?.shouldWriteStabilityBundle |
| ) { |
| return; |
| } |
| triageAttempted = true; |
| await triageAfterFailure( |
| defaultRuntime, |
| { |
| kind: "gateway-startup", |
| phase: "startup", |
| error: formatErrorMessage(error), |
| gateway: "verify-running", |
| }, |
| signal, |
| ); |
| }; |
| const beginBoot = async (startedAtMs: number) => { |
| |
| |
| crashLoopDecision = inspectGatewayCrashLoopBreaker(process.env, startedAtMs); |
| const bootStartReason = crashLoopDecision.tripped |
| ? crashLoopDecision.shouldWriteStabilityBundle |
| ? GATEWAY_CRASH_LOOP_BREAKER_REASON |
| : undefined |
| : crashLoopDecision.recovered |
| ? GATEWAY_CRASH_LOOP_RECOVERED_REASON |
| : undefined; |
| |
| |
| activeBootId = recordGatewayBootStart(process.env, startedAtMs, bootStartReason); |
| bootRecorded = activeBootId !== undefined; |
| channelAutostartSuppression = undefined; |
| tryRecoverChannelAutostartSuppression = undefined; |
| if (crashLoopDecision.recovered) { |
| gatewayLog.info("gateway restart-loop breaker recovered; channel auto-start restored"); |
| } |
| if (!crashLoopDecision.tripped) { |
| return; |
| } |
| const message = |
| `gateway restart-loop breaker tripped: ${crashLoopDecision.uncleanBoots} unclean boot(s) within ${crashLoopDecision.windowMs}ms; ` + |
| `suppressing channel/provider account auto-start. Inspect the stability bundle and fix the startup crash before restarting the service. ${formatGatewayCrashLoopManualChannelStartHint()}`; |
| channelAutostartSuppression = { reason: "crash-loop-breaker", message }; |
| const suppressedBootId = activeBootId; |
| tryRecoverChannelAutostartSuppression = () => { |
| if (!suppressedBootId || activeBootId !== suppressedBootId) { |
| return false; |
| } |
| const decision = inspectGatewayCrashLoopBreaker(process.env); |
| |
| |
| |
| if (!decision.recovered || decision.uncleanBoots !== 0) { |
| return false; |
| } |
| const recoveredBootId = recordGatewayCrashLoopRecovery(suppressedBootId, process.env); |
| if (!recoveredBootId || activeBootId !== suppressedBootId) { |
| return false; |
| } |
| activeBootId = recoveredBootId; |
| gatewayLog.info("gateway restart-loop breaker recovered; channel auto-start restored"); |
| return true; |
| }; |
| gatewayLog.error(message); |
| if (crashLoopDecision.shouldWriteStabilityBundle) { |
| await maybeWriteGatewayStartupFailureBundle( |
| new Error(message), |
| GATEWAY_CRASH_LOOP_BREAKER_REASON, |
| ); |
| } |
| }; |
| const completeBoot = (completion: GatewayBootLifecycleCompletion) => { |
| completeGatewayBootLifecycle(activeBootId, completion, process.env); |
| activeBootId = undefined; |
| }; |
| const startLoop = async (lifecycleLockDeadlineMs?: number) => |
| await runGatewayLoop({ |
| runtime: defaultRuntime, |
| ownsProcessLifecycle: true, |
| lockPort: port, |
| lifecycleLockDeadlineMs, |
| healthHost, |
| beginBoot, |
| completeBoot, |
| onRestartStartupFailure: triageStartupFailure, |
| start: async ({ |
| processStartedAt, |
| startupStartedAt, |
| requestHotReloadRecovery, |
| hostLifecycle, |
| startupOperation, |
| } = {}) => { |
| const startupConfigSnapshotReadForThisStart = startupConfigSnapshotReadForNextStart; |
| startupConfigSnapshotReadForNextStart = undefined; |
| return await startGatewayServer(port, { |
| bind, |
| ...(opts.updateCanary ? { updateCanary: true } : {}), |
| ...(activeBootId ? { bootId: activeBootId } : {}), |
| auth: authOverride, |
| tailscale: tailscaleOverride, |
| ...(processStartedAt !== undefined ? { processStartedAt } : {}), |
| startupStartedAt, |
| hostLifecycle, |
| startupOperation, |
| ...(requestHotReloadRecovery ? { hotReloadRecovery: requestHotReloadRecovery } : {}), |
| ...(startupConfigSnapshotReadForThisStart |
| ? { startupConfigSnapshotRead: startupConfigSnapshotReadForThisStart } |
| : {}), |
| ...(envSidecarStartupMode !== "start" ? { sidecarStartup: envSidecarStartupMode } : {}), |
| ...(channelAutostartSuppression ? { channelAutostartSuppression } : {}), |
| ...(channelAutostartSuppression ? { tryRecoverChannelAutostartSuppression } : {}), |
| ambientEnvTriggers, |
| }); |
| }, |
| }); |
|
|
| const { detectRespawnSupervisor } = await import("../../infra/supervisor-markers.js"); |
| const supervisor = detectRespawnSupervisor(process.env); |
| try { |
| await runGatewayLoopWithSupervisedLockRecovery({ |
| startLoop, |
| supervisor, |
| port, |
| healthHost, |
| log: gatewayLog, |
| probeHealth: createConfiguredGatewayHealthProbe(cfg), |
| }); |
| } catch (err) { |
| if (isGatewayLockError(err)) { |
| const errMessage = formatErrorMessage(err); |
| defaultRuntime.error( |
| `Gateway failed to start: ${errMessage}\nIf the gateway is supervised, stop it with: ${formatCliCommand("openclaw gateway stop")}`, |
| ); |
| try { |
| const [{ formatPortDiagnostics }, { inspectPortUsage }] = await Promise.all([ |
| import("../../infra/ports-format.js"), |
| import("../../infra/ports-inspect.js"), |
| ]); |
| const diagnostics = await inspectPortUsage(port); |
| if (diagnostics.status === "busy") { |
| for (const line of formatPortDiagnostics(diagnostics)) { |
| defaultRuntime.error(line); |
| } |
| } |
| } catch { |
| |
| } |
| const { maybeExplainGatewayServiceStop } = await import("./shared.js"); |
| await maybeExplainGatewayServiceStop(); |
| defaultRuntime.exit(resolveGatewayLockErrorExitCode(err)); |
| return; |
| } |
| if (isInvalidConfigError(err)) { |
| throw err; |
| } |
| await maybeWriteGatewayStartupFailureBundle(err); |
| if (resolveGatewayStartupMaintenanceReason(err)) { |
| throw err; |
| } |
| defaultRuntime.error( |
| `Gateway failed to start: ${formatErrorMessage(err)}. Run ${formatCliCommand("openclaw gateway status --deep")} for diagnostics.`, |
| ); |
| await triageStartupFailure(err); |
| defaultRuntime.exit(resolveGatewayStartupFailureExitCode(err)); |
| } |
| } |
|
|
| |
| export async function runGatewayCommand( |
| opts: GatewayRunOpts, |
| hooks: GatewayRunRuntimeHooks = {}, |
| recoveryDeps?: InvalidConfigRecoveryDeps, |
| ) { |
| if (opts.taskSupervisor) { |
| const { runWindowsGatewayTaskSupervisor } = await import("./task-supervisor.js"); |
| await runWindowsGatewayTaskSupervisor(); |
| return; |
| } |
| try { |
| await runGatewayCommandOnce(opts, hooks); |
| } catch (error) { |
| if (!isInvalidConfigError(error)) { |
| throw error; |
| } |
| defaultRuntime.error(`Gateway failed to start: ${formatErrorMessage(error)}`); |
| if (opts.allowUnconfigured || !isDoctorRecoverableInvalidConfigError(error)) { |
| defaultRuntime.exit(EXIT_CONFIG_ERROR); |
| return; |
| } |
| const { offerInvalidConfigRecovery } = await import("../invalid-config-recovery.js"); |
| const recovery = await offerInvalidConfigRecovery({ |
| runtime: defaultRuntime, |
| deps: recoveryDeps, |
| retry: async () => await runGatewayCommandOnce(opts, hooks), |
| }); |
| if (recovery.status === "recovered") { |
| return; |
| } |
| defaultRuntime.exit(EXIT_CONFIG_ERROR); |
| } |
| } |
|
|
| const testing = { |
| createConfiguredGatewayHealthProbe, |
| isGatewayHealthzResponse, |
| normalizeGatewayHealthProbeHost, |
| probeGatewayHealthz, |
| resolveGatewayLockErrorExitCode, |
| resolveGatewayStartupFailureExitCode, |
| runGatewayLoopWithSupervisedLockRecovery, |
| }; |
|
|
| if (process.env.VITEST || process.env.NODE_ENV === "test") { |
| (globalThis as Record<PropertyKey, unknown>)[Symbol.for("openclaw.gatewayRunTestApi")] = testing; |
| } |
| |
|
|