Added a priority for configuration plugins so organizations can layer them

This commit is contained in:
Thorsten Sommer 2026-08-08 17:27:31 +02:00
parent 21f3b25341
commit 2d3f6761bb
Signed by untrusted user who does not match committer: tsommer
GPG Key ID: 371BBA77A02C0108
8 changed files with 102 additions and 10 deletions

View File

@ -27,6 +27,26 @@ TYPE = "CONFIGURATION"
-- True when this plugin is deployed by an enterprise configuration server: -- True when this plugin is deployed by an enterprise configuration server:
DEPLOYED_USING_CONFIG_SERVER = false DEPLOYED_USING_CONFIG_SERVER = false
-- The priority of this configuration plugin. Optional, defaults to 0.
--
-- It only matters when your organization deploys more than one configuration
-- plugin. A plugin with a higher priority is applied later and therefore wins
-- whenever two of your configuration plugins manage the same setting or define
-- the same object, e.g. the same LLM provider.
--
-- A typical setup: deploy one base configuration for everybody with PRIORITY = 0
-- and one configuration per department with PRIORITY = 100. The department
-- configuration may then override the default model, while everything it does
-- not mention stays at the values of the base configuration.
--
-- Give two plugins that must override each other different priorities. With an
-- equal priority, the order is stable but arbitrary.
--
-- The priority never lifts a local configuration plugin above one of your
-- organization: configuration plugins your IT department deployed are always
-- applied first, whatever a local plugin declares.
PRIORITY = 0
-- The authors of the plugin: -- The authors of the plugin:
AUTHORS = {"<Company Name>"} AUTHORS = {"<Company Name>"}

View File

@ -7,4 +7,15 @@ public interface IAvailablePlugin : IPluginMetadata
public bool IsManagedByConfigServer { get; } public bool IsManagedByConfigServer { get; }
public Guid? ManagedConfigurationId { get; } public Guid? ManagedConfigurationId { get; }
/// <summary>
/// The priority of a configuration plugin. Zero for every other plugin type.
/// </summary>
/// <remarks>
/// Configuration plugins with a higher priority start later and therefore win when two of them
/// manage the same setting or define the same configuration object. The priority only orders
/// plugins of the same origin: a local configuration plugin never starts before one which an
/// organization deployed, no matter which priority it declares.
/// </remarks>
public int ConfigurationPriority { get; }
} }

View File

@ -39,6 +39,17 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
/// </summary> /// </summary>
public bool? DeployedUsingConfigServer { get; } = ReadDeployedUsingConfigServer(state); public bool? DeployedUsingConfigServer { get; } = ReadDeployedUsingConfigServer(state);
/// <summary>
/// The priority of this configuration plugin. Defaults to zero when the plugin declares none.
/// </summary>
/// <remarks>
/// Configuration plugins with a higher priority are applied later and therefore win when two of
/// them manage the same setting or define the same configuration object. This lets an
/// organization deploy one base configuration for everybody and additional configurations which
/// refine it, e.g. per department.
/// </remarks>
public int Priority { get; } = ReadPriority(state);
public async Task InitializeAsync(bool dryRun) public async Task InitializeAsync(bool dryRun)
{ {
if(!this.TryProcessConfiguration(dryRun, out var issue)) if(!this.TryProcessConfiguration(dryRun, out var issue))
@ -129,6 +140,14 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
return null; return null;
} }
private static int ReadPriority(LuaState state)
{
if (state.Environment["PRIORITY"].TryRead<int>(out var priority))
return priority;
return 0;
}
/// <summary> /// <summary>
/// Tries to initialize the UI text content of the plugin. /// Tries to initialize the UI text content of the plugin.
/// </summary> /// </summary>

View File

