diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Loading.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Loading.cs index 6e026533..95844959 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Loading.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Loading.cs @@ -110,10 +110,26 @@ public static partial class PluginFactory LOG.LogInformation($"Successfully loaded plugin: '{pluginMainFile}' (Id='{plugin.Id}', Type='{plugin.Type}', Name='{plugin.Name}', Version='{plugin.Version}', Authors='{string.Join(", ", plugin.Authors)}')"); - var isConfigurationPluginInConfigDirectory = - plugin.Type is PluginType.CONFIGURATION && - pluginPath.StartsWith(CONFIGURATION_PLUGINS_ROOT, StringComparison.OrdinalIgnoreCase); + // + // Plugin IDs must be unique: many lookups resolve a plugin by its ID alone, e.g. + // the base language plugin in PluginFactory.Starting or the owner of a locked + // setting. When two plugins share an ID, the one deployed by the organization's + // IT wins. Otherwise, a manually placed copy could outrank the enterprise + // configuration, which is the exact opposite of what an organization expects: + // + if (AVAILABLE_PLUGINS.FirstOrDefault(candidate => candidate.Id == plugin.Id) is { } duplicatePlugin) + { + if (!IsEnterpriseConfigurationPath(pluginPath) || IsEnterpriseConfigurationPath(duplicatePlugin.LocalPath)) + { + LOG.LogWarning($"Ignoring the plugin '{pluginMainFile}': its ID ('{plugin.Id}') is already used by the plugin at '{duplicatePlugin.LocalPath}'. Plugin IDs must be unique. Please remove one of these plugins."); + continue; + } + LOG.LogWarning($"Ignoring the plugin at '{duplicatePlugin.LocalPath}': it uses the ID ('{plugin.Id}') of the enterprise configuration plugin at '{pluginPath}'. Plugins deployed by your organization's IT take precedence."); + AVAILABLE_PLUGINS.Remove(duplicatePlugin); + } + + var isConfigurationPluginInConfigDirectory = plugin.Type is PluginType.CONFIGURATION && IsEnterpriseConfigurationPath(pluginPath); var isManagedByConfigServer = false; Guid? managedConfigurationId = null; if (plugin is PluginConfiguration configPlugin) diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.cs index 9efa9e9b..505e379b 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.cs @@ -76,6 +76,35 @@ public static partial class PluginFactory return true; } + /// + /// Checks whether a plugin directory belongs to the enterprise configuration area. + /// + /// + /// Only the IT department of an organization deploys plugins there: the config server downloads + /// them into a directory named after their configuration ID. We decide by path on purpose. The + /// Lua field DEPLOYED_USING_CONFIG_SERVER is self-declared, so any plugin could claim to be + /// deployed by an organization. + /// + /// The directory of the plugin. + /// True when the directory is nested in the enterprise configuration directory. + private static bool IsEnterpriseConfigurationPath(string? pluginPath) + { + if (string.IsNullOrWhiteSpace(pluginPath) || string.IsNullOrWhiteSpace(CONFIGURATION_PLUGINS_ROOT)) + return false; + + try + { + var configurationRoot = Path.GetFullPath(CONFIGURATION_PLUGINS_ROOT).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) + Path.DirectorySeparatorChar; + var pluginDirectory = Path.GetFullPath(pluginPath).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) + Path.DirectorySeparatorChar; + return pluginDirectory.StartsWith(configurationRoot, StringComparison.OrdinalIgnoreCase); + } + catch (Exception e) + { + LOG.LogWarning(e, $"Was not able to check whether the plugin directory '{pluginPath}' belongs to the enterprise configuration directory. Treating it as a local plugin."); + return false; + } + } + private static async Task LockHotReloadAsync() { if (!IsInitialized) diff --git a/app/MindWork AI Studio/wwwroot/changelog/v26.8.1.md b/app/MindWork AI Studio/wwwroot/changelog/v26.8.1.md index 1ea9e7bc..08ebf55d 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v26.8.1.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v26.8.1.md @@ -12,4 +12,5 @@ - Fixed configuration-managed settings remaining active after their configuration plugin was removed. - Fixed the integrated code editor to keep errors and other issues in plugin code visible in the footer while scrolling. - Fixed the trusted badge so you can now see at a glance which models are trusted. It is shown consistently for self-hosted models and models from trusted providers. +- Fixed which plugin wins when two plugins claim the same plugin ID. Previously, it was down to chance, so a manually placed copy could take over from a configuration your IT department deployed. Configurations from your organization now always win, and the ignored plugin is reported in the log. - Upgraded dependencies to their latest versions to improve security and stability.