diff --git a/app/MindWork AI Studio/Agents/AssistantAudit/AssistantAuditLevelExtensions.cs b/app/MindWork AI Studio/Agents/AssistantAudit/AssistantAuditLevelExtensions.cs index 54f57329..4e7b05dd 100644 --- a/app/MindWork AI Studio/Agents/AssistantAudit/AssistantAuditLevelExtensions.cs +++ b/app/MindWork AI Studio/Agents/AssistantAudit/AssistantAuditLevelExtensions.cs @@ -9,11 +9,11 @@ public static class AssistantAuditLevelExtensions public static string GetName(this AssistantAuditLevel level) => level switch { AssistantAuditLevel.DANGEROUS => TB("Dangerous"), - AssistantAuditLevel.CAUTION => TB("Needs Review"), + AssistantAuditLevel.CAUTION => TB("Concerning"), AssistantAuditLevel.SAFE => TB("Safe"), _ => TB("Unknown"), }; - + public static Severity GetSeverity(this AssistantAuditLevel level) => level switch { AssistantAuditLevel.DANGEROUS => Severity.Error, @@ -22,5 +22,26 @@ public static class AssistantAuditLevelExtensions _ => Severity.Info, }; + public static Color GetColor(this AssistantAuditLevel level) => level switch + { + AssistantAuditLevel.DANGEROUS => Color.Error, + AssistantAuditLevel.CAUTION => Color.Warning, + AssistantAuditLevel.SAFE => Color.Success, + _ => Color.Default, + }; + + public static string GetIcon(this AssistantAuditLevel level) => level switch + { + AssistantAuditLevel.DANGEROUS => Icons.Material.Filled.Dangerous, + AssistantAuditLevel.CAUTION => Icons.Material.Filled.Warning, + AssistantAuditLevel.SAFE => Icons.Material.Filled.Verified, + _ => Icons.Material.Filled.HelpOutline, + }; + + /// + /// Parses an audit level string and falls back to when parsing fails. + /// + /// The audit level text to parse. + /// The parsed audit level, or for null, empty, or invalid values. public static AssistantAuditLevel Parse(string? value) => Enum.TryParse(value, true, out var level) ? level : AssistantAuditLevel.UNKNOWN; } diff --git a/app/MindWork AI Studio/Assistants/Dynamic/AssistantDynamic.razor b/app/MindWork AI Studio/Assistants/Dynamic/AssistantDynamic.razor index 826052cc..e1a1729c 100644 --- a/app/MindWork AI Studio/Assistants/Dynamic/AssistantDynamic.razor +++ b/app/MindWork AI Studio/Assistants/Dynamic/AssistantDynamic.razor @@ -5,7 +5,21 @@ @using AIStudio.Tools.PluginSystem.Assistants.DataModel.Layout @inherits AssistantBaseCore -@if (this.RootComponent is null) +@if (!string.IsNullOrWhiteSpace(this.securityMessage)) +{ + + + @this.securityMessage + + @if (this.assistantPlugin is not null) + { +
+ +
+ } +
+} +else if (this.RootComponent is null) { @this.T("No assistant plugin are currently installed.") diff --git a/app/MindWork AI Studio/Assistants/Dynamic/AssistantDynamic.razor.cs b/app/MindWork AI Studio/Assistants/Dynamic/AssistantDynamic.razor.cs index 4f7c512c..0e44e832 100644 --- a/app/MindWork AI Studio/Assistants/Dynamic/AssistantDynamic.razor.cs +++ b/app/MindWork AI Studio/Assistants/Dynamic/AssistantDynamic.razor.cs @@ -22,6 +22,7 @@ public partial class AssistantDynamic : AssistantBaseCore protected override bool ShowProfileSelection => this.showFooterProfileSelection; protected override string SubmitText => this.submitText; protected override Func SubmitAction => this.Submit; + protected override bool SubmitDisabled => this.isSecurityBlocked; // Dynamic assistants do not have dedicated settings yet. // Reuse chat-level provider filtering/preselection instead of NONE. protected override Tools.Components Component => Tools.Components.CHAT; @@ -40,6 +41,8 @@ public partial class AssistantDynamic : AssistantBaseCore private readonly HashSet executingSwitchActions = []; private string pluginPath = string.Empty; private PluginAssistantAudit? audit; + private string securityMessage = string.Empty; + private bool isSecurityBlocked; private const string ASSISTANT_QUERY_KEY = "assistantId"; #region Implementation of AssistantBase @@ -66,6 +69,16 @@ public partial class AssistantDynamic : AssistantBaseCore var pluginHash = pluginAssistant.ComputeAuditHash(); this.audit = this.SettingsManager.ConfigurationData.AssistantPluginAudits.FirstOrDefault(x => x.PluginId == pluginAssistant.Id && x.PluginHash == pluginHash); + var securityState = PluginAssistantSecurityResolver.Resolve(this.SettingsManager, pluginAssistant); + if (!securityState.CanStartAssistant) + { + this.assistantPlugin = pluginAssistant; + this.securityMessage = securityState.Description; + this.isSecurityBlocked = true; + base.OnInitialized(); + return; + } + var rootComponent = this.RootComponent; if (rootComponent is not null) { @@ -387,6 +400,13 @@ public partial class AssistantDynamic : AssistantBaseCore private async Task Submit() { + if (this.assistantPlugin is not null) + { + var securityState = PluginAssistantSecurityResolver.Resolve(this.SettingsManager, this.assistantPlugin); + if (!securityState.CanStartAssistant) + return; + } + this.CreateChatThread(); var time = this.AddUserRequest(await this.CollectUserPromptAsync()); await this.AddAIResponseAsync(time); diff --git a/app/MindWork AI Studio/Dialogs/AssistantPluginAuditDialog.razor b/app/MindWork AI Studio/Dialogs/AssistantPluginAuditDialog.razor index df49447d..637f3329 100644 --- a/app/MindWork AI Studio/Dialogs/AssistantPluginAuditDialog.razor +++ b/app/MindWork AI Studio/Dialogs/AssistantPluginAuditDialog.razor @@ -1,5 +1,4 @@ @using AIStudio.Agents.AssistantAudit -@using AIStudio.Components @inherits MSGComponentBase @@ -299,7 +298,7 @@ @(this.audit is null ? T("Cancel") : T("Close")) - + @T("Start Security Check") @if (this.CanEnablePlugin) diff --git a/app/MindWork AI Studio/Dialogs/AssistantPluginAuditDialog.razor.cs b/app/MindWork AI Studio/Dialogs/AssistantPluginAuditDialog.razor.cs index 266e98d6..0c01ae12 100644 --- a/app/MindWork AI Studio/Dialogs/AssistantPluginAuditDialog.razor.cs +++ b/app/MindWork AI Studio/Dialogs/AssistantPluginAuditDialog.razor.cs @@ -1,3 +1,7 @@ +using System.Collections; +using System.Collections.Immutable; +using System.Globalization; +using System.Reflection; using AIStudio.Agents.AssistantAudit; using AIStudio.Components; using AIStudio.Provider; @@ -6,17 +10,12 @@ using AIStudio.Tools.PluginSystem; using AIStudio.Tools.PluginSystem.Assistants; using AIStudio.Tools.PluginSystem.Assistants.DataModel; using Microsoft.AspNetCore.Components; -using System.Collections; -using System.Collections.Immutable; -using System.Globalization; -using System.Reflection; namespace AIStudio.Dialogs; public partial class AssistantPluginAuditDialog : MSGComponentBase { - private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(AssistantPluginAuditDialog).Namespace, - nameof(AssistantPluginAuditDialog)); + private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(AssistantPluginAuditDialog).Namespace, nameof(AssistantPluginAuditDialog)); [CascadingParameter] private IMudDialogInstance MudDialog { get; set; } = null!; @@ -37,7 +36,7 @@ public partial class AssistantPluginAuditDialog : MSGComponentBase private ImmutableDictionary luaFiles = ImmutableDictionary.Create(); private IReadOnlyCollection> componentTreeItems = []; private IReadOnlyCollection> fileSystemTreeItems = []; - private CultureInfo fileInfoCulture = CultureInfo.InvariantCulture; + private CultureInfo currentCultureInfo = CultureInfo.InvariantCulture; private bool isAuditing; private AIStudio.Settings.Provider CurrentProvider => this.SettingsManager.GetPreselectedProvider(Tools.Components.AGENT_ASSISTANT_PLUGIN_AUDIT, null, true); @@ -63,13 +62,14 @@ public partial class AssistantPluginAuditDialog : MSGComponentBase private bool CanEnablePlugin => this.audit is not null && !this.isAuditing && !this.IsActivationBlockedBySettings; private Color EnableButtonColor => this.RequiresActivationConfirmation ? Color.Warning : Color.Success; + private bool justAudited; private const ushort BYTES_PER_KILOBYTE = 1024; protected override async Task OnInitializedAsync() { var activeLanguagePlugin = await this.SettingsManager.GetActiveLanguagePlugin(); - this.fileInfoCulture = this.CreateFileInfoCulture(activeLanguagePlugin.IETFTag); + this.currentCultureInfo = CommonTools.DeriveActiveCultureOrInvariant(activeLanguagePlugin.IETFTag); this.plugin = PluginFactory.RunningPlugins.OfType() .FirstOrDefault(x => x.Id == this.PluginId); @@ -116,6 +116,7 @@ public partial class AssistantPluginAuditDialog : MSGComponentBase finally { this.isAuditing = false; + this.justAudited = true; await this.InvokeAsync(this.StateHasChanged); } } @@ -453,7 +454,7 @@ public partial class AssistantPluginAuditDialog : MSGComponentBase string stringValue when string.IsNullOrWhiteSpace(stringValue) => TB("empty"), string stringValue => stringValue, bool boolValue => boolValue ? "true" : "false", - _ => Convert.ToString(value, System.Globalization.CultureInfo.InvariantCulture) ?? string.Empty, + _ => Convert.ToString(value, CultureInfo.InvariantCulture) ?? string.Empty, }; private string GetStructuredValueCaption(object? value) => value switch @@ -477,37 +478,23 @@ public partial class AssistantPluginAuditDialog : MSGComponentBase _ => Icons.Material.Filled.DataArray, }; - private string FormatFileTimestamp(DateTime timestamp) => timestamp.ToString("g", this.fileInfoCulture); + private string FormatFileTimestamp(DateTime timestamp) => CommonTools.FormatTimestampToGeneral(timestamp, this.currentCultureInfo); private string FormatFileSize(long bytes) { if (bytes < BYTES_PER_KILOBYTE) - return string.Format(this.fileInfoCulture, TB("{0} B"), bytes); + return string.Format(this.currentCultureInfo, TB("{0} B"), bytes); var kilobyte = bytes / (double)BYTES_PER_KILOBYTE; if (kilobyte < BYTES_PER_KILOBYTE) - return string.Format(this.fileInfoCulture, TB("{0:0.##} KB"), kilobyte); + return string.Format(this.currentCultureInfo, TB("{0:0.##} KB"), kilobyte); var megabyte = kilobyte / BYTES_PER_KILOBYTE; if (megabyte < BYTES_PER_KILOBYTE) - return string.Format(this.fileInfoCulture, TB("{0:0.##} MB"), megabyte); + return string.Format(this.currentCultureInfo, TB("{0:0.##} MB"), megabyte); var gigabyte = megabyte / BYTES_PER_KILOBYTE; - return string.Format(this.fileInfoCulture, TB("{0:0.##} GB"), gigabyte); + return string.Format(this.currentCultureInfo, TB("{0:0.##} GB"), gigabyte); } - private CultureInfo CreateFileInfoCulture(string ietfTag) - { - if (string.IsNullOrWhiteSpace(ietfTag)) - return CultureInfo.InvariantCulture; - - try - { - return CultureInfo.GetCultureInfo(ietfTag); - } - catch (CultureNotFoundException) - { - return CultureInfo.InvariantCulture; - } - } }