diff --git a/app/MindWork AI Studio/Plugins/configuration/plugin.lua b/app/MindWork AI Studio/Plugins/configuration/plugin.lua index 44bb1289..e3551ba3 100644 --- a/app/MindWork AI Studio/Plugins/configuration/plugin.lua +++ b/app/MindWork AI Studio/Plugins/configuration/plugin.lua @@ -233,8 +233,9 @@ CONFIG["SETTINGS"] = {} -- that setting. That is intentional: replacing is the only way a department can take -- something back that the base configuration has set. -- --- The affected settings below carry a note. The one exception is --- DataApp.EnabledPreviewFeatures, which adds up across configurations. +-- The affected settings below carry a note. Two settings are the exception and add up +-- across configurations instead: DataApp.EnabledPreviewFeatures and +-- DataAssistantPluginAudit.EnterpriseApprovedPlugins. -- ------ -- Configure the update check interval: @@ -460,10 +461,10 @@ CONFIG["SETTINGS"] = {} -- You can generate the exact hash with the build-script command: -- dotnet run --project app/Build -- assistant-plugin-hash "" --lua-snippet -- --- Replaces, does not merge: a configuration with a higher priority replaces this list --- completely, so a department configuration listing two approvals withdraws every --- approval of the base configuration. Those assistant plugins then require a security --- audit again. Repeat the approvals of the base configuration here to keep them. +-- Adds up, does not replace: approvals of all your configurations are combined, so a +-- department configuration can approve additional assistant plugins without repeating +-- the approvals of the base configuration. Each configuration keeps its own approvals, +-- so removing one of them only withdraws the approvals it had granted. -- CONFIG["SETTINGS"]["DataAssistantPluginAudit.EnterpriseApprovedPlugins"] = { -- { -- ["PluginHash"] = "0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF", diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginConfiguration.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginConfiguration.cs index 509cf2fc..8b078288 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/PluginConfiguration.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginConfiguration.cs @@ -358,16 +358,67 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT switch (successful) { case true: - configMeta.SetValue(configuredApprovals); + // + // Approvals of several configuration plugins add up. An approval list is a pure + // allowlist over hashes: not listing a plugin already means "not approved", so + // replacing the list would only ever withdraw the approvals of another + // configuration without expressing anything new. + // + configMeta.SetPluginContribution(configuredApprovals, this.Id); + + // Merge into the stored list right away, so the approvals of this plugin take + // effect immediately. PluginFactory.LoadAll recomputes the authoritative list once + // every configuration plugin has contributed: + var mergedApprovals = new List(configMeta.GetValue()); + var knownHashes = mergedApprovals.Select(approval => approval.PluginHash).ToHashSet(StringComparer.Ordinal); + mergedApprovals.AddRange(configuredApprovals.Where(approval => knownHashes.Add(approval.PluginHash))); + + configMeta.SetValue(mergedApprovals); configMeta.LockConfiguration(this.Id); break; case false when configMeta.IsLocked && configMeta.LockedByConfigPluginId == this.Id: + configMeta.RemovePluginContribution(this.Id); configMeta.ResetLockedConfiguration(); break; + + case false: + configMeta.RemovePluginContribution(this.Id); + break; } } + /// + /// Recomputes the effective enterprise approvals from the contributions of all configuration plugins. + /// + /// + /// Every configuration plugin merges its own approvals into the stored list while it starts, but + /// nothing there can withdraw the approvals of a plugin which was removed in the meantime. This + /// method rebuilds the list from the remaining contributions and is therefore called once all + /// configuration plugins have been started. + /// + /// True when the effective approvals changed, otherwise false. + public static bool RefreshEnterpriseApprovedAssistantPlugins() + { + if (!ManagedConfiguration.TryGet(x => x.AssistantPluginAudit, x => x.EnterpriseApprovedPlugins, out ConfigMeta> configMeta)) + return false; + + var effectiveApprovals = new List(); + var effectiveHashes = new HashSet(StringComparer.Ordinal); + foreach (var approval in configMeta.PluginContributions.Values.SelectMany(contribution => contribution)) + if (effectiveHashes.Add(approval.PluginHash)) + effectiveApprovals.Add(approval); + + // Compare by hash, so a different order alone does not rewrite the settings on every start: + var currentApprovals = configMeta.GetValue(); + if (currentApprovals.Count == effectiveApprovals.Count && effectiveHashes.SetEquals(currentApprovals.Select(approval => approval.PluginHash))) + return false; + + LOG.LogInformation($"The enterprise approvals for assistant plugins changed from {currentApprovals.Count} to {effectiveApprovals.Count} entries, contributed by {configMeta.PluginContributions.Count} configuration plugin(s)."); + configMeta.SetValue(effectiveApprovals); + return true; + } + private static bool TryParseEnterpriseApprovedAssistantPlugin(int index, LuaTable table, Guid configPluginId, out DataAssistantPluginEnterpriseApproval approval) { approval = new(); diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Loading.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Loading.cs index 3b229233..9a9f1640 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Loading.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Loading.cs @@ -239,6 +239,16 @@ public static partial class PluginFactory if(ManagedConfiguration.CleanupLeftOverManagedConfigurations(AVAILABLE_PLUGINS, deployedEnterpriseConfigPluginIds)) wasConfigurationChanged = true; + // + // The enterprise approvals of all configuration plugins add up. Now that every plugin has + // contributed and the clean-up above has dropped the removed ones, we rebuild the effective + // list. We skip that while a configuration plugin is deployed but could not be loaded: its + // approvals are missing from the contributions, and withdrawing them would demand a new + // security audit for assistant plugins the organization has approved: + // + if(unloadedEnterpriseConfigPluginIds.Count == 0 && PluginConfiguration.RefreshEnterpriseApprovedAssistantPlugins()) + wasConfigurationChanged = true; + // Compatibility shim, see documentation/compatibility-shims/2026-08-orphaned-config-locks.md (remove after 2027-08-06): if (RepairLegacyConfigOnlySettings(unloadedEnterpriseConfigPluginIds.Count > 0)) wasConfigurationChanged = true; diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Starting.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Starting.cs index 888ff096..d9c8abb1 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Starting.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Starting.cs @@ -129,16 +129,25 @@ public static partial class PluginFactory private static void LogAssistantPluginStartupState() { ManagedConfiguration.TryGet(x => x.AssistantPluginAudit, x => x.EnterpriseApprovedPlugins, out ConfigMeta> configMeta); - var approvedByConfigPluginId = configMeta is { IsLocked: true } ? configMeta.LockedByConfigPluginId : Guid.Empty; - var approvedByConfigPluginName = approvedByConfigPluginId == Guid.Empty - ? string.Empty - : AVAILABLE_PLUGINS.FirstOrDefault(x => x.Id == approvedByConfigPluginId)?.Name ?? string.Empty; foreach (var assistantPlugin in RUNNING_PLUGINS.OfType()) { var securityState = PluginAssistantSecurityResolver.Resolve(SettingsManagerAccess, assistantPlugin); if (securityState.IsEnterpriseApproved) { + // + // Several configuration plugins may approve assistant plugins. We look up the one + // which approved this particular plugin instead of naming an arbitrary contributor: + // + var approvedByConfigPluginId = configMeta.PluginContributions + .Where(contribution => contribution.Value.Any(approval => string.Equals(approval.PluginHash, securityState.CurrentHash, StringComparison.Ordinal))) + .Select(contribution => contribution.Key) + .FirstOrDefault(); + + var approvedByConfigPluginName = approvedByConfigPluginId == Guid.Empty + ? string.Empty + : AVAILABLE_PLUGINS.FirstOrDefault(x => x.Id == approvedByConfigPluginId)?.Name ?? string.Empty; + LOG.LogInformation( $"Successfully started assistant plugin: Id='{assistantPlugin.Id}', Type='{assistantPlugin.Type}', Name='{assistantPlugin.Name}', Version='{assistantPlugin.Version}', SecuritySource='EnterpriseApproval', ApprovedByConfigPluginId='{approvedByConfigPluginId}', ApprovedByConfigPluginName='{approvedByConfigPluginName}'"); continue; 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 d347d305..34a69f31 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v26.8.1.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v26.8.1.md @@ -8,6 +8,7 @@ - Added an option for organizations to disable importing, sharing, and exporting plugins. - Added a priority for configuration plugins. Organizations that deploy several configurations can now decide which one wins: a configuration with a higher priority overrides the settings and providers of a lower one. This allows a company-wide base configuration that each department refines for itself. - 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. +- Changed how approvals for assistant plugins combine when your organization deploys several configurations. They now add up, so a department can approve additional assistant plugins without repeating the approvals of the company-wide configuration. Previously, the last configuration replaced all earlier approvals, which silently required a new security check for those assistants. - 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. diff --git a/documentation/Enterprise IT.md b/documentation/Enterprise IT.md index b497c3b9..831be75a 100644 --- a/documentation/Enterprise IT.md +++ b/documentation/Enterprise IT.md @@ -322,7 +322,6 @@ Plan for it in these settings: | Setting | What a partial list costs you | |---|---| | `DataApp.HiddenAssistants` | Assistants hidden by the base configuration become **visible** again | -| `DataAssistantPluginAudit.EnterpriseApprovedPlugins` | Approvals of the base configuration are withdrawn, so those assistant plugins require a security audit again | | `DataSourceSecuritySettings.TrustedProviderIds` | Providers trusted by the base configuration lose that status | | `DataApp.ExternalHttpCustomRootCertificateAllowedHosts` | Hosts of the base configuration stop trusting your root certificates | | `DataConfidence.CustomConfidenceScheme` | Providers left out fall back to the AI Studio default confidence | @@ -330,7 +329,12 @@ Plan for it in these settings: The rule of thumb: whenever a configuration with a higher priority touches one of these settings, it has to repeat every entry it wants to keep. Watch `DataApp.HiddenAssistants` in particular, because it is the only one in this list that opens something up instead of restricting it. -`DataApp.EnabledPreviewFeatures` is the exception. Preview features add up: enable one for the whole organization and another one for a single department, and users of that department get both. Each configuration keeps its own contribution, so removing one of them only withdraws the features that this configuration had enabled. +Two settings are the exception and add up instead of replacing: + +- `DataApp.EnabledPreviewFeatures` — enable one preview feature for the whole organization and another one for a single department, and users of that department get both. +- `DataAssistantPluginAudit.EnterpriseApprovedPlugins` — a department configuration can approve additional assistant plugins without repeating the approvals of the base configuration. Approving is a pure allowlist over hashes, so there is nothing a replacing list could express that adding does not. + +In both cases each configuration keeps its own contribution, so removing one of them only withdraws what this configuration had granted. While a configuration plugin is deployed but cannot be loaded, its approvals are kept: AI Studio does not withdraw approvals it cannot currently read. One clarification for `DataChat.PreselectedDataSourceIds`: the IDs are not limited to the data sources of the same configuration. They are resolved against every known data source, including those of your other configurations and the ones a user configured. IDs that resolve to nothing are ignored.