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.