UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

255 lines (254 loc) 14.3 kB
import { r as normalizeProviderId } from "./provider-id-DMd-TDFp.js"; import { a as loadInstalledPluginIndex } from "./installed-plugin-index-wrDsjyMD.js"; import { l as setPluginInstallRecordMapEntry, n as copyPluginInstallRecordMap } from "./plugin-install-record-map-B44vZyl2.js"; import { a as readPersistedInstalledPluginIndexInstallRecords, d as resolveInstalledPluginIndexStorePath, l as inspectPersistedInstalledPluginIndexInstallRecordsSync, r as loadInstalledPluginIndexInstallRecords } from "./installed-plugin-index-record-reader-SXWwf_BU.js"; import { a as resolveTrustedOfficialClawHubPackageName, i as isTrustedOfficialPluginInstallRecord, o as resolveTrustedSourceLinkedOfficialClawHubInstall } from "./official-external-install-records-DClkub0e.js"; import { t as loadPluginManifestRegistryForInstalledIndex } from "./manifest-registry-installed-DdhpzefH.js"; import { o as readPersistedInstalledPluginIndexSync } from "./installed-plugin-index-store-MVV6aa7C.js"; import { t as ConfigMutationConflictError } from "./mutation-conflict-Be0wSyDG.js"; import { a as writePersistedInstalledPluginIndex } from "./installed-plugin-index-store-write-DRg854w2.js"; import { o as withoutPluginInstallRecords } from "./installed-plugin-index-records-C06Xozeq.js"; import { t as inspectShippedPluginInstallConfigRecords } from "./plugin-install-config-migration-Cruvbiy0.js"; import fs from "node:fs"; import { isDeepStrictEqual } from "node:util"; //#region src/commands/doctor/shared/plugin-registry-migration.ts const DOCTOR_PLUGIN_ID_ALIASES = { openai: ["openai-codex"] }; /** Backfill shipped ClawHub authority only from a catalog-bound legacy install record. */ function migrateOfficialPluginInstallProvenance(records) { const migrated = copyPluginInstallRecordMap(records); for (const [pluginId, record] of Object.entries(records)) { if (record.source !== "clawhub" || record.clawhubUrl !== void 0 || record.clawhubChannel !== void 0 || record.sourcePath !== void 0 || !resolveTrustedSourceLinkedOfficialClawHubInstall({ pluginId, record })) continue; const normalized = { ...record, clawhubUrl: "https://clawhub.ai", clawhubChannel: "official" }; const packageName = resolveTrustedOfficialClawHubPackageName(normalized); if (isTrustedOfficialPluginInstallRecord({ pluginId, packageName, record: normalized })) setPluginInstallRecordMapEntry(migrated, pluginId, normalized); } return migrated; } var InvalidPluginInstallRecordStateError = class extends Error {}; function invalidPersistedInstallRecordMessage(filePath) { return [`Persisted plugin install records are invalid at ${filePath}.`, "Stop the Gateway, back up this database, delete only the config_machine_state row with state_key='plugins.installedIndex' using SQLite tooling, then rerun `openclaw doctor --fix` to rebuild it."].join(" "); } const INVALID_CONFIG_INSTALL_RECORD_MESSAGE = "plugins.installs contains invalid records. Back up openclaw.json, correct or remove the invalid retired plugins.installs record, then rerun `openclaw doctor --fix`."; /** Check the accepted source again inside the config writer's lock. */ function assertShippedPluginInstallConfigImportCurrent(snapshot, imported) { const source = inspectShippedPluginInstallConfigRecords(snapshot.sourceConfig); if (source.status === "missing") return; if (source.status === "invalid") throw new InvalidPluginInstallRecordStateError(INVALID_CONFIG_INSTALL_RECORD_MESSAGE); if (!imported || imported.databasePath !== resolveInstalledPluginIndexStorePath() || !isDeepStrictEqual(imported.source, { path: snapshot.path, hash: snapshot.hash, sourceConfig: snapshot.sourceConfig })) throw new ConfigMutationConflictError("config changed after plugin install migration"); } /** Preserve retired source records before Doctor can restore or rewrite their config. */ async function importShippedPluginInstallConfigForDoctor(snapshot) { const source = inspectShippedPluginInstallConfigRecords(snapshot.sourceConfig); if (source.status === "missing") return; if (source.status === "invalid") throw new InvalidPluginInstallRecordStateError(INVALID_CONFIG_INSTALL_RECORD_MESSAGE); const { readConfigFileSnapshotForWrite, withConfigMutationExclusive } = await import("./config/config.js"); const sourceIdentity = { path: snapshot.path, hash: snapshot.hash, sourceConfig: snapshot.sourceConfig }; const receipt = (databasePath, pluginInventoryChanged) => ({ source: structuredClone(sourceIdentity), databasePath, pluginInventoryChanged }); if (Object.keys(source.records).length === 0) return receipt(resolveInstalledPluginIndexStorePath(), false); const { commitPluginInstallRecordsOnly } = await import("./install-record-commit-DVoT_zjZ.js"); const { withPluginLifecycleLease } = await import("./plugin-lifecycle-lease-CLKma701.js"); return await withPluginLifecycleLease({}, async (lease) => withConfigMutationExclusive(async () => { const prepared = await readConfigFileSnapshotForWrite(); if (prepared.snapshot.path !== snapshot.path || prepared.snapshot.hash !== snapshot.hash || !isDeepStrictEqual(prepared.snapshot.sourceConfig, snapshot.sourceConfig)) throw new ConfigMutationConflictError("config changed before plugin install migration"); const storeOptions = { filePath: lease.databasePath }; const previousInstallRecords = await loadInstalledPluginIndexInstallRecords(storeOptions); const persisted = await readPersistedInstalledPluginIndexInstallRecords(storeOptions); let nextInstallRecords = copyPluginInstallRecordMap(previousInstallRecords); for (const [pluginId, record] of Object.entries(source.records)) if (!persisted || !Object.hasOwn(persisted, pluginId)) setPluginInstallRecordMapEntry(nextInstallRecords, pluginId, record); nextInstallRecords = migrateOfficialPluginInstallProvenance(nextInstallRecords); if (isDeepStrictEqual(nextInstallRecords, persisted)) return receipt(lease.databasePath, false); await commitPluginInstallRecordsOnly({ previousInstallRecords, nextInstallRecords, nextConfig: withoutPluginInstallRecords(snapshot.sourceConfig), verifyConfigFresh: async () => { prepared.writeOptions.assertConfigPathForWrite?.(); const current = await readConfigFileSnapshotForWrite(); if (current.snapshot.path !== prepared.snapshot.path || current.snapshot.hash !== prepared.snapshot.hash || !isDeepStrictEqual(current.writeOptions.includeFileHashesForWrite, prepared.writeOptions.includeFileHashesForWrite) || !isDeepStrictEqual(current.writeOptions.includeFileTargetsForWrite, prepared.writeOptions.includeFileTargetsForWrite)) throw new ConfigMutationConflictError("config changed during plugin install migration"); } }); return receipt(lease.databasePath, true); })); } /** Decide whether Doctor should migrate the plugin registry in this environment. */ function preflightPluginRegistryDoctorMigration(params = {}) { const filePath = resolveInstalledPluginIndexStorePath(params); const persistedState = inspectPersistedInstalledPluginIndexInstallRecordsSync(params); if (persistedState.status === "invalid") throw new InvalidPluginInstallRecordStateError(invalidPersistedInstallRecordMessage(filePath)); const configInstallState = params.config ? inspectShippedPluginInstallConfigRecords(params.config) : void 0; if (configInstallState?.status === "invalid") throw new InvalidPluginInstallRecordStateError(INVALID_CONFIG_INSTALL_RECORD_MESSAGE); if ((params.existsSync ?? fs.existsSync)(filePath)) { const currentRegistry = readPersistedInstalledPluginIndexSync(params); if (currentRegistry) return { action: "skip-existing", filePath, current: currentRegistry }; if (persistedState.status !== "missing") return { action: "migrate", filePath }; } const hasConfigInstallRecords = configInstallState?.status === "valid" && Object.keys(configInstallState.records).length > 0; return { action: params.config && !hasConfigInstallRecords ? "initialize" : "migrate", filePath }; } async function readMigrationConfig(params) { if (params.config) return params.config; if (params.readConfig) return await params.readConfig(); return await (await import("./config/config.js")).readBestEffortConfig(); } function normalizeRegistryReference(value) { if (typeof value !== "string") return; const trimmed = value.trim(); return trimmed ? trimmed.toLowerCase() : void 0; } function createMigrationPluginIdNormalizer(index, manifests) { const aliases = /* @__PURE__ */ new Map(); for (const plugin of index.plugins) { const pluginId = normalizeRegistryReference(plugin.pluginId); if (!pluginId) continue; aliases.set(pluginId, plugin.pluginId); } for (const plugin of manifests) { const pluginId = normalizeRegistryReference(plugin.id); if (!pluginId) continue; aliases.set(pluginId, plugin.id); for (const alias of [ ...plugin.providers, ...plugin.channels, ...plugin.setup?.providers?.map((provider) => provider.id) ?? [], ...plugin.cliBackends, ...plugin.setup?.cliBackends ?? [], ...Object.keys(plugin.modelCatalog?.providers ?? {}), ...plugin.legacyPluginIds ?? [], ...DOCTOR_PLUGIN_ID_ALIASES[plugin.id] ?? [] ]) { const normalizedAlias = normalizeRegistryReference(alias); if (normalizedAlias && !aliases.has(normalizedAlias)) aliases.set(normalizedAlias, plugin.id); } } return (pluginId) => { const normalized = normalizeRegistryReference(pluginId); return normalized ? aliases.get(normalized) ?? pluginId.trim() : pluginId.trim(); }; } function addPluginReference(references, normalizePluginId, value) { if (typeof value !== "string") return; const normalized = normalizePluginId(value); if (normalized) references.add(normalized); } function listConfiguredChannelIds(config) { const channels = config.channels; if (!channels || typeof channels !== "object" || Array.isArray(channels)) return /* @__PURE__ */ new Set(); return new Set(Object.keys(channels).map((channelId) => normalizeRegistryReference(channelId)).filter((channelId) => Boolean(channelId))); } function listConfiguredModelProviderIds(config) { const providers = config.models?.providers; if (!providers || typeof providers !== "object" || Array.isArray(providers)) return /* @__PURE__ */ new Set(); return new Set(Object.keys(providers).map((providerId) => normalizeProviderId(providerId)).filter(Boolean)); } function listMigrationRelevantPluginRecords(params) { const manifestRegistry = loadPluginManifestRegistryForInstalledIndex({ index: params.index, config: params.config, workspaceDir: params.workspaceDir, env: params.env, includeDisabled: true }); const manifestByPluginId = new Map(manifestRegistry.plugins.map((plugin) => [plugin.id, plugin])); const normalizePluginId = createMigrationPluginIdNormalizer(params.index, manifestRegistry.plugins); const referencedPluginIds = /* @__PURE__ */ new Set(); const installedPluginIds = /* @__PURE__ */ new Set(); for (const pluginId of Object.keys(params.installRecords)) addPluginReference(installedPluginIds, normalizePluginId, pluginId); const plugins = params.config.plugins; for (const pluginId of plugins?.allow ?? []) addPluginReference(referencedPluginIds, normalizePluginId, pluginId); for (const pluginId of plugins?.deny ?? []) addPluginReference(referencedPluginIds, normalizePluginId, pluginId); for (const pluginId of Object.keys(plugins?.entries ?? {})) addPluginReference(referencedPluginIds, normalizePluginId, pluginId); for (const pluginId of Object.values(plugins?.slots ?? {})) { if (normalizeRegistryReference(pluginId) === "none") continue; addPluginReference(referencedPluginIds, normalizePluginId, pluginId); } const configuredChannelIds = listConfiguredChannelIds(params.config); const configuredModelProviderIds = listConfiguredModelProviderIds(params.config); return params.index.plugins.filter((plugin) => { if (plugin.origin !== "bundled") return true; const manifest = manifestByPluginId.get(plugin.pluginId); if (plugin.enabledByDefault && (manifest?.providers.length ?? 0) > 0) return true; if (plugin.startup.memory) return true; if ((manifest?.commandAliases ?? []).some((alias) => alias.cliCommand)) return true; if ((manifest?.contracts?.migrationProviders?.length ?? 0) > 0) return true; if (installedPluginIds.has(plugin.pluginId) || referencedPluginIds.has(plugin.pluginId)) return true; if ((manifest?.channels ?? []).some((channelId) => configuredChannelIds.has(normalizeRegistryReference(channelId) ?? ""))) return true; return (manifest?.providers ?? []).some((providerId) => configuredModelProviderIds.has(normalizeProviderId(providerId))); }); } /** Rebuild Doctor's plugin registry from canonical install records when needed. */ async function migratePluginRegistryForDoctor(params = {}) { const preflight = preflightPluginRegistryDoctorMigration(params); if (preflight.action === "skip-existing") return { status: "skip-existing", migrated: false, preflight }; if (params.dryRun) return { status: "dry-run", migrated: false, preflight }; const rawConfig = await readMigrationConfig(params); if (inspectShippedPluginInstallConfigRecords(rawConfig).status === "invalid") throw new InvalidPluginInstallRecordStateError(INVALID_CONFIG_INSTALL_RECORD_MESSAGE); const config = withoutPluginInstallRecords(rawConfig); const installRecords = migrateOfficialPluginInstallProvenance(params.installRecords ?? await loadInstalledPluginIndexInstallRecords(params)); const migrationParams = { ...params, config, installRecords }; const candidateIndex = loadInstalledPluginIndex({ ...migrationParams }); const current = { ...candidateIndex, refreshReason: "migration", plugins: listMigrationRelevantPluginRecords({ index: candidateIndex, config, installRecords, workspaceDir: params.workspaceDir, env: params.env }) }; await writePersistedInstalledPluginIndex(current, params); return { status: "migrated", migrated: true, preflight, current }; } //#endregion export { migratePluginRegistryForDoctor as a, migrateOfficialPluginInstallProvenance as i, assertShippedPluginInstallConfigImportCurrent as n, preflightPluginRegistryDoctorMigration as o, importShippedPluginInstallConfigForDoctor as r, InvalidPluginInstallRecordStateError as t };