perf: lazy-load plugin install runtime helpers

This commit is contained in:
Peter Steinberger
2026-03-22 21:43:58 +00:00
parent 4235fb16d6
commit 5d379f92a3
2 changed files with 120 additions and 61 deletions

View File

@@ -0,0 +1,62 @@
import { fileExists, readJsonFile, resolveArchiveKind } from "../infra/archive.js";
import { writeFileFromPathWithinRoot } from "../infra/fs-safe.js";
import { resolveExistingInstallPath, withExtractedArchiveRoot } from "../infra/install-flow.js";
import {
resolveInstallModeOptions,
resolveTimedInstallModeOptions,
} from "../infra/install-mode-options.js";
import { installPackageDir } from "../infra/install-package-dir.js";
import {
type NpmIntegrityDrift,
type NpmSpecResolution,
resolveArchiveSourcePath,
} from "../infra/install-source-utils.js";
import {
ensureInstallTargetAvailable,
resolveCanonicalInstallTarget,
} from "../infra/install-target.js";
import {
finalizeNpmSpecArchiveInstall,
installFromNpmSpecArchiveWithInstaller,
} from "../infra/npm-pack-install.js";
import { validateRegistryNpmSpec } from "../infra/npm-registry-spec.js";
import { resolveRuntimeServiceVersion } from "../version.js";
import { detectBundleManifestFormat, loadBundleManifest } from "./bundle-manifest.js";
import { scanBundleInstallSource, scanPackageInstallSource } from "./install-security-scan.js";
import {
getPackageManifestMetadata,
loadPluginManifest,
resolvePackageExtensionEntries,
} from "./manifest.js";
import { checkMinHostVersion } from "./min-host-version.js";
import { isPathInside } from "./path-safety.js";
export type { NpmIntegrityDrift, NpmSpecResolution };
export {
checkMinHostVersion,
detectBundleManifestFormat,
ensureInstallTargetAvailable,
fileExists,
finalizeNpmSpecArchiveInstall,
getPackageManifestMetadata,
installFromNpmSpecArchiveWithInstaller,
installPackageDir,
isPathInside,
loadBundleManifest,
loadPluginManifest,
readJsonFile,
resolveArchiveKind,
resolveArchiveSourcePath,
resolveCanonicalInstallTarget,
resolveExistingInstallPath,
resolveInstallModeOptions,
resolvePackageExtensionEntries,
resolveRuntimeServiceVersion,
resolveTimedInstallModeOptions,
scanBundleInstallSource,
scanPackageInstallSource,
validateRegistryNpmSpec,
withExtractedArchiveRoot,
writeFileFromPathWithinRoot,
};

View File

