mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-08-11 20:12:12 +00:00
Kept managed settings untouched when a configuration plugin is deployed but cannot be loaded
This commit is contained in:
parent
6355c91ac6
commit
91506f8314
@ -269,8 +269,12 @@ public static partial class ManagedConfiguration
|
||||
/// change it at all.
|
||||
/// </remarks>
|
||||
/// <param name="availablePlugins">The collection of available plugins to check against.</param>
|
||||
/// <param name="deployedConfigPluginIds">
|
||||
/// The IDs of all configuration plugins which are deployed on this machine, including those which
|
||||
/// could not be loaded. A deployed plugin was not removed, so its settings must stay untouched.
|
||||
/// </param>
|
||||
/// <returns>True when at least one setting was changed, otherwise false.</returns>
|
||||
public static bool CleanupLeftOverManagedConfigurations(IReadOnlyCollection<IAvailablePlugin> availablePlugins)
|
||||
public static bool CleanupLeftOverManagedConfigurations(IReadOnlyCollection<IAvailablePlugin> availablePlugins, IReadOnlySet<Guid> deployedConfigPluginIds)
|
||||
{
|
||||
var wasChanged = false;
|
||||
var registeredSettingNames = new HashSet<string>(StringComparer.Ordinal);
|
||||
@ -289,7 +293,7 @@ public static partial class ManagedConfiguration
|
||||
configMeta.RestoreLockedConfiguration();
|
||||
|
||||
// Check the locked state:
|
||||
if (configMeta.IsLocked && configMeta.LockedByConfigPluginId != Guid.Empty && !IsPluginAvailable(configMeta.LockedByConfigPluginId, availablePlugins))
|
||||
if (configMeta.IsLocked && configMeta.LockedByConfigPluginId != Guid.Empty && !IsPluginPresent(configMeta.LockedByConfigPluginId, availablePlugins, deployedConfigPluginIds))
|
||||
{
|
||||
Log.LogInformation($"Resetting the setting '{configMeta.SettingName}': it was locked by the configuration plugin '{configMeta.LockedByConfigPluginId}', which is not available anymore.");
|
||||
configMeta.ResetLockedConfiguration();
|
||||
@ -297,11 +301,11 @@ public static partial class ManagedConfiguration
|
||||
}
|
||||
|
||||
// Check the editable default state:
|
||||
if (CleanupEditableDefaultState(configMeta, availablePlugins))
|
||||
if (CleanupEditableDefaultState(configMeta, availablePlugins, deployedConfigPluginIds))
|
||||
wasChanged = true;
|
||||
|
||||
// Check the additive plugin contribution:
|
||||
if (configMeta.HasPluginContribution && configMeta.PluginContributionByConfigPluginId != Guid.Empty && !IsPluginAvailable(configMeta.PluginContributionByConfigPluginId, availablePlugins))
|
||||
if (configMeta.HasPluginContribution && configMeta.PluginContributionByConfigPluginId != Guid.Empty && !IsPluginPresent(configMeta.PluginContributionByConfigPluginId, availablePlugins, deployedConfigPluginIds))
|
||||
{
|
||||
Log.LogInformation($"Clearing the plugin contribution for the setting '{configMeta.SettingName}': the configuration plugin '{configMeta.PluginContributionByConfigPluginId}' is not available anymore.");
|
||||
configMeta.ClearPluginContribution();
|
||||
@ -316,7 +320,15 @@ public static partial class ManagedConfiguration
|
||||
return wasChanged;
|
||||
}
|
||||
|
||||
private static bool IsPluginAvailable(Guid configPluginId, IReadOnlyCollection<IAvailablePlugin> availablePlugins) => availablePlugins.Any(x => x.Id == configPluginId);
|
||||
/// <summary>
|
||||
/// Checks whether a configuration plugin is still present on this machine.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A plugin counts as present when it was loaded, or when it is deployed but could not be loaded.
|
||||
/// The latter matters for organizations: a broken configuration plugin is still in charge, so we
|
||||
/// must not treat its settings as left over.
|
||||
/// </remarks>
|
||||
private static bool IsPluginPresent(Guid configPluginId, IReadOnlyCollection<IAvailablePlugin> availablePlugins, IReadOnlySet<Guid> deployedConfigPluginIds) => deployedConfigPluginIds.Contains(configPluginId) || availablePlugins.Any(x => x.Id == configPluginId);
|
||||
|
||||
/// <summary>
|
||||
/// Removes persisted managed states which belong to settings that are not registered anymore.
|
||||
@ -378,7 +390,7 @@ public static partial class ManagedConfiguration
|
||||
|
||||
private static bool ClearEditableDefaultState(string settingName) => SettingsManagerAccess.ConfigurationData.ManagedEditableDefaults.Remove(settingName);
|
||||
|
||||
private static bool CleanupEditableDefaultState(ConfigMetaBase configMeta, IReadOnlyCollection<IAvailablePlugin> availablePlugins)
|
||||
private static bool CleanupEditableDefaultState(ConfigMetaBase configMeta, IReadOnlyCollection<IAvailablePlugin> availablePlugins, IReadOnlySet<Guid> deployedConfigPluginIds)
|
||||
{
|
||||
if (!TryGetEditableDefaultState(configMeta.SettingName, out var editableDefaultState))
|
||||
{
|
||||
@ -389,7 +401,7 @@ public static partial class ManagedConfiguration
|
||||
return true;
|
||||
}
|
||||
|
||||
if (IsPluginAvailable(editableDefaultState.ConfigPluginId, availablePlugins))
|
||||
if (IsPluginPresent(editableDefaultState.ConfigPluginId, availablePlugins, deployedConfigPluginIds))
|
||||
return false;
|
||||
|
||||
Log.LogInformation($"Clearing the editable default of the setting '{configMeta.SettingName}': the configuration plugin '{editableDefaultState.ConfigPluginId}' is not available anymore.");
|
||||
|
||||
@ -255,6 +255,11 @@ public sealed record PluginConfigurationObject
|
||||
/// <param name="configObjectType">The type of configuration object to process.</param>
|
||||
/// <param name="configObjectSelection">A selection expression to retrieve the configuration objects from the main configuration.</param>
|
||||
/// <param name="availablePlugins">A list of currently available plugins.</param>
|
||||
/// <param name="deployedConfigPluginIds">
|
||||
/// The IDs of all configuration plugins which are deployed on this machine, including those which
|
||||
/// could not be loaded. Objects of a deployed plugin are never removed, because the plugin was not
|
||||
/// removed either.
|
||||
/// </param>
|
||||
/// <param name="configObjectList">A list of all existing configuration objects.</param>
|
||||
/// <param name="secretStoreType">An optional parameter specifying the type of secret store to use for deleting associated API keys from the OS keyring, if applicable.</param>
|
||||
/// <param name="deleteSecret">When true, delete the associated non-API-key secret from the OS keyring.</param>
|
||||
@ -263,6 +268,7 @@ public sealed record PluginConfigurationObject
|
||||
PluginConfigurationObjectType configObjectType,
|
||||
Expression<Func<Data, List<TClass>>> configObjectSelection,
|
||||
IList<IAvailablePlugin> availablePlugins,
|
||||
IReadOnlySet<Guid> deployedConfigPluginIds,
|
||||
IList<PluginConfigurationObject> configObjectList,
|
||||
SecretStoreType? secretStoreType = null,
|
||||
bool deleteSecret = false) where TClass : IConfigurationObject
|
||||
@ -281,7 +287,17 @@ public sealed record PluginConfigurationObject
|
||||
var configObjectSourcePluginId = configuredObject.EnterpriseConfigurationPluginId;
|
||||
if(configObjectSourcePluginId == Guid.Empty)
|
||||
continue;
|
||||
|
||||
|
||||
//
|
||||
// Is the source plugin deployed, but could not be loaded? Then we must not touch any of
|
||||
// its objects. The plugin was not removed, it is broken: it might be invalid Lua code,
|
||||
// a missing `plugin.lua`, or an incomplete download. Removing the objects would delete
|
||||
// the organization's providers and data sources, including their secrets, although the
|
||||
// organization still manages this AI Studio instance:
|
||||
//
|
||||
if(deployedConfigPluginIds.Contains(configObjectSourcePluginId) && availablePlugins.All(plugin => plugin.Id != configObjectSourcePluginId))
|
||||
continue;
|
||||
|
||||
// Is the source plugin still available? If not, we can be pretty sure that this configuration object is left
|
||||
// over and should be removed:
|
||||
var templateSourcePlugin = availablePlugins.FirstOrDefault(plugin => plugin.Id == configObjectSourcePluginId);
|
||||
|
||||
@ -46,19 +46,23 @@ public static partial class PluginFactory
|
||||
try
|
||||
{
|
||||
LOG.LogInformation("Start loading plugins.");
|
||||
if (!Directory.Exists(PLUGINS_ROOT))
|
||||
{
|
||||
LOG.LogInformation("No plugins found.");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// Without the plugins directory, we cannot load or start any plugin. Still, we must not
|
||||
// stop here: the clean-up at the end of this method has to run. Otherwise, settings which
|
||||
// a configuration plugin has locked would stay locked forever.
|
||||
//
|
||||
var pluginsDirectoryExists = Directory.Exists(PLUGINS_ROOT);
|
||||
if (!pluginsDirectoryExists)
|
||||
LOG.LogWarning("No plugins found. Checking for left-over configurations of removed configuration plugins.");
|
||||
|
||||
AVAILABLE_PLUGINS.Clear();
|
||||
|
||||
|
||||
//
|
||||
// The easiest way to load all plugins is to find all `plugin.lua` files and load them.
|
||||
// By convention, each plugin is enforced to have a `plugin.lua` file.
|
||||
//
|
||||
var pluginMainFiles = Directory.EnumerateFiles(PLUGINS_ROOT, "plugin.lua", SearchOption.AllDirectories);
|
||||
IEnumerable<string> pluginMainFiles = pluginsDirectoryExists ? Directory.EnumerateFiles(PLUGINS_ROOT, "plugin.lua", SearchOption.AllDirectories) : [];
|
||||
foreach (var pluginMainFile in pluginMainFiles)
|
||||
{
|
||||
try
|
||||
@ -151,8 +155,11 @@ public static partial class PluginFactory
|
||||
}
|
||||
|
||||
// Start or restart all plugins:
|
||||
var configObjects = await RestartAllPlugins(cancellationToken);
|
||||
configObjectList.AddRange(configObjects);
|
||||
if (pluginsDirectoryExists)
|
||||
{
|
||||
var configObjects = await RestartAllPlugins(cancellationToken);
|
||||
configObjectList.AddRange(configObjects);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
@ -168,31 +175,41 @@ public static partial class PluginFactory
|
||||
// =========================================================
|
||||
//
|
||||
|
||||
//
|
||||
// Configuration plugins which are deployed but could not be loaded count as present: they
|
||||
// were not removed, so everything they manage must stay as it is. Otherwise, one broken
|
||||
// configuration plugin would wipe the entire organization configuration:
|
||||
//
|
||||
var deployedConfigPluginIds = GetDeployedConfigPluginIds();
|
||||
var unloadedConfigPluginIds = deployedConfigPluginIds.Where(x => AVAILABLE_PLUGINS.All(plugin => plugin.Id != x)).ToList();
|
||||
foreach (var unloadedConfigPluginId in unloadedConfigPluginIds)
|
||||
LOG.LogWarning($"The configuration plugin '{unloadedConfigPluginId}' is deployed, but was not loaded. Everything it manages stays unchanged, because the plugin was not removed. Please check the errors above and fix the plugin.");
|
||||
|
||||
// Check LLM providers:
|
||||
var wasConfigurationChanged = await PluginConfigurationObject.CleanLeftOverConfigurationObjects(PluginConfigurationObjectType.LLM_PROVIDER, x => x.Providers, AVAILABLE_PLUGINS, configObjectList, SecretStoreType.LLM_PROVIDER);
|
||||
var wasConfigurationChanged = await PluginConfigurationObject.CleanLeftOverConfigurationObjects(PluginConfigurationObjectType.LLM_PROVIDER, x => x.Providers, AVAILABLE_PLUGINS, deployedConfigPluginIds, configObjectList, SecretStoreType.LLM_PROVIDER);
|
||||
|
||||
// Check transcription providers:
|
||||
if(await PluginConfigurationObject.CleanLeftOverConfigurationObjects(PluginConfigurationObjectType.TRANSCRIPTION_PROVIDER, x => x.TranscriptionProviders, AVAILABLE_PLUGINS, configObjectList, SecretStoreType.TRANSCRIPTION_PROVIDER))
|
||||
if(await PluginConfigurationObject.CleanLeftOverConfigurationObjects(PluginConfigurationObjectType.TRANSCRIPTION_PROVIDER, x => x.TranscriptionProviders, AVAILABLE_PLUGINS, deployedConfigPluginIds, configObjectList, SecretStoreType.TRANSCRIPTION_PROVIDER))
|
||||
wasConfigurationChanged = true;
|
||||
|
||||
// Check embedding providers:
|
||||
if(await PluginConfigurationObject.CleanLeftOverConfigurationObjects(PluginConfigurationObjectType.EMBEDDING_PROVIDER, x => x.EmbeddingProviders, AVAILABLE_PLUGINS, configObjectList, SecretStoreType.EMBEDDING_PROVIDER))
|
||||
if(await PluginConfigurationObject.CleanLeftOverConfigurationObjects(PluginConfigurationObjectType.EMBEDDING_PROVIDER, x => x.EmbeddingProviders, AVAILABLE_PLUGINS, deployedConfigPluginIds, configObjectList, SecretStoreType.EMBEDDING_PROVIDER))
|
||||
wasConfigurationChanged = true;
|
||||
|
||||
// Check data sources:
|
||||
if(await PluginConfigurationObject.CleanLeftOverConfigurationObjects(PluginConfigurationObjectType.DATA_SOURCE, x => x.DataSources, AVAILABLE_PLUGINS, configObjectList, SecretStoreType.DATA_SOURCE, deleteSecret: true))
|
||||
if(await PluginConfigurationObject.CleanLeftOverConfigurationObjects(PluginConfigurationObjectType.DATA_SOURCE, x => x.DataSources, AVAILABLE_PLUGINS, deployedConfigPluginIds, configObjectList, SecretStoreType.DATA_SOURCE, deleteSecret: true))
|
||||
wasConfigurationChanged = true;
|
||||
|
||||
// Check chat templates:
|
||||
if(await PluginConfigurationObject.CleanLeftOverConfigurationObjects(PluginConfigurationObjectType.CHAT_TEMPLATE, x => x.ChatTemplates, AVAILABLE_PLUGINS, configObjectList))
|
||||
if(await PluginConfigurationObject.CleanLeftOverConfigurationObjects(PluginConfigurationObjectType.CHAT_TEMPLATE, x => x.ChatTemplates, AVAILABLE_PLUGINS, deployedConfigPluginIds, configObjectList))
|
||||
wasConfigurationChanged = true;
|
||||
|
||||
// Check profiles:
|
||||
if(await PluginConfigurationObject.CleanLeftOverConfigurationObjects(PluginConfigurationObjectType.PROFILE, x => x.Profiles, AVAILABLE_PLUGINS, configObjectList))
|
||||
if(await PluginConfigurationObject.CleanLeftOverConfigurationObjects(PluginConfigurationObjectType.PROFILE, x => x.Profiles, AVAILABLE_PLUGINS, deployedConfigPluginIds, configObjectList))
|
||||
wasConfigurationChanged = true;
|
||||
|
||||
// Check document analysis policies:
|
||||
if(await PluginConfigurationObject.CleanLeftOverConfigurationObjects(PluginConfigurationObjectType.DOCUMENT_ANALYSIS_POLICY, x => x.DocumentAnalysis.Policies, AVAILABLE_PLUGINS, configObjectList))
|
||||
if(await PluginConfigurationObject.CleanLeftOverConfigurationObjects(PluginConfigurationObjectType.DOCUMENT_ANALYSIS_POLICY, x => x.DocumentAnalysis.Policies, AVAILABLE_PLUGINS, deployedConfigPluginIds, configObjectList))
|
||||
wasConfigurationChanged = true;
|
||||
|
||||
// Check left-over mandatory info acceptances:
|
||||
@ -201,11 +218,11 @@ public static partial class PluginFactory
|
||||
|
||||
// Check all managed settings, i.e. settings which a configuration plugin can lock,
|
||||
// provide as an editable default, or contribute to:
|
||||
if(ManagedConfiguration.CleanupLeftOverManagedConfigurations(AVAILABLE_PLUGINS))
|
||||
if(ManagedConfiguration.CleanupLeftOverManagedConfigurations(AVAILABLE_PLUGINS, deployedConfigPluginIds))
|
||||
wasConfigurationChanged = true;
|
||||
|
||||
// Compatibility shim, see documentation/compatibility-shims/2026-08-orphaned-config-locks.md (remove after 2027-08-06):
|
||||
if (RepairLegacyConfigOnlySettings())
|
||||
if (RepairLegacyConfigOnlySettings(unloadedConfigPluginIds.Count > 0))
|
||||
wasConfigurationChanged = true;
|
||||
|
||||
if (wasConfigurationChanged)
|
||||
@ -215,6 +232,38 @@ public static partial class PluginFactory
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines the IDs of all configuration plugins which are deployed on this machine.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// We read these IDs from the file system instead of taking them from the loaded plugins. A
|
||||
/// configuration plugin might be present but not loadable, e.g. due to invalid Lua code, a
|
||||
/// missing `plugin.lua`, or an incomplete download. Such a plugin still manages this AI Studio
|
||||
/// instance, so we must not treat its settings as left over. Configuration plugins deployed by a
|
||||
/// configuration server live in a directory named after their ID, which is the only information
|
||||
/// left when the plugin itself cannot be read.
|
||||
/// </remarks>
|
||||
private static HashSet<Guid> GetDeployedConfigPluginIds()
|
||||
{
|
||||
var deployedConfigPluginIds = new HashSet<Guid>();
|
||||
if (!Directory.Exists(CONFIGURATION_PLUGINS_ROOT))
|
||||
return deployedConfigPluginIds;
|
||||
|
||||
foreach (var configPluginDirectory in Directory.EnumerateDirectories(CONFIGURATION_PLUGINS_ROOT))
|
||||
{
|
||||
if (!Guid.TryParse(Path.GetFileName(configPluginDirectory), out var configPluginId) || configPluginId == Guid.Empty)
|
||||
continue;
|
||||
|
||||
// An empty directory is a left-over of a removed plugin, not a deployed plugin:
|
||||
if (!Directory.EnumerateFileSystemEntries(configPluginDirectory).Any())
|
||||
continue;
|
||||
|
||||
deployedConfigPluginIds.Add(configPluginId);
|
||||
}
|
||||
|
||||
return deployedConfigPluginIds;
|
||||
}
|
||||
|
||||
/// <param name="pluginPath">The directory the plugin is located in, or null when the code has no directory yet.</param>
|
||||
/// <param name="code">The Lua code of the plugin's main file.</param>
|
||||
/// <param name="cancellationToken">Cancellation token for running the Lua code.</param>
|
||||
@ -316,9 +365,20 @@ public static partial class PluginFactory
|
||||
/// 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>
|
||||
/// <param name="hasUnloadedConfigPlugins" >
|
||||
/// True when at least one configuration plugin is deployed but could not be loaded. In that case,
|
||||
/// we cannot tell whether a value comes from that plugin or from a removed one, so we repair
|
||||
/// nothing at all.
|
||||
/// </param>
|
||||
/// <returns>True when at least one setting was repaired, otherwise false.</returns>
|
||||
private static bool RepairLegacyConfigOnlySettings()
|
||||
private static bool RepairLegacyConfigOnlySettings(bool hasUnloadedConfigPlugins)
|
||||
{
|
||||
if (hasUnloadedConfigPlugins)
|
||||
{
|
||||
LOG.LogWarning("Skipping the repair of configuration-only settings: at least one configuration plugin is deployed, but could not be loaded. We try again the next time AI Studio starts.");
|
||||
return false;
|
||||
}
|
||||
|
||||
var data = SettingsManagerAccess.ConfigurationData;
|
||||
var wasRepaired = false;
|
||||
|
||||
|
||||
@ -6,6 +6,7 @@
|
||||
- Added the option to import plugins by dropping a plugin archive onto the plugin page.
|
||||
- Added the dedicated file extension `.mwplugin` for plugin archives.
|
||||
- Added an option for organizations to disable importing, sharing, and exporting plugins.
|
||||
- Improved how your organization's configuration behaves when a configuration plugin is present but cannot be loaded, e.g. because of an error in the plugin. Such a plugin still manages your app, so its settings, providers, data sources, profiles, and chat templates now stay in place instead of being removed.
|
||||
- Fixed reset buttons in assistants. As you may have noticed in the Document Analysis Assistant, resetting it could leave content from the previous analysis visible. Reset buttons now clear previous results completely.
|
||||
- Fixed dropping files after you closed a dialog that accepts files itself. Such a dialog takes over dropped files while it is open, but never handed that role back when you closed it. Afterwards, the chat and the assistants silently ignored dropped files until you switched to another page. Each time you opened such a dialog again, the problem got worse.
|
||||
- Fixed configuration-managed settings remaining active after their configuration plugin was removed.
|
||||
|
||||
@ -23,6 +23,8 @@ At the end of `PluginFactory.LoadAll`, AI Studio checks a fixed list of settings
|
||||
|
||||
Repairing means restoring the default value. Each repair is logged as a warning.
|
||||
|
||||
Nothing is repaired at all while a configuration plugin is deployed but could not be loaded, e.g. because of invalid Lua code. In that situation, we cannot tell whether a value comes from that plugin or from a removed one, so the repair is postponed to the next start.
|
||||
|
||||
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.
|
||||
|
||||
Loading…
Reference in New Issue
Block a user