perf: cache bundled runtime dep manifests

This commit is contained in:
Peter Steinberger
2026-04-26 11:11:43 +01:00
parent f337c9019c
commit b7404399ef
2 changed files with 166 additions and 11 deletions

View File

@@ -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<string, string>;
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();

View File

@@ -877,17 +877,31 @@ export function resolveBundledRuntimeDepsNpmRunner(params: {
},
};
}
function readBundledPluginChannels(pluginDir: string): string[] {
type BundledPluginRuntimeDepsManifest = {
channels: string[];
enabledByDefault: boolean;
};
type BundledPluginRuntimeDepsManifestCache = Map<string, BundledPluginRuntimeDepsManifest>;
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<string, Map<string, Set<string>>>();
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;