mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-09-01 20:09:15 +00:00
Improved enterprise plugin rollouts and protected deployed plugins against changes (#938)
This commit is contained in:
parent
a4c35fc2f2
commit
3940ad7f29
@ -5014,6 +5014,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::ASSISTANTPLUGINEDITORDIALOG::T1294818664"] =
|
||||
-- The assistant plugin could not be resolved.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::ASSISTANTPLUGINEDITORDIALOG::T1823819434"] = "The assistant plugin could not be resolved."
|
||||
|
||||
-- Only locally managed assistant plugins can be edited.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::ASSISTANTPLUGINEDITORDIALOG::T2477919452"] = "Only locally managed assistant plugins can be edited."
|
||||
|
||||
-- The assistant plugin could not be loaded: {0}
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::ASSISTANTPLUGINEDITORDIALOG::T2486953475"] = "The assistant plugin could not be loaded: {0}"
|
||||
|
||||
@ -10873,6 +10876,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T2350673880"]
|
||||
-- The generated assistant plugin uses the ID of another installed plugin.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T2441747251"] = "The generated assistant plugin uses the ID of another installed plugin."
|
||||
|
||||
-- Only locally managed assistant plugins can be edited.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T2477919452"] = "Only locally managed assistant plugins can be edited."
|
||||
|
||||
-- This individual plugin’s directory is outside the expected plugins directory.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T2486199999"] = "This individual plugin’s directory is outside the expected plugins directory."
|
||||
|
||||
|
||||
@ -72,6 +72,14 @@ public partial class AssistantPluginEditorDialog : MSGComponentBase
|
||||
return;
|
||||
}
|
||||
|
||||
// An assistant an organization rolled out must keep the content its enterprise approval
|
||||
// was granted for, so only its IT department may change it:
|
||||
if (this.plugin.IsManagedByConfigServer)
|
||||
{
|
||||
this.issue = T("Only locally managed assistant plugins can be edited.");
|
||||
return;
|
||||
}
|
||||
|
||||
this.pluginFile = Path.Join(this.plugin.LocalPath, PLUGIN_FILE_NAME);
|
||||
if (!File.Exists(this.pluginFile))
|
||||
{
|
||||
|
||||
@ -227,7 +227,7 @@ public partial class Plugins : MSGComponentBase
|
||||
// transient state like an ongoing share: they gate the markup, so a transient value would make
|
||||
// the action buttons disappear and reappear. Transient state belongs into the buttons' Disabled.
|
||||
//
|
||||
private static bool CanEditAssistantPlugin(IAvailablePlugin plugin) => plugin is { IsInternal: false, Type: PluginType.ASSISTANT } && !string.IsNullOrWhiteSpace(plugin.LocalPath);
|
||||
private static bool CanEditAssistantPlugin(IAvailablePlugin plugin) => plugin is { IsInternal: false, IsManagedByConfigServer: false, Type: PluginType.ASSISTANT } && !string.IsNullOrWhiteSpace(plugin.LocalPath);
|
||||
|
||||
/// <summary>
|
||||
/// Whether this plugin is a direct chat launcher whose settings can be changed without AI.
|
||||
|
||||
@ -5016,6 +5016,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::ASSISTANTPLUGINEDITORDIALOG::T1294818664"] =
|
||||
-- The assistant plugin could not be resolved.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::ASSISTANTPLUGINEDITORDIALOG::T1823819434"] = "Das Assistenten-Plugin konnte nicht aufgelöst werden."
|
||||
|
||||
-- Only locally managed assistant plugins can be edited.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::ASSISTANTPLUGINEDITORDIALOG::T2477919452"] = "Nur lokal verwaltete Assistant-Plugins können bearbeitet werden."
|
||||
|
||||
-- The assistant plugin could not be loaded: {0}
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::ASSISTANTPLUGINEDITORDIALOG::T2486953475"] = "Das Assistenten-Plugin konnte nicht geladen werden: {0}"
|
||||
|
||||
@ -10875,6 +10878,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T2350673880"]
|
||||
-- The generated assistant plugin uses the ID of another installed plugin.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T2441747251"] = "Das generierte Assistenten-Plugin verwendet die ID eines anderen installierten Plugins."
|
||||
|
||||
-- Only locally managed assistant plugins can be edited.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T2477919452"] = "Nur lokal verwaltete Assistant-Plugins können bearbeitet werden."
|
||||
|
||||
-- This individual plugin’s directory is outside the expected plugins directory.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T2486199999"] = "Das Verzeichnis dieses einzelnen Plugins liegt außerhalb des erwarteten Plugin-Verzeichnisses."
|
||||
|
||||
|
||||
@ -5016,6 +5016,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::ASSISTANTPLUGINEDITORDIALOG::T1294818664"] =
|
||||
-- The assistant plugin could not be resolved.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::ASSISTANTPLUGINEDITORDIALOG::T1823819434"] = "The assistant plugin could not be resolved."
|
||||
|
||||
-- Only locally managed assistant plugins can be edited.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::ASSISTANTPLUGINEDITORDIALOG::T2477919452"] = "Only locally managed assistant plugins can be edited."
|
||||
|
||||
-- The assistant plugin could not be loaded: {0}
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::ASSISTANTPLUGINEDITORDIALOG::T2486953475"] = "The assistant plugin could not be loaded: {0}"
|
||||
|
||||
@ -10875,6 +10878,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T2350673880"]
|
||||
-- The generated assistant plugin uses the ID of another installed plugin.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T2441747251"] = "The generated assistant plugin uses the ID of another installed plugin."
|
||||
|
||||
-- Only locally managed assistant plugins can be edited.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T2477919452"] = "Only locally managed assistant plugins can be edited."
|
||||
|
||||
-- This individual plugin’s directory is outside the expected plugins directory.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T2486199999"] = "This individual plugin’s directory is outside the expected plugins directory."
|
||||
|
||||
|
||||
@ -133,38 +133,58 @@ public static partial class PluginFactory
|
||||
AVAILABLE_PLUGINS.Remove(duplicatePlugin);
|
||||
}
|
||||
|
||||
var isConfigurationPluginInConfigDirectory = plugin.Type is PluginType.CONFIGURATION && IsEnterpriseConfigurationPath(pluginPath);
|
||||
var isManagedByConfigServer = false;
|
||||
//
|
||||
// An organization may deploy any kind of plugin, not just configurations: the
|
||||
// archive it serves under a configuration ID often carries an assistant plugin
|
||||
// in a subdirectory as well. Everything stored below one of the organization's
|
||||
// directories therefore belongs to that organization, whatever its type is and
|
||||
// however deeply it is nested:
|
||||
//
|
||||
var isInOrganizationDirectory = IsOrganizationConfigurationPath(pluginPath);
|
||||
|
||||
Guid? managedConfigurationId = null;
|
||||
var configurationPriority = 0;
|
||||
bool? declaredAsManagedByConfigServer = null;
|
||||
if (plugin is PluginConfiguration configPlugin)
|
||||
{
|
||||
configurationPriority = configPlugin.Priority;
|
||||
if (configPlugin.DeployedUsingConfigServer.HasValue)
|
||||
isManagedByConfigServer = configPlugin.DeployedUsingConfigServer.Value;
|
||||
|
||||
else if (isConfigurationPluginInConfigDirectory)
|
||||
{
|
||||
isManagedByConfigServer = true;
|
||||
LOG.LogWarning($"The configuration plugin '{plugin.Id}' does not define 'DEPLOYED_USING_CONFIG_SERVER'. Falling back to the plugin path and treating it as managed because it is stored under '{ENTERPRISE_CONFIGURATION_PLUGINS_ROOT}'.");
|
||||
}
|
||||
declaredAsManagedByConfigServer = configPlugin.DeployedUsingConfigServer;
|
||||
}
|
||||
else if (plugin is PluginAssistants assistantPlugin)
|
||||
isManagedByConfigServer = assistantPlugin.IsManagedByConfigServer;
|
||||
else if (plugin is PluginAssistants { HasDeploymentManagementMetadata: true } assistantPlugin)
|
||||
declaredAsManagedByConfigServer = assistantPlugin.IsManagedByConfigServer;
|
||||
|
||||
// For configuration plugins, validate that the plugin ID matches the enterprise config ID
|
||||
// (the directory name under which the plugin was downloaded):
|
||||
if (isConfigurationPluginInConfigDirectory && isManagedByConfigServer)
|
||||
//
|
||||
// The plugin path outranks what a plugin declares about itself. A plugin an
|
||||
// organization deployed could otherwise deny it and escape the withdrawal of that
|
||||
// configuration, while keeping every right the directory grants it:
|
||||
//
|
||||
var isManagedByConfigServer = isInOrganizationDirectory || declaredAsManagedByConfigServer is true;
|
||||
switch (declaredAsManagedByConfigServer)
|
||||
{
|
||||
var directoryName = Path.GetFileName(pluginPath);
|
||||
if (Guid.TryParse(directoryName, out var enterpriseConfigId))
|
||||
case null when isInOrganizationDirectory:
|
||||
LOG.LogWarning($"The {plugin.Type} plugin '{plugin.Id}' does not define 'DEPLOYED_USING_CONFIG_SERVER'. Falling back to the plugin path and treating it as managed because it is stored under '{pluginPath}'.");
|
||||
break;
|
||||
|
||||
case false when isInOrganizationDirectory:
|
||||
LOG.LogWarning($"The {plugin.Type} plugin '{plugin.Id}' declares 'DEPLOYED_USING_CONFIG_SERVER = false', but it is stored under '{pluginPath}' and therefore belongs to your organization. Treating it as managed. Please fix the plugin.");
|
||||
break;
|
||||
}
|
||||
|
||||
//
|
||||
// Which configuration a plugin was deployed with is what ties it to the archive it
|
||||
// came from. Only the configuration plugin itself must carry the configuration ID
|
||||
// as its own ID: a plugin deployed alongside it has an ID of its own:
|
||||
//
|
||||
if (IsEnterpriseConfigurationPath(pluginPath))
|
||||
{
|
||||
if (TryGetDeployedConfigurationId(pluginPath, out var enterpriseConfigId))
|
||||
{
|
||||
managedConfigurationId = enterpriseConfigId;
|
||||
if (enterpriseConfigId != plugin.Id)
|
||||
if (plugin.Type is PluginType.CONFIGURATION && enterpriseConfigId != plugin.Id)
|
||||
LOG.LogWarning($"The configuration plugin's ID ('{plugin.Id}') does not match the enterprise configuration ID ('{enterpriseConfigId}'). These IDs should be identical. Please update the plugin's ID field to match the enterprise configuration ID.");
|
||||
}
|
||||
else
|
||||
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 the {plugin.Type} plugin '{plugin.Id}'. The plugin directory '{pluginPath}' is not nested in a directory named after a configuration ID.");
|
||||
}
|
||||
|
||||
AVAILABLE_PLUGINS.Add(new PluginMetadata(plugin, pluginPath, isManagedByConfigServer, managedConfigurationId, configurationPriority));
|
||||
@ -212,9 +232,28 @@ public static partial class PluginFactory
|
||||
foreach (var testConfigurationPlugin in AVAILABLE_PLUGINS.Where(plugin => plugin.Type is PluginType.CONFIGURATION && IsEnterpriseTestConfigurationPath(plugin.LocalPath)))
|
||||
deployedEnterpriseConfigPluginIds.Add(testConfigurationPlugin.Id);
|
||||
|
||||
//
|
||||
// A deployment does not have to contain a configuration plugin under its own ID: an
|
||||
// organization uses the same channel to roll out assistant plugins and other plugin types.
|
||||
// We therefore collect which deployments contributed a plugin at all, so that such a rollout
|
||||
// is not mistaken for a configuration nobody could read:
|
||||
//
|
||||
var configurationIdsWithLoadedPlugins = AVAILABLE_PLUGINS
|
||||
.Where(plugin => plugin.ManagedConfigurationId.HasValue)
|
||||
.Select(plugin => plugin.ManagedConfigurationId!.Value)
|
||||
.ToHashSet();
|
||||
|
||||
var unloadedEnterpriseConfigPluginIds = deployedEnterpriseConfigPluginIds.Where(x => AVAILABLE_PLUGINS.All(plugin => plugin.Id != x)).ToList();
|
||||
foreach (var unloadedEnterpriseConfigPluginId in unloadedEnterpriseConfigPluginIds)
|
||||
{
|
||||
if (configurationIdsWithLoadedPlugins.Contains(unloadedEnterpriseConfigPluginId))
|
||||
{
|
||||
LOG.LogInformation($"The deployment '{unloadedEnterpriseConfigPluginId}' contains no configuration plugin of its own, but other plugins your organization deployed with it were loaded. Should you expect a configuration plugin here, please check the errors above.");
|
||||
continue;
|
||||
}
|
||||
|
||||
LOG.LogWarning($"The configuration plugin '{unloadedEnterpriseConfigPluginId}' 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, deployedEnterpriseConfigPluginIds, configObjectList, SecretStoreType.LLM_PROVIDER);
|
||||
|
||||
@ -13,24 +13,33 @@ public static partial class PluginFactory
|
||||
private static string INTERNAL_PLUGINS_ROOT = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The directory the config server downloads the configuration plugins of an organization into.
|
||||
/// The directory the config server downloads the plugins of an organization into.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is not the home of configuration plugins in general: a local configuration plugin can
|
||||
/// live in any directory below the plugins root. Only the IT department of an organization
|
||||
/// deploys plugins here, each in a directory named after its configuration ID.
|
||||
/// deploys plugins here, each deployment in a directory named after its configuration ID.<br/><br/>
|
||||
/// A deployment is not limited to a configuration, even though the directory name says so. An
|
||||
/// organization serves one archive per configuration ID and uses it for every kind of plugin:
|
||||
/// assistants, languages, themes, and whatever else follows. Those plugins live in
|
||||
/// subdirectories, each with its own plugin.lua and its own plugin ID, and only the
|
||||
/// configuration plugin itself carries the configuration ID as its ID. Everything below such a
|
||||
/// deployment belongs to the organization, whatever its type is and however deeply it is nested.
|
||||
/// </remarks>
|
||||
private static string ENTERPRISE_CONFIGURATION_PLUGINS_ROOT = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The directory administrators use to try out a configuration before their organization deploys it.
|
||||
/// The directory administrators use to try a deployment out before their organization rolls it out.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Everything stored here acts on behalf of the organization, so that a test behaves like the
|
||||
/// later rollout, including the approval of assistant plugins. In exchange, the directory is
|
||||
/// emptied on every start: a test configuration lives for one session only. It also never gets
|
||||
/// the protection of a deployed configuration, so users can remove or replace it through the user
|
||||
/// interface.
|
||||
/// later rollout, including the approval of assistant plugins and the protection against changes
|
||||
/// through the user interface. It takes every kind of plugin, exactly like a real deployment, so
|
||||
/// the directory structure of the later archive can be reproduced one to one. In exchange, the
|
||||
/// directory is emptied on every start: a test lives for one session only.<br/><br/>
|
||||
/// A test therefore ends by restarting AI Studio, or by removing the files again. Whoever builds
|
||||
/// enterprise plugins places them here by hand in the first place, so both ways are open to them
|
||||
/// anyway, and neither weakens what the directory grants a plugin.
|
||||
/// </remarks>
|
||||
private static string ENTERPRISE_TEST_CONFIGURATION_PLUGINS_ROOT = string.Empty;
|
||||
|
||||
@ -114,9 +123,10 @@ public static partial class PluginFactory
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Only the IT department of an organization deploys plugins there: the config server downloads
|
||||
/// them into a directory named after their configuration ID. We decide by path on purpose. The
|
||||
/// Lua field DEPLOYED_USING_CONFIG_SERVER is self-declared, so any plugin could claim to be
|
||||
/// deployed by an organization.
|
||||
/// each deployment into a directory named after its configuration ID, and a plugin of any type
|
||||
/// may sit in a subdirectory of it. We decide by path on purpose. The Lua field
|
||||
/// DEPLOYED_USING_CONFIG_SERVER is self-declared, so any plugin could claim to be deployed by an
|
||||
/// organization, and one an organization did deploy could deny it.
|
||||
/// </remarks>
|
||||
/// <param name="pluginPath">The directory of the plugin.</param>
|
||||
/// <returns>True when the directory is nested in the enterprise configuration directory.</returns>
|
||||
@ -130,19 +140,61 @@ public static partial class PluginFactory
|
||||
public static bool IsEnterpriseTestConfigurationPath(string? pluginPath) => IsPathInside(ENTERPRISE_TEST_CONFIGURATION_PLUGINS_ROOT, pluginPath);
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether a plugin acts on behalf of an organization, either deployed by a configuration
|
||||
/// server or staged for a test.
|
||||
/// Checks whether a plugin belongs to an organization, either deployed by a configuration server
|
||||
/// or staged for a test.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Use this wherever a configuration speaks for the organization, e.g. when it approves assistant
|
||||
/// plugins or claims a setting against a local configuration plugin. Do not use it where a
|
||||
/// deployed configuration is protected against the user, e.g. against deletion: an administrator
|
||||
/// must be able to get rid of their own test configuration.
|
||||
/// This is the criterion for everything an organization owns, and it holds for every plugin type:
|
||||
/// a configuration speaking for the organization when it approves assistant plugins or claims a
|
||||
/// setting, and the protection of a plugin against the user, e.g. against deletion or editing
|
||||
/// through the user interface.<br/><br/>
|
||||
/// A test deployment is protected just like a real one, so that a test shows what colleagues will
|
||||
/// see later. Administrators end a test by restarting AI Studio or by removing the files they
|
||||
/// placed, which is why they do not need the user interface to get rid of it.<br/><br/>
|
||||
/// Plugins an organization rolls out past these directories, e.g. through an MDM solution, carry
|
||||
/// no path to prove it. Those declare DEPLOYED_USING_CONFIG_SERVER instead, which is read into
|
||||
/// the IsManagedByConfigServer property of a plugin's metadata. Check that property in addition
|
||||
/// to this method wherever a plugin is protected against the user.
|
||||
/// </remarks>
|
||||
/// <param name="pluginPath">The directory of the plugin.</param>
|
||||
/// <returns>True when the directory belongs to the enterprise or the test configuration area.</returns>
|
||||
public static bool IsOrganizationConfigurationPath(string? pluginPath) => IsEnterpriseConfigurationPath(pluginPath) || IsEnterpriseTestConfigurationPath(pluginPath);
|
||||
|
||||
/// <summary>
|
||||
/// Determines which deployed configuration a plugin below the enterprise configuration directory
|
||||
/// belongs to.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A configuration server downloads each configuration into a directory named after its ID. That
|
||||
/// archive may carry more than the configuration itself: organizations deploy assistant plugins
|
||||
/// and other plugin types alongside it, each in its own subdirectory. We therefore look at the
|
||||
/// topmost directory below the enterprise configuration directory instead of the directory the
|
||||
/// plugin lives in, which for such a plugin is a nested one.
|
||||
/// </remarks>
|
||||
/// <param name="pluginPath">The directory of the plugin.</param>
|
||||
/// <param name="configurationId">The ID of the configuration the plugin was deployed with.</param>
|
||||
/// <returns>True when the plugin is nested in a directory named after a configuration ID.</returns>
|
||||
public static bool TryGetDeployedConfigurationId(string? pluginPath, out Guid configurationId)
|
||||
{
|
||||
configurationId = Guid.Empty;
|
||||
if (!IsEnterpriseConfigurationPath(pluginPath))
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
var root = Path.GetFullPath(ENTERPRISE_CONFIGURATION_PLUGINS_ROOT);
|
||||
var relativePath = Path.GetRelativePath(root, Path.GetFullPath(pluginPath!));
|
||||
var deploymentDirectory = relativePath.Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)[0];
|
||||
|
||||
return Guid.TryParse(deploymentDirectory, out configurationId) && configurationId != Guid.Empty;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
LOG.LogWarning(e, $"Was not able to determine the deployed configuration ID for the plugin directory '{pluginPath}'.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ranks how much say a configuration plugin has, based on where it is stored. The higher rank
|
||||
/// wins when two configuration plugins claim the same plugin ID.
|
||||
|
||||
@ -176,13 +176,22 @@ public sealed partial class PluginInstallService
|
||||
return TB("The plugin has no local directory.");
|
||||
|
||||
//
|
||||
// We decide by the plugin path, not by what a plugin declares about itself. Both
|
||||
// DEPLOYED_USING_CONFIG_SERVER and the Assistant Builder metadata are self-declared: a
|
||||
// locally placed plugin could claim to be deployed by an organization, or simply omit the
|
||||
// builder metadata, and would then be impossible to remove through the user interface, which
|
||||
// is exactly the situation this deletion is meant to resolve.
|
||||
// Nothing an organization rolled out belongs to the user, so none of it may be removed here.
|
||||
// The plugin path is the primary criterion and covers every plugin type: a deployed
|
||||
// configuration, a test configuration staged for it, and every plugin an organization ships
|
||||
// alongside them in a subdirectory.
|
||||
//
|
||||
if (PluginFactory.IsEnterpriseConfigurationPath(plugin.LocalPath))
|
||||
if (PluginFactory.IsOrganizationConfigurationPath(plugin.LocalPath))
|
||||
return TB("Plugins deployed by your organization cannot be deleted.");
|
||||
|
||||
//
|
||||
// Organizations also roll plugins out past these directories, e.g. through their MDM
|
||||
// solution. DEPLOYED_USING_CONFIG_SERVER is the only marker such a plugin has, so we honor
|
||||
// it here just like sharing, replacing, and revising already do. A plugin cannot acquire the
|
||||
// flag by accident: an archive declaring it is refused on import, which leaves deliberate
|
||||
// manual placement as the only way in, and the file system as the way back out.
|
||||
//
|
||||
if (plugin.IsManagedByConfigServer)
|
||||
return TB("Plugins deployed by your organization cannot be deleted.");
|
||||
|
||||
if (!PluginFactory.IsInsidePluginsRoot(plugin.LocalPath) || PluginFactory.IsPluginsRoot(plugin.LocalPath))
|
||||
|
||||
@ -21,6 +21,14 @@ public sealed partial class PluginInstallService
|
||||
if (plugin.IsInternal)
|
||||
return CheckError(TB("Internal assistant plugins cannot be edited."));
|
||||
|
||||
//
|
||||
// An assistant an organization rolled out is theirs to change, not the user's. Editing it
|
||||
// would also change its content hash, which is what an enterprise approval is based on: the
|
||||
// assistant would lose its approval and suddenly demand a security audit.
|
||||
//
|
||||
if (plugin.IsManagedByConfigServer)
|
||||
return CheckError(TB("Only locally managed assistant plugins can be edited."));
|
||||
|
||||
if (string.IsNullOrWhiteSpace(plugin.LocalPath))
|
||||
return CheckError(TB("The assistant plugin has no local directory."));
|
||||
|
||||
@ -76,6 +84,11 @@ public sealed partial class PluginInstallService
|
||||
if (plugin.IsInternal)
|
||||
return UpdateError(plugin, plugin.LocalPath, TB("Internal assistant plugins cannot be edited."));
|
||||
|
||||
// See CheckInstalledAssistantUpdateAsync: an organization's assistant must keep the content
|
||||
// its enterprise approval was granted for.
|
||||
if (plugin.IsManagedByConfigServer)
|
||||
return UpdateError(plugin, plugin.LocalPath, TB("Only locally managed assistant plugins can be edited."));
|
||||
|
||||
if (string.IsNullOrWhiteSpace(plugin.LocalPath))
|
||||
return UpdateError(plugin, string.Empty, TB("The assistant plugin has no local directory."));
|
||||
|
||||
|
||||
@ -6,9 +6,14 @@
|
||||
- Improved how much memory AI Studio needs. Working with large documents used to grow the app to several gigabytes, and on macOS that memory was never handed back. AI Studio now stays at a fraction of that and returns memory to your system. This matters most on devices with little memory, such as a Raspberry Pi.
|
||||
- Improved the preview for large documents. It now shows you the beginning of your document instead of loading all of it, so the dialog opens right away. Your complete document still goes to the AI.
|
||||
- Improved how AI Studio deals with rare internal hiccups. When the app window reloads, or when it briefly loses the connection to its own user interface, work which was still running in the background is now ended properly instead of leaving errors behind.
|
||||
- Improved how IT departments roll plugins out. A configuration server can deliver any kind of plugin, not only configurations: one archive may carry assistant plugins and further types alongside a configuration, each in its own folder. The folder for staging a test behaves the same way, so a test can mirror the later rollout exactly. The Enterprise IT documentation describes the whole procedure.
|
||||
- Changed how plugins your organization rolled out are protected. They can no longer be deleted or edited in AI Studio, which already applied to sharing and replacing them. This also covers plugins staged for a test: such a test now ends by restarting AI Studio or by removing the staged files, instead of through the plugin page.
|
||||
- Fixed assistants created by the Assistant Builder being named after an internal placeholder, such as "Model decides", when you left the display name empty. The model now picks a fitting name instead.
|
||||
- Fixed AI Studio reading a document to the end even after you closed its preview. Closing the dialog now stops that work immediately.
|
||||
- Fixed AI Studio holding on to finished chats, presentation images, and plugin data. It releases them now, so memory no longer grows the longer you keep the app running.
|
||||
- Fixed assistants handing an outdated result to the chat. When you sent several results without opening the chat in between, you now receive the one you sent last.
|
||||
- Fixed rare issues when multiple configurations provided introduction texts or mandatory information under the same ID.
|
||||
- Fixed the description of the global voice recording shortcut always appearing in English. On Linux, your desktop asks you to confirm such a shortcut and shows this description; it now appears in your language.
|
||||
- Fixed the description of the global voice recording shortcut always appearing in English. On Linux, your desktop asks you to confirm such a shortcut and shows this description; it now appears in your language.
|
||||
- Fixed the code editor being offered for assistant plugins your organization manages. Editing one would have withdrawn the approval of your IT department and demanded a fresh security audit.
|
||||
- Fixed assistant plugins your organization deployed alongside a configuration not being recognized as centrally managed unless they declared it themselves.
|
||||
- Fixed a misleading warning in the log when an organization deployed an archive that carries no configuration of its own.
|
||||
@ -306,15 +306,20 @@ ID = "9072b77d-ca81-40da-be6a-861da525ef7b"
|
||||
|
||||
## Important: Mark enterprise-managed plugins explicitly
|
||||
|
||||
Configuration plugins deployed by your configuration server should define:
|
||||
Plugins deployed by your configuration server should define:
|
||||
|
||||
```lua
|
||||
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.
|
||||
This holds for every plugin type, not just for configurations. Local, manually managed plugins should set this to `false`. If the field is missing on a plugin below `.config` or `.config-tests`, AI Studio falls back to the plugin path, treats the plugin as managed, and logs a warning.
|
||||
|
||||
The field describes a plugin, it does not grant it anything. Which configurations belong to your organization is always decided by the plugin path: which approvals for assistant plugins are honored, which configuration wins a conflict, and which configuration AI Studio withdraws once you stop referencing it. A configuration stored under `.config` is therefore removed when your organization no longer references its ID, whatever this field says.
|
||||
Inside those two directories the field is a courtesy, not a requirement: the path already proves who the plugin belongs to. You need it for plugins you roll out **past** those directories, for example when your MDM solution places an assistant plugin under `plugins/assistants/`. Such a plugin has no path to prove its origin, and this field is the only marker it has.
|
||||
|
||||
What the field decides, and what it does not:
|
||||
|
||||
- **The plugin path alone** decides which configurations speak for your organization: which approvals for assistant plugins are honored, which configuration wins a conflict, and which configuration AI Studio withdraws once you stop referencing it. A configuration stored under `.config` is therefore removed when your organization no longer references its ID, whatever this field says. A plugin that declares `false` while sitting below `.config` does not escape any of this — AI Studio logs the contradiction and treats it as managed.
|
||||
- **The path and the field together** decide whether a plugin is protected against the user. Whoever carries either marker cannot be deleted, edited, shared, or replaced through the user interface. Your IT department stays the only party that changes it.
|
||||
|
||||
## Priority of configuration plugins
|
||||
|
||||
@ -372,6 +377,28 @@ In both cases each configuration keeps its own contribution, so removing one of
|
||||
|
||||
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.
|
||||
|
||||
## Deploying other plugin types
|
||||
|
||||
A deployment is not limited to a configuration, even though the directory it lands in is called `.config`. Your configuration server serves one archive per configuration ID, and you may use it for every kind of plugin: assistant plugins today, further types such as tool plugins as they arrive. Read the directory name as "centrally configured and rolled out", not as "configurations only".
|
||||
|
||||
Put each plugin into its own subdirectory of the archive:
|
||||
|
||||
```
|
||||
9072b77d-ca81-40da-be6a-861da525ef7b.zip
|
||||
├── plugin.lua ← your configuration plugin, ID = 9072b77d-…
|
||||
└── translation-assistant/
|
||||
└── plugin.lua ← an assistant plugin with an ID of its own
|
||||
```
|
||||
|
||||
AI Studio extracts the whole tree and picks up every `plugin.lua` in it. A few rules apply:
|
||||
|
||||
- **Only the configuration plugin carries the configuration ID.** Every other plugin has its own `ID`, as any plugin does. The archive does not have to contain a configuration plugin at all: an archive that only ships an assistant plugin is fine.
|
||||
- **Everything in the archive belongs to your organization.** Users cannot delete, edit, share, or replace any of it, whatever the individual plugins declare about themselves.
|
||||
- **The withdrawal takes the whole deployment.** Once you stop referencing the configuration ID, AI Studio removes that directory including every plugin you shipped in it. See [Withdrawing a configuration](#withdrawing-a-configuration).
|
||||
- **Assistant plugins still need an approval or an audit.** Deploying an assistant does not approve it. List its hash in `CONFIG["SETTINGS"]["DataAssistantPluginAudit.EnterpriseApprovedPlugins"]` of a configuration you deploy, otherwise users have to run a local security audit before they can activate it. See [Enterprise approval for assistant plugins](#enterprise-approval-for-assistant-plugins).
|
||||
|
||||
If you would rather not use the configuration server for this, roll the plugin out with your MDM solution into the ordinary plugin directory and mark it with `DEPLOYED_USING_CONFIG_SERVER = true`. It is then protected against changes just the same, but it is not tied to a configuration ID, so you have to remove it the same way you placed it.
|
||||
|
||||
## Withdrawing a configuration
|
||||
|
||||
A configuration does not have to stay forever: you stop deploying it, a user deletes a configuration they installed themselves, or a test configuration ends with the next restart. AI Studio then removes what that configuration brought along, such as its providers, data sources, profiles, chat templates, and its approvals for assistant plugins.
|
||||
@ -424,6 +451,8 @@ The reason is what an approval does: it marks an assistant plugin as safe withou
|
||||
|
||||
This is decided by where the plugin is stored, not by its `DEPLOYED_USING_CONFIG_SERVER` field. That field is part of the plugin itself, so any plugin could claim it.
|
||||
|
||||
The field does count elsewhere, namely for protecting a plugin against the user, and that is not a contradiction: there the field only ever takes a possibility away from whoever declared it. Granting an approval works the other way round, so it needs a source no plugin can write.
|
||||
|
||||
If you want to test approvals before rolling a configuration out, see [Local staging and testing](#local-staging-and-testing).
|
||||
|
||||
### Configuration example
|
||||
@ -458,7 +487,7 @@ This prints the canonical hash and, with `--lua-snippet`, also prints a ready-to
|
||||
|
||||
Before you roll a configuration out through a configuration web server, you can stage it on a device and test it end to end, including the enterprise approvals for assistant plugins described above. This needs no configuration web server, no registry, policy, or environment entry, and no encryption secret.
|
||||
|
||||
AI Studio has a dedicated directory for this: `.config-tests`. A configuration stored there speaks for your organization exactly like a deployed one. In exchange, AI Studio empties the directory on every start, so a test configuration is valid for one session.
|
||||
AI Studio has a dedicated directory for this: `.config-tests`. Anything stored there speaks for your organization exactly like a deployed plugin, and it takes every plugin type just like a real deployment does, so you can reproduce the directory structure of your later archive one to one. In exchange, AI Studio empties the directory on every start, so a test is valid for one session.
|
||||
|
||||
Do not use the `.config` directory for this. It belongs to your configuration web server, and AI Studio removes everything there that your organization does not reference anymore.
|
||||
|
||||
@ -473,14 +502,16 @@ Plugins live in the data directory of AI Studio:
|
||||
| Linux | `$XDG_DATA_HOME/com.github.mindwork-ai.ai-studio/data`, usually `~/.local/share/com.github.mindwork-ai.ai-studio/data` |
|
||||
| Linux (Flatpak) | `~/.var/app/org.mindworkai.AIStudio/data/com.github.mindwork-ai.ai-studio/data` |
|
||||
|
||||
### Staging a configuration
|
||||
### Staging a deployment
|
||||
|
||||
Place the files **while AI Studio is running**: the test directory is emptied whenever the app starts.
|
||||
|
||||
1. Start AI Studio. It creates `<data directory>/plugins/.config-tests/` if it does not exist yet.
|
||||
2. Create a directory below it and place your `plugin.lua` there, e.g. `.config-tests/my-department-draft/`. The directory name is up to you here: a test configuration is identified by the `ID` field inside the plugin, not by the directory it lives in.
|
||||
3. Place the assistant plugin you want to test in `<data directory>/plugins/assistants/<any name>/`.
|
||||
4. AI Studio watches the plugin directory and picks both up without a restart. The security card of the assistant then states that your organization approved it, exactly as it will after the rollout.
|
||||
2. Create a directory below it and place your `plugin.lua` there, e.g. `.config-tests/my-department-draft/`. The directory name is up to you here: a plugin is identified by the `ID` field inside it, not by the directory it lives in.
|
||||
3. Place every other plugin of the deployment in a subdirectory of it, e.g. `.config-tests/my-department-draft/translation-assistant/`. This mirrors the archive you will serve later, as described in [Deploying other plugin types](#deploying-other-plugin-types).
|
||||
4. AI Studio watches the plugin directory and picks everything up without a restart. The security card of the assistant then states that your organization approved it, exactly as it will after the rollout.
|
||||
|
||||
You can also keep an assistant plugin you are only iterating on in `<data directory>/plugins/assistants/<any name>/`. Your test configuration approves it by hash either way. The difference is that a plugin outside `.config-tests` is not protected against the user, so this variant no longer mirrors the later rollout.
|
||||
|
||||
While a test configuration is loaded, the Information page reports it, including the directory it was staged in. After a restart, that same page tells you that a test configuration was removed, so nobody has to wonder where the directory went.
|
||||
|
||||
@ -488,22 +519,25 @@ What behaves like the later rollout:
|
||||
|
||||
- The approvals for assistant plugins are honored.
|
||||
- Settings and configuration objects the test configuration manages are protected against local configuration plugins.
|
||||
- Everything staged there is protected against the user: no plugin of the test deployment can be deleted, edited, shared, or replaced through the user interface.
|
||||
- When the test configuration declares the same plugin `ID` as one your organization deployed, the test configuration wins. This is how you try out the next version of an existing configuration under its final ID.
|
||||
|
||||
What deliberately does not:
|
||||
|
||||
- A test configuration has no protection against the user. You can remove it on the plugin page and replace it by importing a new version.
|
||||
- It does not survive a restart.
|
||||
- It is not tied to a configuration ID, so nothing is withdrawn by removing an ID from your devices. A test ends as described in [Cleaning up](#cleaning-up).
|
||||
|
||||
### Testing with a small group
|
||||
|
||||
To let colleagues take part in the test, place the same two directories on each of their devices while AI Studio runs, for example through a script, your MDM solution, or a login script. A configuration web server is not involved, and nothing has to be enabled inside AI Studio. Ordinary user accounts can take part: the data directory belongs to the user, so no administrator rights are needed to place the files.
|
||||
To let colleagues take part in the test, place the same directories on each of their devices while AI Studio runs, for example through a script, your MDM solution, or a login script. A configuration web server is not involved, and nothing has to be enabled inside AI Studio. Ordinary user accounts can take part: the data directory belongs to the user, so no administrator rights are needed to place the files.
|
||||
|
||||
Keep in mind that everybody in the group loses the test configuration the next time they start AI Studio. Either repeat the step, or let your script place the files at every login.
|
||||
|
||||
### Cleaning up
|
||||
|
||||
Restart AI Studio: the test directory is emptied, the approvals are gone, and the assistant requires a security audit again. Every setting your test configuration had taken over returns to the value it had before the test, as described in [Withdrawing a configuration](#withdrawing-a-configuration). To end a test without restarting, delete the configuration on the plugin page.
|
||||
Restart AI Studio: the test directory is emptied, the approvals are gone, and the assistant requires a security audit again. Every setting your test configuration had taken over returns to the value it had before the test, as described in [Withdrawing a configuration](#withdrawing-a-configuration).
|
||||
|
||||
To end a test without restarting, delete the directory you created under `.config-tests` yourself. AI Studio watches the plugin directory and reacts right away, with the same result as a restart. There is no button for this on the plugin page: a test deployment carries the protection of a real one, so the user interface does not remove it. Whoever stages a test writes into the data directory anyway, so both ways are open to them.
|
||||
|
||||
### Security note
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user