UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

849 lines (848 loc) • 38.6 kB
import { l as normalizeOptionalString } from "./string-coerce-1VuiLaaR.mjs"; import { o as resolveUserPath } from "./home-dir-Dc6nN6HB.mjs"; import { _ as normalizeUniqueTrimmedStringList, s as normalizeOptionalTrimmedStringList } from "./string-normalization-CECOqf64.mjs"; import { n as sanitizeForLog } from "./ansi-BeFJ0Uu7.mjs"; import { t as isBlockedObjectKey } from "./prototype-keys-CuYw53fZ.mjs"; import "./utils-DgZn2dLu.mjs"; import { o as resolveCompatibilityHostVersion } from "./version-BJGPJZu6.mjs"; import { n as isPathInside } from "./path-safety-6KJ2Rbnw.mjs"; import { c as pluginCacheLstatSync, d as readPluginCacheDirectory, f as readPluginCacheFile, l as pluginCacheRealpathSync, s as pluginCacheExistsSync, u as pluginCacheStatSync } from "./package-manifest-CEbdS_C4.mjs"; import { r as isForeignBundledPluginRoot } from "./bundled-dir-DkyLPnyV.mjs"; import { b as normalizePluginsConfigWithResolverCore, v as resolvePluginActivationStateShared } from "./config-state-DY_WJYue.mjs"; import { t as normalizePluginPolicyId } from "./plugin-policy-id-1GfPFu0U.mjs"; import { D as resolvePluginCandidateInstallOwner, T as isPluginCandidateInstallOwnerAmbiguous, a as groupPluginRecords, n as discoverOpenClawPlugins } from "./discovery-Dma3bVNk.mjs"; import { a as normalizeManifestChannelCommandDefaults, d as normalizeManifestCatalog, n as isCoreReservedPluginId, r as loadPluginManifest } from "./manifest-BXjFCp0F.mjs"; import { t as PLUGIN_MANIFEST_CONTRACT_KEYS } from "./manifest-contract-keys-WtA6uWDt.mjs"; import { C as isThemeId, g as normalizeThemeDefinition, n as MAX_THEME_DEFINITION_BYTES } from "./theme-_hQKgfH-.mjs"; import { o as loadBundleManifest } from "./bundle-manifest-BiKYAnae.mjs"; import { n as satisfiesPluginApiRange, t as resolvePackagePluginApiRange } from "./package-compat-_DIjXzI0.mjs"; import { t as shouldRejectHardlinkedPluginFiles } from "./hardlink-policy-C9pW4xqI.mjs"; import { n as isBundledPluginInsideDevSourceRoot } from "./dev-source-root-T6MQRtRo.mjs"; import { n as resolvePluginTrust, t as matchesInstalledPluginRecord } from "./installed-plugin-record-match-CVFkDlud.mjs"; import { i as getOfficialExternalPluginCatalogEntryForPackage } from "./official-external-plugin-catalog-Dcv926GC.mjs"; import { r as resolveInstalledPluginIndexStorePath } from "./installed-plugin-index-store-path-vGDuKHrq.mjs"; import { o as getOfficialExternalPluginCatalogManifest } from "./official-external-plugin-catalog-source-B7ojvzQW.mjs"; import { t as isSelfContainedSvg } from "./svg-image-1iOdCRUI.mjs"; import { a as isPluginActivityToolName, i as PORTABLE_PLUGIN_ICON_PATH, n as PLUGIN_ACTIVITY_ICON_PATH, r as PLUGIN_TOOL_ACTIVITY_ICON_DIR, t as PLUGIN_ACTIVITY_ICON_MAX_BYTES } from "./portable-icon-paths-BZ0z5wfc.mjs"; import { t as checkMinHostVersion } from "./min-host-version-DhjoqTWe.mjs"; import path from "node:path"; //#region src/plugins/manifest-install-owner.ts const PLUGIN_MANIFEST_INSTALL_OWNER = Symbol.for("openclaw.pluginManifestInstallOwner"); function recordPluginManifestInstallOwner(record, installOwner, ambiguous = false) { if (!installOwner && !ambiguous) return record; Object.defineProperty(record, PLUGIN_MANIFEST_INSTALL_OWNER, { configurable: false, enumerable: true, value: ambiguous ? { ambiguous: true } : { installOwner } }); return record; } function readPluginManifestInstallOwner(record) { return record[PLUGIN_MANIFEST_INSTALL_OWNER]; } function resolvePluginManifestInstallOwner(record) { return readPluginManifestInstallOwner(record)?.installOwner; } function isPluginManifestInstallOwnerAmbiguous(record) { return readPluginManifestInstallOwner(record)?.ambiguous === true; } //#endregion //#region src/plugins/config-policy.ts function resolvePolicyPluginActivationState(params) { return resolvePluginActivationStateShared({ ...params, activationSource: { plugins: params.sourceConfig ?? params.config, rootConfig: params.sourceRootConfig ?? params.rootConfig } }); } //#endregion //#region src/plugins/manifest-theme-definitions.ts /** Capture palettes and artwork so a published generation never reads changed files. */ function loadManifestThemeDefinitions(params) { if (!params.themes?.length) return; return params.themes.flatMap((theme) => { try { if (params.pluginId === "user" || !isThemeId(`${params.pluginId}/${theme.id}`)) throw new Error("qualified theme ID must be portable, outside user/, and at most 256 characters"); const file = readPluginCacheFile({ rootDir: params.rootDir, relativePath: theme.source, rejectHardlinks: params.rejectHardlinks, maxBytes: MAX_THEME_DEFINITION_BYTES * 4 }); if (!file.ok) throw new Error("source must be a readable JSON file inside the plugin root"); const definition = normalizeThemeDefinition(JSON.parse(file.contents.toString("utf8")), { hatIds: Object.keys(theme.hats ?? {}), critterIds: Object.keys(theme.critters ?? {}) }); if (definition.name !== theme.name || definition.description !== theme.description) throw new Error("name and description must match the manifest declaration"); const readSvg = (source) => { const svgFile = readPluginCacheFile({ rootDir: params.rootDir, relativePath: source, rejectHardlinks: params.rejectHardlinks, maxBytes: PLUGIN_ACTIVITY_ICON_MAX_BYTES }); if (!svgFile.ok) throw new Error(`artwork ${source} must be a readable SVG inside the plugin root, at most ${PLUGIN_ACTIVITY_ICON_MAX_BYTES} bytes`); const svg = svgFile.contents.toString("utf8"); if (!isSelfContainedSvg(svg)) throw new Error(`artwork ${source} must be a self-contained SVG`); return svg; }; const artwork = { ...theme.hats ? { hats: Object.fromEntries(Object.entries(theme.hats).map(([id, source]) => [id, { svg: readSvg(source) }])) } : {}, ...theme.critters ? { critters: Object.fromEntries(Object.entries(theme.critters).map(([id, { source, ...metadata }]) => [id, { svg: readSvg(source), ...metadata }])) } : {} }; return [{ id: theme.id, definition, ...theme.hats || theme.critters ? { artwork } : {} }]; } catch (error) { params.diagnostics.push({ level: "warn", pluginId: params.pluginId, message: `theme ${theme.id} is unavailable: ${error instanceof Error ? error.message : "invalid definition"}` }); return []; } }); } //#endregion //#region src/plugins/manifest-record.ts function resolvePluginSourcePath(sourcePath) { if (pluginCacheExistsSync(sourcePath)) return sourcePath; if (sourcePath.endsWith(".ts")) { const jsPath = sourcePath.slice(0, -3) + ".js"; if (pluginCacheExistsSync(jsPath)) return jsPath; } return sourcePath; } function isPluginRootPath(params) { const resolvedTargetPath = path.resolve(params.targetPath); const resolvedRootPath = path.resolve(params.rootPath); if (!isPathInside(resolvedRootPath, resolvedTargetPath)) return false; const targetRealPath = pluginCacheRealpathSync(resolvedTargetPath); if (!targetRealPath) return params.targetMustExist !== true; if (!isPathInside(params.rootRealPath, targetRealPath)) return false; if (params.rejectHardlinks === true) { const targetStat = pluginCacheStatSync(resolvedTargetPath); if (!targetStat || targetStat.nlink > 1) return false; } return true; } function resolveManifestPluginSourcePath(params) { const pushDiagnostic = () => { params.diagnostics.push({ level: "warn", pluginId: sanitizeForLog(params.pluginId), source: sanitizeForLog(params.manifestPath), message: `plugin manifest ${params.entryName} must resolve inside the plugin root; ignoring entry` }); }; if (!params.entry || path.isAbsolute(params.entry)) { pushDiagnostic(); return; } const rootPath = path.resolve(params.rootDir); const rootRealPath = pluginCacheRealpathSync(rootPath) ?? rootPath; const sourcePath = path.resolve(rootPath, params.entry); if (!isPluginRootPath({ rootPath, targetPath: sourcePath, rootRealPath, rejectHardlinks: params.rejectHardlinks, targetMustExist: pluginCacheExistsSync(sourcePath) })) { pushDiagnostic(); return; } const resolvedSourcePath = resolvePluginSourcePath(sourcePath); if (!isPluginRootPath({ rootPath, targetPath: resolvedSourcePath, rootRealPath, rejectHardlinks: params.rejectHardlinks, targetMustExist: pluginCacheExistsSync(resolvedSourcePath) })) { pushDiagnostic(); return; } return resolvedSourcePath; } function resolvePortablePluginIcons(params) { let root; const resolveIcon = (relativePath) => { const iconPath = path.resolve(params.rootDir, relativePath); const iconStat = pluginCacheLstatSync(iconPath); if (!iconStat?.isFile() || params.rejectHardlinks && iconStat.nlink > 1) return; if (!root) { const rootPath = path.resolve(params.rootDir); root = { path: rootPath, realPath: pluginCacheRealpathSync(rootPath) ?? rootPath }; } return isPluginRootPath({ rootPath: root.path, targetPath: iconPath, rootRealPath: root.realPath, rejectHardlinks: params.rejectHardlinks, targetMustExist: true }) ? iconPath : void 0; }; const iconPath = resolveIcon(PORTABLE_PLUGIN_ICON_PATH); const activityIconPath = resolveIcon(PLUGIN_ACTIVITY_ICON_PATH); const directory = path.resolve(params.rootDir, PLUGIN_TOOL_ACTIVITY_ICON_DIR); if (!isPluginRootPath({ rootPath: params.rootDir, rootRealPath: params.rootDir === root?.path ? root.realPath : pluginCacheRealpathSync(params.rootDir) ?? params.rootDir, targetPath: directory, targetMustExist: true })) return { iconPath, activityIconPath }; let entries; try { entries = readPluginCacheDirectory(directory); } catch { return { iconPath, activityIconPath }; } if (entries.length > 128) return { iconPath, activityIconPath }; const paths = []; for (const name of entries.map((entry) => entry.name).toSorted()) { const toolName = name.endsWith(".svg") ? name.slice(0, -4) : ""; if (!isPluginActivityToolName(toolName)) continue; const toolIconPath = resolveIcon(`${PLUGIN_TOOL_ACTIVITY_ICON_DIR}/${name}`); if (toolIconPath) paths.push([toolName, toolIconPath]); } return { iconPath, activityIconPath, ...paths.length ? { toolActivityIconPaths: Object.fromEntries(paths) } : {} }; } function mergePackageChannelMetaIntoChannelConfigs(params) { const channelId = params.packageChannel?.id?.trim(); if (!channelId || isBlockedObjectKey(channelId) || !params.channelConfigs || !Object.hasOwn(params.channelConfigs, channelId)) return params.channelConfigs; const existing = params.channelConfigs[channelId]; if (!existing) return params.channelConfigs; const label = existing.label ?? normalizeOptionalString(params.packageChannel?.label) ?? ""; const description = existing.description ?? normalizeOptionalString(params.packageChannel?.blurb) ?? ""; const preferOver = existing.preferOver ?? normalizeOptionalTrimmedStringList(params.packageChannel?.preferOver); const commands = existing.commands ?? normalizeManifestChannelCommandDefaults(params.packageChannel?.commands); const merged = Object.create(null); for (const [key, value] of Object.entries(params.channelConfigs)) if (!isBlockedObjectKey(key)) merged[key] = value; merged[channelId] = { ...existing, ...label ? { label } : {}, ...description ? { description } : {}, ...preferOver?.length ? { preferOver } : {}, ...commands ? { commands } : {} }; return merged; } function mergeManifestContracts(manifestContracts, catalogContracts) { if (!catalogContracts) return manifestContracts; const contracts = {}; for (const key of PLUGIN_MANIFEST_CONTRACT_KEYS) { const merged = normalizeUniqueTrimmedStringList([...manifestContracts?.[key] ?? [], ...catalogContracts[key] ?? []]); if (merged.length > 0) contracts[key] = merged; } return Object.keys(contracts).length > 0 ? contracts : void 0; } function mergeCatalogChannelConfigs(params) { if (!params.catalogChannelConfigs) return params.manifestChannelConfigs; const merged = Object.create(null); for (const [key, value] of Object.entries(params.catalogChannelConfigs)) if (!isBlockedObjectKey(key)) merged[key] = value; for (const [key, value] of Object.entries(params.manifestChannelConfigs ?? {})) if (!isBlockedObjectKey(key)) { const catalogValue = merged[key]; merged[key] = catalogValue ? { ...catalogValue, ...value, schema: value.schema ?? catalogValue.schema, ...catalogValue.uiHints || value.uiHints ? { uiHints: { ...catalogValue.uiHints, ...value.uiHints } } : {}, ...value.runtime ?? catalogValue.runtime ? { runtime: value.runtime ?? catalogValue.runtime } : {}, ...value.label ?? catalogValue.label ? { label: value.label ?? catalogValue.label } : {}, ...value.description ?? catalogValue.description ? { description: value.description ?? catalogValue.description } : {}, ...value.preferOver ?? catalogValue.preferOver ? { preferOver: value.preferOver ?? catalogValue.preferOver } : {}, ...value.commands ?? catalogValue.commands ? { commands: value.commands ?? catalogValue.commands } : {} } : value; } return Object.keys(merged).length > 0 ? merged : void 0; } function buildPluginManifestRecord(params) { const pluginId = params.candidate.effectivePluginId ?? params.manifest.id; const resolveCatalogEntry = (entryName) => { const entry = params.manifest[entryName]; return entry === void 0 ? void 0 : resolveManifestPluginSourcePath({ rootDir: params.candidate.rootDir, manifestPath: params.manifestPath, pluginId, entryName, entry, rejectHardlinks: params.rejectHardlinks, diagnostics: params.diagnostics }); }; const manifestChannelConfigs = params.candidate.origin === "bundled" && params.bundledChannelConfigCollector ? params.bundledChannelConfigCollector({ pluginDir: params.candidate.packageDir ?? params.candidate.rootDir, manifest: params.manifest, packageManifest: params.candidate.packageManifest }) : params.manifest.channelConfigs; const officialCatalogManifest = params.candidate.origin !== "bundled" ? getOfficialExternalPluginCatalogManifest(getOfficialExternalPluginCatalogEntryForPackage(params.candidate.packageName) ?? {}) : void 0; const channelConfigs = mergePackageChannelMetaIntoChannelConfigs({ channelConfigs: mergeCatalogChannelConfigs({ manifestChannelConfigs, catalogChannelConfigs: officialCatalogManifest?.channelConfigs }), packageChannel: params.candidate.packageManifest?.channel }); const packageChannelCommands = normalizeManifestChannelCommandDefaults(params.candidate.packageManifest?.channel?.commands); return { id: pluginId, categories: params.manifest.categories, backupResources: params.manifest.backupResources, doctorContract: params.manifest.doctorContract, doctorHealthChecks: params.manifest.doctorHealthChecks, sessionRouteStateOwners: params.manifest.sessionRouteStateOwners, name: normalizeOptionalString(params.manifest.name) ?? params.candidate.packageName, description: normalizeOptionalString(params.manifest.description) ?? params.candidate.packageDescription, catalog: normalizeManifestCatalog({ featured: params.manifest.catalog?.featured ?? officialCatalogManifest?.catalog?.featured, order: params.manifest.catalog?.order ?? officialCatalogManifest?.catalog?.order }), ...resolvePortablePluginIcons({ rootDir: params.candidate.rootDir, rejectHardlinks: params.rejectHardlinks }), version: normalizeOptionalString(params.manifest.version) ?? params.candidate.packageVersion, packageName: params.candidate.packageName, packageVersion: params.candidate.packageVersion, packageDescription: params.candidate.packageDescription, enabledByDefault: params.manifest.enabledByDefault === true ? true : void 0, enabledByDefaultOnPlatforms: params.manifest.enabledByDefaultOnPlatforms, autoEnableWhenConfiguredProviders: params.manifest.autoEnableWhenConfiguredProviders, legacyPluginIds: params.manifest.legacyPluginIds, format: params.candidate.format ?? "openclaw", bundleFormat: params.candidate.bundleFormat, kind: params.manifest.kind, channels: params.manifest.channels ?? [], channelAccountKeyPolicies: params.manifest.channelAccountKeyPolicies, providers: params.manifest.providers ?? [], providerDiscoverySource: resolveCatalogEntry("providerCatalogEntry"), capabilityCatalogSource: params.manifest.capabilityCatalogEntry === void 0 ? void 0 : resolveCatalogEntry("capabilityCatalogEntry") ?? null, modelSupport: params.manifest.modelSupport, modelCatalog: params.manifest.modelCatalog, modelPricing: params.manifest.modelPricing, modelIdNormalization: params.manifest.modelIdNormalization, providerEndpoints: params.manifest.providerEndpoints, providerRequest: params.manifest.providerRequest, secretProviderIntegrations: params.manifest.secretProviderIntegrations, cliBackends: params.manifest.cliBackends ?? [], syntheticAuthRefs: params.manifest.syntheticAuthRefs ?? [], nonSecretAuthMarkers: params.manifest.nonSecretAuthMarkers ?? [], commandAliases: params.manifest.commandAliases, cliCommands: params.manifest.cliCommands, providerUsageAuthEnvVars: params.manifest.providerUsageAuthEnvVars, providerAuthAliases: params.manifest.providerAuthAliases, providerAuthChoices: params.manifest.providerAuthChoices, activation: params.manifest.activation, setup: params.manifest.setup, packageManifest: params.candidate.packageManifest, packageDependencies: params.candidate.packageDependencies, packageOptionalDependencies: params.candidate.packageOptionalDependencies, packageChannel: params.candidate.packageManifest?.channel, packageInstall: params.candidate.packageManifest?.install, trustedOfficialInstall: params.trust.reason === "trusted-official" ? true : void 0, trust: params.trust, qaRunners: params.manifest.qaRunners, dashboard: params.manifest.dashboard, controlUi: params.manifest.controlUi, uiCapabilities: params.manifest.uiCapabilities, themes: params.manifest.themes, themeDefinitions: loadManifestThemeDefinitions({ pluginId, rootDir: params.candidate.rootDir, themes: params.manifest.themes, rejectHardlinks: params.rejectHardlinks, diagnostics: params.diagnostics }), mcpServers: params.manifest.mcpServers, skills: params.manifest.skills ?? [], settingsFiles: [], hooks: [], origin: params.candidate.origin, workspaceDir: params.candidate.workspaceDir, rootDir: params.candidate.rootDir, source: params.candidate.source, setupSource: params.candidate.setupSource, manifestPath: params.manifestPath, schemaCacheKey: params.schemaCacheKey, configSchema: params.configSchema, configUiHints: params.manifest.uiHints, configGroups: params.manifest.configGroups, contracts: mergeManifestContracts(params.manifest.contracts, officialCatalogManifest?.contracts), transcriptSources: params.manifest.transcriptSources, decisionModels: params.manifest.decisionModels, mediaUnderstandingProviderMetadata: params.manifest.mediaUnderstandingProviderMetadata, imageGenerationProviderMetadata: params.manifest.imageGenerationProviderMetadata, videoGenerationProviderMetadata: params.manifest.videoGenerationProviderMetadata, musicGenerationProviderMetadata: params.manifest.musicGenerationProviderMetadata, toolMetadata: params.manifest.toolMetadata, configContracts: params.manifest.configContracts, channelConfigs, ...params.candidate.packageManifest?.channel?.id ? { channelCatalogMeta: { id: params.candidate.packageManifest.channel.id, ...typeof params.candidate.packageManifest.channel.label === "string" ? { label: params.candidate.packageManifest.channel.label } : {}, ...typeof params.candidate.packageManifest.channel.blurb === "string" ? { blurb: params.candidate.packageManifest.channel.blurb } : {}, ...params.candidate.packageManifest.channel.preferOver ? { preferOver: params.candidate.packageManifest.channel.preferOver } : {}, ...packageChannelCommands ? { commands: packageChannelCommands } : {} } } : {} }; } function buildBundleManifestRecord(params) { return { id: params.manifest.id, name: normalizeOptionalString(params.manifest.name) ?? params.candidate.idHint, description: normalizeOptionalString(params.manifest.description), ...resolvePortablePluginIcons({ rootDir: params.candidate.rootDir, rejectHardlinks: params.rejectHardlinks }), version: normalizeOptionalString(params.manifest.version), packageName: params.candidate.packageName, packageVersion: params.candidate.packageVersion, packageDescription: params.candidate.packageDescription, packageManifest: params.candidate.packageManifest, packageDependencies: params.candidate.packageDependencies, packageOptionalDependencies: params.candidate.packageOptionalDependencies, packageChannel: params.candidate.packageManifest?.channel, packageInstall: params.candidate.packageManifest?.install, format: "bundle", bundleFormat: params.candidate.bundleFormat, bundleCapabilities: params.manifest.capabilities, ...params.manifest.onboardingSkill ? { onboardingSkill: params.manifest.onboardingSkill } : {}, activation: params.manifest.activation, channels: [], providers: [], cliBackends: [], syntheticAuthRefs: [], nonSecretAuthMarkers: [], skills: params.manifest.skills ?? [], settingsFiles: params.manifest.settingsFiles ?? [], hooks: params.manifest.hooks ?? [], origin: params.candidate.origin, workspaceDir: params.candidate.workspaceDir, rootDir: params.candidate.rootDir, source: params.candidate.source, manifestPath: params.manifestPath, schemaCacheKey: void 0, configSchema: void 0, configUiHints: void 0, configContracts: void 0, channelConfigs: void 0 }; } //#endregion //#region src/plugins/manifest-registry-build.ts const PLUGIN_ORIGIN_RANK = { config: 0, workspace: 1, global: 2, bundled: 3 }; function rejectCaseFoldedIdCollisions(records, diagnostics) { const rejected = /* @__PURE__ */ new Set(); for (const [policyId, matches] of groupPluginRecords(records, (record) => normalizePluginPolicyId(record.id))) { const declaredIds = [...new Set(matches.map((record) => record.id))].toSorted(); if (declaredIds.length < 2) continue; const message = `plugin ids ${declaredIds.map((id) => JSON.stringify(id)).join(", ")} collide as normalized id ${JSON.stringify(policyId)}; refusing all colliding plugins`; for (const record of matches) { rejected.add(record); diagnostics.push({ level: "error", pluginId: record.id, source: record.source, message }); } } return records.filter((record) => !rejected.has(record)); } function pushNonBundledChannelConfigDescriptorDiagnostic(params) { if (params.record.origin === "bundled" || params.record.format === "bundle") return; const configuredEntry = params.normalized?.entries[params.record.id]; if (params.normalized?.enabled === false || configuredEntry?.enabled === false || params.normalized?.deny.includes(params.record.id) || params.normalized?.allow.length && !params.normalized.allow.includes(params.record.id)) return; const declaredChannels = params.record.channels.map((channelId) => channelId.trim()).filter((channelId) => channelId.length > 0); if (declaredChannels.length === 0) return; const channelConfigs = params.record.channelConfigs ?? {}; const missingChannels = declaredChannels.filter((channelId) => !Object.hasOwn(channelConfigs, channelId)); if (missingChannels.length === 0) return; const safeMissingChannels = missingChannels.map(sanitizeForLog); params.diagnostics.push({ level: "warn", pluginId: sanitizeForLog(params.record.id), source: sanitizeForLog(params.record.manifestPath), message: `channel plugin manifest declares ${safeMissingChannels.join(", ")} without channelConfigs metadata; add openclaw.plugin.json#channelConfigs so config schema and setup surfaces work before runtime loads. Channels without channelConfigs still appear in channel listings, but setup UI may be limited.` }); } function dedupePluginDiagnostics(diagnostics, discoveryDiagnostics) { const seen = /* @__PURE__ */ new Set(); const deduped = []; for (const diagnostic of diagnostics) { const key = JSON.stringify([ diagnostic.level, diagnostic.pluginId ?? "", diagnostic.message, diagnostic.level === "error" || discoveryDiagnostics.has(diagnostic) ? diagnostic.source ?? "" : "" ]); if (seen.has(key)) continue; seen.add(key); deduped.push(diagnostic); } return deduped; } function isStaleForeignBundledPin(params) { return isForeignBundledPluginRoot(params.candidate.rootDir, params.env) && !isBundledPluginInsideDevSourceRoot({ rootDir: params.candidate.rootDir, env: params.env }); } function resolveDuplicatePrecedenceRank(params) { if (params.candidate.origin === "config" || params.candidate.configSelected) return 0; if (params.candidate.origin === "bundled" && isBundledPluginInsideDevSourceRoot({ rootDir: params.candidate.rootDir, env: params.env })) return 1; if (params.candidate.origin === "global" && !isStaleForeignBundledPin({ candidate: params.candidate, env: params.env }) && matchesInstalledPluginRecord({ pluginId: params.pluginId, candidate: params.candidate, config: params.config, env: params.env, installRecords: params.installRecords })) return 2; if (params.candidate.origin === "bundled") return 3; if (params.candidate.origin === "workspace") return 4; return 5; } function isIntentionalInstalledBundledDuplicate(params) { const leftIsInstalled = matchesInstalledPluginRecord({ pluginId: params.pluginId, candidate: params.left, config: params.config, env: params.env, installRecords: params.installRecords }); const rightIsInstalled = matchesInstalledPluginRecord({ pluginId: params.pluginId, candidate: params.right, config: params.config, env: params.env, installRecords: params.installRecords }); return leftIsInstalled && !isStaleForeignBundledPin({ candidate: params.left, env: params.env }) && params.right.origin === "bundled" && !isBundledPluginInsideDevSourceRoot({ rootDir: params.right.rootDir, env: params.env }) || rightIsInstalled && !isStaleForeignBundledPin({ candidate: params.right, env: params.env }) && params.left.origin === "bundled" && !isBundledPluginInsideDevSourceRoot({ rootDir: params.left.rootDir, env: params.env }); } function isSameGlobalPackageDuplicate(left, right) { if (left.origin !== "global" || right.origin !== "global") return false; const leftPackageName = normalizeOptionalString(left.packageName); const rightPackageName = normalizeOptionalString(right.packageName); if (!leftPackageName || leftPackageName !== rightPackageName) return false; const leftPackageVersion = normalizeOptionalString(left.packageVersion); const rightPackageVersion = normalizeOptionalString(right.packageVersion); return Boolean(leftPackageVersion && rightPackageVersion && leftPackageVersion === rightPackageVersion); } function buildPluginManifestRegistry(params) { const config = params.config ?? {}; const normalized = normalizePluginsConfigWithResolverCore(config.plugins); const env = params.env ?? process.env; const registryPath = params.registryPath ?? resolveInstalledPluginIndexStorePath({ env }); const { getInstallRecords } = params; const discovery = params.candidates ? { candidates: params.candidates, diagnostics: params.diagnostics ?? [] } : params.discovery ?? discoverOpenClawPlugins({ workspaceDir: params.workspaceDir, extraPaths: normalized.loadPaths, env, installRecords: getInstallRecords() }); const discovered = new Set(discovery.diagnostics); const diagnostics = [...discovered]; const candidates = discovery.candidates.toSorted((left, right) => Number(right.origin === "config" || right.configSelected === true) - Number(left.origin === "config" || left.configSelected === true)); const seenIds = /* @__PURE__ */ new Map(); const manifestWarnings = /* @__PURE__ */ new Map(); const reportedExplicitOverrides = /* @__PURE__ */ new Set(); const currentHostVersion = resolveCompatibilityHostVersion(env); const explicitConfiguredFileSources = new Set(normalized.loadPaths.map((loadPath) => resolveUserPath(loadPath, env)).filter((loadPath) => pluginCacheStatSync(loadPath)?.isFile() === true).map((loadPath) => path.resolve(loadPath))); for (const candidate of candidates) { const rejectHardlinks = shouldRejectHardlinkedPluginFiles({ origin: candidate.origin, rootDir: candidate.rootDir, env }); const isBundleRecord = (candidate.format ?? "openclaw") === "bundle"; const isManifestlessConfiguredFile = candidate.origin === "config" && explicitConfiguredFileSources.has(path.resolve(candidate.source)) && !pluginCacheExistsSync(path.join(candidate.rootDir, "openclaw.plugin.json")); if (isManifestlessConfiguredFile && isCoreReservedPluginId(candidate.idHint)) { diagnostics.push({ level: "error", pluginId: candidate.idHint, source: candidate.source, message: `plugin manifest id "${candidate.idHint}" is reserved by OpenClaw core` }); continue; } const manifestRes = candidate.origin === "bundled" && candidate.bundledManifest && candidate.bundledManifestPath ? { ok: true, manifest: candidate.bundledManifest, manifestPath: candidate.bundledManifestPath } : isBundleRecord && candidate.bundleFormat ? loadBundleManifest({ rootDir: candidate.rootDir, bundleFormat: candidate.bundleFormat, rejectHardlinks }) : isManifestlessConfiguredFile ? { ok: true, manifest: { id: candidate.idHint, configSchema: { type: "object", additionalProperties: false }, activation: { onStartup: true } }, manifestPath: candidate.source } : loadPluginManifest(candidate.rootDir, rejectHardlinks); if (!manifestRes.ok) { diagnostics.push({ level: "error", pluginId: candidate.diagnosticIdHint ?? candidate.idHint, message: manifestRes.error, source: manifestRes.manifestPath, ..."diagnosticCode" in manifestRes && manifestRes.diagnosticCode ? { code: manifestRes.diagnosticCode } : {} }); continue; } if ("warnings" in manifestRes && manifestRes.warnings) manifestWarnings.set(manifestRes.manifestPath, manifestRes.warnings); const manifest = manifestRes.manifest; const effectivePluginId = candidate.effectivePluginId ?? manifest.id; if (candidate.origin !== "bundled") { const packageManifestSource = path.join(candidate.packageDir ?? candidate.rootDir, "package.json"); const allowLegacyBareMinHostVersion = candidate.origin === "global" && matchesInstalledPluginRecord({ pluginId: effectivePluginId, candidate, config, env, installRecords: getInstallRecords() }); const minHostVersionCheck = checkMinHostVersion({ currentVersion: currentHostVersion, minHostVersion: candidate.packageManifest?.install?.minHostVersion, allowLegacyBareSemver: allowLegacyBareMinHostVersion }); if (!minHostVersionCheck.ok) { diagnostics.push({ level: minHostVersionCheck.kind === "invalid" ? "error" : "warn", pluginId: effectivePluginId, source: packageManifestSource, message: minHostVersionCheck.kind === "invalid" ? `plugin manifest invalid | ${minHostVersionCheck.error}` : minHostVersionCheck.kind === "unknown_host_version" ? `plugin requires OpenClaw >=${minHostVersionCheck.requirement.minimumLabel}, but this host version could not be determined; skipping load` : `plugin requires OpenClaw >=${minHostVersionCheck.requirement.minimumLabel}, but this host is ${minHostVersionCheck.currentVersion}; skipping load` }); continue; } const packagePluginApiRangeCheck = resolvePackagePluginApiRange(candidate.packageManifest); if (!packagePluginApiRangeCheck.ok) { diagnostics.push({ level: "error", pluginId: effectivePluginId, source: packageManifestSource, message: `plugin manifest invalid | ${packagePluginApiRangeCheck.error}` }); continue; } const packagePluginApiRange = packagePluginApiRangeCheck.range; if (packagePluginApiRange && !satisfiesPluginApiRange(currentHostVersion, packagePluginApiRange)) { diagnostics.push({ level: "warn", configDisposition: "preserve", pluginId: effectivePluginId, source: packageManifestSource, message: `plugin requires plugin API ${packagePluginApiRange}, but this host is ${currentHostVersion}; skipping load (check "openclaw --version", OPENCLAW_COMPATIBILITY_HOST_VERSION, or run "openclaw doctor")` }); continue; } } const configSchema = "configSchema" in manifest ? manifest.configSchema : void 0; const schemaCacheKey = (() => { if (!configSchema || isManifestlessConfiguredFile) return; const file = readPluginCacheFile({ rootDir: candidate.rootDir, relativePath: path.relative(candidate.rootDir, manifestRes.manifestPath), rejectHardlinks, maxBytes: 262144 }); return file.ok ? `${manifestRes.manifestPath}:${file.hash}` : manifestRes.manifestPath; })(); const record = isBundleRecord ? buildBundleManifestRecord({ manifest, candidate, manifestPath: manifestRes.manifestPath, rejectHardlinks }) : buildPluginManifestRecord({ manifest, candidate, manifestPath: manifestRes.manifestPath, diagnostics, rejectHardlinks, schemaCacheKey, configSchema, trust: resolvePluginTrust({ registryPath, pluginId: effectivePluginId, candidate, env, installRecords: getInstallRecords() }), ...params.bundledChannelConfigCollector ? { bundledChannelConfigCollector: params.bundledChannelConfigCollector } : {} }); if (candidate.sourcePreferred || candidate.origin === "bundled" && candidate.configSelected) record.sourcePreferred = true; recordPluginManifestInstallOwner(record, resolvePluginCandidateInstallOwner(candidate), isPluginCandidateInstallOwnerAmbiguous(candidate)); const existing = seenIds.get(effectivePluginId); if (existing) { const samePath = existing.candidate.rootDir === candidate.rootDir; if ((() => { if (samePath) return true; const existingReal = pluginCacheRealpathSync(existing.candidate.rootDir); const candidateReal = pluginCacheRealpathSync(candidate.rootDir); return Boolean(existingReal && candidateReal && existingReal === candidateReal); })()) { if (record.sourcePreferred || existing.record.sourcePreferred) { record.sourcePreferred = true; existing.record.sourcePreferred = true; } if (PLUGIN_ORIGIN_RANK[candidate.origin] < PLUGIN_ORIGIN_RANK[existing.candidate.origin]) seenIds.set(effectivePluginId, { candidate, record }); continue; } const candidateRank = resolveDuplicatePrecedenceRank({ pluginId: effectivePluginId, candidate, config, env, installRecords: getInstallRecords() }); const existingRank = resolveDuplicatePrecedenceRank({ pluginId: effectivePluginId, candidate: existing.candidate, config, env, installRecords: getInstallRecords() }); const candidateWins = candidateRank < existingRank; const winnerCandidate = candidateWins ? candidate : existing.candidate; const overriddenCandidate = candidateWins ? existing.candidate : candidate; if (candidateWins) seenIds.set(effectivePluginId, { candidate, record }); if (isIntentionalInstalledBundledDuplicate({ pluginId: effectivePluginId, left: candidate, right: existing.candidate, config, env, installRecords: getInstallRecords() })) continue; if (isSameGlobalPackageDuplicate(candidate, existing.candidate)) continue; const staleForeignPin = winnerCandidate.origin === "bundled" && isStaleForeignBundledPin({ candidate: overriddenCandidate, env }) && matchesInstalledPluginRecord({ pluginId: effectivePluginId, candidate: overriddenCandidate, env, installRecords: getInstallRecords() }); const explicitOverride = !staleForeignPin && Math.min(candidateRank, existingRank) === 0 && candidateRank !== existingRank; if (explicitOverride) { if (reportedExplicitOverrides.has(effectivePluginId)) continue; reportedExplicitOverrides.add(effectivePluginId); } diagnostics.push({ level: explicitOverride ? "info" : "warn", ...explicitOverride ? { code: "explicit-config-plugin-selection" } : {}, pluginId: effectivePluginId, source: overriddenCandidate.source, message: staleForeignPin ? `stale plugin install record: "${effectivePluginId}" is pinned to ${overriddenCandidate.rootDir}, which belongs to a different OpenClaw installation. This installation's bundled plugin is being used instead. No uninstall is needed to use the bundled plugin. Uninstalling, even with \`--keep-files\`, removes plugin configuration; re-enabling does not restore it.` : explicitOverride ? `duplicate plugin id resolved by explicit config-selected plugin; ${overriddenCandidate.origin} plugin will be overridden by config plugin (${winnerCandidate.source})` : `duplicate plugin id detected; ${overriddenCandidate.origin} plugin will be overridden by ${winnerCandidate.origin} plugin (${winnerCandidate.source})` }); continue; } seenIds.set(effectivePluginId, { candidate, record }); } const plugins = rejectCaseFoldedIdCollisions([...seenIds.values()].map(({ record }) => record), diagnostics); for (const record of plugins) { for (const warning of manifestWarnings.get(record.manifestPath) ?? []) diagnostics.push({ level: "warn", pluginId: sanitizeForLog(record.id), source: sanitizeForLog(record.manifestPath), message: sanitizeForLog(warning) }); pushNonBundledChannelConfigDescriptorDiagnostic({ record, diagnostics, normalized }); } return { plugins, diagnostics: dedupePluginDiagnostics(diagnostics, discovered) }; } /** Load manifest metadata from the bundled/source plugin tree without consulting operator state. */ function loadBundledPluginManifestRegistry(params = {}) { const env = params.env ?? process.env; const installRecords = {}; return buildPluginManifestRegistry({ env, getInstallRecords: () => installRecords, discovery: discoverOpenClawPlugins({ env, installRecords, rootScope: "bundled", ...params.bundledRoot ? { bundledRoot: params.bundledRoot } : {} }) }); } //#endregion export { resolvePluginManifestInstallOwner as a, isPluginManifestInstallOwnerAmbiguous as i, loadBundledPluginManifestRegistry as n, resolvePolicyPluginActivationState as r, buildPluginManifestRegistry as t };