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

This commit is contained in:
Thorsten Sommer authored and GitHub committed 2026-03-31 13:02:59 +02:00
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.