From 4a8190240e23e46275b2f8c314113c62e0121f99 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Thu, 6 Aug 2026 08:40:31 +0200 Subject: [PATCH] Fixed loading Lua modules of a plugin during its validation --- .../PluginSystem/PluginFactory.Loading.cs | 14 +++++++++++--- .../Tools/PluginSystem/PluginLoader.cs | 19 +++++++++++++------ .../Services/AssistantPluginInstallService.cs | 5 ++++- 3 files changed, 28 insertions(+), 10 deletions(-) diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Loading.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Loading.cs index b96c11b0..8dd012b9 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Loading.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Loading.cs @@ -385,16 +385,24 @@ public static partial class PluginFactory } } - public static async Task Load(string? pluginPath, string code, CancellationToken cancellationToken = default) + /// The directory the plugin is located in, or null when the code has no directory yet. + /// The Lua code of the plugin's main file. + /// Cancellation token for running the Lua code. + /// + /// The directory the plugin path must be nested in. Without it, the installed plugins directory + /// is used. Validating a plugin before its installation needs this, because the plugin lives in + /// a staging directory at that point and could not load any of its own Lua modules otherwise. + /// + public static async Task Load(string? pluginPath, string code, CancellationToken cancellationToken = default, string? allowedBaseDirectory = null) { if(ForbiddenPlugins.Check(code) is { IsForbidden: true } forbiddenState) return new NoPlugin($"This plugin is forbidden: {forbiddenState.Message}"); - + var state = LuaState.Create(); if (!string.IsNullOrWhiteSpace(pluginPath)) { // Add the module loader so that the plugin can load other Lua modules: - state.ModuleLoader = new PluginLoader(pluginPath); + state.ModuleLoader = new PluginLoader(pluginPath, allowedBaseDirectory); } // Add some useful libraries: diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginLoader.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginLoader.cs index ec81f73c..da7beee6 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/PluginLoader.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginLoader.cs @@ -14,10 +14,17 @@ namespace AIStudio.Tools.PluginSystem; /// Loading other modules outside the plugin directory is not allowed. /// /// The directory where the plugin is located. -public sealed class PluginLoader(string pluginDirectory) : ILuaModuleLoader +/// +/// The directory the plugin directory must be nested in. Without it, the installed plugins directory +/// is used. Validating a plugin before its installation needs this, because the plugin is not +/// installed yet and lives in a staging directory outside the installed plugins directory. +/// +public sealed class PluginLoader(string pluginDirectory, string? allowedBaseDirectory = null) : ILuaModuleLoader { private static readonly string PLUGIN_BASE_PATH = Path.Join(SettingsManager.DataDirectory, "plugins"); + private readonly string baseDirectory = string.IsNullOrWhiteSpace(allowedBaseDirectory) ? PLUGIN_BASE_PATH : allowedBaseDirectory; + #region Implementation of ILuaModuleLoader /// @@ -26,11 +33,11 @@ public sealed class PluginLoader(string pluginDirectory) : ILuaModuleLoader // Ensure that the user doesn't try to escape the plugin directory: if (moduleName.Contains("..") || pluginDirectory.Contains("..")) return false; - - // Ensure that the plugin directory is nested in the plugin base path: - if (!pluginDirectory.StartsWith(PLUGIN_BASE_PATH, StringComparison.OrdinalIgnoreCase)) + + // Ensure that the plugin directory is nested in the allowed base directory: + if (!pluginDirectory.StartsWith(this.baseDirectory, StringComparison.OrdinalIgnoreCase)) return false; - + var path = Path.Join(pluginDirectory, $"{moduleName}.lua"); return File.Exists(path); } @@ -40,7 +47,7 @@ public sealed class PluginLoader(string pluginDirectory) : ILuaModuleLoader { var path = Path.Join(pluginDirectory, $"{moduleName}.lua"); var code = await File.ReadAllTextAsync(path, Encoding.UTF8, cancellationToken); - + return new(moduleName, code); } diff --git a/app/MindWork AI Studio/Tools/Services/AssistantPluginInstallService.cs b/app/MindWork AI Studio/Tools/Services/AssistantPluginInstallService.cs index df9d7ebb..1cb12871 100644 --- a/app/MindWork AI Studio/Tools/Services/AssistantPluginInstallService.cs +++ b/app/MindWork AI Studio/Tools/Services/AssistantPluginInstallService.cs @@ -572,7 +572,10 @@ public sealed class AssistantPluginInstallService private static async Task ValidateAssistantPluginCodeAsync(string pluginDirectory, string pluginCode, string notAssistantIssue, string invalidAssistantIssue, string internalPluginIdIssue, CancellationToken token) { - var plugin = await PluginFactory.Load(pluginDirectory, pluginCode, token); + // The plugin is not installed yet: it sits in a staging directory outside the installed + // plugins directory. We allow that directory as the module base, so the plugin can load its + // own Lua modules, e.g., an icon.lua, while we validate it: + var plugin = await PluginFactory.Load(pluginDirectory, pluginCode, token, pluginDirectory); if (plugin is not PluginAssistants assistantPlugin) return AssistantPluginValidationResult.Failure(string.Format(notAssistantIssue, string.Join("; ", plugin.Issues)));