2025-04-12 21:13:33 +02:00
using AIStudio.Components ;
2026-04-09 10:01:24 +02:00
using AIStudio.Agents.AssistantAudit ;
using AIStudio.Dialogs ;
using AIStudio.Settings.DataModel ;
using AIStudio.Tools.PluginSystem.Assistants ;
2025-03-29 18:40:17 +01:00
using AIStudio.Tools.PluginSystem ;
2026-08-06 10:42:20 +02:00
using AIStudio.Tools.Rust ;
using AIStudio.Tools.Services ;
2025-04-24 13:50:14 +02:00
using Microsoft.AspNetCore.Components ;
2026-04-09 10:01:24 +02:00
using DialogOptions = AIStudio . Dialogs . DialogOptions ;
2025-04-24 13:50:14 +02:00
2025-03-29 18:40:17 +01:00
namespace AIStudio.Pages ;
2025-04-12 21:13:33 +02:00
public partial class Plugins : MSGComponentBase
2025-03-29 18:40:17 +01:00
{
private const string GROUP_ENABLED = "Enabled" ;
private const string GROUP_DISABLED = "Disabled" ;
private const string GROUP_INTERNAL = "Internal" ;
2026-04-09 10:01:24 +02:00
private bool isAutoAuditing ;
2026-08-06 10:42:20 +02:00
private bool isImportingAssistantPlugin ;
2026-04-09 10:01:24 +02:00
private DataAssistantPluginAudit AssistantPluginAuditSettings => this . SettingsManager . ConfigurationData . AssistantPluginAudit ;
2025-03-29 18:40:17 +01:00
private TableGroupDefinition < IPluginMetadata > groupConfig = null !;
2026-04-09 10:01:24 +02:00
[Inject]
private IDialogService DialogService { get ; init ; } = null !;
[Inject]
private AssistantPluginAuditService AssistantPluginAuditService { get ; init ; } = null !;
2026-08-06 10:42:20 +02:00
[Inject]
private PluginShareService PluginShareService { get ; init ; } = null !;
[Inject]
private RustService RustService { get ; init ; } = null !;
[Inject]
2026-08-09 19:01:58 +02:00
private PluginInstallService PluginInstallService { get ; init ; } = null !;
2026-08-06 10:42:20 +02:00
2026-07-15 21:00:25 +02:00
private static readonly ILogger LOG = Program . LOGGER_FACTORY . CreateLogger ( nameof ( Plugins ));
2026-08-06 10:42:20 +02:00
private bool isSharingPlugin ;
private const string IMPORT_ICON =
@"<svg class=""mud-icon-root mud-svg-icon mud-dark-text mud-icon-size-medium"" focusable=""false"" viewBox=""0 0 24 24"" aria-hidden=""true"" role=""img"">
<path d=""M0 0h24v24H0V0z"" fill=""none""></path>
<path d=""M16 5l-1.42 1.42-1.59-1.59V16h-1.98V4.83L9.42 6.42 8 5l4-4 4 4z"" transform=""rotate(180 12 10)""></path>
<path d=""M20 10v11c0 1.1-.9 2-2 2H6c-1.11 0-2-.9-2-2V10c0-1.11.89-2 2-2h3v2H6v11h12V10h-3V8h3c1.1 0 2 .89 2 2z""></path>
</svg>" ;
2026-07-15 21:00:25 +02:00
2025-03-29 18:40:17 +01:00
#region Overrides of ComponentBase
protected override async Task OnInitializedAsync ()
{
2026-09-10 15:28:08 +02:00
this . ApplyFilters ([], [ Event . PLUGINS_RELOADED , Event . CONFIGURATION_CHANGED ]);
2026-08-06 10:42:20 +02:00
2025-03-29 18:40:17 +01:00
this . groupConfig = new TableGroupDefinition < IPluginMetadata >
{
Expandable = true ,
IsInitiallyExpanded = true ,
Selector = pluginMeta =>
{
if ( pluginMeta . IsInternal )
return GROUP_INTERNAL ;
return this . SettingsManager . IsPluginEnabled ( pluginMeta )
? GROUP_ENABLED
: GROUP_DISABLED ;
}
};
await base . OnInitializedAsync ();
}
2026-04-09 10:01:24 +02:00
protected override async Task OnAfterRenderAsync ( bool firstRender )
{
if ( firstRender )
await this . TryAutoAuditAssistantsAsync ();
}
2025-03-29 18:40:17 +01:00
#endregion
2025-04-12 21:13:33 +02:00
2025-03-29 18:40:17 +01:00
private async Task PluginActivationStateChanged ( IPluginMetadata pluginMeta )
{
2026-08-29 14:12:14 +02:00
//
// The switch is disabled for these, so this cannot be reached through the user interface. We
// check anyway: removing the plugin from the enabled list would achieve nothing, because the
// activation is decided live, but it would leave the settings in a state which claims the
// opposite of what the user sees:
//
if ( PluginFactory . IsAssistantActivationEnforced ( pluginMeta . Id ))
return ;
2025-03-29 18:40:17 +01:00
if ( this . SettingsManager . IsPluginEnabled ( pluginMeta ))
2026-04-09 10:01:24 +02:00
{
2025-03-29 18:40:17 +01:00
this . SettingsManager . ConfigurationData . EnabledPlugins . Remove ( pluginMeta . Id );
2026-04-09 10:01:24 +02:00
await this . SettingsManager . StoreSettings ();
await this . MessageBus . SendMessage < bool >( this , Event . CONFIGURATION_CHANGED );
return ;
}
if ( pluginMeta . Type is not PluginType . ASSISTANT )
{
2025-03-29 18:40:17 +01:00
this . SettingsManager . ConfigurationData . EnabledPlugins . Add ( pluginMeta . Id );
2026-04-09 10:01:24 +02:00
await this . SettingsManager . StoreSettings ();
await this . MessageBus . SendMessage < bool >( this , Event . CONFIGURATION_CHANGED );
return ;
}
var assistantPlugin = PluginFactory . RunningPlugins . OfType < PluginAssistants >(). FirstOrDefault ( x => x . Id == pluginMeta . Id );
if ( assistantPlugin is null )
return ;
var securityState = PluginAssistantSecurityResolver . Resolve ( this . SettingsManager , assistantPlugin );
if ( securityState . RequiresAudit )
{
await this . OpenAssistantAuditDialogAsync ( pluginMeta . Id );
return ;
}
2026-07-15 21:00:25 +02:00
if ( securityState is { IsBelowMinimum : true , IsBlocked : true })
2026-04-09 10:01:24 +02:00
{
var blockedAudit = securityState . Audit ;
if ( blockedAudit is not null )
await this . DialogService . ShowMessageBox ( this . T ( "Assistant Audit" ), $"{blockedAudit.Level.GetName()}: {blockedAudit.Summary}" , this . T ( "Close" ));
return ;
}
2026-07-15 21:00:25 +02:00
if ( securityState is { IsBelowMinimum : true , CanOverride : true } &&
2026-04-09 10:01:24 +02:00
! await this . ConfirmActivationBelowMinimumAsync ( pluginMeta . Name , securityState . Audit !. Level ))
{
return ;
}
this . SettingsManager . ConfigurationData . EnabledPlugins . Add ( pluginMeta . Id );
2025-03-29 18:40:17 +01:00
await this . SettingsManager . StoreSettings ();
await this . MessageBus . SendMessage < bool >( this , Event . CONFIGURATION_CHANGED );
}
2026-04-09 10:01:24 +02:00
private async Task OpenAssistantAuditDialogAsync ( Guid pluginId )
{
var parameters = new DialogParameters < AssistantPluginAuditDialog >
{
{ x => x . PluginId , pluginId },
};
var dialog = await this . DialogService . ShowAsync < AssistantPluginAuditDialog >( this . T ( "Assistant Audit" ), parameters , DialogOptions . FULLSCREEN );
var result = await dialog . Result ;
if ( result is null || result . Canceled || result . Data is not AssistantPluginAuditDialogResult auditResult )
return ;
if ( auditResult . Audit is not null )
this . UpsertAuditCard ( auditResult . Audit );
if ( auditResult . ActivatePlugin )
this . SettingsManager . ConfigurationData . EnabledPlugins . Add ( pluginId );
await this . SettingsManager . StoreSettings ();
await this . MessageBus . SendMessage < bool >( this , Event . CONFIGURATION_CHANGED );
}
private async Task < bool > ConfirmActivationBelowMinimumAsync ( string pluginName , AssistantAuditLevel actualLevel )
{
var dialogParameters = new DialogParameters < ConfirmDialog >
{
{
x => x . Message ,
string . Format (
2026-08-25 16:53:19 +02:00
this . T ( "The assistant plugin '{0}' was audited with the level '{1}', which is below the required minimum level '{2}'. Your current settings allow activation anyway, but this may be potentially dangerous. Do you really want to enable this plugin?" ),
2026-04-09 10:01:24 +02:00
pluginName ,
actualLevel . GetName (),
this . AssistantPluginAuditSettings . MinimumLevel . GetName ())
},
};
var dialogReference = await this . DialogService . ShowAsync < ConfirmDialog >( this . T ( "Potentially Dangerous Plugin" ), dialogParameters ,
DialogOptions . FULLSCREEN );
var dialogResult = await dialogReference . Result ;
return dialogResult is not null && ! dialogResult . Canceled ;
}
2026-02-10 15:23:56 +01:00
2026-04-09 10:01:24 +02:00
private bool IsActivationSwitchDisabled ( IPluginMetadata pluginMeta , bool isEnabled )
{
2026-08-29 14:12:14 +02:00
// An assistant plugin your organization requires to stay enabled has no switch to offer:
if ( PluginFactory . IsAssistantActivationEnforced ( pluginMeta . Id ))
return true ;
2026-04-09 10:01:24 +02:00
if ( isEnabled || pluginMeta . Type is not PluginType . ASSISTANT )
return false ;
var assistantPlugin = this . TryGetAssistantPlugin ( pluginMeta . Id );
if ( assistantPlugin is null )
return false ;
var securityState = PluginAssistantSecurityResolver . Resolve ( this . SettingsManager , assistantPlugin );
2026-07-15 21:00:25 +02:00
return securityState is { IsBlocked : true , RequiresAudit : false };
2026-04-09 10:01:24 +02:00
}
private string GetActivationTooltip ( IPluginMetadata pluginMeta , bool isEnabled )
{
2026-08-29 14:12:14 +02:00
if ( PluginFactory . IsAssistantActivationEnforced ( pluginMeta . Id ))
return this . T ( "Your organization requires this assistant to stay enabled" );
2026-04-09 10:01:24 +02:00
if ( isEnabled )
return this . T ( "Disable plugin" );
if ( pluginMeta . Type is not PluginType . ASSISTANT )
return this . T ( "Enable plugin" );
var assistantPlugin = this . TryGetAssistantPlugin ( pluginMeta . Id );
if ( assistantPlugin is null )
return this . T ( "Enable plugin" );
var securityState = PluginAssistantSecurityResolver . Resolve ( this . SettingsManager , assistantPlugin );
if ( securityState . RequiresAudit )
return securityState . ActionLabel ;
return securityState . IsBlocked
? securityState . Description
: this . T ( "Enable plugin" );
}
2026-08-06 10:42:20 +02:00
//
// These methods decide whether an action exists for a plugin at all. They must not depend on
// 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.
//
2026-08-28 14:06:24 +02:00
private static bool CanEditAssistantPlugin ( IAvailablePlugin plugin ) => plugin is { IsInternal : false , IsManagedByConfigServer : false , Type : PluginType . ASSISTANT } && ! string . IsNullOrWhiteSpace ( plugin . LocalPath );
2026-07-15 21:00:25 +02:00
2026-08-27 16:19:35 +02:00
/// <summary>
/// Whether this plugin is a direct chat launcher whose settings can be changed without AI.
/// </summary>
private static bool IsDirectChatLauncher ( IAvailablePlugin plugin )
{
var assistantPlugin = PluginFactory . RunningPlugins . OfType < PluginAssistants >(). FirstOrDefault ( x => x . Id == plugin . Id );
return assistantPlugin is not null && DirectChatLauncherLuaWriter . CanRewrite ( assistantPlugin );
}
2026-07-15 21:00:25 +02:00
private static bool CanReviseAssistantPlugin ( IAvailablePlugin plugin )
{
var assistantPlugin = PluginFactory . RunningPlugins . OfType < PluginAssistants >(). FirstOrDefault ( x => x . Id == plugin . Id );
2026-08-06 10:42:20 +02:00
return plugin is { IsInternal : false , IsManagedByConfigServer : false , Type : PluginType . ASSISTANT } && ! string . IsNullOrWhiteSpace ( plugin . LocalPath ) && assistantPlugin ?. IsManagedByConfigServer is false ;
2026-07-15 21:00:25 +02:00
}
2026-08-06 10:42:20 +02:00
/// <summary>
2026-08-09 19:01:58 +02:00
/// The plugin types users may share. This list has to match what the import accepts, otherwise
/// users would create archives nobody can install.
2026-08-06 10:42:20 +02:00
/// </summary>
2026-08-09 19:01:58 +02:00
private static readonly PluginType [] SHAREABLE_PLUGIN_TYPES = [ PluginType . ASSISTANT , PluginType . CONFIGURATION , PluginType . LANGUAGE ];
/// <summary>
/// Checks whether a plugin may be shared or exported as an archive. Plugins shipped with
/// AI Studio and plugins deployed by an organization stay with their owner.
/// </summary>
private static bool CanSharePlugin ( IAvailablePlugin plugin ) => plugin is { IsInternal : false , IsManagedByConfigServer : false } && SHAREABLE_PLUGIN_TYPES . Contains ( plugin . Type ) && ! string . IsNullOrWhiteSpace ( plugin . LocalPath );
2026-08-06 10:42:20 +02:00
/// <summary>
/// Highlights the plugin table while the user drags a file over the page, so it is visible
/// where the file would land.
/// </summary>
2026-09-10 15:28:08 +02:00
/// <param name="isDropTarget">Whether the page is the target of the drop being aimed right now.</param>
private static string PluginTableClass ( bool isDropTarget ) => isDropTarget
2026-08-06 10:42:20 +02:00
? "border-dashed border rounded-lg mud-border-primary border-4"
: "border-dashed border rounded-lg" ;
/// <summary>
/// Organizations may disable importing plugin archives by using a configuration plugin.
/// </summary>
private bool AllowPluginImport => this . SettingsManager . ConfigurationData . App . AllowUserToImportPlugins ;
/// <summary>
/// Organizations may disable sharing and exporting plugins by using a configuration plugin.
/// </summary>
private bool AllowPluginSharing => this . SettingsManager . ConfigurationData . App . AllowUserToSharePlugins ;
/// <summary>
/// Linux has no native share sheet, hence the plugin archive is exported to a location of the
/// user's choice there. The action must be labeled accordingly.
/// </summary>
private static string SharePluginIcon => OperatingSystem . IsLinux () ? Icons . Material . Filled . FileDownload : Icons . Material . Filled . IosShare ;
private string SharePluginTooltip => OperatingSystem . IsLinux () ? this . T ( "Export plugin archive" ) : this . T ( "Share plugin archive" );
private string SharePluginLockText => OperatingSystem . IsLinux () ? this . T ( "Your organization has disabled exporting plugins." ) : this . T ( "Your organization has disabled sharing plugins." );
2026-07-15 21:00:25 +02:00
private async Task OpenAssistantPluginEditorDialogAsync ( IAvailablePlugin plugin )
{
var parameters = new DialogParameters < AssistantPluginEditorDialog >
{
{ x => x . PluginId , plugin . Id },
{ x => x . PluginLocalPath , plugin . LocalPath },
};
var dialogReference = await this . DialogService . ShowAsync < AssistantPluginEditorDialog >( this . T ( "Edit Assistant Plugin" ), parameters , DialogOptions . BLOCKING_FULLSCREEN );
var dialogResult = await dialogReference . Result ;
if ( dialogResult is null || dialogResult . Canceled || dialogResult . Data is not AssistantPluginEditorDialogResult result )
return ;
await this . MessageBus . SendSuccess ( new ( Icons . Material . Filled . Save , string . Format ( this . T ( "The assistant plugin '{0}' has been successfully saved." ), result . PluginName )));
LOG . LogInformation ( $"The assistant plugin '{result.PluginName}' ({result.PluginId}) has been successfully updated." );
2026-08-06 10:42:20 +02:00
// Saving the plugin ran LoadAll, which already sent PLUGINS_RELOADED. Editing the plugin
// code changes no settings, so there is nothing else to announce:
2026-07-15 21:00:25 +02:00
await this . InvokeAsync ( this . StateHasChanged );
}
private async Task OpenAssistantPluginRevisionDialogAsync ( IAvailablePlugin plugin )
{
2026-08-27 16:19:35 +02:00
//
// Changing a launcher means picking a different workspace, provider, profile, chat template,
// or set of data sources. Prompting a model for that would be a detour, so launchers go to
// the mechanical dialog instead:
//
if ( IsDirectChatLauncher ( plugin ))
{
await this . OpenDirectChatLauncherSettingsDialogAsync ( plugin );
return ;
}
2026-07-15 21:00:25 +02:00
var parameters = new DialogParameters < AssistantPluginRevisionDialog >
{
{ x => x . PluginId , plugin . Id },
{ x => x . PluginLocalPath , plugin . LocalPath },
};
var dialogReference = await this . DialogService . ShowAsync < AssistantPluginRevisionDialog >( this . T ( "Revise Assistant Plugin" ), parameters , DialogOptions . BLOCKING_FULLSCREEN );
var dialogResult = await dialogReference . Result ;
if ( dialogResult is null || dialogResult . Canceled || dialogResult . Data is not AssistantPluginRevisionDialogResult result )
return ;
await this . MessageBus . SendSuccess ( new ( Icons . Material . Filled . AutoFixHigh , string . Format ( this . T ( "The assistant plugin '{0}' has been successfully revised." ), result . PluginName )));
LOG . LogInformation ( $"The assistant plugin '{result.PluginName}' ({result.PluginId}) has been successfully revised." );
2026-08-06 10:42:20 +02:00
// Saving the revision ran LoadAll, which already sent PLUGINS_RELOADED. We still announce the
// configuration change: with automatic audits enabled, the dialog stored an audit result:
2026-08-27 16:19:35 +02:00
await this . MessageBus . SendMessage < bool >( this , Event . CONFIGURATION_CHANGED );
await this . InvokeAsync ( this . StateHasChanged );
}
private async Task OpenDirectChatLauncherSettingsDialogAsync ( IAvailablePlugin plugin )
{
var parameters = new DialogParameters < DirectChatLauncherSettingsDialog >
{
{ x => x . PluginId , plugin . Id },
{ x => x . PluginLocalPath , plugin . LocalPath },
};
var dialogReference = await this . DialogService . ShowAsync < DirectChatLauncherSettingsDialog >( this . T ( "Tile Settings" ), parameters , DialogOptions . BLOCKING_FULLSCREEN );
var dialogResult = await dialogReference . Result ;
if ( dialogResult is null || dialogResult . Canceled || dialogResult . Data is not DirectChatLauncherSettingsDialogResult result )
return ;
await this . MessageBus . SendSuccess ( new ( Icons . Material . Filled . Save , string . Format ( this . T ( "The tile '{0}' has been updated." ), result . PluginName )));
LOG . LogInformation ( $"The chat launcher '{result.PluginName}' ({result.PluginId}) has been successfully updated." );
// Saving ran LoadAll, which already sent PLUGINS_RELOADED. We still announce the
// configuration change: with automatic audits enabled, the dialog stored an audit result:
2026-07-15 21:00:25 +02:00
await this . MessageBus . SendMessage < bool >( this , Event . CONFIGURATION_CHANGED );
await this . InvokeAsync ( this . StateHasChanged );
}
2026-08-06 10:42:20 +02:00
private async Task SharePluginAsync ( IAvailablePlugin plugin )
{
if ( this . isSharingPlugin )
return ;
this . isSharingPlugin = true ;
// invoke a state change right away to guard action buttons
await this . InvokeAsync ( this . StateHasChanged );
try
{
var shareResult = await this . PluginShareService . ShareAsync ( plugin , CancellationToken . None );
if ( shareResult . Cancelled )
return ;
if (! shareResult . Success )
{
LOG . LogError ( $"Sharing the plugin '{shareResult.PluginName}' from archive '{shareResult.ArchivePath}' failed with Issue: '{shareResult.Issue}'." );
await this . MessageBus . SendError ( new ( Icons . Material . Filled . ReportProblem , OperatingSystem . IsLinux () ? T ( "An error occurred while exporting the plugin." ) : T ( "An error occurred while sharing the plugin." )));
return ;
}
// On Linux, the user chose the target location, so we confirm where the archive was stored:
if ( OperatingSystem . IsLinux ())
await this . MessageBus . SendSuccess ( new ( Icons . Material . Filled . FileDownload , string . Format ( T ( "The plugin archive was exported to '{0}'." ), shareResult . ArchivePath )));
}
finally
{
this . isSharingPlugin = false ;
await this . InvokeAsync ( this . StateHasChanged );
}
}
private async Task ImportAssistantPluginAsync ()
{
if ( this . isImportingAssistantPlugin )
return ;
if (! this . AllowPluginImport )
return ;
2026-08-09 19:01:58 +02:00
var selection = await this . RustService . SelectFile ( this . T ( "Import plugin" ), [ FileTypes . PLUGIN_ARCHIVE ]);
2026-08-06 10:42:20 +02:00
if ( selection . UserCancelled )
return ;
await this . ImportPluginArchiveAsync ( selection . SelectedFilePath );
}
/// <summary>
/// Installs a plugin archive, no matter whether the user picked it through the import button or
/// dropped it onto the page.
/// </summary>
/// <param name="archivePath">The local plugin archive to install.</param>
private async Task ImportPluginArchiveAsync ( string archivePath )
{
if ( this . isImportingAssistantPlugin )
return ;
if (! this . AllowPluginImport )
return ;
this . isImportingAssistantPlugin = true ;
await this . InvokeAsync ( this . StateHasChanged );
try
{
2026-08-09 19:01:58 +02:00
var result = await this . PluginInstallService . InstallArchiveAsync ( archivePath , this . ConfirmPluginImportAsync , CancellationToken . None );
2026-08-06 10:42:20 +02:00
if ( result . Cancelled )
return ;
if (! result . Success )
{
LOG . LogError ( "Failed to import assistant plugin archive '{ArchivePath}': {Issue}" , archivePath , result . Issue );
// The user actively started this import, so we report the reason in a dialog
// instead of a snackbar. Refused imports must not be missed:
await this . ShowImportRefusedDialogAsync ( result . Issue );
return ;
}
var message = result . ReplacedExisting
2026-08-09 19:01:58 +02:00
? this . T ( "Plugin updated." )
: this . T ( "Plugin installed." );
2026-08-06 10:42:20 +02:00
// We do not announce the reload ourselves: a successful installation ran LoadAll, which
// already sent PLUGINS_RELOADED. The import changes no settings either, so there is
// nothing to report as a configuration change:
await this . MessageBus . SendSuccess ( new ( Icons . Material . Filled . Extension , message ));
}
finally
{
this . isImportingAssistantPlugin = false ;
await this . InvokeAsync ( this . StateHasChanged );
}
}
/// <summary>
/// Shows the metadata of a validated plugin archive and asks whether it may be installed.
/// </summary>
/// <param name="preview">The metadata the archive declares about itself.</param>
/// <returns>True when the user confirmed the installation.</returns>
private async Task < bool > ConfirmPluginImportAsync ( PluginImportPreview preview )
{
var dialogParameters = new DialogParameters < PluginImportDialog >
{
{ x => x . Preview , preview },
};
var dialogReference = await this . DialogService . ShowAsync < PluginImportDialog >( this . T ( "Install Plugin" ), dialogParameters , DialogOptions . FULLSCREEN );
var dialogResult = await dialogReference . Result ;
return dialogResult is { Canceled : false };
}
private async Task ShowImportRefusedDialogAsync ( string issue )
{
var dialogParameters = new DialogParameters < InformationDialog >
{
{ x => x . Message , string . Format ( this . T ( "The plugin could not be imported: {0}" ), issue ) },
{ x => x . Icon , Icons . Material . Filled . ReportProblem },
{ x => x . IconColor , Color . Error },
};
var dialogReference = await this . DialogService . ShowAsync < InformationDialog >( this . T ( "Import not possible" ), dialogParameters , DialogOptions . FULLSCREEN );
await dialogReference . Result ;
}
2026-02-10 15:23:56 +01:00
private static bool IsSendingMail ( string sourceUrl ) => sourceUrl . TrimStart (). StartsWith ( "mailto:" , StringComparison . OrdinalIgnoreCase );
2025-04-24 13:50:14 +02:00
2026-04-09 10:01:24 +02:00
private PluginAssistants ? TryGetAssistantPlugin ( Guid pluginId ) => PluginFactory . RunningPlugins . OfType < PluginAssistants >(). FirstOrDefault ( x => x . Id == pluginId );
private async Task TryAutoAuditAssistantsAsync ()
{
if ( this . isAutoAuditing || ! this . AssistantPluginAuditSettings . AutomaticallyAuditAssistants )
return ;
this . isAutoAuditing = true ;
try
{
var wasConfigurationChanged = false ;
var assistantPlugins = PluginFactory . RunningPlugins . OfType < PluginAssistants >(). ToList ();
foreach ( var assistantPlugin in assistantPlugins )
{
var securityState = PluginAssistantSecurityResolver . Resolve ( this . SettingsManager , assistantPlugin );
if (! securityState . RequiresAudit )
continue ;
var audit = await this . AssistantPluginAuditService . RunAuditAsync ( assistantPlugin );
if ( audit . Level is AssistantAuditLevel . UNKNOWN )
{
await MessageBus . INSTANCE . SendError ( new ( Icons . Material . Filled . SettingsSuggest , string . Format ( this . T ( "The automatic security audit for the assistant plugin '{0}' failed. Please run it manually." ), assistantPlugin . Name )));
continue ;
}
this . UpsertAuditCard ( audit );
wasConfigurationChanged = true ;
}
if (! wasConfigurationChanged )
return ;
await this . SettingsManager . StoreSettings ();
await this . MessageBus . SendMessage < bool >( this , Event . CONFIGURATION_CHANGED );
}
finally
{
this . isAutoAuditing = false ;
await this . InvokeAsync ( this . StateHasChanged );
}
}
private void UpsertAuditCard ( PluginAssistantAudit audit )
{
var audits = this . SettingsManager . ConfigurationData . AssistantPluginAudits ;
var existingIndex = audits . FindIndex ( x => x . PluginId == audit . PluginId );
if ( existingIndex >= 0 )
audits [ existingIndex ] = audit ;
else
audits . Add ( audit );
}
2025-04-24 13:50:14 +02:00
#region Overrides of MSGComponentBase
protected override async Task ProcessIncomingMessage < T >( ComponentBase ? sendingComponent , Event triggeredEvent , T ? data ) where T : default
{
switch ( triggeredEvent )
{
case Event . PLUGINS_RELOADED :
2026-04-09 10:01:24 +02:00
await this . TryAutoAuditAssistantsAsync ();
await this . InvokeAsync ( this . StateHasChanged );
break ;
case Event . CONFIGURATION_CHANGED :
2025-04-24 13:50:14 +02:00
await this . InvokeAsync ( this . StateHasChanged );
break ;
}
}
#endregion
2026-08-06 10:42:20 +02:00
/// <summary>
2026-09-10 15:28:08 +02:00
/// Decides whether this page may process dropped files: only when the organization allows
/// importing plugins at all and no import is running.
2026-08-06 10:42:20 +02:00
/// </summary>
2026-09-10 15:28:08 +02:00
private bool CanCatchDroppedFile () => this . AllowPluginImport && ! this . isImportingAssistantPlugin ;
2026-08-06 10:42:20 +02:00
/// <summary>
/// Imports a plugin archive the user dropped onto the page. Anything that is not exactly one
/// plugin archive is reported instead of guessing what the user meant.
/// </summary>
/// <param name="droppedPaths">The paths of the dropped files.</param>
private async Task ImportDroppedPluginArchiveAsync ( IReadOnlyList < string > droppedPaths )
{
var archivePaths = droppedPaths . Where ( path => FileTypes . IsAllowedPath ( path , FileTypes . PLUGIN_ARCHIVE )). ToList ();
switch ( archivePaths . Count )
{
case 0 :
await this . MessageBus . SendWarning ( new ( Icons . Material . Filled . ReportProblem , string . Format ( this . T ( "Please drop a plugin archive with the extension {0} or .zip." ), PluginArchive . PLUGIN_FILE_EXTENSION )));
return ;
case > 1 :
await this . MessageBus . SendWarning ( new ( Icons . Material . Filled . ReportProblem , this . T ( "Please drop only one plugin archive at a time." )));
return ;
}
await this . ImportPluginArchiveAsync ( archivePaths [ 0 ]);
}
}