diff --git a/src/plugins/bundled-runtime-deps.ts b/src/plugins/bundled-runtime-deps.ts index 7b16638843d..3042c6eda92 100644 --- a/src/plugins/bundled-runtime-deps.ts +++ b/src/plugins/bundled-runtime-deps.ts @@ -29,8 +29,11 @@ import { type BundledRuntimeDepsPackageManager, type BundledRuntimeDepsPackageManagerRunner, } from "./bundled-runtime-deps-package-manager.js"; -import { normalizePluginsConfig } from "./config-state.js"; -import { passesManifestOwnerBasePolicy } from "./manifest-owner-policy.js"; +import { + normalizePluginsConfigWithResolver, + type NormalizedPluginsConfig, + type NormalizePluginId, +} from "./config-normalization-shared.js"; import { satisfies, validSemver } from "./semver.runtime.js"; export { @@ -832,6 +835,9 @@ function replaceNodeModulesDir(targetDir: string, sourceDir: string): void { type BundledPluginRuntimeDepsManifest = { channels: string[]; enabledByDefault: boolean; + id?: string; + legacyPluginIds: string[]; + providers: string[]; }; type BundledPluginRuntimeDepsManifestCache = Map; @@ -846,36 +852,135 @@ function readBundledPluginRuntimeDepsManifest( } const manifest = readJsonObject(path.join(pluginDir, "openclaw.plugin.json")); const channels = manifest?.channels; + const legacyPluginIds = manifest?.legacyPluginIds; + const providers = manifest?.providers; const runtimeDepsManifest = { channels: Array.isArray(channels) ? channels.filter((entry): entry is string => typeof entry === "string" && entry !== "") : [], enabledByDefault: manifest?.enabledByDefault === true, + ...(typeof manifest?.id === "string" && manifest.id.trim() ? { id: manifest.id } : {}), + legacyPluginIds: Array.isArray(legacyPluginIds) + ? legacyPluginIds.filter( + (entry): entry is string => typeof entry === "string" && entry !== "", + ) + : [], + providers: Array.isArray(providers) + ? providers.filter((entry): entry is string => typeof entry === "string" && entry !== "") + : [], }; cache?.set(pluginDir, runtimeDepsManifest); return runtimeDepsManifest; } +const BUILT_IN_RUNTIME_DEPS_PLUGIN_ALIAS_FALLBACKS: ReadonlyArray< + readonly [alias: string, pluginId: string] +> = [ + ["openai-codex", "openai"], + ["google-gemini-cli", "google"], + ["minimax-portal", "minimax"], + ["minimax-portal-auth", "minimax"], +] as const; + +function addBundledRuntimeDepsPluginAlias( + lookup: Map, + alias: string | undefined, + pluginId: string, +): void { + const normalizedAlias = normalizeOptionalLowercaseString(alias); + if (normalizedAlias) { + lookup.set(normalizedAlias, pluginId); + } +} + +function createBundledRuntimeDepsPluginIdNormalizer(params: { + extensionsDir: string; + manifestCache: BundledPluginRuntimeDepsManifestCache; +}): NormalizePluginId { + const lookup = new Map(); + for (const [alias, pluginId] of BUILT_IN_RUNTIME_DEPS_PLUGIN_ALIAS_FALLBACKS) { + lookup.set(alias, pluginId); + lookup.set(pluginId, pluginId); + } + if (!fs.existsSync(params.extensionsDir)) { + return (id) => { + const trimmed = id.trim(); + const normalized = normalizeOptionalLowercaseString(trimmed); + return (normalized && lookup.get(normalized)) || trimmed; + }; + } + for (const entry of fs.readdirSync(params.extensionsDir, { withFileTypes: true })) { + if (!entry.isDirectory()) { + continue; + } + const fallbackPluginId = entry.name; + const pluginDir = path.join(params.extensionsDir, fallbackPluginId); + const manifest = readBundledPluginRuntimeDepsManifest(pluginDir, params.manifestCache); + const pluginId = manifest.id ?? fallbackPluginId; + addBundledRuntimeDepsPluginAlias(lookup, pluginId, pluginId); + addBundledRuntimeDepsPluginAlias(lookup, fallbackPluginId, pluginId); + for (const providerId of manifest.providers) { + addBundledRuntimeDepsPluginAlias(lookup, providerId, pluginId); + } + for (const legacyPluginId of manifest.legacyPluginIds) { + addBundledRuntimeDepsPluginAlias(lookup, legacyPluginId, pluginId); + } + } + return (id) => { + const trimmed = id.trim(); + const normalized = normalizeOptionalLowercaseString(trimmed); + return (normalized && lookup.get(normalized)) || trimmed; + }; +} + +function passesRuntimeDepsPluginPolicy(params: { + pluginId: string; + plugins: NormalizedPluginsConfig; + allowExplicitlyDisabled?: boolean; + allowRestrictiveAllowlistBypass?: boolean; +}): boolean { + if (!params.plugins.enabled) { + return false; + } + if (params.plugins.deny.includes(params.pluginId)) { + return false; + } + if ( + params.plugins.entries[params.pluginId]?.enabled === false && + params.allowExplicitlyDisabled !== true + ) { + return false; + } + return ( + params.allowRestrictiveAllowlistBypass === true || + params.plugins.allow.length === 0 || + params.plugins.allow.includes(params.pluginId) + ); +} + function isBundledPluginConfiguredForRuntimeDeps(params: { config: OpenClawConfig; + plugins: NormalizedPluginsConfig; pluginId: string; pluginDir: string; includeConfiguredChannels?: boolean; manifestCache?: BundledPluginRuntimeDepsManifestCache; }): boolean { - const plugins = normalizePluginsConfig(params.config.plugins); if ( - !passesManifestOwnerBasePolicy({ - plugin: { id: params.pluginId }, - normalizedConfig: plugins, + !passesRuntimeDepsPluginPolicy({ + pluginId: params.pluginId, + plugins: params.plugins, allowRestrictiveAllowlistBypass: true, }) ) { return false; } - const entry = plugins.entries[params.pluginId]; + const entry = params.plugins.entries[params.pluginId]; const manifest = readBundledPluginRuntimeDepsManifest(params.pluginDir, params.manifestCache); - if (plugins.slots.memory === params.pluginId || plugins.slots.contextEngine === params.pluginId) { + if ( + params.plugins.slots.memory === params.pluginId || + params.plugins.slots.contextEngine === params.pluginId + ) { return true; } let hasExplicitChannelDisable = false; @@ -917,7 +1022,7 @@ function isBundledPluginConfiguredForRuntimeDeps(params: { if (hasExplicitChannelDisable) { return false; } - if (plugins.allow.length > 0 && !plugins.allow.includes(params.pluginId)) { + if (params.plugins.allow.length > 0 && !params.plugins.allow.includes(params.pluginId)) { return false; } if (entry?.enabled === true) { @@ -931,12 +1036,12 @@ function isBundledPluginConfiguredForRuntimeDeps(params: { function isBundledPluginExplicitlyDisabledForRuntimeDeps(params: { config: OpenClawConfig; + plugins: NormalizedPluginsConfig; pluginId: string; pluginDir: string; manifestCache?: BundledPluginRuntimeDepsManifestCache; }): boolean { - const plugins = normalizePluginsConfig(params.config.plugins); - if (plugins.entries[params.pluginId]?.enabled === false) { + if (params.plugins.entries[params.pluginId]?.enabled === false) { return true; } const manifest = readBundledPluginRuntimeDepsManifest(params.pluginDir, params.manifestCache); @@ -959,6 +1064,7 @@ function isBundledPluginExplicitlyDisabledForRuntimeDeps(params: { function shouldIncludeBundledPluginRuntimeDeps(params: { config?: OpenClawConfig; + plugins?: NormalizedPluginsConfig; pluginIds?: ReadonlySet; selectedPluginIds?: ReadonlySet; pluginId: string; @@ -971,8 +1077,10 @@ function shouldIncludeBundledPluginRuntimeDeps(params: { params.selectedPluginIds.has(params.pluginId) && !( params.config && + params.plugins && isBundledPluginExplicitlyDisabledForRuntimeDeps({ config: params.config, + plugins: params.plugins, pluginId: params.pluginId, pluginDir: params.pluginDir, manifestCache: params.manifestCache, @@ -993,15 +1101,21 @@ function shouldIncludeBundledPluginRuntimeDeps(params: { return true; } if (scopedToPluginIds) { - const plugins = normalizePluginsConfig(params.config.plugins); - return passesManifestOwnerBasePolicy({ - plugin: { id: params.pluginId }, - normalizedConfig: plugins, + if (!params.plugins) { + return true; + } + return passesRuntimeDepsPluginPolicy({ + pluginId: params.pluginId, + plugins: params.plugins, allowRestrictiveAllowlistBypass: true, }); } + if (!params.plugins) { + return false; + } return isBundledPluginConfiguredForRuntimeDeps({ config: params.config, + plugins: params.plugins, pluginId: params.pluginId, pluginDir: params.pluginDir, includeConfiguredChannels: params.includeConfiguredChannels, @@ -1015,13 +1129,27 @@ function collectBundledPluginRuntimeDeps(params: { pluginIds?: ReadonlySet; selectedPluginIds?: ReadonlySet; includeConfiguredChannels?: boolean; + manifestCache?: BundledPluginRuntimeDepsManifestCache; + normalizePluginId?: NormalizePluginId; }): { deps: RuntimeDepEntry[]; conflicts: RuntimeDepConflict[]; pluginIds: string[]; } { const versionMap = new Map>>(); - const manifestCache: BundledPluginRuntimeDepsManifestCache = new Map(); + const manifestCache: BundledPluginRuntimeDepsManifestCache = params.manifestCache ?? new Map(); + const needsPluginIdNormalizer = Boolean(params.config); + const normalizePluginId = + params.normalizePluginId ?? + (needsPluginIdNormalizer + ? createBundledRuntimeDepsPluginIdNormalizer({ + extensionsDir: params.extensionsDir, + manifestCache, + }) + : undefined); + const plugins = params.config + ? normalizePluginsConfigWithResolver(params.config.plugins, normalizePluginId) + : undefined; const includedPluginIds = new Set(); for (const entry of fs.readdirSync(params.extensionsDir, { withFileTypes: true })) { @@ -1033,6 +1161,7 @@ function collectBundledPluginRuntimeDeps(params: { if ( !shouldIncludeBundledPluginRuntimeDeps({ config: params.config, + plugins, pluginIds: params.pluginIds, selectedPluginIds: params.selectedPluginIds, pluginId, @@ -1099,12 +1228,13 @@ function collectBundledPluginRuntimeDeps(params: { function normalizePluginIdSet( pluginIds: readonly string[] | undefined, + normalizePluginId: NormalizePluginId = (id) => normalizeOptionalLowercaseString(id) ?? "", ): ReadonlySet | undefined { if (!pluginIds) { return undefined; } const normalized = pluginIds - .map((entry) => normalizeOptionalLowercaseString(entry)) + .map((entry) => normalizePluginId(entry)) .filter((entry): entry is string => Boolean(entry)); return new Set(normalized); } @@ -1128,12 +1258,22 @@ export function scanBundledPluginRuntimeDeps(params: { if (!fs.existsSync(extensionsDir)) { return { deps: [], missing: [], conflicts: [] }; } + const manifestCache: BundledPluginRuntimeDepsManifestCache = new Map(); + const normalizePluginId = + params.config || params.pluginIds || params.selectedPluginIds + ? createBundledRuntimeDepsPluginIdNormalizer({ + extensionsDir, + manifestCache, + }) + : undefined; const { deps, conflicts, pluginIds } = collectBundledPluginRuntimeDeps({ extensionsDir, config: params.config, - pluginIds: normalizePluginIdSet(params.pluginIds), - selectedPluginIds: normalizePluginIdSet(params.selectedPluginIds), + pluginIds: normalizePluginIdSet(params.pluginIds, normalizePluginId), + selectedPluginIds: normalizePluginIdSet(params.selectedPluginIds, normalizePluginId), includeConfiguredChannels: params.includeConfiguredChannels, + manifestCache, + ...(normalizePluginId ? { normalizePluginId } : {}), }); const packageRuntimeDeps = pluginIds.length > 0 ? collectMirroredPackageRuntimeDeps(params.packageRoot) : []; @@ -1705,12 +1845,26 @@ export function ensureBundledPluginRuntimeDeps(params: { config?: OpenClawConfig; installDeps?: (params: BundledRuntimeDepsInstallParams) => void; }): BundledRuntimeDepsEnsureResult { + const extensionsDir = path.dirname(params.pluginRoot); + const manifestCache: BundledPluginRuntimeDepsManifestCache = new Map(); + const normalizePluginId = params.config + ? createBundledRuntimeDepsPluginIdNormalizer({ + extensionsDir, + manifestCache, + }) + : undefined; + const plugins = params.config + ? normalizePluginsConfigWithResolver(params.config.plugins, normalizePluginId) + : undefined; if ( params.config && + plugins && !isBundledPluginConfiguredForRuntimeDeps({ config: params.config, + plugins, pluginId: params.pluginId, pluginDir: params.pluginRoot, + manifestCache, }) ) { return createBundledRuntimeDepsEnsureResult([]); @@ -1738,8 +1892,10 @@ export function ensureBundledPluginRuntimeDeps(params: { let deps = pluginDepEntries; if (usePackageLevelPlan && packageRoot) { const packagePlan = collectBundledPluginRuntimeDeps({ - extensionsDir: path.dirname(params.pluginRoot), + extensionsDir, ...(params.config ? { config: params.config } : {}), + manifestCache, + ...(normalizePluginId ? { normalizePluginId } : {}), }); if (packagePlan.conflicts.length === 0 && packagePlan.deps.length > 0) { deps = mergeRuntimeDepEntries([ diff --git a/src/plugins/semver.runtime.ts b/src/plugins/semver.runtime.ts index 9aafc4da58f..9f9a4d2f848 100644 --- a/src/plugins/semver.runtime.ts +++ b/src/plugins/semver.runtime.ts @@ -1,18 +1,26 @@ import { createRequire } from "node:module"; const require = createRequire(import.meta.url); -const semver = require("semver") as { + +type SemverRuntime = { satisfies(version: string, range: string, options?: { includePrerelease?: boolean }): boolean; valid(version: string): string | null; validRange(range: string): string | null; }; +let semver: SemverRuntime | undefined; + +function getSemver(): SemverRuntime { + semver ??= require("semver") as SemverRuntime; + return semver; +} + export const satisfies = ( version: string, range: string, options?: { includePrerelease?: boolean }, -): boolean => semver.satisfies(version, range, options); +): boolean => getSemver().satisfies(version, range, options); -export const validSemver = (version: string): string | null => semver.valid(version); +export const validSemver = (version: string): string | null => getSemver().valid(version); -export const validRange = (range: string): string | null => semver.validRange(range); +export const validRange = (range: string): string | null => getSemver().validRange(range);