openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
481 lines (480 loc) • 21.9 kB
JavaScript
import { a as normalizeLowercaseStringOrEmpty, c as normalizeOptionalString } from "./string-coerce-mnp54Vah.js";
import { d as resolveGatewayPort, p as resolveIsNixMode } from "./paths-mvMm5bYV.js";
import { m as resolveSecretInputRef } from "./types.secrets-_0JOMGE5.js";
import { a as OPENCLAW_WRAPPER_ENV_KEY } from "./daemon-install-plan.shared-Dr7I1qWf.js";
import { n as buildGatewayInstallPlan } from "./auth-install-policy-O7nc7i9I.js";
import { S as readManagedServiceEnvKeysFromEnvironment, a as isSystemdUnitActive, p as uninstallLegacySystemdUnits } from "./systemd-BXTM6gEb.js";
import { a as resolveSystemNodeInfo, r as renderSystemNodeWarning } from "./runtime-paths-DS_J3TGI.js";
import { t as DEFAULT_GATEWAY_DAEMON_RUNTIME } from "./daemon-runtime-C76za6vm.js";
import { a as trimToUndefined } from "./credential-planner-wESmL_7M.js";
import "./credentials-irvgw8Le.js";
import { t as resolveGatewayAuthToken } from "./auth-token-resolution-AiDzF8d9.js";
import { i as replaceConfigFile } from "./config-C9RxTsn1.js";
import { a as readEmbeddedGatewayToken, i as needsNodeRuntimeMigration, n as auditGatewayServiceConfig, t as SERVICE_AUDIT_CODES } from "./service-audit-Bj05_7pm.js";
import { i as resolveGatewayService } from "./service-MPqQWJ3s.js";
import { i as renderGatewayServiceCleanupHints, n as findExtraGatewayServices } from "./inspect-CrIS0bA0.js";
import { t as note } from "./note-BRSJp0UF.js";
import { t as summarizeGatewayServiceLayout } from "./service-layout-YF-1M350.js";
import { a as resolveServiceRepairPolicy, i as isServiceRepairExternallyManaged, r as confirmDoctorServiceRepair, t as EXTERNAL_SERVICE_REPAIR_NOTE } from "./doctor-service-repair-policy-D0NFzdqc.js";
import { t as isDoctorUpdateRepairMode } from "./doctor-repair-mode-D9ylXEDp.js";
import path from "node:path";
import fs from "node:fs/promises";
import os from "node:os";
import { execFile } from "node:child_process";
import { promisify } from "node:util";
//#region src/commands/doctor-gateway-auth-token.ts
/**
* Resolves the token a managed gateway service can receive at install/update time.
*
* Exec SecretRefs are skipped by default because the service installer cannot safely evaluate
* arbitrary commands; OPENCLAW_GATEWAY_TOKEN remains an explicit env override.
*/
async function resolveGatewayAuthTokenForService(cfg, env, options = {}) {
if (resolveSecretInputRef({
value: cfg.gateway?.auth?.token,
defaults: cfg.secrets?.defaults
}).ref?.source === "exec" && options.allowExecSecretRefs !== true) {
const envToken = trimToUndefined(env.OPENCLAW_GATEWAY_TOKEN);
return envToken ? { token: envToken } : {};
}
const resolved = await resolveGatewayAuthToken({
cfg,
env,
unresolvedReasonStyle: "detailed",
envFallback: "always"
});
if (resolved.token) return { token: resolved.token };
if (!resolved.secretRefConfigured) return {};
if (resolved.unresolvedRefReason?.includes("resolved to an empty value")) return { unavailableReason: resolved.unresolvedRefReason };
return { unavailableReason: `gateway.auth.token SecretRef is configured but unresolved (${resolved.unresolvedRefReason ?? "unknown reason"}).` };
}
//#endregion
//#region src/commands/doctor-gateway-services.ts
/** Doctor repairs for installed gateway service config and duplicate legacy services. */
const execFileAsync = promisify(execFile);
const EXECSTART_REPAIR_CODES = new Set([SERVICE_AUDIT_CODES.gatewayCommandMissing, SERVICE_AUDIT_CODES.gatewayEntrypointMismatch]);
function detectGatewayRuntime(programArguments) {
const first = programArguments?.[0];
if (first) {
const base = normalizeLowercaseStringOrEmpty(path.basename(first));
if (base === "bun" || base === "bun.exe") return "bun";
if (base === "node" || base === "node.exe") return "node";
}
return DEFAULT_GATEWAY_DAEMON_RUNTIME;
}
function findGatewayEntrypoint(programArguments) {
if (!programArguments || programArguments.length === 0) return null;
const gatewayIndex = programArguments.indexOf("gateway");
if (gatewayIndex <= 0) return null;
return programArguments[gatewayIndex - 1] ?? null;
}
function buildGatewayServiceRepairEnv(command) {
const wrapperPath = command?.environment?.[OPENCLAW_WRAPPER_ENV_KEY]?.trim();
if (!wrapperPath || Object.hasOwn(process.env, "OPENCLAW_WRAPPER")) return process.env;
return {
...process.env,
[OPENCLAW_WRAPPER_ENV_KEY]: wrapperPath
};
}
function resolveGatewayServiceWrapperPath(command) {
return normalizeOptionalString(command?.environment?.["OPENCLAW_WRAPPER"]) ?? null;
}
async function buildExpectedGatewayServicePlan(params) {
return buildGatewayInstallPlan({
env: params.serviceInstallEnv,
port: params.port,
runtime: params.runtime,
nodePath: params.nodePath,
existingEnvironment: params.command.environment,
existingEnvironmentValueSources: params.command.environmentValueSources,
warn: (message, title) => note(message, title),
config: params.cfg
});
}
async function buildGatewayServiceAuditInputs(params) {
const port = resolveGatewayPort(params.cfg, process.env);
const runtimeChoice = detectGatewayRuntime(params.command.programArguments);
const expectedPlan = await buildExpectedGatewayServicePlan({
cfg: params.cfg,
command: params.command,
serviceInstallEnv: params.serviceInstallEnv,
port,
runtime: runtimeChoice
});
return {
expectedManagedServiceEnvKeys: readManagedServiceEnvKeysFromEnvironment(expectedPlan.environment),
expectedPlan,
port,
runtimeChoice
};
}
async function normalizeExecutablePath(value) {
const resolvedPath = path.resolve(value);
try {
return await fs.realpath(resolvedPath);
} catch {
return resolvedPath;
}
}
function extractDetailPath(detail, prefix) {
if (!detail.startsWith(prefix)) return null;
const value = detail.slice(prefix.length).trim();
return value.length > 0 ? value : null;
}
function isExecStartRepairIssue(issue) {
return EXECSTART_REPAIR_CODES.has(issue.code);
}
function resolveSystemdScopeFromServicePath(sourcePath) {
const normalized = sourcePath?.replaceAll("\\", "/") ?? "";
return normalized.startsWith("/etc/systemd/") || normalized.startsWith("/usr/lib/systemd/") || normalized.startsWith("/lib/systemd/") ? "system" : "user";
}
function resolveSystemdUnitNameFromServicePath(sourcePath) {
const base = sourcePath ? path.posix.basename(sourcePath.replaceAll("\\", "/")) : "";
return base.endsWith(".service") ? base : "openclaw-gateway.service";
}
function shouldDeferUpdateModeSystemdServiceRepair(params) {
return process.platform === "linux" && isDoctorUpdateRepairMode(params.repairMode) && !params.shouldForce;
}
async function suppressRunningSystemdExecStartRepairs(params) {
if (process.platform !== "linux") return false;
if (!params.issues.some(isExecStartRepairIssue)) return false;
const unitName = resolveSystemdUnitNameFromServicePath(params.command.sourcePath);
const scope = resolveSystemdScopeFromServicePath(params.command.sourcePath);
if (!await isSystemdUnitActive(process.env, unitName, scope)) return false;
const before = params.issues.length;
params.issues.splice(0, params.issues.length, ...params.issues.filter((issue) => !isExecStartRepairIssue(issue)));
if (params.issues.length !== before) note(`Gateway service ${unitName} is running; skipped command/entrypoint rewrites for this doctor pass.`, "Gateway service config");
return true;
}
async function filterInactiveExtraGatewayServices(services) {
if (process.platform !== "linux") return services;
const activeOrLegacy = [];
for (const svc of services) {
if (svc.platform !== "linux" || svc.legacy === true) {
activeOrLegacy.push(svc);
continue;
}
if (await isSystemdUnitActive(process.env, svc.label, svc.scope)) activeOrLegacy.push(svc);
}
return activeOrLegacy;
}
async function cleanupLegacyLaunchdService(params) {
await execFileAsync("launchctl", [
"bootout",
typeof process.getuid === "function" ? `gui/${process.getuid()}` : "gui/501",
params.plistPath
]).catch(() => void 0);
await execFileAsync("launchctl", ["unload", params.plistPath]).catch(() => void 0);
const trashDir = path.join(os.homedir(), ".Trash");
try {
await fs.mkdir(trashDir, { recursive: true });
} catch {}
try {
await fs.access(params.plistPath);
} catch {
return null;
}
const dest = path.join(trashDir, `${params.label}-${Date.now()}.plist`);
try {
await fs.rename(params.plistPath, dest);
return dest;
} catch {
return null;
}
}
function classifyLegacyServices(legacyServices) {
const darwinUserServices = [];
const linuxUserServices = [];
const failed = [];
for (const svc of legacyServices) {
if (svc.platform === "darwin") {
if (svc.scope === "user") darwinUserServices.push(svc);
else failed.push(`${svc.label} (${svc.scope})`);
continue;
}
if (svc.platform === "linux") {
if (svc.scope === "user") linuxUserServices.push(svc);
else failed.push(`${svc.label} (${svc.scope})`);
continue;
}
failed.push(`${svc.label} (${svc.platform})`);
}
return {
darwinUserServices,
linuxUserServices,
failed
};
}
async function cleanupLegacyDarwinServices(services) {
const removed = [];
const failed = [];
for (const svc of services) {
const plistPath = extractDetailPath(svc.detail, "plist:");
if (!plistPath) {
failed.push(`${svc.label} (missing plist path)`);
continue;
}
const dest = await cleanupLegacyLaunchdService({
label: svc.label,
plistPath
});
removed.push(dest ? `${svc.label} -> ${dest}` : svc.label);
}
return {
removed,
failed
};
}
async function cleanupLegacyLinuxUserServices(services, runtime) {
const removed = [];
const failed = [];
try {
const removedUnits = await uninstallLegacySystemdUnits({
env: process.env,
stdout: process.stdout
});
const removedByLabel = new Map(removedUnits.map((unit) => [`${unit.name}.service`, unit]));
for (const svc of services) {
const removedUnit = removedByLabel.get(svc.label);
if (!removedUnit) {
failed.push(`${svc.label} (legacy unit name not recognized)`);
continue;
}
removed.push(`${svc.label} -> ${removedUnit.unitPath}`);
}
} catch (err) {
runtime.error(`Legacy Linux gateway cleanup failed: ${String(err)}`);
for (const svc of services) failed.push(`${svc.label} (linux cleanup failed)`);
}
return {
removed,
failed
};
}
/**
* Audits and optionally rewrites the installed local gateway service configuration.
*
* The repair preserves managed env sources, avoids Nix/remote installs, and can stage service
* updates during updater repair mode instead of immediately installing them.
*/
async function maybeRepairGatewayServiceConfig(cfg, mode, runtime, prompter, options = {}) {
if (resolveIsNixMode(process.env)) {
note("Nix mode detected; skip service updates.", "Gateway");
return;
}
if (mode === "remote") {
note("Gateway mode is remote; skipped local service audit.", "Gateway");
return;
}
const service = resolveGatewayService();
let command;
try {
command = await service.readCommand(process.env);
} catch {
command = null;
}
if (!command) return;
const serviceInstallEnv = buildGatewayServiceRepairEnv(command);
const serviceWrapperPath = resolveGatewayServiceWrapperPath(command);
if (serviceWrapperPath) note(`Gateway service invokes ${OPENCLAW_WRAPPER_ENV_KEY}: ${serviceWrapperPath}`, "Gateway");
const serviceLayout = await summarizeGatewayServiceLayout(command);
const sourceCheckoutWarning = serviceLayout?.entrypointSourceCheckout ? [`Gateway service entrypoint resolves to a source checkout: ${serviceLayout.packageRootReal ?? serviceLayout.packageRoot ?? serviceLayout.entrypointReal ?? serviceLayout.entrypoint}.`, "Run `openclaw doctor --fix` from the intended package install, or reinstall the gateway service with `openclaw gateway install --force`."].join("\n") : null;
const tokenRefConfigured = Boolean(resolveSecretInputRef({
value: cfg.gateway?.auth?.token,
defaults: cfg.secrets?.defaults
}).ref);
const gatewayTokenResolution = await resolveGatewayAuthTokenForService(cfg, process.env, { allowExecSecretRefs: options.allowExecSecretRefs === true });
if (gatewayTokenResolution.unavailableReason) note(`Unable to verify gateway service token drift: ${gatewayTokenResolution.unavailableReason}`, "Gateway service config");
const expectedGatewayToken = tokenRefConfigured ? void 0 : gatewayTokenResolution.token;
const { expectedManagedServiceEnvKeys, expectedPlan, port, runtimeChoice } = await buildGatewayServiceAuditInputs({
cfg,
command,
serviceInstallEnv
});
const audit = await auditGatewayServiceConfig({
env: process.env,
command,
expectedGatewayToken,
expectedManagedServiceEnvKeys,
expectedServicePath: expectedPlan.environment.PATH,
expectedPort: port
});
const serviceToken = readEmbeddedGatewayToken(command);
if (tokenRefConfigured && serviceToken) audit.issues.push({
code: SERVICE_AUDIT_CODES.gatewayTokenMismatch,
message: "Gateway service OPENCLAW_GATEWAY_TOKEN should be unset when gateway.auth.token is SecretRef-managed",
detail: "service token is stale",
level: "recommended"
});
const needsNodeRuntime = needsNodeRuntimeMigration(audit.issues);
const systemNodeInfo = needsNodeRuntime ? await resolveSystemNodeInfo({ env: process.env }) : null;
const systemNodePath = systemNodeInfo?.supported ? systemNodeInfo.path : null;
if (needsNodeRuntime && !systemNodePath && runtimeChoice !== "node") {
const warning = renderSystemNodeWarning(systemNodeInfo);
if (warning) note(warning, "Gateway runtime");
else note("System Node 22 LTS (22.19+) or Node 24 not found. Install via Homebrew/apt/choco and rerun doctor to migrate off Bun/version managers.", "Gateway runtime");
}
const { programArguments } = needsNodeRuntime && systemNodePath ? await buildExpectedGatewayServicePlan({
cfg,
command,
serviceInstallEnv,
port,
runtime: "node",
nodePath: systemNodePath
}) : expectedPlan;
const expectedEntrypoint = findGatewayEntrypoint(programArguments);
const currentEntrypoint = findGatewayEntrypoint(command.programArguments);
const normalizedExpectedEntrypoint = expectedEntrypoint ? await normalizeExecutablePath(expectedEntrypoint) : null;
const normalizedCurrentEntrypoint = currentEntrypoint ? await normalizeExecutablePath(currentEntrypoint) : null;
if (normalizedExpectedEntrypoint && normalizedCurrentEntrypoint && normalizedExpectedEntrypoint !== normalizedCurrentEntrypoint) audit.issues.push({
code: SERVICE_AUDIT_CODES.gatewayEntrypointMismatch,
message: "Gateway service entrypoint does not match the current install.",
detail: `${currentEntrypoint} -> ${expectedEntrypoint}`,
level: "recommended"
});
const serviceRewriteBlocked = await suppressRunningSystemdExecStartRepairs({
command,
issues: audit.issues
});
const hasEntrypointMismatch = audit.issues.some((issue) => issue.code === SERVICE_AUDIT_CODES.gatewayEntrypointMismatch);
const showSourceCheckoutWarning = sourceCheckoutWarning !== null && !hasEntrypointMismatch;
if (audit.issues.length === 0) {
if (sourceCheckoutWarning !== null && !hasEntrypointMismatch) note(sourceCheckoutWarning, "Gateway service config");
return;
}
const serviceRepairExternal = isServiceRepairExternallyManaged(resolveServiceRepairPolicy());
const consolidatedLines = [];
let emittedSourceCheckoutWarning = false;
if (sourceCheckoutWarning !== null && showSourceCheckoutWarning) {
consolidatedLines.push(sourceCheckoutWarning);
consolidatedLines.push("");
emittedSourceCheckoutWarning = true;
}
consolidatedLines.push(...audit.issues.map((issue) => issue.detail ? `- ${issue.message} (${issue.detail})` : `- ${issue.message}`));
note(consolidatedLines.join("\n"), "Gateway service config");
const needsAggressive = audit.issues.filter((issue) => issue.level === "aggressive").length > 0;
if (needsAggressive && !prompter.shouldForce) note("Custom or unexpected service edits detected. Rerun with --force to overwrite.", "Gateway service config");
if (serviceRepairExternal) {
note(EXTERNAL_SERVICE_REPAIR_NOTE, "Gateway service config");
return;
}
if (serviceRewriteBlocked) {
note("Gateway service is running; leaving supervisor metadata unchanged. Stop the service first or use `openclaw gateway install --force` when you want to replace the active launcher.", "Gateway service config");
return;
}
const updateRepairMode = isDoctorUpdateRepairMode(prompter.repairMode);
if (shouldDeferUpdateModeSystemdServiceRepair({
repairMode: prompter.repairMode,
shouldForce: prompter.shouldForce
})) {
note("Update-mode doctor detected gateway service drift but left the live systemd unit unchanged. Review the service file and run `openclaw gateway install --force` when you want OpenClaw to replace operator-owned systemd directives.", "Gateway service config");
return;
}
const repairMessage = needsAggressive ? "Overwrite gateway service config with current defaults now?" : "Update gateway service config to the recommended defaults now?";
if (!(updateRepairMode ? needsAggressive ? await prompter.confirmAggressiveAutoFix({
message: repairMessage,
initialValue: prompter.shouldForce
}) : await prompter.confirmAutoFix({
message: repairMessage,
initialValue: true
}) : await prompter.confirmRuntimeRepair({
message: repairMessage,
initialValue: needsAggressive ? prompter.shouldForce : true,
requiresInteractiveConfirmation: true
}))) {
if (!emittedSourceCheckoutWarning) note("Run `openclaw gateway install --force` when you want to replace the gateway service definition.", "Gateway service config");
return;
}
const serviceEmbeddedToken = readEmbeddedGatewayToken(command);
const gatewayTokenForRepair = expectedGatewayToken ?? serviceEmbeddedToken;
const configuredGatewayToken = typeof cfg.gateway?.auth?.token === "string" ? normalizeOptionalString(cfg.gateway.auth.token) : void 0;
let cfgForServiceInstall = cfg;
if (!updateRepairMode && !tokenRefConfigured && !configuredGatewayToken && gatewayTokenForRepair) {
const nextCfg = {
...cfg,
gateway: {
...cfg.gateway,
auth: {
...cfg.gateway?.auth,
mode: cfg.gateway?.auth?.mode ?? "token",
token: gatewayTokenForRepair
}
}
};
try {
await replaceConfigFile({
nextConfig: nextCfg,
afterWrite: { mode: "auto" }
});
cfgForServiceInstall = nextCfg;
note(expectedGatewayToken ? "Persisted gateway.auth.token from environment before reinstalling service." : "Persisted gateway.auth.token from existing service definition before reinstalling service.", "Gateway");
} catch (err) {
runtime.error(`Failed to persist gateway.auth.token before service repair: ${String(err)}`);
return;
}
}
const updatedPort = resolveGatewayPort(cfgForServiceInstall, process.env);
const updatedPlan = await buildExpectedGatewayServicePlan({
cfg: cfgForServiceInstall,
command,
serviceInstallEnv,
port: updatedPort,
runtime: needsNodeRuntime && systemNodePath ? "node" : runtimeChoice,
nodePath: systemNodePath ?? void 0
});
try {
await (updateRepairMode ? service.stage : service.install)({
env: serviceInstallEnv,
stdout: process.stdout,
programArguments: updatedPlan.programArguments,
workingDirectory: updatedPlan.workingDirectory,
environment: updatedPlan.environment,
environmentValueSources: updatedPlan.environmentValueSources
});
} catch (err) {
runtime.error(`Gateway service update failed: ${String(err)}`);
}
}
/**
* Reports duplicate gateway-like services and removes legacy user services after confirmation.
*/
async function maybeScanExtraGatewayServices(options, runtime, prompter) {
const extraServices = await filterInactiveExtraGatewayServices(await findExtraGatewayServices(process.env, { deep: options.deep }));
if (extraServices.length === 0) return;
note(extraServices.map((svc) => `- ${svc.label} (${svc.scope}, ${svc.detail})`).join("\n"), "Other gateway-like services detected");
const legacyServices = extraServices.filter((svc) => svc.legacy === true);
if (legacyServices.length > 0) {
const serviceRepairPolicy = resolveServiceRepairPolicy();
const serviceRepairExternal = isServiceRepairExternallyManaged(serviceRepairPolicy);
if (serviceRepairExternal) note(EXTERNAL_SERVICE_REPAIR_NOTE, "Legacy gateway cleanup skipped");
if (serviceRepairExternal ? false : await confirmDoctorServiceRepair(prompter, {
message: "Remove legacy gateway services now?",
initialValue: true
}, serviceRepairPolicy)) {
const removed = [];
const { darwinUserServices, linuxUserServices, failed } = classifyLegacyServices(legacyServices);
if (darwinUserServices.length > 0) {
const result = await cleanupLegacyDarwinServices(darwinUserServices);
removed.push(...result.removed);
failed.push(...result.failed);
}
if (linuxUserServices.length > 0) {
const result = await cleanupLegacyLinuxUserServices(linuxUserServices, runtime);
removed.push(...result.removed);
failed.push(...result.failed);
}
if (removed.length > 0) note(removed.map((line) => `- ${line}`).join("\n"), "Legacy gateway removed");
if (failed.length > 0) note(failed.map((line) => `- ${line}`).join("\n"), "Legacy gateway cleanup skipped");
if (removed.length > 0) runtime.log("Legacy gateway services removed. Installing OpenClaw gateway next.");
}
}
const cleanupHints = renderGatewayServiceCleanupHints();
if (cleanupHints.length > 0) note(cleanupHints.map((hint) => `- ${hint}`).join("\n"), "Cleanup hints");
note([
"Recommendation: run a single gateway per machine for most setups.",
"One gateway supports multiple agents.",
"If you need multiple gateways (e.g., a rescue bot on the same host), isolate ports + config/state (see docs: /gateway#multiple-gateways-same-host)."
].join("\n"), "Gateway recommendation");
}
//#endregion
export { maybeRepairGatewayServiceConfig, maybeScanExtraGatewayServices };