From b7404399ef6fa18e14b3492f01e4326facc7b949 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 26 Apr 2026 11:11:43 +0100 Subject: [PATCH] perf: cache bundled runtime dep manifests --- src/plugins/bundled-runtime-deps.test.ts | 135 +++++++++++++++++++++++ src/plugins/bundled-runtime-deps.ts | 42 +++++-- 2 files changed, 166 insertions(+), 11 deletions(-) diff --git a/src/plugins/bundled-runtime-deps.test.ts b/src/plugins/bundled-runtime-deps.test.ts index 825683242f8..053399ccf3d 100644 --- a/src/plugins/bundled-runtime-deps.test.ts +++ b/src/plugins/bundled-runtime-deps.test.ts @@ -14,6 +14,7 @@ import { isWritableDirectory, resolveBundledRuntimeDependencyInstallRoot, resolveBundledRuntimeDepsNpmRunner, + scanBundledPluginRuntimeDeps, type BundledRuntimeDepsInstallParams, } from "./bundled-runtime-deps.js"; @@ -41,6 +42,30 @@ function writeInstalledPackage(rootDir: string, packageName: string, version: st ); } +function writeBundledPluginPackage(params: { + packageRoot: string; + pluginId: string; + deps: Record; + enabledByDefault?: boolean; + channels?: string[]; +}): string { + const pluginRoot = path.join(params.packageRoot, "dist", "extensions", params.pluginId); + fs.mkdirSync(pluginRoot, { recursive: true }); + fs.writeFileSync( + path.join(pluginRoot, "package.json"), + JSON.stringify({ dependencies: params.deps }), + ); + fs.writeFileSync( + path.join(pluginRoot, "openclaw.plugin.json"), + JSON.stringify({ + id: params.pluginId, + enabledByDefault: params.enabledByDefault === true, + ...(params.channels ? { channels: params.channels } : {}), + }), + ); + return pluginRoot; +} + function statfsFixture(params: { bavail: number; bsize?: number; @@ -587,6 +612,116 @@ describe("installBundledRuntimeDeps", () => { }); }); +describe("scanBundledPluginRuntimeDeps config policy", () => { + function setupPolicyPackageRoot(): string { + const packageRoot = makeTempDir(); + writeBundledPluginPackage({ + packageRoot, + pluginId: "alpha", + deps: { "alpha-runtime": "1.0.0" }, + enabledByDefault: true, + }); + writeBundledPluginPackage({ + packageRoot, + pluginId: "telegram", + deps: { "telegram-runtime": "2.0.0" }, + channels: ["telegram"], + }); + return packageRoot; + } + + it.each([ + { + name: "includes default-enabled bundled plugins", + config: {}, + includeConfiguredChannels: false, + expectedDeps: ["alpha-runtime@1.0.0"], + }, + { + name: "keeps default-enabled bundled plugins behind restrictive allowlists", + config: { plugins: { allow: ["browser"] } }, + includeConfiguredChannels: false, + expectedDeps: [], + }, + { + name: "does not let explicit plugin entries bypass restrictive allowlists", + config: { plugins: { allow: ["browser"], entries: { alpha: { enabled: true } } } }, + includeConfiguredChannels: false, + expectedDeps: [], + }, + { + name: "lets deny override default-enabled bundled plugins", + config: { plugins: { deny: ["alpha"] } }, + includeConfiguredChannels: false, + expectedDeps: [], + }, + { + name: "lets disabled entries override default-enabled bundled plugins", + config: { plugins: { entries: { alpha: { enabled: false } } } }, + includeConfiguredChannels: false, + expectedDeps: [], + }, + { + name: "lets explicit bundled channel enablement bypass restrictive allowlists", + config: { + plugins: { allow: ["browser"] }, + channels: { telegram: { enabled: true } }, + }, + includeConfiguredChannels: false, + expectedDeps: ["telegram-runtime@2.0.0"], + }, + { + name: "keeps channel recovery behind restrictive allowlists", + config: { + plugins: { allow: ["browser"] }, + channels: { telegram: { botToken: "123:abc" } }, + }, + includeConfiguredChannels: true, + expectedDeps: [], + }, + { + name: "includes configured channels during recovery without restrictive allowlists", + config: { channels: { telegram: { botToken: "123:abc" } } }, + includeConfiguredChannels: true, + expectedDeps: ["alpha-runtime@1.0.0", "telegram-runtime@2.0.0"], + }, + { + name: "lets explicit channel disable override recovery", + config: { channels: { telegram: { botToken: "123:abc", enabled: false } } }, + includeConfiguredChannels: true, + expectedDeps: ["alpha-runtime@1.0.0"], + }, + ])("$name", ({ config, includeConfiguredChannels, expectedDeps }) => { + const result = scanBundledPluginRuntimeDeps({ + packageRoot: setupPolicyPackageRoot(), + config, + includeConfiguredChannels, + }); + + expect(result.deps.map((dep) => `${dep.name}@${dep.version}`)).toEqual(expectedDeps); + expect(result.conflicts).toEqual([]); + }); + + it("reads each bundled plugin manifest once per runtime-deps scan", () => { + const packageRoot = makeTempDir(); + const pluginRoot = writeBundledPluginPackage({ + packageRoot, + pluginId: "alpha", + deps: { "alpha-runtime": "1.0.0" }, + enabledByDefault: true, + channels: ["alpha"], + }); + const manifestPath = path.join(pluginRoot, "openclaw.plugin.json"); + const readFileSyncSpy = vi.spyOn(fs, "readFileSync"); + + scanBundledPluginRuntimeDeps({ packageRoot, config: {} }); + + expect( + readFileSyncSpy.mock.calls.filter((call) => path.resolve(String(call[0])) === manifestPath), + ).toHaveLength(1); + }); +}); + describe("ensureBundledPluginRuntimeDeps", () => { it("installs plugin-local runtime deps when one is missing", () => { const packageRoot = makeTempDir(); diff --git a/src/plugins/bundled-runtime-deps.ts b/src/plugins/bundled-runtime-deps.ts index 273fc52c6a7..a1384c428b8 100644 --- a/src/plugins/bundled-runtime-deps.ts +++ b/src/plugins/bundled-runtime-deps.ts @@ -877,17 +877,31 @@ export function resolveBundledRuntimeDepsNpmRunner(params: { }, }; } -function readBundledPluginChannels(pluginDir: string): string[] { +type BundledPluginRuntimeDepsManifest = { + channels: string[]; + enabledByDefault: boolean; +}; + +type BundledPluginRuntimeDepsManifestCache = Map; + +function readBundledPluginRuntimeDepsManifest( + pluginDir: string, + cache?: BundledPluginRuntimeDepsManifestCache, +): BundledPluginRuntimeDepsManifest { + const cached = cache?.get(pluginDir); + if (cached) { + return cached; + } const manifest = readJsonObject(path.join(pluginDir, "openclaw.plugin.json")); const channels = manifest?.channels; - if (!Array.isArray(channels)) { - return []; - } - return channels.filter((entry): entry is string => typeof entry === "string" && entry !== ""); -} - -function readBundledPluginEnabledByDefault(pluginDir: string): boolean { - return readJsonObject(path.join(pluginDir, "openclaw.plugin.json"))?.enabledByDefault === true; + const runtimeDepsManifest = { + channels: Array.isArray(channels) + ? channels.filter((entry): entry is string => typeof entry === "string" && entry !== "") + : [], + enabledByDefault: manifest?.enabledByDefault === true, + }; + cache?.set(pluginDir, runtimeDepsManifest); + return runtimeDepsManifest; } function isBundledPluginConfiguredForRuntimeDeps(params: { @@ -895,6 +909,7 @@ function isBundledPluginConfiguredForRuntimeDeps(params: { pluginId: string; pluginDir: string; includeConfiguredChannels?: boolean; + manifestCache?: BundledPluginRuntimeDepsManifestCache; }): boolean { const plugins = normalizePluginsConfig(params.config.plugins); if (!plugins.enabled) { @@ -909,7 +924,8 @@ function isBundledPluginConfiguredForRuntimeDeps(params: { } let hasExplicitChannelDisable = false; let hasConfiguredChannel = false; - for (const channelId of readBundledPluginChannels(params.pluginDir)) { + const manifest = readBundledPluginRuntimeDepsManifest(params.pluginDir, params.manifestCache); + for (const channelId of manifest.channels) { const normalizedChannelId = normalizeOptionalLowercaseString(channelId); if (!normalizedChannelId) { continue; @@ -955,7 +971,7 @@ function isBundledPluginConfiguredForRuntimeDeps(params: { if (hasConfiguredChannel) { return true; } - return readBundledPluginEnabledByDefault(params.pluginDir); + return manifest.enabledByDefault; } function shouldIncludeBundledPluginRuntimeDeps(params: { @@ -964,6 +980,7 @@ function shouldIncludeBundledPluginRuntimeDeps(params: { pluginId: string; pluginDir: string; includeConfiguredChannels?: boolean; + manifestCache?: BundledPluginRuntimeDepsManifestCache; }): boolean { if (params.pluginIds && !params.pluginIds.has(params.pluginId)) { return false; @@ -976,6 +993,7 @@ function shouldIncludeBundledPluginRuntimeDeps(params: { pluginId: params.pluginId, pluginDir: params.pluginDir, includeConfiguredChannels: params.includeConfiguredChannels, + manifestCache: params.manifestCache, }); } @@ -989,6 +1007,7 @@ function collectBundledPluginRuntimeDeps(params: { conflicts: RuntimeDepConflict[]; } { const versionMap = new Map>>(); + const manifestCache: BundledPluginRuntimeDepsManifestCache = new Map(); for (const entry of fs.readdirSync(params.extensionsDir, { withFileTypes: true })) { if (!entry.isDirectory()) { @@ -1003,6 +1022,7 @@ function collectBundledPluginRuntimeDeps(params: { pluginId, pluginDir, includeConfiguredChannels: params.includeConfiguredChannels, + manifestCache, }) ) { continue;