Merge branch 'main' into chunk-data

This commit is contained in:
Paul Koudelka committed 2026-07-01 09:54:12 +02:00
commit 2e06af13e0
133 files changed
+3704 -968

No files matched your search

+26 -10
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
);
}
}
+2
View File
@@ -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 -1
View File
@@ -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": {