UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

521 lines (520 loc) 21.1 kB
import { b as parseStrictPositiveInteger } from "./number-coercion-CJQ8TR--.js"; import "./parse-finite-number-Z7n6tXLk.js"; import { d as resolveGatewayPort, s as resolveConfigPath, y as resolveStateDir } from "./paths-mvMm5bYV.js"; import { t as createLazyImportLoader } from "./lazy-promise-BONnzNfb.js"; import { _ as uniqueStrings } from "./string-normalization-WNUDCpXX.js"; import { m as resolveSecretInputRef } from "./types.secrets-_0JOMGE5.js"; import { n as VERSION } from "./version-Crcn9X9T.js"; import { n as resolvePreferredOpenClawTmpDir, t as POSIX_OPENCLAW_TMP_DIR } from "./tmp-openclaw-dir-DOKojISm.js"; import { t as parseClawHubPluginSpec } from "./clawhub-spec-BilWxjDF.js"; import { s as parseRegistryNpmSpec } from "./npm-registry-spec-DjR-Lha9.js"; import { t as loadInstalledPluginIndexInstallRecords } from "./installed-plugin-index-record-reader-CPHayOZI.js"; import { c as resolveEffectiveEnableState, s as normalizePluginsConfig } from "./config-state-CikNNz7T.js"; import { a as trimToUndefined } from "./credential-planner-wESmL_7M.js"; import "./credentials-irvgw8Le.js"; import { r as createConfigIO } from "./io-Gi7-pyU-.js"; import "./config-C9RxTsn1.js"; import { a as inspectPortConnections, l as formatPortDiagnostics, o as inspectPortUsage } from "./ports-CoxQQbiY.js"; import { i as resolveGatewayService } from "./service-MPqQWJ3s.js"; import { c as pickProbeHostForBind, o as normalizeListenerAddress, s as parsePortFromArgs } from "./shared-BEkveSUA.js"; import { t as gatewaySecretInputPathCanWin } from "./credentials-secret-inputs-B4AcFF94.js"; import { a as readGatewaySecretInputValue, t as ALL_GATEWAY_SECRET_INPUT_PATHS } from "./secret-input-paths-BqHb6OmN.js"; import { a as resolveGatewayProbeCredentialConfig } from "./probe-auth-DoCLHThw.js"; import { t as readLastGatewayErrorLine } from "./diagnostics-C0o1p3sH.js"; import { r as resolveBestEffortGatewayBindHostForDisplay, t as inspectBestEffortPrimaryTailnetIPv4 } from "./network-discovery-display-DwCzf-iO.js"; import { n as readGatewayRestartHandoffSync } from "./restart-handoff-B55foNq9.js"; import { r as resolveTrustedSourceLinkedOfficialNpmSpec, t as resolveTrustedSourceLinkedOfficialClawHubInstall } from "./official-external-install-records-BKv4Hy5M.js"; import JSON5 from "json5"; import path from "node:path"; import fs from "node:fs/promises"; //#region src/logging/log-file-path.ts const LOG_PREFIX = "openclaw"; const LOG_SUFFIX = ".log"; function canUseNodeFs() { const getBuiltinModule = process.getBuiltinModule; if (typeof getBuiltinModule !== "function") return false; try { return getBuiltinModule("fs") !== void 0; } catch { return false; } } function formatLocalDate(date) { return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`; } function resolveDefaultRollingLogFile(date = /* @__PURE__ */ new Date()) { const logDir = canUseNodeFs() ? resolvePreferredOpenClawTmpDir() : POSIX_OPENCLAW_TMP_DIR; return path.join(logDir, `${LOG_PREFIX}-${formatLocalDate(date)}${LOG_SUFFIX}`); } /** Resolves the configured log file or today's rolling default log path. */ function resolveConfiguredLogFilePath(config) { return config?.logging?.file ?? resolveDefaultRollingLogFile(); } //#endregion //#region src/plugins/plugin-version-drift.ts /** * Strip a trailing build qualifier (e.g. `2026.5.4-1` -> `2026.5.4`) so that * a gateway packaged as `2026.5.4-1` is not reported as drifted from a * plugin packaged as `2026.5.4`. Both ends are normalized identically. */ function normalizeVersion(value) { return value.replace(/-\d+$/, ""); } function isPluginEnabled(config, pluginId) { return resolveEffectiveEnableState({ id: pluginId, origin: "global", config: normalizePluginsConfig(config?.plugins), rootConfig: config }).enabled; } function shouldCompareOfficialInstallToGateway(params) { const officialNpmSpec = resolveTrustedSourceLinkedOfficialNpmSpec(params); if (officialNpmSpec) return parseRegistryNpmSpec(officialNpmSpec)?.selectorKind !== "exact-version"; const officialClawHubInstall = resolveTrustedSourceLinkedOfficialClawHubInstall(params); if (officialClawHubInstall) { if (officialClawHubInstall.clawhubSpec) return !parseClawHubPluginSpec(officialClawHubInstall.clawhubSpec)?.version; return parseRegistryNpmSpec(officialClawHubInstall.npmSpec ?? "")?.selectorKind !== "exact-version"; } return false; } /** * Compare active official external plugin installs against the running gateway * version and return any mismatches. * * @param params.gatewayVersion The gateway version string (typically the * `version` field of the installed openclaw package.json). * @param params.installRecords The full set of recorded plugin installs (as * produced by `loadInstalledPluginIndexInstallRecords`). * @param params.config The merged daemon-side OpenClawConfig (optional). * Plugins inactive under the effective activation policy are skipped. * * The returned `drifts` list is sorted by `pluginId` for stable output. */ function detectPluginVersionDrift(params) { const { gatewayVersion, installRecords, config } = params; const normalizedGateway = normalizeVersion(gatewayVersion); const drifts = []; for (const [pluginId, record] of Object.entries(installRecords)) { if (!record) continue; if (!isPluginEnabled(config, pluginId)) continue; if (!shouldCompareOfficialInstallToGateway({ pluginId, record })) continue; const installedVersion = record.resolvedVersion ?? record.version; if (!installedVersion) continue; if (normalizeVersion(installedVersion) === normalizedGateway) continue; drifts.push({ pluginId, installedVersion, gatewayVersion, source: record.source, ...record.resolvedName ? { packageName: record.resolvedName } : {}, ...record.spec ? { spec: record.spec } : {} }); } drifts.sort((a, b) => a.pluginId.localeCompare(b.pluginId)); return { gatewayVersion, drifts }; } //#endregion //#region src/cli/daemon-cli/status.gather.ts const gatewayProbeAuthModuleLoader = createLazyImportLoader(() => import("./probe-auth-CFLBxNcR.js")); const daemonInspectModuleLoader = createLazyImportLoader(() => import("./inspect-BBKvSH9r.js")); const launchdModuleLoader = createLazyImportLoader(() => import("./launchd-SmlGbAAS.js")); const serviceAuditModuleLoader = createLazyImportLoader(() => import("./service-audit-CWuwW6vA.js")); const gatewayTlsModuleLoader = createLazyImportLoader(() => import("./gateway-CrnYmO7m.js")); const daemonProbeModuleLoader = createLazyImportLoader(() => import("./probe-DLc3S_Ub.js")); const restartHealthModuleLoader = createLazyImportLoader(() => import("./restart-health-DZW93nk6.js")); function loadGatewayProbeAuthModule() { return gatewayProbeAuthModuleLoader.load(); } function loadDaemonInspectModule() { return daemonInspectModuleLoader.load(); } function loadLaunchdModule() { return launchdModuleLoader.load(); } function loadServiceAuditModule() { return serviceAuditModuleLoader.load(); } function loadGatewayTlsModule() { return gatewayTlsModuleLoader.load(); } function loadDaemonProbeModule() { return daemonProbeModuleLoader.load(); } function loadRestartHealthModule() { return restartHealthModuleLoader.load(); } function resolveSnapshotRuntimeConfig(snapshot) { if (!snapshot?.valid || !snapshot.runtimeConfig) return null; return snapshot.runtimeConfig; } function coerceStatusConfig(value) { if (!value || typeof value !== "object" || Array.isArray(value)) return {}; return value; } function hasOwnKey(value, key) { return Boolean(value && typeof value === "object" && !Array.isArray(value) && Object.hasOwn(value, key)); } function needsFullStatusConfigRead(raw, parsed) { return raw.includes("$include") || raw.includes("${") || hasOwnKey(parsed, "env"); } async function readFastStatusConfig(configPath) { let raw; try { raw = await fs.readFile(configPath, "utf8"); } catch { return null; } let parsed; try { parsed = JSON5.parse(raw); } catch (err) { return { summary: { path: configPath, exists: true, valid: false, issues: [{ path: "", message: `JSON5 parse failed: ${String(err)}` }] }, cfg: {}, mode: "fast" }; } if (needsFullStatusConfigRead(raw, parsed)) return null; const cfg = coerceStatusConfig(parsed); return { summary: { path: configPath, exists: true, valid: true, controlUi: cfg.gateway?.controlUi }, cfg, mode: "fast" }; } async function readFullStatusConfig(params) { const io = createConfigIO({ env: params.env, configPath: params.configPath, pluginValidation: params.pluginValidation ?? "skip", logger: { error: () => {}, warn: () => {} } }); const snapshot = await io.readConfigFileSnapshot().catch(() => null); const cfg = resolveSnapshotRuntimeConfig(snapshot) ?? io.loadConfig(); return { summary: { path: snapshot?.path ?? params.configPath, exists: snapshot?.exists ?? false, valid: snapshot?.valid ?? true, ...snapshot?.issues?.length ? { issues: snapshot.issues } : {}, ...snapshot?.warnings?.length ? { warnings: snapshot.warnings } : {}, controlUi: cfg.gateway?.controlUi }, cfg, mode: "full" }; } async function readStatusConfig(params) { return (params.deep ? null : await readFastStatusConfig(params.configPath)) ?? await readFullStatusConfig({ env: params.env, configPath: params.configPath, pluginValidation: params.deep ? "full" : "skip" }); } function appendProbeNote(existing, extra) { const values = [existing, extra].filter((value) => Boolean(value?.trim())); if (values.length === 0) return; return uniqueStrings(values).join(" "); } function shouldReportPortUsage(status, rpcOk) { if (status !== "busy") return false; if (rpcOk === true) return false; return true; } function resolveCliStatusSummary(argv = process.argv) { const entrypoint = argv[1]?.trim(); return { version: VERSION, ...entrypoint ? { entrypoint } : {} }; } async function loadDaemonConfigContext(serviceEnv, opts = {}) { const mergedDaemonEnv = { ...process.env, ...serviceEnv ?? void 0 }; const cliConfigPath = resolveConfigPath(process.env, resolveStateDir(process.env)); const daemonConfigPath = resolveConfigPath(mergedDaemonEnv, resolveStateDir(mergedDaemonEnv)); const sameConfigPath = cliConfigPath === daemonConfigPath; const cliConfigRead = await readStatusConfig({ env: process.env, configPath: cliConfigPath, deep: opts.deep }); const daemonConfigRead = sameConfigPath && (cliConfigRead.mode === "fast" || !serviceEnv) ? cliConfigRead : await readStatusConfig({ env: mergedDaemonEnv, configPath: daemonConfigPath, deep: opts.deep }); return { mergedDaemonEnv, cliCfg: cliConfigRead.cfg, daemonCfg: daemonConfigRead.cfg, cliConfigSummary: cliConfigRead.summary, daemonConfigSummary: daemonConfigRead.summary, configMismatch: cliConfigRead.summary.path !== daemonConfigRead.summary.path }; } async function resolveGatewayStatusSummary(params) { const portFromArgs = parsePortFromArgs(params.commandProgramArguments); const daemonPort = portFromArgs ?? resolveGatewayPort(params.daemonCfg, params.mergedDaemonEnv); const portSource = portFromArgs ? "service args" : "env/config"; const bindMode = params.daemonCfg.gateway?.bind ?? "loopback"; const customBindHost = params.daemonCfg.gateway?.customBindHost; const { bindHost, warning: bindHostWarning } = await resolveBestEffortGatewayBindHostForDisplay({ bindMode, customBindHost, warningPrefix: "Status is using fallback network details because interface discovery failed" }); const { tailnetIPv4, warning: tailnetWarning } = inspectBestEffortPrimaryTailnetIPv4({ warningPrefix: "Status could not inspect tailnet addresses" }); const probeHost = pickProbeHostForBind(bindMode, tailnetIPv4, customBindHost); const probeUrlOverride = trimToUndefined(params.rpcUrlOverride) ?? null; const tlsEnabled = params.daemonCfg.gateway?.tls?.enabled === true; const probeUrl = probeUrlOverride ?? `${tlsEnabled ? "wss" : "ws"}://${probeHost}:${daemonPort}`; let 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." : void 0; probeNote = appendProbeNote(probeNote, bindHostWarning); probeNote = appendProbeNote(probeNote, tailnetWarning); return { gateway: { bindMode, bindHost, customBindHost, ...tlsEnabled ? { tlsEnabled } : {}, port: daemonPort, portSource, probeUrl, ...probeNote ? { probeNote } : {} }, daemonPort, cliPort: resolveGatewayPort(params.cliCfg, process.env), probeUrlOverride }; } function toPortStatusSummary(diagnostics) { if (!diagnostics) return; return { port: diagnostics.port, status: diagnostics.status, listeners: diagnostics.listeners, hints: diagnostics.hints }; } async function inspectDaemonPortStatuses(params) { 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) }; } async function inspectEstablishedGatewayClients(params) { if (params.deep !== true || params.gatewayMode === "remote") return; const result = await inspectPortConnections(params.daemonPort).catch(() => null); const establishedClients = result?.connections.filter((connection) => connection.direction !== "server"); if (!result || !establishedClients || establishedClients.length === 0) return; return { port: result.port, established: establishedClients }; } function hasActiveGatewayExecProbeCredential(params) { const cfg = resolveGatewayProbeCredentialConfig({ cfg: params.cfg, mode: params.mode }); return ALL_GATEWAY_SECRET_INPUT_PATHS.some((path) => { if (!gatewaySecretInputPathCanWin({ config: cfg, env: params.env, explicitAuth: params.explicitAuth, modeOverride: params.mode, path, remoteTokenFallback: "remote-only" })) return false; return resolveSecretInputRef({ value: readGatewaySecretInputValue(cfg, path), defaults: cfg.secrets?.defaults }).ref?.source === "exec"; }); } async function gatherDaemonStatus(opts) { const service = resolveGatewayService(); const command = await service.readCommand(process.env).catch(() => null); const serviceEnv = command?.environment ? { ...process.env, ...command.environment } : 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 restartHandoff = opts.deep ? readGatewayRestartHandoffSync(serviceEnv) : null; const configAudit = command ? await loadServiceAuditModule().then(({ auditGatewayServiceConfig }) => auditGatewayServiceConfig({ env: process.env, command })) : { ok: true, issues: [] }; const { mergedDaemonEnv, cliCfg, daemonCfg, cliConfigSummary, daemonConfigSummary, configMismatch } = await loadDaemonConfigContext(command?.environment, { deep: opts.deep }); 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 establishedClients = await inspectEstablishedGatewayClients({ daemonPort, deep: opts.deep, gatewayMode: daemonCfg.gateway?.mode }); const extraServices = opts.deep ? await loadDaemonInspectModule().then(({ findExtraGatewayServices }) => findExtraGatewayServices(process.env, { deep: true })).catch(() => []) : []; const staleUpdateLaunchdJobs = opts.deep && process.platform === "darwin" ? await loadLaunchdModule().then(({ findStaleOpenClawUpdateLaunchdJobs }) => findStaleOpenClawUpdateLaunchdJobs(serviceEnv)).catch(() => []) : []; const timeoutMs = parseStrictPositiveInteger(opts.rpc.timeout ?? void 0) ?? Math.max(1e4, daemonCfg.gateway?.handshakeTimeoutMs ?? 0); const tlsEnabled = daemonCfg.gateway?.tls?.enabled === true; const shouldUseLocalTlsRuntime = opts.probe && !probeUrlOverride && tlsEnabled; const tlsRuntime = shouldUseLocalTlsRuntime ? await loadGatewayTlsModule().then(({ loadGatewayTlsRuntime }) => loadGatewayTlsRuntime(daemonCfg.gateway?.tls)) : void 0; let daemonProbeAuth; let rpcAuthWarning; let allowRpcConfigCredentials = true; let skippedProbeAuthForDisabledExecSecretRef = false; if (opts.probe) { const probeMode = daemonCfg.gateway?.mode === "remote" ? "remote" : "local"; const explicitAuth = { token: opts.rpc.token, password: opts.rpc.password }; if (opts.allowExecSecretRefs !== false || !hasActiveGatewayExecProbeCredential({ cfg: daemonCfg, env: mergedDaemonEnv, explicitAuth, mode: probeMode })) { const probeAuthResolution = await loadGatewayProbeAuthModule().then(({ resolveGatewayProbeAuthSafeWithSecretInputs }) => resolveGatewayProbeAuthSafeWithSecretInputs({ cfg: daemonCfg, mode: probeMode, env: mergedDaemonEnv, explicitAuth })); daemonProbeAuth = probeAuthResolution.auth; rpcAuthWarning = probeAuthResolution.warning; } else { allowRpcConfigCredentials = false; skippedProbeAuthForDisabledExecSecretRef = true; rpcAuthWarning = "Gateway probe auth skipped because gateway credentials use an exec SecretRef and exec SecretRefs are disabled for this status request."; } } const rpc = opts.probe ? await loadDaemonProbeModule().then(({ probeGatewayStatus }) => probeGatewayStatus({ url: gateway.probeUrl, token: daemonProbeAuth?.token, password: daemonProbeAuth?.password, config: daemonCfg, tlsFingerprint: shouldUseLocalTlsRuntime && tlsRuntime?.enabled ? tlsRuntime.fingerprintSha256 : void 0, preauthHandshakeTimeoutMs: daemonCfg.gateway?.handshakeTimeoutMs, timeoutMs, json: opts.rpc.json, requireRpc: opts.requireRpc, allowRpcConfigCredentials, configPath: daemonConfigSummary.path })) : void 0; if (rpc?.ok && !skippedProbeAuthForDisabledExecSecretRef) rpcAuthWarning = void 0; const health = opts.probe && loaded && rpc?.ok !== true ? await loadRestartHealthModule().then(({ inspectGatewayRestart }) => inspectGatewayRestart({ service, port: daemonPort, env: serviceEnv })).catch(() => void 0) : void 0; const gatewayVersion = opts.probe ? (rpc && "server" in rpc ? rpc.server?.version : void 0) ?? (rpc && "version" in rpc ? rpc.version : void 0) ?? null : void 0; let lastError; if (loaded && runtime?.status === "running" && portStatus && portStatus.status !== "busy") lastError = await readLastGatewayErrorLine(mergedDaemonEnv) ?? void 0; let pluginVersionDrift; if (daemonCfg.gateway?.mode !== "remote" && !probeUrlOverride) try { const installRecords = await loadInstalledPluginIndexInstallRecords({ env: mergedDaemonEnv }); pluginVersionDrift = detectPluginVersionDrift({ gatewayVersion: gatewayVersion ?? VERSION, installRecords, config: daemonCfg }); } catch { pluginVersionDrift = void 0; } return { cli: resolveCliStatusSummary(), logFile: resolveConfiguredLogFilePath(cliCfg), service: { label: service.label, loaded, loadedText: service.loadedText, notLoadedText: service.notLoadedText, command, runtime, configAudit, ...restartHandoff ? { restartHandoff } : {}, ...staleUpdateLaunchdJobs.length > 0 ? { staleUpdateLaunchdJobs } : {} }, config: { cli: cliConfigSummary, daemon: daemonConfigSummary, ...configMismatch ? { mismatch: true } : {} }, gateway: { ...gateway, ...opts.probe ? { version: gatewayVersion } : {} }, port: portStatus, ...portCliStatus ? { portCli: portCliStatus } : {}, ...establishedClients ? { connections: establishedClients } : {}, lastError, ...rpc ? { rpc: { ...rpc, url: gateway.probeUrl, ...rpcAuthWarning ? { authWarning: rpcAuthWarning } : {} } } : {}, ...health ? { health: { healthy: health.healthy, staleGatewayPids: health.staleGatewayPids } } : {}, extraServices, ...pluginVersionDrift ? { pluginVersionDrift } : {} }; } function renderPortDiagnosticsForCli(status, rpcOk) { 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 }); } function resolvePortListeningAddresses(status) { return Array.from(new Set(status.port?.listeners?.map((l) => l.address ? normalizeListenerAddress(l.address) : "").filter((v) => Boolean(v)) ?? [])); } //#endregion export { renderPortDiagnosticsForCli as n, resolvePortListeningAddresses as r, gatherDaemonStatus as t };