mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-10-05 16:29:40 +00:00
Merge branch 'main' into chunk-data
This commit is contained in:
commit
2e06af13e0
133 files changed
+3704
-968
No files matched your search
Generated
+26
-10
@@ -214,7 +214,7 @@ dependencies = [
|
||||
"objc2-foundation 0.3.2",
|
||||
"parking_lot",
|
||||
"percent-encoding",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.60.2",
|
||||
"x11rb",
|
||||
]
|
||||
|
||||
@@ -1769,7 +1769,7 @@ dependencies = [
|
||||
"libc",
|
||||
"option-ext",
|
||||
"redox_users",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2066,7 +2066,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3828,7 +3828,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"windows-targets 0.48.5",
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4000,7 +4000,7 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||
|
||||
[[package]]
|
||||
name = "mindwork-ai-studio"
|
||||
version = "26.5.5"
|
||||
version = "26.6.2"
|
||||
dependencies = [
|
||||
"aes 0.9.1",
|
||||
"apple-native-keyring-store",
|
||||
@@ -4041,6 +4041,7 @@ dependencies = [
|
||||
"tauri-plugin-global-shortcut",
|
||||
"tauri-plugin-opener",
|
||||
"tauri-plugin-shell",
|
||||
"tauri-plugin-single-instance",
|
||||
"tauri-plugin-updater",
|
||||
"tauri-plugin-window-state",
|
||||
"tempfile",
|
||||
@@ -4350,7 +4351,7 @@ version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8"
|
||||
dependencies = [
|
||||
"proc-macro-crate 1.3.1",
|
||||
"proc-macro-crate 3.5.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
@@ -5393,7 +5394,7 @@ dependencies = [
|
||||
"once_cell",
|
||||
"socket2",
|
||||
"tracing",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5813,7 +5814,7 @@ dependencies = [
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys 0.12.1",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5872,7 +5873,7 @@ dependencies = [
|
||||
"security-framework",
|
||||
"security-framework-sys",
|
||||
"webpki-root-certs",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7005,6 +7006,21 @@ dependencies = [
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-single-instance"
|
||||
version = "2.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c8f29386f5e9fdc699182388a33ee80a56de436d91b67459e86afef426282af"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri",
|
||||
"thiserror 2.0.18",
|
||||
"tracing",
|
||||
"windows-sys 0.60.2",
|
||||
"zbus",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-updater"
|
||||
version = "2.10.1"
|
||||
@@ -7163,7 +7179,7 @@ dependencies = [
|
||||
"getrandom 0.4.2",
|
||||
"once_cell",
|
||||
"rustix 1.1.4",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
+2
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "mindwork-ai-studio"
|
||||
version = "26.5.5"
|
||||
version = "26.6.2"
|
||||
edition = "2024"
|
||||
description = "MindWork AI Studio"
|
||||
authors = ["Thorsten Sommer"]
|
||||
@@ -14,6 +14,7 @@ tauri-plugin-window-state = { version = "2.4.1" }
|
||||
tauri-plugin-shell = "2.3.5"
|
||||
tauri-plugin-dialog = "2.7.1"
|
||||
tauri-plugin-opener = "2.5.4"
|
||||
tauri-plugin-single-instance = "2"
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
serde_json = "1.0.150"
|
||||
keyring-core = "1.0.0"
|
||||
|
||||
+91
-12
@@ -1,5 +1,6 @@
|
||||
use std::collections::HashMap;
|
||||
use std::convert::Infallible;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Mutex;
|
||||
use std::time::Duration;
|
||||
use async_stream::stream;
|
||||
@@ -10,6 +11,7 @@ use axum::Json;
|
||||
use bytes::Bytes;
|
||||
use log::{debug, error, info, trace, warn};
|
||||
use once_cell::sync::Lazy;
|
||||
use pdfium_render::prelude::Pdfium;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use strum_macros::Display;
|
||||
use tauri::{DragDropEvent,RunEvent, Manager, WindowEvent, generate_context};
|
||||
@@ -86,6 +88,26 @@ pub fn start_tauri() {
|
||||
});
|
||||
|
||||
let app = tauri::Builder::default()
|
||||
.plugin(tauri_plugin_single_instance::init(|app, args, cwd| {
|
||||
info!(Source = "Tauri"; "Prevented second app instance from starting. cwd='{cwd}', args={args:?}");
|
||||
|
||||
let Some(window) = app.get_webview_window("main") else {
|
||||
warn!(Source = "Tauri"; "Second app instance was blocked, but the main window was not available for activation.");
|
||||
return;
|
||||
};
|
||||
|
||||
if let Err(error) = window.show() {
|
||||
warn!(Source = "Tauri"; "Failed to show main window after second app start: {error}");
|
||||
}
|
||||
|
||||
if let Err(error) = window.unminimize() {
|
||||
warn!(Source = "Tauri"; "Failed to unminimize main window after second app start: {error}");
|
||||
}
|
||||
|
||||
if let Err(error) = window.set_focus() {
|
||||
warn!(Source = "Tauri"; "Failed to focus main window after second app start: {error}");
|
||||
}
|
||||
}))
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.plugin(tauri_plugin_shell::init())
|
||||
.plugin(tauri_plugin_opener::init())
|
||||
@@ -955,19 +977,9 @@ fn set_pdfium_path<R: tauri::Runtime>(path_resolver: &PathResolver<R>) {
|
||||
}
|
||||
};
|
||||
|
||||
let candidate_paths = [
|
||||
resource_dir.join("resources").join("libraries"),
|
||||
resource_dir.join("libraries"),
|
||||
];
|
||||
|
||||
let pdfium_source_path = candidate_paths
|
||||
.iter()
|
||||
.find(|path| path.exists())
|
||||
.map(|path| path.to_string_lossy().to_string());
|
||||
|
||||
match pdfium_source_path {
|
||||
match select_pdfium_library_directory(&resource_dir) {
|
||||
Some(path) => {
|
||||
*PDFIUM_LIB_PATH.lock().unwrap() = Some(path);
|
||||
*PDFIUM_LIB_PATH.lock().unwrap() = Some(path.to_string_lossy().to_string());
|
||||
}
|
||||
None => {
|
||||
error!(Source = "Bootloader Tauri"; "Failed to set the PDFium library path.");
|
||||
@@ -975,9 +987,76 @@ fn set_pdfium_path<R: tauri::Runtime>(path_resolver: &PathResolver<R>) {
|
||||
}
|
||||
}
|
||||
|
||||
fn select_pdfium_library_directory(resource_dir: &Path) -> Option<PathBuf> {
|
||||
let candidate_paths = [
|
||||
resource_dir.join("resources").join("libraries"),
|
||||
resource_dir.join("libraries"),
|
||||
];
|
||||
|
||||
for path in candidate_paths {
|
||||
let pdfium_library_path = Pdfium::pdfium_platform_library_name_at_path(&path);
|
||||
if pdfium_library_path.exists() {
|
||||
return Some(path);
|
||||
}
|
||||
|
||||
if path.exists() {
|
||||
warn!(
|
||||
Source = "Bootloader Tauri";
|
||||
"PDFium library directory exists, but the library file was not found at '{path}'.",
|
||||
path = pdfium_library_path.to_string_lossy(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::fs;
|
||||
|
||||
#[test]
|
||||
fn pdfium_library_directory_prefers_resources_libraries() {
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let resources_libraries = temp_dir.path().join("resources").join("libraries");
|
||||
let libraries = temp_dir.path().join("libraries");
|
||||
create_pdfium_library_in(&resources_libraries);
|
||||
create_pdfium_library_in(&libraries);
|
||||
|
||||
assert_eq!(
|
||||
select_pdfium_library_directory(temp_dir.path()),
|
||||
Some(resources_libraries)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pdfium_library_directory_falls_back_when_first_directory_has_no_library() {
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let resources_libraries = temp_dir.path().join("resources").join("libraries");
|
||||
let libraries = temp_dir.path().join("libraries");
|
||||
fs::create_dir_all(&resources_libraries).unwrap();
|
||||
create_pdfium_library_in(&libraries);
|
||||
|
||||
assert_eq!(
|
||||
select_pdfium_library_directory(temp_dir.path()),
|
||||
Some(libraries)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pdfium_library_directory_requires_library_file() {
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
fs::create_dir_all(temp_dir.path().join("resources").join("libraries")).unwrap();
|
||||
fs::create_dir_all(temp_dir.path().join("libraries")).unwrap();
|
||||
|
||||
assert_eq!(select_pdfium_library_directory(temp_dir.path()), None);
|
||||
}
|
||||
|
||||
fn create_pdfium_library_in(path: &Path) {
|
||||
fs::create_dir_all(path).unwrap();
|
||||
fs::File::create(Pdfium::pdfium_platform_library_name_at_path(path)).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tauri_localhost_is_tauri_asset_url() {
|
||||
|
||||
+34
-13
@@ -13,7 +13,13 @@ use crate::runtime_api_token::API_TOKEN;
|
||||
use crate::app_window::change_location_to;
|
||||
use crate::runtime_certificate::CERTIFICATE_FINGERPRINT;
|
||||
use crate::encryption::ENCRYPTION;
|
||||
use crate::environment::{is_dev, DATA_DIRECTORY};
|
||||
use crate::environment::{
|
||||
is_dev, resolve_external_http_custom_root_certificate_policy, DATA_DIRECTORY,
|
||||
DOTNET_ENV_CUSTOM_ROOT_CERTIFICATE_ALLOWED_HOSTS,
|
||||
DOTNET_ENV_CUSTOM_ROOT_CERTIFICATE_BUNDLE_PATH,
|
||||
DOTNET_ENV_CUSTOM_ROOT_CERTIFICATE_POLICY_CONFIGURED,
|
||||
DOTNET_ENV_CUSTOM_ROOT_CERTIFICATES_ENABLED,
|
||||
};
|
||||
use crate::network::get_available_port;
|
||||
use crate::runtime_api::API_SERVER_PORT;
|
||||
use crate::stale_process_cleanup::{kill_stale_process, log_potential_stale_process};
|
||||
@@ -93,6 +99,20 @@ pub async fn dotnet_port(_token: APIToken) -> String {
|
||||
format!("{dotnet_server_port}")
|
||||
}
|
||||
|
||||
fn external_http_custom_root_certificate_policy_environment() -> Vec<(String, String)> {
|
||||
let policy = resolve_external_http_custom_root_certificate_policy();
|
||||
if !policy.is_configured {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
vec![
|
||||
(String::from(DOTNET_ENV_CUSTOM_ROOT_CERTIFICATE_POLICY_CONFIGURED), String::from("true")),
|
||||
(String::from(DOTNET_ENV_CUSTOM_ROOT_CERTIFICATES_ENABLED), policy.enabled.to_string()),
|
||||
(String::from(DOTNET_ENV_CUSTOM_ROOT_CERTIFICATE_BUNDLE_PATH), policy.bundle_path),
|
||||
(String::from(DOTNET_ENV_CUSTOM_ROOT_CERTIFICATE_ALLOWED_HOSTS), policy.allowed_hosts),
|
||||
]
|
||||
}
|
||||
|
||||
/// Creates the startup environment file for the .NET server in the development
|
||||
/// environment. The file is created in the root directory of the repository.
|
||||
/// Creating that env file on a production environment would be a security
|
||||
@@ -113,18 +133,18 @@ pub fn create_startup_env_file() {
|
||||
warn!(Source = "Bootloader .NET"; "Development environment detected; create the startup env file at '../startup.env'.");
|
||||
let env_file_path = std::path::PathBuf::from("..").join("startup.env");
|
||||
let mut env_file = std::fs::File::create(env_file_path).unwrap();
|
||||
let env_file_content = format!(
|
||||
"AI_STUDIO_SECRET_PASSWORD={secret_password}\n\
|
||||
AI_STUDIO_SECRET_KEY_SALT={secret_key_salt}\n\
|
||||
AI_STUDIO_CERTIFICATE_FINGERPRINT={cert_fingerprint}\n\
|
||||
AI_STUDIO_API_PORT={api_port}\n\
|
||||
AI_STUDIO_API_TOKEN={api_token}",
|
||||
let mut env_file_lines = vec![
|
||||
format!("AI_STUDIO_SECRET_PASSWORD={secret_password}"),
|
||||
format!("AI_STUDIO_SECRET_KEY_SALT={secret_key_salt}"),
|
||||
format!("AI_STUDIO_CERTIFICATE_FINGERPRINT={}", CERTIFICATE_FINGERPRINT.get().unwrap()),
|
||||
format!("AI_STUDIO_API_PORT={api_port}"),
|
||||
format!("AI_STUDIO_API_TOKEN={}", API_TOKEN.to_hex_text()),
|
||||
];
|
||||
for (key, value) in external_http_custom_root_certificate_policy_environment() {
|
||||
env_file_lines.push(format!("{key}={value}"));
|
||||
}
|
||||
|
||||
cert_fingerprint = CERTIFICATE_FINGERPRINT.get().unwrap(),
|
||||
api_token = API_TOKEN.to_hex_text()
|
||||
);
|
||||
|
||||
std::io::Write::write_all(&mut env_file, env_file_content.as_bytes()).unwrap();
|
||||
std::io::Write::write_all(&mut env_file, env_file_lines.join("\n").as_bytes()).unwrap();
|
||||
info!(Source = "Bootloader .NET"; "The startup env file was created successfully.");
|
||||
}
|
||||
|
||||
@@ -136,13 +156,14 @@ pub fn start_dotnet_server<R: tauri::Runtime>(app_handle: tauri::AppHandle<R>) {
|
||||
let secret_key_salt = BASE64_STANDARD.encode(ENCRYPTION.secret_key_salt);
|
||||
let api_port = *API_SERVER_PORT;
|
||||
|
||||
let dotnet_server_environment: HashMap<String, String> = HashMap::from_iter([
|
||||
let mut dotnet_server_environment: HashMap<String, String> = HashMap::from_iter([
|
||||
(String::from("AI_STUDIO_SECRET_PASSWORD"), secret_password),
|
||||
(String::from("AI_STUDIO_SECRET_KEY_SALT"), secret_key_salt),
|
||||
(String::from("AI_STUDIO_CERTIFICATE_FINGERPRINT"), CERTIFICATE_FINGERPRINT.get().unwrap().to_string()),
|
||||
(String::from("AI_STUDIO_API_PORT"), format!("{api_port}")),
|
||||
(String::from("AI_STUDIO_API_TOKEN"), API_TOKEN.to_hex_text().to_string()),
|
||||
]);
|
||||
dotnet_server_environment.extend(external_http_custom_root_certificate_policy_environment());
|
||||
|
||||
info!("Try to start the .NET server...");
|
||||
let server_spawn_clone = DOTNET_SERVER.clone();
|
||||
|
||||
+208
-1
@@ -21,6 +21,12 @@ const ENTERPRISE_CONFIG_SERVER_URL_KEY_PREFIX: &str = "config_server_url";
|
||||
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 EXTERNAL_HTTP_CUSTOM_ROOT_CERTIFICATE_POLICY_FILE_NAME: &str = "external_http_custom_root_certificates.yaml";
|
||||
|
||||
pub const DOTNET_ENV_CUSTOM_ROOT_CERTIFICATE_POLICY_CONFIGURED: &str = "AI_STUDIO_EXTERNAL_HTTP_CUSTOM_ROOT_CERTIFICATES_POLICY_CONFIGURED";
|
||||
pub const DOTNET_ENV_CUSTOM_ROOT_CERTIFICATES_ENABLED: &str = "AI_STUDIO_EXTERNAL_HTTP_CUSTOM_ROOT_CERTIFICATES_ENABLED";
|
||||
pub const DOTNET_ENV_CUSTOM_ROOT_CERTIFICATE_BUNDLE_PATH: &str = "AI_STUDIO_EXTERNAL_HTTP_CUSTOM_ROOT_CERTIFICATE_BUNDLE_PATH";
|
||||
pub const DOTNET_ENV_CUSTOM_ROOT_CERTIFICATE_ALLOWED_HOSTS: &str = "AI_STUDIO_EXTERNAL_HTTP_CUSTOM_ROOT_CERTIFICATE_ALLOWED_HOSTS";
|
||||
|
||||
#[cfg(any(target_os = "linux", test))]
|
||||
const FLATPAK_ENTERPRISE_POLICY_DIRECTORY: &str = "/app/etc/MindWorkAI";
|
||||
@@ -99,13 +105,18 @@ fn detect_linux_package_type() -> &'static str {
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn is_flatpak() -> bool {
|
||||
pub(crate) fn is_flatpak() -> bool {
|
||||
env_var_has_value("FLATPAK_ID")
|
||||
|| Path::new("/.flatpak-info").is_file()
|
||||
|| env::var("container")
|
||||
.is_ok_and(|value| value.trim().eq_ignore_ascii_case("flatpak"))
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
pub(crate) fn is_flatpak() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn is_appimage() -> bool {
|
||||
env_var_has_value("APPIMAGE") || env_var_has_value("APPDIR")
|
||||
@@ -257,6 +268,15 @@ pub struct EnterpriseConfig {
|
||||
pub slot: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||
pub struct ExternalHttpCustomRootCertificatePolicy {
|
||||
pub is_configured: bool,
|
||||
pub enabled: bool,
|
||||
pub bundle_path: String,
|
||||
pub allowed_hosts: String,
|
||||
pub source_detail: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
struct EnterpriseSourceValue {
|
||||
value: String,
|
||||
@@ -337,6 +357,10 @@ pub async fn read_enterprise_configs(_token: APIToken) -> Json<Vec<EnterpriseCon
|
||||
Json(resolve_effective_enterprise_config_source().configs)
|
||||
}
|
||||
|
||||
pub fn resolve_external_http_custom_root_certificate_policy() -> ExternalHttpCustomRootCertificatePolicy {
|
||||
load_external_http_custom_root_certificate_policy_from_directories(&enterprise_policy_directories())
|
||||
}
|
||||
|
||||
fn resolve_effective_enterprise_config_source() -> EnterpriseSourceData {
|
||||
select_effective_enterprise_config_source(gather_enterprise_sources())
|
||||
}
|
||||
@@ -646,6 +670,54 @@ fn load_policy_values_from_directories(directories: &[PathBuf]) -> EnterpriseSou
|
||||
values
|
||||
}
|
||||
|
||||
fn load_external_http_custom_root_certificate_policy_from_directories(directories: &[PathBuf]) -> ExternalHttpCustomRootCertificatePolicy {
|
||||
for directory in directories {
|
||||
let path = directory.join(EXTERNAL_HTTP_CUSTOM_ROOT_CERTIFICATE_POLICY_FILE_NAME);
|
||||
let Some(values) = read_policy_yaml_mapping(&path) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if let Some(policy) = parse_external_http_custom_root_certificate_policy(&path, &values) {
|
||||
info!("Using external HTTP custom root certificate policy from '{}'.", policy.source_detail);
|
||||
return policy;
|
||||
}
|
||||
}
|
||||
|
||||
ExternalHttpCustomRootCertificatePolicy::default()
|
||||
}
|
||||
|
||||
fn parse_external_http_custom_root_certificate_policy(path: &Path, values: &HashMap<String, String>) -> Option<ExternalHttpCustomRootCertificatePolicy> {
|
||||
let Some(raw_enabled) = values.get("enabled") else {
|
||||
warn!("Ignoring external HTTP custom root certificate policy '{}': missing 'enabled'.", path.display());
|
||||
return None;
|
||||
};
|
||||
|
||||
let Some(enabled) = parse_policy_boolean_value(raw_enabled) else {
|
||||
warn!("Ignoring external HTTP custom root certificate policy '{}': invalid 'enabled' value.", path.display());
|
||||
return None;
|
||||
};
|
||||
|
||||
let source_detail = path
|
||||
.canonicalize()
|
||||
.unwrap_or_else(|_| path.to_path_buf())
|
||||
.to_string_lossy()
|
||||
.into_owned();
|
||||
|
||||
Some(ExternalHttpCustomRootCertificatePolicy {
|
||||
is_configured: true,
|
||||
enabled,
|
||||
bundle_path: values
|
||||
.get("bundle_path")
|
||||
.and_then(|value| normalize_enterprise_value(value))
|
||||
.unwrap_or_default(),
|
||||
allowed_hosts: values
|
||||
.get("allowed_hosts")
|
||||
.and_then(|value| normalize_enterprise_value(value))
|
||||
.unwrap_or_default(),
|
||||
source_detail,
|
||||
})
|
||||
}
|
||||
|
||||
fn enterprise_policy_file_slot_suffix(file_name: &str) -> Option<&str> {
|
||||
let suffix = file_name
|
||||
.strip_prefix("config")?
|
||||
@@ -737,6 +809,25 @@ fn parse_policy_yaml_value(raw_value: &str) -> Option<String> {
|
||||
Some(String::from(trimmed))
|
||||
}
|
||||
|
||||
fn parse_policy_boolean_value(raw_value: &str) -> Option<bool> {
|
||||
let normalized = raw_value.trim();
|
||||
if normalized.eq_ignore_ascii_case("true")
|
||||
|| normalized == "1"
|
||||
|| normalized.eq_ignore_ascii_case("yes")
|
||||
|| normalized.eq_ignore_ascii_case("on") {
|
||||
return Some(true);
|
||||
}
|
||||
|
||||
if normalized.eq_ignore_ascii_case("false")
|
||||
|| normalized == "0"
|
||||
|| normalized.eq_ignore_ascii_case("no")
|
||||
|| normalized.eq_ignore_ascii_case("off") {
|
||||
return Some(false);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
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
|
||||
@@ -963,10 +1054,12 @@ fn normalize_enterprise_config_id(value: &str) -> Option<String> {
|
||||
mod tests {
|
||||
use super::{
|
||||
enterprise_environment_key_name, enterprise_policy_file_slot_suffix,
|
||||
load_external_http_custom_root_certificate_policy_from_directories,
|
||||
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, EnterpriseSourceValue, EnterpriseSourceValues,
|
||||
ExternalHttpCustomRootCertificatePolicy,
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
@@ -1490,6 +1583,120 @@ mod tests {
|
||||
assert_eq!(source.encryption_secret, "POLICY-SECRET");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_external_http_custom_root_certificate_policy_uses_first_valid_directory() {
|
||||
let directory_a = tempdir().unwrap();
|
||||
let directory_b = tempdir().unwrap();
|
||||
|
||||
fs::write(
|
||||
directory_a.path().join("external_http_custom_root_certificates.yaml"),
|
||||
"enabled: true\nbundle_path: \"/app/etc/MindWorkAI/company-a.pem\"\nallowed_hosts: \"*.a.example.org;eri.a.example.org\"",
|
||||
)
|
||||
.unwrap();
|
||||
fs::write(
|
||||
directory_b.path().join("external_http_custom_root_certificates.yaml"),
|
||||
"enabled: true\nbundle_path: \"/app/etc/MindWorkAI/company-b.pem\"\nallowed_hosts: \"*.b.example.org\"",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let policy = load_external_http_custom_root_certificate_policy_from_directories(&[
|
||||
directory_a.path().to_path_buf(),
|
||||
directory_b.path().to_path_buf(),
|
||||
]);
|
||||
|
||||
assert_eq!(
|
||||
policy,
|
||||
ExternalHttpCustomRootCertificatePolicy {
|
||||
is_configured: true,
|
||||
enabled: true,
|
||||
bundle_path: String::from("/app/etc/MindWorkAI/company-a.pem"),
|
||||
allowed_hosts: String::from("*.a.example.org;eri.a.example.org"),
|
||||
source_detail: policy_path(directory_a.path().join("external_http_custom_root_certificates.yaml")),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_external_http_custom_root_certificate_policy_allows_disabled_policy_to_win() {
|
||||
let directory_a = tempdir().unwrap();
|
||||
let directory_b = tempdir().unwrap();
|
||||
|
||||
fs::write(
|
||||
directory_a.path().join("external_http_custom_root_certificates.yaml"),
|
||||
"enabled: false",
|
||||
)
|
||||
.unwrap();
|
||||
fs::write(
|
||||
directory_b.path().join("external_http_custom_root_certificates.yaml"),
|
||||
"enabled: true\nbundle_path: \"/app/etc/MindWorkAI/company-b.pem\"\nallowed_hosts: \"*.b.example.org\"",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let policy = load_external_http_custom_root_certificate_policy_from_directories(&[
|
||||
directory_a.path().to_path_buf(),
|
||||
directory_b.path().to_path_buf(),
|
||||
]);
|
||||
|
||||
assert_eq!(
|
||||
policy,
|
||||
ExternalHttpCustomRootCertificatePolicy {
|
||||
is_configured: true,
|
||||
enabled: false,
|
||||
bundle_path: String::new(),
|
||||
allowed_hosts: String::new(),
|
||||
source_detail: policy_path(directory_a.path().join("external_http_custom_root_certificates.yaml")),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_external_http_custom_root_certificate_policy_skips_invalid_files() {
|
||||
let directory_a = tempdir().unwrap();
|
||||
let directory_b = tempdir().unwrap();
|
||||
|
||||
fs::write(
|
||||
directory_a.path().join("external_http_custom_root_certificates.yaml"),
|
||||
"enabled: maybe\nbundle_path: \"/app/etc/MindWorkAI/ignored.pem\"",
|
||||
)
|
||||
.unwrap();
|
||||
fs::write(
|
||||
directory_b.path().join("external_http_custom_root_certificates.yaml"),
|
||||
"enabled: yes\nbundle_path: \"/app/etc/MindWorkAI/company-b.pem\"\nallowed_hosts: \"*.b.example.org,eri.b.example.org\"",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let policy = load_external_http_custom_root_certificate_policy_from_directories(&[
|
||||
directory_a.path().to_path_buf(),
|
||||
directory_b.path().to_path_buf(),
|
||||
]);
|
||||
|
||||
assert_eq!(
|
||||
policy,
|
||||
ExternalHttpCustomRootCertificatePolicy {
|
||||
is_configured: true,
|
||||
enabled: true,
|
||||
bundle_path: String::from("/app/etc/MindWorkAI/company-b.pem"),
|
||||
allowed_hosts: String::from("*.b.example.org,eri.b.example.org"),
|
||||
source_detail: policy_path(directory_b.path().join("external_http_custom_root_certificates.yaml")),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_external_http_custom_root_certificate_policy_requires_enabled_key() {
|
||||
let directory = tempdir().unwrap();
|
||||
|
||||
fs::write(
|
||||
directory.path().join("external_http_custom_root_certificates.yaml"),
|
||||
"bundle_path: \"/app/etc/MindWorkAI/company.pem\"\nallowed_hosts: \"*.example.org\"",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let policy = load_external_http_custom_root_certificate_policy_from_directories(&[directory.path().to_path_buf()]);
|
||||
|
||||
assert_eq!(policy, ExternalHttpCustomRootCertificatePolicy::default());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_policy_values_from_directories_ignores_invalid_and_incomplete_files() {
|
||||
let directory = tempdir().unwrap();
|
||||
|
||||
+52
-2
@@ -5,12 +5,13 @@ use std::path::{Path, PathBuf};
|
||||
use std::sync::OnceLock;
|
||||
use log::{info, warn};
|
||||
use tokio::process::Command;
|
||||
use crate::environment::DATA_DIRECTORY;
|
||||
use crate::environment::{DATA_DIRECTORY, is_flatpak};
|
||||
use crate::metadata::META_DATA;
|
||||
|
||||
/// Tracks whether the RID mismatch warning has been logged.
|
||||
static HAS_LOGGED_RID_MISMATCH: OnceLock<()> = OnceLock::new();
|
||||
static HAS_LOGGED_PANDOC_PATH: OnceLock<()> = OnceLock::new();
|
||||
const FLATPAK_PANDOC_PLUGIN_BIN_DIRECTORY: &str = "/app/plugins/pandoc/bin";
|
||||
|
||||
/// Microsoft documents CREATE_NO_WINDOW as a process creation flag with value 0x08000000.
|
||||
/// It starts console applications without opening a console window:
|
||||
@@ -186,8 +187,12 @@ impl PandocProcessBuilder {
|
||||
}
|
||||
|
||||
fn system_pandoc_executable_candidates(executable_name: &str) -> Vec<PathBuf> {
|
||||
Self::system_pandoc_executable_candidates_for(env::consts::OS, executable_name, is_flatpak())
|
||||
}
|
||||
|
||||
fn system_pandoc_executable_candidates_for(os: &str, executable_name: &str, include_flatpak_extension: bool) -> Vec<PathBuf> {
|
||||
let mut candidates: Vec<PathBuf> = Vec::new();
|
||||
match env::consts::OS {
|
||||
match os {
|
||||
"windows" => {
|
||||
Self::push_env_candidate(&mut candidates, "LOCALAPPDATA", &["Pandoc", executable_name]);
|
||||
Self::push_env_candidate(&mut candidates, "ProgramFiles", &["Pandoc", executable_name]);
|
||||
@@ -199,6 +204,9 @@ impl PandocProcessBuilder {
|
||||
candidates.push(PathBuf::from("/usr/bin").join(executable_name));
|
||||
},
|
||||
"linux" => {
|
||||
if include_flatpak_extension {
|
||||
candidates.push(PathBuf::from(FLATPAK_PANDOC_PLUGIN_BIN_DIRECTORY).join(executable_name));
|
||||
}
|
||||
candidates.push(PathBuf::from("/usr/local/bin").join(executable_name));
|
||||
candidates.push(PathBuf::from("/usr/bin").join(executable_name));
|
||||
candidates.push(PathBuf::from("/snap/bin").join(executable_name));
|
||||
@@ -281,4 +289,46 @@ impl PandocProcessBuilder {
|
||||
_ => "pandoc".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{FLATPAK_PANDOC_PLUGIN_BIN_DIRECTORY, PandocProcessBuilder};
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use tempfile::tempdir;
|
||||
|
||||
#[test]
|
||||
fn linux_candidates_include_flatpak_pandoc_extension_first_when_flatpak() {
|
||||
let candidates = PandocProcessBuilder::system_pandoc_executable_candidates_for("linux", "pandoc", true);
|
||||
let flatpak_candidate = PathBuf::from(FLATPAK_PANDOC_PLUGIN_BIN_DIRECTORY).join("pandoc");
|
||||
let usr_local_candidate = PathBuf::from("/usr/local/bin").join("pandoc");
|
||||
|
||||
let flatpak_index = candidates.iter().position(|candidate| candidate == &flatpak_candidate).unwrap();
|
||||
let usr_local_index = candidates.iter().position(|candidate| candidate == &usr_local_candidate).unwrap();
|
||||
|
||||
assert!(flatpak_index < usr_local_index);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn linux_candidates_skip_flatpak_pandoc_extension_when_not_flatpak() {
|
||||
let candidates = PandocProcessBuilder::system_pandoc_executable_candidates_for("linux", "pandoc", false);
|
||||
let flatpak_candidate = PathBuf::from(FLATPAK_PANDOC_PLUGIN_BIN_DIRECTORY).join("pandoc");
|
||||
|
||||
assert!(!candidates.contains(&flatpak_candidate));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_pandoc_search_finds_data_directory_installation() {
|
||||
let directory = tempdir().unwrap();
|
||||
let pandoc_directory = directory.path().join("pandoc").join("bin");
|
||||
fs::create_dir_all(&pandoc_directory).unwrap();
|
||||
let pandoc_path = pandoc_directory.join("pandoc");
|
||||
fs::File::create(&pandoc_path).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
PandocProcessBuilder::find_executable_in_dir(directory.path(), "pandoc").unwrap(),
|
||||
pandoc_path
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -384,6 +384,8 @@ fn set_qdrant_edge_unavailable(reason: String) {
|
||||
status.unavailable_reason = Some(reason);
|
||||
}
|
||||
|
||||
// Temporary compatibility shim until 2026-12-02:
|
||||
// documentation/compatibility-shims/2026-06-qdrant-edge-migration.md
|
||||
fn remove_obsolete_qdrant_sidecar_files<R: tauri::Runtime>(app_handle: &tauri::AppHandle<R>) {
|
||||
let mut paths = Vec::new();
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"productName": "MindWork AI Studio",
|
||||
"mainBinaryName": "MindWork AI Studio",
|
||||
"version": "26.5.5",
|
||||
"version": "26.6.2",
|
||||
"identifier": "com.github.mindwork-ai.ai-studio",
|
||||
|
||||
"build": {
|
||||
|
||||
Reference in new issue
Block a user