@gguf/claw
Version:
Multi-channel AI gateway with extensible messaging integrations
1,184 lines (1,172 loc) • 77.1 kB
JavaScript
import { At as getResolvedLoggerSettings, Pt as CONFIG_PATH, Ut as resolveGatewayPort, Wt as resolveIsNixMode, Yt as resolveStateDir, _ as defaultRuntime, lt as shortenHomePath, n as isTruthyEnvValue, ot as resolveUserPath, pn as resolveRequiredHomeDir, qt as resolveOAuthDir, ut as sleep } from "./entry.js";
import { Gt as CODEX_CLI_PROFILE_ID, Ht as DEFAULT_PROVIDER, N as getModelRefStatus, S as updateAuthProfileStoreWithLock, U as resolveHooksGmailModel, V as resolveConfiguredModelRef, Vt as DEFAULT_MODEL, Wt as CLAUDE_CLI_PROFILE_ID, b as ensureAuthProfileStore, bt as resolveApiKeyForProvider, c as resolveApiKeyForProfile, p as repairOAuthProfileIdMismatch, s as resolveProfileUnusableUntilForDisplay } from "./auth-profiles-DFa1zzNy.js";
import { t as formatCliCommand } from "./command-format-D3syQOZg.js";
import { t as resolveOpenClawPackageRoot } from "./openclaw-root-B5pKN_cp.js";
import { n as runExec, t as runCommandWithTimeout } from "./exec-CBKBIMpA.js";
import { c as resolveAgentWorkspaceDir, i as resolveAgentDir, l as resolveDefaultAgentId, p as DEFAULT_AGENTS_FILENAME } from "./agent-scope-RzK9Zcks.js";
import { t as buildWorkspaceSkillStatus } from "./skills-status-qrtSNHRV.js";
import { l as writeConfigFile, o as readConfigFileSnapshot } from "./config-B2kL1ciP.js";
import { n as callGateway, t as buildGatewayConnectionDetails } from "./call-DXhJGwEy.js";
import { l as resolveGatewayBindHost, n as isLoopbackHost } from "./net-DA6Ow9GQ.js";
import { dn as formatRemainingShort, g as loadOpenClawPlugins, ln as DEFAULT_OAUTH_WARN_MS, un as buildAuthHealthSummary } from "./subagent-registry-C7Edpn23.js";
import { P as resolveMainSessionKey, V as cleanStaleLockFiles, a as loadSessionStore } from "./sessions-BD5dyLxb.js";
import { n as listChannelPlugins } from "./plugins-RqhjLCb6.js";
import { D as DEFAULT_SANDBOX_IMAGE, E as DEFAULT_SANDBOX_COMMON_IMAGE, T as DEFAULT_SANDBOX_BROWSER_IMAGE, f as resolveSandboxScope } from "./sandbox-CO-R8v6J.js";
import { i as resolveGatewayAuth } from "./auth-Dq2pFnjj.js";
import { i as formatPortDiagnostics, n as inspectPortUsage } from "./ports-59j-bA53.js";
import { c as resolveStorePath, n as resolveSessionFilePath, s as resolveSessionTranscriptsDirForAgent } from "./paths-CXpciDEv.js";
import { i as resolveMemoryBackendConfig } from "./memory-cli-C3uas9sI.js";
import { n as resolveMemorySearchConfig } from "./manager-D3ZmOwqt.js";
import { n as loadModelCatalog } from "./model-catalog-DLoDxnxL.js";
import { c as resolveGatewayLaunchAgentLabel, d as resolveGatewaySystemdServiceName, f as resolveGatewayWindowsTaskName, m as resolveNodeLaunchAgentLabel } from "./constants-ClfJ_wF3.js";
import { h as randomToken, n as applyWizardMetadata, p as printWizardHeader, s as guardCancel } from "./onboard-helpers-CEx2tGVB.js";
import { n as isWSLEnv, t as isWSL } from "./wsl-jdLWdiKy.js";
import { n as stylePromptMessage, r as stylePromptTitle, t as stylePromptHint } from "./prompt-style-DwCXob2h.js";
import { t as note$1 } from "./note-D3Xn5qjj.js";
import { t as resolveChannelDefaultAccountId } from "./helpers-UPrYYVP7.js";
import { t as collectChannelStatusIssues } from "./channels-status-issues-DgVhydeW.js";
import { a as gatewayInstallErrorHint, f as renderSystemNodeWarning, i as buildGatewayInstallPlan, m as resolveSystemNodeInfo, n as GATEWAY_DAEMON_RUNTIME_OPTIONS, t as DEFAULT_GATEWAY_DAEMON_RUNTIME } from "./daemon-runtime-DL5x9CWn.js";
import { r as isSystemdUserServiceAvailable } from "./systemd-BeKZd5oD.js";
import { a as launchAgentPlistExists, i as isLaunchAgentLoaded, o as repairLaunchAgentBootstrap, r as isLaunchAgentListed, s as resolveGatewayLogPaths, t as resolveGatewayService } from "./service-CIkh5YiN.js";
import { i as auditGatewayServiceConfig, n as renderSystemdUnavailableHints, o as needsNodeRuntimeMigration, r as SERVICE_AUDIT_CODES, s as formatRuntimeStatus, t as isSystemdUnavailableDetail } from "./systemd-hints-DCQYuPQQ.js";
import { t as readLastGatewayErrorLine } from "./diagnostics-CqIGmTJP.js";
import { n as renderGatewayServiceCleanupHints, t as findExtraGatewayServices } from "./inspect-DfaANKnj.js";
import { r as healthCommand } from "./health-75545GOF.js";
import { n as resolveControlUiDistIndexHealth, r as resolveControlUiDistIndexPathForRoot } from "./control-ui-assets-CwSEyfND.js";
import { t as resolveDmAllowState } from "./dm-policy-shared-BaTHScul.js";
import { n as runGatewayUpdate, t as resolveAgentSessionDirs } from "./session-dirs-DzJ9UfQe.js";
import { n as logConfigUpdated } from "./logging-BGewRZy0.js";
import { n as detectLegacyStateMigrations, r as runLegacyStateMigrations, t as loadAndMaybeMigrateDoctorConfig } from "./doctor-config-flow-Cr5VX2mY.js";
import { t as ensureSystemdUserLingerInteractive } from "./systemd-linger-CgYzp_8H.js";
import { t as formatHealthCheckFailure } from "./health-format-DkjSgkDx.js";
import { n as doctorShellCompletion } from "./doctor-completion-CkzolxKG.js";
import { execFile } from "node:child_process";
import os from "node:os";
import path from "node:path";
import fs from "node:fs";
import { promisify } from "node:util";
import fs$1 from "node:fs/promises";
import { confirm, intro, outro, select } from "@clack/prompts";
//#region src/commands/doctor-auth.ts
async function maybeRepairAnthropicOAuthProfileId(cfg, prompter) {
const repair = repairOAuthProfileIdMismatch({
cfg,
store: ensureAuthProfileStore(),
provider: "anthropic",
legacyProfileId: "anthropic:default"
});
if (!repair.migrated || repair.changes.length === 0) return cfg;
note$1(repair.changes.map((c) => `- ${c}`).join("\n"), "Auth profiles");
if (!await prompter.confirm({
message: "Update Anthropic OAuth profile id in config now?",
initialValue: true
})) return cfg;
return repair.config;
}
function pruneAuthOrder(order, profileIds) {
if (!order) return {
next: order,
changed: false
};
let changed = false;
const next = {};
for (const [provider, list] of Object.entries(order)) {
const filtered = list.filter((id) => !profileIds.has(id));
if (filtered.length !== list.length) changed = true;
if (filtered.length > 0) next[provider] = filtered;
}
return {
next: Object.keys(next).length > 0 ? next : void 0,
changed
};
}
function pruneAuthProfiles(cfg, profileIds) {
const profiles = cfg.auth?.profiles;
const order = cfg.auth?.order;
const nextProfiles = profiles ? { ...profiles } : void 0;
let changed = false;
if (nextProfiles) {
for (const id of profileIds) if (id in nextProfiles) {
delete nextProfiles[id];
changed = true;
}
}
const prunedOrder = pruneAuthOrder(order, profileIds);
if (prunedOrder.changed) changed = true;
if (!changed) return {
next: cfg,
changed: false
};
const nextAuth = nextProfiles || prunedOrder.next ? {
...cfg.auth,
profiles: nextProfiles && Object.keys(nextProfiles).length > 0 ? nextProfiles : void 0,
order: prunedOrder.next
} : void 0;
return {
next: {
...cfg,
auth: nextAuth
},
changed: true
};
}
async function maybeRemoveDeprecatedCliAuthProfiles(cfg, prompter) {
const store = ensureAuthProfileStore(void 0, { allowKeychainPrompt: false });
const deprecated = /* @__PURE__ */ new Set();
if (store.profiles[CLAUDE_CLI_PROFILE_ID] || cfg.auth?.profiles?.[CLAUDE_CLI_PROFILE_ID]) deprecated.add(CLAUDE_CLI_PROFILE_ID);
if (store.profiles[CODEX_CLI_PROFILE_ID] || cfg.auth?.profiles?.[CODEX_CLI_PROFILE_ID]) deprecated.add(CODEX_CLI_PROFILE_ID);
if (deprecated.size === 0) return cfg;
const lines = ["Deprecated external CLI auth profiles detected (no longer supported):"];
if (deprecated.has(CLAUDE_CLI_PROFILE_ID)) lines.push(`- ${CLAUDE_CLI_PROFILE_ID} (Anthropic): use setup-token → ${formatCliCommand("openclaw models auth setup-token")}`);
if (deprecated.has(CODEX_CLI_PROFILE_ID)) lines.push(`- ${CODEX_CLI_PROFILE_ID} (OpenAI Codex): use OAuth → ${formatCliCommand("openclaw models auth login --provider openai-codex")}`);
note$1(lines.join("\n"), "Auth profiles");
if (!await prompter.confirmRepair({
message: "Remove deprecated CLI auth profiles now?",
initialValue: true
})) return cfg;
await updateAuthProfileStoreWithLock({ updater: (nextStore) => {
let mutated = false;
for (const id of deprecated) {
if (nextStore.profiles[id]) {
delete nextStore.profiles[id];
mutated = true;
}
if (nextStore.usageStats?.[id]) {
delete nextStore.usageStats[id];
mutated = true;
}
}
if (nextStore.order) for (const [provider, list] of Object.entries(nextStore.order)) {
const filtered = list.filter((id) => !deprecated.has(id));
if (filtered.length !== list.length) {
mutated = true;
if (filtered.length > 0) nextStore.order[provider] = filtered;
else delete nextStore.order[provider];
}
}
if (nextStore.lastGood) {
for (const [provider, profileId] of Object.entries(nextStore.lastGood)) if (deprecated.has(profileId)) {
delete nextStore.lastGood[provider];
mutated = true;
}
}
return mutated;
} });
const pruned = pruneAuthProfiles(cfg, deprecated);
if (pruned.changed) note$1(Array.from(deprecated.values()).map((id) => `- removed ${id} from config`).join("\n"), "Doctor changes");
return pruned.next;
}
function formatAuthIssueHint(issue) {
if (issue.provider === "anthropic" && issue.profileId === CLAUDE_CLI_PROFILE_ID) return `Deprecated profile. Use ${formatCliCommand("openclaw models auth setup-token")} or ${formatCliCommand("openclaw configure")}.`;
if (issue.provider === "openai-codex" && issue.profileId === CODEX_CLI_PROFILE_ID) return `Deprecated profile. Use ${formatCliCommand("openclaw models auth login --provider openai-codex")} or ${formatCliCommand("openclaw configure")}.`;
return `Re-auth via \`${formatCliCommand("openclaw configure")}\` or \`${formatCliCommand("openclaw onboard")}\`.`;
}
function formatAuthIssueLine(issue) {
const remaining = issue.remainingMs !== void 0 ? ` (${formatRemainingShort(issue.remainingMs)})` : "";
const hint = formatAuthIssueHint(issue);
return `- ${issue.profileId}: ${issue.status}${remaining}${hint ? ` — ${hint}` : ""}`;
}
async function noteAuthProfileHealth(params) {
const store = ensureAuthProfileStore(void 0, { allowKeychainPrompt: params.allowKeychainPrompt });
const unusable = (() => {
const now = Date.now();
const out = [];
for (const profileId of Object.keys(store.usageStats ?? {})) {
const until = resolveProfileUnusableUntilForDisplay(store, profileId);
if (!until || now >= until) continue;
const stats = store.usageStats?.[profileId];
const remaining = formatRemainingShort(until - now);
const kind = typeof stats?.disabledUntil === "number" && now < stats.disabledUntil ? `disabled${stats.disabledReason ? `:${stats.disabledReason}` : ""}` : "cooldown";
const hint = kind.startsWith("disabled:billing") ? "Top up credits (provider billing) or switch provider." : "Wait for cooldown or switch provider.";
out.push(`- ${profileId}: ${kind} (${remaining})${hint ? ` — ${hint}` : ""}`);
}
return out;
})();
if (unusable.length > 0) note$1(unusable.join("\n"), "Auth profile cooldowns");
let summary = buildAuthHealthSummary({
store,
cfg: params.cfg,
warnAfterMs: DEFAULT_OAUTH_WARN_MS
});
const findIssues = () => summary.profiles.filter((profile) => (profile.type === "oauth" || profile.type === "token") && (profile.status === "expired" || profile.status === "expiring" || profile.status === "missing"));
let issues = findIssues();
if (issues.length === 0) return;
if (await params.prompter.confirmRepair({
message: "Refresh expiring OAuth tokens now? (static tokens need re-auth)",
initialValue: true
})) {
const refreshTargets = issues.filter((issue) => issue.type === "oauth" && [
"expired",
"expiring",
"missing"
].includes(issue.status));
const errors = [];
for (const profile of refreshTargets) try {
await resolveApiKeyForProfile({
cfg: params.cfg,
store,
profileId: profile.profileId
});
} catch (err) {
errors.push(`- ${profile.profileId}: ${err instanceof Error ? err.message : String(err)}`);
}
if (errors.length > 0) note$1(errors.join("\n"), "OAuth refresh errors");
summary = buildAuthHealthSummary({
store: ensureAuthProfileStore(void 0, { allowKeychainPrompt: false }),
cfg: params.cfg,
warnAfterMs: DEFAULT_OAUTH_WARN_MS
});
issues = findIssues();
}
if (issues.length > 0) note$1(issues.map((issue) => formatAuthIssueLine({
profileId: issue.profileId,
provider: issue.provider,
status: issue.status,
remainingMs: issue.remainingMs
})).join("\n"), "Model auth");
}
//#endregion
//#region src/commands/doctor-format.ts
function formatGatewayRuntimeSummary(runtime) {
return formatRuntimeStatus(runtime);
}
function buildGatewayRuntimeHints(runtime, options = {}) {
const hints = [];
if (!runtime) return hints;
const platform = options.platform ?? process.platform;
const env = options.env ?? process.env;
const fileLog = (() => {
try {
return getResolvedLoggerSettings().file;
} catch {
return null;
}
})();
if (platform === "linux" && isSystemdUnavailableDetail(runtime.detail)) {
hints.push(...renderSystemdUnavailableHints({ wsl: isWSLEnv() }));
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.status === "stopped") {
hints.push("Service is loaded but not running (likely exited immediately).");
if (fileLog) hints.push(`File logs: ${fileLog}`);
if (platform === "darwin") {
const logs = resolveGatewayLogPaths(env);
hints.push(`Launchd stdout (if installed): ${logs.stdoutPath}`);
hints.push(`Launchd stderr (if installed): ${logs.stderrPath}`);
} else if (platform === "linux") {
const unit = resolveGatewaySystemdServiceName(env.OPENCLAW_PROFILE);
hints.push(`Logs: journalctl --user -u ${unit}.service -n 200 --no-pager`);
} else if (platform === "win32") {
const task = resolveGatewayWindowsTaskName(env.OPENCLAW_PROFILE);
hints.push(`Logs: schtasks /Query /TN "${task}" /V /FO LIST`);
}
}
return hints;
}
//#endregion
//#region src/commands/doctor-gateway-daemon-flow.ts
async function maybeRepairLaunchAgentBootstrap(params) {
if (process.platform !== "darwin") return false;
if (!await isLaunchAgentListed({ env: params.env })) return false;
if (await isLaunchAgentLoaded({ env: params.env })) return false;
if (!await launchAgentPlistExists(params.env)) return false;
note$1("LaunchAgent is listed but not loaded in launchd.", `${params.title} LaunchAgent`);
if (!await params.prompter.confirmSkipInNonInteractive({
message: `Repair ${params.title} LaunchAgent bootstrap now?`,
initialValue: true
})) return false;
params.runtime.log(`Bootstrapping ${params.title} LaunchAgent...`);
const repair = await repairLaunchAgentBootstrap({ env: params.env });
if (!repair.ok) {
params.runtime.error(`${params.title} LaunchAgent bootstrap failed: ${repair.detail ?? "unknown error"}`);
return false;
}
if (!await isLaunchAgentLoaded({ env: params.env })) {
params.runtime.error(`${params.title} LaunchAgent still not loaded after repair.`);
return false;
}
note$1(`${params.title} LaunchAgent repaired.`, `${params.title} LaunchAgent`);
return true;
}
async function maybeRepairGatewayDaemon(params) {
if (params.healthOk) return;
const service = resolveGatewayService();
let loaded = false;
try {
loaded = await service.isLoaded({ env: process.env });
} catch {
loaded = false;
}
let serviceRuntime;
if (loaded) serviceRuntime = await service.readRuntime(process.env).catch(() => void 0);
if (process.platform === "darwin" && params.cfg.gateway?.mode !== "remote") {
const gatewayRepaired = await maybeRepairLaunchAgentBootstrap({
env: process.env,
title: "Gateway",
runtime: params.runtime,
prompter: params.prompter
});
await maybeRepairLaunchAgentBootstrap({
env: {
...process.env,
OPENCLAW_LAUNCHD_LABEL: resolveNodeLaunchAgentLabel()
},
title: "Node",
runtime: params.runtime,
prompter: params.prompter
});
if (gatewayRepaired) {
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 diagnostics = await inspectPortUsage(resolveGatewayPort(params.cfg, process.env));
if (diagnostics.status === "busy") note$1(formatPortDiagnostics(diagnostics).join("\n"), "Gateway port");
else if (loaded && serviceRuntime?.status === "running") {
const lastError = await readLastGatewayErrorLine(process.env);
if (lastError) note$1(`Last gateway error: ${lastError}`, "Gateway");
}
}
if (!loaded) {
if (process.platform === "linux") {
if (!await isSystemdUserServiceAvailable().catch(() => false)) {
note$1(renderSystemdUnavailableHints({ wsl: await isWSL() }).join("\n"), "Gateway");
return;
}
}
note$1("Gateway service not installed.", "Gateway");
if (params.cfg.gateway?.mode !== "remote") {
if (await params.prompter.confirmSkipInNonInteractive({
message: "Install gateway service now?",
initialValue: true
})) {
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 port = resolveGatewayPort(params.cfg, process.env);
const { programArguments, workingDirectory, environment } = await buildGatewayInstallPlan({
env: process.env,
port,
token: params.cfg.gateway?.auth?.token ?? process.env.OPENCLAW_GATEWAY_TOKEN,
runtime: daemonRuntime,
warn: (message, title) => note$1(message, title),
config: params.cfg
});
try {
await service.install({
env: process.env,
stdout: process.stdout,
programArguments,
workingDirectory,
environment
});
} catch (err) {
note$1(`Gateway service install failed: ${String(err)}`, "Gateway");
note$1(gatewayInstallErrorHint(), "Gateway");
}
}
}
return;
}
const summary = formatGatewayRuntimeSummary(serviceRuntime);
const hints = buildGatewayRuntimeHints(serviceRuntime, {
platform: process.platform,
env: process.env
});
if (summary || hints.length > 0) {
const lines = [];
if (summary) lines.push(`Runtime: ${summary}`);
lines.push(...hints);
note$1(lines.join("\n"), "Gateway");
}
if (serviceRuntime?.status !== "running") {
if (await params.prompter.confirmSkipInNonInteractive({
message: "Start gateway service now?",
initialValue: true
})) {
await service.restart({
env: process.env,
stdout: process.stdout
});
await sleep(1500);
}
}
if (process.platform === "darwin") {
const label = resolveGatewayLaunchAgentLabel(process.env.OPENCLAW_PROFILE);
note$1(`LaunchAgent loaded; stopping requires "${formatCliCommand("openclaw gateway stop")}" or launchctl bootout gui/$UID/${label}.`, "Gateway");
}
if (serviceRuntime?.status === "running") {
if (await params.prompter.confirmSkipInNonInteractive({
message: "Restart gateway service now?",
initialValue: true
})) {
await service.restart({
env: process.env,
stdout: process.stdout
});
await sleep(1500);
try {
await healthCommand({
json: false,
timeoutMs: 1e4
}, params.runtime);
} catch (err) {
if (String(err).includes("gateway closed")) {
note$1("Gateway not running.", "Gateway");
note$1(params.gatewayDetailsMessage, "Gateway connection");
} else params.runtime.error(formatHealthCheckFailure(err));
}
}
}
}
//#endregion
//#region src/commands/doctor-gateway-health.ts
async function checkGatewayHealth(params) {
const gatewayDetails = buildGatewayConnectionDetails({ config: params.cfg });
const timeoutMs = typeof params.timeoutMs === "number" && params.timeoutMs > 0 ? params.timeoutMs : 1e4;
let healthOk = false;
try {
await healthCommand({
json: false,
timeoutMs,
config: params.cfg
}, params.runtime);
healthOk = true;
} catch (err) {
if (String(err).includes("gateway closed")) {
note$1("Gateway not running.", "Gateway");
note$1(gatewayDetails.message, "Gateway connection");
} else params.runtime.error(formatHealthCheckFailure(err));
}
if (healthOk) try {
const issues = collectChannelStatusIssues(await callGateway({
method: "channels.status",
params: {
probe: true,
timeoutMs: 5e3
},
timeoutMs: 6e3
}));
if (issues.length > 0) note$1(issues.map((issue) => `- ${issue.channel} ${issue.accountId}: ${issue.message}${issue.fix ? ` (${issue.fix})` : ""}`).join("\n"), "Channel warnings");
} catch {}
return { healthOk };
}
//#endregion
//#region src/commands/doctor-gateway-services.ts
const execFileAsync$1 = promisify(execFile);
function detectGatewayRuntime(programArguments) {
const first = programArguments?.[0];
if (first) {
const base = path.basename(first).toLowerCase();
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 normalizeExecutablePath(value) {
return path.resolve(value);
}
function resolveGatewayAuthToken(cfg, env) {
const configToken = cfg.gateway?.auth?.token?.trim();
if (configToken) return configToken;
return (env.OPENCLAW_GATEWAY_TOKEN ?? env.CLAWDBOT_GATEWAY_TOKEN)?.trim() || void 0;
}
function extractDetailPath(detail, prefix) {
if (!detail.startsWith(prefix)) return null;
const value = detail.slice(prefix.length).trim();
return value.length > 0 ? value : null;
}
async function cleanupLegacyLaunchdService(params) {
await execFileAsync$1("launchctl", [
"bootout",
typeof process.getuid === "function" ? `gui/${process.getuid()}` : "gui/501",
params.plistPath
]).catch(() => void 0);
await execFileAsync$1("launchctl", ["unload", params.plistPath]).catch(() => void 0);
const trashDir = path.join(os.homedir(), ".Trash");
try {
await fs$1.mkdir(trashDir, { recursive: true });
} catch {}
try {
await fs$1.access(params.plistPath);
} catch {
return null;
}
const dest = path.join(trashDir, `${params.label}-${Date.now()}.plist`);
try {
await fs$1.rename(params.plistPath, dest);
return dest;
} catch {
return null;
}
}
async function maybeRepairGatewayServiceConfig(cfg, mode, runtime, prompter) {
if (resolveIsNixMode(process.env)) {
note$1("Nix mode detected; skip service updates.", "Gateway");
return;
}
if (mode === "remote") {
note$1("Gateway mode is remote; skipped local service audit.", "Gateway");
return;
}
const service = resolveGatewayService();
let command = null;
try {
command = await service.readCommand(process.env);
} catch {
command = null;
}
if (!command) return;
const expectedGatewayToken = resolveGatewayAuthToken(cfg, process.env);
const audit = await auditGatewayServiceConfig({
env: process.env,
command,
expectedGatewayToken
});
const needsNodeRuntime = needsNodeRuntimeMigration(audit.issues);
const systemNodeInfo = needsNodeRuntime ? await resolveSystemNodeInfo({ env: process.env }) : null;
const systemNodePath = systemNodeInfo?.supported ? systemNodeInfo.path : null;
if (needsNodeRuntime && !systemNodePath) {
const warning = renderSystemNodeWarning(systemNodeInfo);
if (warning) note$1(warning, "Gateway runtime");
note$1("System Node 22+ not found. Install via Homebrew/apt/choco and rerun doctor to migrate off Bun/version managers.", "Gateway runtime");
}
const port = resolveGatewayPort(cfg, process.env);
const runtimeChoice = detectGatewayRuntime(command.programArguments);
const { programArguments, workingDirectory, environment } = await buildGatewayInstallPlan({
env: process.env,
port,
token: expectedGatewayToken,
runtime: needsNodeRuntime && systemNodePath ? "node" : runtimeChoice,
nodePath: systemNodePath ?? void 0,
warn: (message, title) => note$1(message, title),
config: cfg
});
const expectedEntrypoint = findGatewayEntrypoint(programArguments);
const currentEntrypoint = findGatewayEntrypoint(command.programArguments);
if (expectedEntrypoint && currentEntrypoint && normalizeExecutablePath(expectedEntrypoint) !== normalizeExecutablePath(currentEntrypoint)) audit.issues.push({
code: SERVICE_AUDIT_CODES.gatewayEntrypointMismatch,
message: "Gateway service entrypoint does not match the current install.",
detail: `${currentEntrypoint} -> ${expectedEntrypoint}`,
level: "recommended"
});
if (audit.issues.length === 0) return;
note$1(audit.issues.map((issue) => issue.detail ? `- ${issue.message} (${issue.detail})` : `- ${issue.message}`).join("\n"), "Gateway service config");
const needsAggressive = audit.issues.filter((issue) => issue.level === "aggressive").length > 0;
if (needsAggressive && !prompter.shouldForce) note$1("Custom or unexpected service edits detected. Rerun with --force to overwrite.", "Gateway service config");
if (!(needsAggressive ? await prompter.confirmAggressive({
message: "Overwrite gateway service config with current defaults now?",
initialValue: Boolean(prompter.shouldForce)
}) : await prompter.confirmRepair({
message: "Update gateway service config to the recommended defaults now?",
initialValue: true
}))) return;
try {
await service.install({
env: process.env,
stdout: process.stdout,
programArguments,
workingDirectory,
environment
});
} catch (err) {
runtime.error(`Gateway service update failed: ${String(err)}`);
}
}
async function maybeScanExtraGatewayServices(options, runtime, prompter) {
const extraServices = await findExtraGatewayServices(process.env, { deep: options.deep });
if (extraServices.length === 0) return;
note$1(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) {
if (await prompter.confirmSkipInNonInteractive({
message: "Remove legacy gateway services (clawdbot/moltbot) now?",
initialValue: true
})) {
const removed = [];
const failed = [];
for (const svc of legacyServices) {
if (svc.platform !== "darwin") {
failed.push(`${svc.label} (${svc.platform})`);
continue;
}
if (svc.scope !== "user") {
failed.push(`${svc.label} (${svc.scope})`);
continue;
}
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);
}
if (removed.length > 0) note$1(removed.map((line) => `- ${line}`).join("\n"), "Legacy gateway removed");
if (failed.length > 0) note$1(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$1(cleanupHints.map((hint) => `- ${hint}`).join("\n"), "Cleanup hints");
note$1([
"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
//#region src/commands/doctor-install.ts
function noteSourceInstallIssues(root) {
if (!root) return;
const workspaceMarker = path.join(root, "pnpm-workspace.yaml");
if (!fs.existsSync(workspaceMarker)) return;
const warnings = [];
const nodeModules = path.join(root, "node_modules");
const pnpmStore = path.join(nodeModules, ".pnpm");
const tsxBin = path.join(nodeModules, ".bin", "tsx");
const srcEntry = path.join(root, "src", "entry.ts");
if (fs.existsSync(nodeModules) && !fs.existsSync(pnpmStore)) warnings.push("- node_modules was not installed by pnpm (missing node_modules/.pnpm). Run: pnpm install");
if (fs.existsSync(path.join(root, "package-lock.json"))) warnings.push("- package-lock.json present in a pnpm workspace. If you ran npm install, remove it and reinstall with pnpm.");
if (fs.existsSync(srcEntry) && !fs.existsSync(tsxBin)) warnings.push("- tsx binary is missing for source runs. Run: pnpm install");
if (warnings.length > 0) note$1(warnings.join("\n"), "Install");
}
//#endregion
//#region src/commands/doctor-memory-search.ts
/**
* Check whether memory search has a usable embedding provider.
* Runs as part of `openclaw doctor` — config-only, no network calls.
*/
async function noteMemorySearchHealth(cfg) {
const agentId = resolveDefaultAgentId(cfg);
const agentDir = resolveAgentDir(cfg, agentId);
const resolved = resolveMemorySearchConfig(cfg, agentId);
const hasRemoteApiKey = Boolean(resolved?.remote?.apiKey?.trim());
if (!resolved) {
note$1("Memory search is explicitly disabled (enabled: false).", "Memory search");
return;
}
if (resolveMemoryBackendConfig({
cfg,
agentId
}).backend === "qmd") return;
if (resolved.provider !== "auto") {
if (resolved.provider === "local") {
if (hasLocalEmbeddings(resolved.local)) return;
note$1([
"Memory search provider is set to \"local\" but no local model file was found.",
"",
"Fix (pick one):",
`- Install node-llama-cpp and set a local model path in config`,
`- Switch to a remote provider: ${formatCliCommand("openclaw config set agents.defaults.memorySearch.provider openai")}`,
"",
`Verify: ${formatCliCommand("openclaw memory status --deep")}`
].join("\n"), "Memory search");
return;
}
if (hasRemoteApiKey || await hasApiKeyForProvider(resolved.provider, cfg, agentDir)) return;
const envVar = providerEnvVar(resolved.provider);
note$1([
`Memory search provider is set to "${resolved.provider}" but no API key was found.`,
`Semantic recall will not work without a valid API key.`,
"",
"Fix (pick one):",
`- Set ${envVar} in your environment`,
`- Add credentials: ${formatCliCommand(`openclaw auth add --provider ${resolved.provider}`)}`,
`- To disable: ${formatCliCommand("openclaw config set agents.defaults.memorySearch.enabled false")}`,
"",
`Verify: ${formatCliCommand("openclaw memory status --deep")}`
].join("\n"), "Memory search");
return;
}
if (hasLocalEmbeddings(resolved.local)) return;
for (const provider of [
"openai",
"gemini",
"voyage"
]) if (hasRemoteApiKey || await hasApiKeyForProvider(provider, cfg, agentDir)) return;
note$1([
"Memory search is enabled but no embedding provider is configured.",
"Semantic recall will not work without an embedding provider.",
"",
"Fix (pick one):",
"- Set OPENAI_API_KEY or GEMINI_API_KEY in your environment",
`- Add credentials: ${formatCliCommand("openclaw auth add --provider openai")}`,
`- For local embeddings: configure agents.defaults.memorySearch.provider and local model path`,
`- To disable: ${formatCliCommand("openclaw config set agents.defaults.memorySearch.enabled false")}`,
"",
`Verify: ${formatCliCommand("openclaw memory status --deep")}`
].join("\n"), "Memory search");
}
function hasLocalEmbeddings(local) {
const modelPath = local.modelPath?.trim();
if (!modelPath) return false;
if (/^(hf:|https?:)/i.test(modelPath)) return true;
const resolved = resolveUserPath(modelPath);
try {
return fs.statSync(resolved).isFile();
} catch {
return false;
}
}
async function hasApiKeyForProvider(provider, cfg, agentDir) {
const authProvider = provider === "gemini" ? "google" : provider;
try {
await resolveApiKeyForProvider({
provider: authProvider,
cfg,
agentDir
});
return true;
} catch {
return false;
}
}
function providerEnvVar(provider) {
switch (provider) {
case "openai": return "OPENAI_API_KEY";
case "gemini": return "GEMINI_API_KEY";
case "voyage": return "VOYAGE_API_KEY";
default: return `${provider.toUpperCase()}_API_KEY`;
}
}
//#endregion
//#region src/commands/doctor-platform-notes.ts
const execFileAsync = promisify(execFile);
function resolveHomeDir() {
return process.env.HOME ?? os.homedir();
}
async function noteMacLaunchAgentOverrides() {
if (process.platform !== "darwin") return;
const home = resolveHomeDir();
const markerPath = [path.join(home, ".openclaw", "disable-launchagent")].find((candidate) => fs.existsSync(candidate));
if (!markerPath) return;
const displayMarkerPath = shortenHomePath(markerPath);
note$1([
`- LaunchAgent writes are disabled via ${displayMarkerPath}.`,
"- To restore default behavior:",
` rm ${displayMarkerPath}`
].filter((line) => Boolean(line)).join("\n"), "Gateway (macOS)");
}
async function launchctlGetenv(name) {
try {
const result = await execFileAsync("/bin/launchctl", ["getenv", name], { encoding: "utf8" });
const value = String(result.stdout ?? "").trim();
return value.length > 0 ? value : void 0;
} catch {
return;
}
}
function hasConfigGatewayCreds(cfg) {
const localToken = typeof cfg.gateway?.auth?.token === "string" ? cfg.gateway?.auth?.token.trim() : "";
const localPassword = typeof cfg.gateway?.auth?.password === "string" ? cfg.gateway?.auth?.password.trim() : "";
const remoteToken = typeof cfg.gateway?.remote?.token === "string" ? cfg.gateway?.remote?.token.trim() : "";
const remotePassword = typeof cfg.gateway?.remote?.password === "string" ? cfg.gateway?.remote?.password.trim() : "";
return Boolean(localToken || localPassword || remoteToken || remotePassword);
}
async function noteMacLaunchctlGatewayEnvOverrides(cfg, deps) {
if ((deps?.platform ?? process.platform) !== "darwin") return;
if (!hasConfigGatewayCreds(cfg)) return;
const getenv = deps?.getenv ?? launchctlGetenv;
const deprecatedLaunchctlEntries = [["CLAWDBOT_GATEWAY_TOKEN", await getenv("CLAWDBOT_GATEWAY_TOKEN")], ["CLAWDBOT_GATEWAY_PASSWORD", await getenv("CLAWDBOT_GATEWAY_PASSWORD")]].filter((entry) => Boolean(entry[1]?.trim()));
if (deprecatedLaunchctlEntries.length > 0) {
const lines = ["- Deprecated launchctl environment variables detected (ignored).", ...deprecatedLaunchctlEntries.map(([key]) => `- \`${key}\` is set; use \`OPENCLAW_${key.slice(key.indexOf("_") + 1)}\` instead.`)];
(deps?.noteFn ?? note$1)(lines.join("\n"), "Gateway (macOS)");
}
const tokenEntries = [["OPENCLAW_GATEWAY_TOKEN", await getenv("OPENCLAW_GATEWAY_TOKEN")]];
const passwordEntries = [["OPENCLAW_GATEWAY_PASSWORD", await getenv("OPENCLAW_GATEWAY_PASSWORD")]];
const tokenEntry = tokenEntries.find(([, value]) => value?.trim());
const passwordEntry = passwordEntries.find(([, value]) => value?.trim());
const envToken = tokenEntry?.[1]?.trim() ?? "";
const envPassword = passwordEntry?.[1]?.trim() ?? "";
const envTokenKey = tokenEntry?.[0];
const envPasswordKey = passwordEntry?.[0];
if (!envToken && !envPassword) return;
const lines = [
"- launchctl environment overrides detected (can cause confusing unauthorized errors).",
envToken && envTokenKey ? `- \`${envTokenKey}\` is set; it overrides config tokens.` : void 0,
envPassword ? `- \`${envPasswordKey ?? "OPENCLAW_GATEWAY_PASSWORD"}\` is set; it overrides config passwords.` : void 0,
"- Clear overrides and restart the app/gateway:",
envTokenKey ? ` launchctl unsetenv ${envTokenKey}` : void 0,
envPasswordKey ? ` launchctl unsetenv ${envPasswordKey}` : void 0
].filter((line) => Boolean(line));
(deps?.noteFn ?? note$1)(lines.join("\n"), "Gateway (macOS)");
}
function noteDeprecatedLegacyEnvVars(env = process.env, deps) {
const entries = Object.entries(env).filter(([key, value]) => key.startsWith("CLAWDBOT_") && value?.trim()).map(([key]) => key);
if (entries.length === 0) return;
const lines = [
"- Deprecated legacy environment variables detected (ignored).",
"- Use OPENCLAW_* equivalents instead:",
...entries.map((key) => {
return ` ${key} -> OPENCLAW_${key.slice(key.indexOf("_") + 1)}`;
})
];
(deps?.noteFn ?? note$1)(lines.join("\n"), "Environment");
}
//#endregion
//#region src/commands/doctor-prompter.ts
function createDoctorPrompter(params) {
const yes = params.options.yes === true;
const requestedNonInteractive = params.options.nonInteractive === true;
const shouldRepair = params.options.repair === true || yes;
const shouldForce = params.options.force === true;
const isTty = Boolean(process.stdin.isTTY);
const nonInteractive = requestedNonInteractive || !isTty && !yes;
const canPrompt = isTty && !yes && !nonInteractive;
const confirmDefault = async (p) => {
if (nonInteractive) return false;
if (shouldRepair) return true;
if (!canPrompt) return Boolean(p.initialValue ?? false);
return guardCancel(await confirm({
...p,
message: stylePromptMessage(p.message)
}), params.runtime);
};
return {
confirm: confirmDefault,
confirmRepair: async (p) => {
if (nonInteractive) return false;
return confirmDefault(p);
},
confirmAggressive: async (p) => {
if (nonInteractive) return false;
if (shouldRepair && shouldForce) return true;
if (shouldRepair && !shouldForce) return false;
if (!canPrompt) return Boolean(p.initialValue ?? false);
return guardCancel(await confirm({
...p,
message: stylePromptMessage(p.message)
}), params.runtime);
},
confirmSkipInNonInteractive: async (p) => {
if (nonInteractive) return false;
if (shouldRepair) return true;
return confirmDefault(p);
},
select: async (p, fallback) => {
if (!canPrompt || shouldRepair) return fallback;
return guardCancel(await select({
...p,
message: stylePromptMessage(p.message),
options: p.options.map((opt) => opt.hint === void 0 ? opt : {
...opt,
hint: stylePromptHint(opt.hint)
})
}), params.runtime);
},
shouldRepair,
shouldForce
};
}
//#endregion
//#region src/commands/doctor-sandbox.ts
function resolveSandboxScript(scriptRel) {
const candidates = /* @__PURE__ */ new Set();
candidates.add(process.cwd());
const argv1 = process.argv[1];
if (argv1) {
const normalized = path.resolve(argv1);
candidates.add(path.resolve(path.dirname(normalized), ".."));
candidates.add(path.resolve(path.dirname(normalized)));
}
for (const root of candidates) {
const scriptPath = path.join(root, scriptRel);
if (fs.existsSync(scriptPath)) return {
scriptPath,
cwd: root
};
}
return null;
}
async function runSandboxScript(scriptRel, runtime) {
const script = resolveSandboxScript(scriptRel);
if (!script) {
note$1(`Unable to locate ${scriptRel}. Run it from the repo root.`, "Sandbox");
return false;
}
runtime.log(`Running ${scriptRel}...`);
const result = await runCommandWithTimeout(["bash", script.scriptPath], {
timeoutMs: 1200 * 1e3,
cwd: script.cwd
});
if (result.code !== 0) {
runtime.error(`Failed running ${scriptRel}: ${result.stderr.trim() || result.stdout.trim() || "unknown error"}`);
return false;
}
runtime.log(`Completed ${scriptRel}.`);
return true;
}
async function isDockerAvailable() {
try {
await runExec("docker", [
"version",
"--format",
"{{.Server.Version}}"
], { timeoutMs: 5e3 });
return true;
} catch {
return false;
}
}
async function dockerImageExists(image) {
try {
await runExec("docker", [
"image",
"inspect",
image
], { timeoutMs: 5e3 });
return true;
} catch (error) {
const stderr = error?.stderr || error?.message || "";
if (String(stderr).includes("No such image")) return false;
throw error;
}
}
function resolveSandboxDockerImage(cfg) {
const image = cfg.agents?.defaults?.sandbox?.docker?.image?.trim();
return image ? image : DEFAULT_SANDBOX_IMAGE;
}
function resolveSandboxBrowserImage(cfg) {
const image = cfg.agents?.defaults?.sandbox?.browser?.image?.trim();
return image ? image : DEFAULT_SANDBOX_BROWSER_IMAGE;
}
function updateSandboxDockerImage(cfg, image) {
return {
...cfg,
agents: {
...cfg.agents,
defaults: {
...cfg.agents?.defaults,
sandbox: {
...cfg.agents?.defaults?.sandbox,
docker: {
...cfg.agents?.defaults?.sandbox?.docker,
image
}
}
}
}
};
}
function updateSandboxBrowserImage(cfg, image) {
return {
...cfg,
agents: {
...cfg.agents,
defaults: {
...cfg.agents?.defaults,
sandbox: {
...cfg.agents?.defaults?.sandbox,
browser: {
...cfg.agents?.defaults?.sandbox?.browser,
image
}
}
}
}
};
}
async function handleMissingSandboxImage(params, runtime, prompter) {
if (await dockerImageExists(params.image)) return;
const buildHint = params.buildScript ? `Build it with ${params.buildScript}.` : "Build or pull it first.";
note$1(`Sandbox ${params.kind} image missing: ${params.image}. ${buildHint}`, "Sandbox");
let built = false;
if (params.buildScript) {
if (await prompter.confirmSkipInNonInteractive({
message: `Build ${params.kind} sandbox image now?`,
initialValue: true
})) built = await runSandboxScript(params.buildScript, runtime);
}
if (built) return;
}
async function maybeRepairSandboxImages(cfg, runtime, prompter) {
const sandbox = cfg.agents?.defaults?.sandbox;
const mode = sandbox?.mode ?? "off";
if (!sandbox || mode === "off") return cfg;
if (!await isDockerAvailable()) {
note$1("Docker not available; skipping sandbox image checks.", "Sandbox");
return cfg;
}
let next = cfg;
const changes = [];
const dockerImage = resolveSandboxDockerImage(cfg);
await handleMissingSandboxImage({
kind: "base",
image: dockerImage,
buildScript: dockerImage === DEFAULT_SANDBOX_COMMON_IMAGE ? "scripts/sandbox-common-setup.sh" : dockerImage === DEFAULT_SANDBOX_IMAGE ? "scripts/sandbox-setup.sh" : void 0,
updateConfig: (image) => {
next = updateSandboxDockerImage(next, image);
changes.push(`Updated agents.defaults.sandbox.docker.image → ${image}`);
}
}, runtime, prompter);
if (sandbox.browser?.enabled) await handleMissingSandboxImage({
kind: "browser",
image: resolveSandboxBrowserImage(cfg),
buildScript: "scripts/sandbox-browser-setup.sh",
updateConfig: (image) => {
next = updateSandboxBrowserImage(next, image);
changes.push(`Updated agents.defaults.sandbox.browser.image → ${image}`);
}
}, runtime, prompter);
if (changes.length > 0) note$1(changes.join("\n"), "Doctor changes");
return next;
}
function noteSandboxScopeWarnings(cfg) {
const globalSandbox = cfg.agents?.defaults?.sandbox;
const agents = Array.isArray(cfg.agents?.list) ? cfg.agents.list : [];
const warnings = [];
for (const agent of agents) {
const agentId = agent.id;
const agentSandbox = agent.sandbox;
if (!agentSandbox) continue;
if (resolveSandboxScope({
scope: agentSandbox.scope ?? globalSandbox?.scope,
perSession: agentSandbox.perSession ?? globalSandbox?.perSession
}) !== "shared") continue;
const overrides = [];
if (agentSandbox.docker && Object.keys(agentSandbox.docker).length > 0) overrides.push("docker");
if (agentSandbox.browser && Object.keys(agentSandbox.browser).length > 0) overrides.push("browser");
if (agentSandbox.prune && Object.keys(agentSandbox.prune).length > 0) overrides.push("prune");
if (overrides.length === 0) continue;
warnings.push([`- agents.list (id "${agentId}") sandbox ${overrides.join("/")} overrides ignored.`, ` scope resolves to "shared".`].join("\n"));
}
if (warnings.length > 0) note$1(warnings.join("\n"), "Sandbox");
}
//#endregion
//#region src/commands/doctor-security.ts
async function noteSecurityWarnings(cfg) {
const warnings = [];
const auditHint = `- Run: ${formatCliCommand("openclaw security audit --deep")}`;
const gatewayBind = cfg.gateway?.bind ?? "loopback";
const customBindHost = cfg.gateway?.customBindHost?.trim();
const bindMode = [
"auto",
"lan",
"loopback",
"custom",
"tailnet"
].includes(gatewayBind) ? gatewayBind : void 0;
const resolvedBindHost = bindMode ? await resolveGatewayBindHost(bindMode, customBindHost) : "0.0.0.0";
const isExposed = !isLoopbackHost(resolvedBindHost);
const resolvedAuth = resolveGatewayAuth({
authConfig: cfg.gateway?.auth,
env: process.env,
tailscaleMode: cfg.gateway?.tailscale?.mode ?? "off"
});
const authToken = resolvedAuth.token?.trim() ?? "";
const authPassword = resolvedAuth.password?.trim() ?? "";
const hasToken = authToken.length > 0;
const hasPassword = authPassword.length > 0;
const hasSharedSecret = resolvedAuth.mode === "token" && hasToken || resolvedAuth.mode === "password" && hasPassword;
const bindDescriptor = `"${gatewayBind}" (${resolvedBindHost})`;
if (isExposed) if (!hasSharedSecret) {
const authFixLines = resolvedAuth.mode === "password" ? [` Fix: ${formatCliCommand("openclaw configure")} to set a password`, ` Or switch to token: ${formatCliCommand("openclaw config set gateway.auth.mode token")}`] : [` Fix: ${formatCliCommand("openclaw doctor --fix")} to generate a token`, ` Or set token directly: ${formatCliCommand("openclaw config set gateway.auth.mode token")}`];
warnings.push(`- CRITICAL: Gateway bound to ${bindDescriptor} without authentication.`, ` Anyone on your network (or internet if port-forwarded) can fully control your agent.`, ` Fix: ${formatCliCommand("openclaw config set gateway.bind loopback")}`, ...authFixLines);
} else warnings.push(`- WARNING: Gateway bound to ${bindDescriptor} (network-accessible).`, ` Ensure your auth credentials are strong and not exposed.`);
const warnDmPolicy = async (params) => {
const dmPolicy = params.dmPolicy;
const policyPath = params.policyPath ?? `${params.allowFromPath}policy`;
const { hasWildcard, allowCount, isMultiUserDm } = await resolveDmAllowState({
provider: params.provider,
allowFrom: params.allowFrom,
normalizeEntry: params.normalizeEntry
});
const dmScope = cfg.session?.dmScope ?? "main";
if (dmPolicy === "open") {
const allowFromPath = `${params.allowFromPath}allowFrom`;
warnings.push(`- ${params.label} DMs: OPEN (${policyPath}="open"). Anyone can DM it.`);
if (!hasWildcard) warnings.push(`- ${params.label} DMs: config invalid — "open" requires ${allowFromPath} to include "*".`);
}
if (dmPolicy === "disabled") {
warnings.push(`- ${params.label} DMs: disabled (${policyPath}="disabled").`);
return;
}
if (dmPolicy !== "open" && allowCount === 0) {
warnings.push(`- ${params.label} DMs: locked (${policyPath}="${dmPolicy}") with no allowlist; unknown senders will be blocked / get a pairing code.`);
warnings.push(` ${params.approveHint}`);
}
if (dmScope === "main" && isMultiUserDm) warnings.push(`- ${params.label} DMs: multiple senders share the main session; run: ` + formatCliCommand("openclaw config set session.dmScope \"per-channel-peer\"") + " (or \"per-account-channel-peer\" for multi-account channels) to isolate sessions.");
};
for (const plugin of listChannelPlugins()) {
if (!plugin.security) continue;
const defaultAccountId = resolveChannelDefaultAccountId({
plugin,
cfg,
accountIds: plugin.config.listAccountIds(cfg)
});
const account = plugin.config.resolveAccount(cfg, defaultAccountId);
if (!(plugin.config.isEnabled ? plugin.config.isEnabled(account, cfg) : true)) continue;
if (!(plugin.config.isConfigured ? await plugin.config.isConfigured(account, cfg) : true)) continue;
const dmPolicy = plugin.security.resolveDmPolicy?.({
cfg,
accountId: defaultAccountId,
account
});
if (dmPolicy) await warnDmPolicy({
label: plugin.meta.label ?? plugin.id,
provider: plugin.id,
dmPolicy: dmPolicy.policy,
allowFrom: dmPolicy.allowFrom,
policyPath: dmPolicy.policyPath,
allowFromPath: dmPolicy.allowFromPath,
approveHint: dmPolicy.approveHint,
normalizeEntry: dmPolicy.normalizeEntry
});
if (plugin.security.collectWarnings) {
const extra = await plugin.security.collectWarnings({
cfg,
accountId: defaultAccountId,
account
});
if (extra?.length) warnings.push(...extra);
}
}
const lines = warnings.length > 0 ? warnings : ["- No channel security warnings detected."];
lines.push(auditHint);
note$1(lines.join("\n"), "Security");
}
//#endregion
//#region src/commands/doctor-session-locks.ts
const DEFAULT_STALE_MS = 1800 * 1e3;
function formatAge(ageMs) {
if (ageMs === null) return "unknown";
const seconds = Math.floor(ageMs / 1e3);
if (seconds < 60) return `${seconds}s`;
const minutes = Math.floor(seconds / 60);
const remainingSeconds = seconds % 60;
if (minutes < 60) return `${minutes}m${remainingSeconds}s`;
return `${Math.floor(minutes / 60)}h${minutes % 60}m`;
}
function formatLockLine(lock) {
const pidStatus = lock.pid === null ? "pid=missing" : `pid=${lock.pid} (${lock.pidAlive ? "alive" : "dead"})`;
const ageStatus = `age=${formatAge(lock.ageMs)}`;
const staleStatus = lock.stale ? `stale=yes (${lock.staleReasons.join(", ") || "unknown"})` : "stale=no";
const removedStatus = lock.removed ? " [removed]" : "";
return `- ${shortenHomePath(lock.lockPath)} ${pidStatus} ${ageStatus} ${staleStatus}${removedStatus}`;
}
async function noteSessionLockHealth(params) {
const s