Skip to content

Commit e9d0132

Browse files
committed
fix: isolate dev source plugin aliases
1 parent ae800e1 commit e9d0132

8 files changed

Lines changed: 398 additions & 18 deletions

src/plugins/bundled-capability-runtime.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
} from "./bundled-compat.js";
99
import { resolveBundledPluginRepoEntryPath } from "./bundled-plugin-metadata.js";
1010
import { createCapturedPluginRegistration } from "./captured-registration.js";
11+
import { resolveOpenClawDevSourceRoot } from "./dev-source-root.js";
1112
import { discoverOpenClawPlugins, type PluginDiscoveryResult } from "./discovery.js";
1213
import type { PluginLoadOptions } from "./loader.js";
1314
import { loadPluginManifestRegistry } from "./manifest-registry.js";
@@ -201,6 +202,7 @@ export function loadBundledCapabilityRuntimeRegistry(params: {
201202
discovery?: PluginDiscoveryResult;
202203
}) {
203204
const env = params.env ?? process.env;
205+
const devSourceRoot = resolveOpenClawDevSourceRoot(env);
204206
const pluginIds = new Set(params.pluginIds);
205207
const registry = createEmptyPluginRegistry();
206208
const moduleLoaders: PluginModuleLoaderCache = createPluginModuleLoaderCache();
@@ -219,6 +221,7 @@ export function loadBundledCapabilityRuntimeRegistry(params: {
219221
process.argv[1],
220222
import.meta.url,
221223
params.pluginSdkResolution,
224+
devSourceRoot,
222225
),
223226
pluginSdkResolution: params.pluginSdkResolution,
224227
env,
@@ -228,6 +231,7 @@ export function loadBundledCapabilityRuntimeRegistry(params: {
228231
cache: moduleLoaders,
229232
modulePath,
230233
importerUrl: import.meta.url,
234+
devSourceRoot,
231235
loaderFilename: import.meta.url,
232236
...(aliasMap ? { aliasMap } : {}),
233237
pluginSdkResolution: params.pluginSdkResolution,

src/plugins/loader.runtime-registry.test.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import fs from "node:fs";
2+
import os from "node:os";
3+
import path from "node:path";
14
import { afterEach, describe, expect, it } from "vitest";
25
import { getCompactionProvider, registerCompactionProvider } from "./compaction-provider.js";
36
import { getEmbeddingProvider, registerEmbeddingProvider } from "./embedding-providers.js";
@@ -84,6 +87,14 @@ function requireMemoryEmbeddingProvider(providerId: string) {
8487
return provider;
8588
}
8689

90+
function makeOpenClawDevSourceRoot(): string {
91+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-loader-dev-source-"));
92+
fs.writeFileSync(path.join(root, "package.json"), JSON.stringify({ name: "openclaw" }), "utf-8");
93+
fs.mkdirSync(path.join(root, "src"), { recursive: true });
94+
fs.mkdirSync(path.join(root, "extensions"), { recursive: true });
95+
return root;
96+
}
97+
8798
describe("getCompatibleActivePluginRegistry", () => {
8899
it("reuses the active registry only when the load context cache key matches", () => {
89100
const registry = createEmptyPluginRegistry();
@@ -309,6 +320,31 @@ describe("getCompatibleActivePluginRegistry", () => {
309320
expect(cacheKey).not.toContain("telegram configured");
310321
});
311322

323+
it("separates dev source root precedence in the loader cache key", () => {
324+
const devSourceRoot = makeOpenClawDevSourceRoot();
325+
try {
326+
const baseOptions = {
327+
config: {
328+
plugins: {
329+
allow: ["demo"],
330+
load: { paths: ["/tmp/demo.js"] },
331+
},
332+
},
333+
env: { ...process.env, OPENCLAW_DEV_SOURCE_ROOT: undefined },
334+
};
335+
336+
const base = testing.resolvePluginLoadCacheContext(baseOptions).cacheKey;
337+
const dev = testing.resolvePluginLoadCacheContext({
338+
...baseOptions,
339+
env: { ...process.env, OPENCLAW_DEV_SOURCE_ROOT: devSourceRoot },
340+
}).cacheKey;
341+
342+
expect(dev).not.toBe(base);
343+
} finally {
344+
fs.rmSync(devSourceRoot, { recursive: true, force: true });
345+
}
346+
});
347+
312348
it("separates raw env substitution mode in the loader cache key", () => {
313349
const baseOptions = {
314350
config: {

src/plugins/loader.ts

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ import {
5252
type NormalizedPluginsConfig,
5353
} from "./config-state.js";
5454
import { isPluginEnabledByDefaultForPlatform } from "./default-enablement.js";
55+
import { resolveOpenClawDevSourceRoot } from "./dev-source-root.js";
5556
import {
5657
discoverOpenClawPlugins,
5758
type PluginCandidate,
@@ -545,25 +546,31 @@ function runPluginRegisterSync(
545546
}
546547
}
547548

548-
function createPluginModuleLoader(options: Pick<PluginLoadOptions, "pluginSdkResolution">) {
549+
function createPluginModuleLoader(options: {
550+
devSourceRoot?: string | null;
551+
pluginSdkResolution?: PluginSdkResolutionPreference;
552+
}) {
549553
const moduleLoaders: PluginModuleLoaderCache = createPluginModuleLoaderCache();
550554
const createLoaderForModule = (modulePath: string) => {
551555
installOpenClawPluginSdkNativeResolver({
552556
argv1: process.argv[1],
553557
moduleUrl: import.meta.url,
554558
pluginModulePath: modulePath,
559+
devSourceRoot: options.devSourceRoot,
555560
pluginSdkResolution: options.pluginSdkResolution,
556561
});
557562
return getCachedPluginModuleLoader({
558563
cache: moduleLoaders,
559564
modulePath,
560565
importerUrl: import.meta.url,
561566
loaderFilename: modulePath,
567+
devSourceRoot: options.devSourceRoot,
562568
aliasMap: buildPluginLoaderAliasMap(
563569
modulePath,
564570
process.argv[1],
565571
import.meta.url,
566572
options.pluginSdkResolution,
573+
options.devSourceRoot,
567574
),
568575
pluginSdkResolution: options.pluginSdkResolution,
569576
});
@@ -906,6 +913,7 @@ function buildCacheKey(params: {
906913
activationMetadataKey?: string;
907914
installs?: Record<string, PluginInstallRecord>;
908915
env: NodeJS.ProcessEnv;
916+
devSourceRoot?: string | null;
909917
onlyPluginIds?: string[];
910918
includeSetupOnlyChannelPlugins?: boolean;
911919
forceSetupOnlyChannelPlugins?: boolean;
@@ -927,6 +935,7 @@ function buildCacheKey(params: {
927935
});
928936
const { roots, loadPaths } = discoveryContext;
929937
const bundledPackage = resolveBundledPackageCacheIdentity(roots.stock);
938+
const devSourceRoot = params.devSourceRoot ?? "";
930939
const installs = Object.fromEntries(
931940
Object.entries(params.installs ?? {}).map(([pluginId, install]) => [
932941
pluginId,
@@ -964,6 +973,7 @@ function buildCacheKey(params: {
964973
const activationMode = params.activate === false ? "snapshot" : "active";
965974
return `${roots.workspace ?? ""}::${roots.global ?? ""}::${roots.stock ?? ""}::${JSON.stringify({
966975
bundledPackage,
976+
devSourceRoot,
967977
discoveryFingerprint: fingerprintPluginDiscoveryContext(discoveryContext),
968978
...params.plugins,
969979
installs,
@@ -1280,6 +1290,7 @@ function resolvePluginLoadCacheContext(options: PluginLoadOptions = {}) {
12801290
...(options.installRecords ?? loadInstalledPluginIndexInstallRecordsSync({ env })),
12811291
...cfg.plugins?.installs,
12821292
};
1293+
const devSourceRoot = resolveOpenClawDevSourceRoot(env);
12831294
const cacheKey = buildCacheKey({
12841295
workspaceDir: options.workspaceDir,
12851296
plugins: shouldResolveRawConfigEnvVars
@@ -1291,6 +1302,7 @@ function resolvePluginLoadCacheContext(options: PluginLoadOptions = {}) {
12911302
}),
12921303
installs: installRecords,
12931304
env,
1305+
devSourceRoot,
12941306
onlyPluginIds,
12951307
includeSetupOnlyChannelPlugins,
12961308
forceSetupOnlyChannelPlugins,
@@ -1322,6 +1334,7 @@ function resolvePluginLoadCacheContext(options: PluginLoadOptions = {}) {
13221334
shouldLoadModules: options.loadModules !== false,
13231335
runtimeSubagentMode,
13241336
installRecords,
1337+
devSourceRoot,
13251338
cacheKey,
13261339
};
13271340
}
@@ -1717,6 +1730,7 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
17171730
cacheKey,
17181731
runtimeSubagentMode,
17191732
installRecords,
1733+
devSourceRoot,
17201734
} = resolvePluginLoadCacheContext(options);
17211735
const logger = options.logger ?? defaultLogger();
17221736
const validateOnly = options.mode === "validate";
@@ -1765,7 +1779,10 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
17651779
}
17661780

17671781
// Lazy: avoid creating module loaders when all plugins are disabled (common in unit tests).
1768-
const loadPluginModule = createPluginModuleLoader(options);
1782+
const loadPluginModule = createPluginModuleLoader({
1783+
devSourceRoot,
1784+
pluginSdkResolution: options.pluginSdkResolution,
1785+
});
17691786

17701787
let createPluginRuntimeFactory:
17711788
| ((options?: CreatePluginRuntimeOptions) => PluginRuntime)
@@ -1777,6 +1794,7 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
17771794
return createPluginRuntimeFactory;
17781795
}
17791796
const runtimeModuleResolution = resolvePluginRuntimeModulePathWithDiagnostics({
1797+
devSourceRoot,
17801798
pluginSdkResolution: options.pluginSdkResolution,
17811799
});
17821800
const runtimeModulePath = runtimeModuleResolution.resolvedPath;
@@ -2772,13 +2790,17 @@ export async function loadOpenClawPluginCliRegistry(
27722790
onlyPluginIds,
27732791
cacheKey,
27742792
installRecords,
2793+
devSourceRoot,
27752794
} = resolvePluginLoadCacheContext({
27762795
...options,
27772796
activate: false,
27782797
});
27792798
const logger = options.logger ?? defaultLogger();
27802799
const onlyPluginIdSet = createPluginIdScopeSet(onlyPluginIds);
2781-
const loadPluginModule = createPluginModuleLoader(options);
2800+
const loadPluginModule = createPluginModuleLoader({
2801+
devSourceRoot,
2802+
pluginSdkResolution: options.pluginSdkResolution,
2803+
});
27822804
const { registry, registerCli } = createPluginRegistry({
27832805
logger,
27842806
runtime: {} as PluginRuntime,

src/plugins/plugin-module-loader-cache.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export type ResolvePluginModuleLoaderCacheEntryParams = {
2727
loaderFilename?: string;
2828
aliasMap?: Record<string, string>;
2929
tryNative?: boolean;
30+
devSourceRoot?: string | null;
3031
pluginSdkResolution?: PluginSdkResolutionPreference;
3132
cacheScopeKey?: string;
3233
sharedCacheScopeKey?: string;
@@ -134,6 +135,7 @@ function resolveDefaultPluginModuleLoaderConfig(
134135
modulePath: params.modulePath,
135136
argv1: params.argvEntry ?? process.argv[1],
136137
moduleUrl: params.importerUrl,
138+
devSourceRoot: params.devSourceRoot,
137139
...(params.preferBuiltDist ? { preferBuiltDist: true } : {}),
138140
...(params.pluginSdkResolution ? { pluginSdkResolution: params.pluginSdkResolution } : {}),
139141
});

src/plugins/plugin-sdk-native-resolver.test.ts

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,18 @@ function writeNormalizationCoreSource(root: string): string {
7979
return sourcePath;
8080
}
8181

82+
function addFakePluginSdkDistExport(root: string, subpath: string): string {
83+
const packageJsonPath = path.join(root, "package.json");
84+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8")) as {
85+
exports: Record<string, string>;
86+
};
87+
const distPath = path.join(root, "dist", "plugin-sdk", `${subpath}.js`);
88+
packageJson.exports[`./plugin-sdk/${subpath}`] = `./dist/plugin-sdk/${subpath}.js`;
89+
writeJsonFile(packageJsonPath, packageJson);
90+
fs.writeFileSync(distPath, `export const ${subpath.replaceAll("-", "_")} = true;\n`, "utf8");
91+
return distPath;
92+
}
93+
8294
describe("installOpenClawPluginSdkNativeResolver", () => {
8395
it("resolves installed plugin SDK imports to the dev source root", () => {
8496
const stableRoot = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-sdk-native-stable-"));
@@ -111,6 +123,85 @@ describe("installOpenClawPluginSdkNativeResolver", () => {
111123
}
112124
});
113125

126+
it("resolves installed plugin SDK imports to an explicit dev source root", () => {
127+
const stableRoot = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-sdk-native-stable-"));
128+
const devRoot = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-sdk-native-dev-source-"));
129+
const { loaderModulePath } = writeFakeOpenClawPackage(stableRoot);
130+
writeFakeOpenClawPackage(devRoot);
131+
fs.mkdirSync(path.join(devRoot, "src"), { recursive: true });
132+
fs.mkdirSync(path.join(devRoot, "extensions"), { recursive: true });
133+
const externalPluginEntry = writeExternalPluginEntry(path.join(stableRoot, "external-plugin"));
134+
135+
const installedAliases = installOpenClawPluginSdkNativeResolver({
136+
modulePath: loaderModulePath,
137+
pluginModulePath: externalPluginEntry,
138+
devSourceRoot: devRoot,
139+
});
140+
141+
expect(installedAliases).toContain("openclaw/plugin-sdk/agent-runtime");
142+
const requireFromPlugin = createRequire(externalPluginEntry);
143+
expect(fs.realpathSync(requireFromPlugin.resolve("openclaw/plugin-sdk/agent-runtime"))).toBe(
144+
fs.realpathSync(path.join(devRoot, "dist", "plugin-sdk", "agent-runtime.js")),
145+
);
146+
});
147+
148+
it("updates native SDK aliases when the same plugin parent switches dev source roots", () => {
149+
const stableRoot = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-sdk-native-stable-"));
150+
const devRoot = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-sdk-native-dev-source-"));
151+
const { loaderModulePath } = writeFakeOpenClawPackage(stableRoot);
152+
writeFakeOpenClawPackage(devRoot);
153+
fs.mkdirSync(path.join(devRoot, "src"), { recursive: true });
154+
fs.mkdirSync(path.join(devRoot, "extensions"), { recursive: true });
155+
const externalPluginEntry = writeExternalPluginEntry(path.join(stableRoot, "external-plugin"));
156+
const requireFromPlugin = createRequire(externalPluginEntry);
157+
158+
installOpenClawPluginSdkNativeResolver({
159+
modulePath: loaderModulePath,
160+
pluginModulePath: externalPluginEntry,
161+
});
162+
expect(fs.realpathSync(requireFromPlugin.resolve("openclaw/plugin-sdk/agent-runtime"))).toBe(
163+
fs.realpathSync(path.join(stableRoot, "dist", "plugin-sdk", "agent-runtime.js")),
164+
);
165+
166+
installOpenClawPluginSdkNativeResolver({
167+
modulePath: loaderModulePath,
168+
pluginModulePath: externalPluginEntry,
169+
devSourceRoot: devRoot,
170+
});
171+
172+
expect(fs.realpathSync(requireFromPlugin.resolve("openclaw/plugin-sdk/agent-runtime"))).toBe(
173+
fs.realpathSync(path.join(devRoot, "dist", "plugin-sdk", "agent-runtime.js")),
174+
);
175+
});
176+
177+
it("removes stale native SDK aliases when a later dev root omits a subpath", () => {
178+
const stableRoot = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-sdk-native-stable-"));
179+
const devRoot = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-sdk-native-dev-source-"));
180+
const { loaderModulePath } = writeFakeOpenClawPackage(stableRoot);
181+
writeFakeOpenClawPackage(devRoot);
182+
const stableExtraPath = addFakePluginSdkDistExport(stableRoot, "stable-extra");
183+
fs.mkdirSync(path.join(devRoot, "src"), { recursive: true });
184+
fs.mkdirSync(path.join(devRoot, "extensions"), { recursive: true });
185+
const externalPluginEntry = writeExternalPluginEntry(path.join(stableRoot, "external-plugin"));
186+
const requireFromPlugin = createRequire(externalPluginEntry);
187+
188+
installOpenClawPluginSdkNativeResolver({
189+
modulePath: loaderModulePath,
190+
pluginModulePath: externalPluginEntry,
191+
});
192+
expect(fs.realpathSync(requireFromPlugin.resolve("openclaw/plugin-sdk/stable-extra"))).toBe(
193+
fs.realpathSync(stableExtraPath),
194+
);
195+
196+
installOpenClawPluginSdkNativeResolver({
197+
modulePath: loaderModulePath,
198+
pluginModulePath: externalPluginEntry,
199+
devSourceRoot: devRoot,
200+
});
201+
202+
expect(() => requireFromPlugin.resolve("openclaw/plugin-sdk/stable-extra")).toThrow();
203+
});
204+
114205
it("keeps native aliases on JS dist artifacts when source files exist", () => {
115206
const root = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-sdk-native-source-resolver-"));
116207
const { loaderModulePath } = writeFakeOpenClawPackage(root);

src/plugins/plugin-sdk-native-resolver.ts

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ export type InstallOpenClawPluginSdkNativeResolverOptions = {
3838
allowedParentRoots?: readonly string[];
3939
argv1?: string;
4040
moduleUrl?: string;
41+
devSourceRoot?: string | null;
4142
pluginSdkResolution?: PluginSdkResolutionPreference;
4243
};
4344

@@ -220,6 +221,7 @@ function listPluginSdkNativeAliases(
220221
// Native require hooks must point at JavaScript artifacts, even when the
221222
// plugin loader itself is configured to prefer source imports.
222223
"dist",
224+
options.devSourceRoot,
223225
),
224226
)
225227
.filter(([specifier]) => isPluginSdkAliasSpecifier(specifier))
@@ -302,9 +304,9 @@ function registerNativeAlias(params: {
302304
}): void {
303305
const entries = pluginSdkNativeAliases.get(params.request) ?? [];
304306
for (const parentRoot of params.parentRoots) {
305-
if (
306-
entries.some((entry) => entry.parentRoot === parentRoot && entry.target === params.target)
307-
) {
307+
const existingIndex = entries.findIndex((entry) => entry.parentRoot === parentRoot);
308+
if (existingIndex !== -1) {
309+
entries[existingIndex] = { parentRoot, target: params.target };
308310
continue;
309311
}
310312
entries.push({ parentRoot, target: params.target });
@@ -314,10 +316,26 @@ function registerNativeAlias(params: {
314316
}
315317
}
316318

319+
function clearNativeAliasesForParentRoots(parentRoots: readonly string[]): void {
320+
if (parentRoots.length === 0) {
321+
return;
322+
}
323+
const parentRootSet = new Set(parentRoots);
324+
for (const [request, entries] of pluginSdkNativeAliases) {
325+
const nextEntries = entries.filter((entry) => !parentRootSet.has(entry.parentRoot));
326+
if (nextEntries.length === 0) {
327+
pluginSdkNativeAliases.delete(request);
328+
} else {
329+
pluginSdkNativeAliases.set(request, nextEntries);
330+
}
331+
}
332+
}
333+
317334
export function installOpenClawPluginSdkNativeResolver(
318335
options: InstallOpenClawPluginSdkNativeResolverOptions = {},
319336
): string[] {
320337
const parentRoots = resolveAllowedParentRoots(options);
338+
clearNativeAliasesForParentRoots(parentRoots);
321339
for (const [specifier, target] of listPluginSdkNativeAliases(options)) {
322340
registerNativeAlias({ request: specifier, target, parentRoots });
323341
}

0 commit comments

Comments
 (0)