diff --git a/app/MindWork AI Studio/Settings/ConfigMeta.cs b/app/MindWork AI Studio/Settings/ConfigMeta.cs index b6a0ed7f..cbb4d6b9 100644 --- a/app/MindWork AI Studio/Settings/ConfigMeta.cs +++ b/app/MindWork AI Studio/Settings/ConfigMeta.cs @@ -31,7 +31,7 @@ public record ConfigMeta : ConfigMetaBase /// /// The persisted name of the configuration setting. /// - private string SettingName { get; } + public string SettingName { get; } /// /// Indicates whether the configuration is locked by a configuration plugin. diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Loading.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Loading.cs index 7a73ffca..c24ae75f 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Loading.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Loading.cs @@ -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. + // ========================================================= + // + + /// + /// Repairs settings that were configured by a configuration plugin which was removed before + /// AI Studio started to persist the configuration ownership. + /// + /// + /// 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.

+ /// 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. + ///
+ /// True when at least one setting was repaired, otherwise false. + 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; + } + + /// + /// Restores the default of a boolean setting when it is switched off without being managed. + /// + private static bool RepairLegacyConfigOnlyFlag(Expression> configSelection, Expression> 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; + } + + /// + /// Clears a set-based setting when it contains entries without being managed. + /// + private static bool RepairLegacyConfigOnlyCollection(Expression> configSelection, Expression>> 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; + } + + /// + /// Clears a list-based setting when it contains entries without being managed. + /// + private static bool RepairLegacyConfigOnlyCollection(Expression> configSelection, Expression>> 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; + } } diff --git a/documentation/compatibility-shims/2026-08-orphaned-config-locks.md b/documentation/compatibility-shims/2026-08-orphaned-config-locks.md new file mode 100644 index 00000000..577962e2 --- /dev/null +++ b/documentation/compatibility-shims/2026-08-orphaned-config-locks.md @@ -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.