openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
1,147 lines (1,146 loc) • 46.4 kB
JavaScript
import { n as isExperimentalClawsEnabled } from "./experimental-BMzbGmT5.js";
import { c as resolveUserPath } from "./home-dir-BPhrG-aM.js";
import { r as isPathInside } from "./path-guards-Cp-mGr3-.js";
import "./utils-P__uGsPB.js";
import { O as tryResolveSoleAgentId, i as listAgentEntries, o as listAgentIds } from "./agent-scope-config-DcbEhP0R.js";
import { a as isDefaultInstallIdentity, w as resolveStateDir } from "./paths-D2sRr1a_.js";
import { v as resolveSecretInputRef } from "./types.secrets-kC0nOetj.js";
import "./agent-scope-DbtJyKUL.js";
import { n as hasAmbiguousGatewayAuthModeConfig } from "./auth-mode-policy-CgmWtSyI.js";
import { t as resolveGatewayAuthToken } from "./auth-token-resolution-B9de62Be.js";
import { n as resolveGatewayAuth } from "./auth-resolve-O5AKX-sb.js";
import "./auth-CyN_wFeb.js";
import { t as resolveSkillWorkshopConfig } from "./config-Cjp42tXL.js";
import { r as hasConfiguredCommandOwners } from "./doctor-command-owner-BMpVjvKQ.js";
import { i as noteChromeMcpBrowserReadiness, n as maybeArchiveLegacyClawdBrowserProfileResidue, r as maybeRepairOwnedChromeExtensionNativeHosts, t as detectLegacyClawdBrowserProfileResidue } from "./doctor-browser-CJ-DImWa.js";
import { a as shellCompletionStatusToRepairEffects, i as shellCompletionStatusToHealthFindings, t as checkShellCompletionStatus } from "./doctor-completion-CtrQMyqI.js";
import { n as disableUnavailableSkillsInConfig, r as formatMissingSkillSummary } from "./doctor-skills-core-DZQVCoNO.js";
import { i as uiProtocolFreshnessIssueToRepairEffects, r as uiProtocolFreshnessIssueToHealthFinding, t as detectUiProtocolFreshnessIssues } from "./doctor-ui-Dr3wujX_.js";
import { i as resolveKnownModelRefMigrationTarget, n as collectCodexRuntimeCompatibilityWarnings, o as collectDisabledCodexPluginRouteIssues } from "./codex-route-warnings-BVqoTxlz.js";
import { t as getSkippedExecRefStaticError } from "./exec-resolution-policy-B4IeYZE3.js";
import { t as detectSkillWorkshopToolPolicyDiagnostic } from "./tool-policy-diagnostic-DR9qNS2Q.js";
import { t as hasActiveGatewayExecCredential } from "./doctor-gateway-exec-credential-jPkNF-y8.js";
import { t as resolveDoctorWorkspaceSuggestionScopes } from "./doctor-workspace-suggestion-scopes-DVUN9Qwi.js";
import { t as defineSplitHealthCheckInput } from "./health-check-adapter-EMFeb2Zf.js";
import path from "node:path";
import { lstat, realpath, rm } from "node:fs/promises";
//#region src/flows/doctor-removed-workspaces-state-check.ts
const CHECK_ID = "core/doctor/removed-workspaces-state";
function resolveRemovedWorkspacesStateDir() {
return path.join(resolveStateDir(process.env), "workspaces");
}
async function pathKind(target) {
try {
const stats = await lstat(target);
if (stats.isDirectory()) return "directory";
return stats.isFile() ? "file" : null;
} catch (error) {
if (error.code === "ENOENT") return null;
throw error;
}
}
async function hasRemovedWorkspacesFingerprint(target) {
if (await pathKind(target) !== "directory") return false;
if (await pathKind(path.join(target, "workspaces.sqlite")) === "file") return true;
const [widgetsKind, dataKind] = await Promise.all([pathKind(path.join(target, "widgets")), pathKind(path.join(target, "data"))]);
return widgetsKind === "directory" && dataKind === "directory";
}
async function canonicalPath(target) {
const resolved = path.resolve(target);
try {
return await realpath(resolved);
} catch (error) {
const code = error.code;
if (code === "ENOENT" || code === "ENOTDIR") return resolved;
throw error;
}
}
async function configuredAgentWorkspaceCollisions(cfg, target) {
const configured = [{
label: "agents.defaults.workspace",
workspace: cfg.agents?.defaults?.workspace
}, ...listAgentEntries(cfg).map((agent) => ({
label: `agents.list.${agent.id}.workspace`,
workspace: agent.workspace
}))];
const resolvedTarget = await canonicalPath(target);
return (await Promise.all(configured.filter((entry) => typeof entry.workspace === "string" && entry.workspace.trim().length > 0).map(async (entry) => ({
label: entry.label,
resolvedWorkspace: await canonicalPath(resolveUserPath(entry.workspace, process.env))
})))).filter((entry) => isPathInside(resolvedTarget, entry.resolvedWorkspace) || isPathInside(entry.resolvedWorkspace, resolvedTarget)).map((entry) => entry.label);
}
function collisionWarning(target, collisions) {
return `Retired Workspaces plugin fingerprints remain at ${target}, but ${collisions.join(", ")} resolves to that directory or an overlapping path. Automatic removal is disabled.`;
}
function repairEffect(target, dryRun) {
return {
kind: "state",
action: dryRun ? "would-remove-retired-workspaces-state" : "remove-retired-workspaces-state",
target,
dryRunSafe: false
};
}
const removedWorkspacesStateCheck = {
id: CHECK_ID,
kind: "core",
description: "State from the retired experimental Workspaces plugin has been removed.",
source: "doctor",
async detect(ctx, scope) {
const target = resolveRemovedWorkspacesStateDir();
const scopedPaths = new Set(scope?.paths ?? []);
if (scopedPaths.size > 0 && !scopedPaths.has(target) || !await hasRemovedWorkspacesFingerprint(target)) return [];
const collisions = await configuredAgentWorkspaceCollisions(ctx.cfg, target);
if (collisions.length > 0) return [{
checkId: CHECK_ID,
severity: "warning",
message: collisionWarning(target, collisions),
path: target
}];
return [{
checkId: CHECK_ID,
severity: "warning",
message: `Retired Workspaces plugin state remains at ${target}.`,
path: target,
fixHint: "Run `openclaw doctor --fix` to remove the stale plugin state."
}];
},
async repair(ctx) {
const target = resolveRemovedWorkspacesStateDir();
if (!await hasRemovedWorkspacesFingerprint(target)) return {
status: "skipped",
reason: "retired Workspaces plugin fingerprints are absent",
changes: []
};
const collisions = await configuredAgentWorkspaceCollisions(ctx.cfg, target);
if (collisions.length > 0) {
const warning = collisionWarning(target, collisions);
return {
status: "skipped",
reason: warning,
changes: [],
warnings: [warning]
};
}
const dryRun = ctx.dryRun === true;
const effects = [repairEffect(target, dryRun)];
if (dryRun) return {
changes: [`Would remove retired Workspaces plugin state at ${target}.`],
effects
};
await rm(target, {
force: true,
recursive: true
});
return {
changes: [`Removed retired Workspaces plugin state at ${target}.`],
effects
};
}
};
//#endregion
//#region src/flows/doctor-core-checks.ts
const BROWSER_CLAWD_PROFILE_RESIDUE_CHECK_ID = "core/doctor/browser-clawd-profile-residue";
const CODEX_SESSION_ROUTES_CHECK_ID = "core/doctor/codex-session-routes";
const FINAL_CONFIG_VALIDATION_CHECK_ID = "core/doctor/final-config-validation";
const GATEWAY_DAEMON_CHECK_ID = "core/doctor/gateway-daemon";
const GATEWAY_HEALTH_CHECK_ID = "core/doctor/gateway-health";
const GATEWAY_SERVICES_EXTRA_CHECK_ID = "core/doctor/gateway-services/extra";
const TELEGRAM_GENERAL_TOPIC_CONVERSATIONS_CHECK_ID = "core/doctor/telegram-general-topic-conversations";
const SKILL_WORKSHOP_TOOL_POLICY_CHECK_ID = "core/doctor/skill-workshop-tool-policy";
const loadDoctorCoreChecksRuntimeModule = async () => await import("./doctor-core-checks.runtime.js");
const loadDoctorWorkspaceModule = async () => await import("./doctor-workspace-xbU1cpEa.js");
async function detectUnavailableSkillsWithRuntime(ctx) {
const runtime = await loadDoctorCoreChecksRuntimeModule();
return ctx.cwd ? runtime.detectUnavailableSkills(ctx.cfg, ctx.cwd) : [];
}
async function collectSecurityWarningsWithRuntime(cfg) {
const { collectSecurityWarnings } = await import("./doctor-security-tmzbCt2L.js");
return collectSecurityWarnings(cfg);
}
async function collectWorkspaceSuggestionNotesWithRuntime(workspaceDir) {
const { collectWorkspaceBackupTip } = await import("./doctor-state-integrity-CnQHOdsR.js");
const { MEMORY_SYSTEM_PROMPT, shouldSuggestMemorySystem } = await loadDoctorWorkspaceModule();
const notes = [];
const backupTip = collectWorkspaceBackupTip(workspaceDir);
if (backupTip) notes.push(backupTip);
if (await shouldSuggestMemorySystem(workspaceDir)) notes.push(MEMORY_SYSTEM_PROMPT);
return notes;
}
async function collectRuntimeToolSchemaFindingsWithRuntime(ctx) {
const runtime = await loadDoctorCoreChecksRuntimeModule();
const runWithPluginMetadataSnapshot = ctx.runWithPluginMetadataSnapshot;
return runtime.collectRuntimeToolSchemaFindings(ctx.cfg, runWithPluginMetadataSnapshot ? { runWithPluginMetadataSnapshot } : void 0);
}
async function collectProviderCatalogProjectionFindingsWithRuntime(ctx) {
return (await loadDoctorCoreChecksRuntimeModule()).collectProviderCatalogProjectionFindings(ctx.cfg, ctx.cwd);
}
async function collectLocalAudioAccelerationFindingsWithRuntime() {
return (await loadDoctorCoreChecksRuntimeModule()).collectLocalAudioAccelerationFindings();
}
async function collectGatewayHealthFindingsWithRuntime(ctx) {
return (await loadDoctorCoreChecksRuntimeModule()).collectGatewayHealthFindings(ctx);
}
async function collectGatewayDaemonFindingsWithRuntime(ctx) {
return (await loadDoctorCoreChecksRuntimeModule()).collectGatewayDaemonFindings(ctx);
}
async function listGatewayCronJobsWithRuntime(ctx) {
if (await hasActiveGatewayExecCredential({ cfg: ctx.cfg }) && ctx.allowExecSecretRefs !== true) throw new Error("Gateway cron inventory skipped because credentials use an exec SecretRef; rerun doctor with --allow-exec.");
const { callGateway } = await import("./call-Bo4Qx1ax.js");
const jobs = [];
let offset = 0;
let snapshotRevision;
let total;
while (total === void 0 || offset < total) {
const page = await callGateway({
method: "cron.list",
params: {
includeDisabled: true,
limit: 200,
offset
},
timeoutMs: 3e3,
config: ctx.cfg,
deviceIdentity: null
});
if (!(Array.isArray(page.jobs) && typeof page.snapshotRevision === "string" && page.snapshotRevision.length > 0 && Number.isSafeInteger(page.total) && page.total >= 0 && page.offset === offset && Number.isSafeInteger(page.limit) && page.limit > 0 && typeof page.hasMore === "boolean" && (page.nextOffset === null || Number.isSafeInteger(page.nextOffset)))) throw new Error("Gateway returned an invalid cron inventory response.");
if (snapshotRevision !== void 0 && page.snapshotRevision !== snapshotRevision || total !== void 0 && page.total !== total) throw new Error("Gateway cron inventory changed while doctor was reading it.");
snapshotRevision ??= page.snapshotRevision;
total ??= page.total;
jobs.push(...page.jobs);
if (!page.hasMore) {
if (page.nextOffset !== null || jobs.length !== total) throw new Error("Gateway returned an inconsistent cron inventory response.");
return jobs;
}
const expectedNextOffset = offset + page.jobs.length;
if (page.nextOffset !== expectedNextOffset || expectedNextOffset <= offset || expectedNextOffset >= total) throw new Error("Gateway returned an invalid cron inventory cursor.");
offset = expectedNextOffset;
}
throw new Error("Gateway returned an incomplete cron inventory response.");
}
const defaultCoreHealthCheckDeps = {
detectUnavailableSkills: detectUnavailableSkillsWithRuntime,
collectSecurityWarnings: collectSecurityWarningsWithRuntime,
collectWorkspaceSuggestionNotes: collectWorkspaceSuggestionNotesWithRuntime,
collectRuntimeToolSchemaFindings: collectRuntimeToolSchemaFindingsWithRuntime,
collectProviderCatalogProjectionFindings: collectProviderCatalogProjectionFindingsWithRuntime,
collectLocalAudioAccelerationFindings: collectLocalAudioAccelerationFindingsWithRuntime,
collectGatewayHealthFindings: collectGatewayHealthFindingsWithRuntime,
collectGatewayDaemonFindings: collectGatewayDaemonFindingsWithRuntime,
listGatewayCronJobs: listGatewayCronJobsWithRuntime
};
function configValidationIssuesToHealthFindings(issues) {
return issues.map((issue) => ({
checkId: FINAL_CONFIG_VALIDATION_CHECK_ID,
severity: "error",
message: issue.message,
path: issue.path || "<root>"
}));
}
const gatewayConfigCheck = {
id: "core/doctor/gateway-config",
kind: "core",
description: "openclaw.jsonc gateway block is set and unambiguous.",
source: "doctor",
async detect(ctx) {
const findings = [];
if (!ctx.cfg.gateway?.mode) findings.push({
checkId: "core/doctor/gateway-config",
severity: "warning",
message: "gateway.mode is unset; gateway start will be blocked.",
path: "gateway.mode",
fixHint: "Run `openclaw configure` and set Gateway mode (local/remote), or `openclaw config set gateway.mode local`."
});
if (ctx.cfg.gateway?.mode !== "remote" && hasAmbiguousGatewayAuthModeConfig(ctx.cfg)) findings.push({
checkId: "core/doctor/gateway-config",
severity: "warning",
message: "gateway.auth.token and gateway.auth.password are both configured while gateway.auth.mode is unset; auth selection is ambiguous.",
path: "gateway.auth.mode",
fixHint: "Set an explicit mode: `openclaw config set gateway.auth.mode token` or `... password`."
});
return findings;
}
};
const commandOwnerCheck = {
id: "core/doctor/command-owner",
kind: "core",
description: "An owner account is configured for owner-only commands.",
source: "doctor",
async detect(ctx) {
if (hasConfiguredCommandOwners(ctx.cfg)) return [];
return [{
checkId: "core/doctor/command-owner",
severity: "info",
message: "No command owner is configured. Owner-only commands (/diagnostics, /export-trajectory, /config, exec approvals) have no allowed sender.",
path: "commands.ownerAllowFrom",
fixHint: "Set commands.ownerAllowFrom to your channel user id, e.g. `openclaw config set commands.ownerAllowFrom '[\"telegram:123456789\"]'`."
}];
}
};
const skillWorkshopToolPolicyCheck = {
id: SKILL_WORKSHOP_TOOL_POLICY_CHECK_ID,
kind: "core",
description: "Autonomous Skill Workshop capture has a callable review tool.",
source: "doctor",
async detect(ctx) {
const workshopEnabled = resolveSkillWorkshopConfig(ctx.cfg).autonomous.mode !== "off";
const listedAgentIds = listAgentIds(ctx.cfg);
return (listedAgentIds.length > 0 ? listedAgentIds : [void 0]).flatMap((agentId) => {
const diagnostic = detectSkillWorkshopToolPolicyDiagnostic({
config: ctx.cfg,
workshopEnabled,
...agentId ? { agentId } : {}
});
return diagnostic ? [diagnostic] : [];
}).map((diagnostic) => ({
checkId: SKILL_WORKSHOP_TOOL_POLICY_CHECK_ID,
severity: "warning",
message: diagnostic.detail,
path: diagnostic.source,
target: diagnostic.agentId,
requirement: "Autonomous Skill Workshop review requires the skill_workshop tool.",
fixHint: diagnostic.fix
}));
}
};
function resolveDoctorMode(cfg) {
return cfg.gateway?.mode === "remote" ? "remote" : "local";
}
function buildGatewayTokenSecretRefUnavailableMessage(params) {
if (params.unresolvedRefReason) return `Gateway token SecretRef could not be resolved: ${params.unresolvedRefReason}`;
if (params.ref.source === "exec") {
const staticError = getSkippedExecRefStaticError({
ref: params.ref,
config: params.cfg
});
if (staticError) return `Gateway token SecretRef could not be verified: ${staticError}`;
return "Gateway token SecretRef uses an exec provider and did not resolve.";
}
return "Gateway token is managed via SecretRef and is currently unavailable.";
}
function buildGatewayTokenSecretRefFixHint(ref) {
if (ref.source === "exec") return "Run `openclaw doctor --allow-exec` to verify exec SecretRefs during doctor, or `openclaw secrets audit --allow-exec` to audit all exec SecretRefs.";
return "Resolve or rotate the external secret source, then rerun doctor.";
}
const gatewayAuthCheck = {
id: "core/doctor/gateway-auth",
kind: "core",
description: "Local Gateway auth mode has a usable token or another explicit auth mode.",
source: "doctor",
async detect(ctx) {
if (resolveDoctorMode(ctx.cfg) !== "local") return [];
const gatewayTokenRef = resolveSecretInputRef({
value: ctx.cfg.gateway?.auth?.token,
defaults: ctx.cfg.secrets?.defaults
}).ref;
const auth = resolveGatewayAuth({
authConfig: ctx.cfg.gateway?.auth,
tailscaleMode: ctx.cfg.gateway?.tailscale?.mode ?? "off"
});
const hasInlineToken = typeof auth.token === "string" && auth.token.trim() !== "";
if (!(auth.mode !== "password" && auth.mode !== "none" && auth.mode !== "trusted-proxy" && (auth.mode !== "token" || !hasInlineToken || Boolean(gatewayTokenRef)))) return [];
let unresolvedRefReason;
if (gatewayTokenRef && gatewayTokenRef.source === "exec") {
if (getSkippedExecRefStaticError({
ref: gatewayTokenRef,
config: ctx.cfg
})) unresolvedRefReason = void 0;
else if (ctx.allowExecSecretRefs !== true) return [];
else {
const resolvedToken = await resolveGatewayAuthToken({
cfg: ctx.cfg,
env: process.env,
unresolvedReasonStyle: "detailed",
envFallback: "never"
});
if (resolvedToken.source === "secretRef") return [];
unresolvedRefReason = resolvedToken.unresolvedRefReason;
}
} else {
const resolvedToken = await resolveGatewayAuthToken({
cfg: ctx.cfg,
env: process.env,
unresolvedReasonStyle: "detailed"
});
if (gatewayTokenRef ? resolvedToken.source === "secretRef" : resolvedToken.token) return [];
unresolvedRefReason = resolvedToken.unresolvedRefReason;
}
if (gatewayTokenRef) return [{
checkId: "core/doctor/gateway-auth",
severity: "warning",
message: buildGatewayTokenSecretRefUnavailableMessage({
cfg: ctx.cfg,
ref: gatewayTokenRef,
unresolvedRefReason
}),
path: "gateway.auth.token",
fixHint: buildGatewayTokenSecretRefFixHint(gatewayTokenRef)
}];
return [{
checkId: "core/doctor/gateway-auth",
severity: "warning",
message: "Gateway auth is off or missing a token.",
path: "gateway.auth",
fixHint: "Run `openclaw doctor --fix --generate-gateway-token` to generate a token."
}];
}
};
const hooksModelCheck = {
id: "core/doctor/hooks-model",
kind: "core",
description: "hooks.gmail.model resolves to an allowed catalog model.",
source: "doctor",
async detect(ctx) {
if (!ctx.cfg.hooks?.gmail?.model?.trim()) return [];
const { DEFAULT_MODEL, DEFAULT_PROVIDER } = await import("./defaults-RjT9WtG0.js");
const { loadPreparedModelCatalog } = await import("./prepared-model-catalog-BH7NC_p8.js");
const { getModelRefStatus, resolveConfiguredModelRef, resolveHooksGmailModel } = await import("./model-selection-DEDE6TxI.js");
const hooksModelRef = resolveHooksGmailModel({
cfg: ctx.cfg,
defaultProvider: DEFAULT_PROVIDER
});
if (!hooksModelRef) return [{
checkId: "core/doctor/hooks-model",
severity: "warning",
message: `hooks.gmail.model "${ctx.cfg.hooks.gmail.model}" could not be resolved.`,
path: "hooks.gmail.model"
}];
const { provider: defaultProvider, model: defaultModel } = resolveConfiguredModelRef({
cfg: ctx.cfg,
defaultProvider: DEFAULT_PROVIDER,
defaultModel: DEFAULT_MODEL
});
const catalog = await loadPreparedModelCatalog({
config: ctx.cfg,
readOnly: true,
providerDiscoveryProviderIds: []
});
const status = getModelRefStatus({
cfg: ctx.cfg,
catalog,
ref: hooksModelRef,
defaultProvider,
defaultModel
});
const findings = [];
if (!status.allowed) findings.push({
checkId: "core/doctor/hooks-model",
severity: "warning",
message: `hooks.gmail.model "${status.key}" is not allowed by agents.defaults.modelPolicy.allow.`,
path: "hooks.gmail.model",
fixHint: "Add the model or its provider wildcard to agents.defaults.modelPolicy.allow, or remove hooks.gmail.model."
});
if (!status.inCatalog) findings.push({
checkId: "core/doctor/hooks-model",
severity: "warning",
message: `hooks.gmail.model "${status.key}" is not in the model catalog.`,
path: "hooks.gmail.model",
fixHint: "Choose a model from the configured provider catalog."
});
return findings;
}
};
const legacyStateCheck = {
id: "core/doctor/legacy-state",
kind: "core",
description: "Legacy sessions, agent state, and channel auth paths have been migrated.",
source: "doctor",
defaultEnabled: false,
async detect(ctx) {
const { detectLegacyStateMigrations } = await import("./state-migrations.doctor-DtjzVXti.js");
const { prepareLegacySessionSurfaces } = await import("./legacy-session-surfaces-zNTu1u7j.js");
const legacySessionSurfaces = prepareLegacySessionSurfaces({ config: ctx.cfg });
const detected = await detectLegacyStateMigrations({
cfg: ctx.cfg,
doctorOnlyStateMigrations: true,
legacySessionSurfaces
});
return [...detected.preview.map((line) => ({
checkId: "core/doctor/legacy-state",
severity: "warning",
message: line.replace(/^- /, ""),
path: detected.stateDir,
fixHint: "Run `openclaw doctor --fix` to migrate legacy state."
})), ...detected.warnings.map((warning) => ({
checkId: "core/doctor/legacy-state",
severity: "warning",
message: warning,
path: detected.stateDir,
fixHint: "Resolve the warning, then rerun `openclaw doctor --fix`."
}))];
}
};
const bootstrapSizeCheck = {
id: "core/doctor/bootstrap-size",
kind: "core",
description: "Workspace bootstrap files fit within configured injection limits.",
source: "doctor",
async detect(ctx) {
if (!ctx.cwd) return [];
const { buildBootstrapInjectionStats, analyzeBootstrapBudget } = await import("./bootstrap-budget-CzhmrS_V.js");
const { resolveBootstrapContextForRun } = await import("./bootstrap-files-DeR3VfpW.js");
const { resolveBootstrapMaxChars, resolveBootstrapTotalMaxChars } = await import("./embedded-agent-helpers-DFsMTgyD.js");
const defaultAgentId = tryResolveSoleAgentId(ctx.cfg);
const workspaceDir = ctx.cwd;
const { bootstrapFiles, contextFiles } = await resolveBootstrapContextForRun({
workspaceDir,
config: ctx.cfg,
agentId: defaultAgentId,
readOnlyState: true
});
const analysis = analyzeBootstrapBudget({
files: buildBootstrapInjectionStats({
bootstrapFiles,
injectedFiles: contextFiles
}),
bootstrapMaxChars: resolveBootstrapMaxChars(ctx.cfg, defaultAgentId),
bootstrapTotalMaxChars: resolveBootstrapTotalMaxChars(ctx.cfg, defaultAgentId)
});
const findings = [];
for (const file of analysis.truncatedFiles) findings.push({
checkId: "core/doctor/bootstrap-size",
severity: "warning",
message: `${file.name} exceeds bootstrap limits and will be truncated.`,
path: file.path,
fixHint: "Reduce the file size or tune `agents.entries.*.bootstrapMaxChars` / `bootstrapTotalMaxChars` for this agent, or the corresponding `agents.defaults.*` fallback."
});
for (const file of analysis.nearLimitFiles) {
if (file.truncated) continue;
findings.push({
checkId: "core/doctor/bootstrap-size",
severity: "info",
message: `${file.name} is near the configured bootstrap file limit.`,
path: file.path,
fixHint: "Reduce the file size or tune `agents.entries.*.bootstrapMaxChars` for this agent, or `agents.defaults.bootstrapMaxChars` as fallback, for per-file limits."
});
}
if (analysis.totalNearLimit) findings.push({
checkId: "core/doctor/bootstrap-size",
severity: analysis.hasTruncation ? "warning" : "info",
message: "Total bootstrap context is near the configured total limit.",
path: workspaceDir,
fixHint: "Reduce bootstrap file sizes or tune `agents.entries.*.bootstrapTotalMaxChars` for this agent, or `agents.defaults.bootstrapTotalMaxChars` as fallback."
});
return findings;
}
};
function createRuntimeToolSchemaCheck(deps) {
return {
id: "core/doctor/runtime-tool-schemas",
kind: "core",
description: "Active agent tool schemas project into model/runtime-compatible tool inputs.",
source: "doctor",
async detect(ctx) {
return deps.collectRuntimeToolSchemaFindings(ctx);
}
};
}
function createProviderCatalogProjectionCheck(deps) {
return {
id: "core/doctor/provider-catalog-projection",
kind: "core",
description: "Provider catalog hooks project into unified text model catalog rows.",
source: "doctor",
async detect(ctx) {
return deps.collectProviderCatalogProjectionFindings(ctx);
}
};
}
function createModelReferenceCheck() {
return {
id: "core/doctor/model-references",
kind: "core",
description: "Configured model references have installed or configured provider owners.",
source: "doctor",
async detect(ctx) {
const { inspectConfiguredModelReferences } = await import("./model-reference-validation-BzXQGw8Q.js");
return inspectConfiguredModelReferences({
cfg: ctx.cfg,
env: ctx.env,
workspaceDir: ctx.cwd
}).flatMap((inspection) => {
const migrationTarget = resolveKnownModelRefMigrationTarget(ctx.cfg, inspection.ref);
const migrationFinding = migrationTarget ? {
message: `Configured model "${inspection.ref}" is a legacy reference. Doctor can migrate it to "${migrationTarget}".`,
requirement: `canonical model reference "${migrationTarget}"`,
fixHint: `Run \`openclaw doctor --fix\` to migrate this model reference to "${migrationTarget}".`
} : void 0;
if (inspection.status === "unknown-provider") return [{
checkId: "core/doctor/model-references",
severity: "warning",
source: "doctor",
target: inspection.ref,
...migrationFinding ?? {
message: `Configured model "${inspection.ref}" uses unknown provider "${inspection.provider}". No installed plugin manifest or models.providers entry declares it.`,
requirement: "an installed plugin manifest or models.providers configuration",
fixHint: "Install a plugin that declares this provider, configure it under models.providers, or remove the model reference."
}
}];
if (inspection.status === "unknown-model" && inspection.active) return [{
checkId: "core/doctor/model-references",
severity: "info",
source: "doctor",
target: inspection.ref,
...migrationFinding ?? {
message: `Configured model "${inspection.ref}" uses a known provider but is not in the local model catalog. It may be newly released or self-hosted.`,
requirement: "a provider-supported model id",
fixHint: "Verify the model id with the provider, or rerun with --severity-min info after refreshing the local catalog."
}
}];
return [];
});
}
};
}
function normalizeDoctorNoteLine(line) {
return line.replace(/^- /, "").trim();
}
function noteTextToFinding(params) {
const lines = params.text.split("\n");
const first = normalizeDoctorNoteLine(lines[0] ?? params.text);
const rest = lines.slice(1).join("\n");
return {
checkId: params.checkId,
severity: params.severity,
message: first,
...params.target ? { target: params.target } : {},
...rest ? { fixHint: rest } : {}
};
}
function inferCapturedNoteSeverity(text) {
if (text.includes("CRITICAL")) return "error";
if (text.includes("- Fix:") || text.includes("unavailable") || text.includes("not found") || text.includes("missing") || text.includes("not readable") || text.includes("not writable") || text.includes("readonly")) return "warning";
return "info";
}
function createNoteCollector(checkId) {
const findings = [];
const noteFn = (message) => {
const text = noteMessageToText(message);
if (!text.trim()) return;
const severity = inferCapturedNoteSeverity(text);
if (severity === "info") return;
findings.push(noteTextToFinding({
checkId,
severity,
text
}));
};
return {
findings,
noteFn
};
}
function noteMessageToText(message) {
if (message instanceof Error) return message.message;
if (message == null) return "";
if (typeof message === "string") return message;
if (typeof message === "number" || typeof message === "boolean" || typeof message === "bigint") return String(message);
try {
return JSON.stringify(message) ?? "";
} catch {
return "";
}
}
const claudeCliCheck = {
id: "core/doctor/claude-cli",
kind: "core",
description: "Claude CLI readiness is captured as structured findings.",
source: "doctor",
async detect(ctx) {
const { noteClaudeCliHealth } = await import("./doctor-claude-cli-DSt53Jzg.js");
const collector = createNoteCollector("core/doctor/claude-cli");
noteClaudeCliHealth(ctx.cfg, {
noteFn: collector.noteFn,
...ctx.cwd ? { workspaceDir: ctx.cwd } : {}
});
return collector.findings;
}
};
function createSecurityCheck(deps) {
return {
id: "core/doctor/security",
kind: "core",
description: "Security posture checks produce structured findings.",
source: "doctor",
async detect(ctx) {
return (await deps.collectSecurityWarnings(ctx.cfg)).map(securityAuditFindingToHealthFinding);
}
};
}
function securityAuditFindingToHealthFinding(finding) {
const detailLines = finding.detail.split("\n");
const firstDetail = detailLines.shift() ?? "";
const fixHint = [...detailLines, ...finding.remediation?.split("\n") ?? []].join("\n");
return {
checkId: "core/doctor/security",
severity: finding.severity === "critical" ? "error" : finding.severity === "warn" ? "warning" : "info",
message: `${finding.title}${firstDetail ? `: ${firstDetail}` : ""}`,
...fixHint ? { fixHint } : {}
};
}
const openAIOAuthTlsCheck = {
id: "core/doctor/oauth-tls",
kind: "core",
description: "OpenAI OAuth TLS prerequisites are satisfied before browser auth.",
source: "doctor",
async detect(ctx) {
const { formatOpenAIOAuthTlsPreflightFix, runOpenAIOAuthTlsPreflight, shouldRunOpenAIOAuthTlsPrerequisites } = await import("./provider-openai-chatgpt-oauth-tls-bw_2GiA-.js");
if (!shouldRunOpenAIOAuthTlsPrerequisites({
cfg: ctx.cfg,
deep: ctx.mode === "doctor"
})) return [];
const result = await runOpenAIOAuthTlsPreflight({ timeoutMs: 4e3 });
if (result.ok || result.kind !== "tls-cert") return [];
return [noteTextToFinding({
checkId: "core/doctor/oauth-tls",
severity: "warning",
text: formatOpenAIOAuthTlsPreflightFix(result)
})];
}
};
const legacyWhatsAppCrontabCheck = {
id: "core/doctor/legacy-whatsapp-crontab",
kind: "core",
description: "Legacy WhatsApp crontab health entries are detected as structured findings.",
source: "doctor",
defaultEnabled: false,
async detect() {
const { collectLegacyWhatsAppCrontabHealthWarning } = await import("./cron-DIGTAz45.js");
const warning = await collectLegacyWhatsAppCrontabHealthWarning();
if (!warning) return [];
return [noteTextToFinding({
checkId: "core/doctor/legacy-whatsapp-crontab",
severity: "warning",
text: warning
})];
}
};
const legacyCronStoreCheck = {
id: "core/doctor/legacy-cron-store",
kind: "core",
description: "Legacy cron store, run-log, and payload state is normalized.",
source: "doctor",
defaultEnabled: false,
async detect(ctx) {
const { collectLegacyCronStoreHealthFindings } = await import("./cron-DIGTAz45.js");
return collectLegacyCronStoreHealthFindings({ cfg: ctx.cfg });
}
};
const codexSessionRoutesCheck = {
id: CODEX_SESSION_ROUTES_CHECK_ID,
kind: "core",
description: "Codex runtime routes are compatible with the configured plugin harness.",
source: "doctor",
async detect(ctx) {
const disabledPluginFindings = collectDisabledCodexPluginRouteIssues(ctx.cfg, ctx.env).map((issue) => ({
checkId: CODEX_SESSION_ROUTES_CHECK_ID,
severity: "warning",
message: [`${issue.path} routes ${issue.modelRef} to ${issue.canonicalModel}`, "with Codex runtime, but the Codex plugin is disabled by config."].join(" "),
path: issue.path,
target: issue.canonicalModel,
requirement: "Codex plugin enabled for routes that use the Codex runtime.",
fixHint: issue.repairBlocked ? ["Enable plugins.entries.codex and plugin loading, and remove codex from plugins.deny;", "or set the affected OpenAI models to an OpenClaw runtime policy."].join(" ") : ["Run `openclaw doctor --fix`: it enables plugins.entries.codex,", "or set the affected OpenAI models to an OpenClaw runtime policy."].join(" ")
}));
const compatibilityFindings = collectCodexRuntimeCompatibilityWarnings(ctx.cfg, ctx.env).map((text) => noteTextToFinding({
checkId: CODEX_SESSION_ROUTES_CHECK_ID,
severity: "warning",
text
}));
return [...disabledPluginFindings, ...compatibilityFindings];
}
};
const telegramGeneralTopicConversationsCheck = {
id: TELEGRAM_GENERAL_TOPIC_CONVERSATIONS_CHECK_ID,
kind: "core",
description: "Telegram General-topic conversation bindings use the canonical chat target.",
source: "doctor",
async detect(ctx) {
const { detectTelegramGeneralTopicConversationRepairs } = await import("./doctor-telegram-general-topic-conversations-DuqWB6Cl.js");
return detectTelegramGeneralTopicConversationRepairs({
cfg: ctx.cfg,
...ctx.env ? { env: ctx.env } : {}
}).map((repair) => ({
checkId: TELEGRAM_GENERAL_TOPIC_CONVERSATIONS_CHECK_ID,
severity: "warning",
message: `Agent ${repair.agentId} has a stale Telegram General-topic conversation identity.`,
target: repair.agentId,
requirement: "One canonical chat-scoped conversation binding for Telegram General topic.",
fixHint: "Run `openclaw doctor --fix` to merge the stale topic-qualified identity."
}));
},
async repair(ctx) {
const { repairTelegramGeneralTopicConversations } = await import("./doctor-telegram-general-topic-conversations-DuqWB6Cl.js");
const effect = {
kind: "state",
action: ctx.dryRun ? "would-merge-stale-bindings" : "merge-stale-bindings",
target: "Telegram General topic conversations",
dryRunSafe: false
};
if (ctx.dryRun) return {
changes: ["Would merge stale Telegram General-topic identities."],
effects: [effect]
};
const repaired = await repairTelegramGeneralTopicConversations({
cfg: ctx.cfg,
...ctx.env ? { env: ctx.env } : {}
});
return {
changes: [`Merged ${repaired} stale Telegram General-topic conversation identity row(s).`],
effects: repaired > 0 ? [effect] : []
};
}
};
const gatewayServicesExtraCheck = {
id: GATEWAY_SERVICES_EXTRA_CHECK_ID,
kind: "core",
description: "Extra gateway-like services are represented as structured findings.",
source: "doctor",
async detect(ctx) {
const coreCtx = ctx;
const { detectExtraGatewayServiceIssues, extraGatewayServiceToHealthFinding } = await import("./doctor-gateway-services-BozKsRzS.js");
return (await detectExtraGatewayServiceIssues({ deep: coreCtx.deep === true })).map(extraGatewayServiceToHealthFinding);
},
async repair(ctx) {
const coreCtx = ctx;
const { detectExtraGatewayServiceIssues, extraGatewayServiceToRepairEffects } = await import("./doctor-gateway-services-BozKsRzS.js");
const effects = (await detectExtraGatewayServiceIssues({ deep: coreCtx.deep === true })).flatMap(extraGatewayServiceToRepairEffects);
if (ctx.dryRun === true) return {
status: "repaired",
changes: [],
effects
};
return {
status: "skipped",
reason: "legacy doctor gateway service contribution owns cleanup",
changes: [],
effects
};
}
};
const gatewayPlatformNotesCheck = {
id: "core/doctor/gateway-services/platform-notes",
kind: "core",
description: "Gateway platform notes are captured as structured findings.",
source: "doctor",
async detect(ctx) {
if (!isDefaultInstallIdentity(process.env)) return [];
const { collectMacGatewayPlatformWarnings } = await import("./doctor-platform-notes-u2dKqvJ4.js");
return (await collectMacGatewayPlatformWarnings(ctx.cfg)).map((warning) => noteTextToFinding({
checkId: "core/doctor/gateway-services/platform-notes",
severity: "warning",
text: warning
}));
}
};
function createGatewayHealthCheck(deps) {
return {
id: GATEWAY_HEALTH_CHECK_ID,
kind: "core",
description: "Authenticated Gateway health and degraded secret owners are structured findings.",
source: "doctor",
defaultEnabled: false,
async detect(ctx) {
return deps.collectGatewayHealthFindings(ctx);
}
};
}
function createGatewayDaemonCheck(deps) {
return {
id: GATEWAY_DAEMON_CHECK_ID,
kind: "core",
description: "Local Gateway daemon service state is represented as structured findings.",
source: "doctor",
defaultEnabled: false,
async detect(ctx) {
return deps.collectGatewayDaemonFindings(ctx);
}
};
}
const browserCheck = {
id: "core/doctor/browser",
kind: "core",
description: "Browser readiness is captured as structured findings.",
source: "doctor",
async detect(ctx) {
const collector = createNoteCollector("core/doctor/browser");
await noteChromeMcpBrowserReadiness(ctx.cfg, { noteFn: collector.noteFn });
return collector.findings;
},
async repair(ctx) {
if (ctx.dryRun === true) return {
status: "skipped",
reason: "native-host repair requires filesystem writes",
changes: []
};
const result = await maybeRepairOwnedChromeExtensionNativeHosts();
return {
...result.changes.length === 0 && result.warnings.length > 0 ? {
status: "failed",
reason: result.warnings.join("; ")
} : {},
changes: result.changes,
warnings: result.warnings
};
}
};
function createSkillsReadinessCheck(deps) {
const detectUnavailableSkills = async (ctx) => {
const runWithPluginMetadataSnapshot = ctx.runWithPluginMetadataSnapshot;
const detect = ctx.cwd ? () => deps.detectUnavailableSkills(ctx) : async () => [];
if (!runWithPluginMetadataSnapshot) return await detect();
return await runWithPluginMetadataSnapshot({
config: ctx.cfg,
workspaceDir: ctx.cwd
}, detect);
};
return {
id: "core/doctor/skills-readiness",
kind: "core",
description: "Allowed skills are usable in the current runtime environment.",
source: "doctor",
defaultEnabled: false,
async detect(ctx, scope) {
return filterUnavailableSkillsForScope(await detectUnavailableSkills(ctx), scope?.paths).map(unavailableSkillToFinding);
},
async repair(ctx, findings) {
const unavailable = filterUnavailableSkillsForScope(await detectUnavailableSkills(ctx), findings.map((finding) => finding.path));
if (unavailable.length === 0) return { changes: [] };
return {
config: disableUnavailableSkillsInConfig(ctx.cfg, unavailable),
changes: unavailable.map((skill) => `Disabled unavailable skill ${skill.name}.`),
effects: unavailable.map((skill) => ({
kind: "config",
action: ctx.dryRun === true ? "would-disable-skill" : "disable-skill",
target: skillReadinessPath(skill),
dryRunSafe: true
}))
};
}
};
}
function unavailableSkillToFinding(skill) {
return {
checkId: "core/doctor/skills-readiness",
severity: "warning",
message: `${skill.name} is allowed but unavailable: ${formatMissingSkillSummary(skill)}.`,
path: skillReadinessPath(skill),
fixHint: "Install/configure the missing requirement, or run `openclaw doctor --fix` to disable unused unavailable skills."
};
}
function filterUnavailableSkillsForScope(unavailable, paths) {
const scopedPaths = new Set(paths?.filter((pathLocal) => pathLocal !== void 0) ?? []);
if (scopedPaths.size === 0) return [...unavailable];
return unavailable.filter((skill) => scopedPaths.has(skillReadinessPath(skill)));
}
function skillReadinessPath(skill) {
return `skills.entries.${skill.skillKey}.enabled`;
}
function browserResidueDeps(ctx) {
return ctx.configPath ? { configDir: path.dirname(ctx.configPath) } : {};
}
function browserResidueFinding(residue) {
return {
checkId: BROWSER_CLAWD_PROFILE_RESIDUE_CHECK_ID,
severity: "warning",
message: `Legacy managed browser profile residue was found at ${residue.legacyProfileDir}.`,
path: residue.legacyProfileDir,
ocPath: "oc://state/browser/clawd",
fixHint: "Run `openclaw doctor --fix` to archive the stale clawd profile safely instead of deleting it in place."
};
}
function formatWouldArchiveBrowserResidue(residue) {
return [
"Would archive legacy clawd managed browser profile residue.",
`- legacy profile: ${residue.legacyProfileDir}`,
`- canonical profile: ${residue.canonicalUserDataDir}`
].join("\n");
}
const browserClawdProfileResidueCheck = {
id: BROWSER_CLAWD_PROFILE_RESIDUE_CHECK_ID,
kind: "core",
description: "Legacy clawd managed browser profile residue has been archived after the OpenClaw rename.",
source: "doctor",
async detect(ctx, scope) {
const residue = await detectLegacyClawdBrowserProfileResidue(ctx.cfg, browserResidueDeps(ctx));
if (!residue) return [];
const scopedPaths = new Set(scope?.paths ?? []);
if (scopedPaths.size > 0 && !scopedPaths.has(residue.legacyProfileDir)) return [];
return [browserResidueFinding(residue)];
},
async repair(ctx) {
const residue = await detectLegacyClawdBrowserProfileResidue(ctx.cfg, browserResidueDeps(ctx));
if (!residue) return {
status: "skipped",
reason: "legacy clawd browser profile residue no longer exists",
changes: []
};
const effect = {
kind: "state",
action: ctx.dryRun === true ? "would-archive-legacy-browser-profile-residue" : "archive-legacy-browser-profile-residue",
target: residue.legacyProfileDir,
dryRunSafe: false
};
if (ctx.dryRun === true) return {
changes: [formatWouldArchiveBrowserResidue(residue)],
effects: [effect]
};
const result = await maybeArchiveLegacyClawdBrowserProfileResidue(ctx.cfg, browserResidueDeps(ctx));
if (result.changes.length === 0 && result.warnings.length > 0) return {
status: "failed",
reason: result.warnings.join("; "),
changes: [],
warnings: result.warnings,
effects: []
};
return {
changes: result.changes,
warnings: result.warnings,
effects: result.changes.length > 0 ? [effect] : []
};
}
};
const finalConfigValidationCheck = {
id: FINAL_CONFIG_VALIDATION_CHECK_ID,
kind: "core",
description: "Active openclaw.jsonc parses and conforms to the config schema.",
source: "doctor",
async detect() {
const { readConfigFileSnapshot } = await import("./config/config.js");
const snap = await readConfigFileSnapshot({ observe: false });
if (!snap.exists || snap.valid) return [];
return configValidationIssuesToHealthFindings(snap.issues);
}
};
const shellCompletionCheck = {
id: "core/doctor/shell-completion",
kind: "core",
description: "Shell completion uses the cached completion path when configured.",
source: "doctor",
async detect() {
return shellCompletionStatusToHealthFindings(await checkShellCompletionStatus());
},
async repair(ctx) {
const status = await checkShellCompletionStatus();
const effects = shellCompletionStatusToRepairEffects(status);
if (ctx.dryRun === true) return {
status: "repaired",
changes: [],
effects
};
return {
status: "skipped",
reason: "legacy doctor shell-completion repair owns real mutations",
changes: [],
effects
};
}
};
const uiProtocolFreshnessCheck = {
id: "core/doctor/ui-protocol-freshness",
kind: "core",
description: "Control UI assets are present and current with the Gateway protocol schema.",
source: "doctor",
async detect() {
return (await detectUiProtocolFreshnessIssues()).map(uiProtocolFreshnessIssueToHealthFinding);
},
async repair(ctx) {
const effects = (await detectUiProtocolFreshnessIssues()).flatMap(uiProtocolFreshnessIssueToRepairEffects);
if (ctx.dryRun === true) return {
status: "repaired",
changes: [],
effects
};
return {
status: "skipped",
reason: "legacy doctor UI freshness repair owns real mutations",
changes: [],
effects
};
}
};
function createWorkspaceSuggestionsCheck(deps) {
return {
id: "core/doctor/workspace-suggestions",
kind: "core",
description: "Workspace backup and memory-system suggestions are captured as structured findings.",
defaultEnabled: false,
source: "doctor",
async detect(ctx) {
const scopes = resolveDoctorWorkspaceSuggestionScopes(ctx.cfg);
return (await Promise.all(scopes.map(async ({ agentId, workspaceDir, labelAgent }) => {
const prefix = labelAgent ? `Agent "${agentId}": ` : "";
return (await deps.collectWorkspaceSuggestionNotes(workspaceDir)).map((text) => noteTextToFinding({
checkId: "core/doctor/workspace-suggestions",
severity: "info",
text: `${prefix}${text}`,
...labelAgent ? { target: agentId } : {}
}));
}))).flat();
}
};
}
function createConvertedWorkflowChecks(deps) {
return [
claudeCliCheck,
gatewayAuthCheck,
legacyStateCheck,
removedWorkspacesStateCheck,
legacyWhatsAppCrontabCheck,
legacyCronStoreCheck,
codexSessionRoutesCheck,
telegramGeneralTopicConversationsCheck,
shellCompletionCheck,
uiProtocolFreshnessCheck,
gatewayServicesExtraCheck,
gatewayPlatformNotesCheck,
createGatewayHealthCheck(deps),
createGatewayDaemonCheck(deps),
createSecurityCheck(deps),
browserCheck,
openAIOAuthTlsCheck,
hooksModelCheck,
bootstrapSizeCheck,
createModelReferenceCheck(),
createProviderCatalogProjectionCheck(deps),
{
id: "core/doctor/local-audio-acceleration",
kind: "core",
description: "Local STT auto-selection and acceleration evidence are visible.",
source: "doctor",
async detect() {
return await deps.collectLocalAudioAccelerationFindings();
}
},
createRuntimeToolSchemaCheck(deps),
createWorkspaceSuggestionsCheck(deps),
skillWorkshopToolPolicyCheck,
...isExperimentalClawsEnabled() ? [{
id: "core/doctor/claws-state",
kind: "core",
description: "Claw lifecycle ownership and managed resources are consistent.",
defaultEnabled: false,
source: "doctor",
async detect(ctx) {
const [{ collectClawStateHealthFindings }, { listConfiguredMcpServers }] = await Promise.all([import("./doctor-WOCFLq4r.js"), import("./mcp-config-Ca8Lo4Am.js")]);
return await collectClawStateHealthFindings({
cfg: ctx.cfg,
env: process.env,
listMcpServers: listConfiguredMcpServers,
cronGateway: { list: async () => await deps.listGatewayCronJobs(ctx) }
});
}
}] : []
];
}
function createCoreHealthChecks(deps = defaultCoreHealthCheckDeps) {
return [
gatewayConfigCheck,
...createConvertedWorkflowChecks(deps),
commandOwnerCheck,
createSkillsReadinessCheck(deps),
browserClawdProfileResidueCheck,
finalConfigValidationCheck
].map(defineSplitHealthCheckInput);
}
const CORE_HEALTH_CHECKS = createCoreHealthChecks();
//#endregion
export { createCoreHealthChecks as a, configValidationIssuesToHealthFindings as i, buildGatewayTokenSecretRefFixHint as n, buildGatewayTokenSecretRefUnavailableMessage as r, CORE_HEALTH_CHECKS as t };