From 77f7c8df8d11fa4dd506ea6d3f590778d262b69a Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sun, 17 May 2026 00:12:57 +0800 Subject: [PATCH] fix(plugins): contain bundled entry paths --- src/plugins/bundled-plugin-metadata.test.ts | 82 +++++++++++++++++++++ src/plugins/bundled-plugin-metadata.ts | 22 +++++- 2 files changed, 102 insertions(+), 2 deletions(-) diff --git a/src/plugins/bundled-plugin-metadata.test.ts b/src/plugins/bundled-plugin-metadata.test.ts index 703a16dfa49..f262552a851 100644 --- a/src/plugins/bundled-plugin-metadata.test.ts +++ b/src/plugins/bundled-plugin-metadata.test.ts @@ -637,6 +637,56 @@ describe("bundled plugin metadata", () => { ); }); + it("keeps generated entry path resolution inside bundled plugin roots", () => { + const tempRoot = createGeneratedPluginTempRoot("openclaw-bundled-plugin-path-contained-"); + const sourcePluginRoot = path.join(tempRoot, "extensions", "alpha"); + const distPluginRoot = path.join(tempRoot, "dist", "extensions", "alpha"); + const absoluteEscape = path.join(tempRoot, "absolute.js"); + const absolutePluginEntry = path.join(sourcePluginRoot, "index.ts"); + + fs.mkdirSync(sourcePluginRoot, { recursive: true }); + fs.mkdirSync(distPluginRoot, { recursive: true }); + fs.writeFileSync(absolutePluginEntry, "export {};\n", "utf8"); + fs.writeFileSync(path.join(tempRoot, "extensions", "escape.ts"), "export {};\n", "utf8"); + fs.writeFileSync( + path.join(tempRoot, "dist", "extensions", "escape.js"), + "export {};\n", + "utf8", + ); + fs.writeFileSync(absoluteEscape, "export {};\n", "utf8"); + + expect( + resolveBundledPluginGeneratedPath( + tempRoot, + { + source: absolutePluginEntry, + built: absolutePluginEntry, + }, + "alpha", + ), + ).toBe(absolutePluginEntry); + expect( + resolveBundledPluginGeneratedPath( + tempRoot, + { + source: "../escape.ts", + built: "../escape.js", + }, + "alpha", + ), + ).toBeNull(); + expect( + resolveBundledPluginGeneratedPath( + tempRoot, + { + source: absoluteEscape, + built: absoluteEscape, + }, + "alpha", + ), + ).toBeNull(); + }); + it("scans direct plugin-tree overrides and resolves generated paths from that scan dir", () => { const tempRoot = createGeneratedPluginTempRoot("openclaw-bundled-plugin-direct-tree-"); const pluginsDir = path.join(tempRoot, "bundled-plugins"); @@ -767,6 +817,38 @@ describe("bundled plugin metadata", () => { ).toBe(path.join(distPluginRoot, "index.js")); }); + it("keeps bundled repo entry path resolution inside the plugin directory", () => { + const tempRoot = createGeneratedPluginTempRoot("openclaw-bundled-plugin-repo-contained-"); + const pluginRoot = path.join(tempRoot, "extensions", "alpha"); + + writeJson(path.join(pluginRoot, "package.json"), { + name: "@openclaw/alpha", + version: "0.0.1", + openclaw: { + extensions: ["../escape.ts"], + }, + }); + writeJson(path.join(pluginRoot, "openclaw.plugin.json"), { + id: "alpha", + configSchema: { type: "object" }, + }); + fs.writeFileSync(path.join(tempRoot, "extensions", "escape.ts"), "export {};\n", "utf8"); + fs.mkdirSync(path.join(tempRoot, "dist", "extensions"), { recursive: true }); + fs.writeFileSync( + path.join(tempRoot, "dist", "extensions", "escape.js"), + "export {};\n", + "utf8", + ); + + expect( + resolveBundledPluginRepoEntryPath({ + rootDir: tempRoot, + pluginId: "alpha", + preferBuilt: true, + }), + ).toBeNull(); + }); + it("merges runtime channel schema metadata with manifest-owned channel config fields", () => { const tempRoot = createGeneratedPluginTempRoot("openclaw-bundled-plugin-channel-configs-"); diff --git a/src/plugins/bundled-plugin-metadata.ts b/src/plugins/bundled-plugin-metadata.ts index 9cc4c625d9d..6e5a9589d01 100644 --- a/src/plugins/bundled-plugin-metadata.ts +++ b/src/plugins/bundled-plugin-metadata.ts @@ -255,7 +255,10 @@ export function resolveBundledPluginGeneratedPath( }); for (const baseDir of baseDirs) { for (const entryPath of entryOrder) { - const candidate = path.resolve(baseDir, normalizeRelativePluginEntryPath(entryPath)); + const candidate = resolveBundledPluginEntryCandidate(baseDir, entryPath); + if (!candidate) { + continue; + } if (fs.existsSync(candidate)) { return candidate; } @@ -268,6 +271,18 @@ function normalizeRelativePluginEntryPath(entryPath: string): string { return entryPath.replace(/^\.\//u, ""); } +function resolveBundledPluginEntryCandidate(baseDir: string, entryPath: string): string | null { + const normalizedEntryPath = normalizeRelativePluginEntryPath(entryPath); + const candidate = path.isAbsolute(normalizedEntryPath) + ? path.normalize(normalizedEntryPath) + : path.resolve(baseDir, normalizedEntryPath); + const relative = path.relative(baseDir, candidate); + if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) { + return null; + } + return candidate; +} + export function resolveBundledPluginRepoEntryPath(params: { rootDir: string; pluginId: string; @@ -297,7 +312,10 @@ export function resolveBundledPluginRepoEntryPath(params: { for (const baseDir of baseDirs) { for (const entryPath of entryOrder) { - const candidate = path.resolve(baseDir, normalizeRelativePluginEntryPath(entryPath)); + const candidate = resolveBundledPluginEntryCandidate(baseDir, entryPath); + if (!candidate) { + continue; + } if (fs.existsSync(candidate)) { return candidate; }