UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

294 lines (293 loc) 15.8 kB
import { t as sanitizeForLog } from "./ansi-DrXAcdMD.js"; import { r as asNullableRecord } from "./record-coerce-DItp3I4t.js"; import { E as tryResolveDefaultAgentId, m as resolveAgentWorkspaceDir } from "./agent-scope-config-DcbEhP0R.js"; import { t as CHANNEL_IDS } from "./ids-BVZRYG0I.js"; import { n as defaultSlotIdForKey } from "./slots-CQdAEuat.js"; import { a as isRetiredPluginId, i as isExplicitPluginDisableMarker, s as normalizePluginId } from "./config-state-BkU1frVq.js"; import { p as listOfficialExternalPluginCatalogEntries, w as resolveOfficialExternalPluginLookupIds } from "./official-external-plugin-catalog-Dzu7dwBN.js"; import { i as loadInstalledPluginIndexInstallRecordsSync } from "./installed-plugin-index-record-reader-SXWwf_BU.js"; import "./agent-scope-DbtJyKUL.js"; import { c as loadManifestMetadataSnapshot } from "./manifest-contract-eligibility-BbV7X6pV.js"; import { t as listMutableCodexRouteAgentEntries } from "./codex-route-agent-entries-BI7NtrYf.js"; import "./installed-plugin-index-records-C06Xozeq.js"; //#region src/commands/doctor/shared/stale-plugin-repair-preservation.ts function normalizeIds(ids) { return new Set([...ids ?? []].map((id) => normalizePluginId(id)).filter((id) => Boolean(id))); } function filterRepairableStalePluginHits(params) { const preserveIds = normalizeIds(params.preservePluginIds); const surfacePreserveIds = Object.fromEntries(Object.entries(params.surfacePreservePluginIds ?? {}).map(([surface, ids]) => [surface, normalizeIds(ids)])); return params.hits.filter((hit) => { const id = normalizePluginId(hit.pluginId); return !preserveIds.has(id) && !surfacePreserveIds[hit.surface]?.has(id); }); } //#endregion //#region src/commands/doctor/shared/stale-plugin-config.ts const CHANNEL_CONFIG_META_KEYS = /* @__PURE__ */ new Set(["defaults", "modelByChannel"]); function collectPluginRegistryState(cfg, env) { const environment = env ?? process.env; const defaultAgentId = tryResolveDefaultAgentId(cfg); const workspaceDir = defaultAgentId ? resolveAgentWorkspaceDir(cfg, defaultAgentId) : void 0; const registry = loadManifestMetadataSnapshot({ config: cfg, workspaceDir: workspaceDir ?? void 0, env: environment }).manifestRegistry; const knownIds = new Set(registry.plugins.map((plugin) => plugin.id)); const officialLookupIds = new Set(listOfficialExternalPluginCatalogEntries().flatMap((entry) => resolveOfficialExternalPluginLookupIds(entry).map(normalizePluginId)).filter(Boolean)); const installedIds = /* @__PURE__ */ new Set(); for (const pluginId of Object.keys(cfg.plugins?.installs ?? {})) { const normalized = normalizePluginId(pluginId); if (normalized) installedIds.add(normalized); } try { for (const pluginId of Object.keys(loadInstalledPluginIndexInstallRecordsSync({ env: environment }))) { const normalized = normalizePluginId(pluginId); if (normalized) installedIds.add(normalized); } } catch {} const knownChannelIds = new Set(CHANNEL_IDS.map((channelId) => normalizePluginId(channelId))); for (const plugin of registry.plugins) for (const channelId of plugin.channels) { const normalized = normalizePluginId(channelId); if (normalized) knownChannelIds.add(normalized); } return { knownIds, officialLookupIds, knownChannelIds, missingInstalledIds: new Set([...installedIds].filter((pluginId) => !knownIds.has(pluginId))), hasDiscoveryErrors: registry.diagnostics.some((diag) => diag.level === "error") }; } /** Return true when plugin discovery errors should pause stale-plugin auto-removal. */ function isStalePluginAutoRepairBlocked(cfg, env) { if (cfg.plugins?.enabled === false) return false; return collectPluginRegistryState(cfg, env).hasDiscoveryErrors; } /** Scan plugin/channel config surfaces for ids no longer present in manifests or installs. */ function scanStalePluginConfig(cfg, env) { if (cfg.plugins?.enabled === false) return []; return scanStalePluginConfigWithState(cfg, collectPluginRegistryState(cfg, env ?? process.env)); } function scanStalePluginConfigWithState(cfg, registryState) { const plugins = asNullableRecord(cfg.plugins); const { knownIds, officialLookupIds } = registryState; const hits = []; const staleEvidenceIds = new Set(registryState.missingInstalledIds); for (const surface of ["allow", "deny"]) { const list = Array.isArray(plugins?.[surface]) ? plugins[surface] : []; for (const rawPluginId of list) { if (typeof rawPluginId !== "string") continue; const pluginId = normalizePluginId(rawPluginId); if (!pluginId || knownIds.has(pluginId) || officialLookupIds.has(pluginId) || registryState.knownChannelIds.has(pluginId)) continue; hits.push({ pluginId: rawPluginId, pathLabel: `plugins.${surface}`, surface }); staleEvidenceIds.add(pluginId); } } const entries = asNullableRecord(plugins?.entries); if (entries) for (const [rawPluginId, entry] of Object.entries(entries)) { const pluginId = normalizePluginId(rawPluginId); if (!pluginId || isExplicitPluginDisableMarker(entry) && !isRetiredPluginId(pluginId) || knownIds.has(pluginId) || officialLookupIds.has(pluginId) || registryState.knownChannelIds.has(pluginId)) continue; hits.push({ pluginId: rawPluginId, pathLabel: `plugins.entries.${rawPluginId}`, surface: "entries" }); staleEvidenceIds.add(pluginId); } const slots = asNullableRecord(plugins?.slots); if (slots) for (const slotKey of ["memory", "contextEngine"]) { const rawPluginId = slots[slotKey]; if (typeof rawPluginId !== "string") continue; const pluginId = normalizePluginId(rawPluginId); const defaultSlotId = defaultSlotIdForKey(slotKey); if (!pluginId || rawPluginId.trim().toLowerCase() === "none" || pluginId === normalizePluginId(defaultSlotId) || knownIds.has(pluginId)) continue; hits.push({ pluginId: rawPluginId, pathLabel: `plugins.slots.${slotKey}`, surface: "slot", slotKey }); } const staleChannelIds = collectDanglingChannelIds({ cfg, registryState, staleEvidenceIds }); for (const channelId of staleChannelIds) hits.push({ pluginId: channelId, pathLabel: `channels.${channelId}`, surface: "channel" }); for (const hit of collectDependentChannelConfigHits(cfg, staleChannelIds)) hits.push(hit); return hits; } function collectDanglingChannelIds(params) { const channels = asNullableRecord(params.cfg.channels); if (!channels) return []; const ids = []; const seen = /* @__PURE__ */ new Set(); for (const channelId of Object.keys(channels)) { if (CHANNEL_CONFIG_META_KEYS.has(channelId)) continue; const normalized = normalizePluginId(channelId); if (!normalized || params.registryState.knownChannelIds.has(normalized) || !params.staleEvidenceIds.has(normalized) || seen.has(normalized)) continue; seen.add(normalized); ids.push(channelId); } return ids; } function collectDependentChannelConfigHits(cfg, channelIds) { if (channelIds.length === 0) return []; const staleChannelIds = new Set(channelIds.map((channelId) => normalizePluginId(channelId))); const hits = []; const defaultTarget = cfg.agents?.defaults?.heartbeat?.target; if (typeof defaultTarget === "string" && staleChannelIds.has(normalizePluginId(defaultTarget))) hits.push({ pluginId: defaultTarget, pathLabel: "agents.defaults.heartbeat.target", surface: "heartbeat" }); for (const { agent, path } of listMutableCodexRouteAgentEntries(cfg)) { const target = asNullableRecord(agent.heartbeat)?.target; if (typeof target !== "string" || !staleChannelIds.has(normalizePluginId(target))) continue; hits.push({ pluginId: target, pathLabel: `${path}.heartbeat.target`, surface: "heartbeat" }); } const modelByChannel = asNullableRecord(cfg.channels?.modelByChannel); if (modelByChannel) for (const [providerId, channelMap] of Object.entries(modelByChannel)) { const channels = asNullableRecord(channelMap); if (!channels) continue; for (const channelId of Object.keys(channels)) { if (!staleChannelIds.has(normalizePluginId(channelId))) continue; hits.push({ pluginId: channelId, pathLabel: `channels.modelByChannel.${providerId}.${channelId}`, surface: "modelByChannel" }); } } return hits; } const isPolicySurfaceHit = (hit) => hit.surface === "allow" || hit.surface === "deny" || hit.surface === "entries"; function formatStalePluginHitWarning(hit) { if (isPolicySurfaceHit(hit)) return null; if (hit.surface === "slot") return `- ${hit.pathLabel}: slot references missing plugin "${hit.pluginId}".`; if (hit.surface === "channel") return `- ${hit.pathLabel}: dangling channel config for missing plugin "${hit.pluginId}" was found.`; if (hit.surface === "heartbeat") return `- ${hit.pathLabel}: heartbeat target references missing channel plugin "${hit.pluginId}".`; return `- ${hit.pathLabel}: model override references missing channel plugin "${hit.pluginId}".`; } /** Format warnings for stale plugin config hits. */ function collectStalePluginConfigWarnings(params) { const hits = filterRepairableStalePluginHits(params); if (hits.length === 0) return []; const policyPluginIds = [...new Set(hits.filter(isPolicySurfaceHit).map((hit) => hit.pluginId))].toSorted((a, b) => a.localeCompare(b)); const lines = hits.map((hit) => formatStalePluginHitWarning(hit)).filter((line) => line !== null); if (policyPluginIds.length > 0) lines.unshift(`- Stale plugin references (plugins.allow/deny/entries): ${policyPluginIds.join(", ")}.`); if (params.autoRepairBlocked) lines.push(`- Auto-removal is paused because plugin discovery currently has errors. Fix plugin discovery first, then rerun "${params.doctorFixCommand}".`); else lines.push(`- Run "${params.doctorFixCommand}" to remove stale plugin ids and dangling channel references.`); return lines.map((line) => sanitizeForLog(line)); } /** Remove stale plugin ids and dangling channel references when discovery is healthy. */ function maybeRepairStalePluginConfig(cfg, env, params) { if (cfg.plugins?.enabled === false) return { config: cfg, changes: [] }; const registryState = collectPluginRegistryState(cfg, env ?? process.env); if (registryState.hasDiscoveryErrors) return { config: cfg, changes: [] }; const hits = filterRepairableStalePluginHits({ hits: scanStalePluginConfigWithState(cfg, registryState), preservePluginIds: params?.preservePluginIds, surfacePreservePluginIds: params?.surfacePreservePluginIds }); if (hits.length === 0) return { config: cfg, changes: [] }; const next = structuredClone(cfg); const nextPlugins = asNullableRecord(next.plugins); const allowIds = hits.filter((hit) => hit.surface === "allow").map((hit) => hit.pluginId); if (allowIds.length > 0 && Array.isArray(nextPlugins?.allow)) { const staleAllowIds = new Set(allowIds.map((pluginId) => normalizePluginId(pluginId))); nextPlugins.allow = nextPlugins.allow.filter((pluginId) => typeof pluginId !== "string" || !staleAllowIds.has(normalizePluginId(pluginId))); } const denyIds = hits.filter((hit) => hit.surface === "deny").map((hit) => hit.pluginId); if (denyIds.length > 0 && Array.isArray(nextPlugins?.deny)) { const staleDenyIds = new Set(denyIds.map((pluginId) => normalizePluginId(pluginId))); nextPlugins.deny = nextPlugins.deny.filter((pluginId) => typeof pluginId !== "string" || !staleDenyIds.has(normalizePluginId(pluginId))); } const entryIds = hits.filter((hit) => hit.surface === "entries").map((hit) => hit.pluginId); if (entryIds.length > 0) { const entries = asNullableRecord(nextPlugins?.entries); if (entries) { const staleEntryIds = new Set(entryIds.map((pluginId) => normalizePluginId(pluginId))); for (const pluginId of Object.keys(entries)) if (staleEntryIds.has(normalizePluginId(pluginId))) delete entries[pluginId]; } } const slotHits = hits.filter((hit) => hit.surface === "slot" && hit.slotKey !== void 0); if (slotHits.length > 0) { const slots = asNullableRecord(nextPlugins?.slots); if (slots) { for (const hit of slotHits) delete slots[hit.slotKey]; if (Object.keys(slots).length === 0 && nextPlugins) delete nextPlugins.slots; } } const channelIds = hits.filter((hit) => hit.surface === "channel").map((hit) => hit.pluginId); if (channelIds.length > 0) removeDanglingChannelReferences(next, channelIds); const changes = []; if (allowIds.length > 0) changes.push(`- plugins.allow: removed ${allowIds.length} stale plugin id${allowIds.length === 1 ? "" : "s"} (${allowIds.join(", ")})`); if (denyIds.length > 0) changes.push(`- plugins.deny: removed ${denyIds.length} stale plugin id${denyIds.length === 1 ? "" : "s"} (${denyIds.join(", ")})`); if (entryIds.length > 0) changes.push(`- plugins.entries: removed ${entryIds.length} stale plugin entr${entryIds.length === 1 ? "y" : "ies"} (${entryIds.join(", ")})`); if (slotHits.length > 0) changes.push(`- plugins.slots: reset ${slotHits.length} stale plugin slot${slotHits.length === 1 ? "" : "s"} (${slotHits.map((hit) => `${hit.slotKey}: ${hit.pluginId} -> ${defaultSlotIdForKey(hit.slotKey)}`).join(", ")})`); if (channelIds.length > 0) { changes.push(`- channels: removed ${channelIds.length} stale channel config${channelIds.length === 1 ? "" : "s"} (${channelIds.join(", ")})`); const heartbeatCount = hits.filter((hit) => hit.surface === "heartbeat").length; const heartbeatIds = [...new Set(hits.filter((hit) => hit.surface === "heartbeat").map((hit) => hit.pluginId))]; if (heartbeatCount > 0) changes.push(`- agents heartbeat: removed ${heartbeatCount} stale heartbeat target${heartbeatCount === 1 ? "" : "s"} (${heartbeatIds.join(", ")})`); const modelByChannelCount = hits.filter((hit) => hit.surface === "modelByChannel").length; const modelByChannelIds = [...new Set(hits.filter((hit) => hit.surface === "modelByChannel").map((hit) => hit.pluginId))]; if (modelByChannelCount > 0) changes.push(`- channels.modelByChannel: removed ${modelByChannelCount} stale channel model override${modelByChannelCount === 1 ? "" : "s"} (${modelByChannelIds.join(", ")})`); } return { config: next, changes }; } function removeDanglingChannelReferences(config, channelIds) { const staleChannelIds = new Set(channelIds.map((channelId) => normalizePluginId(channelId))); const channels = asNullableRecord(config.channels); if (channels) { for (const channelId of Object.keys(channels)) { if (CHANNEL_CONFIG_META_KEYS.has(channelId)) continue; if (staleChannelIds.has(normalizePluginId(channelId))) delete channels[channelId]; } const modelByChannel = asNullableRecord(channels.modelByChannel); if (modelByChannel) { for (const [providerId, channelMap] of Object.entries(modelByChannel)) { const channelsForProvider = asNullableRecord(channelMap); if (!channelsForProvider) continue; for (const channelId of Object.keys(channelsForProvider)) if (staleChannelIds.has(normalizePluginId(channelId))) delete channelsForProvider[channelId]; if (Object.keys(channelsForProvider).length === 0) delete modelByChannel[providerId]; } if (Object.keys(modelByChannel).length === 0) delete channels.modelByChannel; } } const defaultsHeartbeat = config.agents?.defaults?.heartbeat; if (defaultsHeartbeat && typeof defaultsHeartbeat.target === "string" && staleChannelIds.has(normalizePluginId(defaultsHeartbeat.target))) delete defaultsHeartbeat.target; for (const { agent } of listMutableCodexRouteAgentEntries(config)) { const heartbeat = asNullableRecord(agent.heartbeat); if (heartbeat && typeof heartbeat.target === "string" && staleChannelIds.has(normalizePluginId(heartbeat.target))) delete heartbeat.target; } } //#endregion export { scanStalePluginConfig as i, isStalePluginAutoRepairBlocked as n, maybeRepairStalePluginConfig as r, collectStalePluginConfigWarnings as t };