UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

331 lines (330 loc) 13.2 kB
import { S as createConfigIO, s as readConfigFileSnapshot } from "./io.runtime-B9iJRs3w.js"; import { E as tryResolveDefaultAgentId, m as resolveAgentWorkspaceDir } from "./agent-scope-config-DcbEhP0R.js"; import { f as resolveConfigPath, w as resolveStateDir } from "./paths-D2sRr1a_.js"; import { i as prepareSqliteReadOnlyLocationSync } from "./sqlite-readonly-location-BC9PgENz.js"; import { s as resolveOpenClawStateSqlitePath } from "./openclaw-state-db-schema-version-c1ZL6JGz.js"; import { i as withPluginInstallRoots, r as resolvePluginInstallRoots } from "./install-root-context-BSQp2WLR.js"; import "./agent-scope-DbtJyKUL.js"; import { s as maybeLoadDotEnvForConfig } from "./io.read-helpers-ZKp-UiGx.js"; import "./config-Cs0XXL3x.js"; import { i as configValidationIssuesToHealthFindings } from "./doctor-core-checks-BvtLKzuC.js"; import { t as scrubDoctorErrorMessage } from "./doctor-error-message-PVjhuNzO.js"; import { i as listExtensionHealthChecksForDoctor } from "./health-check-registry-CBs_fO63.js"; import { n as healthFindingMeetsSeverity, r as parseHealthFindingSeverity } from "./health-checks-DrfuiOOz.js"; import { n as runDoctorLintChecks, r as selectUpdateReadinessChecks, t as exitCodeFromFindings } from "./doctor-lint-flow-Dzj8Qk94.js"; import { u as isPostCoreConvergencePass } from "./update-phase-J3w3q1-f.js"; import { n as resolveBundledHealthCheckPluginStateMode, t as registerBundledHealthChecks } from "./bundled-health-checks-Pa6weB1A.js"; import { t as resolveDoctorContributionHealthChecks } from "./doctor-health-contributions-ILex_TEG.js"; import fs from "node:fs"; import path from "node:path"; import os from "node:os"; //#region src/commands/doctor-lint.ts /** CLI entrypoint for non-mutating doctor lint health checks. */ const RUNTIME_TOOL_SCHEMA_CHECK_ID = "core/doctor/runtime-tool-schemas"; 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"; } }; function detectMode(opts) { if (opts.json === true) return "json"; return process.stdout.isTTY ? "human" : "json"; } /** * Runs registered doctor health checks in human or JSON mode and returns the lint exit code. * * Invalid config is reported before regular health checks because most checks need a parsed config * and workspace root. */ async function runDoctorLintCli(runtime, opts) { const execution = await prepareDoctorLintExecution(runtime, opts); execution.writeOutput(); return execution.exitCode; } /** Collect advisory doctor findings without writing output or repairing operator state. */ async function collectDoctorFindings(runtime) { return (await prepareDoctorLintExecution(runtime, { severityMin: "info" })).findings; } async function prepareDoctorLintExecution(runtime, opts) { 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 }; const updateReadiness = isPostCoreConvergencePass(sourceEnv) ? "post-plugin" : void 0; const effectiveOpts = updateReadiness ? { ...opts, updateReadiness } : opts; const pluginStateMode = resolveBundledHealthCheckPluginStateMode(effectiveOpts); const stateView = { pluginMetadataEnv: sourceEnv, sourceEnv, readConfigSnapshot: () => pluginStateMode === "direct" ? readConfigFileSnapshot({ observe: false }) : createConfigIO({ env: sourceEnv, configPath: resolveConfigPath(sourceEnv, resolveStateDir(sourceEnv)), observe: false, pluginValidation: pluginStateMode === "deferred" ? "core-only" : void 0 }).readConfigFileSnapshot(), runWithPluginStateSnapshot: async (run) => withReadOnlyPluginStateSnapshot(sourceEnv, run) }; if (pluginStateMode !== "isolated") return await executeDoctorLint(runtime, effectiveOpts, sevMin, stateView); try { return await withReadOnlyPluginStateSnapshot(sourceEnv, async (pluginMetadataEnv) => executeDoctorLint(runtime, effectiveOpts, sevMin, { ...stateView, pluginMetadataEnv, runWithPluginStateSnapshot: async (run) => run(pluginMetadataEnv) })); } catch (error) { if (!(error instanceof DoctorLintStateSnapshotError)) throw error; return createStateSnapshotFailureExecution(runtime, effectiveOpts, sevMin, error); } } async function executeDoctorLint(runtime, opts, sevMin, stateView) { const snapshot = await stateView.readConfigSnapshot(); if (snapshot.exists && !snapshot.valid) { const findings = configValidationIssuesToHealthFindings(snapshot.issues); const visible = findings.filter((finding) => healthFindingMeetsSeverity(finding, sevMin)); return { exitCode: exitCodeFromFindings(findings, sevMin), findings: visible, writeOutput() { if (detectMode(opts) === "json") { writeJsonResult({ ok: false, checksRun: 1, checksSkipped: 0, findings: visible }); 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}`); } } }; } const sourceEnv = { ...stateView.sourceEnv }; const defaultAgentId = tryResolveDefaultAgentId(snapshot.config); const ctx = { mode: "lint", runtime, cfg: snapshot.config, cwd: defaultAgentId ? resolveAgentWorkspaceDir(snapshot.config, defaultAgentId) : process.cwd(), env: sourceEnv, allowExecSecretRefs: opts.allowExec === true, ...snapshot.path !== void 0 ? { configPath: snapshot.path } : {} }; registerBundledHealthChecks({ cfg: snapshot.config, cwd: ctx.cwd, env: stateView.pluginMetadataEnv, runWithPluginStateSnapshot: stateView.runWithPluginStateSnapshot, updateReadiness: opts.updateReadiness }); const registeredExtensionChecks = listExtensionHealthChecksForDoctor([]); const onlyRegisteredExtensionChecks = opts.onlyIds !== void 0 && opts.onlyIds.length > 0 && opts.onlyIds.every((id) => registeredExtensionChecks.some((check) => check.id === id)); const coreChecks = onlyRegisteredExtensionChecks ? [] : await resolveDoctorContributionHealthChecks(); const extensionChecks = onlyRegisteredExtensionChecks ? registeredExtensionChecks : listExtensionHealthChecksForDoctor(coreChecks); const runWithPrivateStateSnapshot = async (run) => await stateView.runWithPluginStateSnapshot(async () => await run()); const runWithSourceState = async (run) => opts.updateReadiness ? run() : withDoctorLintStateEnv(sourceEnv, run); const coreCtx = { ...ctx, deep: opts.deep === true, runWithPrivateStateSnapshot, runWithSourceState }; const checks = [...coreChecks.map((check) => withCoreLintContext(check, coreCtx)), ...extensionChecks]; const runOpts = { checks: opts.updateReadiness ? selectUpdateReadinessChecks(checks, opts.updateReadiness) : checks, includeAllChecks: opts.updateReadiness !== void 0 || opts.includeAllChecks === true, ...opts.skipIds && opts.skipIds.length > 0 ? { skipIds: opts.skipIds } : {}, ...opts.onlyIds && opts.onlyIds.length > 0 ? { onlyIds: opts.onlyIds } : {} }; const result = await runDoctorLintChecks(ctx, runOpts); const visible = result.findings.filter((finding) => healthFindingMeetsSeverity(finding, sevMin)); const exitCode = exitCodeFromFindings(result.findings, sevMin); return { exitCode, findings: visible, writeOutput() { if (detectMode(opts) === "json") { writeJsonResult({ ok: exitCode === 0, checksRun: result.checksRun, checksSkipped: result.checksSkipped, findings: visible }); return; } process.stdout.write(`doctor --lint: ran ${result.checksRun} check(s), ${visible.length} finding(s)\n`); if (visible.length === 0) { process.stdout.write(" no findings\n"); return; } for (const f of visible) { 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) { const sourceDatabasePath = resolveOpenClawStateSqlitePath(sourceEnv); let cleanup; let privateRoot; let prepared; try { if (fs.existsSync(sourceDatabasePath)) { prepared = prepareSqliteReadOnlyLocationSync(sourceDatabasePath); privateRoot = path.dirname(prepared.location); cleanup = prepared.cleanup; } else { privateRoot = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-doctor-lint-state-")); cleanup = () => { try { fs.rmSync(privateRoot, { force: true, recursive: true }); return true; } catch { return false; } }; } } catch (error) { throw new DoctorLintStateSnapshotError(error); } let outcome; let runStarted = false; try { const privateStateDir = path.join(privateRoot, "openclaw-state"); const privateDatabasePath = resolveOpenClawStateSqlitePath({ ...sourceEnv, OPENCLAW_STATE_DIR: privateStateDir }); 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 sourceConfigPath = resolveConfigPath(sourceEnv, resolveStateDir(sourceEnv)); const privateEnv = { ...sourceEnv, OPENCLAW_CONFIG_PATH: sourceConfigPath, OPENCLAW_STATE_DIR: privateStateDir }; const installRoots = resolvePluginInstallRoots(sourceEnv); outcome = { ok: true, value: await withDoctorLintStateEnv(privateEnv, () => withPluginInstallRoots({ ...installRoots, stateDir: privateStateDir }, async () => { runStarted = true; return await run(privateEnv); })) }; } catch (error) { outcome = { ok: false, error }; } if (!cleanup()) throw new DoctorLintStateSnapshotError(/* @__PURE__ */ new Error("Temporary doctor lint state snapshot cleanup did not complete.")); if (!outcome.ok) throw runStarted ? outcome.error : new DoctorLintStateSnapshotError(outcome.error); return outcome.value; } 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 createStateSnapshotFailureExecution(runtime, opts, sevMin, error) { const finding = { checkId: "core/doctor/lint-state-inspection", severity: "error", source: "doctor", target: "plugin-state", requirement: "read-only-plugin-state-inspection", message: `Doctor lint could not inspect plugin state without mutating the live state database (${scrubDoctorErrorMessage(error.cause ?? error)}).`, fixHint: "Keep the current Gateway running, resolve the state database inspection error, then rerun this check." }; const visible = healthFindingMeetsSeverity(finding, sevMin) ? [finding] : []; return { exitCode: exitCodeFromFindings([finding], sevMin), findings: visible, writeOutput() { if (detectMode(opts) === "json") { writeJsonResult({ ok: false, checksRun: 0, checksSkipped: 0, findings: visible }); return; } runtime.error(`doctor --lint: ${finding.message}`); runtime.error(`fix: ${finding.fixHint}`); } }; } function withCoreLintContext(check, ctx) { return { ...check, detect(_ctx, scope) { const detect = async () => await check.detect(ctx, scope); if (check.id === RUNTIME_TOOL_SCHEMA_CHECK_ID) return ctx.runWithPrivateStateSnapshot(detect); return check.id === AUTH_PROFILE_CHECK_ID ? ctx.runWithSourceState(detect) : detect(); } }; } function writeJsonResult(result) { process.stdout.write(JSON.stringify({ ok: result.ok, checksRun: result.checksRun, checksSkipped: result.checksSkipped, findings: result.findings.map(toJsonFinding) }) + "\n"); } function toJsonFinding(f) { return { checkId: f.checkId, severity: f.severity, message: f.message, ...f.source !== void 0 ? { source: f.source } : {}, ...f.path !== void 0 ? { path: f.path } : {}, ...f.line !== void 0 ? { line: f.line } : {}, ...f.column !== void 0 ? { column: f.column } : {}, ...f.ocPath !== void 0 ? { ocPath: f.ocPath } : {}, ...f.target !== void 0 ? { target: f.target } : {}, ...f.requirement !== void 0 ? { requirement: f.requirement } : {}, ...f.fixHint !== void 0 ? { fixHint: f.fixHint } : {} }; } //#endregion export { collectDoctorFindings, runDoctorLintCli };