Fixed preview features contributed by more than one configuration plugin

This commit is contained in:
Thorsten Sommer 2026-08-08 17:43:29 +02:00
parent 2d3f6761bb
commit ebfb2b7ace
Signed by untrusted user who does not match committer: tsommer
GPG Key ID: 371BBA77A02C0108
6 changed files with 53 additions and 49 deletions

View File

@ -108,8 +108,10 @@ public partial class SettingsPanelApp : SettingsPanelBase
private HashSet<PreviewFeatures> 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<PreviewFeatures> GetSelectedPreviewFeatures()

View File

@ -33,26 +33,29 @@ public record ConfigMeta<TClass, TValue> : ConfigMetaBase
public required TValue Default { get; init; }
/// <summary>
/// The additive value contribution provided by a configuration plugin.
/// The additive value contributions, one per contributing configuration plugin.
/// </summary>
public TValue PluginContribution { get; private set; } = default!;
/// <summary>
/// Stores an additive plugin contribution.
/// </summary>
public void SetPluginContribution(TValue value, Guid pluginId)
{
this.PluginContribution = value;
this.PluginContributionByConfigPluginId = pluginId;
this.HasPluginContribution = true;
}
/// <remarks>
/// 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.
/// </remarks>
public IReadOnlyDictionary<Guid, TValue> PluginContributions => this.pluginContributions;
/// <inheritdoc/>
public override void ClearPluginContribution()
{
this.PluginContribution = default!;
base.ClearPluginContribution();
}
public override IReadOnlyCollection<Guid> ContributingConfigPluginIds => this.pluginContributions.Keys;
private readonly Dictionary<Guid, TValue> pluginContributions = [];
/// <summary>
/// Stores the additive contribution of one configuration plugin, replacing its previous one.
/// </summary>
/// <param name="value">The contributed value.</param>
/// <param name="pluginId">The contributing configuration plugin.</param>
public void SetPluginContribution(TValue value, Guid pluginId) => this.pluginContributions[pluginId] = value;
/// <inheritdoc/>
public override bool RemovePluginContribution(Guid configPluginId) => this.pluginContributions.Remove(configPluginId);
/// <inheritdoc/>
protected override void Reset()

View File

@ -38,14 +38,19 @@ public abstract record ConfigMetaBase(string SettingName) : IConfig
public Guid EditableDefaultByConfigPluginId { get; private set; }
/// <summary>
/// Indicates whether a plugin contribution is available.
/// The configuration plugins which contribute to this setting.
/// </summary>
public bool HasPluginContribution { get; protected set; }
/// <remarks>
/// 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.
/// </remarks>
public abstract IReadOnlyCollection<Guid> ContributingConfigPluginIds { get; }
/// <summary>
/// The ID of the plugin that provided the additive value contribution.
/// Indicates whether at least one configuration plugin contributes to this setting.
/// </summary>
public Guid PluginContributionByConfigPluginId { get; protected set; }
public bool HasPluginContribution => this.ContributingConfigPluginIds.Count > 0;
/// <summary>
/// Locks the configuration state, indicating that it is controlled by a specific plugin.
@ -133,13 +138,11 @@ public abstract record ConfigMetaBase(string SettingName) : IConfig
}
/// <summary>
/// Clears the additive plugin contribution without changing the current value.
/// Removes the contribution of one configuration plugin without changing the current value.
/// </summary>
public virtual void ClearPluginContribution()
{
this.PluginContributionByConfigPluginId = Guid.Empty;
this.HasPluginContribution = false;
}
/// <param name="configPluginId">The configuration plugin whose contribution is removed.</param>
/// <returns>True when that plugin had a contribution, otherwise false.</returns>
public abstract bool RemovePluginContribution(Guid configPluginId);
/// <summary>
/// Resets the configuration property to its default value.

View File

@ -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<TValue>(configuredValue), configPluginId);
}
else if (configMeta.HasPluginContribution && configMeta.PluginContributionByConfigPluginId == configPluginId)
{
configMeta.ClearPluginContribution();
}
else
configMeta.RemovePluginContribution(configPluginId);
if (configMeta.IsLocked && configMeta.LockedByConfigPluginId == configPluginId)
configMeta.UnlockConfiguration();

View File

@ -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;
}
}

View File

@ -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.