284 lines
9.4 KiB
Rust
284 lines
9.4 KiB
Rust
mod commands;
|
|
mod db;
|
|
mod error;
|
|
mod models;
|
|
mod services;
|
|
|
|
use std::sync::{Arc, Mutex};
|
|
use tauri::Manager;
|
|
|
|
pub struct AppState {
|
|
pub db: Arc<Mutex<rusqlite::Connection>>,
|
|
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<dyn std::error::Error>> {
|
|
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<dyn std::error::Error>> {
|
|
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);
|
|
}
|
|
}
|