openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
253 lines (252 loc) • 11.9 kB
JavaScript
import { n as isExperimentalClawsEnabled } from "./experimental-BMzbGmT5.js";
import "./src-vebZIeLe.js";
import { t as coerceErrorMessage } from "./error-coercion-D_-xJ90S.js";
import { t as stableStringify } from "./stable-stringify-C8X7niaI.js";
import { i as openOpenClawStateDatabase, r as openExistingOpenClawStateDatabaseReadOnly } from "./openclaw-state-db-BRTnL-D8.js";
import { i as resolveDefaultCronStaggerMs } from "./types-DQrueG6d.js";
import { t as listConfiguredMcpServers } from "./mcp-config-BmNLJlVl.js";
import { r as readClawStatus } from "./lifecycle-state-CAXv1SuG.js";
import { createHash } from "node:crypto";
//#region src/claws/doctor.ts
const CLAW_STATE_CHECK_ID = "core/doctor/claws-state";
function finding(params) {
return {
checkId: CLAW_STATE_CHECK_ID,
source: "doctor",
severity: params.severity ?? "warning",
...params
};
}
function cronExecutionDigest(value) {
return `sha256:${createHash("sha256").update(stableStringify(value)).digest("hex")}`;
}
function expectedCronExecutionDigest(record, cron) {
const job = cron.job;
const staggerMs = resolveDefaultCronStaggerMs(job.schedule.cron);
return cronExecutionDigest({
declarationKey: cron.declarationKey,
ownerAgentId: record.install.agentId,
enabled: true,
schedule: {
kind: "cron",
expr: job.schedule.cron,
...job.schedule.timezone ? { tz: job.schedule.timezone } : {},
...staggerMs !== void 0 ? { staggerMs } : {}
},
sessionTarget: job.session === "main" ? `session:agent:${record.install.agentId}:main` : job.session,
wakeMode: "now",
payload: {
kind: "agentTurn",
message: job.message
},
delivery: job.delivery ? {
mode: job.delivery.mode,
...job.delivery.channel ? { channel: job.delivery.channel } : {}
} : { mode: "none" }
});
}
function liveCronExecutionDigest(job) {
return cronExecutionDigest({
declarationKey: job.declarationKey,
ownerAgentId: job.owner?.agentId ?? job.agentId,
enabled: job.enabled,
schedule: job.schedule,
sessionTarget: job.sessionTarget,
wakeMode: job.wakeMode,
payload: job.payload,
delivery: job.delivery ?? { mode: "none" }
});
}
function collectInstallFindings(record, cronInventory) {
const agentId = record.install.agentId;
const findings = [];
if (record.install.status !== "complete") findings.push(finding({
message: `Claw agent ${JSON.stringify(agentId)} has an incomplete install record (${record.install.status}).`,
path: `claws.${agentId}`,
target: agentId,
requirement: "Claw installs should complete or retain explicit partial ownership state",
fixHint: "Inspect `openclaw claws status` before retrying or removing this Claw."
}));
if (record.agentState !== "present") findings.push(finding({
message: record.agentState === "missing" ? `Claw-owned agent ${JSON.stringify(agentId)} is missing from config.` : `Claw-owned agent ${JSON.stringify(agentId)} changed after installation.`,
path: `agents.list.${agentId}`,
target: agentId,
requirement: "Claw-owned agent config should match its recorded install digest",
fixHint: "Inspect the agent change before removing or replacing Claw-owned state."
}));
for (const file of record.workspaceFiles) {
if (file.state === "unchanged") continue;
findings.push(finding({
message: file.state === "missing" ? `Claw-managed workspace file is missing: ${file.path}` : file.state === "modified" ? `Claw-managed workspace file changed after installation: ${file.path}` : `Claw-managed workspace file is unsafe to inspect: ${file.path}${file.message ? ` (${file.message})` : ""}`,
path: `claws.${agentId}.workspace.${file.path}`,
target: `${file.workspace}:${file.path}`,
requirement: "Claw-managed workspace files should remain inspectable with recorded content",
fixHint: "Keep intentional local edits, or inspect the file before removing the Claw."
}));
}
for (const pkg of record.packages) {
if (pkg.extensionCompatibility && pkg.extensionCompatibility.state !== "compatible") findings.push(finding({
message: `Claw extension ${JSON.stringify(pkg.extension?.id ?? pkg.ref)} has ${pkg.extensionCompatibility.state} host compatibility state${pkg.extensionCompatibility.message ? `: ${pkg.extensionCompatibility.message}` : "."}`,
path: `claws.${agentId}.extensions.${pkg.extension?.id ?? pkg.ref}`,
target: `${pkg.source}:${pkg.ref}@${pkg.version}`,
requirement: "Claw extensions should retain their consented canonical capability mapping",
fixHint: "Preview a Claw update before accepting the host's current extension mapping."
}));
if (pkg.state === "present") continue;
findings.push(finding({
message: `Claw ${pkg.kind} ${JSON.stringify(`${pkg.ref}@${pkg.version}`)} has ${pkg.state} lifecycle state.`,
path: `claws.${agentId}.packages.${pkg.kind}.${pkg.ref}`,
target: `${pkg.source}:${pkg.ref}@${pkg.version}`,
requirement: "Claw package references should match canonical installed package state",
fixHint: "Inspect package state with `openclaw claws status` before updating or removing the Claw."
}));
}
for (const server of record.mcpServers) {
if (server.state === "present") continue;
findings.push(finding({
message: `Claw MCP server ${JSON.stringify(server.name)} has ${server.state} ownership state${server.error ? `: ${server.error}` : "."}`,
path: `mcp.servers.${server.name}`,
target: server.name,
requirement: "Claw MCP ownership should be complete and match live canonical config",
fixHint: server.state === "failed" ? "Remove the partial Claw to release its non-owning reference." : "Inspect MCP config drift before removing or replacing Claw-owned state."
}));
}
for (const cron of record.cronJobs) {
if (cron.status !== "complete" || !cron.schedulerJobId) {
findings.push(finding({
message: `Claw cron declaration ${JSON.stringify(cron.manifestId)} has ${cron.status} ownership state${cron.error ? `: ${cron.error}` : "."}`,
path: `claws.${agentId}.cronJobs.${cron.manifestId}`,
target: cron.schedulerJobId ?? cron.declarationKey,
requirement: "Claw cron ownership should resolve to a persisted scheduler job id",
fixHint: "Reconcile the declaration with the gateway before removing the Claw."
}));
continue;
}
if (!cronInventory) {
findings.push(finding({
message: `Claw cron declaration ${JSON.stringify(cron.manifestId)} live Gateway state is unknown; no cron inventory was available.`,
path: `claws.${agentId}.cronJobs.${cron.manifestId}`,
target: cron.schedulerJobId,
requirement: "Claw cron health requires live Gateway corroboration by job id, declaration key, owner, enabled state, and execution digest",
fixHint: "Run diagnostics with Gateway cron inventory available before treating this cron as healthy."
}));
continue;
}
if (!cronInventory.ok) {
findings.push(finding({
message: `Claw cron declaration ${JSON.stringify(cron.manifestId)} live Gateway state is unknown: ${cronInventory.error}`,
path: `claws.${agentId}.cronJobs.${cron.manifestId}`,
target: cron.schedulerJobId,
requirement: "Claw cron health requires live Gateway corroboration by job id, declaration key, owner, enabled state, and execution digest",
fixHint: "Restore Gateway cron inventory before treating this cron as healthy."
}));
continue;
}
const live = cronInventory.jobs.find((job) => job.id === cron.schedulerJobId);
if (!live) {
findings.push(finding({
message: `Claw cron declaration ${JSON.stringify(cron.manifestId)} is missing from live Gateway inventory.`,
path: `claws.${agentId}.cronJobs.${cron.manifestId}`,
target: cron.schedulerJobId,
requirement: "Claw cron health requires live Gateway corroboration by scheduler job id",
fixHint: "Recreate or reconcile the Gateway automation before treating this Claw as healthy."
}));
continue;
}
const ownerAgentId = live.owner?.agentId ?? live.agentId;
const expectedDigest = expectedCronExecutionDigest(record, cron);
const liveDigest = liveCronExecutionDigest(live);
if (live.declarationKey !== cron.declarationKey || ownerAgentId !== agentId || !live.enabled || liveDigest !== expectedDigest) findings.push(finding({
message: `Claw cron declaration ${JSON.stringify(cron.manifestId)} differs from live Gateway job ${JSON.stringify(live.id)}.`,
path: `claws.${agentId}.cronJobs.${cron.manifestId}`,
target: cron.schedulerJobId,
requirement: "Claw cron health requires live Gateway job id, declaration key, owner, enabled state, and execution digest to match",
fixHint: "Inspect Gateway cron drift before updating or removing this Claw."
}));
}
return findings;
}
function tableExists(db, name) {
return Boolean(db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?").get(name));
}
function orphanedAgentIds(options) {
const { db } = openOpenClawStateDatabase(options);
const installed = /* @__PURE__ */ new Set();
if (tableExists(db, "claw_installs")) for (const row of db.prepare("SELECT agent_id FROM claw_installs").all()) installed.add(row.agent_id);
const referenced = /* @__PURE__ */ new Set();
for (const table of [
"claw_workspace_files",
"claw_package_refs",
"claw_mcp_server_refs",
"claw_cron_refs"
]) {
if (!tableExists(db, table)) continue;
for (const row of db.prepare(`SELECT DISTINCT agent_id FROM ${table}`).all()) referenced.add(row.agent_id);
}
return [...referenced].filter((agentId) => !installed.has(agentId)).toSorted();
}
function hasClawMcpServerRefs(db) {
return tableExists(db, "claw_mcp_server_refs") && Boolean(db.prepare("SELECT 1 FROM claw_mcp_server_refs LIMIT 1").get());
}
function orphanedReferenceFinding(agentId) {
return finding({
message: `Claw ownership references for agent ${JSON.stringify(agentId)} have no root install record.`,
path: `claws.${agentId}`,
target: agentId,
requirement: "Claw-owned resources should have a matching claw_installs row",
fixHint: "Inspect the state database and live resources before deleting orphaned references."
});
}
async function collectClawStateHealthFindings(options = {}) {
if (!isExperimentalClawsEnabled(options.env ?? process.env)) return [];
let database;
try {
database = await openExistingOpenClawStateDatabaseReadOnly(options);
if (!database) return [];
const orphanedRefs = orphanedAgentIds({
...options,
database,
readOnly: true
});
if (!tableExists(database.db, "claw_installs")) return orphanedRefs.map(orphanedReferenceFinding);
let sourceMcpServers = options.sourceMcpServers ?? {};
if (hasClawMcpServerRefs(database.db) && !options.sourceMcpServers) {
const listed = await (options.listMcpServers ?? listConfiguredMcpServers)();
if (!listed.ok) throw new Error(listed.error);
sourceMcpServers = listed.mcpServers;
}
const status = await readClawStatus(void 0, {
...options,
database,
readOnly: true,
...options.cfg ? { config: options.cfg } : {},
sourceMcpServers
});
const hasCronRefs = status.records.some((record) => record.cronJobs.length > 0);
let cronInventory;
if (hasCronRefs && options.cronGateway) try {
cronInventory = {
ok: true,
jobs: await options.cronGateway.list({ includeDisabled: true })
};
} catch (error) {
cronInventory = {
ok: false,
error: coerceErrorMessage(error)
};
}
const findings = status.records.flatMap((record) => collectInstallFindings(record, cronInventory));
for (const agentId of orphanedRefs) findings.push(orphanedReferenceFinding(agentId));
return findings;
} catch (error) {
return [finding({
severity: "error",
message: `Could not inspect Claw lifecycle state: ${coerceErrorMessage(error)}`,
requirement: "Claw doctor diagnostics require readable lifecycle state"
})];
} finally {
database?.walMaintenance.close();
}
}
//#endregion
export { collectClawStateHealthFindings };