mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-03-29 17:31:37 +00:00
Merge af9b054491 into 658a8aa125
This commit is contained in:
commit
48bf3cf02f
@ -25,13 +25,22 @@ public static partial class PluginFactory
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes the enterprise encryption service by reading the encryption secret
|
/// Initializes the enterprise encryption service by reading the encryption secret
|
||||||
/// from the Windows Registry or environment variables.
|
/// from the effective enterprise source.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="rustService">The Rust service to use for reading the encryption secret.</param>
|
/// <param name="rustService">The Rust service to use for reading the encryption secret.</param>
|
||||||
public static async Task InitializeEnterpriseEncryption(Services.RustService rustService)
|
public static async Task InitializeEnterpriseEncryption(Services.RustService rustService)
|
||||||
{
|
{
|
||||||
LOG.LogInformation("Initializing enterprise encryption service...");
|
|
||||||
var encryptionSecret = await rustService.EnterpriseEnvConfigEncryptionSecret();
|
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>();
|
var enterpriseEncryptionLogger = Program.LOGGER_FACTORY.CreateLogger<EnterpriseEncryption>();
|
||||||
EnterpriseEncryption = new EnterpriseEncryption(enterpriseEncryptionLogger, encryptionSecret);
|
EnterpriseEncryption = new EnterpriseEncryption(enterpriseEncryptionLogger, encryptionSecret);
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,8 @@
|
|||||||
using AIStudio.Tools.PluginSystem;
|
using AIStudio.Tools.PluginSystem;
|
||||||
|
using AIStudio.Settings;
|
||||||
|
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
namespace AIStudio.Tools.Services;
|
namespace AIStudio.Tools.Services;
|
||||||
|
|
||||||
@ -8,8 +12,14 @@ public sealed class EnterpriseEnvironmentService(ILogger<EnterpriseEnvironmentSe
|
|||||||
|
|
||||||
public static bool HasValidEnterpriseSnapshot { get; private set; }
|
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 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
|
#if DEBUG
|
||||||
private static readonly TimeSpan CHECK_INTERVAL = TimeSpan.FromMinutes(6);
|
private static readonly TimeSpan CHECK_INTERVAL = TimeSpan.FromMinutes(6);
|
||||||
#else
|
#else
|
||||||
@ -39,6 +49,7 @@ public sealed class EnterpriseEnvironmentService(ILogger<EnterpriseEnvironmentSe
|
|||||||
logger.LogInformation("Start updating of the enterprise environment.");
|
logger.LogInformation("Start updating of the enterprise environment.");
|
||||||
HasValidEnterpriseSnapshot = false;
|
HasValidEnterpriseSnapshot = false;
|
||||||
var previousSnapshot = BuildNormalizedSnapshot(CURRENT_ENVIRONMENTS);
|
var previousSnapshot = BuildNormalizedSnapshot(CURRENT_ENVIRONMENTS);
|
||||||
|
var previousSecretSnapshot = CURRENT_SECRET_SNAPSHOT;
|
||||||
|
|
||||||
//
|
//
|
||||||
// Step 1: Fetch all active configurations.
|
// Step 1: Fetch all active configurations.
|
||||||
@ -55,6 +66,21 @@ public sealed class EnterpriseEnvironmentService(ILogger<EnterpriseEnvironmentSe
|
|||||||
return;
|
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.
|
// Step 2: Determine ETags and build the list of reachable configurations.
|
||||||
// IMPORTANT: when one config server fails, we continue with the others.
|
// 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.");
|
logger.LogInformation("AI Studio runs without any enterprise configurations.");
|
||||||
|
|
||||||
var effectiveSnapshot = BuildNormalizedSnapshot(effectiveEnvironments);
|
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_ENVIRONMENTS = effectiveEnvironments;
|
||||||
|
CURRENT_SECRET_SNAPSHOT = nextSecretSnapshot;
|
||||||
HasValidEnterpriseSnapshot = true;
|
HasValidEnterpriseSnapshot = true;
|
||||||
|
|
||||||
if (!previousSnapshot.SequenceEqual(effectiveSnapshot))
|
if (!previousSnapshot.SequenceEqual(effectiveSnapshot) || wasSecretChanged)
|
||||||
await MessageBus.INSTANCE.SendMessage<bool>(null, Event.ENTERPRISE_ENVIRONMENTS_CHANGED);
|
await MessageBus.INSTANCE.SendMessage<bool>(null, Event.ENTERPRISE_ENVIRONMENTS_CHANGED);
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
@ -193,8 +229,81 @@ public sealed class EnterpriseEnvironmentService(ILogger<EnterpriseEnvironmentSe
|
|||||||
.ToList();
|
.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)
|
private static string NormalizeServerUrl(string serverUrl)
|
||||||
{
|
{
|
||||||
return serverUrl.Trim().TrimEnd('/');
|
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 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 `\[ ... \]`.
|
- 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.
|
- 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 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 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.
|
- Improved the chat performance by reducing unnecessary UI updates, making chats smoother and more responsive, especially in longer conversations.
|
||||||
|
|||||||
@ -15,123 +15,118 @@ AI Studio checks about every 16 minutes to see if the configuration ID, the serv
|
|||||||
## Configure the devices
|
## Configure the devices
|
||||||
So that MindWork AI Studio knows where to load which configuration, this information must be provided as metadata on employees' devices. Currently, the following options are available:
|
So that MindWork AI Studio knows where to load which configuration, this information must be provided as metadata on employees' devices. Currently, the following options are available:
|
||||||
|
|
||||||
- **Registry** (only available for Microsoft Windows): On Windows devices, AI Studio first tries to read the information from the registry. The registry information can be managed and distributed centrally as a so-called Group Policy Object (GPO).
|
- **Windows Registry / GPO**: On Windows, AI Studio first tries to read the enterprise configuration metadata from the registry. This is the preferred option for centrally managed Windows devices.
|
||||||
|
|
||||||
- **Environment variables**: On all operating systems (on Windows as a fallback after the registry), AI Studio tries to read the configuration metadata from environment variables.
|
- **Policy files**: AI Studio can read simple YAML policy files from a system-wide directory. On Linux and macOS, this is the preferred option. On Windows, it is used as a fallback after the registry.
|
||||||
|
|
||||||
|
- **Environment variables**: Environment variables are still supported on all operating systems, but they are now only used as the last fallback.
|
||||||
|
|
||||||
|
### Source order and fallback behavior
|
||||||
|
|
||||||
|
AI Studio does **not** merge the registry, policy files, and environment variables. Instead, it checks them in order:
|
||||||
|
|
||||||
|
- **Windows:** Registry -> Policy files -> Environment variables
|
||||||
|
- **Linux:** Policy files -> Environment variables
|
||||||
|
- **macOS:** Policy files -> Environment variables
|
||||||
|
|
||||||
|
For enterprise configurations, AI Studio uses the **first source that contains at least one valid enterprise configuration**.
|
||||||
|
|
||||||
|
For the encryption secret, AI Studio uses the **first source that contains a non-empty encryption secret**, even if that source does not contain any enterprise configuration IDs or server URLs. This allows secret-only setups during migration or on machines that only need encrypted API key support.
|
||||||
|
|
||||||
### Multiple configurations (recommended)
|
### Multiple configurations (recommended)
|
||||||
|
|
||||||
AI Studio supports loading multiple enterprise configurations simultaneously. This enables hierarchical configuration schemes, e.g., organization-wide settings combined with department-specific settings. The following keys and variables are used:
|
AI Studio supports loading multiple enterprise configurations simultaneously. This enables hierarchical configuration schemes, such as organization-wide settings combined with institute- or department-specific settings.
|
||||||
|
|
||||||
- Key `HKEY_CURRENT_USER\Software\github\MindWork AI Studio\Enterprise IT`, value `configs` or variable `MINDWORK_AI_STUDIO_ENTERPRISE_CONFIGS`: A combined format containing one or more configuration entries. Each entry consists of a configuration ID and a server URL separated by `@`. Multiple entries are separated by `;`. The format is: `id1@url1;id2@url2;id3@url3`. The configuration ID must be a valid [GUID](https://en.wikipedia.org/wiki/Universally_unique_identifier#Globally_unique_identifier).
|
The preferred format is a fixed set of indexed pairs:
|
||||||
|
|
||||||
- Key `HKEY_CURRENT_USER\Software\github\MindWork AI Studio\Enterprise IT`, value `config_encryption_secret` or variable `MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_ENCRYPTION_SECRET`: A base64-encoded 32-byte encryption key for decrypting API keys in configuration plugins. This is optional and only needed if you want to include encrypted API keys in your configuration. All configurations share the same encryption secret.
|
- Registry values `config_id0` to `config_id9` together with `config_server_url0` to `config_server_url9`
|
||||||
|
- Environment variables `MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_ID0` to `MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_ID9` together with `MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_SERVER_URL0` to `MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_SERVER_URL9`
|
||||||
|
- Policy files `config0.yaml` to `config9.yaml`
|
||||||
|
|
||||||
**Example:** To configure two enterprise configurations (one for the organization and one for a department):
|
Each configuration ID must be a valid [GUID](https://en.wikipedia.org/wiki/Universally_unique_identifier#Globally_unique_identifier). Up to ten configurations are supported per device.
|
||||||
|
|
||||||
```
|
If multiple configurations define the same setting, the first definition wins. For indexed pairs and policy files, the order is slot `0`, then `1`, and so on up to `9`.
|
||||||
MINDWORK_AI_STUDIO_ENTERPRISE_CONFIGS=9072b77d-ca81-40da-be6a-861da525ef7b@https://intranet.my-company.com:30100/ai-studio/configuration;a1b2c3d4-e5f6-7890-abcd-ef1234567890@https://intranet.my-company.com:30100/ai-studio/department-config
|
|
||||||
|
### Windows registry example
|
||||||
|
|
||||||
|
The Windows registry path is:
|
||||||
|
|
||||||
|
`HKEY_CURRENT_USER\Software\github\MindWork AI Studio\Enterprise IT`
|
||||||
|
|
||||||
|
Example values:
|
||||||
|
|
||||||
|
- `config_id0` = `9072b77d-ca81-40da-be6a-861da525ef7b`
|
||||||
|
- `config_server_url0` = `https://intranet.example.org/ai-studio/configuration`
|
||||||
|
- `config_id1` = `a1b2c3d4-e5f6-7890-abcd-ef1234567890`
|
||||||
|
- `config_server_url1` = `https://intranet.example.org/ai-studio/department-config`
|
||||||
|
- `config_encryption_secret` = `BASE64...`
|
||||||
|
|
||||||
|
This approach works well with GPOs because each slot can be managed independently without rewriting a shared combined string.
|
||||||
|
|
||||||
|
### Policy files
|
||||||
|
|
||||||
|
#### Windows policy directory
|
||||||
|
|
||||||
|
`%ProgramData%\MindWorkAI\AI-Studio\`
|
||||||
|
|
||||||
|
#### Linux policy directories
|
||||||
|
|
||||||
|
AI Studio checks each directory listed in `$XDG_CONFIG_DIRS` and looks for a `mindwork-ai-studio` subdirectory in each one. If `$XDG_CONFIG_DIRS` is empty or not set, AI Studio falls back to:
|
||||||
|
|
||||||
|
`/etc/xdg/mindwork-ai-studio/`
|
||||||
|
|
||||||
|
The directories from `$XDG_CONFIG_DIRS` are processed in order.
|
||||||
|
|
||||||
|
#### macOS policy directory
|
||||||
|
|
||||||
|
`/Library/Application Support/MindWork/AI Studio/`
|
||||||
|
|
||||||
|
#### Policy file names and content
|
||||||
|
|
||||||
|
Configuration files:
|
||||||
|
|
||||||
|
- `config0.yaml`
|
||||||
|
- `config1.yaml`
|
||||||
|
- ...
|
||||||
|
- `config9.yaml`
|
||||||
|
|
||||||
|
Each configuration file contains one configuration ID and one server URL:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
id: "9072b77d-ca81-40da-be6a-861da525ef7b"
|
||||||
|
server_url: "https://intranet.example.org/ai-studio/configuration"
|
||||||
```
|
```
|
||||||
|
|
||||||
**Priority:** When multiple configurations define the same setting (e.g., a provider with the same ID), the first definition wins. The order of entries in the variable determines priority. Place the organization-wide configuration first, followed by department-specific configurations if the organization should have higher priority.
|
Optional encryption secret file:
|
||||||
|
|
||||||
### Windows GPO / PowerShell example for `configs`
|
- `config_encryption_secret.yaml`
|
||||||
|
|
||||||
If you distribute multiple GPOs, each GPO should read and write the same registry value (`configs`) and only update its own `id@url` entry. Other entries must stay untouched.
|
```yaml
|
||||||
|
config_encryption_secret: "BASE64..."
|
||||||
The following PowerShell example provides helper functions for appending and removing entries safely:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
$RegistryPath = "HKCU:\Software\github\MindWork AI Studio\Enterprise IT"
|
|
||||||
$ConfigsValueName = "configs"
|
|
||||||
|
|
||||||
function Get-ConfigEntries {
|
|
||||||
param([string]$RawValue)
|
|
||||||
|
|
||||||
if ([string]::IsNullOrWhiteSpace($RawValue)) { return @() }
|
|
||||||
|
|
||||||
$entries = @()
|
|
||||||
foreach ($part in $RawValue.Split(';')) {
|
|
||||||
$trimmed = $part.Trim()
|
|
||||||
if ([string]::IsNullOrWhiteSpace($trimmed)) { continue }
|
|
||||||
|
|
||||||
$pair = $trimmed.Split('@', 2)
|
|
||||||
if ($pair.Count -ne 2) { continue }
|
|
||||||
|
|
||||||
$id = $pair[0].Trim().ToLowerInvariant()
|
|
||||||
$url = $pair[1].Trim()
|
|
||||||
if ([string]::IsNullOrWhiteSpace($id) -or [string]::IsNullOrWhiteSpace($url)) { continue }
|
|
||||||
|
|
||||||
$entries += [PSCustomObject]@{
|
|
||||||
Id = $id
|
|
||||||
Url = $url
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $entries
|
|
||||||
}
|
|
||||||
|
|
||||||
function ConvertTo-ConfigValue {
|
|
||||||
param([array]$Entries)
|
|
||||||
|
|
||||||
return ($Entries | ForEach-Object { "$($_.Id)@$($_.Url)" }) -join ';'
|
|
||||||
}
|
|
||||||
|
|
||||||
function Add-EnterpriseConfigEntry {
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory=$true)][Guid]$ConfigId,
|
|
||||||
[Parameter(Mandatory=$true)][string]$ServerUrl
|
|
||||||
)
|
|
||||||
|
|
||||||
if (-not (Test-Path $RegistryPath)) {
|
|
||||||
New-Item -Path $RegistryPath -Force | Out-Null
|
|
||||||
}
|
|
||||||
|
|
||||||
$raw = (Get-ItemProperty -Path $RegistryPath -Name $ConfigsValueName -ErrorAction SilentlyContinue).$ConfigsValueName
|
|
||||||
$entries = Get-ConfigEntries -RawValue $raw
|
|
||||||
$normalizedId = $ConfigId.ToString().ToLowerInvariant()
|
|
||||||
$normalizedUrl = $ServerUrl.Trim()
|
|
||||||
|
|
||||||
# Replace only this one ID, keep all other entries unchanged.
|
|
||||||
$entries = @($entries | Where-Object { $_.Id -ne $normalizedId })
|
|
||||||
$entries += [PSCustomObject]@{
|
|
||||||
Id = $normalizedId
|
|
||||||
Url = $normalizedUrl
|
|
||||||
}
|
|
||||||
|
|
||||||
Set-ItemProperty -Path $RegistryPath -Name $ConfigsValueName -Type String -Value (ConvertTo-ConfigValue -Entries $entries)
|
|
||||||
}
|
|
||||||
|
|
||||||
function Remove-EnterpriseConfigEntry {
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory=$true)][Guid]$ConfigId
|
|
||||||
)
|
|
||||||
|
|
||||||
if (-not (Test-Path $RegistryPath)) { return }
|
|
||||||
|
|
||||||
$raw = (Get-ItemProperty -Path $RegistryPath -Name $ConfigsValueName -ErrorAction SilentlyContinue).$ConfigsValueName
|
|
||||||
$entries = Get-ConfigEntries -RawValue $raw
|
|
||||||
$normalizedId = $ConfigId.ToString().ToLowerInvariant()
|
|
||||||
|
|
||||||
# Remove only this one ID, keep all other entries unchanged.
|
|
||||||
$updated = @($entries | Where-Object { $_.Id -ne $normalizedId })
|
|
||||||
Set-ItemProperty -Path $RegistryPath -Name $ConfigsValueName -Type String -Value (ConvertTo-ConfigValue -Entries $updated)
|
|
||||||
}
|
|
||||||
|
|
||||||
# Example usage:
|
|
||||||
# Add-EnterpriseConfigEntry -ConfigId "9072b77d-ca81-40da-be6a-861da525ef7b" -ServerUrl "https://intranet.example.org:30100/ai-studio/configuration"
|
|
||||||
# Remove-EnterpriseConfigEntry -ConfigId "9072b77d-ca81-40da-be6a-861da525ef7b"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Single configuration (legacy)
|
### Environment variable example
|
||||||
|
|
||||||
The following single-configuration keys and variables are still supported for backwards compatibility. AI Studio always reads both the multi-config and legacy variables and merges all found configurations into one list. If a configuration ID appears in both, the entry from the multi-config format takes priority (first occurrence wins). This means you can migrate to the new format incrementally without losing existing configurations:
|
If you need the fallback environment-variable format, configure the values like this:
|
||||||
|
|
||||||
- Key `HKEY_CURRENT_USER\Software\github\MindWork AI Studio\Enterprise IT`, value `config_id` or variable `MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_ID`: This must be a valid [GUID](https://en.wikipedia.org/wiki/Universally_unique_identifier#Globally_unique_identifier). It uniquely identifies the configuration. You can use an ID per department, institute, or even per person.
|
```bash
|
||||||
|
MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_ID0=9072b77d-ca81-40da-be6a-861da525ef7b
|
||||||
|
MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_SERVER_URL0=https://intranet.example.org/ai-studio/configuration
|
||||||
|
MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_ID1=a1b2c3d4-e5f6-7890-abcd-ef1234567890
|
||||||
|
MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_SERVER_URL1=https://intranet.example.org/ai-studio/department-config
|
||||||
|
MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_ENCRYPTION_SECRET=BASE64...
|
||||||
|
```
|
||||||
|
|
||||||
- Key `HKEY_CURRENT_USER\Software\github\MindWork AI Studio\Enterprise IT`, value `config_server_url` or variable `MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_SERVER_URL`: An HTTP or HTTPS address using an IP address or DNS name. This is the web server from which AI Studio attempts to load the specified configuration as a ZIP file.
|
### Legacy formats (still supported)
|
||||||
|
|
||||||
- Key `HKEY_CURRENT_USER\Software\github\MindWork AI Studio\Enterprise IT`, value `config_encryption_secret` or variable `MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_ENCRYPTION_SECRET`: A base64-encoded 32-byte encryption key for decrypting API keys in configuration plugins. This is optional and only needed if you want to include encrypted API keys in your configuration.
|
The following older formats are still supported for backwards compatibility:
|
||||||
|
|
||||||
|
- Registry value `configs` or environment variable `MINDWORK_AI_STUDIO_ENTERPRISE_CONFIGS`: Combined format `id1@url1;id2@url2;...`
|
||||||
|
- Registry value `config_id` or environment variable `MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_ID`
|
||||||
|
- Registry value `config_server_url` or environment variable `MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_SERVER_URL`
|
||||||
|
- Registry value `config_encryption_secret` or environment variable `MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_ENCRYPTION_SECRET`
|
||||||
|
|
||||||
|
Within a single source, AI Studio reads the new indexed pairs first, then the combined legacy format, and finally the legacy single-configuration format. This makes it possible to migrate gradually without breaking older setups.
|
||||||
|
|
||||||
### How configurations are downloaded
|
### How configurations are downloaded
|
||||||
|
|
||||||
@ -183,7 +178,7 @@ intranet.my-company.com:30100 {
|
|||||||
|
|
||||||
## Important: Plugin ID must match the enterprise configuration ID
|
## Important: Plugin ID must match the enterprise configuration ID
|
||||||
|
|
||||||
The `ID` field inside your configuration plugin (the Lua file) **must** be identical to the enterprise configuration ID used in the registry or environment variable. AI Studio uses this ID to match downloaded configurations to their plugins. If the IDs do not match, AI Studio will log a warning and the configuration may not be displayed correctly on the Information page.
|
The `ID` field inside your configuration plugin (the Lua file) **must** be identical to the enterprise configuration ID configured on the client device, whether it comes from the registry, a policy file, or an environment variable. AI Studio uses this ID to match downloaded configurations to their plugins. If the IDs do not match, AI Studio will log a warning and the configuration may not be displayed correctly on the Information page.
|
||||||
|
|
||||||
For example, if your enterprise configuration ID is `9072b77d-ca81-40da-be6a-861da525ef7b`, then your plugin must declare:
|
For example, if your enterprise configuration ID is `9072b77d-ca81-40da-be6a-861da525ef7b`, then your plugin must declare:
|
||||||
|
|
||||||
@ -233,9 +228,10 @@ You can include encrypted API keys in your configuration plugins for cloud provi
|
|||||||
In AI Studio, enable the "Show administration settings" toggle in the app settings. Then click the "Generate encryption secret and copy to clipboard" button in the "Enterprise Administration" section. This generates a cryptographically secure 256-bit key and copies it to your clipboard as a base64 string.
|
In AI Studio, enable the "Show administration settings" toggle in the app settings. Then click the "Generate encryption secret and copy to clipboard" button in the "Enterprise Administration" section. This generates a cryptographically secure 256-bit key and copies it to your clipboard as a base64 string.
|
||||||
|
|
||||||
2. **Deploy the encryption secret:**
|
2. **Deploy the encryption secret:**
|
||||||
Distribute the secret to all client machines via Group Policy (Windows Registry) or environment variables:
|
Distribute the secret to all client machines using any supported enterprise source. The secret can be deployed on its own, even when no enterprise configuration IDs or server URLs are defined on that machine:
|
||||||
- Registry: `HKEY_CURRENT_USER\Software\github\MindWork AI Studio\Enterprise IT\config_encryption_secret`
|
- Windows Registry / GPO: `HKEY_CURRENT_USER\Software\github\MindWork AI Studio\Enterprise IT\config_encryption_secret`
|
||||||
- Environment: `MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_ENCRYPTION_SECRET`
|
- Policy file: `config_encryption_secret.yaml`
|
||||||
|
- Environment fallback: `MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_ENCRYPTION_SECRET`
|
||||||
|
|
||||||
You must also deploy the same secret on the machine where you will export the encrypted API keys (step 3).
|
You must also deploy the same secret on the machine where you will export the encrypted API keys (step 3).
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user