Extracted the compatibility shim for orphaned configuration locks into its own method and documented it

This commit is contained in:
Thorsten Sommer 2026-08-06 19:15:47 +02:00
parent 45a272e805
commit 6a30d5ff83
Signed by untrusted user who does not match committer: tsommer
GPG Key ID: 371BBA77A02C0108
3 changed files with 135 additions and 65 deletions

View File

@ -31,7 +31,7 @@ public record ConfigMeta<TClass, TValue> : ConfigMetaBase
/// <summary>
/// The persisted name of the configuration setting.
/// </summary>
private string SettingName { get; }
public string SettingName { get; }
/// <summary>
/// Indicates whether the configuration is locked by a configuration plugin.

View File

@ -1,5 +1,7 @@
using System.Linq.Expressions;
using System.Text;
using AIStudio.Settings;
using AIStudio.Settings.DataModel;
using AIStudio.Tools.PluginSystem.Assistants;
using Lua;
using Lua.Standard;
@ -378,72 +380,10 @@ public static partial class PluginFactory
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.AssistantPluginAudit, x => x.EnterpriseApprovedPlugins, AVAILABLE_PLUGINS))
wasConfigurationChanged = true;
// Compatibility shim: repair config-only values that may predate persisted lock ownership.
// these values can only be set by a config plugin and therefore cause the biggest problem, since the user can not change them himself
if (ManagedConfiguration.TryGet(x => x.App, x => x.ShowIntroduction, out var showIntroductionMeta)
&& showIntroductionMeta.ManagedMode is null
&& !SettingsManagerAccess.ConfigurationData.App.ShowIntroduction)
{
showIntroductionMeta.ResetLockedConfiguration();
// Compatibility shim, see documentation/compatibility-shims/2026-08-orphaned-config-locks.md (remove after 2027-08-06):
if (RepairLegacyConfigOnlySettings())
wasConfigurationChanged = true;
}
if (ManagedConfiguration.TryGet(x => x.App, x => x.ShowQuickStartGuide, out var showQuickStartGuideMeta)
&& showQuickStartGuideMeta.ManagedMode is null
&& !SettingsManagerAccess.ConfigurationData.App.ShowQuickStartGuide)
{
showQuickStartGuideMeta.ResetLockedConfiguration();
wasConfigurationChanged = true;
}
if (ManagedConfiguration.TryGet(x => x.App, x => x.ShowLastChangelog, out var showLastChangelogMeta)
&& showLastChangelogMeta.ManagedMode is null
&& !SettingsManagerAccess.ConfigurationData.App.ShowLastChangelog)
{
showLastChangelogMeta.ResetLockedConfiguration();
wasConfigurationChanged = true;
}
if (ManagedConfiguration.TryGet(x => x.App, x => x.ShowVision, out var showVisionMeta)
&& showVisionMeta.ManagedMode is null
&& !SettingsManagerAccess.ConfigurationData.App.ShowVision)
{
showVisionMeta.ResetLockedConfiguration();
wasConfigurationChanged = true;
}
if (ManagedConfiguration.TryGet(x => x.App, x => x.AllowUserToAddProvider, out var allowUserToAddProviderMeta)
&& allowUserToAddProviderMeta.ManagedMode is null
&& !SettingsManagerAccess.ConfigurationData.App.AllowUserToAddProvider)
{
allowUserToAddProviderMeta.ResetLockedConfiguration();
wasConfigurationChanged = true;
}
if (ManagedConfiguration.TryGet(x => x.App, x => x.HiddenAssistants, out var hiddenAssistantsMeta)
&& hiddenAssistantsMeta.ManagedMode is null
&& SettingsManagerAccess.ConfigurationData.App.HiddenAssistants.Count > 0)
{
hiddenAssistantsMeta.ResetLockedConfiguration();
wasConfigurationChanged = true;
}
if (ManagedConfiguration.TryGet(x => x.DataSourceSecurity, x => x.TrustedProviderIds, out var trustedProviderIdsMeta)
&& trustedProviderIdsMeta.ManagedMode is null
&& SettingsManagerAccess.ConfigurationData.DataSourceSecurity.TrustedProviderIds.Count > 0)
{
trustedProviderIdsMeta.ResetLockedConfiguration();
wasConfigurationChanged = true;
}
if (ManagedConfiguration.TryGet(x => x.AssistantPluginAudit, x => x.EnterpriseApprovedPlugins, out var enterpriseApprovedPluginsMeta)
&& enterpriseApprovedPluginsMeta.ManagedMode is null
&& SettingsManagerAccess.ConfigurationData.AssistantPluginAudit.EnterpriseApprovedPlugins.Count > 0)
{
enterpriseApprovedPluginsMeta.ResetLockedConfiguration();
wasConfigurationChanged = true;
}
if (wasConfigurationChanged)
{
await SettingsManagerAccess.StoreSettings();
@ -526,4 +466,100 @@ public static partial class PluginFactory
return new NoPlugin("This plugin type is not supported yet. Please try again with a future version of AI Studio.");
}
}
//
// =========================================================
// Compatibility shim. Please read the related document
// before you change anything here:
//
// documentation/compatibility-shims/2026-08-orphaned-config-locks.md
//
// Remove after 2027-08-06. Everything from here down to the
// end of this file belongs to the shim and can be deleted
// in one piece.
// =========================================================
//
/// <summary>
/// Repairs settings that were configured by a configuration plugin which was removed before
/// AI Studio started to persist the configuration ownership.
/// </summary>
/// <remarks>
/// All settings listed here share two properties: a configuration plugin can set them, and
/// there is no user interface to change them back. Therefore, any value that differs from the
/// default must originate from a configuration plugin. When such a setting is not managed
/// anymore, its plugin is gone and we restore the default value.<br/><br/>
/// This is only valid as long as none of these settings gets a user interface. When you add
/// one, remove the setting from this method and from the shim's document.
/// </remarks>
/// <returns>True when at least one setting was repaired, otherwise false.</returns>
private static bool RepairLegacyConfigOnlySettings()
{
var data = SettingsManagerAccess.ConfigurationData;
var wasRepaired = false;
// Settings which are enabled by default and which only a configuration plugin can switch off:
wasRepaired |= RepairLegacyConfigOnlyFlag(x => x.App, x => x.ShowIntroduction, data.App.ShowIntroduction);
wasRepaired |= RepairLegacyConfigOnlyFlag(x => x.App, x => x.ShowQuickStartGuide, data.App.ShowQuickStartGuide);
wasRepaired |= RepairLegacyConfigOnlyFlag(x => x.App, x => x.ShowLastChangelog, data.App.ShowLastChangelog);
wasRepaired |= RepairLegacyConfigOnlyFlag(x => x.App, x => x.ShowVision, data.App.ShowVision);
wasRepaired |= RepairLegacyConfigOnlyFlag(x => x.App, x => x.AllowUserToAddProvider, data.App.AllowUserToAddProvider);
wasRepaired |= RepairLegacyConfigOnlyFlag(x => x.App, x => x.AllowUserToImportPlugins, data.App.AllowUserToImportPlugins);
wasRepaired |= RepairLegacyConfigOnlyFlag(x => x.App, x => x.AllowUserToSharePlugins, data.App.AllowUserToSharePlugins);
// Collections which stay empty unless a configuration plugin fills them:
wasRepaired |= RepairLegacyConfigOnlyCollection(x => x.App, x => x.HiddenAssistants, data.App.HiddenAssistants.Count);
wasRepaired |= RepairLegacyConfigOnlyCollection(x => x.DataSourceSecurity, x => x.TrustedProviderIds, data.DataSourceSecurity.TrustedProviderIds.Count);
wasRepaired |= RepairLegacyConfigOnlyCollection(x => x.AssistantPluginAudit, x => x.EnterpriseApprovedPlugins, data.AssistantPluginAudit.EnterpriseApprovedPlugins.Count);
return wasRepaired;
}
/// <summary>
/// Restores the default of a boolean setting when it is switched off without being managed.
/// </summary>
private static bool RepairLegacyConfigOnlyFlag<TClass>(Expression<Func<Data, TClass>> configSelection, Expression<Func<TClass, bool>> propertyExpression, bool currentValue)
{
if (currentValue)
return false;
if (!ManagedConfiguration.TryGet(configSelection, propertyExpression, out var configMeta) || configMeta.ManagedMode is not null)
return false;
LOG.LogWarning($"Repairing the setting '{configMeta.SettingName}': it was switched off by a configuration plugin which is not available anymore.");
configMeta.ResetLockedConfiguration();
return true;
}
/// <summary>
/// Clears a set-based setting when it contains entries without being managed.
/// </summary>
private static bool RepairLegacyConfigOnlyCollection<TClass, TValue>(Expression<Func<Data, TClass>> configSelection, Expression<Func<TClass, ISet<TValue>>> propertyExpression, int currentCount)
{
if (currentCount is 0)
return false;
if (!ManagedConfiguration.TryGet(configSelection, propertyExpression, out var configMeta) || configMeta.ManagedMode is not null)
return false;
LOG.LogWarning($"Repairing the setting '{configMeta.SettingName}': it was filled by a configuration plugin which is not available anymore.");
configMeta.ResetLockedConfiguration();
return true;
}
/// <summary>
/// Clears a list-based setting when it contains entries without being managed.
/// </summary>
private static bool RepairLegacyConfigOnlyCollection<TClass, TValue>(Expression<Func<Data, TClass>> configSelection, Expression<Func<TClass, IList<TValue>>> propertyExpression, int currentCount)
{
if (currentCount is 0)
return false;
if (!ManagedConfiguration.TryGet(configSelection, propertyExpression, out var configMeta) || configMeta.ManagedMode is not null)
return false;
LOG.LogWarning($"Repairing the setting '{configMeta.SettingName}': it was filled by a configuration plugin which is not available anymore.");
configMeta.ResetLockedConfiguration();
return true;
}
}

