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)