mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-08-24 20:52: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)]
|
@attribute [Route(Routes.ASSISTANT_META_ASSISTANT)]
|
||||||
|
@using AIStudio.Agents.AssistantAudit
|
||||||
@using AIStudio.Tools.PluginSystem.Assistants.DataModel
|
@using AIStudio.Tools.PluginSystem.Assistants.DataModel
|
||||||
@inherits AssistantBaseCore<AIStudio.Dialogs.Settings.NoSettingsPanel>
|
@inherits AssistantBaseCore<AIStudio.Dialogs.Settings.NoSettingsPanel>
|
||||||
|
|
||||||
@ -38,7 +39,7 @@
|
|||||||
</MudExpansionPanels>
|
</MudExpansionPanels>
|
||||||
|
|
||||||
<ProviderSelection @bind-ProviderSettings="@this.ProviderSettings" ValidateProvider="@this.ValidatingProvider"/>
|
<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
|
else
|
||||||
{
|
{
|
||||||
@ -71,11 +72,188 @@ else
|
|||||||
</MudStack>
|
</MudStack>
|
||||||
|
|
||||||
<ProviderSelection @bind-ProviderSettings="@this.ProviderSettings" ValidateProvider="@this.ValidatingProvider"/>
|
<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 {
|
@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>
|
<MudCardContent>
|
||||||
<MudSkeleton />
|
<MudSkeleton />
|
||||||
<MudSkeleton Animation="Animation.False" />
|
<MudSkeleton Animation="Animation.False" />
|
||||||
|
|||||||
@ -4,9 +4,11 @@ using System.Reflection;
|
|||||||
// ReSharper restore RedundantUsingDirective
|
// ReSharper restore RedundantUsingDirective
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using AIStudio.Chat;
|
using AIStudio.Agents.AssistantAudit;
|
||||||
using AIStudio.Dialogs;
|
using AIStudio.Dialogs;
|
||||||
using AIStudio.Dialogs.Settings;
|
using AIStudio.Dialogs.Settings;
|
||||||
|
using AIStudio.Tools.PluginSystem;
|
||||||
|
using AIStudio.Tools.PluginSystem.Assistants;
|
||||||
using AIStudio.Tools.PluginSystem.Assistants.DataModel;
|
using AIStudio.Tools.PluginSystem.Assistants.DataModel;
|
||||||
using AIStudio.Tools.Services;
|
using AIStudio.Tools.Services;
|
||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
@ -22,6 +24,9 @@ public partial class AssistantBuilder : AssistantBaseCore<NoSettingsPanel>
|
|||||||
[Inject]
|
[Inject]
|
||||||
private AssistantPluginInstallService AssistantPluginInstallService { get; init; } = null!;
|
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 ILogger LOGGER = Program.LOGGER_FACTORY.CreateLogger(nameof(AssistantBuilder));
|
||||||
private static readonly JsonSerializerOptions UNTRUSTED_PROMPT_JSON_OPTIONS = new()
|
private static readonly JsonSerializerOptions UNTRUSTED_PROMPT_JSON_OPTIONS = new()
|
||||||
{
|
{
|
||||||
@ -63,15 +68,12 @@ public partial class AssistantBuilder : AssistantBaseCore<NoSettingsPanel>
|
|||||||
BuilderStep.DONE => this.GenerateLuaAssistant,
|
BuilderStep.DONE => this.GenerateLuaAssistant,
|
||||||
_ => this.GenerateAssistantSpec,
|
_ => this.GenerateAssistantSpec,
|
||||||
};
|
};
|
||||||
protected override bool SubmitDisabled => this.isAgentRunning || this.isInstallingPlugin;
|
protected override bool SubmitDisabled => this.isAgentRunning || this.IsInstallFlowRunning;
|
||||||
protected override bool ShowResult => this.step is BuilderStep.DONE;
|
protected override bool ShowResult => false;
|
||||||
protected override bool ShowEntireChatThread => this.step is BuilderStep.DONE;
|
protected override bool ShowEntireChatThread => false;
|
||||||
protected override bool AllowProfiles => false;
|
protected override bool AllowProfiles => false;
|
||||||
protected override bool ShowProfileSelection => false;
|
protected override bool ShowProfileSelection => false;
|
||||||
protected override bool ShowCopyResult => this.step is BuilderStep.DONE;
|
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 bool HasSettingsPanel => false;
|
||||||
protected override Func<string> Result2Copy => () => !string.IsNullOrWhiteSpace(this.generatedLuaAssistant)
|
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 BuilderStep step = BuilderStep.DESCRIBE;
|
||||||
private bool isAgentRunning;
|
private bool isAgentRunning;
|
||||||
|
private bool isCheckingPlugin;
|
||||||
private bool isInstallingPlugin;
|
private bool isInstallingPlugin;
|
||||||
|
private bool isAuditingPlugin;
|
||||||
|
private bool isEnablingPlugin;
|
||||||
private string assistantDescription = string.Empty;
|
private string assistantDescription = string.Empty;
|
||||||
private AssistantCategory selectedCategory;
|
private AssistantCategory selectedCategory;
|
||||||
private string customCategory = string.Empty;
|
private string customCategory = string.Empty;
|
||||||
@ -97,8 +102,14 @@ public partial class AssistantBuilder : AssistantBaseCore<NoSettingsPanel>
|
|||||||
private string reviewNotes = string.Empty;
|
private string reviewNotes = string.Empty;
|
||||||
private string generatedLuaAssistant = string.Empty;
|
private string generatedLuaAssistant = string.Empty;
|
||||||
private Guid pluginId = Guid.NewGuid();
|
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 =
|
private static readonly AssistantContextFile[] ASSISTANT_CONTEXT_FILES =
|
||||||
[
|
[
|
||||||
new("Assistant plugin schema", "Plugins/assistants/README.md", IsRequired: true),
|
new("Assistant plugin schema", "Plugins/assistants/README.md", IsRequired: true),
|
||||||
@ -113,6 +124,56 @@ public partial class AssistantBuilder : AssistantBaseCore<NoSettingsPanel>
|
|||||||
REVIEW_SPEC,
|
REVIEW_SPEC,
|
||||||
DONE,
|
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 =
|
private static readonly AssistantComponentType[] ASSISTANT_COMPONENT_OPTIONS =
|
||||||
[
|
[
|
||||||
AssistantComponentType.TEXT_AREA,
|
AssistantComponentType.TEXT_AREA,
|
||||||
@ -145,6 +206,7 @@ public partial class AssistantBuilder : AssistantBaseCore<NoSettingsPanel>
|
|||||||
this.generatedAssistantSpec = string.Empty;
|
this.generatedAssistantSpec = string.Empty;
|
||||||
this.reviewNotes = string.Empty;
|
this.reviewNotes = string.Empty;
|
||||||
this.generatedLuaAssistant = string.Empty;
|
this.generatedLuaAssistant = string.Empty;
|
||||||
|
this.ResetInstallFlow();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override bool MightPreselectValues() => false;
|
protected override bool MightPreselectValues() => false;
|
||||||
@ -240,8 +302,8 @@ public partial class AssistantBuilder : AssistantBaseCore<NoSettingsPanel>
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.ResetInstallFlow();
|
||||||
this.generatedLuaAssistant = parsedResponse.FullLua.Trim();
|
this.generatedLuaAssistant = parsedResponse.FullLua.Trim();
|
||||||
this.AddGeneratedLuaPreviewResult();
|
|
||||||
this.step = BuilderStep.DONE;
|
this.step = BuilderStep.DONE;
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
@ -254,12 +316,14 @@ public partial class AssistantBuilder : AssistantBaseCore<NoSettingsPanel>
|
|||||||
{
|
{
|
||||||
this.step = BuilderStep.DESCRIBE;
|
this.step = BuilderStep.DESCRIBE;
|
||||||
this.generatedLuaAssistant = string.Empty;
|
this.generatedLuaAssistant = string.Empty;
|
||||||
|
this.ResetInstallFlow();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void BackToSpecReview()
|
private void BackToSpecReview()
|
||||||
{
|
{
|
||||||
this.step = BuilderStep.REVIEW_SPEC;
|
this.step = BuilderStep.REVIEW_SPEC;
|
||||||
this.generatedLuaAssistant = string.Empty;
|
this.generatedLuaAssistant = string.Empty;
|
||||||
|
this.ResetInstallFlow();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task EditDraftAndDiscardPluginPreview()
|
private async Task EditDraftAndDiscardPluginPreview()
|
||||||
@ -291,6 +355,7 @@ public partial class AssistantBuilder : AssistantBaseCore<NoSettingsPanel>
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
this.generatedLuaAssistant = string.Empty;
|
this.generatedLuaAssistant = string.Empty;
|
||||||
|
this.ResetInstallFlow();
|
||||||
this.step = BuilderStep.REVIEW_SPEC;
|
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.
|
- The future Lua plugin must be loadable by AI Studio.
|
||||||
- Include assumptions instead of asking follow-up questions.
|
- Include assumptions instead of asking follow-up questions.
|
||||||
- Treat filled optional guidance as explicit user intent.
|
- 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
|
- 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;
|
return string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task InstallPluginAsync()
|
private async Task CheckGeneratedAssistantAsync()
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(this.generatedLuaAssistant))
|
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;
|
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;
|
this.isInstallingPlugin = true;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var result = await this.AssistantPluginInstallService.InstallAsync(this.generatedLuaAssistant, CancellationToken.None);
|
var result = await this.AssistantPluginInstallService.InstallAsync(this.generatedLuaAssistant, CancellationToken.None);
|
||||||
|
this.pluginInstallResult = result;
|
||||||
if (!result.Success)
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var message = result.ReplacedExisting
|
this.installedAssistantPlugin = ResolveAssistantPlugin(result.PluginId);
|
||||||
? string.Format(T("The assistant plugin \"{0}\" was updated."), result.PluginName)
|
if (this.installedAssistantPlugin is null)
|
||||||
: string.Format(T("The assistant plugin \"{0}\" was installed."), result.PluginName);
|
{
|
||||||
this.Snackbar.Add(message, Severity.Success);
|
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
|
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);
|
if (this.installedAssistantPlugin is null)
|
||||||
var fence = new string('`', fenceLength);
|
return;
|
||||||
return $"""
|
|
||||||
{fence}lua
|
|
||||||
{lua.Trim()}
|
|
||||||
{fence}
|
|
||||||
""";
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int GetLongestBacktickRun(string text)
|
this.ClearInstallStepIssue();
|
||||||
{
|
this.stepperIndex = (int)BuilderInstallStep.SECURITY_CHECK;
|
||||||
var longestRun = 0;
|
this.isAuditingPlugin = true;
|
||||||
var currentRun = 0;
|
try
|
||||||
|
|
||||||
foreach (var character in text)
|
|
||||||
{
|
{
|
||||||
if (character is '`')
|
this.pluginAudit = await this.AssistantPluginAuditService.RunAuditAsync(this.installedAssistantPlugin);
|
||||||
|
if (this.pluginAudit.Level is AssistantAuditLevel.UNKNOWN)
|
||||||
{
|
{
|
||||||
currentRun++;
|
this.FailInstallStep(BuilderInstallStep.SECURITY_CHECK, T("The security check could not determine a result."));
|
||||||
longestRun = Math.Max(longestRun, currentRun);
|
await this.MessageBus.SendError(new(Icons.Material.Filled.GppMaybe, T("The security check could not be completed.")));
|
||||||
continue;
|
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()
|
private async Task<string> LoadAssistantBuilderContextAsync()
|
||||||
|
|||||||
@ -6,6 +6,7 @@ using AIStudio.Tools.PluginSystem.Assistants;
|
|||||||
namespace AIStudio.Tools.Services;
|
namespace AIStudio.Tools.Services;
|
||||||
|
|
||||||
public sealed record AssistantPluginInstallResult(bool Success, Guid PluginId, string PluginName, string PluginDirectory, bool ReplacedExisting, string Issue);
|
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
|
public sealed class AssistantPluginInstallService
|
||||||
{
|
{
|
||||||
@ -17,6 +18,7 @@ public sealed class AssistantPluginInstallService
|
|||||||
private readonly SemaphoreSlim installSemaphore = new(1, 1);
|
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 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)
|
public AssistantPluginInstallService(ILogger<AssistantPluginInstallService> logger)
|
||||||
{
|
{
|
||||||
@ -24,6 +26,43 @@ public sealed class AssistantPluginInstallService
|
|||||||
this.logger.LogInformation("The assistant plugin install service has been initialized.");
|
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>
|
/// <summary>
|
||||||
/// Installs generated Lua assistant plugin code into the user plugin directory.
|
/// Installs generated Lua assistant plugin code into the user plugin directory.
|
||||||
/// Writes the plugin into a temporary staging directory first, validates it through the
|
/// Writes the plugin into a temporary staging directory first, validates it through the
|
||||||
@ -39,44 +78,27 @@ public sealed class AssistantPluginInstallService
|
|||||||
/// </returns>
|
/// </returns>
|
||||||
public async Task<AssistantPluginInstallResult> InstallAsync(string lua, CancellationToken token)
|
public async Task<AssistantPluginInstallResult> InstallAsync(string lua, CancellationToken token)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(lua))
|
if (!TryGetAssistantPluginsRoot(out var assistantPluginsRoot, out var rootIssue))
|
||||||
return Error("No Lua plugin code was generated.");
|
return Error(rootIssue);
|
||||||
|
|
||||||
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.");
|
|
||||||
|
|
||||||
await this.installSemaphore.WaitAsync(token);
|
await this.installSemaphore.WaitAsync(token);
|
||||||
|
AssistantPluginValidationResult validation;
|
||||||
try
|
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);
|
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? backupDirectory = null;
|
||||||
string? finalDirectory = null;
|
string? finalDirectory = null;
|
||||||
var replacedExisting = false;
|
var replacedExisting = false;
|
||||||
|
|
||||||
try
|
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);
|
finalDirectory = DetermineFinalDirectory(assistantPluginsRoot, assistantPlugin);
|
||||||
if (!IsPathInsideDirectory(assistantPluginsRoot, finalDirectory))
|
if (!IsPathInsideDirectory(assistantPluginsRoot, finalDirectory))
|
||||||
return Error("The resolved plugin directory is outside the assistant plugin directory.");
|
return Error("The resolved plugin directory is outside the assistant plugin directory.");
|
||||||
@ -125,17 +147,7 @@ public sealed class AssistantPluginInstallService
|
|||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
if (Directory.Exists(stagingDirectory))
|
this.TryDeleteStagingDirectory(stagingDirectory);
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Directory.Delete(stagingDirectory, true);
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
this.logger.LogError(e, "Failed to delete assistant plugin staging directory '{StagingDirectory}'.", stagingDirectory);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
@ -144,6 +156,83 @@ public sealed class AssistantPluginInstallService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
private static string DetermineFinalDirectory(string assistantPluginsRoot, PluginAssistants assistantPlugin)
|
||||||
{
|
{
|
||||||
var existingPlugin = PluginFactory.AvailablePlugins
|
var existingPlugin = PluginFactory.AvailablePlugins
|
||||||
@ -206,4 +295,9 @@ public sealed class AssistantPluginInstallService
|
|||||||
var childPath = Path.GetFullPath(path).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) + Path.DirectorySeparatorChar;
|
var childPath = Path.GetFullPath(path).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) + Path.DirectorySeparatorChar;
|
||||||
return childPath.StartsWith(parentPath, StringComparison.OrdinalIgnoreCase);
|
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