Fixed deleting assistant plugins which the Assistant Builder did not create

This commit is contained in:
Thorsten Sommer 2026-08-09 16:21:52 +02:00
parent eeb890f8f1
commit b45f55cfe1
Signed by untrusted user who does not match committer: tsommer
GPG Key ID: 371BBA77A02C0108
2 changed files with 44 additions and 21 deletions

View File

@ -109,6 +109,34 @@ public static partial class PluginFactory
/// <returns>True when the directory is nested in the plugins directory.</returns> /// <returns>True when the directory is nested in the plugins directory.</returns>
public static bool IsInsidePluginsRoot(string? pluginPath) => IsPathInside(PLUGINS_ROOT, pluginPath); public static bool IsInsidePluginsRoot(string? pluginPath) => IsPathInside(PLUGINS_ROOT, pluginPath);
/// <summary>
/// Checks whether a plugin directory is the plugins directory itself.
/// </summary>
/// <remarks>
/// A `plugin.lua` placed directly in the plugins directory makes that directory the plugin
/// directory. Removing or replacing such a plugin means touching its directory, which would take
/// every other plugin with it.
/// </remarks>
/// <param name="pluginPath">The directory of the plugin.</param>
/// <returns>True when the directory is the plugins directory.</returns>
public static bool IsPluginsRoot(string? pluginPath)
{
if (string.IsNullOrWhiteSpace(pluginPath) || string.IsNullOrWhiteSpace(PLUGINS_ROOT))
return false;
try
{
var root = Path.GetFullPath(PLUGINS_ROOT).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
var pluginDirectory = Path.GetFullPath(pluginPath).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
return string.Equals(root, pluginDirectory, StringComparison.OrdinalIgnoreCase);
}
catch (Exception e)
{
LOG.LogWarning(e, $"Was not able to check whether the plugin directory '{pluginPath}' is the plugins directory. Treating it as the plugins directory.");
return true;
}
}
private static bool IsPathInside(string rootDirectory, string? pluginPath) private static bool IsPathInside(string rootDirectory, string? pluginPath)
{ {
if (string.IsNullOrWhiteSpace(pluginPath) || string.IsNullOrWhiteSpace(rootDirectory)) if (string.IsNullOrWhiteSpace(pluginPath) || string.IsNullOrWhiteSpace(rootDirectory))

View File

@ -9,16 +9,16 @@ namespace AIStudio.Tools.Services;
public sealed partial class PluginInstallService public sealed partial class PluginInstallService
{ {
/// <summary> /// <summary>
/// Checks whether a local plugin is an Assistant Builder generated assistant that users may delete. /// Checks whether an assistant plugin is one that users may delete.
/// </summary> /// </summary>
public static bool CanDeleteInstalledAssistant(IAvailablePlugin plugin) => string.IsNullOrWhiteSpace(GetAssistantDeletionEligibilityIssue(plugin)); public static bool CanDeleteInstalledAssistant(IAvailablePlugin plugin) => string.IsNullOrWhiteSpace(GetAssistantDeletionEligibilityIssue(plugin));
/// <summary> /// <summary>
/// The plugin types users may remove through the user interface, besides their own assistants. /// The plugin types users may remove through the user interface, besides assistants.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// Assistants have their own path, because only those generated by the Assistant Builder may be /// Assistants have their own path, because a running assistant must not be pulled away from
/// deleted and because a running assistant must not be pulled away from under a user. /// under a user and because removing one also removes its security audit.
/// </remarks> /// </remarks>
private static readonly PluginType[] DELETABLE_LOCAL_PLUGIN_TYPES = [PluginType.CONFIGURATION, PluginType.LANGUAGE]; private static readonly PluginType[] DELETABLE_LOCAL_PLUGIN_TYPES = [PluginType.CONFIGURATION, PluginType.LANGUAGE];
@ -229,27 +229,22 @@ public sealed partial class PluginInstallService
if (plugin.IsInternal) if (plugin.IsInternal)
return TB("Internal assistant plugins cannot be deleted."); return TB("Internal assistant plugins cannot be deleted.");
if (plugin.IsManagedByConfigServer)
return TB("Config Server managed assistant plugins cannot be deleted.");
if (string.IsNullOrWhiteSpace(plugin.LocalPath)) if (string.IsNullOrWhiteSpace(plugin.LocalPath))
return TB("The assistant plugin has no local directory."); return TB("The assistant plugin has no local directory.");
var assistantPlugin = PluginFactory.RunningPlugins //
.OfType<PluginAssistants>() // Like for every other plugin type, the plugin path decides, not what the plugin declares
.FirstOrDefault(candidate => candidate.Id == plugin.Id && IsSameDirectory(candidate.PluginPath, plugin.LocalPath)); // about itself. Neither DEPLOYED_USING_CONFIG_SERVER nor the Assistant Builder metadata is
// suitable here: a locally placed assistant could declare itself as deployed by an
if (assistantPlugin is null || assistantPlugin.IsInternal || !assistantPlugin.IsAssistantBuilderGenerated) // organization, or simply omit the builder metadata, and would then be impossible to remove
return TB("Only assistants generated by the Assistant Builder can be deleted."); // through the user interface. Editing, revising, and sharing an assistant already follow the
// path, so deleting one must not be the single action that trusts the plugin's own claims.
if (assistantPlugin.IsManagedByConfigServer) //
if (PluginFactory.IsEnterpriseConfigurationPath(plugin.LocalPath))
return TB("Config Server managed assistant plugins cannot be deleted."); return TB("Config Server managed assistant plugins cannot be deleted.");
if (!TryGetPluginRoot(PluginType.ASSISTANT, out var assistantPluginsRoot, out var rootIssue)) if (!PluginFactory.IsInsidePluginsRoot(plugin.LocalPath) || PluginFactory.IsPluginsRoot(plugin.LocalPath))
return rootIssue; return TB("The assistant plugin directory is outside the plugins directory.");
if (!IsPathInsideDirectory(assistantPluginsRoot, plugin.LocalPath) || IsSameDirectory(assistantPluginsRoot, plugin.LocalPath))
return TB("The assistant plugin directory is outside the local assistant plugin directory.");
return Directory.Exists(plugin.LocalPath) return Directory.Exists(plugin.LocalPath)
? string.Empty ? string.Empty
@ -276,7 +271,7 @@ public sealed partial class PluginInstallService
if (PluginFactory.IsEnterpriseConfigurationPath(plugin.LocalPath)) if (PluginFactory.IsEnterpriseConfigurationPath(plugin.LocalPath))
return TB("Plugins deployed by your organization cannot be deleted."); return TB("Plugins deployed by your organization cannot be deleted.");
if (!PluginFactory.IsInsidePluginsRoot(plugin.LocalPath)) if (!PluginFactory.IsInsidePluginsRoot(plugin.LocalPath) || PluginFactory.IsPluginsRoot(plugin.LocalPath))
return TB("The plugin directory is outside the plugins directory."); return TB("The plugin directory is outside the plugins directory.");
return Directory.Exists(plugin.LocalPath) return Directory.Exists(plugin.LocalPath)