From 2aacc4053bdff48eb6e9d258c32f38dd95872e02 Mon Sep 17 00:00:00 2001 From: Shakker Date: Tue, 28 Apr 2026 02:25:10 +0100 Subject: [PATCH] refactor: accept supplied plugin manifest registry --- src/plugins/loader.test.ts | 26 ++++++++++++++++ src/plugins/loader.ts | 61 +++++++++++++++++++++++++++----------- 2 files changed, 70 insertions(+), 17 deletions(-) diff --git a/src/plugins/loader.test.ts b/src/plugins/loader.test.ts index 0ea045d084c..0d54a18a3c5 100644 --- a/src/plugins/loader.test.ts +++ b/src/plugins/loader.test.ts @@ -60,6 +60,7 @@ import { useNoBundledPlugins, writePlugin, } from "./loader.test-fixtures.js"; +import { loadPluginManifestRegistry } from "./manifest-registry.js"; import { listMemoryEmbeddingProviders, registerMemoryEmbeddingProvider, @@ -897,6 +898,31 @@ afterAll(() => { }); describe("loadOpenClawPlugins", () => { + it("can load scoped plugins from a supplied manifest registry without rereading manifests", () => { + useNoBundledPlugins(); + const plugin = writePlugin({ + id: "supplied-manifest", + body: `module.exports = { id: "supplied-manifest", register() {} };`, + }); + const config = { + plugins: { + load: { paths: [plugin.file] }, + allow: [plugin.id], + }, + }; + const manifestRegistry = loadPluginManifestRegistry({ config, cache: false }); + fs.rmSync(path.join(plugin.dir, "openclaw.plugin.json")); + + const registry = loadOpenClawPlugins({ + cache: false, + config, + manifestRegistry, + onlyPluginIds: [plugin.id], + }); + + expect(registry.plugins.find((entry) => entry.id === plugin.id)?.status).toBe("loaded"); + }); + it("refreshes bundled plugin-sdk aliases without deleting the shared alias directory", () => { const distRoot = makeTempDir(); const pluginSdkDir = path.join(distRoot, "plugin-sdk"); diff --git a/src/plugins/loader.ts b/src/plugins/loader.ts index 6b69b1ec329..406bcff8c61 100644 --- a/src/plugins/loader.ts +++ b/src/plugins/loader.ts @@ -68,7 +68,7 @@ import { type NormalizedPluginsConfig, type PluginActivationState, } from "./config-state.js"; -import { discoverOpenClawPlugins } from "./discovery.js"; +import { discoverOpenClawPlugins, type PluginCandidate } from "./discovery.js"; import { getGlobalHookRunner, initializeGlobalHookRunner } from "./hook-runner-global.js"; import { toSafeImportPath } from "./import-specifier.js"; import { loadInstalledPluginIndexInstallRecordsSync } from "./installed-plugin-index-records.js"; @@ -79,7 +79,11 @@ import { } from "./interactive-registry.js"; import { getCachedPluginJitiLoader, type PluginJitiLoaderCache } from "./jiti-loader-cache.js"; import { PluginLoaderCacheState } from "./loader-cache-state.js"; -import { loadPluginManifestRegistry, type PluginManifestRecord } from "./manifest-registry.js"; +import { + loadPluginManifestRegistry, + type PluginManifestRecord, + type PluginManifestRegistry, +} from "./manifest-registry.js"; import type { PluginBundleFormat, PluginDiagnostic, PluginFormat } from "./manifest-types.js"; import type { PluginManifestContracts } from "./manifest.js"; import { @@ -173,6 +177,7 @@ export type PluginLoadOptions = { installBundledRuntimeDeps?: boolean; throwOnLoadError?: boolean; bundledRuntimeDepsInstaller?: (params: BundledRuntimeDepsInstallParams) => void; + manifestRegistry?: PluginManifestRegistry; }; const CLI_METADATA_ENTRY_BASENAMES = [ @@ -253,6 +258,20 @@ const LAZY_RUNTIME_REFLECTION_KEYS = [ "modelAuth", ] as const satisfies readonly (keyof PluginRuntime)[]; +function createPluginCandidatesFromManifestRegistry( + manifestRegistry: PluginManifestRegistry, +): PluginCandidate[] { + return manifestRegistry.plugins.map((record) => ({ + idHint: record.id, + rootDir: record.rootDir, + source: record.source, + origin: record.origin, + ...(record.workspaceDir !== undefined ? { workspaceDir: record.workspaceDir } : {}), + ...(record.format !== undefined ? { format: record.format } : {}), + ...(record.bundleFormat !== undefined ? { bundleFormat: record.bundleFormat } : {}), + })); +} + export function clearPluginLoaderCache(): void { pluginLoaderCacheState.clear(); clearBundledRuntimeDependencyNodePaths(); @@ -2311,21 +2330,29 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi activateGlobalSideEffects: shouldActivate, }); - const discovery = discoverOpenClawPlugins({ - workspaceDir: options.workspaceDir, - extraPaths: normalized.loadPaths, - cache: options.cache, - env, - }); - const manifestRegistry = loadPluginManifestRegistry({ - config: cfg, - workspaceDir: options.workspaceDir, - cache: options.cache, - env, - candidates: discovery.candidates, - diagnostics: discovery.diagnostics, - installRecords: Object.keys(installRecords).length > 0 ? installRecords : undefined, - }); + const suppliedManifestRegistry = options.manifestRegistry; + const discovery = suppliedManifestRegistry + ? { + candidates: createPluginCandidatesFromManifestRegistry(suppliedManifestRegistry), + diagnostics: [] as PluginDiagnostic[], + } + : discoverOpenClawPlugins({ + workspaceDir: options.workspaceDir, + extraPaths: normalized.loadPaths, + cache: options.cache, + env, + }); + const manifestRegistry = + suppliedManifestRegistry ?? + loadPluginManifestRegistry({ + config: cfg, + workspaceDir: options.workspaceDir, + cache: options.cache, + env, + candidates: discovery.candidates, + diagnostics: discovery.diagnostics, + installRecords: Object.keys(installRecords).length > 0 ? installRecords : undefined, + }); pushDiagnostics(registry.diagnostics, manifestRegistry.diagnostics); warnWhenAllowlistIsOpen({ emitWarning: shouldActivate,