mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-10-04 14:52:19 +00:00
Improved local plugin handling (share, import, delete) (#900)
Build and Release / Read metadata (push) Blocked by required conditions
Build and Release / Sync Flatpak repo (push) Blocked by required conditions
Build and Release / Collect Flatpak artifacts (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Read metadata (push) Blocked by required conditions
Build and Release / Sync Flatpak repo (push) Blocked by required conditions
Build and Release / Collect Flatpak artifacts (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
This commit is contained in:
1 parent
0eb747b386
commit
6e143aafaa
47 files changed
+3108
-1480
No files matched your search
@@ -21,9 +21,31 @@ public static partial class PluginFactory
|
||||
/// deploys plugins here, each in a directory named after its configuration ID.
|
||||
/// </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.
|
||||
/// </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.
|
||||
/// </remarks>
|
||||
private static string ENTERPRISE_TEST_CONFIGURATION_PLUGINS_ROOT = string.Empty;
|
||||
|
||||
private static string HOT_RELOAD_LOCK_FILE = string.Empty;
|
||||
private static FileSystemWatcher HOT_RELOAD_WATCHER = null!;
|
||||
|
||||
/// <summary>
|
||||
/// How many test configurations were removed while AI Studio was starting.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The user interface reports this: an administrator who placed a test configuration and restarted
|
||||
/// AI Studio would otherwise face an empty directory without any explanation.
|
||||
/// </remarks>
|
||||
public static int RemovedTestConfigurationsAtStartup { get; private set; }
|
||||
|
||||
public static ILanguagePlugin BaseLanguage { get; private set; } = NoPluginLanguage.INSTANCE;
|
||||
|
||||
public static bool IsInitialized { get; private set; }
|
||||
@@ -75,10 +97,12 @@ public static partial class PluginFactory
|
||||
HOT_RELOAD_LOCK_FILE = Path.Join(PLUGINS_ROOT, ".lock");
|
||||
INTERNAL_PLUGINS_ROOT = Path.Join(PLUGINS_ROOT, ".internal");
|
||||
ENTERPRISE_CONFIGURATION_PLUGINS_ROOT = Path.Join(PLUGINS_ROOT, ".config");
|
||||
|
||||
ENTERPRISE_TEST_CONFIGURATION_PLUGINS_ROOT = Path.Join(PLUGINS_ROOT, ".config-tests");
|
||||
|
||||
if (!Directory.Exists(PLUGINS_ROOT))
|
||||
Directory.CreateDirectory(PLUGINS_ROOT);
|
||||
|
||||
|
||||
ClearTestConfigurationPlugins();
|
||||
HOT_RELOAD_WATCHER = new(PLUGINS_ROOT);
|
||||
IsInitialized = true;
|
||||
LOG.LogInformation("Plugin factory initialized successfully.");
|
||||
@@ -96,20 +120,130 @@ public static partial class PluginFactory
|
||||
/// </remarks>
|
||||
/// <param name="pluginPath">The directory of the plugin.</param>
|
||||
/// <returns>True when the directory is nested in the enterprise configuration directory.</returns>
|
||||
private static bool IsEnterpriseConfigurationPath(string? pluginPath)
|
||||
public static bool IsEnterpriseConfigurationPath(string? pluginPath) => IsPathInside(ENTERPRISE_CONFIGURATION_PLUGINS_ROOT, pluginPath);
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether a plugin directory belongs to the test configuration area.
|
||||
/// </summary>
|
||||
/// <param name="pluginPath">The directory of the plugin.</param>
|
||||
/// <returns>True when the directory is nested in the test configuration directory.</returns>
|
||||
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.
|
||||
/// </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.
|
||||
/// </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>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A test configuration outranks a deployed one on purpose: an administrator tries out the next
|
||||
/// version of a configuration under the ID it will have later. Local configuration plugins rank
|
||||
/// lowest, so nobody can push aside what an organization deployed.
|
||||
/// </remarks>
|
||||
private static int GetConfigurationAuthority(string? pluginPath)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(pluginPath) || string.IsNullOrWhiteSpace(ENTERPRISE_CONFIGURATION_PLUGINS_ROOT))
|
||||
if (IsEnterpriseTestConfigurationPath(pluginPath))
|
||||
return 2;
|
||||
|
||||
return IsEnterpriseConfigurationPath(pluginPath) ? 1 : 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Empties the test configuration directory.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A test configuration carries the rights of an organization configuration without anybody having
|
||||
/// deployed it. It must therefore never outlive the session it was placed in, and administrators
|
||||
/// get a predictable lifetime instead of a configuration which is swept away at some point.
|
||||
/// </remarks>
|
||||
private static void ClearTestConfigurationPlugins()
|
||||
{
|
||||
RemovedTestConfigurationsAtStartup = 0;
|
||||
try
|
||||
{
|
||||
if (Directory.Exists(ENTERPRISE_TEST_CONFIGURATION_PLUGINS_ROOT))
|
||||
{
|
||||
var removedTestConfigurations = Directory.EnumerateDirectories(ENTERPRISE_TEST_CONFIGURATION_PLUGINS_ROOT).Count();
|
||||
Directory.Delete(ENTERPRISE_TEST_CONFIGURATION_PLUGINS_ROOT, true);
|
||||
RemovedTestConfigurationsAtStartup = removedTestConfigurations;
|
||||
|
||||
if (removedTestConfigurations > 0)
|
||||
LOG.LogWarning($"Removed {removedTestConfigurations} test configuration(s) from '{ENTERPRISE_TEST_CONFIGURATION_PLUGINS_ROOT}'. Test configurations are valid for one session only.");
|
||||
}
|
||||
|
||||
Directory.CreateDirectory(ENTERPRISE_TEST_CONFIGURATION_PLUGINS_ROOT);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
LOG.LogError(e, $"Failed to empty the test configuration directory '{ENTERPRISE_TEST_CONFIGURATION_PLUGINS_ROOT}'.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether a plugin directory is stored below the plugins directory of AI Studio.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Everything that removes or replaces plugin files checks this first, so a plugin directory
|
||||
/// which points somewhere else can never be touched.
|
||||
/// </remarks>
|
||||
/// <param name="pluginPath">The directory of the plugin.</param>
|
||||
/// <returns>True when the directory is nested in the plugins directory.</returns>
|
||||
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 configurationRoot = Path.GetFullPath(ENTERPRISE_CONFIGURATION_PLUGINS_ROOT).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) + Path.DirectorySeparatorChar;
|
||||
var pluginDirectory = Path.GetFullPath(pluginPath).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) + Path.DirectorySeparatorChar;
|
||||
return pluginDirectory.StartsWith(configurationRoot, StringComparison.OrdinalIgnoreCase);
|
||||
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}' belongs to the enterprise configuration directory. Treating it as a local plugin.");
|
||||
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))
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
var root = Path.GetFullPath(rootDirectory).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) + Path.DirectorySeparatorChar;
|
||||
var pluginDirectory = Path.GetFullPath(pluginPath).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) + Path.DirectorySeparatorChar;
|
||||
return pluginDirectory.StartsWith(root, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
LOG.LogWarning(e, $"Was not able to check whether the plugin directory '{pluginPath}' is nested in '{rootDirectory}'. Treating it as unrelated.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -135,6 +269,27 @@ public static partial class PluginFactory
|
||||
return Directory.Exists(Path.Join(ENTERPRISE_CONFIGURATION_PLUGINS_ROOT, configPluginId.ToString()));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether a configuration plugin speaks for an organization: either deployed by its IT
|
||||
/// department, or staged as a test configuration.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A test configuration is only ever loaded, never merely present: it is emptied on every start,
|
||||
/// so there is no unloadable leftover to account for.
|
||||
/// </remarks>
|
||||
/// <param name="configPluginId">The ID of the configuration plugin.</param>
|
||||
/// <returns>True when the plugin speaks for an organization, false when it is local or unknown.</returns>
|
||||
public static bool IsOrganizationConfigurationPlugin(Guid configPluginId)
|
||||
{
|
||||
if (configPluginId == Guid.Empty || !IsInitialized)
|
||||
return false;
|
||||
|
||||
if (IsEnterpriseConfigurationPlugin(configPluginId))
|
||||
return true;
|
||||
|
||||
return AVAILABLE_PLUGINS.Any(plugin => plugin.Id == configPluginId && plugin.Type is PluginType.CONFIGURATION && IsEnterpriseTestConfigurationPath(plugin.LocalPath));
|
||||
}
|
||||
|
||||
private static async Task LockHotReloadAsync()
|
||||
{
|
||||
if (!IsInitialized)
|
||||
|
||||
Reference in new issue
Block a user