mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-21 08:25:00 +00:00
Plugins: fix env-aware root resolution and caching (#44046)
Merged via squash.
Prepared head SHA: 6e8852a188
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
This commit is contained in:
committed by
GitHub
parent
688e3f0863
commit
e6897c800b
@@ -3,6 +3,7 @@ import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { createJiti } from "jiti";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import type { PluginInstallRecord } from "../config/types.plugins.js";
|
||||
import type { GatewayRequestHandler } from "../gateway/server-methods/types.js";
|
||||
import { openBoundaryFileSync } from "../infra/boundary-file-read.js";
|
||||
import { resolveOpenClawPackageRootSync } from "../infra/openclaw-root.js";
|
||||
@@ -21,6 +22,7 @@ import { initializeGlobalHookRunner } from "./hook-runner-global.js";
|
||||
import { loadPluginManifestRegistry } from "./manifest-registry.js";
|
||||
import { isPathInside, safeStatSync } from "./path-safety.js";
|
||||
import { createPluginRegistry, type PluginRecord, type PluginRegistry } from "./registry.js";
|
||||
import { resolvePluginCacheInputs } from "./roots.js";
|
||||
import { setActivePluginRegistry } from "./runtime.js";
|
||||
import { createPluginRuntime, type CreatePluginRuntimeOptions } from "./runtime/index.js";
|
||||
import type { PluginRuntime } from "./runtime/types.js";
|
||||
@@ -37,6 +39,9 @@ export type PluginLoadResult = PluginRegistry;
|
||||
export type PluginLoadOptions = {
|
||||
config?: OpenClawConfig;
|
||||
workspaceDir?: string;
|
||||
// Allows callers to resolve plugin roots and load paths against an explicit env
|
||||
// instead of the process-global environment.
|
||||
env?: NodeJS.ProcessEnv;
|
||||
logger?: PluginLogger;
|
||||
coreGatewayHandlers?: Record<string, GatewayRequestHandler>;
|
||||
runtimeOptions?: CreatePluginRuntimeOptions;
|
||||
@@ -44,8 +49,13 @@ export type PluginLoadOptions = {
|
||||
mode?: "full" | "validate";
|
||||
};
|
||||
|
||||
const MAX_PLUGIN_REGISTRY_CACHE_ENTRIES = 32;
|
||||
const registryCache = new Map<string, PluginRegistry>();
|
||||
|
||||
export function clearPluginLoaderCache(): void {
|
||||
registryCache.clear();
|
||||
}
|
||||
|
||||
const defaultLogger = () => createSubsystemLogger("plugins");
|
||||
|
||||
type PluginSdkAliasCandidateKind = "dist" | "src";
|
||||
@@ -162,14 +172,66 @@ export const __testing = {
|
||||
listPluginSdkExportedSubpaths,
|
||||
resolvePluginSdkAliasCandidateOrder,
|
||||
resolvePluginSdkAliasFile,
|
||||
maxPluginRegistryCacheEntries: MAX_PLUGIN_REGISTRY_CACHE_ENTRIES,
|
||||
};
|
||||
|
||||
function getCachedPluginRegistry(cacheKey: string): PluginRegistry | undefined {
|
||||
const cached = registryCache.get(cacheKey);
|
||||
if (!cached) {
|
||||
return undefined;
|
||||
}
|
||||
// Refresh insertion order so frequently reused registries survive eviction.
|
||||
registryCache.delete(cacheKey);
|
||||
registryCache.set(cacheKey, cached);
|
||||
return cached;
|
||||
}
|
||||
|
||||
function setCachedPluginRegistry(cacheKey: string, registry: PluginRegistry): void {
|
||||
if (registryCache.has(cacheKey)) {
|
||||
registryCache.delete(cacheKey);
|
||||
}
|
||||
registryCache.set(cacheKey, registry);
|
||||
while (registryCache.size > MAX_PLUGIN_REGISTRY_CACHE_ENTRIES) {
|
||||
const oldestKey = registryCache.keys().next().value;
|
||||
if (!oldestKey) {
|
||||
break;
|
||||
}
|
||||
registryCache.delete(oldestKey);
|
||||
}
|
||||
}
|
||||
|
||||
function buildCacheKey(params: {
|
||||
workspaceDir?: string;
|
||||
plugins: NormalizedPluginsConfig;
|
||||
installs?: Record<string, PluginInstallRecord>;
|
||||
env: NodeJS.ProcessEnv;
|
||||
}): string {
|
||||
const workspaceKey = params.workspaceDir ? resolveUserPath(params.workspaceDir) : "";
|
||||
return `${workspaceKey}::${JSON.stringify(params.plugins)}`;
|
||||
const { roots, loadPaths } = resolvePluginCacheInputs({
|
||||
workspaceDir: params.workspaceDir,
|
||||
loadPaths: params.plugins.loadPaths,
|
||||
env: params.env,
|
||||
});
|
||||
const installs = Object.fromEntries(
|
||||
Object.entries(params.installs ?? {}).map(([pluginId, install]) => [
|
||||
pluginId,
|
||||
{
|
||||
...install,
|
||||
installPath:
|
||||
typeof install.installPath === "string"
|
||||
? resolveUserPath(install.installPath, params.env)
|
||||
: install.installPath,
|
||||
sourcePath:
|
||||
typeof install.sourcePath === "string"
|
||||
? resolveUserPath(install.sourcePath, params.env)
|
||||
: install.sourcePath,
|
||||
},
|
||||
]),
|
||||
);
|
||||
return `${roots.workspace ?? ""}::${roots.global ?? ""}::${roots.stock ?? ""}::${JSON.stringify({
|
||||
...params.plugins,
|
||||
installs,
|
||||
loadPaths,
|
||||
})}`;
|
||||
}
|
||||
|
||||
function validatePluginConfig(params: {
|
||||
@@ -306,12 +368,16 @@ function createPathMatcher(): PathMatcher {
|
||||
return { exact: new Set<string>(), dirs: [] };
|
||||
}
|
||||
|
||||
function addPathToMatcher(matcher: PathMatcher, rawPath: string): void {
|
||||
function addPathToMatcher(
|
||||
matcher: PathMatcher,
|
||||
rawPath: string,
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
): void {
|
||||
const trimmed = rawPath.trim();
|
||||
if (!trimmed) {
|
||||
return;
|
||||
}
|
||||
const resolved = resolveUserPath(trimmed);
|
||||
const resolved = resolveUserPath(trimmed, env);
|
||||
if (!resolved) {
|
||||
return;
|
||||
}
|
||||
@@ -336,10 +402,11 @@ function matchesPathMatcher(matcher: PathMatcher, sourcePath: string): boolean {
|
||||
function buildProvenanceIndex(params: {
|
||||
config: OpenClawConfig;
|
||||
normalizedLoadPaths: string[];
|
||||
env: NodeJS.ProcessEnv;
|
||||
}): PluginProvenanceIndex {
|
||||
const loadPathMatcher = createPathMatcher();
|
||||
for (const loadPath of params.normalizedLoadPaths) {
|
||||
addPathToMatcher(loadPathMatcher, loadPath);
|
||||
addPathToMatcher(loadPathMatcher, loadPath, params.env);
|
||||
}
|
||||
|
||||
const installRules = new Map<string, InstallTrackingRule>();
|
||||
@@ -356,7 +423,7 @@ function buildProvenanceIndex(params: {
|
||||
rule.trackedWithoutPaths = true;
|
||||
} else {
|
||||
for (const trackedPath of trackedPaths) {
|
||||
addPathToMatcher(rule.matcher, trackedPath);
|
||||
addPathToMatcher(rule.matcher, trackedPath, params.env);
|
||||
}
|
||||
}
|
||||
installRules.set(pluginId, rule);
|
||||
@@ -369,8 +436,9 @@ function isTrackedByProvenance(params: {
|
||||
pluginId: string;
|
||||
source: string;
|
||||
index: PluginProvenanceIndex;
|
||||
env: NodeJS.ProcessEnv;
|
||||
}): boolean {
|
||||
const sourcePath = resolveUserPath(params.source);
|
||||
const sourcePath = resolveUserPath(params.source, params.env);
|
||||
const installRule = params.index.installRules.get(params.pluginId);
|
||||
if (installRule) {
|
||||
if (installRule.trackedWithoutPaths) {
|
||||
@@ -413,6 +481,7 @@ function warnAboutUntrackedLoadedPlugins(params: {
|
||||
registry: PluginRegistry;
|
||||
provenance: PluginProvenanceIndex;
|
||||
logger: PluginLogger;
|
||||
env: NodeJS.ProcessEnv;
|
||||
}) {
|
||||
for (const plugin of params.registry.plugins) {
|
||||
if (plugin.status !== "loaded" || plugin.origin === "bundled") {
|
||||
@@ -423,6 +492,7 @@ function warnAboutUntrackedLoadedPlugins(params: {
|
||||
pluginId: plugin.id,
|
||||
source: plugin.source,
|
||||
index: params.provenance,
|
||||
env: params.env,
|
||||
})
|
||||
) {
|
||||
continue;
|
||||
@@ -445,19 +515,22 @@ function activatePluginRegistry(registry: PluginRegistry, cacheKey: string): voi
|
||||
}
|
||||
|
||||
export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegistry {
|
||||
const env = options.env ?? process.env;
|
||||
// Test env: default-disable plugins unless explicitly configured.
|
||||
// This keeps unit/gateway suites fast and avoids loading heavyweight plugin deps by accident.
|
||||
const cfg = applyTestPluginDefaults(options.config ?? {}, process.env);
|
||||
const cfg = applyTestPluginDefaults(options.config ?? {}, env);
|
||||
const logger = options.logger ?? defaultLogger();
|
||||
const validateOnly = options.mode === "validate";
|
||||
const normalized = normalizePluginsConfig(cfg.plugins);
|
||||
const cacheKey = buildCacheKey({
|
||||
workspaceDir: options.workspaceDir,
|
||||
plugins: normalized,
|
||||
installs: cfg.plugins?.installs,
|
||||
env,
|
||||
});
|
||||
const cacheEnabled = options.cache !== false;
|
||||
if (cacheEnabled) {
|
||||
const cached = registryCache.get(cacheKey);
|
||||
const cached = getCachedPluginRegistry(cacheKey);
|
||||
if (cached) {
|
||||
activatePluginRegistry(cached, cacheKey);
|
||||
return cached;
|
||||
@@ -510,11 +583,13 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
|
||||
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,
|
||||
});
|
||||
@@ -532,6 +607,7 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
|
||||
const provenance = buildProvenanceIndex({
|
||||
config: cfg,
|
||||
normalizedLoadPaths: normalized.loadPaths,
|
||||
env,
|
||||
});
|
||||
|
||||
// Lazy: avoid creating the Jiti loader when all plugins are disabled (common in unit tests).
|
||||
@@ -810,10 +886,11 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
|
||||
registry,
|
||||
provenance,
|
||||
logger,
|
||||
env,
|
||||
});
|
||||
|
||||
if (cacheEnabled) {
|
||||
registryCache.set(cacheKey, registry);
|
||||
setCachedPluginRegistry(cacheKey, registry);
|
||||
}
|
||||
activatePluginRegistry(registry, cacheKey);
|
||||
return registry;
|
||||
|
||||
Reference in New Issue
Block a user