openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
387 lines (386 loc) • 17.5 kB
JavaScript
import { d as resolveGatewayPort } from "./paths-mvMm5bYV.js";
import { t as formatCliCommand } from "./command-format-CKGmlpAQ.js";
import { _ as sleep } from "./utils-CCC-BEJH.js";
import { g as resolveNodeLaunchAgentLabel, m as resolveGatewayWindowsTaskName, p as resolveGatewaySystemdServiceName, u as resolveGatewayLaunchAgentLabel } from "./constants-e049XCW0.js";
import { n as buildGatewayInstallPlan, r as gatewayInstallErrorHint } from "./auth-install-policy-O7nc7i9I.js";
import { h as classifySystemdUnavailableDetail, o as isSystemdUserServiceAvailable } from "./systemd-BXTM6gEb.js";
import { a as getResolvedLoggerSettings } from "./logger-Brhy0cvC.js";
import { n as GATEWAY_DAEMON_RUNTIME_OPTIONS, t as DEFAULT_GATEWAY_DAEMON_RUNTIME } from "./daemon-runtime-C76za6vm.js";
import "./config-C9RxTsn1.js";
import { t as resolveGatewayInstallToken } from "./gateway-install-token-jVqLCOms.js";
import { a as inspectPortConnections, d as isExpectedGatewayListeners, l as formatPortDiagnostics, o as inspectPortUsage } from "./ports-CoxQQbiY.js";
import { h as repairLaunchAgentBootstrap, l as launchAgentPlistExists, s as isLaunchAgentLoaded } from "./launchd-FSKBDZ2p.js";
import { i as resolveGatewayService, t as describeGatewayServiceRestart } from "./service-MPqQWJ3s.js";
import { a as isSystemdUnavailableDetail, i as resolveDaemonContainerContext, o as renderSystemdUnavailableHints, r as formatRuntimeStatus, t as buildPlatformRuntimeLogHints } from "./runtime-hints-BXTNKBW9.js";
import { r as isWSLEnv, t as isWSL } from "./wsl-Bb_g7JpO.js";
import { n as isSystemdCgroupHygieneRisk, t as getSystemdCgroupHygieneSummary } from "./service-runtime-BCh8yaEB.js";
import { t as readLastGatewayErrorLine } from "./diagnostics-C0o1p3sH.js";
import { n as readGatewayRestartHandoffSync, t as formatGatewayRestartHandoffDiagnostic } from "./restart-handoff-B55foNq9.js";
import { r as findSystemGatewayServices } from "./inspect-CrIS0bA0.js";
import "./logging-CvNAh8ZC.js";
import { t as note } from "./note-BRSJp0UF.js";
import { n as formatHealthCheckFailure } from "./health-format-ebE8IYQV.js";
import { i as healthCommand } from "./health-DeCbGguB.js";
import { a as resolveServiceRepairPolicy, i as isServiceRepairExternallyManaged, n as SERVICE_REPAIR_POLICY_ENV, r as confirmDoctorServiceRepair, t as EXTERNAL_SERVICE_REPAIR_NOTE } from "./doctor-service-repair-policy-D0NFzdqc.js";
//#region src/commands/doctor-format.ts
/** Formatting helpers for gateway runtime summaries and doctor repair hints. */
/** Formats the platform-specific gateway service runtime into a compact status line. */
function formatGatewayRuntimeSummary(runtime) {
return formatRuntimeStatus(runtime);
}
/** Builds follow-up hints for stopped, missing, or unhealthy gateway service runtimes. */
function buildGatewayRuntimeHints(runtime, options = {}) {
const hints = [];
if (!runtime) return hints;
const platform = options.platform ?? process.platform;
const env = options.env ?? process.env;
const container = Boolean(resolveDaemonContainerContext(env));
const fileLog = (() => {
try {
return getResolvedLoggerSettings().file;
} catch {
return null;
}
})();
if (platform === "linux" && isSystemdUnavailableDetail(runtime.detail)) {
hints.push(...renderSystemdUnavailableHints({
wsl: isWSLEnv(),
kind: classifySystemdUnavailableDetail(runtime.detail),
container
}));
if (fileLog) hints.push(`File logs: ${fileLog}`);
return hints;
}
if (runtime.cachedLabel && platform === "darwin") {
const label = resolveGatewayLaunchAgentLabel(env.OPENCLAW_PROFILE);
hints.push(`LaunchAgent label cached but plist missing. Clear with: launchctl bootout gui/$UID/${label}`);
hints.push(`Then reinstall: ${formatCliCommand("openclaw gateway install", env)}`);
}
if (runtime.missingUnit) {
hints.push(`Service not installed. Run: ${formatCliCommand("openclaw gateway install", env)}`);
if (fileLog) hints.push(`File logs: ${fileLog}`);
return hints;
}
if (runtime.missingGuiSession && platform === "darwin") {
hints.push("LaunchAgent requires a logged-in macOS GUI session; SSH/headless/sudo shells cannot bootstrap gui/$UID.");
hints.push(`Sign in to the macOS desktop as this user, then run: ${formatCliCommand("openclaw gateway restart", env)}`);
hints.push("For headless VM setups, enable auto-login for the target user or use a custom LaunchDaemon (not shipped).");
if (fileLog) hints.push(`File logs: ${fileLog}`);
return hints;
}
if (runtime.missingSupervision && platform === "darwin") {
hints.push(`LaunchAgent installed but not loaded. Run: ${formatCliCommand("openclaw gateway restart", env)}`);
if (fileLog) hints.push(`File logs: ${fileLog}`);
return hints;
}
if (runtime.status === "stopped") {
hints.push("Service is loaded but not running (likely exited immediately).");
if (fileLog) hints.push(`File logs: ${fileLog}`);
hints.push(...buildPlatformRuntimeLogHints({
platform,
env,
systemdServiceName: resolveGatewaySystemdServiceName(env.OPENCLAW_PROFILE),
windowsTaskName: resolveGatewayWindowsTaskName(env.OPENCLAW_PROFILE)
}));
}
if (platform === "linux" && isSystemdCgroupHygieneRisk(runtime.systemd)) {
const unit = runtime.systemd?.unit ?? `${resolveGatewaySystemdServiceName(env.OPENCLAW_PROFILE)}.service`;
const summary = getSystemdCgroupHygieneSummary(runtime.systemd);
if (summary) hints.push(`Systemd cgroup hygiene looks elevated: ${summary}.`, "This usually means old helper or browser processes may still be attached to the gateway service.", `Run: systemctl --user show ${unit} -p KillMode -p TasksCurrent -p MemoryCurrent -p MainPID`, `Run: systemd-cgls --user-unit ${unit}`, `After reviewing service settings, run: ${formatCliCommand("openclaw gateway restart", env)}`);
}
return hints;
}
//#endregion
//#region src/commands/doctor-gateway-daemon-flow.ts
/** Doctor gateway daemon repair flow for service install, bootstrap, restart, and port hints. */
function noteGatewayRuntime(serviceRuntime, env) {
const summary = formatGatewayRuntimeSummary(serviceRuntime);
const hints = buildGatewayRuntimeHints(serviceRuntime, {
platform: process.platform,
env
});
if (!summary && hints.length === 0) return;
const lines = [];
if (summary) lines.push(`Runtime: ${summary}`);
lines.push(...hints);
note(lines.join("\n"), "Gateway");
}
async function maybeRepairLaunchAgentBootstrap(params) {
if (process.platform !== "darwin") return { status: "skipped" };
if (!await launchAgentPlistExists(params.env)) return { status: "skipped" };
if (await isLaunchAgentLoaded({ env: params.env })) return { status: "skipped" };
note("LaunchAgent is installed but not loaded in launchd.", `${params.title} LaunchAgent`);
if (params.serviceRepairExternal) {
note(EXTERNAL_SERVICE_REPAIR_NOTE, `${params.title} LaunchAgent`);
return { status: "skipped" };
}
if (!await confirmDoctorServiceRepair(params.prompter, {
message: `Repair ${params.title} LaunchAgent bootstrap now?`,
initialValue: true
})) return { status: "skipped" };
params.runtime.log(`Bootstrapping ${params.title} LaunchAgent...`);
const repair = await repairLaunchAgentBootstrap({ env: params.env });
if (!repair.ok) {
if (repair.status === "gui-session-unavailable") return {
status: "gui-session-unavailable",
detail: repair.detail
};
params.runtime.error(`${params.title} LaunchAgent bootstrap failed: ${repair.detail ?? "unknown error"}`);
return { status: "skipped" };
}
if (!await isLaunchAgentLoaded({ env: params.env })) {
params.runtime.error(`${params.title} LaunchAgent still not loaded after repair.`);
return { status: "skipped" };
}
note(`${params.title} LaunchAgent repaired.`, `${params.title} LaunchAgent`);
return { status: "repaired" };
}
function renderBlockingSystemGatewayServices(services) {
return [
"System-level OpenClaw gateway service detected while the user gateway service is not installed.",
...services.map((svc) => `- ${svc.label} (${svc.detail})`),
"OpenClaw will not install a second user-level gateway service automatically.",
"Run `openclaw gateway status --deep` or `openclaw doctor --deep` to inspect duplicate services.",
`Set ${SERVICE_REPAIR_POLICY_ENV}=external if a system supervisor owns the gateway lifecycle.`
].join("\n");
}
function renderEstablishedGatewayConnections(connections) {
return [
"Established Gateway TCP clients detected:",
...connections.slice(0, 8).map((connection) => {
return `- ${connection.pid ? `pid=${connection.pid}` : "pid=?"} ${connection.direction}${connection.command ? ` ${connection.command}` : ""}${connection.address ? ` ${connection.address}` : ""}${connection.commandLine ? ` cmd=${connection.commandLine}` : ""}`;
}),
...connections.length > 8 ? [`- ... ${connections.length - 8} more connection(s)`] : [],
"If logs show protocol mismatch after rollback, stop stale OpenClaw client processes listed here and rerun doctor."
].join("\n");
}
async function maybeReportEstablishedGatewayClients(params) {
if (!params.deep || params.cfg.gateway?.mode === "remote") return;
const establishedClients = (await inspectPortConnections(params.port ?? resolveGatewayPort(params.cfg, process.env)).catch(() => null))?.connections.filter((connection) => connection.direction !== "server");
if (establishedClients && establishedClients.length > 0) note(renderEstablishedGatewayConnections(establishedClients), "Gateway clients");
}
/**
* Repairs or diagnoses the local gateway service after the health check fails.
*
* Remote gateway mode is only diagnosed; local mode may bootstrap launchd, install missing
* services, report port conflicts, or restart unhealthy supervision when policy allows.
*/
async function maybeRepairGatewayDaemon(params) {
if (params.healthOk) {
await maybeReportEstablishedGatewayClients({
cfg: params.cfg,
deep: params.options.deep ?? false
});
return;
}
if (params.healthSkipped && params.cfg.gateway?.mode === "remote") return;
const serviceRepairPolicy = resolveServiceRepairPolicy();
const serviceRepairExternal = isServiceRepairExternallyManaged(serviceRepairPolicy);
const service = resolveGatewayService();
const isLocalDarwinGateway = process.platform === "darwin" && params.cfg.gateway?.mode !== "remote";
let loaded;
try {
loaded = await service.isLoaded({ env: process.env });
} catch {
loaded = false;
}
let serviceRuntime;
const command = params.options.deep ? await Promise.resolve(service.readCommand(process.env)).catch(() => null) : null;
const serviceEnv = command?.environment ? {
...process.env,
...command.environment
} : process.env;
if (loaded || isLocalDarwinGateway) serviceRuntime = await service.readRuntime(serviceEnv).catch(() => void 0);
if (params.options.deep) {
const handoff = readGatewayRestartHandoffSync(serviceEnv);
if (handoff) note(formatGatewayRestartHandoffDiagnostic(handoff), "Gateway");
}
if (isLocalDarwinGateway) {
const gatewayRepair = serviceRuntime?.missingGuiSession ? {
status: "gui-session-unavailable",
detail: serviceRuntime.detail ?? ""
} : await maybeRepairLaunchAgentBootstrap({
env: process.env,
title: "Gateway",
runtime: params.runtime,
prompter: params.prompter,
serviceRepairExternal
});
await maybeRepairLaunchAgentBootstrap({
env: {
...process.env,
OPENCLAW_LAUNCHD_LABEL: resolveNodeLaunchAgentLabel()
},
title: "Node",
runtime: params.runtime,
prompter: params.prompter,
serviceRepairExternal
});
if (gatewayRepair.status === "gui-session-unavailable") serviceRuntime = {
status: "unknown",
detail: gatewayRepair.detail || serviceRuntime?.detail,
missingSupervision: true,
missingGuiSession: true
};
if (gatewayRepair.status === "repaired") {
loaded = await service.isLoaded({ env: process.env });
if (loaded) serviceRuntime = await service.readRuntime(process.env).catch(() => void 0);
}
}
if (params.cfg.gateway?.mode !== "remote") {
const port = resolveGatewayPort(params.cfg, process.env);
const diagnostics = await inspectPortUsage(port);
await maybeReportEstablishedGatewayClients({
cfg: params.cfg,
deep: params.options.deep ?? false,
port
});
if (diagnostics.status === "busy" && !isExpectedGatewayListeners(diagnostics.listeners, diagnostics.port)) note(formatPortDiagnostics(diagnostics).join("\n"), "Gateway port");
else if (loaded && serviceRuntime?.status === "running") {
const lastError = await readLastGatewayErrorLine(process.env);
if (lastError) note(`Last gateway error: ${lastError}`, "Gateway");
}
}
if (!loaded) {
if (isLocalDarwinGateway && (serviceRuntime?.missingGuiSession || serviceRuntime?.missingSupervision || serviceRuntime?.cachedLabel)) {
noteGatewayRuntime(serviceRuntime, process.env);
return;
}
if (process.platform === "linux") {
if (!await isSystemdUserServiceAvailable().catch(() => false)) {
note(renderSystemdUnavailableHints({
wsl: await isWSL(),
kind: "generic_unavailable"
}).join("\n"), "Gateway");
return;
}
}
note("Gateway service not installed.", "Gateway");
if (params.cfg.gateway?.mode !== "remote") {
if (process.platform === "linux") {
const systemGatewayServices = await findSystemGatewayServices();
if (systemGatewayServices.length > 0) {
note(renderBlockingSystemGatewayServices(systemGatewayServices), "Gateway");
return;
}
}
if (serviceRepairExternal) {
note(EXTERNAL_SERVICE_REPAIR_NOTE, "Gateway");
return;
}
const install = await confirmDoctorServiceRepair(params.prompter, {
message: "Install gateway service now?",
initialValue: true,
requiresInteractiveConfirmation: true
}, serviceRepairPolicy);
if (!install) note(`Run ${formatCliCommand("openclaw gateway install")} when you want to install the gateway service.`, "Gateway");
if (install) {
const daemonRuntime = await params.prompter.select({
message: "Gateway service runtime",
options: GATEWAY_DAEMON_RUNTIME_OPTIONS,
initialValue: DEFAULT_GATEWAY_DAEMON_RUNTIME
}, DEFAULT_GATEWAY_DAEMON_RUNTIME);
const tokenResolution = await resolveGatewayInstallToken({
config: params.cfg,
env: process.env
});
for (const warning of tokenResolution.warnings) note(warning, "Gateway");
if (tokenResolution.unavailableReason) {
note([
"Gateway service install aborted.",
tokenResolution.unavailableReason,
"Fix gateway auth config/token input and rerun doctor."
].join("\n"), "Gateway");
return;
}
const port = resolveGatewayPort(params.cfg, process.env);
const { programArguments, workingDirectory, environment, environmentValueSources } = await buildGatewayInstallPlan({
env: process.env,
port,
runtime: daemonRuntime,
warn: (message, title) => note(message, title),
config: params.cfg
});
try {
await service.install({
env: process.env,
stdout: process.stdout,
programArguments,
workingDirectory,
environment,
environmentValueSources
});
} catch (err) {
note(`Gateway service install failed: ${String(err)}`, "Gateway");
note(gatewayInstallErrorHint(), "Gateway");
}
}
}
return;
}
noteGatewayRuntime(serviceRuntime, process.env);
if (serviceRuntime?.status !== "running") {
if (params.healthSkipped && serviceRuntime?.status !== "stopped") return;
if (serviceRepairExternal) {
note(EXTERNAL_SERVICE_REPAIR_NOTE, "Gateway");
return;
}
if (await confirmDoctorServiceRepair(params.prompter, {
message: "Start gateway service now?",
initialValue: true
}, serviceRepairPolicy)) {
const restartStatus = describeGatewayServiceRestart("Gateway", await service.restart({
env: process.env,
stdout: process.stdout
}));
if (!restartStatus.scheduled) await sleep(1500);
else note(restartStatus.message, "Gateway");
}
}
if (process.platform === "darwin") {
const label = resolveGatewayLaunchAgentLabel(process.env.OPENCLAW_PROFILE);
note(`LaunchAgent loaded; stopping requires "${formatCliCommand("openclaw gateway stop")}" or launchctl bootout gui/$UID/${label}.`, "Gateway");
}
if (serviceRuntime?.status === "running") {
if (params.healthSkipped) return;
if (serviceRepairExternal) {
note(EXTERNAL_SERVICE_REPAIR_NOTE, "Gateway");
return;
}
if (readGatewayRestartHandoffSync(serviceEnv)) try {
await healthCommand({
json: false,
timeoutMs: 1e4
}, params.runtime);
note("Gateway is healthy after recent restart; skipping restart prompt.", "Gateway");
return;
} catch {}
if (await confirmDoctorServiceRepair(params.prompter, {
message: "Restart gateway service now?",
initialValue: false
}, serviceRepairPolicy)) {
const restartStatus = describeGatewayServiceRestart("Gateway", await service.restart({
env: process.env,
stdout: process.stdout
}));
if (restartStatus.scheduled) {
note(restartStatus.message, "Gateway");
return;
}
await sleep(1500);
try {
await healthCommand({
json: false,
timeoutMs: 1e4
}, params.runtime);
} catch (err) {
if (String(err).includes("gateway closed")) {
note("Gateway not running.", "Gateway");
note(params.gatewayDetailsMessage, "Gateway connection");
} else params.runtime.error(formatHealthCheckFailure(err));
}
}
}
}
//#endregion
export { maybeRepairGatewayDaemon };