From 5138d3f8b6ba9a1c786c64e4c0dfafca13771cc4 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 30 Apr 2026 04:13:20 +0100 Subject: [PATCH] fix(plugins): resolve plugin paths from root --- src/plugins/plugin-registry.test.ts | 22 ++++++++++++++++++++++ src/plugins/registry.ts | 10 +++++++++- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/src/plugins/plugin-registry.test.ts b/src/plugins/plugin-registry.test.ts index 8a2cafd713d..cad8c204791 100644 --- a/src/plugins/plugin-registry.test.ts +++ b/src/plugins/plugin-registry.test.ts @@ -32,6 +32,7 @@ import { resolveProviderOwners, resolveSetupProviderOwners, } from "./plugin-registry.js"; +import { resolvePluginPath } from "./registry.js"; import { cleanupTrackedTempDirs, makeTrackedTempDir } from "./test-helpers/fs-fixtures.js"; const tempDirs: string[] = []; @@ -144,6 +145,27 @@ function createIndex( } describe("plugin registry facade", () => { + it("resolves relative plugin API paths against the plugin root", () => { + const pluginRoot = path.join(makeTempDir(), "plugins", "demo"); + + expect(resolvePluginPath("data/cache.json", pluginRoot)).toBe( + path.join(pluginRoot, "data", "cache.json"), + ); + expect(resolvePluginPath("./data/cache.json", pluginRoot)).toBe( + path.join(pluginRoot, "data", "cache.json"), + ); + }); + + it("keeps absolute and home plugin API paths user-resolved", () => { + const pluginRoot = path.join(makeTempDir(), "plugins", "demo"); + const absolute = path.resolve(pluginRoot, "..", "outside.txt"); + + expect(resolvePluginPath(absolute, pluginRoot)).toBe(resolvePluginPath(absolute, undefined)); + expect(resolvePluginPath("~/openclaw/plugin.txt", pluginRoot)).toBe( + resolvePluginPath("~/openclaw/plugin.txt", undefined), + ); + }); + it("resolves cold plugin records and contribution owners without loading runtime", () => { const rootDir = makeTempDir(); const candidate = createCandidate(rootDir); diff --git a/src/plugins/registry.ts b/src/plugins/registry.ts index 5f5c24f5bf1..5ac44350194 100644 --- a/src/plugins/registry.ts +++ b/src/plugins/registry.ts @@ -236,6 +236,14 @@ const constrainLegacyPromptInjectionHook = ( export { createEmptyPluginRegistry } from "./registry-empty.js"; +export function resolvePluginPath(input: string, rootDir: string | undefined): string { + const trimmed = input.trim(); + if (!trimmed || path.isAbsolute(trimmed) || trimmed.startsWith("~")) { + return resolveUserPath(input); + } + return rootDir ? path.resolve(rootDir, trimmed) : resolveUserPath(input); +} + const ACTIVE_PLUGIN_HOOK_REGISTRATIONS_KEY = Symbol.for("openclaw.activePluginHookRegistrations"); const activePluginHookRegistrations = resolveGlobalSingleton< Map[1] }>> @@ -2020,7 +2028,7 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) { pluginConfig: params.pluginConfig, runtime: resolvePluginRuntime(record.id), logger: normalizeLogger(registryParams.logger), - resolvePath: (input: string) => resolveUserPath(input), + resolvePath: (input: string) => resolvePluginPath(input, record.rootDir), handlers: { ...(registrationCapabilities.capabilityHandlers ? {