fix(plugins): stop repeated manifest realpath scans (#115783)

Fixes #115524 by keeping path canonicalization scoped to each manifest and persisted registry build.

Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
Peter Steinberger
2026-07-29 02:10:49 -07:00
committed by GitHub
parent a27e5ee607
commit 05516e7a5f
4 changed files with 112 additions and 28 deletions
+24
View File
@@ -1906,6 +1906,30 @@ describe("loadPluginManifestRegistry", () => {
);
});
it("resolves a manifest provider catalog source only once per registry build", () => {
const dir = makeTempDir();
const providerDiscoverySource = path.join(dir, "provider-discovery.js");
writeManifest(dir, {
id: "cached-provider",
providers: ["cached-provider"],
providerCatalogEntry: "./provider-discovery.js",
configSchema: { type: "object" },
});
fs.writeFileSync(providerDiscoverySource, "export default {};\n", "utf8");
const realpathSpy = vi.spyOn(fs, "realpathSync");
const registry = loadSingleCandidateRegistry({
idHint: "cached-provider",
rootDir: dir,
origin: "bundled",
});
expect(registry.plugins[0]?.providerDiscoverySource).toBe(providerDiscoverySource);
expect(
realpathSpy.mock.calls.filter(([filePath]) => filePath === providerDiscoverySource),
).toHaveLength(1);
});
it("ignores provider catalog entries outside the plugin root", () => {
const root = makeTempDir();
const pluginDir = path.join(root, "plugin");
+9 -2
View File
@@ -92,6 +92,7 @@ function isPluginRootPath(params: {
rootPath: string;
targetPath: string;
rootRealPath: string;
realpathCache: Map<string, string>;
rejectHardlinks?: boolean;
targetMustExist?: boolean;
}): boolean {
@@ -100,7 +101,7 @@ function isPluginRootPath(params: {
if (!isPathInside(resolvedRootPath, resolvedTargetPath)) {
return false;
}
const targetRealPath = safeRealpathSync(resolvedTargetPath);
const targetRealPath = safeRealpathSync(resolvedTargetPath, params.realpathCache);
if (!targetRealPath) {
return params.targetMustExist !== true;
}
@@ -124,6 +125,7 @@ function resolveManifestPluginSourcePath(params: {
entry: string;
rejectHardlinks: boolean;
diagnostics: PluginDiagnostic[];
realpathCache: Map<string, string>;
}): string | undefined {
const pushDiagnostic = () => {
params.diagnostics.push({
@@ -140,13 +142,14 @@ function resolveManifestPluginSourcePath(params: {
}
const rootPath = path.resolve(params.rootDir);
const rootRealPath = safeRealpathSync(rootPath) ?? rootPath;
const rootRealPath = safeRealpathSync(rootPath, params.realpathCache) ?? rootPath;
const sourcePath = path.resolve(rootPath, params.entry);
if (
!isPluginRootPath({
rootPath,
targetPath: sourcePath,
rootRealPath,
realpathCache: params.realpathCache,
rejectHardlinks: params.rejectHardlinks,
targetMustExist: fs.existsSync(sourcePath),
})
@@ -161,6 +164,7 @@ function resolveManifestPluginSourcePath(params: {
rootPath,
targetPath: resolvedSourcePath,
rootRealPath,
realpathCache: params.realpathCache,
rejectHardlinks: params.rejectHardlinks,
targetMustExist: fs.existsSync(resolvedSourcePath),
})
@@ -512,6 +516,7 @@ function buildRecord(params: {
manifestPath: string;
diagnostics: PluginDiagnostic[];
rejectHardlinks: boolean;
realpathCache: Map<string, string>;
schemaCacheKey?: string;
configSchema?: Record<string, unknown>;
bundledChannelConfigCollector?: BundledChannelConfigCollector;
@@ -577,6 +582,7 @@ function buildRecord(params: {
entry: providerSourceEntry.entry,
rejectHardlinks: params.rejectHardlinks,
diagnostics: params.diagnostics,
realpathCache: params.realpathCache,
})
: undefined,
modelSupport: params.manifest.modelSupport,
@@ -1160,6 +1166,7 @@ export function loadPluginManifestRegistry(
manifestPath: manifestRes.manifestPath,
diagnostics,
rejectHardlinks,
realpathCache,
schemaCacheKey,
configSchema,
trustedOfficialInstall: isTrustedOfficialPluginInstall({
@@ -1043,6 +1043,35 @@ describe("loadPluginRegistrySnapshotWithMetadata", () => {
expect(result.snapshot.plugins.map((plugin) => plugin.pluginId)).toEqual(["codex", "whatsapp"]);
});
it("resolves a persisted bundled root only once per registry load", () => {
const tempRoot = makeTempDir();
const packageRoot = path.join(tempRoot, "openclaw");
const bundledRoot = path.join(packageRoot, "dist", "extensions");
const stateDir = path.join(tempRoot, "state");
const env = {
OPENCLAW_BUNDLED_PLUGINS_DIR: bundledRoot,
OPENCLAW_STATE_DIR: stateDir,
OPENCLAW_VERSION: "2026.4.26",
VITEST: "true",
};
const pluginIds = ["bundled-one", "bundled-two", "bundled-three", "bundled-four"];
for (const pluginId of pluginIds) {
writeBundledPlugin(path.join(bundledRoot, pluginId), pluginId, "index.js");
}
const index = loadInstalledPluginIndex({ config: {}, env, stateDir });
writePersistedInstalledPluginIndexSync(index, { stateDir });
const realpathSpy = vi.spyOn(fs, "realpathSync");
const result = loadPluginRegistrySnapshotWithMetadata({ config: {}, env, stateDir });
expect(result.source).toBe("persisted");
expect(result.snapshot.plugins.map((plugin) => plugin.pluginId).toSorted()).toEqual(
pluginIds.toSorted(),
);
expect(realpathSpy.mock.calls.filter(([filePath]) => filePath === bundledRoot)).toHaveLength(1);
});
it("treats a persisted source bundled root as stale once its built peer appears", () => {
const tempRoot = makeTempDir();
const packageRoot = path.join(tempRoot, "openclaw");
+50 -26
View File
@@ -36,6 +36,7 @@ import {
} from "./installed-plugin-index.js";
import { loadPluginManifestRegistry } from "./manifest-registry.js";
import { getPackageManifestMetadata, type PackageManifest } from "./manifest.js";
import { safeRealpathSync } from "./path-safety.js";
import { registerPluginMetadataProcessMemoLifecycleClear } from "./plugin-metadata-lifecycle.js";
import type { PluginRegistrySnapshotSource } from "./plugin-registry-snapshot.types.js";
@@ -216,6 +217,7 @@ function hasMismatchedPersistedConfigPathPlugins(
index: InstalledPluginIndex,
params: LoadPluginRegistryParams,
env: NodeJS.ProcessEnv,
realpathCache: Map<string, string>,
): boolean {
const loadPaths = normalizePluginsConfig(params.config?.plugins).loadPaths;
const discovery = discoverConfiguredPluginLoadPaths({
@@ -230,22 +232,18 @@ function hasMismatchedPersistedConfigPathPlugins(
candidates: discovery.candidates,
diagnostics: discovery.diagnostics,
installRecords: extractPluginInstallRecordsFromInstalledPluginIndex(index),
}).plugins.map((plugin) => resolveComparablePath(plugin.rootDir));
}).plugins.map((plugin) => resolveComparablePath(plugin.rootDir, realpathCache));
const persistedRoots = index.plugins
.filter((plugin) => plugin.origin === "config")
.map((plugin) => resolveComparablePath(plugin.rootDir));
.map((plugin) => resolveComparablePath(plugin.rootDir, realpathCache));
if (configuredRoots.length !== persistedRoots.length) {
return true;
}
return configuredRoots.some((rootDir, position) => rootDir !== persistedRoots[position]);
}
function resolveComparablePath(filePath: string): string {
try {
return fs.realpathSync(filePath);
} catch {
return path.resolve(filePath);
}
function resolveComparablePath(filePath: string, realpathCache: Map<string, string>): string {
return safeRealpathSync(filePath, realpathCache) ?? path.resolve(filePath);
}
function isRelativePathInsideOrEqual(relativePath: string): boolean {
@@ -257,10 +255,14 @@ function isRelativePathInsideOrEqual(relativePath: string): boolean {
);
}
function isPathInsideOrEqual(childPath: string, parentPath: string): boolean {
function isPathInsideOrEqual(
childPath: string,
parentPath: string,
realpathCache: Map<string, string>,
): boolean {
const relative = path.relative(
resolveComparablePath(parentPath),
resolveComparablePath(childPath),
resolveComparablePath(parentPath, realpathCache),
resolveComparablePath(childPath, realpathCache),
);
return isRelativePathInsideOrEqual(relative);
}
@@ -268,6 +270,7 @@ function isPathInsideOrEqual(childPath: string, parentPath: string): boolean {
function hasMismatchedPersistedBundledPluginRoot(
index: InstalledPluginIndex,
env: NodeJS.ProcessEnv,
realpathCache: Map<string, string>,
): boolean {
const bundledPluginsDir = resolveBundledPluginsDir(env);
if (!bundledPluginsDir) {
@@ -282,7 +285,12 @@ function hasMismatchedPersistedBundledPluginRoot(
bundledRoot: bundledPluginsDir,
env,
});
return !isAllowedPersistedBundledPluginRoot(plugin, bundledPluginsDir, sourceOverlayDirs);
return !isAllowedPersistedBundledPluginRoot(
plugin,
bundledPluginsDir,
sourceOverlayDirs,
realpathCache,
);
});
}
@@ -290,28 +298,33 @@ function isAllowedPersistedBundledPluginRoot(
plugin: InstalledPluginIndexRecord,
bundledPluginsDir: string,
sourceOverlayDirs: readonly string[],
realpathCache: Map<string, string>,
): boolean {
const pluginRootDir = plugin.rootDir;
const legacyRoot = buildLegacyBundledRootPath(bundledPluginsDir);
if (isPathInsideOrEqual(pluginRootDir, bundledPluginsDir)) {
if (isPathInsideOrEqual(pluginRootDir, bundledPluginsDir, realpathCache)) {
if (!legacyRoot || !isSourceCheckoutBundledPluginRoot(legacyRoot)) {
return true;
}
const relativePluginRoot = path.relative(
resolveComparablePath(bundledPluginsDir),
resolveComparablePath(pluginRootDir),
resolveComparablePath(bundledPluginsDir, realpathCache),
resolveComparablePath(pluginRootDir, realpathCache),
);
return !sourcePluginOptsOutOfBundledDist(path.join(legacyRoot, relativePluginRoot));
}
if (sourceOverlayDirs.some((overlayDir) => isPathInsideOrEqual(pluginRootDir, overlayDir))) {
if (
sourceOverlayDirs.some((overlayDir) =>
isPathInsideOrEqual(pluginRootDir, overlayDir, realpathCache),
)
) {
return true;
}
if (!legacyRoot || !isSourceCheckoutBundledPluginRoot(legacyRoot)) {
return false;
}
const relativePluginRoot = path.relative(
resolveComparablePath(legacyRoot),
resolveComparablePath(pluginRootDir),
resolveComparablePath(legacyRoot, realpathCache),
resolveComparablePath(pluginRootDir, realpathCache),
);
if (!isRelativePathInsideOrEqual(relativePluginRoot)) {
return false;
@@ -353,7 +366,10 @@ function hashExistingFile(filePath: string): string | null {
}
}
function resolveRecordPackageJsonPath(plugin: InstalledPluginIndexRecord): string | null {
function resolveRecordPackageJsonPath(
plugin: InstalledPluginIndexRecord,
realpathCache: Map<string, string>,
): string | null {
const packageJsonPath = plugin.packageJson?.path;
if (!packageJsonPath) {
return null;
@@ -365,8 +381,8 @@ function resolveRecordPackageJsonPath(plugin: InstalledPluginIndexRecord): strin
return null;
}
const realRelative = path.relative(
resolveComparablePath(rootDir),
resolveComparablePath(resolved),
resolveComparablePath(rootDir, realpathCache),
resolveComparablePath(resolved, realpathCache),
);
return isRelativePathInsideOrEqual(realRelative) ? resolved : null;
}
@@ -384,7 +400,10 @@ function hasStalePersistedPluginDiagnostics(index: InstalledPluginIndex): boolea
});
}
function hasStalePersistedPluginMetadata(index: InstalledPluginIndex): boolean {
function hasStalePersistedPluginMetadata(
index: InstalledPluginIndex,
realpathCache: Map<string, string>,
): boolean {
return index.plugins.some((plugin) => {
if (!hasOptionalMissingPluginManifestFile(plugin)) {
const manifestSignatureMatches = fileSignatureMatches(
@@ -398,7 +417,7 @@ function hasStalePersistedPluginMetadata(index: InstalledPluginIndex): boolean {
}
}
}
const packageJsonPath = resolveRecordPackageJsonPath(plugin);
const packageJsonPath = resolveRecordPackageJsonPath(plugin, realpathCache);
if (!plugin.packageJson?.hash) {
return false;
}
@@ -476,6 +495,9 @@ export function loadPluginRegistrySnapshotWithMetadata(
if (memo) {
return memo;
}
// Bound canonical paths to this registry build; lifecycle changes must
// never reuse security-sensitive symlink or plugin-root resolutions.
const realpathCache = new Map<string, string>();
const diagnostics: PluginRegistrySnapshotDiagnostic[] = [];
const disabledByCaller = params.preferPersisted === false;
const persistedReadsEnabled = !disabledByCaller;
@@ -501,14 +523,16 @@ export function loadPluginRegistrySnapshotWithMetadata(
message:
"Persisted plugin registry points at missing plugin files; using derived plugin index. Run `openclaw plugins registry --refresh` to update the persisted registry.",
});
} else if (hasMismatchedPersistedBundledPluginRoot(persistedIndex, env)) {
} else if (hasMismatchedPersistedBundledPluginRoot(persistedIndex, env, realpathCache)) {
diagnostics.push({
level: "warn",
code: "persisted-registry-stale-source",
message:
"Persisted plugin registry points at a different bundled plugin tree; using derived plugin index. Run `openclaw plugins registry --refresh` to update the persisted registry.",
});
} else if (hasMismatchedPersistedConfigPathPlugins(persistedIndex, params, env)) {
} else if (
hasMismatchedPersistedConfigPathPlugins(persistedIndex, params, env, realpathCache)
) {
diagnostics.push({
level: "warn",
code: "persisted-registry-stale-source",
@@ -529,7 +553,7 @@ export function loadPluginRegistrySnapshotWithMetadata(
message:
"Persisted plugin registry is missing config-path startup metadata; using derived plugin index. Run `openclaw plugins registry --refresh` to update the persisted registry.",
});
} else if (hasStalePersistedPluginMetadata(persistedIndex)) {
} else if (hasStalePersistedPluginMetadata(persistedIndex, realpathCache)) {
diagnostics.push({
level: "warn",
code: "persisted-registry-stale-source",