diff --git a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua
index 7020549e..bfca8ca0 100644
--- a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua
+++ b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua
@@ -6046,9 +6046,15 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1137744461"] = "ID mismatch: the
-- This is a private AI Studio installation. It runs without an enterprise configuration.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1209549230"] = "This is a private AI Studio installation. It runs without an enterprise configuration."
+-- Copies the configuration origin to the clipboard
+UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T125850635"] = "Copies the configuration origin to the clipboard"
+
-- Unknown configuration plugin
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1290340974"] = "Unknown configuration plugin"
+-- Copies the configuration slot to the clipboard
+UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1347508205"] = "Copies the configuration slot to the clipboard"
+
-- This library is used to read PDF files. This is necessary, e.g., for using PDFs as a data source for a chat.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1388816916"] = "This library is used to read PDF files. This is necessary, e.g., for using PDFs as a data source for a chat."
@@ -6145,6 +6151,12 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2371107659"] = "installation pro
-- Installed Pandoc version: Pandoc is not installed or not available.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2374031539"] = "Installed Pandoc version: Pandoc is not installed or not available."
+-- Configuration origin:
+UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2435772109"] = "Configuration origin:"
+
+-- Configuration slot:
+UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T254943559"] = "Configuration slot:"
+
-- This library is used to determine the language of the operating system. This is necessary to set the language of the user interface.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2557014401"] = "This library is used to determine the language of the operating system. This is necessary to set the language of the user interface."
@@ -6199,6 +6211,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2868174483"] = "The .NET backend
-- AI Studio runs with an enterprise configuration and configuration servers. The configuration plugins are not yet available.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2924964415"] = "AI Studio runs with an enterprise configuration and configuration servers. The configuration plugins are not yet available."
+-- Copies the configuration source to the clipboard
+UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2929232062"] = "Copies the configuration source to the clipboard"
+
-- Changelog
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3017574265"] = "Changelog"
@@ -6259,6 +6274,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3722989559"] = "This library is
-- Username provided by the OS
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3764549776"] = "Username provided by the OS"
+-- Configuration source:
+UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3801531724"] = "Configuration source:"
+
-- this version does not met the requirements
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3813932670"] = "this version does not met the requirements"
diff --git a/app/MindWork AI Studio/Pages/Information.razor b/app/MindWork AI Studio/Pages/Information.razor
index 13f2e941..ef24db6b 100644
--- a/app/MindWork AI Studio/Pages/Information.razor
+++ b/app/MindWork AI Studio/Pages/Information.razor
@@ -89,18 +89,7 @@
{
+ Items="@this.BuildEnterpriseConfigurationItems(env)"/>
}
+ Items="@this.BuildEnterpriseConfigurationItems(env)"/>
continue;
}
}
diff --git a/app/MindWork AI Studio/Pages/Information.razor.cs b/app/MindWork AI Studio/Pages/Information.razor.cs
index 9f7250ac..9ac8b800 100644
--- a/app/MindWork AI Studio/Pages/Information.razor.cs
+++ b/app/MindWork AI Studio/Pages/Information.razor.cs
@@ -324,6 +324,55 @@ public partial class Information : MSGComponentBase
?? this.configPlugins.FirstOrDefault(plugin => plugin.ManagedConfigurationId is null && plugin.Id == configurationId);
}
+ private IReadOnlyList BuildEnterpriseConfigurationItems(EnterpriseEnvironment environment, IAvailablePlugin? plugin = null)
+ {
+ var items = new List
+ {
+ new(Icons.Material.Filled.ArrowRightAlt,
+ $"{T("Enterprise configuration ID:")} {environment.ConfigurationId}",
+ environment.ConfigurationId.ToString(),
+ T("Copies the config ID to the clipboard")),
+
+ new(Icons.Material.Filled.ArrowRightAlt,
+ $"{T("Configuration server:")} {environment.ConfigurationServerUrl}",
+ environment.ConfigurationServerUrl,
+ T("Copies the server URL to the clipboard"),
+ "margin-top: 4px;"),
+
+ new(Icons.Material.Filled.ArrowRightAlt,
+ $"{T("Configuration source:")} {environment.Source}",
+ environment.Source,
+ T("Copies the configuration source to the clipboard"),
+ "margin-top: 4px;"),
+ };
+
+ if (!string.IsNullOrWhiteSpace(environment.SourceDetail))
+ {
+ items.Add(new ConfigInfoRowItem(Icons.Material.Filled.ArrowRightAlt,
+ $"{T("Configuration origin:")} {environment.SourceDetail}",
+ environment.SourceDetail,
+ T("Copies the configuration origin to the clipboard"),
+ "margin-top: 4px;"));
+ }
+
+ items.Add(new ConfigInfoRowItem(Icons.Material.Filled.ArrowRightAlt,
+ $"{T("Configuration slot:")} {environment.Slot}",
+ environment.Slot,
+ T("Copies the configuration slot to the clipboard"),
+ "margin-top: 4px;"));
+
+ if (plugin is not null)
+ {
+ items.Add(new ConfigInfoRowItem(Icons.Material.Filled.ArrowRightAlt,
+ $"{T("Configuration plugin ID:")} {plugin.Id}",
+ plugin.Id.ToString(),
+ T("Copies the configuration plugin ID to the clipboard"),
+ "margin-top: 4px;"));
+ }
+
+ return items;
+ }
+
private bool IsManagedConfigurationIdMismatch(IAvailablePlugin plugin, Guid configurationId)
{
return plugin.ManagedConfigurationId == configurationId && plugin.Id != configurationId;
diff --git a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua
index eba11f38..70d999dd 100644
--- a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua
+++ b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua
@@ -6048,9 +6048,15 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1137744461"] = "ID-Konflikt: Die
-- This is a private AI Studio installation. It runs without an enterprise configuration.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1209549230"] = "Dies ist eine private AI Studio-Installation. Sie läuft ohne Unternehmenskonfiguration."
+-- Copies the configuration origin to the clipboard
+UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T125850635"] = "Kopiert den Ursprung der Konfiguration in die Zwischenablage"
+
-- Unknown configuration plugin
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1290340974"] = "Unbekanntes Konfigurations-Plugin"
+-- Copies the configuration slot to the clipboard
+UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1347508205"] = "Kopiert den Slot der Konfiguration in die Zwischenablage"
+
-- This library is used to read PDF files. This is necessary, e.g., for using PDFs as a data source for a chat.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1388816916"] = "Diese Bibliothek wird verwendet, um PDF-Dateien zu lesen. Das ist zum Beispiel notwendig, um PDFs als Datenquelle für einen Chat zu nutzen."
@@ -6147,6 +6153,12 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2371107659"] = "Installation vom
-- Installed Pandoc version: Pandoc is not installed or not available.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2374031539"] = "Installierte Pandoc-Version: Pandoc ist nicht installiert oder nicht verfügbar."
+-- Configuration origin:
+UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2435772109"] = "Ursprung der Konfiguration:"
+
+-- Configuration slot:
+UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T254943559"] = "Slot der Konfiguration:"
+
-- This library is used to determine the language of the operating system. This is necessary to set the language of the user interface.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2557014401"] = "Diese Bibliothek wird verwendet, um die Sprache des Betriebssystems zu erkennen. Dies ist notwendig, um die Sprache der Benutzeroberfläche einzustellen."
@@ -6201,6 +6213,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2868174483"] = "Das .NET-Backend
-- AI Studio runs with an enterprise configuration and configuration servers. The configuration plugins are not yet available.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2924964415"] = "AI Studio wird mit Unternehmenskonfigurationen und Konfigurationsservern betrieben. Die Konfigurations-Plugins sind noch nicht verfügbar."
+-- Copies the configuration source to the clipboard
+UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2929232062"] = "Kopiert die Quelle der Konfiguration in die Zwischenablage"
+
-- Changelog
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3017574265"] = "Änderungsprotokoll"
@@ -6261,6 +6276,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3722989559"] = "Diese Bibliothek
-- Username provided by the OS
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3764549776"] = "Vom Betriebssystem bereitgestellter Benutzername"
+-- Configuration source:
+UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3801531724"] = "Quelle der Konfiguration:"
+
-- this version does not met the requirements
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3813932670"] = "diese Version erfüllt die Anforderungen nicht"
diff --git a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua
index 01e80406..59f951c3 100644
--- a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua
+++ b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua
@@ -6048,9 +6048,15 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1137744461"] = "ID mismatch: the
-- This is a private AI Studio installation. It runs without an enterprise configuration.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1209549230"] = "This is a private AI Studio installation. It runs without an enterprise configuration."
+-- Copies the configuration origin to the clipboard
+UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T125850635"] = "Copies the configuration origin to the clipboard"
+
-- Unknown configuration plugin
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1290340974"] = "Unknown configuration plugin"
+-- Copies the configuration slot to the clipboard
+UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1347508205"] = "Copies the configuration slot to the clipboard"
+
-- This library is used to read PDF files. This is necessary, e.g., for using PDFs as a data source for a chat.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1388816916"] = "This library is used to read PDF files. This is necessary, e.g., for using PDFs as a data source for a chat."
@@ -6147,6 +6153,12 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2371107659"] = "installation pro
-- Installed Pandoc version: Pandoc is not installed or not available.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2374031539"] = "Installed Pandoc version: Pandoc is not installed or not available."
+-- Configuration origin:
+UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2435772109"] = "Configuration origin:"
+
+-- Configuration slot:
+UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T254943559"] = "Configuration slot:"
+
-- This library is used to determine the language of the operating system. This is necessary to set the language of the user interface.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2557014401"] = "This library is used to determine the language of the operating system. This is necessary to set the language of the user interface."
@@ -6201,6 +6213,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2868174483"] = "The .NET backend
-- AI Studio runs with an enterprise configuration and configuration servers. The configuration plugins are not yet available.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2924964415"] = "AI Studio runs with an enterprise configuration and configuration servers. The configuration plugins are not yet available."
+-- Copies the configuration source to the clipboard
+UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2929232062"] = "Copies the configuration source to the clipboard"
+
-- Changelog
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3017574265"] = "Changelog"
@@ -6261,6 +6276,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3722989559"] = "This library is
-- Username provided by the OS
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3764549776"] = "Username provided by the OS"
+-- Configuration source:
+UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3801531724"] = "Configuration source:"
+
-- this version does not met the requirements
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3813932670"] = "this version does not met the requirements"
diff --git a/app/MindWork AI Studio/Tools/EnterpriseEnvironment.cs b/app/MindWork AI Studio/Tools/EnterpriseEnvironment.cs
index 952ec3b2..abdffd4e 100644
--- a/app/MindWork AI Studio/Tools/EnterpriseEnvironment.cs
+++ b/app/MindWork AI Studio/Tools/EnterpriseEnvironment.cs
@@ -2,7 +2,7 @@ using System.Net.Http.Headers;
namespace AIStudio.Tools;
-public readonly record struct EnterpriseEnvironment(string ConfigurationServerUrl, Guid ConfigurationId, EntityTagHeaderValue? ETag)
+public readonly record struct EnterpriseEnvironment(string ConfigurationServerUrl, Guid ConfigurationId, string Source, string SourceDetail, string Slot, EntityTagHeaderValue? ETag)
{
public bool IsActive => !string.IsNullOrWhiteSpace(this.ConfigurationServerUrl) && this.ConfigurationId != Guid.Empty;
}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/Rust/EnterpriseConfig.cs b/app/MindWork AI Studio/Tools/Rust/EnterpriseConfig.cs
index bc6fb15e..197b6143 100644
--- a/app/MindWork AI Studio/Tools/Rust/EnterpriseConfig.cs
+++ b/app/MindWork AI Studio/Tools/Rust/EnterpriseConfig.cs
@@ -1,3 +1,3 @@
namespace AIStudio.Tools.Rust;
-public sealed record EnterpriseConfig(string Id, string ServerUrl);
\ No newline at end of file
+public sealed record EnterpriseConfig(string Id, string ServerUrl, string Source, string SourceDetail, string Slot);
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/Services/EnterpriseEnvironmentService.cs b/app/MindWork AI Studio/Tools/Services/EnterpriseEnvironmentService.cs
index 6db55a6c..90e8606b 100644
--- a/app/MindWork AI Studio/Tools/Services/EnterpriseEnvironmentService.cs
+++ b/app/MindWork AI Studio/Tools/Services/EnterpriseEnvironmentService.cs
@@ -14,7 +14,7 @@ public sealed class EnterpriseEnvironmentService(ILogger new EnterpriseEnvironmentSnapshot(
environment.ConfigurationId,
NormalizeServerUrl(environment.ConfigurationServerUrl),
+ environment.Source,
+ environment.SourceDetail,
+ environment.Slot,
environment.ETag?.ToString()))
.OrderBy(environment => environment.ConfigurationId)
.ToList();
diff --git a/app/MindWork AI Studio/Tools/Services/RustService.Enterprise.cs b/app/MindWork AI Studio/Tools/Services/RustService.Enterprise.cs
index d78567f4..f1155645 100644
--- a/app/MindWork AI Studio/Tools/Services/RustService.Enterprise.cs
+++ b/app/MindWork AI Studio/Tools/Services/RustService.Enterprise.cs
@@ -47,7 +47,7 @@ public sealed partial class RustService
foreach (var config in configs)
{
if (Guid.TryParse(config.Id, out var id))
- environments.Add(new EnterpriseEnvironment(config.ServerUrl, id, null));
+ environments.Add(new EnterpriseEnvironment(config.ServerUrl, id, config.Source, config.SourceDetail, config.Slot, null));
else
this.logger!.LogWarning($"Skipping enterprise config with invalid ID: '{config.Id}'.");
}
diff --git a/app/MindWork AI Studio/wwwroot/changelog/v26.6.1.md b/app/MindWork AI Studio/wwwroot/changelog/v26.6.1.md
index 7e4a82af..7f286123 100644
--- a/app/MindWork AI Studio/wwwroot/changelog/v26.6.1.md
+++ b/app/MindWork AI Studio/wwwroot/changelog/v26.6.1.md
@@ -1 +1,3 @@
# v26.6.1, build 241 (2026-06-xx xx:xx UTC)
+- Added support for up to 100 thousand enterprise configuration slots, using fixed-width slot names such as `config_00000` while keeping the existing first ten slot names compatible.
+- Improved the enterprise configuration details on the information page by showing where each configuration comes from and which configuration slot was used.
diff --git a/documentation/Enterprise IT.md b/documentation/Enterprise IT.md
index 221a24db..168a96d5 100644
--- a/documentation/Enterprise IT.md
+++ b/documentation/Enterprise IT.md
@@ -39,13 +39,15 @@ AI Studio supports loading multiple enterprise configurations simultaneously. Th
The preferred format is a fixed set of indexed pairs:
-- 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`
+- Registry values `config_id_00000` to `config_id_99999` together with `config_server_url_00000` to `config_server_url_99999`
+- Environment variables `MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_ID_00000` to `MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_ID_99999` together with `MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_SERVER_URL_00000` to `MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_SERVER_URL_99999`
+- Policy files `config_00000.yaml` to `config_99999.yaml`
-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.
+Each configuration ID must be a valid [GUID](https://en.wikipedia.org/wiki/Universally_unique_identifier#Globally_unique_identifier). Up to 100,000 indexed configuration slots 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`.
+If multiple configurations define the same setting, the first definition wins. For indexed pairs and policy files, the order is slot `00000`, then `00001`, and so on up to `99999`.
+
+For backwards compatibility, the older slot names `0` to `9` without an underscore are still supported. AI Studio also accepts other numeric slot suffixes with up to five digits. Slot suffixes are matched exactly, so `config_id_1`, `config_id_01`, and `config_id_00001` are treated as separate slots. Use the five-digit format with an underscore for new deployments.
### Windows registry example
@@ -55,10 +57,10 @@ The Windows registry path is:
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_id_00000` = `9072b77d-ca81-40da-be6a-861da525ef7b`
+- `config_server_url_00000` = `https://intranet.example.org/ai-studio/configuration`
+- `config_id_10503` = `a1b2c3d4-e5f6-7890-abcd-ef1234567890`
+- `config_server_url_10503` = `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.
@@ -85,10 +87,10 @@ The directories from `$XDG_CONFIG_DIRS` are processed in order.
Configuration files:
-- `config0.yaml`
-- `config1.yaml`
+- `config_00000.yaml`
+- `config_00001.yaml`
- ...
-- `config9.yaml`
+- `config_99999.yaml`
Each configuration file contains one configuration ID and one server URL:
@@ -110,10 +112,10 @@ config_encryption_secret: "BASE64..."
If you need the fallback environment-variable format, configure the values like this:
```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_ID_00000=9072b77d-ca81-40da-be6a-861da525ef7b
+MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_SERVER_URL_00000=https://intranet.example.org/ai-studio/configuration
+MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_ID_10503=a1b2c3d4-e5f6-7890-abcd-ef1234567890
+MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_SERVER_URL_10503=https://intranet.example.org/ai-studio/department-config
MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_ENCRYPTION_SECRET=BASE64...
```
diff --git a/runtime/src/environment.rs b/runtime/src/environment.rs
index 3f8dd43c..989153cd 100644
--- a/runtime/src/environment.rs
+++ b/runtime/src/environment.rs
@@ -11,13 +11,22 @@ use sys_locale::get_locale;
const DEFAULT_LANGUAGE: &str = "en-US";
-const ENTERPRISE_CONFIG_SLOT_COUNT: usize = 10;
+const ENTERPRISE_CONFIG_SLOT_MAX: u32 = 99_999;
+const ENTERPRISE_CONFIG_SLOT_WIDTH: usize = 5;
+
+const ENTERPRISE_CONFIG_ID_KEY_PREFIX: &str = "config_id";
+const ENTERPRISE_CONFIG_SERVER_URL_KEY_PREFIX: &str = "config_server_url";
#[cfg(target_os = "windows")]
const ENTERPRISE_REGISTRY_KEY_PATH: &str = r"Software\github\MindWork AI Studio\Enterprise IT";
const ENTERPRISE_POLICY_SECRET_FILE_NAME: &str = "config_encryption_secret.yaml";
+const ENTERPRISE_ENV_CONFIG_ID_PREFIX: &str = "MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_ID";
+const ENTERPRISE_ENV_CONFIG_SERVER_URL_PREFIX: &str = "MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_SERVER_URL";
+const ENTERPRISE_ENV_CONFIGS: &str = "MINDWORK_AI_STUDIO_ENTERPRISE_CONFIGS";
+const ENTERPRISE_ENV_CONFIG_ENCRYPTION_SECRET: &str = "MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_ENCRYPTION_SECRET";
+
/// The data directory where the application stores its data.
pub static DATA_DIRECTORY: OnceLock = OnceLock::new();
@@ -187,8 +196,53 @@ pub async fn read_user_language(_token: APIToken) -> String {
pub struct EnterpriseConfig {
pub id: String,
pub server_url: String,
+ pub source: String,
+ pub source_detail: String,
+ pub slot: String,
}
+#[derive(Clone, Debug, PartialEq, Eq)]
+struct EnterpriseSourceValue {
+ value: String,
+ source_detail: String,
+}
+
+impl EnterpriseSourceValue {
+ fn new(value: String, source_detail: String) -> Self {
+ Self {
+ value,
+ source_detail,
+ }
+ }
+}
+
+trait EnterpriseSourceValueAccess {
+ fn value(&self) -> &str;
+ fn source_detail(&self) -> &str;
+}
+
+impl EnterpriseSourceValueAccess for EnterpriseSourceValue {
+ fn value(&self) -> &str {
+ &self.value
+ }
+
+ fn source_detail(&self) -> &str {
+ &self.source_detail
+ }
+}
+
+impl EnterpriseSourceValueAccess for String {
+ fn value(&self) -> &str {
+ self
+ }
+
+ fn source_detail(&self) -> &str {
+ ""
+ }
+}
+
+type EnterpriseSourceValues = HashMap;
+
#[derive(Clone, Debug, Default, PartialEq, Eq)]
struct EnterpriseSourceData {
source_name: String,
@@ -292,7 +346,7 @@ fn load_registry_enterprise_source() -> EnterpriseSourceData {
info!(r"Trying to read enterprise configuration metadata from 'HKEY_CURRENT_USER\{}'.", ENTERPRISE_REGISTRY_KEY_PATH);
- let mut values = HashMap::new();
+ let mut values = EnterpriseSourceValues::new();
let key = match CURRENT_USER.open(ENTERPRISE_REGISTRY_KEY_PATH) {
Ok(key) => key,
Err(_) => {
@@ -304,32 +358,40 @@ fn load_registry_enterprise_source() -> EnterpriseSourceData {
}
};
- for index in 0..ENTERPRISE_CONFIG_SLOT_COUNT {
- insert_registry_value(&mut values, &key, &format!("config_id{index}"));
- insert_registry_value(&mut values, &key, &format!("config_server_url{index}"));
- }
+ match key.values() {
+ Ok(registry_values) => {
+ for (key_name, value) in registry_values {
+ let Some(source_key_name) = enterprise_registry_value_key_name(&key_name) else {
+ continue;
+ };
- for key_name in [
- "configs",
- "config_id",
- "config_server_url",
- "config_encryption_secret",
- ] {
- insert_registry_value(&mut values, &key, key_name);
+ match String::try_from(value) {
+ Ok(value) => {
+ values.insert(source_key_name, EnterpriseSourceValue::new(value, String::new()));
+ },
+
+ Err(error) => {
+ warn!(r"Could not read enterprise registry value 'HKEY_CURRENT_USER\{}\{}' as string: {}.", ENTERPRISE_REGISTRY_KEY_PATH, key_name, error);
+ },
+ }
+ }
+ },
+
+ Err(error) => {
+ warn!(r"Could not enumerate enterprise registry values from 'HKEY_CURRENT_USER\{}': {}.", ENTERPRISE_REGISTRY_KEY_PATH, error);
+ },
}
parse_enterprise_source_values("Windows registry", &values)
}
#[cfg(target_os = "windows")]
-fn insert_registry_value(
- values: &mut HashMap,
- key: &windows_registry::Key,
- key_name: &str,
-) {
- if let Ok(value) = key.get_string(key_name) {
- values.insert(String::from(key_name), value);
+fn enterprise_registry_value_key_name(key_name: &str) -> Option {
+ if is_legacy_enterprise_source_key(key_name) {
+ return Some(String::from(key_name));
}
+
+ enterprise_indexed_source_key_name(key_name)
}
fn load_policy_file_enterprise_source() -> EnterpriseSourceData {
@@ -342,26 +404,85 @@ fn load_policy_file_enterprise_source() -> EnterpriseSourceData {
fn load_environment_enterprise_source() -> EnterpriseSourceData {
info!("Trying to read enterprise configuration metadata from environment variables.");
- let mut values = HashMap::new();
- for index in 0..ENTERPRISE_CONFIG_SLOT_COUNT {
- insert_env_value(&mut values, &format!("MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_ID{index}"), &format!("config_id{index}"));
- insert_env_value(&mut values, &format!("MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_SERVER_URL{index}"), &format!("config_server_url{index}"));
+ let mut values = EnterpriseSourceValues::new();
+ for (env_name, value) in env::vars() {
+ if let Some(source_key_name) = enterprise_environment_key_name(&env_name) {
+ let source_detail = enterprise_environment_source_detail(&source_key_name, &env_name);
+ values.insert(source_key_name, EnterpriseSourceValue::new(value, source_detail));
+ }
}
- insert_env_value(&mut values, "MINDWORK_AI_STUDIO_ENTERPRISE_CONFIGS", "configs");
- insert_env_value(&mut values, "MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_ID", "config_id");
- insert_env_value(&mut values, "MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_SERVER_URL", "config_server_url");
- insert_env_value(&mut values, "MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_ENCRYPTION_SECRET", "config_encryption_secret");
-
parse_enterprise_source_values("environment variables", &values)
}
-fn insert_env_value(values: &mut HashMap, env_name: &str, key_name: &str) {
- if let Ok(value) = env::var(env_name) {
- values.insert(String::from(key_name), value);
+fn enterprise_environment_source_detail(source_key_name: &str, env_name: &str) -> String {
+ if source_key_name == "config_id"
+ || enterprise_source_key_suffix(source_key_name, ENTERPRISE_CONFIG_ID_KEY_PREFIX).is_some() {
+ String::from(env_name)
+ } else {
+ String::new()
}
}
+fn enterprise_environment_key_name(env_name: &str) -> Option {
+ if enterprise_env_key_equals(env_name, ENTERPRISE_ENV_CONFIGS) {
+ return Some(String::from("configs"));
+ }
+
+ if enterprise_env_key_equals(env_name, ENTERPRISE_ENV_CONFIG_ID_PREFIX) {
+ return Some(String::from("config_id"));
+ }
+
+ if enterprise_env_key_equals(env_name, ENTERPRISE_ENV_CONFIG_SERVER_URL_PREFIX) {
+ return Some(String::from("config_server_url"));
+ }
+
+ if enterprise_env_key_equals(env_name, ENTERPRISE_ENV_CONFIG_ENCRYPTION_SECRET) {
+ return Some(String::from("config_encryption_secret"));
+ }
+
+ if let Some(suffix) = enterprise_env_key_suffix(env_name, ENTERPRISE_ENV_CONFIG_ID_PREFIX) {
+ return Some(format!("config_id{suffix}"));
+ }
+
+ if let Some(suffix) = enterprise_env_key_suffix(env_name, ENTERPRISE_ENV_CONFIG_SERVER_URL_PREFIX) {
+ return Some(format!("config_server_url{suffix}"));
+ }
+
+ None
+}
+
+#[cfg(target_os = "windows")]
+fn enterprise_env_key_equals(env_name: &str, expected: &str) -> bool {
+ env_name.eq_ignore_ascii_case(expected)
+}
+
+#[cfg(not(target_os = "windows"))]
+fn enterprise_env_key_equals(env_name: &str, expected: &str) -> bool {
+ env_name == expected
+}
+
+#[cfg(target_os = "windows")]
+fn enterprise_env_key_suffix<'a>(env_name: &'a str, prefix: &str) -> Option<&'a str> {
+ if env_name.len() < prefix.len() {
+ return None;
+ }
+
+ let (raw_prefix, suffix) = env_name.split_at(prefix.len());
+ if raw_prefix.eq_ignore_ascii_case(prefix) {
+ normalize_enterprise_slot_suffix(suffix)
+ } else {
+ None
+ }
+}
+
+#[cfg(not(target_os = "windows"))]
+fn enterprise_env_key_suffix<'a>(env_name: &'a str, prefix: &str) -> Option<&'a str> {
+ env_name
+ .strip_prefix(prefix)
+ .and_then(normalize_enterprise_slot_suffix)
+}
+
#[cfg(target_os = "windows")]
fn enterprise_policy_directories() -> Vec {
let base = env::var_os("ProgramData")
@@ -406,19 +527,49 @@ fn linux_policy_directories_from_xdg(xdg_config_dirs: Option<&str>) -> Vec HashMap {
- let mut values = HashMap::new();
+fn load_policy_values_from_directories(directories: &[PathBuf]) -> EnterpriseSourceValues {
+ let mut values = EnterpriseSourceValues::new();
for directory in directories {
info!("Checking enterprise policy directory '{}'.", directory.display());
- for index in 0..ENTERPRISE_CONFIG_SLOT_COUNT {
- let path = directory.join(format!("config{index}.yaml"));
+ let entries = match fs::read_dir(directory) {
+ Ok(entries) => entries,
+ Err(error) => {
+ info!("Could not enumerate enterprise policy directory '{}': {}.", directory.display(), error);
+ continue;
+ },
+ };
+
+ for entry in entries {
+ let entry = match entry {
+ Ok(entry) => entry,
+ Err(error) => {
+ warn!("Could not read an entry from enterprise policy directory '{}': {}.", directory.display(), error);
+ continue;
+ },
+ };
+
+ let file_name = entry.file_name();
+ let Some(file_name) = file_name.to_str() else {
+ continue;
+ };
+
+ let Some(suffix) = enterprise_policy_file_slot_suffix(file_name) else {
+ continue;
+ };
+
+ let path = entry.path();
if let Some(config_values) = read_policy_yaml_mapping(&path) {
+ let source_detail = path
+ .canonicalize()
+ .unwrap_or_else(|_| path.clone())
+ .to_string_lossy()
+ .into_owned();
if let Some(id) = config_values.get("id") {
- insert_first_non_empty_value(&mut values, &format!("config_id{index}"), id);
+ insert_first_non_empty_value(&mut values, &format!("config_id{suffix}"), id, &source_detail);
}
if let Some(server_url) = config_values.get("server_url") {
- insert_first_non_empty_value(&mut values, &format!("config_server_url{index}"), server_url);
+ insert_first_non_empty_value(&mut values, &format!("config_server_url{suffix}"), server_url, &source_detail);
}
}
}
@@ -426,13 +577,21 @@ fn load_policy_values_from_directories(directories: &[PathBuf]) -> HashMap Option<&str> {
+ let suffix = file_name
+ .strip_prefix("config")?
+ .strip_suffix(".yaml")?;
+
+ normalize_enterprise_slot_suffix(suffix)
+}
+
fn read_policy_yaml_mapping(path: &Path) -> Option> {
if !path.exists() {
return None;
@@ -516,27 +675,118 @@ fn parse_policy_yaml_value(raw_value: &str) -> Option {
Some(String::from(trimmed))
}
-fn insert_first_non_empty_value(values: &mut HashMap, key: &str, raw_value: &str) {
+fn insert_first_non_empty_value(values: &mut EnterpriseSourceValues, key: &str, raw_value: &str, source_detail: &str) {
if let Some(value) = normalize_enterprise_value(raw_value) {
- values.entry(String::from(key)).or_insert(value);
+ values
+ .entry(String::from(key))
+ .or_insert_with(|| EnterpriseSourceValue::new(value, String::from(source_detail)));
}
}
-fn parse_enterprise_source_values(
+#[cfg(target_os = "windows")]
+fn is_legacy_enterprise_source_key(key_name: &str) -> bool {
+ matches!(
+ key_name,
+ "configs" | "config_id" | "config_server_url" | "config_encryption_secret"
+ )
+}
+
+#[cfg(target_os = "windows")]
+fn enterprise_indexed_source_key_name(key_name: &str) -> Option {
+ if let Some(suffix) = enterprise_source_key_suffix(key_name, ENTERPRISE_CONFIG_ID_KEY_PREFIX) {
+ return Some(format!("config_id{suffix}"));
+ }
+
+ if let Some(suffix) = enterprise_source_key_suffix(key_name, ENTERPRISE_CONFIG_SERVER_URL_KEY_PREFIX) {
+ return Some(format!("config_server_url{suffix}"));
+ }
+
+ None
+}
+
+fn enterprise_source_key_suffix<'a>(key_name: &'a str, prefix: &str) -> Option<&'a str> {
+ key_name
+ .strip_prefix(prefix)
+ .and_then(normalize_enterprise_slot_suffix)
+}
+
+fn normalize_enterprise_slot_suffix(raw_suffix: &str) -> Option<&str> {
+ let suffix = raw_suffix.strip_prefix('_').unwrap_or(raw_suffix);
+ if is_enterprise_slot_suffix(suffix) {
+ Some(suffix)
+ } else {
+ None
+ }
+}
+
+fn is_enterprise_slot_suffix(suffix: &str) -> bool {
+ !suffix.is_empty()
+ && suffix.len() <= ENTERPRISE_CONFIG_SLOT_WIDTH
+ && suffix.chars().all(|c| c.is_ascii_digit())
+ && suffix.parse::().is_ok_and(|index| index <= ENTERPRISE_CONFIG_SLOT_MAX)
+}
+
+fn collect_enterprise_config_slots(values: &HashMap) -> Vec {
+ let mut slots = HashSet::new();
+ for key_name in values.keys() {
+ if let Some(suffix) = enterprise_source_key_suffix(key_name, ENTERPRISE_CONFIG_ID_KEY_PREFIX)
+ && is_enterprise_slot_suffix(suffix) {
+ slots.insert(String::from(suffix));
+ continue;
+ }
+
+ if let Some(suffix) = enterprise_source_key_suffix(key_name, ENTERPRISE_CONFIG_SERVER_URL_KEY_PREFIX)
+ && is_enterprise_slot_suffix(suffix) {
+ slots.insert(String::from(suffix));
+ }
+ }
+
+ let mut slots: Vec = slots.into_iter().collect();
+ slots.sort_by(|left, right| {
+ let left_index = left.parse::().unwrap_or(ENTERPRISE_CONFIG_SLOT_MAX);
+ let right_index = right.parse::().unwrap_or(ENTERPRISE_CONFIG_SLOT_MAX);
+
+ left_index
+ .cmp(&right_index)
+ .then_with(|| enterprise_slot_width_rank(left).cmp(&enterprise_slot_width_rank(right)))
+ .then_with(|| left.len().cmp(&right.len()))
+ .then_with(|| left.cmp(right))
+ });
+ slots
+}
+
+fn enterprise_slot_width_rank(suffix: &str) -> u8 {
+ if suffix.len() == ENTERPRISE_CONFIG_SLOT_WIDTH {
+ 0
+ } else {
+ 1
+ }
+}
+
+fn indexed_enterprise_source_value<'a, T: EnterpriseSourceValueAccess>(
+ values: &'a HashMap,
+ prefix: &str,
+ suffix: &str,
+) -> Option<&'a T> {
+ let separated_key = format!("{prefix}_{suffix}");
+ values
+ .get(&separated_key)
+ .or_else(|| values.get(&format!("{prefix}{suffix}")))
+}
+
+fn parse_enterprise_source_values(
source_name: &str,
- values: &HashMap,
+ values: &HashMap,
) -> EnterpriseSourceData {
let mut configs = Vec::new();
let mut seen_ids = HashSet::new();
- for index in 0..ENTERPRISE_CONFIG_SLOT_COUNT {
- let id_key = format!("config_id{index}");
- let server_url_key = format!("config_server_url{index}");
+ for suffix in collect_enterprise_config_slots(values) {
add_enterprise_config_pair(
source_name,
- &format!("indexed slot {index}"),
- values.get(&id_key).map(String::as_str),
- values.get(&server_url_key).map(String::as_str),
+ &format!("indexed slot {suffix}"),
+ indexed_enterprise_source_value(values, ENTERPRISE_CONFIG_ID_KEY_PREFIX, &suffix),
+ indexed_enterprise_source_value(values, ENTERPRISE_CONFIG_SERVER_URL_KEY_PREFIX, &suffix),
&mut configs,
&mut seen_ids,
);
@@ -544,7 +794,7 @@ fn parse_enterprise_source_values(
if let Some(combined) = values
.get("configs")
- .and_then(|value| normalize_enterprise_value(value))
+ .and_then(|value| normalize_enterprise_value(value.value()))
{
add_combined_enterprise_configs(source_name, &combined, &mut configs, &mut seen_ids);
}
@@ -552,15 +802,15 @@ fn parse_enterprise_source_values(
add_enterprise_config_pair(
source_name,
"legacy single configuration",
- values.get("config_id").map(String::as_str),
- values.get("config_server_url").map(String::as_str),
+ values.get("config_id"),
+ values.get("config_server_url"),
&mut configs,
&mut seen_ids,
);
let encryption_secret = values
.get("config_encryption_secret")
- .and_then(|value| normalize_enterprise_value(value))
+ .and_then(|value| normalize_enterprise_value(value.value()))
.unwrap_or_default();
EnterpriseSourceData {
@@ -572,26 +822,32 @@ fn parse_enterprise_source_values(
fn add_enterprise_config_pair(
source_name: &str,
- context: &str,
- raw_id: Option<&str>,
- raw_server_url: Option<&str>,
+ slot: &str,
+ raw_id: Option<&impl EnterpriseSourceValueAccess>,
+ raw_server_url: Option<&impl EnterpriseSourceValueAccess>,
configs: &mut Vec,
seen_ids: &mut HashSet,
) {
- let id = raw_id.and_then(normalize_enterprise_config_id);
- let server_url = raw_server_url.and_then(normalize_enterprise_value);
+ let id = raw_id.and_then(|value| normalize_enterprise_config_id(value.value()));
+ let server_url = raw_server_url.and_then(|value| normalize_enterprise_value(value.value()));
match (id, server_url) {
(Some(id), Some(server_url)) => {
if seen_ids.insert(id.clone()) {
- configs.push(EnterpriseConfig { id, server_url });
+ configs.push(EnterpriseConfig {
+ id,
+ server_url,
+ source: String::from(source_name),
+ source_detail: raw_id.map(|value| String::from(value.source_detail())).unwrap_or_default(),
+ slot: String::from(slot),
+ });
} else {
- info!("Ignoring duplicate enterprise configuration '{}' from {} in '{}'.", id, source_name, context);
+ info!("Ignoring duplicate enterprise configuration '{}' from {} in '{}'.", id, source_name, slot);
}
}
(Some(_), None) | (None, Some(_)) => {
- warn!("Ignoring incomplete enterprise configuration from {} in '{}'.", source_name, context);
+ warn!("Ignoring incomplete enterprise configuration from {} in '{}'.", source_name, slot);
}
(None, None) => {}
@@ -615,11 +871,13 @@ fn add_combined_enterprise_configs(
continue;
};
+ let id = EnterpriseSourceValue::new(String::from(raw_id), String::new());
+ let server_url = EnterpriseSourceValue::new(String::from(raw_server_url), String::new());
add_enterprise_config_pair(
source_name,
&format!("combined legacy entry {}", index + 1),
- Some(raw_id),
- Some(raw_server_url),
+ Some(&id),
+ Some(&server_url),
configs,
seen_ids,
);
@@ -642,10 +900,11 @@ fn normalize_enterprise_config_id(value: &str) -> Option {
#[cfg(test)]
mod tests {
use super::{
+ enterprise_environment_key_name, enterprise_policy_file_slot_suffix,
linux_policy_directories_from_xdg, load_policy_values_from_directories,
normalize_locale_tag, parse_enterprise_source_values,
select_effective_enterprise_config_source, select_effective_enterprise_secret_source,
- EnterpriseConfig, EnterpriseSourceData,
+ EnterpriseConfig, EnterpriseSourceData, EnterpriseSourceValue, EnterpriseSourceValues,
};
use std::collections::HashMap;
use std::fs;
@@ -656,6 +915,30 @@ mod tests {
const TEST_ID_B: &str = "a1b2c3d4-e5f6-7890-abcd-ef1234567890";
const TEST_ID_C: &str = "11111111-2222-3333-4444-555555555555";
+ fn enterprise_config(
+ id: &str,
+ server_url: &str,
+ source: &str,
+ source_detail: &str,
+ slot: &str,
+ ) -> EnterpriseConfig {
+ EnterpriseConfig {
+ id: String::from(id),
+ server_url: String::from(server_url),
+ source: String::from(source),
+ source_detail: String::from(source_detail),
+ slot: String::from(slot),
+ }
+ }
+
+ fn policy_path(path: PathBuf) -> String {
+ path
+ .canonicalize()
+ .unwrap_or(path)
+ .to_string_lossy()
+ .into_owned()
+ }
+
#[test]
fn normalize_locale_tag_supports_common_linux_formats() {
assert_eq!(
@@ -707,18 +990,9 @@ mod tests {
assert_eq!(
source.configs,
vec![
- EnterpriseConfig {
- id: String::from("9072b77d-ca81-40da-be6a-861da525ef7b"),
- server_url: String::from("https://indexed.example.org"),
- },
- EnterpriseConfig {
- id: String::from(TEST_ID_B),
- server_url: String::from("https://combined.example.org"),
- },
- EnterpriseConfig {
- id: String::from(TEST_ID_C),
- server_url: String::from("https://legacy.example.org"),
- },
+ enterprise_config("9072b77d-ca81-40da-be6a-861da525ef7b", "https://indexed.example.org", "test", "", "indexed slot 0"),
+ enterprise_config(TEST_ID_B, "https://combined.example.org", "test", "", "combined legacy entry 2"),
+ enterprise_config(TEST_ID_C, "https://legacy.example.org", "test", "", "legacy single configuration"),
]
);
assert_eq!(source.encryption_secret, "secret");
@@ -743,35 +1017,164 @@ mod tests {
assert_eq!(
source.configs,
vec![
- EnterpriseConfig {
- id: String::from("9072b77d-ca81-40da-be6a-861da525ef7b"),
- server_url: String::from("https://slot0.example.org"),
- },
- EnterpriseConfig {
- id: String::from(TEST_ID_B),
- server_url: String::from("https://slot4.example.org"),
- },
+ enterprise_config("9072b77d-ca81-40da-be6a-861da525ef7b", "https://slot0.example.org", "test", "", "indexed slot 0"),
+ enterprise_config(TEST_ID_B, "https://slot4.example.org", "test", "", "indexed slot 4"),
]
);
}
+ #[test]
+ fn parse_enterprise_source_values_supports_padded_and_high_indexed_slots() {
+ let mut values = HashMap::new();
+ values.insert(String::from("config_id_00000"), String::from(TEST_ID_A));
+ values.insert(
+ String::from("config_server_url_00000"),
+ String::from("https://slot0.example.org"),
+ );
+ values.insert(String::from("config_id_10503"), String::from(TEST_ID_B));
+ values.insert(
+ String::from("config_server_url_10503"),
+ String::from("https://slot10503.example.org"),
+ );
+
+ let source = parse_enterprise_source_values("test", &values);
+
+ assert_eq!(
+ source.configs,
+ vec![
+ enterprise_config("9072b77d-ca81-40da-be6a-861da525ef7b", "https://slot0.example.org", "test", "", "indexed slot 00000"),
+ enterprise_config(TEST_ID_B, "https://slot10503.example.org", "test", "", "indexed slot 10503"),
+ ]
+ );
+ }
+
+ #[test]
+ fn parse_enterprise_source_values_treats_slot_widths_as_distinct_slots() {
+ let mut values = HashMap::new();
+ values.insert(String::from("config_id_00001"), String::from(TEST_ID_A));
+ values.insert(
+ String::from("config_server_url_00001"),
+ String::from("https://padded.example.org"),
+ );
+ values.insert(String::from("config_id1"), String::from(TEST_ID_B));
+ values.insert(
+ String::from("config_server_url1"),
+ String::from("https://legacy-slot.example.org"),
+ );
+
+ let source = parse_enterprise_source_values("test", &values);
+
+ assert_eq!(
+ source.configs,
+ vec![
+ enterprise_config("9072b77d-ca81-40da-be6a-861da525ef7b", "https://padded.example.org", "test", "", "indexed slot 00001"),
+ enterprise_config(TEST_ID_B, "https://legacy-slot.example.org", "test", "", "indexed slot 1"),
+ ]
+ );
+ }
+
+ #[test]
+ fn parse_enterprise_source_values_ignores_invalid_slot_suffixes() {
+ let mut values = HashMap::new();
+ values.insert(String::from("config_id_99999"), String::from(TEST_ID_A));
+ values.insert(
+ String::from("config_server_url_99999"),
+ String::from("https://valid.example.org"),
+ );
+ values.insert(String::from("config_id_100000"), String::from(TEST_ID_B));
+ values.insert(
+ String::from("config_server_url_100000"),
+ String::from("https://too-high.example.org"),
+ );
+ values.insert(String::from("config_id_abc"), String::from(TEST_ID_C));
+ values.insert(
+ String::from("config_server_url_abc"),
+ String::from("https://letters.example.org"),
+ );
+
+ let source = parse_enterprise_source_values("test", &values);
+
+ assert_eq!(
+ source.configs,
+ vec![enterprise_config("9072b77d-ca81-40da-be6a-861da525ef7b", "https://valid.example.org", "test", "", "indexed slot 99999")]
+ );
+ }
+
+ #[test]
+ fn enterprise_environment_key_name_maps_indexed_and_legacy_names() {
+ assert_eq!(
+ enterprise_environment_key_name("MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_ID_10503"),
+ Some(String::from("config_id10503"))
+ );
+ assert_eq!(
+ enterprise_environment_key_name("MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_SERVER_URL_00000"),
+ Some(String::from("config_server_url00000"))
+ );
+ assert_eq!(
+ enterprise_environment_key_name("MINDWORK_AI_STUDIO_ENTERPRISE_CONFIGS"),
+ Some(String::from("configs"))
+ );
+ assert_eq!(
+ enterprise_environment_key_name("MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_ID_100000"),
+ None
+ );
+ }
+
+ #[test]
+ fn parse_enterprise_source_values_keeps_environment_id_variable_as_source_detail() {
+ let mut values = EnterpriseSourceValues::new();
+ values.insert(
+ String::from("config_id00000"),
+ EnterpriseSourceValue::new(
+ String::from(TEST_ID_A),
+ String::from("MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_ID_00000"),
+ ),
+ );
+ values.insert(
+ String::from("config_server_url00000"),
+ EnterpriseSourceValue::new(String::from("https://env.example.org"), String::new()),
+ );
+
+ let source = parse_enterprise_source_values("environment variables", &values);
+
+ assert_eq!(
+ source.configs,
+ vec![enterprise_config(
+ "9072b77d-ca81-40da-be6a-861da525ef7b",
+ "https://env.example.org",
+ "environment variables",
+ "MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_ID_00000",
+ "indexed slot 00000"
+ )]
+ );
+ }
+
+ #[test]
+ fn enterprise_policy_file_slot_suffix_accepts_valid_slot_file_names() {
+ assert_eq!(enterprise_policy_file_slot_suffix("config0.yaml"), Some("0"));
+ assert_eq!(
+ enterprise_policy_file_slot_suffix("config_00000.yaml"),
+ Some("00000")
+ );
+ assert_eq!(
+ enterprise_policy_file_slot_suffix("config_10503.yaml"),
+ Some("10503")
+ );
+ assert_eq!(enterprise_policy_file_slot_suffix("config_100000.yaml"), None);
+ assert_eq!(enterprise_policy_file_slot_suffix("config_abc.yaml"), None);
+ }
+
#[test]
fn select_effective_enterprise_config_source_uses_first_source_with_configs_only() {
let selected = select_effective_enterprise_config_source(vec![
EnterpriseSourceData {
source_name: String::from("registry"),
- configs: vec![EnterpriseConfig {
- id: TEST_ID_A.to_lowercase(),
- server_url: String::from("https://registry.example.org"),
- }],
+ configs: vec![enterprise_config(&TEST_ID_A.to_lowercase(), "https://registry.example.org", "registry", "", "indexed slot 0")],
encryption_secret: String::new(),
},
EnterpriseSourceData {
source_name: String::from("environment"),
- configs: vec![EnterpriseConfig {
- id: String::from(TEST_ID_B),
- server_url: String::from("https://env.example.org"),
- }],
+ configs: vec![enterprise_config(TEST_ID_B, "https://env.example.org", "environment", "", "indexed slot 0")],
encryption_secret: String::from("ENV-SECRET"),
},
]);
@@ -791,10 +1194,7 @@ mod tests {
},
EnterpriseSourceData {
source_name: String::from("environment"),
- configs: vec![EnterpriseConfig {
- id: String::from(TEST_ID_B),
- server_url: String::from("https://env.example.org"),
- }],
+ configs: vec![enterprise_config(TEST_ID_B, "https://env.example.org", "environment", "", "indexed slot 0")],
encryption_secret: String::new(),
},
]);
@@ -809,10 +1209,7 @@ mod tests {
let selected = select_effective_enterprise_secret_source(vec![
EnterpriseSourceData {
source_name: String::from("registry"),
- configs: vec![EnterpriseConfig {
- id: TEST_ID_A.to_lowercase(),
- server_url: String::from("https://registry.example.org"),
- }],
+ configs: vec![enterprise_config(&TEST_ID_A.to_lowercase(), "https://registry.example.org", "registry", "", "indexed slot 0")],
encryption_secret: String::new(),
},
EnterpriseSourceData {
@@ -918,19 +1315,19 @@ mod tests {
]);
assert_eq!(
- values.get("config_id0").map(String::as_str),
+ values.get("config_id0").map(|value| value.value.as_str()),
Some("9072b77d-ca81-40da-be6a-861da525ef7b")
);
assert_eq!(
- values.get("config_server_url0").map(String::as_str),
+ values.get("config_server_url0").map(|value| value.value.as_str()),
Some("https://org.example.org")
);
assert_eq!(
- values.get("config_id1").map(String::as_str),
+ values.get("config_id1").map(|value| value.value.as_str()),
Some("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb")
);
assert_eq!(
- values.get("config_encryption_secret").map(String::as_str),
+ values.get("config_encryption_secret").map(|value| value.value.as_str()),
Some("SECRET-A")
);
}
@@ -956,14 +1353,40 @@ mod tests {
assert_eq!(
source.configs,
vec![
- EnterpriseConfig {
- id: String::from("9072b77d-ca81-40da-be6a-861da525ef7b"),
- server_url: String::from("https://slot0.example.org"),
- },
- EnterpriseConfig {
- id: String::from(TEST_ID_B),
- server_url: String::from("https://slot4.example.org"),
- },
+ enterprise_config("9072b77d-ca81-40da-be6a-861da525ef7b", "https://slot0.example.org", "policy files", &policy_path(directory.path().join("config0.yaml")), "indexed slot 0"),
+ enterprise_config(TEST_ID_B, "https://slot4.example.org", "policy files", &policy_path(directory.path().join("config4.yaml")), "indexed slot 4"),
+ ]
+ );
+ }
+
+ #[test]
+ fn load_policy_values_from_directories_supports_padded_and_high_policy_slots() {
+ let directory = tempdir().unwrap();
+
+ fs::write(
+ directory.path().join("config_00000.yaml"),
+ "id: \"9072b77d-ca81-40da-be6a-861da525ef7b\"\nserver_url: \"https://slot0.example.org\"",
+ )
+ .unwrap();
+ fs::write(
+ directory.path().join("config_10503.yaml"),
+ "id: \"a1b2c3d4-e5f6-7890-abcd-ef1234567890\"\nserver_url: \"https://slot10503.example.org\"",
+ )
+ .unwrap();
+ fs::write(
+ directory.path().join("config_100000.yaml"),
+ "id: \"11111111-2222-3333-4444-555555555555\"\nserver_url: \"https://ignored.example.org\"",
+ )
+ .unwrap();
+
+ let values = load_policy_values_from_directories(&[directory.path().to_path_buf()]);
+ let source = parse_enterprise_source_values("policy files", &values);
+
+ assert_eq!(
+ source.configs,
+ vec![
+ enterprise_config("9072b77d-ca81-40da-be6a-861da525ef7b", "https://slot0.example.org", "policy files", &policy_path(directory.path().join("config_00000.yaml")), "indexed slot 00000"),
+ enterprise_config(TEST_ID_B, "https://slot10503.example.org", "policy files", &policy_path(directory.path().join("config_10503.yaml")), "indexed slot 10503"),
]
);
}