mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-11-04 18:07:21 +00:00
Improved the security of the connection between the app window and AI Studio (#1146)
This commit is contained in:
1 parent
64dc621a7d
commit
faae2951f7
26 files changed
+1056
-88
No files matched your search
@@ -116,8 +116,8 @@ else
|
||||
</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
<MudButton Variant="Variant.Outlined" Size="Size.Small" Disabled="@this.IsSpecificationDownloadDisabled" Href="@this.selectedERIVersion.SpecificationURL()" Target="_blank">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Link" Class="mr-2"/> @T("Download specification")
|
||||
<MudButton Variant="Variant.Outlined" Size="Size.Small" Disabled="@this.IsSpecificationSavingDisabled" OnClick="@this.SaveSpecificationAsync">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Save" Class="mr-2"/> @T("Save specification")
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ using AIStudio.Dialogs;
|
||||
using AIStudio.Dialogs.Settings;
|
||||
using AIStudio.Settings.DataModel;
|
||||
using AIStudio.Tools.AssistantSessions;
|
||||
using AIStudio.Tools.Rust;
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
@@ -13,7 +14,7 @@ namespace AIStudio.Assistants.ERI;
|
||||
public partial class AssistantERI : AssistantBaseCore<SettingsDialogERIServer>
|
||||
{
|
||||
[Inject]
|
||||
private HttpClient HttpClient { get; set; } = null!;
|
||||
private WebRootFiles WebRootFiles { get; set; } = null!;
|
||||
|
||||
[Inject]
|
||||
private IDialogService DialogService { get; init; } = null!;
|
||||
@@ -375,9 +376,43 @@ public partial class AssistantERI : AssistantBaseCore<SettingsDialogERIServer>
|
||||
private bool IsERIInputDisabled => this.IsNoneERIServerSelected || this.IsProcessing;
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether the selected ERI specification cannot be downloaded.
|
||||
/// Gets whether the selected ERI specification cannot be saved.
|
||||
/// </summary>
|
||||
private bool IsSpecificationDownloadDisabled => !this.selectedERIVersion.WasSpecificationSelected() || this.IsERIInputDisabled;
|
||||
private bool IsSpecificationSavingDisabled => !this.selectedERIVersion.WasSpecificationSelected() || this.IsERIInputDisabled;
|
||||
|
||||
/// <summary>
|
||||
/// Lets the user save the selected ERI specification, a copy of the one the app carries.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The app server answers the app window only, so a link to the file would fail in the browser
|
||||
/// of the system. The copy also matches the installed version of AI Studio.
|
||||
/// </remarks>
|
||||
private async Task SaveSpecificationAsync()
|
||||
{
|
||||
var specificationPath = this.selectedERIVersion.SpecificationURL();
|
||||
var response = await this.RustService.SaveFile(T("Save the ERI specification"), [FileTypes.JSON], Path.GetFileName(specificationPath));
|
||||
if (response.UserCancelled)
|
||||
return;
|
||||
|
||||
await using var feedback = new FileExportFeedback();
|
||||
feedback.ShowRunning(response.SaveFilePath);
|
||||
try
|
||||
{
|
||||
if (await this.WebRootFiles.CopyToAsync(specificationPath, response.SaveFilePath))
|
||||
{
|
||||
await feedback.SucceededAsync(Icons.Material.Filled.CheckCircle, T("The ERI specification was saved."), response.SaveFilePath, true);
|
||||
return;
|
||||
}
|
||||
|
||||
this.Logger.LogError("The app carries no ERI specification at '{SpecificationPath}'.", specificationPath);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
this.Logger.LogError(exception, "Failed to save the ERI specification {ERIVersion}.", this.selectedERIVersion);
|
||||
}
|
||||
|
||||
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Cancel, T("The ERI specification could not be saved.")));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether the generated-code target directory selection should be disabled.
|
||||
@@ -843,7 +878,7 @@ public partial class AssistantERI : AssistantBaseCore<SettingsDialogERIServer>
|
||||
var baseDirectorySnapshot = this.baseDirectory;
|
||||
var previouslyGeneratedFilesSnapshot = this.previouslyGeneratedFiles.ToArray();
|
||||
|
||||
this.eriSpecification = await this.selectedERIVersion.ReadSpecification(this.HttpClient);
|
||||
this.eriSpecification = await this.selectedERIVersion.ReadSpecification(this.WebRootFiles);
|
||||
if (string.IsNullOrWhiteSpace(this.eriSpecification))
|
||||
{
|
||||
this.AddInputIssue(T("The ERI specification could not be loaded. Please try again later."));
|
||||
|
||||
@@ -2,26 +2,14 @@ namespace AIStudio.Assistants.ERI;
|
||||
|
||||
public static class ERIVersionExtensions
|
||||
{
|
||||
public static async Task<string> ReadSpecification(this ERIVersion version, HttpClient httpClient)
|
||||
{
|
||||
try
|
||||
{
|
||||
var url = version.SpecificationURL();
|
||||
using var response = await httpClient.GetAsync(url);
|
||||
return await response.Content.ReadAsStringAsync();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
public static Task<string> ReadSpecification(this ERIVersion version, WebRootFiles webRootFiles) => webRootFiles.ReadTextAsync(version.SpecificationURL());
|
||||
|
||||
public static string SpecificationURL(this ERIVersion version)
|
||||
{
|
||||
var nameLower = version.ToString().ToLowerInvariant();
|
||||
var filename = $"{nameLower}.json";
|
||||
return $"specs/eri/{filename}";
|
||||
}
|
||||
|
||||
|
||||
public static bool WasSpecificationSelected(this ERIVersion version) => version != ERIVersion.NONE;
|
||||
}
|
||||
@@ -1537,12 +1537,21 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::ASSISTANTERI::T2205711729"] = "Opera
|
||||
-- You can integrate additional libraries. Perhaps you want to evaluate the prompts in advance using a machine learning method or analyze them with a text mining approach? Or maybe you want to preprocess images in the prompts? For such advanced scenarios, you can specify which libraries you want to use here. It's best to describe which library you want to integrate for which purpose. This way, the LLM that writes the ERI server for you can try to use these libraries effectively. This should result in less rework being necessary. If you don't know the necessary libraries, you can instead attempt to describe the intended use. The LLM can then attempt to choose suitable libraries. However, hallucinations can occur, and fictional libraries might be selected.
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::ASSISTANTERI::T2241311898"] = "You can integrate additional libraries. Perhaps you want to evaluate the prompts in advance using a machine learning method or analyze them with a text mining approach? Or maybe you want to preprocess images in the prompts? For such advanced scenarios, you can specify which libraries you want to use here. It's best to describe which library you want to integrate for which purpose. This way, the LLM that writes the ERI server for you can try to use these libraries effectively. This should result in less rework being necessary. If you don't know the necessary libraries, you can instead attempt to describe the intended use. The LLM can then attempt to choose suitable libraries. However, hallucinations can occur, and fictional libraries might be selected."
|
||||
|
||||
-- The ERI specification was saved.
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::ASSISTANTERI::T2258857117"] = "The ERI specification was saved."
|
||||
|
||||
-- Save the ERI specification
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::ASSISTANTERI::T2260814268"] = "Save the ERI specification"
|
||||
|
||||
-- The ERI specification could not be loaded. Please try again later.
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::ASSISTANTERI::T2265640084"] = "The ERI specification could not be loaded. Please try again later."
|
||||
|
||||
-- You can imagine it like this: Hypothetically, when Wikipedia implemented the ERI, it would vectorize all pages using an embedding method. All of Wikipedia’s data would remain with Wikipedia, including the vector database (decentralized approach). Then, any AI Studio user could add Wikipedia as a data source to significantly reduce the hallucination of the LLM in knowledge questions.
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::ASSISTANTERI::T2299793803"] = "You can imagine it like this: Hypothetically, when Wikipedia implemented the ERI, it would vectorize all pages using an embedding method. All of Wikipedia’s data would remain with Wikipedia, including the vector database (decentralized approach). Then, any AI Studio user could add Wikipedia as a data source to significantly reduce the hallucination of the LLM in knowledge questions."
|
||||
|
||||
-- The ERI specification could not be saved.
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::ASSISTANTERI::T2361742875"] = "The ERI specification could not be saved."
|
||||
|
||||
-- Are you sure you want to delete the ERI server preset '{0}'?
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::ASSISTANTERI::T2417944396"] = "Are you sure you want to delete the ERI server preset '{0}'?"
|
||||
|
||||
@@ -1672,9 +1681,6 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::ASSISTANTERI::T3709185824"] = "Yes,
|
||||
-- Common ERI server settings
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::ASSISTANTERI::T371718034"] = "Common ERI server settings"
|
||||
|
||||
-- Download specification
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::ASSISTANTERI::T3770339156"] = "Download specification"
|
||||
|
||||
-- Are you sure you want to delete the embedding '{0}'?
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::ASSISTANTERI::T3792084967"] = "Are you sure you want to delete the embedding '{0}'?"
|
||||
|
||||
@@ -1741,6 +1747,9 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::ASSISTANTERI::T746826190"] = "ERI se
|
||||
-- Are you sure you want to delete the retrieval process '{0}'?
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::ASSISTANTERI::T748762927"] = "Are you sure you want to delete the retrieval process '{0}'?"
|
||||
|
||||
-- Save specification
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::ASSISTANTERI::T756799053"] = "Save specification"
|
||||
|
||||
-- Data source: hostname
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::ASSISTANTERI::T761552889"] = "Data source: hostname"
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ namespace AIStudio.Components;
|
||||
public partial class Changelog : MSGComponentBase
|
||||
{
|
||||
[Inject]
|
||||
private HttpClient HttpClient { get; set; } = null!;
|
||||
private WebRootFiles WebRootFiles { get; set; } = null!;
|
||||
|
||||
#region Overrides of ComponentBase
|
||||
|
||||
@@ -23,7 +23,6 @@ public partial class Changelog : MSGComponentBase
|
||||
|
||||
private async Task ReadLogAsync()
|
||||
{
|
||||
using var response = await this.HttpClient.GetAsync($"changelog/{this.SelectedLog.Filename}");
|
||||
this.LogContent = await response.Content.ReadAsStringAsync();
|
||||
this.LogContent = await this.WebRootFiles.ReadTextAsync($"changelog/{this.SelectedLog.Filename}");
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@ namespace AIStudio.Pages;
|
||||
public partial class Home : MSGComponentBase
|
||||
{
|
||||
[Inject]
|
||||
private HttpClient HttpClient { get; init; } = null!;
|
||||
private WebRootFiles WebRootFiles { get; init; } = null!;
|
||||
|
||||
[Inject]
|
||||
private NavigationManager NavigationManager { get; init; } = null!;
|
||||
@@ -222,8 +222,7 @@ public partial class Home : MSGComponentBase
|
||||
private async Task ReadLastChangeAsync()
|
||||
{
|
||||
var latest = Changelog.LOGS.MaxBy(n => n.Build);
|
||||
using var response = await this.HttpClient.GetAsync($"changelog/{latest.Filename}");
|
||||
this.LastChangeContent = await response.Content.ReadAsStringAsync();
|
||||
this.LastChangeContent = await this.WebRootFiles.ReadTextAsync($"changelog/{latest.Filename}");
|
||||
|
||||
await this.InvokeAsync(this.StateHasChanged);
|
||||
}
|
||||
|
||||
+12
-3
@@ -1539,12 +1539,21 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::ASSISTANTERI::T2205711729"] = "Betri
|
||||
-- You can integrate additional libraries. Perhaps you want to evaluate the prompts in advance using a machine learning method or analyze them with a text mining approach? Or maybe you want to preprocess images in the prompts? For such advanced scenarios, you can specify which libraries you want to use here. It's best to describe which library you want to integrate for which purpose. This way, the LLM that writes the ERI server for you can try to use these libraries effectively. This should result in less rework being necessary. If you don't know the necessary libraries, you can instead attempt to describe the intended use. The LLM can then attempt to choose suitable libraries. However, hallucinations can occur, and fictional libraries might be selected.
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::ASSISTANTERI::T2241311898"] = "Sie können zusätzliche Bibliotheken integrieren. Vielleicht möchten Sie die Prompts im Voraus mit einer Machine-Learning-Methode auswerten oder sie mithilfe eines Text-Mining-Ansatzes analysieren? Oder möchten Sie vielleicht Bilder in den Prompts vorverarbeiten? Für solche erweiterten Szenarien können Sie hier angeben, welche Bibliotheken Sie verwenden möchten. Beschreiben Sie am besten, welche Bibliothek Sie für welchen Zweck einbinden möchten. So kann das LLM, das den ERI-Server für Sie erstellt, versuchen, diese Bibliotheken effektiv zu nutzen. Dadurch sollte weniger Nacharbeit nötig sein. Falls Sie die benötigten Bibliotheken nicht kennen, können Sie stattdessen versuchen, den geplanten Einsatzzweck zu beschreiben. Das LLM kann dann versuchen, passende Bibliotheken auszuwählen. Allerdings können dabei Halluzinationen auftreten und fiktive Bibliotheken gewählt werden."
|
||||
|
||||
-- The ERI specification was saved.
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::ASSISTANTERI::T2258857117"] = "Die ERI-Spezifikation wurde gespeichert."
|
||||
|
||||
-- Save the ERI specification
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::ASSISTANTERI::T2260814268"] = "ERI-Spezifikation speichern"
|
||||
|
||||
-- The ERI specification could not be loaded. Please try again later.
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::ASSISTANTERI::T2265640084"] = "Die ERI-Spezifikation konnte nicht geladen werden. Bitte versuchen Sie es später erneut."
|
||||
|
||||
-- You can imagine it like this: Hypothetically, when Wikipedia implemented the ERI, it would vectorize all pages using an embedding method. All of Wikipedia’s data would remain with Wikipedia, including the vector database (decentralized approach). Then, any AI Studio user could add Wikipedia as a data source to significantly reduce the hallucination of the LLM in knowledge questions.
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::ASSISTANTERI::T2299793803"] = "Sie können es sich so vorstellen: Hypothetisch gesehen würde die Wikipedia beim Einsatz von ERI alle Seiten mithilfe einer Einbettungsmethode „vektorisieren“. Alle Daten von Wikipedia, einschließlich der Vektordatenbank, würden bei der Wikipedia bleiben (dezentraler Ansatz). Danach könnte jeder Nutzer von MindWork AI Studio Wikipedia als Datenquelle hinzufügen, um Halluzinationen des LLM bei Wissensfragen deutlich zu reduzieren."
|
||||
|
||||
-- The ERI specification could not be saved.
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::ASSISTANTERI::T2361742875"] = "Die ERI-Spezifikation konnte nicht gespeichert werden."
|
||||
|
||||
-- Are you sure you want to delete the ERI server preset '{0}'?
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::ASSISTANTERI::T2417944396"] = "Sind Sie sicher, dass Sie die ERI-Server-Voreinstellung „{0}“ löschen möchten?"
|
||||
|
||||
@@ -1674,9 +1683,6 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::ASSISTANTERI::T3709185824"] = "Ja, b
|
||||
-- Common ERI server settings
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::ASSISTANTERI::T371718034"] = "Allgemeine ERI-Server-Einstellungen"
|
||||
|
||||
-- Download specification
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::ASSISTANTERI::T3770339156"] = "Spezifikation herunterladen"
|
||||
|
||||
-- Are you sure you want to delete the embedding '{0}'?
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::ASSISTANTERI::T3792084967"] = "Möchten Sie die Einbettung „{0}“ wirklich löschen?"
|
||||
|
||||
@@ -1743,6 +1749,9 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::ASSISTANTERI::T746826190"] = "ERI-Se
|
||||
-- Are you sure you want to delete the retrieval process '{0}'?
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::ASSISTANTERI::T748762927"] = "Sind Sie sicher, dass Sie den Abrufprozess „{0}“ löschen möchten?"
|
||||
|
||||
-- Save specification
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::ASSISTANTERI::T756799053"] = "Spezifikation speichern"
|
||||
|
||||
-- Data source: hostname
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::ASSISTANTERI::T761552889"] = "Datenquelle: Hostname"
|
||||
|
||||
|
||||
+12
-3
@@ -1539,12 +1539,21 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::ASSISTANTERI::T2205711729"] = "Opera
|
||||
-- You can integrate additional libraries. Perhaps you want to evaluate the prompts in advance using a machine learning method or analyze them with a text mining approach? Or maybe you want to preprocess images in the prompts? For such advanced scenarios, you can specify which libraries you want to use here. It's best to describe which library you want to integrate for which purpose. This way, the LLM that writes the ERI server for you can try to use these libraries effectively. This should result in less rework being necessary. If you don't know the necessary libraries, you can instead attempt to describe the intended use. The LLM can then attempt to choose suitable libraries. However, hallucinations can occur, and fictional libraries might be selected.
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::ASSISTANTERI::T2241311898"] = "You can integrate additional libraries. Perhaps you want to evaluate the prompts in advance using a machine learning method or analyze them with a text mining approach? Or maybe you want to preprocess images in the prompts? For such advanced scenarios, you can specify which libraries you want to use here. It's best to describe which library you want to integrate for which purpose. This way, the LLM that writes the ERI server for you can try to use these libraries effectively. This should result in less rework being necessary. If you don't know the necessary libraries, you can instead attempt to describe the intended use. The LLM can then attempt to choose suitable libraries. However, hallucinations can occur, and fictional libraries might be selected."
|
||||
|
||||
-- The ERI specification was saved.
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::ASSISTANTERI::T2258857117"] = "The ERI specification was saved."
|
||||
|
||||
-- Save the ERI specification
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::ASSISTANTERI::T2260814268"] = "Save the ERI specification"
|
||||
|
||||
-- The ERI specification could not be loaded. Please try again later.
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::ASSISTANTERI::T2265640084"] = "The ERI specification could not be loaded. Please try again later."
|
||||
|
||||
-- You can imagine it like this: Hypothetically, when Wikipedia implemented the ERI, it would vectorize all pages using an embedding method. All of Wikipedia’s data would remain with Wikipedia, including the vector database (decentralized approach). Then, any AI Studio user could add Wikipedia as a data source to significantly reduce the hallucination of the LLM in knowledge questions.
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::ASSISTANTERI::T2299793803"] = "You can imagine it like this: Hypothetically, when Wikipedia implemented the ERI, it would vectorize all pages using an embedding method. All of Wikipedia’s data would remain with Wikipedia, including the vector database (decentralized approach). Then, any AI Studio user could add Wikipedia as a data source to significantly reduce the hallucination of the LLM in knowledge questions."
|
||||
|
||||
-- The ERI specification could not be saved.
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::ASSISTANTERI::T2361742875"] = "The ERI specification could not be saved."
|
||||
|
||||
-- Are you sure you want to delete the ERI server preset '{0}'?
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::ASSISTANTERI::T2417944396"] = "Are you sure you want to delete the ERI server preset '{0}'?"
|
||||
|
||||
@@ -1674,9 +1683,6 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::ASSISTANTERI::T3709185824"] = "Yes,
|
||||
-- Common ERI server settings
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::ASSISTANTERI::T371718034"] = "Common ERI server settings"
|
||||
|
||||
-- Download specification
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::ASSISTANTERI::T3770339156"] = "Download specification"
|
||||
|
||||
-- Are you sure you want to delete the embedding '{0}'?
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::ASSISTANTERI::T3792084967"] = "Are you sure you want to delete the embedding '{0}'?"
|
||||
|
||||
@@ -1743,6 +1749,9 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::ASSISTANTERI::T746826190"] = "ERI se
|
||||
-- Are you sure you want to delete the retrieval process '{0}'?
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::ASSISTANTERI::T748762927"] = "Are you sure you want to delete the retrieval process '{0}'?"
|
||||
|
||||
-- Save specification
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::ASSISTANTERI::T756799053"] = "Save specification"
|
||||
|
||||
-- Data source: hostname
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ERI::ASSISTANTERI::T761552889"] = "Data source: hostname"
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ using AIStudio.Tools.Web;
|
||||
|
||||
using Microsoft.AspNetCore.Components.Server.Circuits;
|
||||
using Microsoft.AspNetCore.DataProtection;
|
||||
using Microsoft.AspNetCore.HostFiltering;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core;
|
||||
using Microsoft.Extensions.Logging.Console;
|
||||
|
||||
@@ -32,11 +33,6 @@ using MudBlazor.Services;
|
||||
|
||||
using MudExtensions.Services;
|
||||
|
||||
#if !DEBUG
|
||||
using System.Reflection;
|
||||
using Microsoft.Extensions.FileProviders;
|
||||
#endif
|
||||
|
||||
namespace AIStudio;
|
||||
|
||||
internal sealed class Program
|
||||
@@ -96,7 +92,14 @@ internal sealed class Program
|
||||
}
|
||||
|
||||
API_TOKEN = apiToken;
|
||||
|
||||
|
||||
var uiToken = Environment.GetEnvironmentVariable(AppAccessPolicy.UI_TOKEN_ENVIRONMENT_VARIABLE);
|
||||
if(string.IsNullOrWhiteSpace(uiToken))
|
||||
{
|
||||
Console.WriteLine($"Error: The {AppAccessPolicy.UI_TOKEN_ENVIRONMENT_VARIABLE} environment variable is not set.");
|
||||
return;
|
||||
}
|
||||
|
||||
using var rust = new RustService(rustApiPort, certificateFingerprint);
|
||||
var appPort = await rust.GetAppPort();
|
||||
if(appPort == 0)
|
||||
@@ -283,12 +286,18 @@ internal sealed class Program
|
||||
options.KeepAliveInterval = TimeSpan.FromSeconds(30);
|
||||
});
|
||||
|
||||
builder.Services.AddSingleton(new HttpClient
|
||||
{
|
||||
BaseAddress = new Uri($"http://localhost:{appPort}")
|
||||
});
|
||||
// For absolute URLs only. The app reads its own files through WebRootFiles:
|
||||
builder.Services.AddSingleton(new HttpClient());
|
||||
builder.Services.AddSingleton<WebRootFiles>();
|
||||
|
||||
builder.WebHost.UseUrls($"http://localhost:{appPort}");
|
||||
//
|
||||
// We listen on the IPv4 loopback address only, where the app window goes. With localhost, the
|
||||
// server would listen on the IPv6 loopback address as well, and a client which resolves
|
||||
// localhost could end up with another process holding that port there.
|
||||
//
|
||||
builder.WebHost.UseUrls($"http://127.0.0.1:{appPort}");
|
||||
builder.Services.Configure<HostFilteringOptions>(AppAccessPolicy.ConfigureHostFiltering);
|
||||
builder.Services.AddSingleton(new AppAccessPolicy(appPort, uiToken));
|
||||
|
||||
#if DEBUG
|
||||
builder.WebHost.UseWebRoot("wwwroot");
|
||||
@@ -343,6 +352,13 @@ internal sealed class Program
|
||||
|
||||
DATABASE_CLIENT_PROVIDER = app.Services.GetRequiredService<DatabaseClientProvider>();
|
||||
|
||||
//
|
||||
// These two come first, so that no request reaches anything else without passing them. We add
|
||||
// the host filtering ourselves instead of relying on the defaults of ASP.NET Core to do so:
|
||||
//
|
||||
app.UseHostFiltering();
|
||||
app.UseMiddleware<AppAccessMiddleware>();
|
||||
|
||||
programLogger.LogInformation("Initialize internal file system.");
|
||||
app.Use(Redirect.HandlerContentAsync);
|
||||
app.Use(FileHandler.HandlerAsync);
|
||||
@@ -351,10 +367,9 @@ internal sealed class Program
|
||||
app.UseStaticFiles();
|
||||
app.UseDeveloperExceptionPage();
|
||||
#else
|
||||
var fileProvider = new ManifestEmbeddedFileProvider(Assembly.GetAssembly(type: typeof(Program))!, "wwwroot");
|
||||
app.UseStaticFiles(new StaticFileOptions
|
||||
{
|
||||
FileProvider = fileProvider,
|
||||
FileProvider = app.Services.GetRequiredService<WebRootFiles>().FileProvider,
|
||||
RequestPath = string.Empty,
|
||||
});
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
using Microsoft.Extensions.Primitives;
|
||||
|
||||
namespace AIStudio.Tools.Security;
|
||||
|
||||
/// <summary>
|
||||
/// Refuses every request to the app server which does not come from the app window.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The middleware runs before every other one, so it covers pages, static files, the Blazor hub
|
||||
/// with all of its transports, and every endpoint alike. A release build makes no exception: a
|
||||
/// request without the session cookie gets refused, whatever it asks for. Only a development build
|
||||
/// also takes its boot link, with which another browser gets the cookie.
|
||||
///
|
||||
/// A request which carries an Origin header must, in addition, come from a page of the app.
|
||||
/// Browsers send that header with requests made by a script, the WebSocket of the Blazor hub
|
||||
/// included, but not when they load a page. A sandboxed iframe sends "null", which gets refused as
|
||||
/// well.
|
||||
/// </remarks>
|
||||
/// <param name="next">The next middleware.</param>
|
||||
/// <param name="policy">Decides which requests may pass.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
public sealed class AppAccessMiddleware(RequestDelegate next, AppAccessPolicy policy, ILogger<AppAccessMiddleware> logger)
|
||||
{
|
||||
/// <summary>
|
||||
/// The shortest interval between two log entries about refused requests. Any process on this
|
||||
/// machine can send requests, and an entry for each of them would flood the log.
|
||||
/// </summary>
|
||||
private static readonly TimeSpan REFUSAL_LOG_INTERVAL = TimeSpan.FromMinutes(1);
|
||||
|
||||
/// <summary>
|
||||
/// What a refused request gets to read. The app window only shows it when it lost its cookie,
|
||||
/// and a new start gives it a new one.
|
||||
/// </summary>
|
||||
private const string REFUSAL_TEXT = "This window cannot access AI Studio. Please restart AI Studio.";
|
||||
|
||||
private readonly Lock refusalLock = new();
|
||||
private DateTimeOffset lastRefusalLog = DateTimeOffset.MinValue;
|
||||
private int refusalsSinceLastLog;
|
||||
|
||||
public async Task InvokeAsync(HttpContext context)
|
||||
{
|
||||
var origin = context.Request.Headers.Origin;
|
||||
if (!StringValues.IsNullOrEmpty(origin) && (origin.Count != 1 || !policy.IsAppOrigin(origin.ToString())))
|
||||
{
|
||||
await this.RefuseAsync(context, "another origin");
|
||||
return;
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
if (HttpMethods.IsGet(context.Request.Method) && context.Request.Path.Equals(DEV_BOOT_PATH, StringComparison.Ordinal))
|
||||
{
|
||||
await this.BootAsync(context);
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (!policy.HasSessionCookie(context.Request))
|
||||
{
|
||||
await this.RefuseAsync(context, "a client without the session cookie");
|
||||
return;
|
||||
}
|
||||
|
||||
await next(context);
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
/// <summary>
|
||||
/// The path of the boot link which development builds write for other browsers, such as the
|
||||
/// one of an agent. A release build knows neither the link nor this path.
|
||||
/// </summary>
|
||||
private const string DEV_BOOT_PATH = "/dev/boot";
|
||||
|
||||
/// <summary>
|
||||
/// Trades the token of a boot link for the session cookie and sends the browser on to the app.
|
||||
/// The browser opens the link by itself, so it sends the cookie along after the redirect.
|
||||
/// </summary>
|
||||
private async Task BootAsync(HttpContext context)
|
||||
{
|
||||
var token = context.Request.Query["token"].ToString();
|
||||
if (!policy.IsUIToken(token))
|
||||
{
|
||||
await this.RefuseAsync(context, "a boot link with a wrong token");
|
||||
return;
|
||||
}
|
||||
|
||||
context.Response.Cookies.Append(AppAccessPolicy.SESSION_COOKIE_NAME, token, new CookieOptions
|
||||
{
|
||||
HttpOnly = true,
|
||||
SameSite = SameSiteMode.Lax,
|
||||
Path = "/",
|
||||
IsEssential = true
|
||||
});
|
||||
|
||||
context.Response.Redirect("/");
|
||||
}
|
||||
#endif
|
||||
|
||||
private async Task RefuseAsync(HttpContext context, string reason)
|
||||
{
|
||||
this.LogRefusal(context, reason);
|
||||
context.Response.StatusCode = StatusCodes.Status403Forbidden;
|
||||
context.Response.ContentType = "text/plain; charset=utf-8";
|
||||
await context.Response.WriteAsync(REFUSAL_TEXT);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs a refused request, at most once per interval, together with the number of requests
|
||||
/// refused since the last entry. The entry names the path but never the query or any header.
|
||||
/// </summary>
|
||||
private void LogRefusal(HttpContext context, string reason)
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
int refusals;
|
||||
lock (this.refusalLock)
|
||||
{
|
||||
this.refusalsSinceLastLog++;
|
||||
if (now - this.lastRefusalLog < REFUSAL_LOG_INTERVAL)
|
||||
return;
|
||||
|
||||
refusals = this.refusalsSinceLastLog;
|
||||
this.refusalsSinceLastLog = 0;
|
||||
this.lastRefusalLog = now;
|
||||
}
|
||||
|
||||
logger.LogWarning("Refused {Refusals} request(s) to the app server since the last report. The latest one, '{Method} {Path}', came from {Reason}.", refusals, context.Request.Method, context.Request.Path.Value, reason);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
using Microsoft.AspNetCore.HostFiltering;
|
||||
|
||||
namespace AIStudio.Tools.Security;
|
||||
|
||||
/// <summary>
|
||||
/// Decides which requests may reach the app server.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The Rust runtime makes a new token for every start. It gives the token to this server through
|
||||
/// an environment variable, and to the app window as the value of the session cookie. Nothing else
|
||||
/// on this machine has the cookie, so a request with it comes from the app window.
|
||||
/// </remarks>
|
||||
/// <param name="appPort">The port the app server listens on.</param>
|
||||
/// <param name="uiToken">The token of this start, which the session cookie carries.</param>
|
||||
public sealed class AppAccessPolicy(int appPort, string uiToken)
|
||||
{
|
||||
/// <summary>
|
||||
/// The name of the session cookie. The Rust runtime sets the cookie under the same name.
|
||||
/// </summary>
|
||||
public const string SESSION_COOKIE_NAME = "ai_studio_session";
|
||||
|
||||
/// <summary>
|
||||
/// The environment variable which carries the token from the Rust runtime to this server.
|
||||
/// </summary>
|
||||
public const string UI_TOKEN_ENVIRONMENT_VARIABLE = "AI_STUDIO_UI_TOKEN";
|
||||
|
||||
/// <summary>
|
||||
/// The host names under which the app server answers. A request for any other name, such as one
|
||||
/// which a website points at the loopback address, gets refused.
|
||||
/// </summary>
|
||||
private static readonly string[] ALLOWED_HOSTS = ["127.0.0.1", "localhost", "[::1]"];
|
||||
|
||||
private readonly HashSet<string> allowedOrigins = ALLOWED_HOSTS.Select(host => $"http://{host}:{appPort}").ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
private readonly byte[] uiTokenBytes = Encoding.UTF8.GetBytes(uiToken);
|
||||
|
||||
/// <summary>
|
||||
/// Lets the host filtering of ASP.NET Core accept only the host names of the app server.
|
||||
/// </summary>
|
||||
/// <param name="options">The options of the host filtering.</param>
|
||||
public static void ConfigureHostFiltering(HostFilteringOptions options)
|
||||
{
|
||||
options.AllowedHosts = [..ALLOWED_HOSTS];
|
||||
options.AllowEmptyHosts = false;
|
||||
options.IncludeFailureMessage = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether the given origin is the app itself.
|
||||
/// </summary>
|
||||
/// <param name="origin">The value of the Origin header of a request.</param>
|
||||
/// <returns>True when a page of the app made the request.</returns>
|
||||
public bool IsAppOrigin(string origin) => this.allowedOrigins.Contains(origin);
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether the given request carries the session cookie of this start.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The comparison takes the same time no matter where a wrong value differs, so the time of a
|
||||
/// refusal tells nothing about how much of a guess was right.
|
||||
/// </remarks>
|
||||
/// <param name="request">The request.</param>
|
||||
/// <returns>True when the request comes from the app window.</returns>
|
||||
public bool HasSessionCookie(HttpRequest request) => request.Cookies.TryGetValue(SESSION_COOKIE_NAME, out var value) && this.IsUIToken(value);
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether the given value is the token of this start, in a time which does not depend
|
||||
/// on where a wrong value differs.
|
||||
/// </summary>
|
||||
/// <param name="value">The value to check.</param>
|
||||
/// <returns>True when the value is the token.</returns>
|
||||
public bool IsUIToken(string? value) => !string.IsNullOrEmpty(value) && CryptographicOperations.FixedTimeEquals(Encoding.UTF8.GetBytes(value), this.uiTokenBytes);
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
using System.Reflection;
|
||||
|
||||
using Microsoft.Extensions.FileProviders;
|
||||
|
||||
namespace AIStudio.Tools;
|
||||
|
||||
/// <summary>
|
||||
/// Reads the files below wwwroot, which every build embeds into the app.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The app reads files such as its changelogs directly instead of requesting them over HTTP from
|
||||
/// its own server, which answers the app window only. The release build serves its static files
|
||||
/// from the same provider.
|
||||
/// </remarks>
|
||||
public sealed class WebRootFiles
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the files below wwwroot.
|
||||
/// </summary>
|
||||
public IFileProvider FileProvider { get; } = new ManifestEmbeddedFileProvider(Assembly.GetAssembly(type: typeof(Program))!, "wwwroot");
|
||||
|
||||
/// <summary>
|
||||
/// Reads a text file below wwwroot.
|
||||
/// </summary>
|
||||
/// <param name="path">The path below wwwroot, such as <c>changelog/v26.10.1.md</c>.</param>
|
||||
/// <returns>The text of the file, or an empty string when there is no such file.</returns>
|
||||
public async Task<string> ReadTextAsync(string path)
|
||||
{
|
||||
var file = this.FileProvider.GetFileInfo(path);
|
||||
if (!file.Exists)
|
||||
return string.Empty;
|
||||
|
||||
await using var fileStream = file.CreateReadStream();
|
||||
using var reader = new StreamReader(fileStream);
|
||||
return await reader.ReadToEndAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copies a file below wwwroot to the given place, such as one the user chose to save it to.
|
||||
/// </summary>
|
||||
/// <param name="path">The path below wwwroot, such as <c>specs/eri/v1.json</c>.</param>
|
||||
/// <param name="destinationPath">Where to write the copy. An existing file there gets replaced.</param>
|
||||
/// <returns>True when the copy was written, false when there is no such file below wwwroot.</returns>
|
||||
public async Task<bool> CopyToAsync(string path, string destinationPath)
|
||||
{
|
||||
var file = this.FileProvider.GetFileInfo(path);
|
||||
if (!file.Exists)
|
||||
return false;
|
||||
|
||||
await using var source = file.CreateReadStream();
|
||||
await using var destination = File.Create(destinationPath);
|
||||
await source.CopyToAsync(destination);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
- Added a setting for the size of the interface, which you find in the app settings. You can choose a size for each of your screens, and AI Studio switches to it when you move its window to another screen. Thanks, Paul Koudelka (`PaulKoudelka`), for this contribution.
|
||||
- Improved the documentation for IT departments, which now lives in our [wiki](https://github.com/MindWorkAI/AI-Studio/wiki/Enterprise-IT). It is sorted by task, explains the setup on Windows, Linux, and macOS, and offers complete configurations for typical needs, such as using only local models. The link in the app settings leads there, and the wiki also holds the installation guide in English and German.
|
||||
- Improved the German translation throughout: we proofread all texts and corrected wrong labels as well as spelling and grammar mistakes. The texts now always address you formally and use the same terms everywhere.
|
||||
- Improved the security of the connection between the app window and AI Studio.
|
||||
- Changed the size of AI Studio on laptop screens: it now appears in the same size on every laptop, whatever the scaling of your operating system. If you prefer the previous size, choose "System scaling" for the interface size in the app settings. Contributed by Paul Koudelka.
|
||||
- Fixed AI Studio becoming unusable when an update failed after its download. The app now restarts with the version you have installed, so you can simply try the update again.
|
||||
- Fixed configuration plugins you installed yourself being able to change the tool settings of your organization, such as the minimum provider confidence of a tool. These settings now follow the same rules as all other settings, which also gives you back your own values once your organization removes its configuration.
|
||||
|
||||
Reference in new issue
Block a user