mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-08-12 10:32:11 +00:00
added a stepper to interactively show all the processes from validating and installing to automatic security audits
This commit is contained in:
parent
52a1657c0a
commit
386ac8f02a
@ -1,4 +1,5 @@
|
||||
@attribute [Route(Routes.ASSISTANT_META_ASSISTANT)]
|
||||
@using AIStudio.Agents.AssistantAudit
|
||||
@using AIStudio.Tools.PluginSystem.Assistants.DataModel
|
||||
@inherits AssistantBaseCore<AIStudio.Dialogs.Settings.NoSettingsPanel>
|
||||
|
||||
@ -38,7 +39,7 @@
|
||||
</MudExpansionPanels>
|
||||
|
||||
<ProviderSelection @bind-ProviderSettings="@this.ProviderSettings" ValidateProvider="@this.ValidatingProvider"/>
|
||||
<MudAlert Severity="Severity.Info" Square="true" Class="mb-2 mt-n1" Style="width: max-content">@this.highPerformanceLLMInfo</MudAlert>
|
||||
<MudAlert Severity="Severity.Info" Square="true" Class="mb-2 mt-n1" Style="width: max-content">@this.HighPerformanceLLMInfo</MudAlert>
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -71,11 +72,188 @@ else
|
||||
</MudStack>
|
||||
|
||||
<ProviderSelection @bind-ProviderSettings="@this.ProviderSettings" ValidateProvider="@this.ValidatingProvider"/>
|
||||
<MudAlert Severity="Severity.Info" Square="true" Class="mb-2 mt-n1" Style="width: max-content">@this.highPerformanceLLMInfo</MudAlert>
|
||||
<MudAlert Severity="Severity.Info" Square="true" Class="mb-2 mt-n1" Style="width: max-content">@this.HighPerformanceLLMInfo</MudAlert>
|
||||
}
|
||||
|
||||
@code {
|
||||
private protected override RenderFragment? AfterSubmitContent => @<MudCard>
|
||||
private protected override RenderFragment? BelowSubmitContent => this.step is BuilderStep.DONE && !string.IsNullOrWhiteSpace(this.generatedLuaAssistant)
|
||||
? @<MudStack Spacing="3" Class="mb-3">
|
||||
<MudExpansionPanels Dense="@true" Elevation="0" Class="rounded">
|
||||
<MudExpansionPanel Dense="@true" Class="border-solid border rounded pt-n4" Style="border-color: #BDBDBD">
|
||||
<TitleContent>
|
||||
<div class="d-flex align-center">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Code" Class="mr-3" Color="Color.Primary"/>
|
||||
<MudText Typo="Typo.button">
|
||||
@T("Generated Lua plugin")
|
||||
</MudText>
|
||||
</div>
|
||||
</TitleContent>
|
||||
<ChildContent>
|
||||
<MudTextField T="string" Text="@this.generatedLuaAssistant" ReadOnly="true" Variant="Variant.Outlined" Lines="18" Class="mt-2" Style="font-family: monospace"/>
|
||||
</ChildContent>
|
||||
</MudExpansionPanel>
|
||||
</MudExpansionPanels>
|
||||
|
||||
<MudStepper @bind-ActiveIndex="@this.stepperIndex" CompletedStepColor="Color.Primary" CurrentStepColor="Color.Primary" ErrorStepColor="Color.Error" NonLinear="@false" ShowResetButton="@false" Class="mb-3">
|
||||
<ChildContent>
|
||||
<MudStep Title="@T("Validate plugin")" Completed="@this.PluginCheckCompleted" HasError="@this.IsInstallStepFailed(BuilderInstallStep.CHECK_PLUGIN)">
|
||||
<MudStack Spacing="2" Class="mt-2">
|
||||
@if (this.isCheckingPlugin)
|
||||
{
|
||||
<MudProgressLinear Color="Color.Primary" Indeterminate="@true"/>
|
||||
<MudText Typo="Typo.body2">@T("Validating the generated assistant...")</MudText>
|
||||
}
|
||||
else if (this.IsInstallStepFailed(BuilderInstallStep.CHECK_PLUGIN))
|
||||
{
|
||||
<MudAlert Severity="Severity.Error" Dense="@true">
|
||||
@T("The generated assistant could not be checked.")
|
||||
@if (!string.IsNullOrWhiteSpace(this.installFlowIssue))
|
||||
{
|
||||
<span> @string.Format(T("Issue: {0}"), this.installFlowIssue)</span>
|
||||
}
|
||||
</MudAlert>
|
||||
}
|
||||
else if (this.PluginCheckCompleted)
|
||||
{
|
||||
<MudAlert Severity="Severity.Info" Dense="@true" Icon="@Icons.Material.Filled.CheckCircle">
|
||||
@string.Format(T("The generated assistant \"{0}\" is valid and runnable."), this.pluginCheckResult?.PluginName ?? T("Unknown assistant"))
|
||||
</MudAlert>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" StartIcon="@Icons.Material.Filled.Settings" Disabled="@(!this.CanRunPluginCheck)" OnClick="@(async () => await this.CheckGeneratedAssistantAsync())">
|
||||
@T("Validate generated assistant")
|
||||
</MudButton>
|
||||
}
|
||||
</MudStack>
|
||||
</MudStep>
|
||||
|
||||
<MudStep Title="@T("Install assistant")" Completed="@this.PluginInstallCompleted" HasError="@this.IsInstallStepFailed(BuilderInstallStep.INSTALL_ASSISTANT)">
|
||||
<MudStack Spacing="2" Class="mt-2">
|
||||
@if (this.isInstallingPlugin)
|
||||
{
|
||||
<MudProgressLinear Color="Color.Primary" Indeterminate="@true"/>
|
||||
<MudText Typo="Typo.body2">@T("Installing the assistant...")</MudText>
|
||||
}
|
||||
else if (this.IsInstallStepFailed(BuilderInstallStep.INSTALL_ASSISTANT))
|
||||
{
|
||||
<MudAlert Severity="Severity.Error" Dense="@true">
|
||||
@T("The assistant could not be installed.")
|
||||
@if (!string.IsNullOrWhiteSpace(this.installFlowIssue))
|
||||
{
|
||||
<span> @string.Format(T("Issue: {0}"), this.installFlowIssue)</span>
|
||||
}
|
||||
</MudAlert>
|
||||
}
|
||||
else if (this.PluginInstallCompleted)
|
||||
{
|
||||
<MudAlert Severity="Severity.Info" Dense="@true" Icon="@Icons.Material.Filled.Extension">
|
||||
@(this.pluginInstallResult?.ReplacedExisting is true
|
||||
? string.Format(T("The assistant \"{0}\" was updated."), this.pluginInstallResult?.PluginName ?? T("Unknown assistant"))
|
||||
: string.Format(T("The assistant \"{0}\" was installed."), this.pluginInstallResult?.PluginName ?? T("Unknown assistant")))
|
||||
</MudAlert>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" StartIcon="@Icons.Material.Filled.Extension" Disabled="@(!this.CanInstallPlugin)" OnClick="@(async () => await this.InstallGeneratedAssistantAsync())">
|
||||
@T("Install assistant")
|
||||
</MudButton>
|
||||
}
|
||||
</MudStack>
|
||||
</MudStep>
|
||||
|
||||
<MudStep Title="@T("Security audit")" Completed="@(!this.AuditRequiredForActivation || this.AuditCompleted)" HasError="@this.IsInstallStepFailed(BuilderInstallStep.SECURITY_CHECK)">
|
||||
<MudStack Spacing="2" Class="mt-2">
|
||||
@if (this.isAuditingPlugin)
|
||||
{
|
||||
<MudProgressLinear Color="Color.Primary" Indeterminate="@true"/>
|
||||
<MudText Typo="Typo.body2">@T("Auditing assistants safety...")</MudText>
|
||||
}
|
||||
else if (this.IsInstallStepFailed(BuilderInstallStep.SECURITY_CHECK))
|
||||
{
|
||||
<MudAlert Severity="Severity.Error" Dense="@true">
|
||||
@T("The security audit could not be completed.")
|
||||
@if (!string.IsNullOrWhiteSpace(this.installFlowIssue))
|
||||
{
|
||||
<span> @string.Format(T("Issue: {0}"), this.installFlowIssue)</span>
|
||||
}
|
||||
</MudAlert>
|
||||
}
|
||||
else if (this.AuditCompleted)
|
||||
{
|
||||
<MudAlert Severity="@this.AuditSeverity" Dense="@true" Icon="@this.pluginAudit!.Level.GetIcon()">
|
||||
<strong>@this.pluginAudit.Level.GetName()</strong><span>: @this.pluginAudit.Summary</span>
|
||||
</MudAlert>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" StartIcon="@Icons.Material.Filled.Security" Disabled="@(!this.CanRunAudit)" OnClick="@(async () => await this.RunSecurityCheckAsync())">
|
||||
@T("Start security audit")
|
||||
</MudButton>
|
||||
}
|
||||
</MudStack>
|
||||
</MudStep>
|
||||
|
||||
<MudStep Title="@T("Enable assistant")" Completed="@this.EnableCompleted" HasError="@this.IsInstallStepFailed(BuilderInstallStep.ENABLE_ASSISTANT)">
|
||||
<MudStack Spacing="2" Class="mt-2">
|
||||
@if (this.isEnablingPlugin)
|
||||
{
|
||||
<MudProgressLinear Color="Color.Primary" Indeterminate="@true"/>
|
||||
<MudText Typo="Typo.body2">@T("Enabling the assistant...")</MudText>
|
||||
}
|
||||
else if (this.IsInstallStepFailed(BuilderInstallStep.ENABLE_ASSISTANT))
|
||||
{
|
||||
<MudAlert Severity="Severity.Error" Dense="@true">
|
||||
@T("The assistant cannot be enabled.")
|
||||
@if (!string.IsNullOrWhiteSpace(this.installFlowIssue))
|
||||
{
|
||||
<span> @string.Format(T("Issue: {0}"), this.installFlowIssue)</span>
|
||||
}
|
||||
</MudAlert>
|
||||
}
|
||||
else if (this.EnableCompleted)
|
||||
{
|
||||
<MudAlert Severity="Severity.Info" Dense="@true" Icon="@Icons.Material.Filled.ToggleOn">
|
||||
@T("The assistant is enabled.")
|
||||
</MudAlert>
|
||||
}
|
||||
else
|
||||
{
|
||||
@if (this.RequiresActivationConfirmation)
|
||||
{
|
||||
<MudAlert Severity="Severity.Warning" Dense="@true" Icon="@Icons.Material.Filled.WarningAmber">
|
||||
@T("The security check is below your required level. Your settings allow activation after confirmation.")
|
||||
</MudAlert>
|
||||
}
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" StartIcon="@Icons.Material.Filled.ToggleOn" Disabled="@(!this.CanEnableAssistant)" OnClick="@(async () => await this.EnableInstalledAssistantAsync())">
|
||||
@T("Enable assistant")
|
||||
</MudButton>
|
||||
}
|
||||
</MudStack>
|
||||
</MudStep>
|
||||
|
||||
<MudStep Title="@T("Open assistant")">
|
||||
<MudStack Spacing="2" Class="mt-2">
|
||||
@if (this.CanOpenAssistant)
|
||||
{
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" StartIcon="@Icons.Material.Filled.OpenInNew" OnClick="@this.OpenInstalledAssistant">
|
||||
@T("Open assistant")
|
||||
</MudButton>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudText Typo="Typo.body2">@T("Enable the assistant before opening it.")</MudText>
|
||||
}
|
||||
</MudStack>
|
||||
</MudStep>
|
||||
</ChildContent>
|
||||
<ActionContent Context="_">
|
||||
</ActionContent>
|
||||
</MudStepper>
|
||||
</MudStack>
|
||||
: null;
|
||||
|
||||
private protected override RenderFragment AfterSubmitContent => @<MudCard>
|
||||
<MudCardContent>
|
||||
<MudSkeleton />
|
||||
<MudSkeleton Animation="Animation.False" />
|
||||
|
||||
@ -4,9 +4,11 @@ using System.Reflection;
|
||||
// ReSharper restore RedundantUsingDirective
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using AIStudio.Chat;
|
||||
using AIStudio.Agents.AssistantAudit;
|
||||
using AIStudio.Dialogs;
|
||||
using AIStudio.Dialogs.Settings;
|
||||
using AIStudio.Tools.PluginSystem;
|
||||
using AIStudio.Tools.PluginSystem.Assistants;
|
||||
using AIStudio.Tools.PluginSystem.Assistants.DataModel;
|
||||
using AIStudio.Tools.Services;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
@ -22,6 +24,9 @@ public partial class AssistantBuilder : AssistantBaseCore<NoSettingsPanel>
|
||||
[Inject]
|
||||
private AssistantPluginInstallService AssistantPluginInstallService { get; init; } = null!;
|
||||
|
||||
[Inject]
|
||||
private AssistantPluginAuditService AssistantPluginAuditService { get; init; } = null!;
|
||||
|
||||
private static readonly ILogger LOGGER = Program.LOGGER_FACTORY.CreateLogger(nameof(AssistantBuilder));
|
||||
private static readonly JsonSerializerOptions UNTRUSTED_PROMPT_JSON_OPTIONS = new()
|
||||
{
|
||||
@ -63,15 +68,12 @@ public partial class AssistantBuilder : AssistantBaseCore<NoSettingsPanel>
|
||||
BuilderStep.DONE => this.GenerateLuaAssistant,
|
||||
_ => this.GenerateAssistantSpec,
|
||||
};
|
||||
protected override bool SubmitDisabled => this.isAgentRunning || this.isInstallingPlugin;
|
||||
protected override bool ShowResult => this.step is BuilderStep.DONE;
|
||||
protected override bool ShowEntireChatThread => this.step is BuilderStep.DONE;
|
||||
protected override bool SubmitDisabled => this.isAgentRunning || this.IsInstallFlowRunning;
|
||||
protected override bool ShowResult => false;
|
||||
protected override bool ShowEntireChatThread => false;
|
||||
protected override bool AllowProfiles => false;
|
||||
protected override bool ShowProfileSelection => false;
|
||||
protected override bool ShowCopyResult => this.step is BuilderStep.DONE;
|
||||
protected override IReadOnlyList<IButtonData> FooterButtons => this.step is BuilderStep.DONE
|
||||
? [new ButtonData(T("Install assistant"), Icons.Material.Filled.Extension, Color.Primary, T("Install this generated assistant as a plugin."), this.InstallPluginAsync, () => this.isAgentRunning || this.isInstallingPlugin || string.IsNullOrWhiteSpace(this.generatedLuaAssistant))]
|
||||
: [];
|
||||
|
||||
protected override bool HasSettingsPanel => false;
|
||||
protected override Func<string> Result2Copy => () => !string.IsNullOrWhiteSpace(this.generatedLuaAssistant)
|
||||
@ -80,7 +82,10 @@ public partial class AssistantBuilder : AssistantBaseCore<NoSettingsPanel>
|
||||
|
||||
private BuilderStep step = BuilderStep.DESCRIBE;
|
||||
private bool isAgentRunning;
|
||||
private bool isCheckingPlugin;
|
||||
private bool isInstallingPlugin;
|
||||
private bool isAuditingPlugin;
|
||||
private bool isEnablingPlugin;
|
||||
private string assistantDescription = string.Empty;
|
||||
private AssistantCategory selectedCategory;
|
||||
private string customCategory = string.Empty;
|
||||
@ -97,8 +102,14 @@ public partial class AssistantBuilder : AssistantBaseCore<NoSettingsPanel>
|
||||
private string reviewNotes = string.Empty;
|
||||
private string generatedLuaAssistant = string.Empty;
|
||||
private Guid pluginId = Guid.NewGuid();
|
||||
|
||||
private string highPerformanceLLMInfo => T("It is recommended to a powerful LLM.");
|
||||
private string HighPerformanceLLMInfo => T("It is recommended to a powerful LLM.");
|
||||
private int stepperIndex;
|
||||
private AssistantPluginCheckResult? pluginCheckResult;
|
||||
private AssistantPluginInstallResult? pluginInstallResult;
|
||||
private PluginAssistantAudit? pluginAudit;
|
||||
private PluginAssistants? installedAssistantPlugin;
|
||||
private BuilderInstallStep? failedInstallStep;
|
||||
private string installFlowIssue = string.Empty;
|
||||
private static readonly AssistantContextFile[] ASSISTANT_CONTEXT_FILES =
|
||||
[
|
||||
new("Assistant plugin schema", "Plugins/assistants/README.md", IsRequired: true),
|
||||
@ -113,6 +124,56 @@ public partial class AssistantBuilder : AssistantBaseCore<NoSettingsPanel>
|
||||
REVIEW_SPEC,
|
||||
DONE,
|
||||
}
|
||||
|
||||
private enum BuilderInstallStep
|
||||
{
|
||||
CHECK_PLUGIN = 0,
|
||||
INSTALL_ASSISTANT = 1,
|
||||
SECURITY_CHECK = 2,
|
||||
ENABLE_ASSISTANT = 3,
|
||||
OPEN_ASSISTANT = 4,
|
||||
}
|
||||
|
||||
private bool IsInstallFlowRunning => this.isCheckingPlugin || this.isInstallingPlugin || this.isAuditingPlugin || this.isEnablingPlugin;
|
||||
|
||||
private bool PluginCheckCompleted => this.pluginCheckResult?.Success is true;
|
||||
|
||||
private bool PluginInstallCompleted => this.pluginInstallResult?.Success is true;
|
||||
|
||||
private bool AuditCompleted => this.pluginAudit is not null && this.pluginAudit.Level is not AssistantAuditLevel.UNKNOWN;
|
||||
|
||||
private bool AuditRequiredForActivation => this.SettingsManager.ConfigurationData.AssistantPluginAudit.RequireAuditBeforeActivation;
|
||||
|
||||
private bool EnableCompleted => this.pluginInstallResult is not null && this.SettingsManager.ConfigurationData.EnabledPlugins.Contains(this.pluginInstallResult.PluginId);
|
||||
|
||||
private bool CanRunPluginCheck => !this.IsInstallFlowRunning && !string.IsNullOrWhiteSpace(this.generatedLuaAssistant);
|
||||
|
||||
private bool CanInstallPlugin => !this.IsInstallFlowRunning && this.PluginCheckCompleted;
|
||||
|
||||
private bool CanRunAudit => !this.IsInstallFlowRunning && this.PluginInstallCompleted && this.installedAssistantPlugin is not null;
|
||||
|
||||
private bool CanEnableAssistant => !this.IsInstallFlowRunning && this.PluginInstallCompleted && !this.IsActivationBlockedBySettings;
|
||||
|
||||
private bool CanOpenAssistant => this.EnableCompleted && this.pluginInstallResult is not null;
|
||||
|
||||
private bool IsAuditBelowMinimum => this.pluginAudit is not null && this.pluginAudit.Level < this.SettingsManager.ConfigurationData.AssistantPluginAudit.MinimumLevel;
|
||||
|
||||
private bool IsActivationBlockedBySettings => this.AuditRequiredForActivation &&
|
||||
(!this.AuditCompleted ||
|
||||
this.IsAuditBelowMinimum && this.SettingsManager.ConfigurationData.AssistantPluginAudit.BlockActivationBelowMinimum);
|
||||
|
||||
private bool RequiresActivationConfirmation => this.AuditCompleted &&
|
||||
this.IsAuditBelowMinimum &&
|
||||
!this.IsActivationBlockedBySettings;
|
||||
|
||||
private Severity AuditSeverity => this.pluginAudit?.Level switch
|
||||
{
|
||||
AssistantAuditLevel.DANGEROUS => Severity.Error,
|
||||
AssistantAuditLevel.CAUTION => Severity.Warning,
|
||||
AssistantAuditLevel.SAFE => Severity.Info,
|
||||
_ => Severity.Normal,
|
||||
};
|
||||
|
||||
private static readonly AssistantComponentType[] ASSISTANT_COMPONENT_OPTIONS =
|
||||
[
|
||||
AssistantComponentType.TEXT_AREA,
|
||||
@ -145,6 +206,7 @@ public partial class AssistantBuilder : AssistantBaseCore<NoSettingsPanel>
|
||||
this.generatedAssistantSpec = string.Empty;
|
||||
this.reviewNotes = string.Empty;
|
||||
this.generatedLuaAssistant = string.Empty;
|
||||
this.ResetInstallFlow();
|
||||
}
|
||||
|
||||
protected override bool MightPreselectValues() => false;
|
||||
@ -240,8 +302,8 @@ public partial class AssistantBuilder : AssistantBaseCore<NoSettingsPanel>
|
||||
return;
|
||||
}
|
||||
|
||||
this.ResetInstallFlow();
|
||||
this.generatedLuaAssistant = parsedResponse.FullLua.Trim();
|
||||
this.AddGeneratedLuaPreviewResult();
|
||||
this.step = BuilderStep.DONE;
|
||||
}
|
||||
finally
|
||||
@ -254,12 +316,14 @@ public partial class AssistantBuilder : AssistantBaseCore<NoSettingsPanel>
|
||||
{
|
||||
this.step = BuilderStep.DESCRIBE;
|
||||
this.generatedLuaAssistant = string.Empty;
|
||||
this.ResetInstallFlow();
|
||||
}
|
||||
|
||||
private void BackToSpecReview()
|
||||
{
|
||||
this.step = BuilderStep.REVIEW_SPEC;
|
||||
this.generatedLuaAssistant = string.Empty;
|
||||
this.ResetInstallFlow();
|
||||
}
|
||||
|
||||
private async Task EditDraftAndDiscardPluginPreview()
|
||||
@ -291,6 +355,7 @@ public partial class AssistantBuilder : AssistantBaseCore<NoSettingsPanel>
|
||||
return;
|
||||
|
||||
this.generatedLuaAssistant = string.Empty;
|
||||
this.ResetInstallFlow();
|
||||
this.step = BuilderStep.REVIEW_SPEC;
|
||||
}
|
||||
|
||||
@ -353,7 +418,8 @@ public partial class AssistantBuilder : AssistantBaseCore<NoSettingsPanel>
|
||||
- The future Lua plugin must be loadable by AI Studio.
|
||||
- Include assumptions instead of asking follow-up questions.
|
||||
- Treat filled optional guidance as explicit user intent.
|
||||
- Keep technical identifiers untranslated, such as TEXT_AREA, DROPDOWN, PROVIDER_SELECTION, PROFILE_SELECTION, BuildPrompt, and plugin.lua.
|
||||
- Do not mention the PROVIDER_SELECTION or the submit button in the ## {{T("UI Components")}} section as they are mandatory anyway.
|
||||
- Keep technical identifiers untranslated, such as TEXT_AREA, DROPDOWN, PROFILE_SELECTION, BuildPrompt, and plugin.lua.
|
||||
- Exception: Do not use technical identifiers in the "{{T("Inputs")}}" section, it should be easy comprehensible what the usual user input will be
|
||||
""";
|
||||
|
||||
@ -504,28 +570,71 @@ public partial class AssistantBuilder : AssistantBaseCore<NoSettingsPanel>
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
private async Task InstallPluginAsync()
|
||||
private async Task CheckGeneratedAssistantAsync()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(this.generatedLuaAssistant))
|
||||
{
|
||||
this.Snackbar.Add(T("No assistant plugin was generated yet."), Severity.Warning);
|
||||
await this.MessageBus.SendError(new(Icons.Material.Filled.Extension, T("No assistant plugin was generated yet.")));
|
||||
return;
|
||||
}
|
||||
|
||||
this.ResetInstallFlow();
|
||||
this.stepperIndex = (int)BuilderInstallStep.CHECK_PLUGIN;
|
||||
this.isCheckingPlugin = true;
|
||||
try
|
||||
{
|
||||
var result = await this.AssistantPluginInstallService.CheckInstallabilityAsync(this.generatedLuaAssistant, CancellationToken.None);
|
||||
this.pluginCheckResult = result;
|
||||
if (!result.Success)
|
||||
{
|
||||
this.FailInstallStep(BuilderInstallStep.CHECK_PLUGIN, result.Issue);
|
||||
await this.MessageBus.SendError(new(Icons.Material.Filled.ReportProblem, T("The generated assistant could not be checked.")));
|
||||
return;
|
||||
}
|
||||
|
||||
await this.MessageBus.SendSuccess(new(Icons.Material.Filled.CheckCircle, T("The generated assistant can be installed.")));
|
||||
this.stepperIndex = (int)BuilderInstallStep.INSTALL_ASSISTANT;
|
||||
}
|
||||
finally
|
||||
{
|
||||
this.isCheckingPlugin = false;
|
||||
await this.InvokeAsync(this.StateHasChanged);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task InstallGeneratedAssistantAsync()
|
||||
{
|
||||
if (!this.PluginCheckCompleted)
|
||||
return;
|
||||
|
||||
this.ClearInstallStepIssue();
|
||||
this.stepperIndex = (int)BuilderInstallStep.INSTALL_ASSISTANT;
|
||||
this.isInstallingPlugin = true;
|
||||
try
|
||||
{
|
||||
var result = await this.AssistantPluginInstallService.InstallAsync(this.generatedLuaAssistant, CancellationToken.None);
|
||||
this.pluginInstallResult = result;
|
||||
if (!result.Success)
|
||||
{
|
||||
this.Snackbar.Add(result.Issue, Severity.Error);
|
||||
this.FailInstallStep(BuilderInstallStep.INSTALL_ASSISTANT, result.Issue);
|
||||
await this.MessageBus.SendError(new(Icons.Material.Filled.ReportProblem, T("The assistant could not be installed.")));
|
||||
return;
|
||||
}
|
||||
|
||||
var message = result.ReplacedExisting
|
||||
? string.Format(T("The assistant plugin \"{0}\" was updated."), result.PluginName)
|
||||
: string.Format(T("The assistant plugin \"{0}\" was installed."), result.PluginName);
|
||||
this.Snackbar.Add(message, Severity.Success);
|
||||
this.installedAssistantPlugin = ResolveAssistantPlugin(result.PluginId);
|
||||
if (this.installedAssistantPlugin is null)
|
||||
{
|
||||
this.FailInstallStep(BuilderInstallStep.INSTALL_ASSISTANT, T("The installed assistant could not be loaded."));
|
||||
await this.MessageBus.SendError(new(Icons.Material.Filled.ReportProblem, T("The installed assistant could not be loaded.")));
|
||||
return;
|
||||
}
|
||||
|
||||
await this.MessageBus.SendSuccess(new(Icons.Material.Filled.Extension, result.ReplacedExisting ? T("Assistant updated.") : T("Assistant installed.")));
|
||||
this.stepperIndex = this.AuditRequiredForActivation
|
||||
? (int)BuilderInstallStep.SECURITY_CHECK
|
||||
: this.EnableCompleted
|
||||
? (int)BuilderInstallStep.OPEN_ASSISTANT
|
||||
: (int)BuilderInstallStep.ENABLE_ASSISTANT;
|
||||
}
|
||||
finally
|
||||
{
|
||||
@ -534,49 +643,146 @@ public partial class AssistantBuilder : AssistantBaseCore<NoSettingsPanel>
|
||||
}
|
||||
}
|
||||
|
||||
private static string CreateLuaCodeFence(string lua)
|
||||
private async Task RunSecurityCheckAsync()
|
||||
{
|
||||
var fenceLength = Math.Max(3, GetLongestBacktickRun(lua) + 1);
|
||||
var fence = new string('`', fenceLength);
|
||||
return $"""
|
||||
{fence}lua
|
||||
{lua.Trim()}
|
||||
{fence}
|
||||
""";
|
||||
}
|
||||
if (this.installedAssistantPlugin is null)
|
||||
return;
|
||||
|
||||
private static int GetLongestBacktickRun(string text)
|
||||
{
|
||||
var longestRun = 0;
|
||||
var currentRun = 0;
|
||||
|
||||
foreach (var character in text)
|
||||
this.ClearInstallStepIssue();
|
||||
this.stepperIndex = (int)BuilderInstallStep.SECURITY_CHECK;
|
||||
this.isAuditingPlugin = true;
|
||||
try
|
||||
{
|
||||
if (character is '`')
|
||||
this.pluginAudit = await this.AssistantPluginAuditService.RunAuditAsync(this.installedAssistantPlugin);
|
||||
if (this.pluginAudit.Level is AssistantAuditLevel.UNKNOWN)
|
||||
{
|
||||
currentRun++;
|
||||
longestRun = Math.Max(longestRun, currentRun);
|
||||
continue;
|
||||
this.FailInstallStep(BuilderInstallStep.SECURITY_CHECK, T("The security check could not determine a result."));
|
||||
await this.MessageBus.SendError(new(Icons.Material.Filled.GppMaybe, T("The security check could not be completed.")));
|
||||
return;
|
||||
}
|
||||
|
||||
currentRun = 0;
|
||||
}
|
||||
this.UpsertAudit(this.pluginAudit);
|
||||
await this.SettingsManager.StoreSettings();
|
||||
await this.MessageBus.SendSuccess(new(
|
||||
this.pluginAudit.Level.GetIcon(),
|
||||
this.pluginAudit.Findings.Count == 0
|
||||
? T("Security check completed. No security issues were found.")
|
||||
: T("Security check completed with findings.")));
|
||||
|
||||
return longestRun;
|
||||
if (this.IsActivationBlockedBySettings)
|
||||
{
|
||||
this.stepperIndex = (int)BuilderInstallStep.ENABLE_ASSISTANT;
|
||||
this.FailInstallStep(BuilderInstallStep.ENABLE_ASSISTANT, T("This assistant cannot be enabled because the security check is below your required level."));
|
||||
await this.MessageBus.SendError(new(Icons.Material.Filled.Block, T("The assistant cannot be enabled because it is below your required security level.")));
|
||||
return;
|
||||
}
|
||||
|
||||
this.stepperIndex = this.EnableCompleted
|
||||
? (int)BuilderInstallStep.OPEN_ASSISTANT
|
||||
: (int)BuilderInstallStep.ENABLE_ASSISTANT;
|
||||
}
|
||||
finally
|
||||
{
|
||||
this.isAuditingPlugin = false;
|
||||
await this.InvokeAsync(this.StateHasChanged);
|
||||
}
|
||||
}
|
||||
|
||||
private void AddGeneratedLuaPreviewResult()
|
||||
private async Task EnableInstalledAssistantAsync()
|
||||
{
|
||||
this.ChatThread?.Blocks.Add(new()
|
||||
if (this.pluginInstallResult is null || this.IsActivationBlockedBySettings)
|
||||
return;
|
||||
|
||||
if (this.RequiresActivationConfirmation && !await this.ConfirmActivationBelowMinimumAsync())
|
||||
return;
|
||||
|
||||
this.ClearInstallStepIssue();
|
||||
this.stepperIndex = (int)BuilderInstallStep.ENABLE_ASSISTANT;
|
||||
this.isEnablingPlugin = true;
|
||||
try
|
||||
{
|
||||
if (!this.SettingsManager.ConfigurationData.EnabledPlugins.Contains(this.pluginInstallResult.PluginId))
|
||||
this.SettingsManager.ConfigurationData.EnabledPlugins.Add(this.pluginInstallResult.PluginId);
|
||||
|
||||
await this.SettingsManager.StoreSettings();
|
||||
await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED);
|
||||
await this.MessageBus.SendSuccess(new(Icons.Material.Filled.ToggleOn, T("Assistant enabled.")));
|
||||
this.stepperIndex = (int)BuilderInstallStep.OPEN_ASSISTANT;
|
||||
}
|
||||
finally
|
||||
{
|
||||
this.isEnablingPlugin = false;
|
||||
await this.InvokeAsync(this.StateHasChanged);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<bool> ConfirmActivationBelowMinimumAsync()
|
||||
{
|
||||
var dialogParameters = new DialogParameters<ConfirmDialog>
|
||||
{
|
||||
Time = DateTimeOffset.Now,
|
||||
ContentType = ContentType.TEXT,
|
||||
Role = ChatRole.AI,
|
||||
Content = new ContentText
|
||||
{
|
||||
Text = CreateLuaCodeFence(this.generatedLuaAssistant),
|
||||
x => x.Message,
|
||||
string.Format(
|
||||
T("The assistant \"{0}\" was checked with the level \"{1}\", which is below your required level \"{2}\". Your settings allow activation anyway, but this may be unsafe. Do you want to enable this assistant?"),
|
||||
this.pluginInstallResult?.PluginName ?? T("Unknown assistant"),
|
||||
this.pluginAudit?.Level.GetName() ?? T("Unknown"),
|
||||
this.SettingsManager.ConfigurationData.AssistantPluginAudit.MinimumLevel.GetName())
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
var dialogReference = await this.DialogService.ShowAsync<ConfirmDialog>(T("Potentially Unsafe Assistant"), dialogParameters, DialogOptions.FULLSCREEN);
|
||||
var dialogResult = await dialogReference.Result;
|
||||
return dialogResult is not null && !dialogResult.Canceled;
|
||||
}
|
||||
|
||||
private void OpenInstalledAssistant()
|
||||
{
|
||||
if (this.pluginInstallResult is null)
|
||||
return;
|
||||
|
||||
this.NavigationManager.NavigateTo($"{Routes.ASSISTANT_DYNAMIC}?assistantId={this.pluginInstallResult.PluginId}");
|
||||
}
|
||||
|
||||
private static PluginAssistants? ResolveAssistantPlugin(Guid pluginId) => PluginFactory.RunningPlugins.OfType<PluginAssistants>().FirstOrDefault(plugin => plugin.Id == pluginId);
|
||||
|
||||
private void UpsertAudit(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);
|
||||
}
|
||||
|
||||
private void FailInstallStep(BuilderInstallStep installStep, string issue)
|
||||
{
|
||||
this.failedInstallStep = installStep;
|
||||
this.installFlowIssue = issue;
|
||||
this.stepperIndex = (int)installStep;
|
||||
}
|
||||
|
||||
private void ClearInstallStepIssue()
|
||||
{
|
||||
this.failedInstallStep = null;
|
||||
this.installFlowIssue = string.Empty;
|
||||
}
|
||||
|
||||
private bool IsInstallStepFailed(BuilderInstallStep installStep) => this.failedInstallStep == installStep;
|
||||
|
||||
private void ResetInstallFlow()
|
||||
{
|
||||
this.stepperIndex = (int)BuilderInstallStep.CHECK_PLUGIN;
|
||||
this.isCheckingPlugin = false;
|
||||
this.isInstallingPlugin = false;
|
||||
this.isAuditingPlugin = false;
|
||||
this.isEnablingPlugin = false;
|
||||
this.pluginCheckResult = null;
|
||||
this.pluginInstallResult = null;
|
||||
this.pluginAudit = null;
|
||||
this.installedAssistantPlugin = null;
|
||||
this.failedInstallStep = null;
|
||||
this.installFlowIssue = string.Empty;
|
||||
}
|
||||
|
||||
private async Task<string> LoadAssistantBuilderContextAsync()
|
||||
|
||||
@ -6,6 +6,7 @@ using AIStudio.Tools.PluginSystem.Assistants;
|
||||
namespace AIStudio.Tools.Services;
|
||||
|
||||
public sealed record AssistantPluginInstallResult(bool Success, Guid PluginId, string PluginName, string PluginDirectory, bool ReplacedExisting, string Issue);
|
||||
public sealed record AssistantPluginCheckResult(bool Success, Guid PluginId, string PluginName, string Issue);
|
||||
|
||||
public sealed class AssistantPluginInstallService
|
||||
{
|
||||
@ -17,6 +18,7 @@ public sealed class AssistantPluginInstallService
|
||||
private readonly SemaphoreSlim installSemaphore = new(1, 1);
|
||||
|
||||
private static AssistantPluginInstallResult Error(string issue) => new(false, Guid.Empty, string.Empty, string.Empty, false, issue);
|
||||
private static AssistantPluginCheckResult CheckError(string issue) => new(false, Guid.Empty, string.Empty, issue);
|
||||
|
||||
public AssistantPluginInstallService(ILogger<AssistantPluginInstallService> logger)
|
||||
{
|
||||
@ -24,6 +26,43 @@ public sealed class AssistantPluginInstallService
|
||||
this.logger.LogInformation("The assistant plugin install service has been initialized.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether generated Lua assistant plugin code can be loaded and installed.
|
||||
/// The plugin is written to a temporary staging directory and validated through the
|
||||
/// normal plugin loader, but it is not moved into the user plugin directory.
|
||||
/// </summary>
|
||||
/// <param name="lua">The full generated <c>plugin.lua</c> content.</param>
|
||||
/// <param name="token">A cancellation token for file IO and Lua validation.</param>
|
||||
/// <returns>
|
||||
/// Check result that contains success state, plugin metadata, and a user-facing issue when validation failed.
|
||||
/// </returns>
|
||||
public async Task<AssistantPluginCheckResult> CheckInstallabilityAsync(string lua, CancellationToken token)
|
||||
{
|
||||
if (!TryGetAssistantPluginsRoot(out var assistantPluginsRoot, out var rootIssue))
|
||||
return CheckError(rootIssue);
|
||||
|
||||
await this.installSemaphore.WaitAsync(token);
|
||||
var stagingDirectory = string.Empty;
|
||||
try
|
||||
{
|
||||
var validation = await this.ValidateIntoStagingAsync(lua, token);
|
||||
if (!validation.Success || validation.AssistantPlugin is null)
|
||||
return CheckError(validation.Issue);
|
||||
|
||||
stagingDirectory = validation.StagingDirectory;
|
||||
var finalDirectory = DetermineFinalDirectory(assistantPluginsRoot, validation.AssistantPlugin);
|
||||
if (!IsPathInsideDirectory(assistantPluginsRoot, finalDirectory))
|
||||
return CheckError("The resolved plugin directory is outside the assistant plugin directory.");
|
||||
|
||||
return new(true, validation.AssistantPlugin.Id, validation.AssistantPlugin.Name, string.Empty);
|
||||
}
|
||||
finally
|
||||
{
|
||||
this.TryDeleteStagingDirectory(stagingDirectory);
|
||||
this.installSemaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Installs generated Lua assistant plugin code into the user plugin directory.
|
||||
/// Writes the plugin into a temporary staging directory first, validates it through the
|
||||
@ -39,44 +78,27 @@ public sealed class AssistantPluginInstallService
|
||||
/// </returns>
|
||||
public async Task<AssistantPluginInstallResult> InstallAsync(string lua, CancellationToken token)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(lua))
|
||||
return Error("No Lua plugin code was generated.");
|
||||
|
||||
var pluginCode = lua.Trim();
|
||||
if (!PluginFactory.IsInitialized)
|
||||
return Error("The plugin system is not initialized yet.");
|
||||
|
||||
var dataDirectory = SettingsManager.DataDirectory;
|
||||
if (string.IsNullOrWhiteSpace(dataDirectory))
|
||||
return Error("The AI Studio data directory is not initialized yet.");
|
||||
if (!TryGetAssistantPluginsRoot(out var assistantPluginsRoot, out var rootIssue))
|
||||
return Error(rootIssue);
|
||||
|
||||
await this.installSemaphore.WaitAsync(token);
|
||||
AssistantPluginValidationResult validation;
|
||||
try
|
||||
{
|
||||
var assistantPluginsRoot = Path.Join(dataDirectory, "plugins", PluginType.ASSISTANT.GetDirectory());
|
||||
validation = await this.ValidateIntoStagingAsync(lua, token);
|
||||
if (!validation.Success || validation.AssistantPlugin is null)
|
||||
return Error(validation.Issue);
|
||||
|
||||
Directory.CreateDirectory(assistantPluginsRoot);
|
||||
|
||||
var stagingDirectory = Path.Join(Path.GetTempPath(), $"{ASSISTANT_BUILDER_DIRECTORY_PREFIX}.staging-{Guid.NewGuid():N}");
|
||||
var stagingDirectory = validation.StagingDirectory;
|
||||
var assistantPlugin = validation.AssistantPlugin;
|
||||
string? backupDirectory = null;
|
||||
string? finalDirectory = null;
|
||||
var replacedExisting = false;
|
||||
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(stagingDirectory);
|
||||
var stagedPluginFile = Path.Join(stagingDirectory, PLUGIN_FILE_NAME);
|
||||
await File.WriteAllTextAsync(stagedPluginFile, pluginCode, Encoding.UTF8, token);
|
||||
|
||||
var plugin = await PluginFactory.Load(stagingDirectory, pluginCode, token);
|
||||
if (plugin is not PluginAssistants assistantPlugin)
|
||||
return Error($"The generated plugin is not an assistant plugin. Issue: {string.Join("; ", plugin.Issues)}");
|
||||
|
||||
if (!assistantPlugin.IsValid)
|
||||
return Error($"The generated assistant plugin is invalid. Issue: {string.Join("; ", assistantPlugin.Issues)}");
|
||||
|
||||
if (PluginFactory.AvailablePlugins.Any(plugin => plugin.Type is PluginType.ASSISTANT && plugin.Id == assistantPlugin.Id && plugin.IsInternal))
|
||||
return Error("The generated assistant plugin uses the ID of an internal AI Studio plugin.");
|
||||
|
||||
finalDirectory = DetermineFinalDirectory(assistantPluginsRoot, assistantPlugin);
|
||||
if (!IsPathInsideDirectory(assistantPluginsRoot, finalDirectory))
|
||||
return Error("The resolved plugin directory is outside the assistant plugin directory.");
|
||||
@ -125,17 +147,7 @@ public sealed class AssistantPluginInstallService
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (Directory.Exists(stagingDirectory))
|
||||
{
|
||||
try
|
||||
{
|
||||
Directory.Delete(stagingDirectory, true);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
this.logger.LogError(e, "Failed to delete assistant plugin staging directory '{StagingDirectory}'.", stagingDirectory);
|
||||
}
|
||||
}
|
||||
this.TryDeleteStagingDirectory(stagingDirectory);
|
||||
}
|
||||
}
|
||||
finally
|
||||
@ -143,6 +155,83 @@ public sealed class AssistantPluginInstallService
|
||||
this.installSemaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<AssistantPluginValidationResult> ValidateIntoStagingAsync(string lua, CancellationToken token)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(lua))
|
||||
return AssistantPluginValidationResult.Error("No Lua plugin code was generated.");
|
||||
|
||||
if (!PluginFactory.IsInitialized)
|
||||
return AssistantPluginValidationResult.Error("The plugin system is not initialized yet.");
|
||||
|
||||
var pluginCode = lua.Trim();
|
||||
var stagingDirectory = Path.Join(Path.GetTempPath(), $"{ASSISTANT_BUILDER_DIRECTORY_PREFIX}.staging-{Guid.NewGuid():N}");
|
||||
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(stagingDirectory);
|
||||
var stagedPluginFile = Path.Join(stagingDirectory, PLUGIN_FILE_NAME);
|
||||
await File.WriteAllTextAsync(stagedPluginFile, pluginCode, Encoding.UTF8, token);
|
||||
|
||||
var plugin = await PluginFactory.Load(stagingDirectory, pluginCode, token);
|
||||
if (plugin is not PluginAssistants assistantPlugin)
|
||||
{
|
||||
this.TryDeleteStagingDirectory(stagingDirectory);
|
||||
return AssistantPluginValidationResult.Error($"The generated plugin is not an assistant plugin. Issue: {string.Join("; ", plugin.Issues)}");
|
||||
}
|
||||
|
||||
if (!assistantPlugin.IsValid)
|
||||
{
|
||||
this.TryDeleteStagingDirectory(stagingDirectory);
|
||||
return AssistantPluginValidationResult.Error($"The generated assistant plugin is invalid. Issue: {string.Join("; ", assistantPlugin.Issues)}");
|
||||
}
|
||||
|
||||
if (PluginFactory.AvailablePlugins.Any(plugin => plugin.Type is PluginType.ASSISTANT && plugin.Id == assistantPlugin.Id && plugin.IsInternal))
|
||||
{
|
||||
this.TryDeleteStagingDirectory(stagingDirectory);
|
||||
return AssistantPluginValidationResult.Error("The generated assistant plugin uses the ID of an internal AI Studio plugin.");
|
||||
}
|
||||
|
||||
return new(true, stagingDirectory, assistantPlugin, string.Empty);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
this.logger.LogError(e, "Failed to validate generated assistant plugin.");
|
||||
this.TryDeleteStagingDirectory(stagingDirectory);
|
||||
return AssistantPluginValidationResult.Error(e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryGetAssistantPluginsRoot(out string assistantPluginsRoot, out string issue)
|
||||
{
|
||||
assistantPluginsRoot = string.Empty;
|
||||
issue = string.Empty;
|
||||
|
||||
var dataDirectory = SettingsManager.DataDirectory;
|
||||
if (string.IsNullOrWhiteSpace(dataDirectory))
|
||||
{
|
||||
issue = "The AI Studio data directory is not initialized yet.";
|
||||
return false;
|
||||
}
|
||||
|
||||
assistantPluginsRoot = Path.Join(dataDirectory, "plugins", PluginType.ASSISTANT.GetDirectory());
|
||||
return true;
|
||||
}
|
||||
|
||||
private void TryDeleteStagingDirectory(string stagingDirectory)
|
||||
{
|
||||
if (!Directory.Exists(stagingDirectory))
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
Directory.Delete(stagingDirectory, true);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
this.logger.LogError(e, "Failed to delete assistant plugin staging directory '{StagingDirectory}'.", stagingDirectory);
|
||||
}
|
||||
}
|
||||
|
||||
private static string DetermineFinalDirectory(string assistantPluginsRoot, PluginAssistants assistantPlugin)
|
||||
{
|
||||
@ -206,4 +295,9 @@ public sealed class AssistantPluginInstallService
|
||||
var childPath = Path.GetFullPath(path).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) + Path.DirectorySeparatorChar;
|
||||
return childPath.StartsWith(parentPath, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private sealed record AssistantPluginValidationResult(bool Success, string StagingDirectory, PluginAssistants? AssistantPlugin, string Issue)
|
||||
{
|
||||
public static AssistantPluginValidationResult Error(string issue) => new(false, string.Empty, null, issue);
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user