mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-10-04 16:09:40 +00:00
Improved enterprise config support (#708)
Build and Release / Read metadata (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,deb,updater, appimage,deb) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,deb,updater, appimage,deb) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Read metadata (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,deb,updater, appimage,deb) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,deb,updater, appimage,deb) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
This commit is contained in:
1 parent
6146446fa9
commit
3b5a025c25
5 files changed
+1043
-302
No files matched your search
@@ -25,13 +25,22 @@ public static partial class PluginFactory
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the enterprise encryption service by reading the encryption secret
|
||||
/// from the Windows Registry or environment variables.
|
||||
/// from the effective enterprise source.
|
||||
/// </summary>
|
||||
/// <param name="rustService">The Rust service to use for reading the encryption secret.</param>
|
||||
public static async Task InitializeEnterpriseEncryption(Services.RustService rustService)
|
||||
{
|
||||
LOG.LogInformation("Initializing enterprise encryption service...");
|
||||
var encryptionSecret = await rustService.EnterpriseEnvConfigEncryptionSecret();
|
||||
InitializeEnterpriseEncryption(encryptionSecret);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the enterprise encryption service using a prefetched secret value.
|
||||
/// </summary>
|
||||
/// <param name="encryptionSecret">The base64-encoded enterprise encryption secret.</param>
|
||||
public static void InitializeEnterpriseEncryption(string? encryptionSecret)
|
||||
{
|
||||
LOG.LogInformation("Initializing enterprise encryption service...");
|
||||
var enterpriseEncryptionLogger = Program.LOGGER_FACTORY.CreateLogger<EnterpriseEncryption>();
|
||||
EnterpriseEncryption = new EnterpriseEncryption(enterpriseEncryptionLogger, encryptionSecret);
|
||||
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
using AIStudio.Tools.PluginSystem;
|
||||
using AIStudio.Settings;
|
||||
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace AIStudio.Tools.Services;
|
||||
|
||||
@@ -7,8 +11,14 @@ public sealed class EnterpriseEnvironmentService(ILogger<EnterpriseEnvironmentSe
|
||||
public static List<EnterpriseEnvironment> CURRENT_ENVIRONMENTS = [];
|
||||
|
||||
public static bool HasValidEnterpriseSnapshot { get; private set; }
|
||||
|
||||
private static EnterpriseSecretSnapshot CURRENT_SECRET_SNAPSHOT;
|
||||
|
||||
private readonly record struct EnterpriseEnvironmentSnapshot(Guid ConfigurationId, string ConfigurationServerUrl, string? ETag);
|
||||
|
||||
private readonly record struct EnterpriseSecretSnapshot(bool HasSecret, string Fingerprint);
|
||||
|
||||
private readonly record struct EnterpriseSecretTarget(string SecretId, string SecretName, SecretStoreType StoreType) : ISecretId;
|
||||
|
||||
#if DEBUG
|
||||
private static readonly TimeSpan CHECK_INTERVAL = TimeSpan.FromMinutes(6);
|
||||
@@ -39,6 +49,7 @@ public sealed class EnterpriseEnvironmentService(ILogger<EnterpriseEnvironmentSe
|
||||
logger.LogInformation("Start updating of the enterprise environment.");
|
||||
HasValidEnterpriseSnapshot = false;
|
||||
var previousSnapshot = BuildNormalizedSnapshot(CURRENT_ENVIRONMENTS);
|
||||
var previousSecretSnapshot = CURRENT_SECRET_SNAPSHOT;
|
||||
|
||||
//
|
||||
// Step 1: Fetch all active configurations.
|
||||
@@ -55,6 +66,21 @@ public sealed class EnterpriseEnvironmentService(ILogger<EnterpriseEnvironmentSe
|
||||
return;
|
||||
}
|
||||
|
||||
string enterpriseEncryptionSecret;
|
||||
try
|
||||
{
|
||||
enterpriseEncryptionSecret = await rustService.EnterpriseEnvConfigEncryptionSecret();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.LogError(e, "Failed to fetch the enterprise encryption secret from the Rust service.");
|
||||
await MessageBus.INSTANCE.SendMessage(null, Event.RUST_SERVICE_UNAVAILABLE, "EnterpriseEnvConfigEncryptionSecret failed");
|
||||
return;
|
||||
}
|
||||
|
||||
var nextSecretSnapshot = await BuildSecretSnapshot(enterpriseEncryptionSecret);
|
||||
var wasSecretChanged = previousSecretSnapshot != nextSecretSnapshot;
|
||||
|
||||
//
|
||||
// Step 2: Determine ETags and build the list of reachable configurations.
|
||||
// IMPORTANT: when one config server fails, we continue with the others.
|
||||
@@ -169,10 +195,20 @@ public sealed class EnterpriseEnvironmentService(ILogger<EnterpriseEnvironmentSe
|
||||
logger.LogInformation("AI Studio runs without any enterprise configurations.");
|
||||
|
||||
var effectiveSnapshot = BuildNormalizedSnapshot(effectiveEnvironments);
|
||||
|
||||
if (PluginFactory.IsInitialized && wasSecretChanged)
|
||||
{
|
||||
logger.LogInformation("The enterprise encryption secret changed. Refreshing the enterprise encryption service and reloading plugins.");
|
||||
PluginFactory.InitializeEnterpriseEncryption(enterpriseEncryptionSecret);
|
||||
await this.RemoveEnterpriseManagedApiKeysAsync();
|
||||
await PluginFactory.LoadAll();
|
||||
}
|
||||
|
||||
CURRENT_ENVIRONMENTS = effectiveEnvironments;
|
||||
CURRENT_SECRET_SNAPSHOT = nextSecretSnapshot;
|
||||
HasValidEnterpriseSnapshot = true;
|
||||
|
||||
if (!previousSnapshot.SequenceEqual(effectiveSnapshot))
|
||||
if (!previousSnapshot.SequenceEqual(effectiveSnapshot) || wasSecretChanged)
|
||||
await MessageBus.INSTANCE.SendMessage<bool>(null, Event.ENTERPRISE_ENVIRONMENTS_CHANGED);
|
||||
}
|
||||
catch (Exception e)
|
||||
@@ -193,8 +229,81 @@ public sealed class EnterpriseEnvironmentService(ILogger<EnterpriseEnvironmentSe
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static async Task<EnterpriseSecretSnapshot> BuildSecretSnapshot(string secret)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(secret))
|
||||
return new EnterpriseSecretSnapshot(false, string.Empty);
|
||||
|
||||
return new EnterpriseSecretSnapshot(true, await ComputeSecretFingerprint(secret));
|
||||
}
|
||||
|
||||
private static async Task<string> ComputeSecretFingerprint(string secret)
|
||||
{
|
||||
using var secretStream = new MemoryStream(Encoding.UTF8.GetBytes(secret));
|
||||
var hash = await SHA256.HashDataAsync(secretStream);
|
||||
return Convert.ToHexString(hash);
|
||||
}
|
||||
|
||||
private static string NormalizeServerUrl(string serverUrl)
|
||||
{
|
||||
return serverUrl.Trim().TrimEnd('/');
|
||||
}
|
||||
|
||||
private async Task RemoveEnterpriseManagedApiKeysAsync()
|
||||
{
|
||||
var secretTargets = GetEnterpriseManagedSecretTargets();
|
||||
if (secretTargets.Count == 0)
|
||||
{
|
||||
logger.LogInformation("No enterprise-managed API keys are currently known in the settings. No keyring cleanup is required.");
|
||||
return;
|
||||
}
|
||||
|
||||
logger.LogInformation("Removing {SecretCount} enterprise-managed API key(s) from the OS keyring after an enterprise encryption secret change.", secretTargets.Count);
|
||||
foreach (var target in secretTargets)
|
||||
{
|
||||
try
|
||||
{
|
||||
var deleteResult = await rustService.DeleteAPIKey(target, target.StoreType);
|
||||
if (deleteResult.Success)
|
||||
{
|
||||
if (deleteResult.WasEntryFound)
|
||||
logger.LogInformation("Successfully deleted enterprise-managed API key '{SecretName}' from the OS keyring.", target.SecretName);
|
||||
else
|
||||
logger.LogInformation("Enterprise-managed API key '{SecretName}' was already absent from the OS keyring.", target.SecretName);
|
||||
}
|
||||
else
|
||||
logger.LogWarning("Failed to delete enterprise-managed API key '{SecretName}' from the OS keyring: {Issue}", target.SecretName, deleteResult.Issue);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.LogWarning(e, "Failed to delete enterprise-managed API key '{SecretName}' from the OS keyring.", target.SecretName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static List<EnterpriseSecretTarget> GetEnterpriseManagedSecretTargets()
|
||||
{
|
||||
var configurationData = Program.SERVICE_PROVIDER.GetRequiredService<SettingsManager>().ConfigurationData;
|
||||
var secretTargets = new HashSet<EnterpriseSecretTarget>();
|
||||
|
||||
AddEnterpriseManagedSecretTargets(configurationData.Providers, SecretStoreType.LLM_PROVIDER, secretTargets);
|
||||
AddEnterpriseManagedSecretTargets(configurationData.EmbeddingProviders, SecretStoreType.EMBEDDING_PROVIDER, secretTargets);
|
||||
AddEnterpriseManagedSecretTargets(configurationData.TranscriptionProviders, SecretStoreType.TRANSCRIPTION_PROVIDER, secretTargets);
|
||||
|
||||
return secretTargets.ToList();
|
||||
}
|
||||
|
||||
private static void AddEnterpriseManagedSecretTargets<TSecret>(
|
||||
IEnumerable<TSecret> secrets,
|
||||
SecretStoreType storeType,
|
||||
ISet<EnterpriseSecretTarget> secretTargets) where TSecret : ISecretId, IConfigurationObject
|
||||
{
|
||||
foreach (var secret in secrets)
|
||||
{
|
||||
if (!secret.IsEnterpriseConfiguration || secret.EnterpriseConfigurationPluginId == Guid.Empty)
|
||||
continue;
|
||||
|
||||
secretTargets.Add(new EnterpriseSecretTarget(secret.SecretId, secret.SecretName, storeType));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@
|
||||
- Added a start-page setting, so AI Studio can now open directly on your preferred page when the app starts. Configuration plugins can also provide and optionally lock this default for organizations.
|
||||
- Added math rendering in chats for LaTeX display formulas, including block formats such as `$$ ... $$` and `\[ ... \]`.
|
||||
- Released the document analysis assistant after an intense testing phase.
|
||||
- Improved enterprise deployment for organizations: administrators can now provide up to 10 centrally managed enterprise configuration slots, use policy files on Linux and macOS, and continue using older configuration formats as a fallback during migration.
|
||||
- Improved the profile selection for assistants and the chat. You can now explicitly choose between the app default profile, no profile, or a specific profile.
|
||||
- Improved the performance by caching the OS language detection and requesting the user language only once per app start.
|
||||
- Improved the chat performance by reducing unnecessary UI updates, making chats smoother and more responsive, especially in longer conversations.
|
||||
|
||||
Reference in new issue
Block a user