mod commands; mod db; mod error; mod models; mod services; use std::sync::{Arc, Mutex}; use tauri::Manager; pub struct AppState { pub db: Arc>, pub encryption_key: [u8; 32], pub http_client: reqwest::Client, pub process_registry: services::process_registry::ProcessRegistry, pub activity_state: services::activity_state::ActivityState, } #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { tauri::Builder::default() .plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_notification::init()) .setup(|app| { let db_dir = app.path().app_data_dir()?; std::fs::create_dir_all(&db_dir)?; let db_path = db_dir.join("orchai.db"); let conn = db::init(&db_path).expect("Failed to initialize database"); let key_path = db_dir.join("orchai.key"); let encryption_key = load_or_generate_key(&key_path)?; let http_client = reqwest::Client::new(); let process_registry = services::process_registry::ProcessRegistry::default(); let activity_state = services::activity_state::ActivityState::default(); let db_arc = Arc::new(Mutex::new(conn)); app.manage(AppState { db: db_arc.clone(), encryption_key, http_client: http_client.clone(), process_registry: process_registry.clone(), activity_state: activity_state.clone(), }); // Start background poller services::poller::start( db_arc.clone(), encryption_key, http_client.clone(), app.handle().clone(), activity_state.clone(), ); services::graylog_poller::start( db_arc.clone(), encryption_key, http_client, app.handle().clone(), activity_state, ); // Start agent orchestrator services::orchestrator::start( db_arc.clone(), app.handle().clone(), process_registry.clone(), ); // Start agent task runner services::task_runner::start(db_arc, app.handle().clone(), process_registry); Ok(()) }) .invoke_handler(tauri::generate_handler![ commands::agent::create_agent, commands::agent::list_agents, commands::agent::get_agent, commands::agent::update_agent, commands::agent::delete_agent, commands::agent::improve_agent_prompt, commands::project::create_project, commands::project::list_projects, commands::project::get_project, commands::project::update_project, commands::project::delete_project, commands::credential::set_tuleap_credentials, commands::credential::get_tuleap_credentials, commands::credential::delete_tuleap_credentials, commands::credential::test_tuleap_connection, commands::graylog::set_graylog_credentials, commands::graylog::get_graylog_credentials, commands::graylog::delete_graylog_credentials, commands::graylog::test_graylog_connection, commands::graylog::manual_graylog_poll, commands::graylog::list_graylog_subjects, commands::graylog::list_graylog_detections, commands::tracker::add_tracker, commands::tracker::list_trackers, commands::tracker::update_tracker, commands::tracker::remove_tracker, commands::tracker::get_tracker_fields, commands::tracker::list_processed_tickets, commands::poller::manual_poll, commands::poller::get_queue_status, commands::poller::get_project_throughput, commands::poller::get_runtime_activity, commands::notification::list_notifications, commands::notification::mark_notification_read, commands::notification::mark_all_notifications_read, commands::module::list_project_modules, commands::module::set_project_module_enabled, commands::orchestrator::get_ticket_result, commands::orchestrator::retry_ticket, commands::orchestrator::cancel_ticket, commands::live_agent::create_live_session, commands::live_agent::list_live_sessions, commands::live_agent::list_live_messages, commands::live_agent::set_live_session_archived, commands::live_agent::send_live_message, commands::task::create_agent_task, commands::task::list_agent_tasks, commands::task::retry_agent_task, commands::task::cancel_agent_task, commands::worktree::list_worktrees, commands::worktree::get_worktree_diff, commands::worktree::apply_fix_to_branch, commands::worktree::delete_worktree_cmd, commands::worktree::list_local_branches, commands::worktree::list_local_branches_for_worktree, ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); } fn load_or_generate_key(path: &std::path::Path) -> Result<[u8; 32], Box> { use rand::RngCore; if path.exists() { return read_key(path); } let mut key = [0u8; 32]; rand::rngs::OsRng.fill_bytes(&mut key); match write_new_key(path, &key) { Ok(()) => Ok(key), Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => read_key(path), Err(err) => Err(err.into()), } } fn read_key(path: &std::path::Path) -> Result<[u8; 32], Box> { enforce_key_permissions(path)?; let bytes = std::fs::read(path)?; if bytes.len() != 32 { return Err("Invalid key file size".into()); } let mut key = [0u8; 32]; key.copy_from_slice(&bytes); Ok(key) } fn write_new_key(path: &std::path::Path, key: &[u8; 32]) -> std::io::Result<()> { #[cfg(unix)] { use std::fs::OpenOptions; use std::io::Write; use std::os::unix::fs::OpenOptionsExt; let mut file = OpenOptions::new() .create_new(true) .write(true) .mode(0o600) .open(path)?; file.write_all(key)?; file.sync_all()?; } #[cfg(not(unix))] { std::fs::write(path, key)?; } Ok(()) } fn enforce_key_permissions(path: &std::path::Path) -> std::io::Result<()> { #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; let metadata = std::fs::metadata(path)?; let current_mode = metadata.permissions().mode() & 0o777; if current_mode != 0o600 { let mut permissions = metadata.permissions(); permissions.set_mode(0o600); std::fs::set_permissions(path, permissions)?; } } #[cfg(not(unix))] { let _ = path; } Ok(()) } #[cfg(test)] mod tests { use super::*; use std::fs; use tempfile::tempdir; #[test] fn test_load_or_generate_key_creates_key() { let dir = tempdir().expect("tempdir should be created"); let path = dir.path().join("orchai.key"); let key = load_or_generate_key(&path).expect("key generation should succeed"); assert_eq!(key.len(), 32); let persisted = fs::read(&path).expect("key file should exist"); assert_eq!(persisted.len(), 32); assert_eq!(persisted, key); } #[test] fn test_load_or_generate_key_reads_existing_key() { let dir = tempdir().expect("tempdir should be created"); let path = dir.path().join("orchai.key"); let expected = [42u8; 32]; fs::write(&path, expected).expect("existing key should be written"); let loaded = load_or_generate_key(&path).expect("existing key should be loaded"); assert_eq!(loaded, expected); } #[cfg(unix)] #[test] fn test_load_or_generate_key_creates_private_permissions() { use std::os::unix::fs::PermissionsExt; let dir = tempdir().expect("tempdir should be created"); let path = dir.path().join("orchai.key"); load_or_generate_key(&path).expect("key generation should succeed"); let mode = fs::metadata(&path) .expect("metadata should be readable") .permissions() .mode() & 0o777; assert_eq!(mode, 0o600); } #[cfg(unix)] #[test] fn test_load_or_generate_key_hardens_existing_permissions() { use std::os::unix::fs::PermissionsExt; let dir = tempdir().expect("tempdir should be created"); let path = dir.path().join("orchai.key"); fs::write(&path, [7u8; 32]).expect("existing key should be written"); let mut permissions = fs::metadata(&path) .expect("metadata should be readable") .permissions(); permissions.set_mode(0o644); fs::set_permissions(&path, permissions).expect("permissions should be set"); load_or_generate_key(&path).expect("existing key should be loaded"); let mode = fs::metadata(&path) .expect("metadata should be readable") .permissions() .mode() & 0o777; assert_eq!(mode, 0o600); } }