From b45f55cfe10dcb8f0d068d182309f157f09b8632 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sun, 9 Aug 2026 16:21:52 +0200 Subject: [PATCH] Fixed deleting assistant plugins which the Assistant Builder did not create --- .../Tools/PluginSystem/PluginFactory.cs | 28 ++++++++++++++ .../Services/PluginInstallService.Delete.cs | 37 ++++++++----------- 2 files changed, 44 insertions(+), 21 deletions(-) diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.cs index 93d7142a..5143c2ee 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.cs @@ -109,6 +109,34 @@ public static partial class PluginFactory /// True when the directory is nested in the plugins directory. public static bool IsInsidePluginsRoot(string? pluginPath) => IsPathInside(PLUGINS_ROOT, pluginPath); + /// + /// Checks whether a plugin directory is the plugins directory itself. + /// + /// + /// 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. + /// + /// The directory of the plugin. + /// True when the directory is the plugins directory. + 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) { if (string.IsNullOrWhiteSpace(pluginPath) || string.IsNullOrWhiteSpace(rootDirectory)) diff --git a/app/MindWork AI Studio/Tools/Services/PluginInstallService.Delete.cs b/app/MindWork AI Studio/Tools/Services/PluginInstallService.Delete.cs index 42de9274..e2227a83 100644 --- a/app/MindWork AI Studio/Tools/Services/PluginInstallService.Delete.cs +++ b/app/MindWork AI Studio/Tools/Services/PluginInstallService.Delete.cs @@ -9,16 +9,16 @@ namespace AIStudio.Tools.Services; public sealed partial class PluginInstallService { /// - /// 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. /// public static bool CanDeleteInstalledAssistant(IAvailablePlugin plugin) => string.IsNullOrWhiteSpace(GetAssistantDeletionEligibilityIssue(plugin)); /// - /// 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. /// /// - /// Assistants have their own path, because only those generated by the Assistant Builder may be - /// deleted and because a running assistant must not be pulled away from under a user. + /// Assistants have their own path, because a running assistant must not be pulled away from + /// under a user and because removing one also removes its security audit. /// private static readonly PluginType[] DELETABLE_LOCAL_PLUGIN_TYPES = [PluginType.CONFIGURATION, PluginType.LANGUAGE]; @@ -229,27 +229,22 @@ public sealed partial class PluginInstallService if (plugin.IsInternal) 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)) return TB("The assistant plugin has no local directory."); - var assistantPlugin = PluginFactory.RunningPlugins - .OfType() - .FirstOrDefault(candidate => candidate.Id == plugin.Id && IsSameDirectory(candidate.PluginPath, plugin.LocalPath)); - - if (assistantPlugin is null || assistantPlugin.IsInternal || !assistantPlugin.IsAssistantBuilderGenerated) - return TB("Only assistants generated by the Assistant Builder can be deleted."); - - if (assistantPlugin.IsManagedByConfigServer) + // + // Like for every other plugin type, the plugin path decides, not what the plugin declares + // 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 + // organization, or simply omit the builder metadata, and would then be impossible to remove + // 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 (PluginFactory.IsEnterpriseConfigurationPath(plugin.LocalPath)) return TB("Config Server managed assistant plugins cannot be deleted."); - if (!TryGetPluginRoot(PluginType.ASSISTANT, out var assistantPluginsRoot, out var rootIssue)) - return rootIssue; - - if (!IsPathInsideDirectory(assistantPluginsRoot, plugin.LocalPath) || IsSameDirectory(assistantPluginsRoot, plugin.LocalPath)) - return TB("The assistant plugin directory is outside the local assistant plugin directory."); + if (!PluginFactory.IsInsidePluginsRoot(plugin.LocalPath) || PluginFactory.IsPluginsRoot(plugin.LocalPath)) + return TB("The assistant plugin directory is outside the plugins directory."); return Directory.Exists(plugin.LocalPath) ? string.Empty @@ -276,7 +271,7 @@ public sealed partial class PluginInstallService if (PluginFactory.IsEnterpriseConfigurationPath(plugin.LocalPath)) 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 Directory.Exists(plugin.LocalPath)