@ -132,8 +132,10 @@ public static partial class PluginFactory
var isConfigurationPluginInConfigDirectory = plugin.Type is PluginType.CONFIGURATION && IsEnterpriseConfigurationPath(pluginPath); var isConfigurationPluginInConfigDirectory = plugin.Type is PluginType.CONFIGURATION && IsEnterpriseConfigurationPath(pluginPath);
var isManagedByConfigServer = false; var isManagedByConfigServer = false;
Guid? managedConfigurationId = null; Guid? managedConfigurationId = null;
var configurationPriority = 0;
if (plugin is PluginConfiguration configPlugin) if (plugin is PluginConfiguration configPlugin)
{ {
configurationPriority = configPlugin.Priority;
if (configPlugin.DeployedUsingConfigServer.HasValue) if (configPlugin.DeployedUsingConfigServer.HasValue)
isManagedByConfigServer = configPlugin.DeployedUsingConfigServer.Value; isManagedByConfigServer = configPlugin.DeployedUsingConfigServer.Value;
@ -161,7 +163,7 @@ public static partial class PluginFactory
LOG.LogWarning($"Could not determine the managed configuration ID for configuration plugin '{plugin.Id}'. The plugin directory '{pluginPath}' does not end with a valid GUID."); LOG.LogWarning($"Could not determine the managed configuration ID for configuration plugin '{plugin.Id}'. The plugin directory '{pluginPath}' does not end with a valid GUID.");
} }
AVAILABLE_PLUGINS.Add(new PluginMetadata(plugin, pluginPath, isManagedByConfigServer, managedConfigurationId)); AVAILABLE_PLUGINS.Add(new PluginMetadata(plugin, pluginPath, isManagedByConfigServer, managedConfigurationId, configurationPriority));
} }
catch (Exception e) catch (Exception e)
{ {

View File

@ -53,17 +53,24 @@ public static partial class PluginFactory
// //
// Iterate over all available plugins and try to start them. We do that in a deterministic // Iterate over all available plugins and try to start them. We do that in a deterministic
// order, starting with the configuration plugins of the organization. Two reasons: // order, starting with the configuration plugins of the organization. Three reasons:
// //
// - Configuration plugins write settings and configuration objects. Whoever writes one // - Configuration plugins write settings and configuration objects. Whoever writes one
// first owns it, so the organization has to come first: its configuration is the baseline // first owns it, so the organization has to come first: its configuration is the baseline
// every other plugin has to respect. // every other plugin has to respect.
// //
// - Within one origin, the declared priority decides. An organization can deploy a base
// configuration for everybody and refine it, e.g. per department: the higher priority is
// applied later and therefore wins.
//
// - Without an explicit order, the sequence is the one Directory.EnumerateFiles produced in // - Without an explicit order, the sequence is the one Directory.EnumerateFiles produced in
// LoadAll. That order is not guaranteed, so the same installation could behave // LoadAll. That order is not guaranteed, so the same installation could behave
// differently on two machines. // differently on two machines. The plugin directory breaks any remaining tie.
// //
foreach (var availablePlugin in AVAILABLE_PLUGINS.OrderBy(GetStartupRank).ThenBy(plugin => plugin.LocalPath, StringComparer.OrdinalIgnoreCase)) foreach (var availablePlugin in AVAILABLE_PLUGINS
.OrderBy(GetStartupRank)
.ThenBy(plugin => plugin.ConfigurationPriority)
.ThenBy(plugin => plugin.LocalPath, StringComparer.OrdinalIgnoreCase))
{ {
if(cancellationToken.IsCancellationRequested) if(cancellationToken.IsCancellationRequested)
{ {
@ -105,7 +112,9 @@ public static partial class PluginFactory
/// The configuration plugins an organization deployed go first: they are the baseline for /// The configuration plugins an organization deployed go first: they are the baseline for
/// everything else. Local configuration plugins follow, so they can add to that baseline instead /// everything else. Local configuration plugins follow, so they can add to that baseline instead
/// of replacing parts of it. All remaining plugin types write no settings at all, so their rank /// of replacing parts of it. All remaining plugin types write no settings at all, so their rank
/// is irrelevant for the outcome. /// is irrelevant for the outcome.<br/><br/>
/// The rank comes before the declared priority on purpose: a local configuration plugin must not
/// be able to jump ahead of an organization by declaring a high priority.
/// </remarks> /// </remarks>
/// <param name="plugin">The plugin about to be started.</param> /// <param name="plugin">The plugin about to be started.</param>
/// <returns>The startup rank of the plugin.</returns> /// <returns>The startup rank of the plugin.</returns>

View File

@ -1,6 +1,6 @@
namespace AIStudio.Tools.PluginSystem; namespace AIStudio.Tools.PluginSystem;
public sealed class PluginMetadata(PluginBase plugin, string localPath, bool isManagedByConfigServer = false, Guid? managedConfigurationId = null) : IAvailablePlugin public sealed class PluginMetadata(PluginBase plugin, string localPath, bool isManagedByConfigServer = false, Guid? managedConfigurationId = null, int configurationPriority = 0) : IAvailablePlugin
{ {
#region Implementation of IPluginMetadata #region Implementation of IPluginMetadata
@ -56,5 +56,8 @@ public sealed class PluginMetadata(PluginBase plugin, string localPath, bool isM
public Guid? ManagedConfigurationId { get; } = managedConfigurationId; public Guid? ManagedConfigurationId { get; } = managedConfigurationId;
/// <inheritdoc />
public int ConfigurationPriority { get; } = configurationPriority;
#endregion #endregion
} }

View File

@ -6,6 +6,7 @@
- Added the option to import plugins by dropping a plugin archive onto the plugin page. - 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 the dedicated file extension `.mwplugin` for plugin archives.
- Added an option for organizations to disable importing, sharing, and exporting plugins. - 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. - 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 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 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.

View File

@ -54,7 +54,7 @@ The preferred format is a fixed set of indexed pairs:
Each configuration ID must be a valid [GUID](https://en.wikipedia.org/wiki/Universally_unique_identifier#Globally_unique_identifier). Up to 100,000 indexed configuration slots are supported per device. Each configuration ID must be a valid [GUID](https://en.wikipedia.org/wiki/Universally_unique_identifier#Globally_unique_identifier). Up to 100,000 indexed configuration slots are supported per device.
If multiple configurations define the same setting, the first definition wins. For indexed pairs and policy files, the order is slot `00000`, then `00001`, and so on up to `99999`. The slot order determines which configurations are downloaded, not which one wins a conflict. When two of your configuration plugins define the same setting or the same object, the declared priority decides. See [Priority of configuration plugins](#priority-of-configuration-plugins).
For backwards compatibility, the older slot names `0` to `9` without an underscore are still supported. AI Studio also accepts other numeric slot suffixes with up to five digits. Slot suffixes are matched exactly, so `config_id_1`, `config_id_01`, and `config_id_00001` are treated as separate slots. Use the five-digit format with an underscore for new deployments. For backwards compatibility, the older slot names `0` to `9` without an underscore are still supported. AI Studio also accepts other numeric slot suffixes with up to five digits. Slot suffixes are matched exactly, so `config_id_1`, `config_id_01`, and `config_id_00001` are treated as separate slots. Use the five-digit format with an underscore for new deployments.
@ -284,6 +284,33 @@ DEPLOYED_USING_CONFIG_SERVER = true
Local, manually managed configuration plugins should set this to `false`. If the field is missing, AI Studio falls back to the plugin path (`.config`) to determine whether the plugin is managed and logs a warning. Local, manually managed configuration plugins should set this to `false`. If the field is missing, AI Studio falls back to the plugin path (`.config`) to determine whether the plugin is managed and logs a warning.
## Priority of configuration plugins
When you deploy more than one configuration, two of your configuration plugins may manage the same setting or define the same object, e.g. the same LLM provider. The optional `PRIORITY` field decides which one wins:
```lua
PRIORITY = 100
```
A configuration plugin with a higher priority is applied later and therefore wins. The field is optional and defaults to `0`.
A typical layered setup:
| Configuration | `PRIORITY` | Role |
|---|---|---|
| Organization-wide base | `0` | Providers, update behavior, and security settings for everybody |
| Department | `100` | Refines the base, e.g. a different default model |
| Project or lab | `200` | Refines the department configuration |
A configuration only overrides what it actually defines. Everything it does not mention keeps the value of the configuration below it. The same applies when you remove a configuration later: its settings fall back to the configuration below, not to the AI Studio defaults.
Give two configurations that must override each other different priorities. With an equal priority, the order is stable across restarts but arbitrary, so the outcome is not the one you designed.
Two guarantees are independent of the priority:
- A local configuration plugin never wins against one your IT department deployed, whatever priority it declares. Local plugins are always applied afterwards, and they may not take over a setting or an object that belongs to one of your configurations.
- Two plugins must not share the same plugin ID. If that happens, AI Studio keeps the one your IT department deployed and logs a warning for the other.
## Example AI Studio configuration ## Example AI Studio configuration
The latest example of an AI Studio configuration via configuration plugin can always be found in the repository in the `app/MindWork AI Studio/Plugins/configuration` folder. Here are the links to the files: The latest example of an AI Studio configuration via configuration plugin can always be found in the repository in the `app/MindWork AI Studio/Plugins/configuration` folder. Here are the links to the files: