diff --git a/app/MindWork AI Studio/wwwroot/changelog/v26.10.1.md b/app/MindWork AI Studio/wwwroot/changelog/v26.10.1.md index fe92f69d7..e5dda572e 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v26.10.1.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v26.10.1.md @@ -61,6 +61,8 @@ - Fixed the security notice about removed instructions (prompt injection) appearing only minutes later while AI Studio updated a data source in the background. The notice also mixed your own attachments with files of the data source. Now each notice appears as soon as AI Studio has read the content, for example, the attachments of your message, and it names only that content. - Fixed the security notice about removed instructions (prompt injection) appearing several times for one answer when AI Studio checks which passages of your data sources fit your question: once for every passage it checked, and once more for all of them together. You now get one notice per answer, and a file counts once, however many of its passages were affected. - Fixed the example configuration for IT departments, which spelled llama.cpp in a way AI Studio does not accept. AI Studio ignored a self-hosted provider set up with that spelling. The example now also names only the self-hosted servers which can create embeddings or transcribe audio. +- Fixed building AI Studio from source on Windows, where the checks that run before every build failed because two of them expected the line breaks and file paths of macOS and Linux. Thanks, Paul Koudelka (`PaulKoudelka`), for this contribution. +- Fixed a problem on Windows where AI Studio could fail to read the configuration of your organization when the name of an environment variable on your computer contained umlauts or other special characters. - Updated the code contributions on the supporters page, which now thank everyone who has contributed code to AI Studio so far. - Upgraded several libraries to improve security. - Upgraded to Rust v1.99.0 diff --git a/app/Tests/Models/Corpus/CapabilitySnapshot.cs b/app/Tests/Models/Corpus/CapabilitySnapshot.cs index 91d25ca36..d6453ceb1 100644 --- a/app/Tests/Models/Corpus/CapabilitySnapshot.cs +++ b/app/Tests/Models/Corpus/CapabilitySnapshot.cs @@ -66,7 +66,16 @@ public static class CapabilitySnapshot /// private const string NO_SYSTEM_PROMPT_ROLE = "(unknown)"; - private const string HEADER = + /// + /// The comment block every snapshot starts with. + /// + /// + /// A raw string literal takes its line breaks from this source file as it was checked out, and Git + /// for Windows checks it out with CRLF unless told otherwise. The snapshot has no carriage returns, + /// so the header must not bring any along: otherwise no snapshot on Windows ever matches, and the + /// failure names no model at all. + /// + private static readonly string HEADER = """ # What the rules answer, for every model of the corpus. # @@ -93,7 +102,7 @@ public static class CapabilitySnapshot # what each of them must answer, but it states capabilities alone. # - """; + """.ReplaceLineEndings("\n"); /// /// The directory this source file lives in, filled in by the compiler. diff --git a/metadata.txt b/metadata.txt index 4afa201ab..87f044842 100644 --- a/metadata.txt +++ b/metadata.txt @@ -9,4 +9,4 @@ 9f102728b59, release osx-arm64 148.0.7763.0 -0.8.0 +0.8.0 \ No newline at end of file diff --git a/runtime/src/environment.rs b/runtime/src/environment.rs index 284435bc4..b49ad8304 100644 --- a/runtime/src/environment.rs +++ b/runtime/src/environment.rs @@ -859,11 +859,18 @@ fn enterprise_env_key_equals(env_name: &str, expected: &str) -> bool { #[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; - } + enterprise_env_key_suffix_ignoring_case(env_name, prefix) +} - let (raw_prefix, suffix) = env_name.split_at(prefix.len()); +/// Returns the slot suffix of an environment variable name, comparing the prefix without +/// regard to case, the way Windows compares the names of environment variables. +/// +/// Every environment variable passes through here, whatever characters its name contains. +/// Where the length of the prefix ends inside one of them, the name cannot start with the +/// prefix, and splitting it there would panic. +#[cfg(any(target_os = "windows", test))] +fn enterprise_env_key_suffix_ignoring_case<'a>(env_name: &'a str, prefix: &str) -> Option<&'a str> { + let (raw_prefix, suffix) = env_name.split_at_checked(prefix.len())?; if raw_prefix.eq_ignore_ascii_case(prefix) { normalize_enterprise_slot_suffix(suffix) } else { @@ -1368,8 +1375,9 @@ fn normalize_enterprise_config_id(value: &str) -> Option { #[cfg(test)] mod tests { use super::{ - directory_is_writable, enterprise_environment_key_name, - enterprise_policy_file_slot_suffix, has_managed_installation_marker, + directory_is_writable, enterprise_env_key_suffix_ignoring_case, + enterprise_environment_key_name, enterprise_policy_file_slot_suffix, + has_managed_installation_marker, is_windows_machine_wide_installation, is_windows_per_user_installation, load_external_http_custom_root_certificate_policy_from_directories, linux_policy_directories_from_xdg, load_policy_values_from_directories, @@ -1378,6 +1386,7 @@ mod tests { select_effective_enterprise_secret_source, update_target_installation_kind, EnterpriseConfig, EnterpriseSourceData, EnterpriseSourceValue, EnterpriseSourceValues, ExternalHttpCustomRootCertificatePolicy, InstallationKind, LinuxPackageType, + ENTERPRISE_ENV_CONFIG_ID_PREFIX, ENTERPRISE_ENV_CONFIG_SERVER_URL_PREFIX, MANAGED_INSTALLATION_MARKER_FILE_NAME, }; use std::collections::HashMap; @@ -1626,6 +1635,33 @@ mod tests { ); } + #[test] + fn enterprise_env_key_suffix_ignoring_case_accepts_any_case() { + assert_eq!( + enterprise_env_key_suffix_ignoring_case("mindwork_ai_studio_enterprise_config_id_10503", ENTERPRISE_ENV_CONFIG_ID_PREFIX), + Some("10503") + ); + assert_eq!( + enterprise_env_key_suffix_ignoring_case("Mindwork_AI_Studio_Enterprise_Config_Server_URL_00000", ENTERPRISE_ENV_CONFIG_SERVER_URL_PREFIX), + Some("00000") + ); + assert_eq!( + enterprise_env_key_suffix_ignoring_case("MINDWORK_AI_STUDIO", ENTERPRISE_ENV_CONFIG_ID_PREFIX), + None + ); + } + + #[test] + fn enterprise_env_key_suffix_ignoring_case_skips_names_where_the_prefix_ends_inside_a_character() { + for prefix in [ENTERPRISE_ENV_CONFIG_ID_PREFIX, ENTERPRISE_ENV_CONFIG_SERVER_URL_PREFIX] { + // Every "Ä" takes two bytes, and both prefixes are an odd number of bytes long: + let name = "Ä".repeat(prefix.len()); + assert!(!name.is_char_boundary(prefix.len())); + + assert_eq!(enterprise_env_key_suffix_ignoring_case(&name, prefix), None); + } + } + #[test] fn parse_enterprise_source_values_keeps_environment_id_variable_as_source_detail() { let mut values = EnterpriseSourceValues::new(); diff --git a/runtime/src/log.rs b/runtime/src/log.rs index 49170971c..c46e0c01f 100644 --- a/runtime/src/log.rs +++ b/runtime/src/log.rs @@ -17,6 +17,14 @@ use crate::environment::{is_dev, is_flatpak}; const FLATPAK_PERSISTENT_DATA_DIRECTORY: &str = "/var/data"; +/// The name of the log file, without its extension. On Unix systems, +/// the file starts with a dot to be hidden. +const LOG_BASENAME: &str = if cfg!(unix) { + ".AI Studio Events" +} else { + "AI Studio Events" +}; + /// The key under which a log record carries the time its event happened, in microseconds /// since the Unix epoch. A record without it happened when it reached the logger. The /// formatters start the line with this time and never write the key itself. @@ -77,18 +85,10 @@ pub fn init_logging(bundle_identifier: &str) { log_config.push_str("qdrant_edge::common::mmap::ops=error, "); log_config.push_str("qdrant_edge::common::mmap::advice=info"); - // Configure the initial filename. On Unix systems, the file should start - // with a dot to be hidden. - let log_basename = match cfg!(unix) - { - true => ".AI Studio Events", - false => "AI Studio Events", - }; - let (startup_log_directory, fallback_warning) = get_startup_log_path(bundle_identifier); let log_path = FileSpec::default() .directory(startup_log_directory) - .basename(log_basename) + .basename(LOG_BASENAME) .suppress_timestamp() .suffix("log"); @@ -243,12 +243,8 @@ where fn ensure_log_directory_is_writable(directory: &Path) -> Result<(), String> { create_dir_all(directory).map_err(|error| format!("could not create '{}': {error}", directory.display()))?; - let log_file_path = directory.join(if cfg!(unix) { - ".AI Studio Events.log" - } else { - "AI Studio Events.log" - }); - + let log_file_path = directory.join(format!("{LOG_BASENAME}.log")); + OpenOptions::new() .create(true) .append(true) @@ -773,12 +769,14 @@ mod tests { #[test] fn startup_log_path_storage_uses_selected_fallback_path() { - let temporary = PathBuf::from("/tmp").join(BUNDLE_IDENTIFIER).join("data"); + // The temporary directory is absolute on every OS, while "/tmp" is not on Windows, + // where the stored path would then gain a drive letter and backslashes: + let temporary = temp_dir().join(BUNDLE_IDENTIFIER).join("data"); let (selected, _) = select_flatpak_startup_log_path( BUNDLE_IDENTIFIER, None, PathBuf::from("/var/data"), - PathBuf::from("/tmp"), + temp_dir(), |candidate| { if candidate == temporary { Ok(()) @@ -787,18 +785,16 @@ mod tests { } }, ).unwrap(); + let expected = selected.join(format!("{LOG_BASENAME}.log")); let log_path = FileSpec::default() .directory(selected) - .basename(".AI Studio Events") + .basename(LOG_BASENAME) .suppress_timestamp() .suffix("log"); let storage = OnceLock::new(); store_startup_log_path(&storage, &log_path); - assert_eq!( - storage.get().unwrap(), - "/tmp/org.mindworkai.AIStudio/data/.AI Studio Events.log", - ); + assert_eq!(Path::new(storage.get().unwrap()), expected); } } \ No newline at end of file