From ebfb2b7acef609f273b1fdfb9792f051c69aaf91 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sat, 8 Aug 2026 17:43:29 +0200 Subject: [PATCH] Fixed preview features contributed by more than one configuration plugin --- .../Settings/SettingsPanelApp.razor.cs | 6 ++- app/MindWork AI Studio/Settings/ConfigMeta.cs | 37 ++++++++++--------- .../Settings/ConfigMetaBase.cs | 23 +++++++----- .../Settings/ManagedConfiguration.Parsing.cs | 23 ++++-------- .../Settings/ManagedConfiguration.cs | 12 ++++-- .../wwwroot/changelog/v26.8.1.md | 1 + 6 files changed, 53 insertions(+), 49 deletions(-) diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelApp.razor.cs b/app/MindWork AI Studio/Components/Settings/SettingsPanelApp.razor.cs index 3f43d8a3..a05a4e98 100644 --- a/app/MindWork AI Studio/Components/Settings/SettingsPanelApp.razor.cs +++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelApp.razor.cs @@ -108,8 +108,10 @@ public partial class SettingsPanelApp : SettingsPanelBase private HashSet GetPluginContributedPreviewFeatures() { + // Several configuration plugins may contribute at the same time, e.g. one preview feature + // for the whole organization and another one for a single department: if (ManagedConfiguration.TryGet(x => x.App, x => x.EnabledPreviewFeatures, out var meta) && meta.HasPluginContribution) - return meta.PluginContribution.Where(x => !x.IsReleased()).ToHashSet(); + return meta.PluginContributions.Values.SelectMany(contribution => contribution).Where(x => !x.IsReleased()).ToHashSet(); return []; } @@ -122,7 +124,7 @@ public partial class SettingsPanelApp : SettingsPanelBase if (!ManagedConfiguration.TryGet(x => x.App, x => x.EnabledPreviewFeatures, out var meta) || !meta.HasPluginContribution) return false; - return meta.PluginContribution.Contains(feature); + return meta.PluginContributions.Values.Any(contribution => contribution.Contains(feature)); } private HashSet GetSelectedPreviewFeatures() diff --git a/app/MindWork AI Studio/Settings/ConfigMeta.cs b/app/MindWork AI Studio/Settings/ConfigMeta.cs index 712e43d6..e11d1df1 100644 --- a/app/MindWork AI Studio/Settings/ConfigMeta.cs +++ b/app/MindWork AI Studio/Settings/ConfigMeta.cs @@ -33,26 +33,29 @@ public record ConfigMeta : ConfigMetaBase public required TValue Default { get; init; } /// - /// The additive value contribution provided by a configuration plugin. + /// The additive value contributions, one per contributing configuration plugin. /// - public TValue PluginContribution { get; private set; } = default!; - - /// - /// Stores an additive plugin contribution. - /// - public void SetPluginContribution(TValue value, Guid pluginId) - { - this.PluginContribution = value; - this.PluginContributionByConfigPluginId = pluginId; - this.HasPluginContribution = true; - } + /// + /// Every configuration plugin keeps its own contribution, so removing one of them leaves the + /// contributions of the others intact. Callers that need the overall contribution combine the + /// values themselves: only they know how to combine the concrete type. + /// + public IReadOnlyDictionary PluginContributions => this.pluginContributions; /// - public override void ClearPluginContribution() - { - this.PluginContribution = default!; - base.ClearPluginContribution(); - } + public override IReadOnlyCollection ContributingConfigPluginIds => this.pluginContributions.Keys; + + private readonly Dictionary pluginContributions = []; + + /// + /// Stores the additive contribution of one configuration plugin, replacing its previous one. + /// + /// The contributed value. + /// The contributing configuration plugin. + public void SetPluginContribution(TValue value, Guid pluginId) => this.pluginContributions[pluginId] = value; + + /// + public override bool RemovePluginContribution(Guid configPluginId) => this.pluginContributions.Remove(configPluginId); /// protected override void Reset() diff --git a/app/MindWork AI Studio/Settings/ConfigMetaBase.cs b/app/MindWork AI Studio/Settings/ConfigMetaBase.cs index 5c34dee5..4f6549ad 100644 --- a/app/MindWork AI Studio/Settings/ConfigMetaBase.cs +++ b/app/MindWork AI Studio/Settings/ConfigMetaBase.cs @@ -38,14 +38,19 @@ public abstract record ConfigMetaBase(string SettingName) : IConfig public Guid EditableDefaultByConfigPluginId { get; private set; } /// - /// Indicates whether a plugin contribution is available. + /// The configuration plugins which contribute to this setting. /// - public bool HasPluginContribution { get; protected set; } + /// + /// Contributions are additive, so several configuration plugins may contribute at the same time + /// and each of them keeps its own contribution. An organization might enable one preview feature + /// for everybody and another one for a single department, for example. + /// + public abstract IReadOnlyCollection ContributingConfigPluginIds { get; } /// - /// The ID of the plugin that provided the additive value contribution. + /// Indicates whether at least one configuration plugin contributes to this setting. /// - public Guid PluginContributionByConfigPluginId { get; protected set; } + public bool HasPluginContribution => this.ContributingConfigPluginIds.Count > 0; /// /// Locks the configuration state, indicating that it is controlled by a specific plugin. @@ -133,13 +138,11 @@ public abstract record ConfigMetaBase(string SettingName) : IConfig } /// - /// Clears the additive plugin contribution without changing the current value. + /// Removes the contribution of one configuration plugin without changing the current value. /// - public virtual void ClearPluginContribution() - { - this.PluginContributionByConfigPluginId = Guid.Empty; - this.HasPluginContribution = false; - } + /// The configuration plugin whose contribution is removed. + /// True when that plugin had a contribution, otherwise false. + public abstract bool RemovePluginContribution(Guid configPluginId); /// /// Resets the configuration property to its default value. diff --git a/app/MindWork AI Studio/Settings/ManagedConfiguration.Parsing.cs b/app/MindWork AI Studio/Settings/ManagedConfiguration.Parsing.cs index 77d0a0df..7b33a546 100644 --- a/app/MindWork AI Studio/Settings/ManagedConfiguration.Parsing.cs +++ b/app/MindWork AI Studio/Settings/ManagedConfiguration.Parsing.cs @@ -2,7 +2,6 @@ using System.Globalization; using System.Linq.Expressions; using AIStudio.Settings.DataModel; -using AIStudio.Tools.PluginSystem; using Lua; @@ -655,17 +654,11 @@ 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; - } - + // + // Contributions need no protection against a takeover: every configuration plugin has its + // own contribution, so no plugin can replace or drop the contribution of another one. This + // is also why a local configuration plugin may contribute next to one of an organization. + // if (successful) { var configInstance = configSelection.Compile().Invoke(SettingsManagerAccess.ConfigurationData); @@ -675,10 +668,8 @@ public static partial class ManagedConfiguration configMeta.SetValue(merged); configMeta.SetPluginContribution(new HashSet(configuredValue), configPluginId); } - else if (configMeta.HasPluginContribution && configMeta.PluginContributionByConfigPluginId == configPluginId) - { - configMeta.ClearPluginContribution(); - } + else + configMeta.RemovePluginContribution(configPluginId); if (configMeta.IsLocked && configMeta.LockedByConfigPluginId == configPluginId) configMeta.UnlockConfiguration(); diff --git a/app/MindWork AI Studio/Settings/ManagedConfiguration.cs b/app/MindWork AI Studio/Settings/ManagedConfiguration.cs index e88db801..a99b21ce 100644 --- a/app/MindWork AI Studio/Settings/ManagedConfiguration.cs +++ b/app/MindWork AI Studio/Settings/ManagedConfiguration.cs @@ -350,11 +350,15 @@ public static partial class ManagedConfiguration if (CleanupEditableDefaultState(configMeta, availablePlugins, deployedEnterpriseConfigPluginIds)) wasChanged = true; - // Check the additive plugin contribution: - if (configMeta.HasPluginContribution && configMeta.PluginContributionByConfigPluginId != Guid.Empty && !IsPluginPresent(configMeta.PluginContributionByConfigPluginId, availablePlugins, deployedEnterpriseConfigPluginIds)) + // Check the additive plugin contributions. Every contributing plugin is checked on its + // own, so one removed plugin does not take the contributions of the others with it: + foreach (var contributingConfigPluginId in configMeta.ContributingConfigPluginIds.ToList()) { - Log.LogInformation($"Clearing the plugin contribution for the setting '{configMeta.SettingName}': the configuration plugin '{configMeta.PluginContributionByConfigPluginId}' is not available anymore."); - configMeta.ClearPluginContribution(); + if (contributingConfigPluginId != Guid.Empty && IsPluginPresent(contributingConfigPluginId, availablePlugins, deployedEnterpriseConfigPluginIds)) + continue; + + Log.LogInformation($"Clearing the contribution of the configuration plugin '{contributingConfigPluginId}' to the setting '{configMeta.SettingName}': the plugin is not available anymore."); + configMeta.RemovePluginContribution(contributingConfigPluginId); wasChanged = true; } } 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 0e88e40d..d347d305 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v26.8.1.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v26.8.1.md @@ -13,5 +13,6 @@ - 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 preview features contributed by several configuration plugins at once. Only the most recent contribution was recognized as coming from your organization, so features enabled by another configuration looked as if you had switched them on yourself. Each configuration is now tracked separately, which lets your organization enable one preview feature company-wide and another one for a single department. - 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.