diff --git a/app/MindWork AI Studio/Settings/ManagedConfiguration.Parsing.cs b/app/MindWork AI Studio/Settings/ManagedConfiguration.Parsing.cs
index df146bbe..77d0a0df 100644
--- a/app/MindWork AI Studio/Settings/ManagedConfiguration.Parsing.cs
+++ b/app/MindWork AI Studio/Settings/ManagedConfiguration.Parsing.cs
@@ -2,6 +2,7 @@ using System.Globalization;
using System.Linq.Expressions;
using AIStudio.Settings.DataModel;
+using AIStudio.Tools.PluginSystem;
using Lua;
@@ -654,6 +655,17 @@ public static partial class ManagedConfiguration
if (dryRun)
return successful;
+ // The contribution is additive, but its ownership is not: taking it over from a
+ // configuration plugin of an organization would hand the whole setting to a local plugin:
+ if (configMeta.HasPluginContribution
+ && configMeta.PluginContributionByConfigPluginId != configPluginId
+ && PluginFactory.IsEnterpriseConfigurationPlugin(configMeta.PluginContributionByConfigPluginId)
+ && !PluginFactory.IsEnterpriseConfigurationPlugin(configPluginId))
+ {
+ Log.LogWarning($"The configuration plugin '{configPluginId}' tried to contribute to the setting '{configMeta.SettingName}', which the configuration plugin '{configMeta.PluginContributionByConfigPluginId}' of your organization contributes to. Ignoring the attempt: configurations deployed by your organization's IT take precedence.");
+ return false;
+ }
+
if (successful)
{
var configInstance = configSelection.Compile().Invoke(SettingsManagerAccess.ConfigurationData);
@@ -905,6 +917,11 @@ public static partial class ManagedConfiguration
if(dryRun)
return successful;
+ // The setting might belong to the IT department of an organization. In that case, no local
+ // configuration plugin may touch it, no matter what it declares:
+ if (!MayManageSetting(configPluginId, configMeta))
+ return false;
+
switch (successful)
{
case true:
@@ -954,6 +971,11 @@ public static partial class ManagedConfiguration
if (dryRun)
return successful;
+ // The setting might belong to the IT department of an organization. In that case, no local
+ // configuration plugin may touch it, no matter what it declares:
+ if (!MayManageSetting(configPluginId, configMeta))
+ return false;
+
switch (successful)
{
case true when managedMode is ManagedConfigurationMode.LOCKED:
diff --git a/app/MindWork AI Studio/Settings/ManagedConfiguration.cs b/app/MindWork AI Studio/Settings/ManagedConfiguration.cs
index 5d83121e..e88db801 100644
--- a/app/MindWork AI Studio/Settings/ManagedConfiguration.cs
+++ b/app/MindWork AI Studio/Settings/ManagedConfiguration.cs
@@ -257,6 +257,51 @@ public static partial class ManagedConfiguration
return false;
}
+ ///
+ /// Checks whether a configuration plugin may manage a setting, or whether that setting belongs
+ /// to the IT department of an organization.
+ ///
+ ///
+ /// A local configuration plugin must not take over a setting an organization manages. Otherwise,
+ /// anyone could hand out a configuration plugin that quietly replaces parts of the organization
+ /// configuration, e.g. the address of a self-hosted provider.
+ /// Between two configuration plugins of the same organization, we do not interfere: both belong
+ /// to the IT department, so the one processed later wins, as before.
+ ///
+ /// The configuration plugin which wants to manage the setting.
+ /// The configuration metadata of the setting.
+ /// True when the plugin may manage this setting, otherwise false.
+ private static bool MayManageSetting(Guid configPluginId, ConfigMetaBase configMeta)
+ {
+ var owningConfigPluginId = GetSettingOwner(configMeta);
+ if (owningConfigPluginId == Guid.Empty || owningConfigPluginId == configPluginId)
+ return true;
+
+ if (!PluginFactory.IsEnterpriseConfigurationPlugin(owningConfigPluginId))
+ return true;
+
+ if (PluginFactory.IsEnterpriseConfigurationPlugin(configPluginId))
+ return true;
+
+ Log.LogWarning($"The configuration plugin '{configPluginId}' tried to manage the setting '{configMeta.SettingName}', which is managed by the configuration plugin '{owningConfigPluginId}' of your organization. Ignoring the attempt: configurations deployed by your organization's IT take precedence.");
+ return false;
+ }
+
+ ///
+ /// Determines the configuration plugin which currently manages a setting, if any.
+ ///
+ private static Guid GetSettingOwner(ConfigMetaBase configMeta)
+ {
+ if (configMeta.IsLocked && configMeta.LockedByConfigPluginId != Guid.Empty)
+ return configMeta.LockedByConfigPluginId;
+
+ // The editable default is persisted as well, so we prefer it over the in-memory state:
+ if (TryGetEditableDefaultState(configMeta.SettingName, out var editableDefaultState) && editableDefaultState.ConfigPluginId != Guid.Empty)
+ return editableDefaultState.ConfigPluginId;
+
+ return configMeta.EditableDefaultByConfigPluginId;
+ }
+
///
/// Removes all managed states whose configuration plugin is not available anymore.
///
diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginConfigurationObject.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginConfigurationObject.cs
index bd9cf2b9..18eedfb2 100644
--- a/app/MindWork AI Studio/Tools/PluginSystem/PluginConfigurationObject.cs
+++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginConfigurationObject.cs
@@ -131,11 +131,14 @@ public sealed record PluginConfigurationObject
continue;
var objectIndex = storedObjects.FindIndex(t => t.Id == configObject.Id);
-
+
// Case: The object already exists, we update it:
if (objectIndex > -1)
{
var existingObject = storedObjects[objectIndex];
+ if (!MayReplaceConfigurationObject(existingObject, configPluginId))
+ continue;
+
configObject = configObject with { Num = existingObject.Num };
storedObjects[objectIndex] = (TClass)configObject;
}
@@ -220,6 +223,9 @@ public sealed record PluginConfigurationObject
if (objectIndex > -1)
{
var existingObject = storedObjects[objectIndex];
+ if (!MayReplaceConfigurationObject(existingObject, configPluginId))
+ continue;
+
configObject = configObject with { Num = existingObject.Num };
storedObjects[objectIndex] = configObject;
}
@@ -248,6 +254,35 @@ public sealed record PluginConfigurationObject
}
}
+ ///
+ /// Checks whether a configuration plugin may replace a stored configuration object, or whether
+ /// that object belongs to the IT department of an organization.
+ ///
+ ///
+ /// Configuration objects are matched by their ID alone. Without this check, a local configuration
+ /// plugin could claim the ID of an object an organization deployed and replace it, e.g. to point
+ /// a self-hosted LLM provider at a different host.
+ /// Between two configuration plugins of the same organization, we do not interfere: both belong
+ /// to the IT department, so the one processed later wins, as before.
+ ///
+ /// The configuration object which is stored already.
+ /// The configuration plugin which wants to replace that object.
+ /// True when the plugin may replace the object, otherwise false.
+ private static bool MayReplaceConfigurationObject(IConfigurationObject existingObject, Guid configPluginId)
+ {
+ if (!existingObject.IsEnterpriseConfiguration || existingObject.EnterpriseConfigurationPluginId == configPluginId)
+ return true;
+
+ if (!PluginFactory.IsEnterpriseConfigurationPlugin(existingObject.EnterpriseConfigurationPluginId))
+ return true;
+
+ if (PluginFactory.IsEnterpriseConfigurationPlugin(configPluginId))
+ return true;
+
+ LOG.LogWarning("The configuration plugin '{ConfigPluginId}' tried to replace the object '{ConfigObjectName}' (id={ConfigObjectId}), which belongs to the configuration plugin '{OwningConfigPluginId}' of your organization. Ignoring the attempt: configurations deployed by your organization's IT take precedence.", configPluginId, existingObject.Name, existingObject.Id, existingObject.EnterpriseConfigurationPluginId);
+ return false;
+ }
+
///
/// Cleans up configuration objects of a specified type that are no longer associated with any available plugin.
///
diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.cs
index 9e9584aa..6101ec26 100644
--- a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.cs
+++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.cs
@@ -114,6 +114,27 @@ public static partial class PluginFactory
}
}
+ ///
+ /// Checks whether a configuration plugin was deployed by the IT department of an organization.
+ ///
+ ///
+ /// A plugin which is deployed but could not be loaded still counts: it might be broken, e.g. due
+ /// to invalid Lua code or an incomplete download, but it was not removed. Everything it manages
+ /// stays under the control of the organization until the plugin is gone for good.
+ ///
+ /// The ID of the configuration plugin.
+ /// True when the plugin belongs to an organization, false when it is local or unknown.
+ public static bool IsEnterpriseConfigurationPlugin(Guid configPluginId)
+ {
+ if (configPluginId == Guid.Empty || !IsInitialized)
+ return false;
+
+ if (AVAILABLE_PLUGINS.Any(plugin => plugin.Id == configPluginId && plugin.Type is PluginType.CONFIGURATION && IsEnterpriseConfigurationPath(plugin.LocalPath)))
+ return true;
+
+ return Directory.Exists(Path.Join(ENTERPRISE_CONFIGURATION_PLUGINS_ROOT, configPluginId.ToString()));
+ }
+
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 08ebf55d..b2d99da3 100644
--- a/app/MindWork AI Studio/wwwroot/changelog/v26.8.1.md
+++ b/app/MindWork AI Studio/wwwroot/changelog/v26.8.1.md
@@ -12,5 +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.
+- Fixed which configuration wins when two configuration plugins collide, e.g. by claiming the same plugin ID, by managing the same setting, or by defining the same provider. Previously, this was down to chance, so a local configuration plugin could take over parts of the configuration your IT department deployed. Configurations from your organization now always win, and every ignored attempt is reported in the log.
- Upgraded dependencies to their latest versions to improve security and stability.