@@ -1,45 +1,24 @@
import fs from "node:fs/promises";
import path from "node:path";
import { fileExists, readJsonFile, resolveArchiveKind } from "../infra/archive.js";
import { writeFileFromPathWithinRoot } from "../infra/fs-safe.js";
import { resolveExistingInstallPath, withExtractedArchiveRoot } from "../infra/install-flow.js";
import {
resolveInstallModeOptions,
resolveTimedInstallModeOptions,
} from "../infra/install-mode-options.js";
import { installPackageDir } from "../infra/install-package-dir.js";
import {
resolveSafeInstallDir,
safeDirName,
safePathSegmentHashed,
unscopedPackageName,
} from "../infra/install-safe-path.js";
import {
type NpmIntegrityDrift,
type NpmSpecResolution,
resolveArchiveSourcePath,
} from "../infra/install-source-utils.js";
import {
ensureInstallTargetAvailable,
resolveCanonicalInstallTarget,
} from "../infra/install-target.js";
import {
finalizeNpmSpecArchiveInstall,
installFromNpmSpecArchiveWithInstaller,
} from "../infra/npm-pack-install.js";
import { validateRegistryNpmSpec } from "../infra/npm-registry-spec.js";
import { isPathInside } from "../security/scan-paths.js";
import { type NpmIntegrityDrift, type NpmSpecResolution } from "../infra/install-source-utils.js";
import { CONFIG_DIR, resolveUserPath } from "../utils.js";
import { resolveRuntimeServiceVersion } from "../version.js";
import { detectBundleManifestFormat, loadBundleManifest } from "./bundle-manifest.js";
import { scanBundleInstallSource, scanPackageInstallSource } from "./install-security-scan.js";
import {
getPackageManifestMetadata,
loadPluginManifest,
resolvePackageExtensionEntries,
type PackageManifest as PluginPackageManifest,
} from "./manifest.js";
import { checkMinHostVersion } from "./min-host-version.js";
let pluginInstallRuntimePromise: Promise<typeof import("./install.runtime.js")> | undefined;
async function loadPluginInstallRuntime() {
pluginInstallRuntimePromise ??= import("./install.runtime.js");
return pluginInstallRuntimePromise;
}
type PluginInstallLogger = {
info?: (message: string) => void;
@@ -281,10 +260,11 @@ async function installPluginDirectoryIntoExtensions(params: {
afterCopy?: (installedDir: string) => Promise<void>;
nameEncoder?: (pluginId: string) => string;
}): Promise<InstallPluginResult> {
const runtime = await loadPluginInstallRuntime();
const extensionsDir = params.extensionsDir
? resolveUserPath(params.extensionsDir)
: path.join(CONFIG_DIR, "extensions");
const targetDirResult = await resolveCanonicalInstallTarget({
const targetDirResult = await runtime.resolveCanonicalInstallTarget({
baseDir: extensionsDir,
id: params.pluginId,
invalidNameMessage: "invalid plugin name: path traversal detected",
@@ -295,7 +275,7 @@ async function installPluginDirectoryIntoExtensions(params: {
return { ok: false, error: targetDirResult.error };
}
const targetDir = targetDirResult.targetDir;
const availability = await ensureInstallTargetAvailable({
const availability = await runtime.ensureInstallTargetAvailable({
mode: params.mode,
targetDir,
alreadyExistsError: `plugin already exists: ${targetDir} (delete it first)`,
@@ -314,7 +294,7 @@ async function installPluginDirectoryIntoExtensions(params: {
});
}
const installRes = await installPackageDir({
const installRes = await runtime.installPackageDir({
sourceDir: params.sourceDir,
targetDir,
mode: params.mode,
@@ -363,13 +343,17 @@ async function installBundleFromSourceDir(
sourceDir: string;
} & PackageInstallCommonParams,
): Promise<InstallPluginResult | null> {
const bundleFormat = detectBundleManifestFormat(params.sourceDir);
const runtime = await loadPluginInstallRuntime();
const bundleFormat = runtime.detectBundleManifestFormat(params.sourceDir);
if (!bundleFormat) {
return null;
}
const { logger, timeoutMs, mode, dryRun } = resolveTimedInstallModeOptions(params, defaultLogger);
const manifestRes = loadBundleManifest({
const { logger, timeoutMs, mode, dryRun } = runtime.resolveTimedInstallModeOptions(
params,
defaultLogger,
);
const manifestRes = runtime.loadBundleManifest({
rootDir: params.sourceDir,
bundleFormat,
rejectHardlinks: true,
@@ -392,7 +376,7 @@ async function installBundleFromSourceDir(
}
try {
await scanBundleInstallSource({
await runtime.scanBundleInstallSource({
sourceDir: params.sourceDir,
pluginId,
logger,
@@ -446,13 +430,14 @@ async function installPluginFromSourceDir(
}
async function detectNativePackageInstallSource(packageDir: string): Promise<boolean> {
const runtime = await loadPluginInstallRuntime();
const manifestPath = path.join(packageDir, "package.json");
if (!(await fileExists(manifestPath))) {
if (!(await runtime.fileExists(manifestPath))) {
return false;
}
try {
const manifest = await readJsonFile<PackageManifest>(manifestPath);
const manifest = await runtime.readJsonFile<PackageManifest>(manifestPath);
return ensureOpenClawExtensions({ manifest }).ok;
} catch {
return false;
@@ -464,16 +449,20 @@ async function installPluginFromPackageDir(
packageDir: string;
} & PackageInstallCommonParams,
): Promise<InstallPluginResult> {
const { logger, timeoutMs, mode, dryRun } = resolveTimedInstallModeOptions(params, defaultLogger);
const runtime = await loadPluginInstallRuntime();
const { logger, timeoutMs, mode, dryRun } = runtime.resolveTimedInstallModeOptions(
params,
defaultLogger,
);
const manifestPath = path.join(params.packageDir, "package.json");
if (!(await fileExists(manifestPath))) {
if (!(await runtime.fileExists(manifestPath))) {
return { ok: false, error: "extracted package missing package.json" };
}
let manifest: PackageManifest;
try {
manifest = await readJsonFile<PackageManifest>(manifestPath);
manifest = await runtime.readJsonFile<PackageManifest>(manifestPath);
} catch (err) {
return { ok: false, error: `invalid package.json: ${String(err)}` };
}
@@ -497,7 +486,7 @@ async function installPluginFromPackageDir(
// This avoids a latent key-mismatch bug: if the manifest id (e.g. "memory-cognee")
// differs from the npm package name (e.g. "cognee-openclaw"), the plugin registry
// uses the manifest id as the authoritative key, so the config entry must match it.
const ocManifestResult = loadPluginManifest(params.packageDir);
const ocManifestResult = runtime.loadPluginManifest(params.packageDir);
const manifestPluginId =
ocManifestResult.ok && ocManifestResult.manifest.id
? ocManifestResult.manifest.id.trim()
@@ -529,9 +518,9 @@ async function installPluginFromPackageDir(
);
}
const packageMetadata = getPackageManifestMetadata(manifest);
const minHostVersionCheck = checkMinHostVersion({
currentVersion: resolveRuntimeServiceVersion(),
const packageMetadata = runtime.getPackageManifestMetadata(manifest);
const minHostVersionCheck = runtime.checkMinHostVersion({
currentVersion: runtime.resolveRuntimeServiceVersion(),
minHostVersion: packageMetadata?.install?.minHostVersion,
});
if (!minHostVersionCheck.ok) {
@@ -556,7 +545,7 @@ async function installPluginFromPackageDir(
};
}
try {
await scanPackageInstallSource({
await runtime.scanPackageInstallSource({
packageDir: params.packageDir,
pluginId,
logger,
@@ -587,11 +576,11 @@ async function installPluginFromPackageDir(
afterCopy: async (installedDir) => {
for (const entry of extensions) {
const resolvedEntry = path.resolve(installedDir, entry);
if (!isPathInside(installedDir, resolvedEntry)) {
if (!runtime.isPathInside(installedDir, resolvedEntry)) {
logger.warn?.(`extension entry escapes plugin directory: ${entry}`);
continue;
}
if (!(await fileExists(resolvedEntry))) {
if (!(await runtime.fileExists(resolvedEntry))) {
logger.warn?.(`extension entry not found: ${entry}`);
}
}
@@ -604,16 +593,17 @@ export async function installPluginFromArchive(
archivePath: string;
} & PackageInstallCommonParams,
): Promise<InstallPluginResult> {
const runtime = await loadPluginInstallRuntime();
const logger = params.logger ?? defaultLogger;
const timeoutMs = params.timeoutMs ?? 120_000;
const mode = params.mode ?? "install";
const archivePathResult = await resolveArchiveSourcePath(params.archivePath);
const archivePathResult = await runtime.resolveArchiveSourcePath(params.archivePath);
if (!archivePathResult.ok) {
return archivePathResult;
}
const archivePath = archivePathResult.path;
return await withExtractedArchiveRoot({
return await runtime.withExtractedArchiveRoot({
archivePath,
tempDirPrefix: "openclaw-plugin-",
timeoutMs,
@@ -639,8 +629,9 @@ export async function installPluginFromDir(
dirPath: string;
} & PackageInstallCommonParams,
): Promise<InstallPluginResult> {
const runtime = await loadPluginInstallRuntime();
const dirPath = resolveUserPath(params.dirPath);
if (!(await fileExists(dirPath))) {
if (!(await runtime.fileExists(dirPath))) {
return { ok: false, error: `directory not found: ${dirPath}` };
}
const stat = await fs.stat(dirPath);
@@ -661,10 +652,11 @@ export async function installPluginFromFile(params: {
mode?: "install" | "update";
dryRun?: boolean;
}): Promise<InstallPluginResult> {
const { logger, mode, dryRun } = resolveInstallModeOptions(params, defaultLogger);
const runtime = await loadPluginInstallRuntime();
const { logger, mode, dryRun } = runtime.resolveInstallModeOptions(params, defaultLogger);
const filePath = resolveUserPath(params.filePath);
if (!(await fileExists(filePath))) {
if (!(await runtime.fileExists(filePath))) {
return { ok: false, error: `file not found: ${filePath}` };
}
@@ -681,7 +673,7 @@ export async function installPluginFromFile(params: {
}
const targetFile = path.join(extensionsDir, `${safeFileName(pluginId)}${path.extname(filePath)}`);
const availability = await ensureInstallTargetAvailable({
const availability = await runtime.ensureInstallTargetAvailable({
mode,
targetDir: targetFile,
alreadyExistsError: `plugin already exists: ${targetFile} (delete it first)`,
@@ -696,7 +688,7 @@ export async function installPluginFromFile(params: {
logger.info?.(`Installing to ${targetFile}`);
try {
await writeFileFromPathWithinRoot({
await runtime.writeFileFromPathWithinRoot({
rootDir: extensionsDir,
relativePath: path.basename(targetFile),
sourcePath: filePath,
@@ -719,10 +711,14 @@ export async function installPluginFromNpmSpec(params: {
expectedIntegrity?: string;
onIntegrityDrift?: (params: PluginNpmIntegrityDriftParams) => boolean | Promise<boolean>;
}): Promise<InstallPluginResult> {
const { logger, timeoutMs, mode, dryRun } = resolveTimedInstallModeOptions(params, defaultLogger);
const runtime = await loadPluginInstallRuntime();
const { logger, timeoutMs, mode, dryRun } = runtime.resolveTimedInstallModeOptions(
params,
defaultLogger,
);
const expectedPluginId = params.expectedPluginId;
const spec = params.spec.trim();
const specError = validateRegistryNpmSpec(spec);
const specError = runtime.validateRegistryNpmSpec(spec);
if (specError) {
return {
ok: false,
@@ -732,7 +728,7 @@ export async function installPluginFromNpmSpec(params: {
}
logger.info?.(`Downloading ${spec}`);
const flowResult = await installFromNpmSpecArchiveWithInstaller({
const flowResult = await runtime.installFromNpmSpecArchiveWithInstaller({
tempDirPrefix: "openclaw-npm-pack-",
spec,
timeoutMs,
@@ -751,7 +747,7 @@ export async function installPluginFromNpmSpec(params: {
expectedPluginId,
},
});
const finalized = finalizeNpmSpecArchiveInstall(flowResult);
const finalized = runtime.finalizeNpmSpecArchiveInstall(flowResult);
if (!finalized.ok && isNpmPackageNotFoundMessage(finalized.error)) {
return {
ok: false,
@@ -767,7 +763,8 @@ export async function installPluginFromPath(
path: string;
} & PackageInstallCommonParams,
): Promise<InstallPluginResult> {
const pathResult = await resolveExistingInstallPath(params.path);
const runtime = await loadPluginInstallRuntime();
const pathResult = await runtime.resolveExistingInstallPath(params.path);
if (!pathResult.ok) {
return pathResult;
}
@@ -781,7 +778,7 @@ export async function installPluginFromPath(
});
}
const archiveKind = resolveArchiveKind(resolved);
const archiveKind = runtime.resolveArchiveKind(resolved);
if (archiveKind) {
return await installPluginFromArchive({
archivePath: resolved,