| import { |
| createConfigIO, |
| resolveConfigPath, |
| resolveGatewayPort, |
| resolveStateDir, |
| } from "../../config/config.js"; |
| import type { |
| OpenClawConfig, |
| GatewayBindMode, |
| GatewayControlUiConfig, |
| } from "../../config/types.js"; |
| import { readLastGatewayErrorLine } from "../../daemon/diagnostics.js"; |
| import type { FindExtraGatewayServicesOptions } from "../../daemon/inspect.js"; |
| import { findExtraGatewayServices } from "../../daemon/inspect.js"; |
| import type { ServiceConfigAudit } from "../../daemon/service-audit.js"; |
| import { auditGatewayServiceConfig } from "../../daemon/service-audit.js"; |
| import type { GatewayServiceRuntime } from "../../daemon/service-runtime.js"; |
| import { resolveGatewayService } from "../../daemon/service.js"; |
| import { trimToUndefined } from "../../gateway/credentials.js"; |
| import { resolveGatewayBindHost } from "../../gateway/net.js"; |
| import { resolveGatewayProbeAuthWithSecretInputs } from "../../gateway/probe-auth.js"; |
| import { parseStrictPositiveInteger } from "../../infra/parse-finite-number.js"; |
| import { |
| formatPortDiagnostics, |
| inspectPortUsage, |
| type PortListener, |
| type PortUsageStatus, |
| } from "../../infra/ports.js"; |
| import { pickPrimaryTailnetIPv4 } from "../../infra/tailnet.js"; |
| import { loadGatewayTlsRuntime } from "../../infra/tls/gateway.js"; |
| import { probeGatewayStatus } from "./probe.js"; |
| import { normalizeListenerAddress, parsePortFromArgs, pickProbeHostForBind } from "./shared.js"; |
| import type { GatewayRpcOpts } from "./types.js"; |
|
|
| type ConfigSummary = { |
| path: string; |
| exists: boolean; |
| valid: boolean; |
| issues?: Array<{ path: string; message: string }>; |
| controlUi?: GatewayControlUiConfig; |
| }; |
|
|
| type GatewayStatusSummary = { |
| bindMode: GatewayBindMode; |
| bindHost: string; |
| customBindHost?: string; |
| port: number; |
| portSource: "service args" | "env/config"; |
| probeUrl: string; |
| probeNote?: string; |
| }; |
|
|
| type PortStatusSummary = { |
| port: number; |
| status: PortUsageStatus; |
| listeners: PortListener[]; |
| hints: string[]; |
| }; |
|
|
| type DaemonConfigContext = { |
| mergedDaemonEnv: Record<string, string | undefined>; |
| cliCfg: OpenClawConfig; |
| daemonCfg: OpenClawConfig; |
| cliConfigSummary: ConfigSummary; |
| daemonConfigSummary: ConfigSummary; |
| configMismatch: boolean; |
| }; |
|
|
| type ResolvedGatewayStatus = { |
| gateway: GatewayStatusSummary; |
| daemonPort: number; |
| cliPort: number; |
| probeUrlOverride: string | null; |
| }; |
|
|
| export type DaemonStatus = { |
| service: { |
| label: string; |
| loaded: boolean; |
| loadedText: string; |
| notLoadedText: string; |
| command?: { |
| programArguments: string[]; |
| workingDirectory?: string; |
| environment?: Record<string, string>; |
| sourcePath?: string; |
| } | null; |
| runtime?: GatewayServiceRuntime; |
| configAudit?: ServiceConfigAudit; |
| }; |
| config?: { |
| cli: ConfigSummary; |
| daemon?: ConfigSummary; |
| mismatch?: boolean; |
| }; |
| gateway?: GatewayStatusSummary; |
| port?: { |
| port: number; |
| status: PortUsageStatus; |
| listeners: PortListener[]; |
| hints: string[]; |
| }; |
| portCli?: { |
| port: number; |
| status: PortUsageStatus; |
| listeners: PortListener[]; |
| hints: string[]; |
| }; |
| lastError?: string; |
| rpc?: { |
| ok: boolean; |
| error?: string; |
| url?: string; |
| }; |
| extraServices: Array<{ label: string; detail: string; scope: string }>; |
| }; |
|
|
| function shouldReportPortUsage(status: PortUsageStatus | undefined, rpcOk?: boolean) { |
| if (status !== "busy") { |
| return false; |
| } |
| if (rpcOk === true) { |
| return false; |
| } |
| return true; |
| } |
|
|
| async function loadDaemonConfigContext( |
| serviceEnv?: Record<string, string>, |
| ): Promise<DaemonConfigContext> { |
| const mergedDaemonEnv = { |
| ...(process.env as Record<string, string | undefined>), |
| ...(serviceEnv ?? undefined), |
| } satisfies Record<string, string | undefined>; |
|
|
| const cliConfigPath = resolveConfigPath(process.env, resolveStateDir(process.env)); |
| const daemonConfigPath = resolveConfigPath( |
| mergedDaemonEnv as NodeJS.ProcessEnv, |
| resolveStateDir(mergedDaemonEnv as NodeJS.ProcessEnv), |
| ); |
|
|
| const cliIO = createConfigIO({ env: process.env, configPath: cliConfigPath }); |
| const daemonIO = createConfigIO({ |
| env: mergedDaemonEnv, |
| configPath: daemonConfigPath, |
| }); |
|
|
| const [cliSnapshot, daemonSnapshot] = await Promise.all([ |
| cliIO.readConfigFileSnapshot().catch(() => null), |
| daemonIO.readConfigFileSnapshot().catch(() => null), |
| ]); |
| const cliCfg = cliIO.loadConfig(); |
| const daemonCfg = daemonIO.loadConfig(); |
|
|
| const cliConfigSummary: ConfigSummary = { |
| path: cliSnapshot?.path ?? cliConfigPath, |
| exists: cliSnapshot?.exists ?? false, |
| valid: cliSnapshot?.valid ?? true, |
| ...(cliSnapshot?.issues?.length ? { issues: cliSnapshot.issues } : {}), |
| controlUi: cliCfg.gateway?.controlUi, |
| }; |
| const daemonConfigSummary: ConfigSummary = { |
| path: daemonSnapshot?.path ?? daemonConfigPath, |
| exists: daemonSnapshot?.exists ?? false, |
| valid: daemonSnapshot?.valid ?? true, |
| ...(daemonSnapshot?.issues?.length ? { issues: daemonSnapshot.issues } : {}), |
| controlUi: daemonCfg.gateway?.controlUi, |
| }; |
|
|
| return { |
| mergedDaemonEnv, |
| cliCfg, |
| daemonCfg, |
| cliConfigSummary, |
| daemonConfigSummary, |
| configMismatch: cliConfigSummary.path !== daemonConfigSummary.path, |
| }; |
| } |
|
|
| async function resolveGatewayStatusSummary(params: { |
| daemonCfg: OpenClawConfig; |
| cliCfg: OpenClawConfig; |
| mergedDaemonEnv: Record<string, string | undefined>; |
| commandProgramArguments?: string[]; |
| rpcUrlOverride?: string; |
| }): Promise<ResolvedGatewayStatus> { |
| const portFromArgs = parsePortFromArgs(params.commandProgramArguments); |
| const daemonPort = portFromArgs ?? resolveGatewayPort(params.daemonCfg, params.mergedDaemonEnv); |
| const portSource: GatewayStatusSummary["portSource"] = portFromArgs |
| ? "service args" |
| : "env/config"; |
| const bindMode: GatewayBindMode = params.daemonCfg.gateway?.bind ?? "loopback"; |
| const customBindHost = params.daemonCfg.gateway?.customBindHost; |
| const bindHost = await resolveGatewayBindHost(bindMode, customBindHost); |
| const tailnetIPv4 = pickPrimaryTailnetIPv4(); |
| const probeHost = pickProbeHostForBind(bindMode, tailnetIPv4, customBindHost); |
| const probeUrlOverride = trimToUndefined(params.rpcUrlOverride) ?? null; |
| const scheme = params.daemonCfg.gateway?.tls?.enabled === true ? "wss" : "ws"; |
| const probeUrl = probeUrlOverride ?? `${scheme}://${probeHost}:${daemonPort}`; |
| const probeNote = |
| !probeUrlOverride && bindMode === "lan" |
| ? `bind=lan listens on 0.0.0.0 (all interfaces); probing via ${probeHost}.` |
| : !probeUrlOverride && bindMode === "loopback" |
| ? "Loopback-only gateway; only local clients can connect." |
| : undefined; |
|
|
| return { |
| gateway: { |
| bindMode, |
| bindHost, |
| customBindHost, |
| port: daemonPort, |
| portSource, |
| probeUrl, |
| ...(probeNote ? { probeNote } : {}), |
| }, |
| daemonPort, |
| cliPort: resolveGatewayPort(params.cliCfg, process.env), |
| probeUrlOverride, |
| }; |
| } |
|
|
| function toPortStatusSummary( |
| diagnostics: Awaited<ReturnType<typeof inspectPortUsage>> | null, |
| ): PortStatusSummary | undefined { |
| if (!diagnostics) { |
| return undefined; |
| } |
| return { |
| port: diagnostics.port, |
| status: diagnostics.status, |
| listeners: diagnostics.listeners, |
| hints: diagnostics.hints, |
| }; |
| } |
|
|
| async function inspectDaemonPortStatuses(params: { |
| daemonPort: number; |
| cliPort: number; |
| }): Promise<{ portStatus?: PortStatusSummary; portCliStatus?: PortStatusSummary }> { |
| const [portDiagnostics, portCliDiagnostics] = await Promise.all([ |
| inspectPortUsage(params.daemonPort).catch(() => null), |
| params.cliPort !== params.daemonPort |
| ? inspectPortUsage(params.cliPort).catch(() => null) |
| : null, |
| ]); |
| return { |
| portStatus: toPortStatusSummary(portDiagnostics), |
| portCliStatus: toPortStatusSummary(portCliDiagnostics), |
| }; |
| } |
|
|
| export async function gatherDaemonStatus( |
| opts: { |
| rpc: GatewayRpcOpts; |
| probe: boolean; |
| deep?: boolean; |
| } & FindExtraGatewayServicesOptions, |
| ): Promise<DaemonStatus> { |
| const service = resolveGatewayService(); |
| const command = await service.readCommand(process.env).catch(() => null); |
| const serviceEnv = command?.environment |
| ? ({ |
| ...process.env, |
| ...command.environment, |
| } satisfies NodeJS.ProcessEnv) |
| : process.env; |
| const [loaded, runtime] = await Promise.all([ |
| service.isLoaded({ env: serviceEnv }).catch(() => false), |
| service.readRuntime(serviceEnv).catch((err) => ({ status: "unknown", detail: String(err) })), |
| ]); |
| const configAudit = await auditGatewayServiceConfig({ |
| env: process.env, |
| command, |
| }); |
| const { |
| mergedDaemonEnv, |
| cliCfg, |
| daemonCfg, |
| cliConfigSummary, |
| daemonConfigSummary, |
| configMismatch, |
| } = await loadDaemonConfigContext(command?.environment); |
| const { gateway, daemonPort, cliPort, probeUrlOverride } = await resolveGatewayStatusSummary({ |
| cliCfg, |
| daemonCfg, |
| mergedDaemonEnv, |
| commandProgramArguments: command?.programArguments, |
| rpcUrlOverride: opts.rpc.url, |
| }); |
| const { portStatus, portCliStatus } = await inspectDaemonPortStatuses({ |
| daemonPort, |
| cliPort, |
| }); |
|
|
| const extraServices = await findExtraGatewayServices( |
| process.env as Record<string, string | undefined>, |
| { deep: Boolean(opts.deep) }, |
| ).catch(() => []); |
|
|
| const timeoutMs = parseStrictPositiveInteger(opts.rpc.timeout ?? "10000") ?? 10_000; |
|
|
| const tlsEnabled = daemonCfg.gateway?.tls?.enabled === true; |
| const shouldUseLocalTlsRuntime = opts.probe && !probeUrlOverride && tlsEnabled; |
| const tlsRuntime = shouldUseLocalTlsRuntime |
| ? await loadGatewayTlsRuntime(daemonCfg.gateway?.tls) |
| : undefined; |
| const daemonProbeAuth = opts.probe |
| ? await resolveGatewayProbeAuthWithSecretInputs({ |
| cfg: daemonCfg, |
| mode: daemonCfg.gateway?.mode === "remote" ? "remote" : "local", |
| env: mergedDaemonEnv as NodeJS.ProcessEnv, |
| explicitAuth: { |
| token: opts.rpc.token, |
| password: opts.rpc.password, |
| }, |
| }) |
| : undefined; |
|
|
| const rpc = opts.probe |
| ? await probeGatewayStatus({ |
| url: gateway.probeUrl, |
| token: daemonProbeAuth?.token, |
| password: daemonProbeAuth?.password, |
| tlsFingerprint: |
| shouldUseLocalTlsRuntime && tlsRuntime?.enabled |
| ? tlsRuntime.fingerprintSha256 |
| : undefined, |
| timeoutMs, |
| json: opts.rpc.json, |
| configPath: daemonConfigSummary.path, |
| }) |
| : undefined; |
|
|
| let lastError: string | undefined; |
| if (loaded && runtime?.status === "running" && portStatus && portStatus.status !== "busy") { |
| lastError = (await readLastGatewayErrorLine(mergedDaemonEnv as NodeJS.ProcessEnv)) ?? undefined; |
| } |
|
|
| return { |
| service: { |
| label: service.label, |
| loaded, |
| loadedText: service.loadedText, |
| notLoadedText: service.notLoadedText, |
| command, |
| runtime, |
| configAudit, |
| }, |
| config: { |
| cli: cliConfigSummary, |
| daemon: daemonConfigSummary, |
| ...(configMismatch ? { mismatch: true } : {}), |
| }, |
| gateway, |
| port: portStatus, |
| ...(portCliStatus ? { portCli: portCliStatus } : {}), |
| lastError, |
| ...(rpc ? { rpc: { ...rpc, url: gateway.probeUrl } } : {}), |
| extraServices, |
| }; |
| } |
|
|
| export function renderPortDiagnosticsForCli(status: DaemonStatus, rpcOk?: boolean): string[] { |
| if (!status.port || !shouldReportPortUsage(status.port.status, rpcOk)) { |
| return []; |
| } |
| return formatPortDiagnostics({ |
| port: status.port.port, |
| status: status.port.status, |
| listeners: status.port.listeners, |
| hints: status.port.hints, |
| }); |
| } |
|
|
| export function resolvePortListeningAddresses(status: DaemonStatus): string[] { |
| const addrs = Array.from( |
| new Set( |
| status.port?.listeners |
| ?.map((l) => (l.address ? normalizeListenerAddress(l.address) : "")) |
| .filter((v): v is string => Boolean(v)) ?? [], |
| ), |
| ); |
| return addrs; |
| } |
|
|