View File

@ -0,0 +1,34 @@
# Orphaned Configuration Locks
- Status: Active
- Introduced: 2026-08-06
- Remove after: 2027-08-06
- Code references:
- `app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Loading.cs` (`RepairLegacyConfigOnlySettings`, `RepairLegacyConfigOnlyFlag`, `RepairLegacyConfigOnlyCollection`)
## User Impact
Until this release, AI Studio persisted the value a configuration plugin had set, but not the information which plugin owned that value. After a restart, the ownership was lost. When the configuration plugin was removed in the meantime, the cleanup in `PluginFactory.LoadAll` could not recognize the value as left over, so it stayed active forever.
For most settings, this was an inconvenience only, because users can change them in the settings dialog. For settings without any user interface, it was a dead end: hidden assistants stayed hidden, adding providers stayed disabled, and the home page panels stayed switched off. The only workaround was to edit the settings file by hand.
Installations that lost the ownership this way cannot be repaired by the new persistence alone, because the missing information cannot be reconstructed. They need this one-time repair.
## Compatibility Behavior
At the end of `PluginFactory.LoadAll`, AI Studio checks a fixed list of settings. A setting is repaired when it is not managed by any configuration plugin at that moment and still holds a value that only a configuration plugin could have produced:
- `DataApp.ShowIntroduction`, `DataApp.ShowQuickStartGuide`, `DataApp.ShowLastChangelog`, `DataApp.ShowVision`, `DataApp.AllowUserToAddProvider`, `DataApp.AllowUserToImportPlugins`, `DataApp.AllowUserToSharePlugins`: enabled by default, so a disabled value is repaired.
- `DataApp.HiddenAssistants`, `DataSourceSecuritySettings.TrustedProviderIds`, `DataAssistantPluginAudit.EnterpriseApprovedPlugins`: empty by default, so a filled collection is repaired.
Repairing means restoring the default value. Each repair is logged as a warning.
The check runs on every start, not once. This is safe because none of these settings has a user interface that writes to it, so a non-default value can only originate from a configuration plugin. This is the load-bearing assumption of the whole shim: as soon as one of these settings gets a user interface, the shim would overwrite the user's choice on every start. In that case, remove the setting from `RepairLegacyConfigOnlySettings` and from the list above.
Settings that a configuration plugin can lock but that users can change themselves are deliberately not part of this list. Their owner is persisted from this release on, and the regular left-over cleanup handles them.
## Removal Checklist
- Remove `RepairLegacyConfigOnlySettings`, `RepairLegacyConfigOnlyFlag`, and `RepairLegacyConfigOnlyCollection` from `PluginFactory.Loading.cs`, including the call and the comment in `LoadAll`.
- Update this document's status to `Removed`.
- No changelog entry is needed, because removing the shim is not user-visible.