From 34657b4820659eb81a866c89a683ece207349483 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Thu, 28 May 2026 21:33:05 +0200 Subject: [PATCH 1/3] Added support for up to 100,000 enterprise configuration slots --- .../wwwroot/changelog/v26.6.1.md | 1 + documentation/Enterprise IT.md | 34 +- runtime/src/environment.rs | 416 ++++++++++++++++-- 3 files changed, 397 insertions(+), 54 deletions(-) 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 7e4a82af8..a445d60eb 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,2 @@ # v26.6.1, build 241 (2026-06-xx xx:xx UTC) +- Added support for up to 100,000 enterprise configuration slots, using fixed-width slot names such as `config00000` while keeping the existing first ten slot names compatible. diff --git a/documentation/Enterprise IT.md b/documentation/Enterprise IT.md index 221a24db6..10703ead6 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_id00000` to `config_id99999` together with `config_server_url00000` to `config_server_url99999` +- Environment variables `MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_ID00000` to `MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_ID99999` together with `MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_SERVER_URL00000` to `MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_SERVER_URL99999` +- Policy files `config00000.yaml` to `config99999.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` are still supported. AI Studio also accepts other numeric slot suffixes with up to five digits. Slot suffixes are matched exactly, so `config_id1`, `config_id01`, and `config_id00001` are treated as separate slots. Use the five-digit format 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_id00000` = `9072b77d-ca81-40da-be6a-861da525ef7b` +- `config_server_url00000` = `https://intranet.example.org/ai-studio/configuration` +- `config_id10503` = `a1b2c3d4-e5f6-7890-abcd-ef1234567890` +- `config_server_url10503` = `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` +- `config00000.yaml` +- `config00001.yaml` - ... -- `config9.yaml` +- `config99999.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_ID00000=9072b77d-ca81-40da-be6a-861da525ef7b +MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_SERVER_URL00000=https://intranet.example.org/ai-studio/configuration +MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_ID10503=a1b2c3d4-e5f6-7890-abcd-ef1234567890 +MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_SERVER_URL10503=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 3f8dd43c2..b4aa8d604 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(); @@ -304,32 +313,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; + }; + + match String::try_from(value) { + Ok(value) => { + values.insert(source_key_name, value); + }, + + Err(error) => { + warn!(r"Could not read enterprise registry value 'HKEY_CURRENT_USER\{}\{}' as string: {}.", ENTERPRISE_REGISTRY_KEY_PATH, key_name, error); + }, + } + } + }, - for key_name in [ - "configs", - "config_id", - "config_server_url", - "config_encryption_secret", - ] { - insert_registry_value(&mut values, &key, key_name); + 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 { @@ -343,23 +360,72 @@ 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}")); + for (env_name, value) in env::vars() { + if let Some(source_key_name) = enterprise_environment_key_name(&env_name) { + values.insert(source_key_name, value); + } } - 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_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) + && is_enterprise_slot_suffix(suffix) { + return Some(format!("config_id{suffix}")); + } + + if let Some(suffix) = enterprise_env_key_suffix(env_name, ENTERPRISE_ENV_CONFIG_SERVER_URL_PREFIX) + && is_enterprise_slot_suffix(suffix) { + 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) { + Some(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) } #[cfg(target_os = "windows")] @@ -410,15 +476,40 @@ fn load_policy_values_from_directories(directories: &[PathBuf]) -> HashMap 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) { 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); } 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); } } } @@ -433,6 +524,18 @@ fn load_policy_values_from_directories(directories: &[PathBuf]) -> HashMap Option<&str> { + let suffix = file_name + .strip_prefix("config")? + .strip_suffix(".yaml")?; + + if is_enterprise_slot_suffix(suffix) { + Some(suffix) + } else { + None + } +} + fn read_policy_yaml_mapping(path: &Path) -> Option> { if !path.exists() { return None; @@ -522,6 +625,77 @@ fn insert_first_non_empty_value(values: &mut HashMap, key: &str, } } +#[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) + && is_enterprise_slot_suffix(suffix) { + return Some(format!("config_id{suffix}")); + } + + if let Some(suffix) = enterprise_source_key_suffix(key_name, ENTERPRISE_CONFIG_SERVER_URL_KEY_PREFIX) + && is_enterprise_slot_suffix(suffix) { + 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) +} + +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 parse_enterprise_source_values( source_name: &str, values: &HashMap, @@ -529,12 +703,12 @@ fn parse_enterprise_source_values( 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) { + let id_key = format!("config_id{suffix}"); + let server_url_key = format!("config_server_url{suffix}"); add_enterprise_config_pair( source_name, - &format!("indexed slot {index}"), + &format!("indexed slot {suffix}"), values.get(&id_key).map(String::as_str), values.get(&server_url_key).map(String::as_str), &mut configs, @@ -642,6 +816,7 @@ 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, @@ -755,6 +930,133 @@ mod tests { ); } + #[test] + fn parse_enterprise_source_values_supports_padded_and_high_indexed_slots() { + let mut values = HashMap::new(); + values.insert(String::from("config_id00000"), String::from(TEST_ID_A)); + values.insert( + String::from("config_server_url00000"), + String::from("https://slot0.example.org"), + ); + values.insert(String::from("config_id10503"), String::from(TEST_ID_B)); + values.insert( + String::from("config_server_url10503"), + String::from("https://slot10503.example.org"), + ); + + let source = parse_enterprise_source_values("test", &values); + + 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://slot10503.example.org"), + }, + ] + ); + } + + #[test] + fn parse_enterprise_source_values_treats_slot_widths_as_distinct_slots() { + let mut values = HashMap::new(); + values.insert(String::from("config_id00001"), String::from(TEST_ID_A)); + values.insert( + String::from("config_server_url00001"), + 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![ + EnterpriseConfig { + id: String::from("9072b77d-ca81-40da-be6a-861da525ef7b"), + server_url: String::from("https://padded.example.org"), + }, + EnterpriseConfig { + id: String::from(TEST_ID_B), + server_url: String::from("https://legacy-slot.example.org"), + }, + ] + ); + } + + #[test] + fn parse_enterprise_source_values_ignores_invalid_slot_suffixes() { + let mut values = HashMap::new(); + values.insert(String::from("config_id99999"), String::from(TEST_ID_A)); + values.insert( + String::from("config_server_url99999"), + String::from("https://valid.example.org"), + ); + values.insert(String::from("config_id100000"), String::from(TEST_ID_B)); + values.insert( + String::from("config_server_url100000"), + String::from("https://too-high.example.org"), + ); + values.insert(String::from("config_idabc"), String::from(TEST_ID_C)); + values.insert( + String::from("config_server_urlabc"), + String::from("https://letters.example.org"), + ); + + let source = parse_enterprise_source_values("test", &values); + + assert_eq!( + source.configs, + vec![EnterpriseConfig { + id: String::from("9072b77d-ca81-40da-be6a-861da525ef7b"), + server_url: String::from("https://valid.example.org"), + }] + ); + } + + #[test] + fn enterprise_environment_key_name_maps_indexed_and_legacy_names() { + assert_eq!( + enterprise_environment_key_name("MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_ID10503"), + Some(String::from("config_id10503")) + ); + assert_eq!( + enterprise_environment_key_name("MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_SERVER_URL00000"), + 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_ID100000"), + None + ); + } + + #[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("config00000.yaml"), + Some("00000") + ); + assert_eq!( + enterprise_policy_file_slot_suffix("config10503.yaml"), + Some("10503") + ); + assert_eq!(enterprise_policy_file_slot_suffix("config100000.yaml"), None); + assert_eq!(enterprise_policy_file_slot_suffix("configabc.yaml"), None); + } + #[test] fn select_effective_enterprise_config_source_uses_first_source_with_configs_only() { let selected = select_effective_enterprise_config_source(vec![ @@ -968,6 +1270,44 @@ mod tests { ); } + #[test] + fn load_policy_values_from_directories_supports_padded_and_high_policy_slots() { + let directory = tempdir().unwrap(); + + fs::write( + directory.path().join("config00000.yaml"), + "id: \"9072b77d-ca81-40da-be6a-861da525ef7b\"\nserver_url: \"https://slot0.example.org\"", + ) + .unwrap(); + fs::write( + directory.path().join("config10503.yaml"), + "id: \"a1b2c3d4-e5f6-7890-abcd-ef1234567890\"\nserver_url: \"https://slot10503.example.org\"", + ) + .unwrap(); + fs::write( + directory.path().join("config100000.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![ + 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://slot10503.example.org"), + }, + ] + ); + } + #[test] fn load_policy_values_from_directories_supports_secret_only_policy_files() { let directory = tempdir().unwrap(); From c32ecfcc1b476f8a0fa76a3c26d202e85844546d Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Thu, 28 May 2026 21:44:48 +0200 Subject: [PATCH 2/3] Improved readability --- .../wwwroot/changelog/v26.6.1.md | 2 +- documentation/Enterprise IT.md | 30 +++--- runtime/src/environment.rs | 99 +++++++++++-------- 3 files changed, 73 insertions(+), 58 deletions(-) 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 a445d60eb..f7e43b94f 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v26.6.1.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v26.6.1.md @@ -1,2 +1,2 @@ # v26.6.1, build 241 (2026-06-xx xx:xx UTC) -- Added support for up to 100,000 enterprise configuration slots, using fixed-width slot names such as `config00000` while keeping the existing first ten slot names compatible. +- Added support for up to 100,000 enterprise configuration slots, using fixed-width slot names such as `config_00000` while keeping the existing first ten slot names compatible. diff --git a/documentation/Enterprise IT.md b/documentation/Enterprise IT.md index 10703ead6..168a96d50 100644 --- a/documentation/Enterprise IT.md +++ b/documentation/Enterprise IT.md @@ -39,15 +39,15 @@ AI Studio supports loading multiple enterprise configurations simultaneously. Th The preferred format is a fixed set of indexed pairs: -- Registry values `config_id00000` to `config_id99999` together with `config_server_url00000` to `config_server_url99999` -- Environment variables `MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_ID00000` to `MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_ID99999` together with `MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_SERVER_URL00000` to `MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_SERVER_URL99999` -- Policy files `config00000.yaml` to `config99999.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 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 `00000`, then `00001`, and so on up to `99999`. -For backwards compatibility, the older slot names `0` to `9` are still supported. AI Studio also accepts other numeric slot suffixes with up to five digits. Slot suffixes are matched exactly, so `config_id1`, `config_id01`, and `config_id00001` are treated as separate slots. Use the five-digit format for new deployments. +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 @@ -57,10 +57,10 @@ The Windows registry path is: Example values: -- `config_id00000` = `9072b77d-ca81-40da-be6a-861da525ef7b` -- `config_server_url00000` = `https://intranet.example.org/ai-studio/configuration` -- `config_id10503` = `a1b2c3d4-e5f6-7890-abcd-ef1234567890` -- `config_server_url10503` = `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. @@ -87,10 +87,10 @@ The directories from `$XDG_CONFIG_DIRS` are processed in order. Configuration files: -- `config00000.yaml` -- `config00001.yaml` +- `config_00000.yaml` +- `config_00001.yaml` - ... -- `config99999.yaml` +- `config_99999.yaml` Each configuration file contains one configuration ID and one server URL: @@ -112,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_ID00000=9072b77d-ca81-40da-be6a-861da525ef7b -MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_SERVER_URL00000=https://intranet.example.org/ai-studio/configuration -MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_ID10503=a1b2c3d4-e5f6-7890-abcd-ef1234567890 -MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_SERVER_URL10503=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 b4aa8d604..6933f5d72 100644 --- a/runtime/src/environment.rs +++ b/runtime/src/environment.rs @@ -386,13 +386,11 @@ fn enterprise_environment_key_name(env_name: &str) -> Option { return Some(String::from("config_encryption_secret")); } - if let Some(suffix) = enterprise_env_key_suffix(env_name, ENTERPRISE_ENV_CONFIG_ID_PREFIX) - && is_enterprise_slot_suffix(suffix) { + 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) - && is_enterprise_slot_suffix(suffix) { + if let Some(suffix) = enterprise_env_key_suffix(env_name, ENTERPRISE_ENV_CONFIG_SERVER_URL_PREFIX) { return Some(format!("config_server_url{suffix}")); } @@ -417,7 +415,7 @@ fn enterprise_env_key_suffix<'a>(env_name: &'a str, prefix: &str) -> Option<&'a let (raw_prefix, suffix) = env_name.split_at(prefix.len()); if raw_prefix.eq_ignore_ascii_case(prefix) { - Some(suffix) + normalize_enterprise_slot_suffix(suffix) } else { None } @@ -425,7 +423,9 @@ fn enterprise_env_key_suffix<'a>(env_name: &'a str, prefix: &str) -> Option<&'a #[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) + env_name + .strip_prefix(prefix) + .and_then(normalize_enterprise_slot_suffix) } #[cfg(target_os = "windows")] @@ -529,11 +529,7 @@ fn enterprise_policy_file_slot_suffix(file_name: &str) -> Option<&str> { .strip_prefix("config")? .strip_suffix(".yaml")?; - if is_enterprise_slot_suffix(suffix) { - Some(suffix) - } else { - None - } + normalize_enterprise_slot_suffix(suffix) } fn read_policy_yaml_mapping(path: &Path) -> Option> { @@ -635,13 +631,11 @@ fn is_legacy_enterprise_source_key(key_name: &str) -> bool { #[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) - && is_enterprise_slot_suffix(suffix) { + 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) - && is_enterprise_slot_suffix(suffix) { + if let Some(suffix) = enterprise_source_key_suffix(key_name, ENTERPRISE_CONFIG_SERVER_URL_KEY_PREFIX) { return Some(format!("config_server_url{suffix}")); } @@ -649,7 +643,18 @@ fn enterprise_indexed_source_key_name(key_name: &str) -> Option { } fn enterprise_source_key_suffix<'a>(key_name: &'a str, prefix: &str) -> Option<&'a str> { - key_name.strip_prefix(prefix) + 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 { @@ -696,6 +701,18 @@ fn enterprise_slot_width_rank(suffix: &str) -> u8 { } } +fn indexed_enterprise_source_value<'a>( + values: &'a HashMap, + prefix: &str, + suffix: &str, +) -> Option<&'a str> { + let separated_key = format!("{prefix}_{suffix}"); + values + .get(&separated_key) + .or_else(|| values.get(&format!("{prefix}{suffix}"))) + .map(String::as_str) +} + fn parse_enterprise_source_values( source_name: &str, values: &HashMap, @@ -704,13 +721,11 @@ fn parse_enterprise_source_values( let mut seen_ids = HashSet::new(); for suffix in collect_enterprise_config_slots(values) { - let id_key = format!("config_id{suffix}"); - let server_url_key = format!("config_server_url{suffix}"); add_enterprise_config_pair( source_name, &format!("indexed slot {suffix}"), - values.get(&id_key).map(String::as_str), - values.get(&server_url_key).map(String::as_str), + 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, ); @@ -933,14 +948,14 @@ mod tests { #[test] fn parse_enterprise_source_values_supports_padded_and_high_indexed_slots() { let mut values = HashMap::new(); - values.insert(String::from("config_id00000"), String::from(TEST_ID_A)); + values.insert(String::from("config_id_00000"), String::from(TEST_ID_A)); values.insert( - String::from("config_server_url00000"), + String::from("config_server_url_00000"), String::from("https://slot0.example.org"), ); - values.insert(String::from("config_id10503"), String::from(TEST_ID_B)); + values.insert(String::from("config_id_10503"), String::from(TEST_ID_B)); values.insert( - String::from("config_server_url10503"), + String::from("config_server_url_10503"), String::from("https://slot10503.example.org"), ); @@ -964,9 +979,9 @@ mod tests { #[test] fn parse_enterprise_source_values_treats_slot_widths_as_distinct_slots() { let mut values = HashMap::new(); - values.insert(String::from("config_id00001"), String::from(TEST_ID_A)); + values.insert(String::from("config_id_00001"), String::from(TEST_ID_A)); values.insert( - String::from("config_server_url00001"), + String::from("config_server_url_00001"), String::from("https://padded.example.org"), ); values.insert(String::from("config_id1"), String::from(TEST_ID_B)); @@ -995,19 +1010,19 @@ mod tests { #[test] fn parse_enterprise_source_values_ignores_invalid_slot_suffixes() { let mut values = HashMap::new(); - values.insert(String::from("config_id99999"), String::from(TEST_ID_A)); + values.insert(String::from("config_id_99999"), String::from(TEST_ID_A)); values.insert( - String::from("config_server_url99999"), + String::from("config_server_url_99999"), String::from("https://valid.example.org"), ); - values.insert(String::from("config_id100000"), String::from(TEST_ID_B)); + values.insert(String::from("config_id_100000"), String::from(TEST_ID_B)); values.insert( - String::from("config_server_url100000"), + String::from("config_server_url_100000"), String::from("https://too-high.example.org"), ); - values.insert(String::from("config_idabc"), String::from(TEST_ID_C)); + values.insert(String::from("config_id_abc"), String::from(TEST_ID_C)); values.insert( - String::from("config_server_urlabc"), + String::from("config_server_url_abc"), String::from("https://letters.example.org"), ); @@ -1025,11 +1040,11 @@ mod tests { #[test] fn enterprise_environment_key_name_maps_indexed_and_legacy_names() { assert_eq!( - enterprise_environment_key_name("MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_ID10503"), + 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_URL00000"), + enterprise_environment_key_name("MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_SERVER_URL_00000"), Some(String::from("config_server_url00000")) ); assert_eq!( @@ -1037,7 +1052,7 @@ mod tests { Some(String::from("configs")) ); assert_eq!( - enterprise_environment_key_name("MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_ID100000"), + enterprise_environment_key_name("MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_ID_100000"), None ); } @@ -1046,15 +1061,15 @@ mod tests { 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("config00000.yaml"), + enterprise_policy_file_slot_suffix("config_00000.yaml"), Some("00000") ); assert_eq!( - enterprise_policy_file_slot_suffix("config10503.yaml"), + enterprise_policy_file_slot_suffix("config_10503.yaml"), Some("10503") ); - assert_eq!(enterprise_policy_file_slot_suffix("config100000.yaml"), None); - assert_eq!(enterprise_policy_file_slot_suffix("configabc.yaml"), None); + assert_eq!(enterprise_policy_file_slot_suffix("config_100000.yaml"), None); + assert_eq!(enterprise_policy_file_slot_suffix("config_abc.yaml"), None); } #[test] @@ -1275,17 +1290,17 @@ mod tests { let directory = tempdir().unwrap(); fs::write( - directory.path().join("config00000.yaml"), + directory.path().join("config_00000.yaml"), "id: \"9072b77d-ca81-40da-be6a-861da525ef7b\"\nserver_url: \"https://slot0.example.org\"", ) .unwrap(); fs::write( - directory.path().join("config10503.yaml"), + directory.path().join("config_10503.yaml"), "id: \"a1b2c3d4-e5f6-7890-abcd-ef1234567890\"\nserver_url: \"https://slot10503.example.org\"", ) .unwrap(); fs::write( - directory.path().join("config100000.yaml"), + directory.path().join("config_100000.yaml"), "id: \"11111111-2222-3333-4444-555555555555\"\nserver_url: \"https://ignored.example.org\"", ) .unwrap(); From 39d77d53d996a6219e66336dd2d91dfe53d356dc Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sun, 31 May 2026 12:02:26 +0200 Subject: [PATCH 3/3] Improved the enterprise configuration details on the information page --- .../Assistants/I18N/allTexts.lua | 18 ++ .../Pages/Information.razor | 45 +-- .../Pages/Information.razor.cs | 49 +++ .../plugin.lua | 18 ++ .../plugin.lua | 18 ++ .../Tools/EnterpriseEnvironment.cs | 2 +- .../Tools/Rust/EnterpriseConfig.cs | 2 +- .../Services/EnterpriseEnvironmentService.cs | 5 +- .../Tools/Services/RustService.Enterprise.cs | 2 +- .../wwwroot/changelog/v26.6.1.md | 3 +- runtime/src/environment.rs | 286 +++++++++++------- 11 files changed, 292 insertions(+), 156 deletions(-) diff --git a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua index 7020549e2..bfca8ca00 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 13f2e941a..ef24db6b2 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 9f7250ac3..9ac8b800e 100644 --- a/app/MindWork AI Studio/Pages/Information.razor.cs +++ b/app/MindWork AI Studio/Pages/Information.razor.cs @@ -324,6 +324,55 @@ await this.InvokeAsync(async () => ?? 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 eba11f381..70d999dd4 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 01e80406a..59f951c31 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 952ec3b29..abdffd4eb 100644 --- a/app/MindWork AI Studio/Tools/EnterpriseEnvironment.cs +++ b/app/MindWork AI Studio/Tools/EnterpriseEnvironment.cs @@ -2,7 +2,7 @@ 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 bc6fb15eb..197b61439 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 6db55a6c5..90e8606b0 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 BuildNormalizedSnapshot(IEnum .Select(environment => 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 d78567f41..f11556450 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 async Task> EnterpriseEnvConfigs() 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 f7e43b94f..7f2861230 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v26.6.1.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v26.6.1.md @@ -1,2 +1,3 @@ # v26.6.1, build 241 (2026-06-xx xx:xx UTC) -- Added support for up to 100,000 enterprise configuration slots, using fixed-width slot names such as `config_00000` while keeping the existing first ten slot names compatible. +- 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/runtime/src/environment.rs b/runtime/src/environment.rs index 6933f5d72..989153cd0 100644 --- a/runtime/src/environment.rs +++ b/runtime/src/environment.rs @@ -196,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, @@ -301,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(_) => { @@ -322,7 +367,7 @@ fn load_registry_enterprise_source() -> EnterpriseSourceData { match String::try_from(value) { Ok(value) => { - values.insert(source_key_name, value); + values.insert(source_key_name, EnterpriseSourceValue::new(value, String::new())); }, Err(error) => { @@ -359,16 +404,26 @@ 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(); + let mut values = EnterpriseSourceValues::new(); for (env_name, value) in env::vars() { if let Some(source_key_name) = enterprise_environment_key_name(&env_name) { - values.insert(source_key_name, value); + let source_detail = enterprise_environment_source_detail(&source_key_name, &env_name); + values.insert(source_key_name, EnterpriseSourceValue::new(value, source_detail)); } } parse_enterprise_source_values("environment variables", &values) } +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")); @@ -472,8 +527,8 @@ 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()); let entries = match fs::read_dir(directory) { @@ -504,12 +559,17 @@ fn load_policy_values_from_directories(directories: &[PathBuf]) -> HashMap HashMap 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))); } } @@ -664,7 +726,7 @@ fn is_enterprise_slot_suffix(suffix: &str) -> bool { && suffix.parse::().is_ok_and(|index| index <= ENTERPRISE_CONFIG_SLOT_MAX) } -fn collect_enterprise_config_slots(values: &HashMap) -> Vec { +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) @@ -701,21 +763,20 @@ fn enterprise_slot_width_rank(suffix: &str) -> u8 { } } -fn indexed_enterprise_source_value<'a>( - values: &'a HashMap, +fn indexed_enterprise_source_value<'a, T: EnterpriseSourceValueAccess>( + values: &'a HashMap, prefix: &str, suffix: &str, -) -> Option<&'a str> { +) -> Option<&'a T> { let separated_key = format!("{prefix}_{suffix}"); values .get(&separated_key) .or_else(|| values.get(&format!("{prefix}{suffix}"))) - .map(String::as_str) } -fn parse_enterprise_source_values( +fn parse_enterprise_source_values( source_name: &str, - values: &HashMap, + values: &HashMap, ) -> EnterpriseSourceData { let mut configs = Vec::new(); let mut seen_ids = HashSet::new(); @@ -733,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); } @@ -741,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 { @@ -761,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) => {} @@ -804,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, ); @@ -835,7 +904,7 @@ mod tests { 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; @@ -846,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!( @@ -897,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"); @@ -933,14 +1017,8 @@ 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"), ] ); } @@ -964,14 +1042,8 @@ 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://slot10503.example.org"), - }, + 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"), ] ); } @@ -995,14 +1067,8 @@ mod tests { assert_eq!( source.configs, vec![ - EnterpriseConfig { - id: String::from("9072b77d-ca81-40da-be6a-861da525ef7b"), - server_url: String::from("https://padded.example.org"), - }, - EnterpriseConfig { - id: String::from(TEST_ID_B), - server_url: String::from("https://legacy-slot.example.org"), - }, + 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"), ] ); } @@ -1030,10 +1096,7 @@ mod tests { assert_eq!( source.configs, - vec![EnterpriseConfig { - id: String::from("9072b77d-ca81-40da-be6a-861da525ef7b"), - server_url: String::from("https://valid.example.org"), - }] + vec![enterprise_config("9072b77d-ca81-40da-be6a-861da525ef7b", "https://valid.example.org", "test", "", "indexed slot 99999")] ); } @@ -1057,6 +1120,35 @@ mod tests { ); } + #[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")); @@ -1077,18 +1169,12 @@ mod tests { 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"), }, ]); @@ -1108,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(), }, ]); @@ -1126,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 { @@ -1235,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") ); } @@ -1273,14 +1353,8 @@ 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"), ] ); } @@ -1311,14 +1385,8 @@ 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://slot10503.example.org"), - }, + 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"), ] ); }