UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

478 lines (477 loc) • 21.9 kB
import { r as resolveStateDir } from "./state-dir-BHJORNly.mjs"; import { m as resolveConfigPath } from "./paths-DGIwWbpu.mjs"; import { t as SqliteSnapshotCleanupError } from "./sqlite-readonly-location-cleanup-CPS8QEIp.mjs"; import { c as resolveOpenClawStateSqlitePath } from "./openclaw-state-db.paths-DfifnSGp.mjs"; import { n as prepareSqliteReadOnlyLocationSync } from "./sqlite-snapshot-source-BPapZs5n.mjs"; import { i as resolveUpdateRehearsalRoot } from "./update-rehearsal-paths-DTVdt0cq.mjs"; import { s as closeOpenClawStateDatabaseByPathAsync } from "./openclaw-state-db-cache-BKCnj4of.mjs"; import { c as withDisposableOpenClawStateReads, m as withOpenClawStateDatabaseReadSnapshot, o as withArtifactPreservingStateReads } from "./openclaw-state-db-readonly-CC57AD_q.mjs"; import { d as resolveAgentWorkspaceDir } from "./agent-scope-config-CDYpzA0r.mjs"; import { o as tryResolveDefaultAgentId } from "./agent-roster-BXD1uMJD.mjs"; import { l as isPostCoreConvergencePass, u as isUpdateDoctorLintPass } from "./update-phase-BClSJ17n.mjs"; import { c as readDeferredPluginMigrations } from "./deferred-plugin-migrations-by1ypkxx.mjs"; import { a as withPluginInstallRoots, i as resolvePluginInstallRoots } from "./install-root-context-DhumTK8D.mjs"; import { a as withPluginSourceCaptureStorage, t as getPluginSourceCaptureStorage } from "./plugin-source-capture-context-B9nV7ZEy.mjs"; import { o as maybeLoadDotEnvForConfig, t as createConfigIO } from "./io.factory-0ZgSoWaa.mjs"; import "./agent-scope-CG17uXFn.mjs"; import { t as captureRuntimeConfig } from "./runtime-source-projection-rvQFgz1Z.mjs"; import { c as readConfigFileSnapshot, d as readConfigFileSnapshotWithPluginMetadata } from "./io.runtime-BsUXSsD4.mjs"; import "./config-DLycWFvB.mjs"; import { t as UPDATE_DOCTOR_DISPOSAL_WARNING_PREFIX } from "./update-doctor-lint-kv_-CSRP.mjs"; import { r as closeAuthProfileReadPool } from "./sqlite-read-pool-B3hiJ5bv.mjs"; import "./sqlite-CW8wNN4G.mjs"; import { t as scrubDoctorErrorMessage } from "./doctor-error-message-CBAGGfGW.mjs"; import { n as configValidationIssuesToHealthFindings } from "./doctor-config-validation-findings-C1THItUh.mjs"; import { n as listExtensionHealthChecksForDoctor } from "./health-check-registry-CBxbsDqv.mjs"; import { i as parseHealthFindingSeverity, n as healthFindingMeetsSeverity, r as isHealthCheckEnabledByDefault } from "./health-checks-B3n_-tw2.mjs"; import { n as runDoctorLintChecks, r as selectUpdateReadinessChecks, t as exitCodeFromFindings } from "./doctor-lint-flow-CEtrAUUS.mjs"; import { a as writeJsonResult, n as createStateSnapshotFailureFinding, r as detectDoctorLintOutputMode, t as createStateSnapshotFailureExecution } from "./doctor-lint-output-DAJb22Jp.mjs"; import { n as resolveDoctorUpdateBudget, t as admitDoctorUpdateInspection } from "./doctor-update-budget-7GZXmhPI.mjs"; import fs from "node:fs"; import path from "node:path"; import os from "node:os"; //#region src/commands/doctor-lint-runner.ts const RUNTIME_TOOL_SCHEMA_CHECK_ID = "core/doctor/runtime-tool-schemas"; const PROJECT_CLONE_SHAPE_CHECK_ID = "core/doctor/project-clone-shape"; const SKILLS_READINESS_CHECK_ID = "core/doctor/skills-readiness"; const AUTH_PROFILE_CHECK_ID = "core/doctor/auth-profiles"; var DoctorLintStateSnapshotError = class extends Error { constructor(cause) { super(`Doctor lint could not prepare a private plugin-state snapshot: ${scrubDoctorErrorMessage(cause)}`, { cause }); this.name = "DoctorLintStateSnapshotError"; } }; /** The rehearsal worker keeps its private state alive after publishing the check result. */ async function runDoctorLintCliInProcess(runtime, opts, reportBeforeDisposal = false) { let reported; const report = (execution) => { execution.writeOutput(); reported = execution; }; try { const execution = await withArtifactPreservingStateReads(() => prepareDoctorLintExecution(runtime, opts, reportBeforeDisposal ? report : void 0)); if (!reported) report(execution); else for (const warning of execution.cleanupWarnings ?? []) runtime.error(`${UPDATE_DOCTOR_DISPOSAL_WARNING_PREFIX}: ${warning.message}`); return execution.exitCode; } catch (error) { if (!reported) throw error; runtime.error(`${UPDATE_DOCTOR_DISPOSAL_WARNING_PREFIX}: ${scrubDoctorErrorMessage(error)}`); return reported.exitCode; } } /** Collect advisory doctor findings without writing output or repairing operator state. */ async function collectDoctorFindings(runtime) { return (await withArtifactPreservingStateReads(() => prepareDoctorLintExecution(runtime, { severityMin: "info" }))).findings; } async function prepareDoctorLintExecution(runtime, opts, onChecksComplete) { const sevMin = opts.severityMin === void 0 ? "warning" : parseHealthFindingSeverity(opts.severityMin); if (sevMin === null) throw new Error("Invalid --severity-min value. Expected one of: info, warning, error."); maybeLoadDotEnvForConfig(process.env); const sourceEnv = { ...process.env }; if (resolveUpdateRehearsalRoot(sourceEnv) && !opts.onlyIds?.length) { const snapshot = await createConfigIO({ env: sourceEnv, observe: false, pluginValidation: "core-only" }).readConfigFileSnapshot(); if (snapshot.valid) { const budget = await resolveDoctorUpdateBudget({ cfg: snapshot.config, env: sourceEnv }); if (budget && !admitDoctorUpdateInspection(budget, "agent", [{ id: "core/doctor/lint-inspection", label: "Doctor lint inspection" }])) { const warnings = [...budget.deferred.values()]; return { checksRun: 0, checksSkipped: 0, exitCode: 0, findings: warnings, writeOutput() { if (detectDoctorLintOutputMode(opts) === "json") writeJsonResult({ ok: true, checksRun: 0, checksSkipped: 0, findings: [], warnings }); else for (const finding of warnings) { runtime.log(`[warning] ${finding.checkId} [${finding.errorCode}]: ${finding.message}`); runtime.log(finding.fixHint ?? "Run `openclaw doctor` after activation."); } } }; } } } const cleanupWarnings = isUpdateDoctorLintPass(sourceEnv) ? [] : void 0; const run = () => prepareDoctorLintStateExecution(runtime, opts, sevMin, sourceEnv, cleanupWarnings, onChecksComplete); if (opts.onlyIds?.length) return await run(); let execution; try { return await withOpenClawStateDatabaseReadSnapshot(async () => execution = await run(), { env: sourceEnv }); } catch (error) { if (!execution || !cleanupWarnings || !(error instanceof SqliteSnapshotCleanupError)) throw error; recordSnapshotCleanupWarning(cleanupWarnings); return execution; } } async function prepareDoctorLintStateExecution(runtime, opts, sevMin, sourceEnv, cleanupWarnings, onChecksComplete) { const updateReadiness = isPostCoreConvergencePass(sourceEnv) ? "post-plugin" : void 0; const effectiveOpts = updateReadiness ? { ...opts, updateReadiness } : opts; const { resolveBundledHealthCheckPluginStateMode } = await import("./bundled-health-checks-DNGo_s75.mjs"); const pluginStateMode = resolveBundledHealthCheckPluginStateMode(effectiveOpts); let coreChecks; const deferredCheckIds = /* @__PURE__ */ new Set(); if (resolveUpdateRehearsalRoot(sourceEnv) && !updateReadiness && !opts.onlyIds?.length) { const { resolveDoctorContributionHealthChecks } = await import("./doctor-health-contributions-Bwz_Auf8.mjs"); coreChecks = await resolveDoctorContributionHealthChecks(); for (const check of coreChecks) if ((check.updateWork?.kind === "inspection" || check.updateWork?.kind === "standalone") && (opts.includeAllChecks === true || isHealthCheckEnabledByDefault(check)) && !opts.skipIds?.includes(check.id)) deferredCheckIds.add(check.id); } const prepareRuntimeValidation = !deferredCheckIds.has(RUNTIME_TOOL_SCHEMA_CHECK_ID) && (pluginStateMode === "isolated" || !effectiveOpts.onlyIds?.length || effectiveOpts.onlyIds.includes(RUNTIME_TOOL_SCHEMA_CHECK_ID)); const readConfigSnapshot = async (deferredPluginMigrations) => { const io = pluginStateMode === "direct" ? { readConfigFileSnapshot, readConfigFileSnapshotWithPluginMetadata } : createConfigIO({ env: sourceEnv, configPath: resolveConfigPath(sourceEnv, resolveStateDir(sourceEnv)), observe: false, pluginValidation: pluginStateMode === "deferred" ? "core-only" : void 0, deferredPluginMigrations }); return pluginStateMode === "deferred" || !prepareRuntimeValidation ? io.readConfigFileSnapshot({ observe: false }) : (await io.readConfigFileSnapshotWithPluginMetadata({ observe: false, prepareValidation: "runtime" })).snapshot; }; const stateView = { coreChecks, deferredCheckIds, cleanupWarnings, pluginMetadataEnv: sourceEnv, sourceEnv, readConfigSnapshot, runWithPluginStateSnapshot: async (run) => withReadOnlyPluginStateSnapshot(sourceEnv, run, cleanupWarnings) }; if (pluginStateMode !== "isolated" && !onChecksComplete) return await executeDoctorLint(runtime, effectiveOpts, sevMin, stateView); let checksReported = false; let completedExecution; try { return await stateView.runWithPluginStateSnapshot(async (pluginMetadataEnv) => { const pending = readDeferredPluginMigrations({ env: pluginMetadataEnv }); const disposals = []; try { const execution = await executeDoctorLint(runtime, effectiveOpts, sevMin, { ...stateView, pluginMetadataEnv, readConfigSnapshot: () => readConfigSnapshot(pending), runWithPluginStateSnapshot: async (run) => run(pluginMetadataEnv), deferInspectionDisposal: onChecksComplete ? (dispose) => disposals.push(dispose) : void 0 }); completedExecution = execution; onChecksComplete?.(execution); checksReported = onChecksComplete !== void 0; return execution; } finally { for (const dispose of disposals) try { await dispose(); } catch (error) { runtime.error(`${UPDATE_DOCTOR_DISPOSAL_WARNING_PREFIX}: ${scrubDoctorErrorMessage(error)}`); } } }); } catch (error) { if (checksReported || !(error instanceof DoctorLintStateSnapshotError)) throw error; return createStateSnapshotFailureExecution(runtime, effectiveOpts, sevMin, error, completedExecution); } } async function executeDoctorLint(runtime, opts, sevMin, stateView) { const snapshot = await stateView.readConfigSnapshot(); if (snapshot.exists && !snapshot.valid) { const { collectNodeRuntimeFindings } = await import("./node-runtime-diagnostics-dS1d8zId.mjs"); const runtimeFindings = await collectNodeRuntimeFindings(stateView.sourceEnv); const findings = [...configValidationIssuesToHealthFindings(snapshot.issues), ...runtimeFindings]; const visible = findings.filter((finding) => healthFindingMeetsSeverity(finding, sevMin)); return { checksRun: 1, checksSkipped: 0, exitCode: exitCodeFromFindings(findings, sevMin), findings: visible, cleanupWarnings: stateView.cleanupWarnings, writeOutput() { if (detectDoctorLintOutputMode(opts) === "json") { writeJsonResult({ ok: false, checksRun: 1, checksSkipped: 0, findings: visible, warnings: stateView.cleanupWarnings }); return; } runtime.error("doctor --lint: config file exists but does not parse cleanly."); for (const issue of snapshot.issues) { const issuePath = issue.path || "<root>"; runtime.error(`- ${issuePath}: ${issue.message}`); } for (const finding of runtimeFindings.filter((entry) => healthFindingMeetsSeverity(entry, sevMin))) runtime.error(finding.fixHint ? `${finding.message}\n${finding.fixHint}` : finding.message); for (const warning of stateView.cleanupWarnings ?? []) runtime.error(`${UPDATE_DOCTOR_DISPOSAL_WARNING_PREFIX}: ${warning.message}`); } }; } const cfg = captureRuntimeConfig(snapshot.config); const sourceEnv = { ...stateView.sourceEnv }; const defaultAgentId = tryResolveDefaultAgentId(cfg); const ctx = { mode: "lint", runtime, cfg, cwd: defaultAgentId ? resolveAgentWorkspaceDir(cfg, defaultAgentId) : process.cwd(), env: sourceEnv, allowExecSecretRefs: opts.allowExec === true, ...snapshot.path !== void 0 ? { configPath: snapshot.path } : {} }; const { registerBundledHealthChecks } = await import("./bundled-health-checks-DNGo_s75.mjs"); const availabilityFindings = registerBundledHealthChecks({ cfg, cwd: ctx.cwd, env: stateView.pluginMetadataEnv, runWithPluginStateSnapshot: stateView.runWithPluginStateSnapshot, updateReadiness: opts.updateReadiness }); const registeredExtensionChecks = listExtensionHealthChecksForDoctor([], availabilityFindings); const onlyRegisteredExtensionChecks = opts.onlyIds !== void 0 && opts.onlyIds.length > 0 && opts.onlyIds.every((id) => registeredExtensionChecks.some((check) => check.id === id)); let coreChecks = onlyRegisteredExtensionChecks ? [] : stateView.coreChecks; if (!coreChecks) { const { resolveDoctorContributionHealthChecks } = await import("./doctor-health-contributions-Bwz_Auf8.mjs"); coreChecks = await resolveDoctorContributionHealthChecks(); } const extensionChecks = onlyRegisteredExtensionChecks ? registeredExtensionChecks : listExtensionHealthChecksForDoctor(coreChecks, availabilityFindings); const runWithPrivateStateSnapshot = async (run) => await stateView.runWithPluginStateSnapshot(async () => await run()); const runWithSourceState = async (run) => opts.updateReadiness ? run() : withDoctorLintStateEnv(sourceEnv, run); const coreCtx = { ...ctx, env: opts.updateReadiness ? stateView.pluginMetadataEnv : sourceEnv, lintConfigSnapshot: snapshot, deep: opts.deep === true, runWithPrivateStateSnapshot, runWithSourceState, deferInspectionDisposal: stateView.deferInspectionDisposal }; const checks = [...coreChecks.map((check) => withCoreLintContext(check, coreCtx, availabilityFindings)), ...extensionChecks]; const runOpts = { checks: opts.updateReadiness ? selectUpdateReadinessChecks(checks, opts.updateReadiness) : checks, includeAllChecks: opts.updateReadiness !== void 0 || opts.includeAllChecks === true, skipIds: [...opts.skipIds ?? [], ...stateView.deferredCheckIds ?? []], ...opts.onlyIds && opts.onlyIds.length > 0 ? { onlyIds: opts.onlyIds } : {} }; const result = await runDoctorLintChecks(ctx, runOpts); const detectedFindings = [...result.findings, ...coreChecks.filter((check) => stateView.deferredCheckIds?.has(check.id)).map((check) => ({ checkId: check.id, source: "doctor", severity: "warning", errorCode: "update-inspection-deferred", requirement: "update-validation-scope", message: "Advisory inspection deferred until after update activation; required migration, config, plugin, and Gateway readiness checks remain enabled.", fixHint: `Run \`openclaw doctor --lint --only ${check.id}\` after activation to complete this inspection.` }))]; const advisoryChecks = new Set(coreChecks.filter((check) => check.updateWork?.kind === "inspection" || check.updateWork?.kind === "standalone").map((check) => check.id)); const findings = isUpdateDoctorLintPass(stateView.sourceEnv) ? detectedFindings.map((finding) => finding.severity === "error" && advisoryChecks.has(finding.checkId) ? { ...finding, severity: "warning" } : finding) : detectedFindings; const visible = findings.filter((finding) => healthFindingMeetsSeverity(finding, sevMin)); const warnings = findings.filter((finding) => !healthFindingMeetsSeverity(finding, sevMin) && (finding.errorCode === "OPENCLAW_STATE_LEASE_ABORTED" || isUpdateDoctorLintPass(stateView.sourceEnv) && finding.severity === "warning")); const exitCode = exitCodeFromFindings(findings, sevMin); return { checksRun: result.checksRun, checksSkipped: result.checksSkipped, exitCode, findings: visible, warnings, cleanupWarnings: stateView.cleanupWarnings, writeOutput() { if (detectDoctorLintOutputMode(opts) === "json") { writeJsonResult({ ok: exitCode === 0, checksRun: result.checksRun, checksSkipped: result.checksSkipped, findings: visible, warnings: [...warnings, ...stateView.cleanupWarnings ?? []] }); return; } const displayed = [ ...visible, ...warnings, ...stateView.cleanupWarnings ?? [] ]; process.stdout.write(`doctor --lint: ran ${result.checksRun} check(s), ${displayed.length} finding(s)\n`); if (displayed.length === 0) { process.stdout.write(" no findings\n"); return; } for (const f of displayed) { const where = f.path !== void 0 ? ` ${f.path}` : ""; const line = f.line !== void 0 ? `:${f.line}` : ""; process.stdout.write(` [${f.severity}] ${f.checkId}${where}${line} - ${f.message}\n`); if (f.fixHint !== void 0) process.stdout.write(` fix: ${f.fixHint}\n`); } } }; } async function withReadOnlyPluginStateSnapshot(sourceEnv, run, cleanupWarnings) { const sourceDatabasePath = resolveOpenClawStateSqlitePath(sourceEnv); const captureStorage = Object.freeze({ stateDir: getPluginSourceCaptureStorage()?.stateDir ?? resolveStateDir(sourceEnv), placement: "temporary" }); let cleanup; let privateRoot; let prepared; try { if (fs.existsSync(sourceDatabasePath)) { prepared = prepareSqliteReadOnlyLocationSync(sourceDatabasePath); privateRoot = path.dirname(prepared.location); cleanup = prepared.cleanupAsync; } else { privateRoot = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-doctor-lint-state-")); cleanup = async () => { try { await fs.promises.rm(privateRoot, { force: true, recursive: true }); return true; } catch { return false; } }; } } catch (error) { throw new DoctorLintStateSnapshotError(error); } const privateStateDir = path.join(privateRoot, "openclaw-state"); const privateDatabasePath = resolveOpenClawStateSqlitePath({ ...sourceEnv, OPENCLAW_STATE_DIR: privateStateDir }); const privateEnv = { ...sourceEnv, OPENCLAW_CONFIG_PATH: resolveConfigPath(sourceEnv, resolveStateDir(sourceEnv)), OPENCLAW_STATE_DIR: privateStateDir }; return await withDoctorLintStateEnv(privateEnv, async () => { let outcome; let runStarted = false; try { fs.mkdirSync(path.dirname(privateDatabasePath), { recursive: true, mode: 448 }); if (prepared) for (const suffix of [ "", "-journal", "-shm", "-wal" ]) { const sourcePath = `${prepared.location}${suffix}`; if (fs.existsSync(sourcePath)) fs.renameSync(sourcePath, `${privateDatabasePath}${suffix}`); } const installRoots = resolvePluginInstallRoots(sourceEnv); outcome = { ok: true, value: await withPluginSourceCaptureStorage(captureStorage, () => withDisposableOpenClawStateReads(privateDatabasePath, () => withPluginInstallRoots({ ...installRoots, stateDir: privateStateDir }, async () => { runStarted = true; return await run(privateEnv); }))) }; } catch (error) { outcome = { ok: false, error }; } try { const retirementErrors = []; try { closeAuthProfileReadPool({ kind: "root", rootPath: privateStateDir }); } catch (error) { retirementErrors.push(error); } try { await closeOpenClawStateDatabaseByPathAsync(privateDatabasePath); } catch (error) { retirementErrors.push(error); } if (retirementErrors.length > 0) throw new AggregateError(retirementErrors, retirementErrors.map((error) => scrubDoctorErrorMessage(error)).join("; ")); if (!await cleanup()) { const message = "Temporary doctor lint state snapshot cleanup did not complete."; if (!cleanupWarnings) throw new Error(message); recordSnapshotCleanupWarning(cleanupWarnings); } } catch (error) { throw new DoctorLintStateSnapshotError(outcome.ok ? error : new AggregateError([outcome.error, error], `${scrubDoctorErrorMessage(outcome.error)}; ${scrubDoctorErrorMessage(error)}`)); } if (!outcome.ok) throw runStarted ? outcome.error : new DoctorLintStateSnapshotError(outcome.error); return outcome.value; }); } function recordSnapshotCleanupWarning(warnings) { warnings.push({ checkId: "core/doctor/lint-state-inspection", severity: "warning", requirement: "temporary-snapshot-cleanup", message: "Temporary doctor lint state snapshot cleanup did not complete.", fixHint: "Rerun `openclaw doctor --lint` after the update to check snapshot cleanup." }); } async function withDoctorLintStateEnv(env, run) { const stateDir = resolveStateDir(env); const overrides = { OPENCLAW_CONFIG_PATH: resolveConfigPath(env, stateDir), OPENCLAW_STATE_DIR: stateDir }; const previous = Object.keys(overrides).map((key) => [key, process.env[key]]); Object.assign(process.env, overrides); try { return await run(); } finally { for (const [key, value] of previous) if (value === void 0) delete process.env[key]; else process.env[key] = value; } } function withCoreLintContext(check, ctx, availabilityFindings) { return { ...check, detect(_ctx, scope) { const detect = async () => [...await check.detect(ctx, scope), ...availabilityFindings.filter((finding) => finding.checkId === check.id)]; const inspectPrivate = async (run) => { let completed; try { return await ctx.runWithPrivateStateSnapshot(async () => completed = await run()); } catch (error) { if (!completed?.length || !(error instanceof DoctorLintStateSnapshotError)) throw error; return [...completed, createStateSnapshotFailureFinding(error)]; } }; if (check.id === SKILLS_READINESS_CHECK_ID) return inspectPrivate(() => ctx.runWithSourceState(detect)); if (check.id === RUNTIME_TOOL_SCHEMA_CHECK_ID || check.id === PROJECT_CLONE_SHAPE_CHECK_ID) return inspectPrivate(detect); return check.id === AUTH_PROFILE_CHECK_ID ? ctx.runWithSourceState(detect) : detect(); } }; } //#endregion export { collectDoctorFindings, runDoctorLintCliInProcess };