diff --git a/v2/bterminal-core/src/sandbox.rs b/v2/bterminal-core/src/sandbox.rs index 70c77fe..2c41d1b 100644 --- a/v2/bterminal-core/src/sandbox.rs +++ b/v2/bterminal-core/src/sandbox.rs @@ -152,11 +152,18 @@ impl SandboxConfig { .restrict_self() .map_err(|e| format!("Landlock: restrict_self failed: {e}"))?; + // Landlock enforcement states: + // - Enforced: kernel 6.2+ with ABI V3 (full filesystem restriction) + // - NotEnforced: kernel 5.13–6.1 (Landlock exists but ABI too old for V3) + // - Error (caught above): kernel <5.13 (no Landlock LSM available) let enforced = status.ruleset != RulesetStatus::NotEnforced; if enforced { log::info!("Landlock sandbox applied ({} rw, {} ro paths)", self.rw_paths.len(), self.ro_paths.len()); } else { - log::warn!("Landlock sandbox was not enforced (kernel may lack support)"); + log::warn!( + "Landlock not enforced — sidecar runs without filesystem restrictions. \ + Kernel 6.2+ required for enforcement." + ); } Ok(enforced) diff --git a/v2/src-tauri/src/btmsg.rs b/v2/src-tauri/src/btmsg.rs index f3b5a8c..b5ca5aa 100644 --- a/v2/src-tauri/src/btmsg.rs +++ b/v2/src-tauri/src/btmsg.rs @@ -1724,6 +1724,41 @@ mod tests { assert!(json.get("agent_id").is_none(), "should not have snake_case"); } + // ---- WAL checkpoint tests ---- + + #[test] + fn test_checkpoint_wal_on_temp_database() { + let dir = tempfile::tempdir().unwrap(); + let db_path = dir.path().join("test_wal.db"); + + // Create a WAL-mode database with some data + { + let conn = Connection::open(&db_path).unwrap(); + conn.query_row("PRAGMA journal_mode=WAL", [], |_| Ok(())).unwrap(); + conn.execute_batch("CREATE TABLE t (id INTEGER PRIMARY KEY, val TEXT);").unwrap(); + for i in 0..100 { + conn.execute("INSERT INTO t (val) VALUES (?)", params![format!("row-{i}")]).unwrap(); + } + // Keep connection open while we checkpoint from another connection + // to simulate the real scenario (app holds a conn, background task checkpoints) + + // Run checkpoint from a separate connection (like the background task does) + let result = crate::checkpoint_wal(&db_path); + assert!(result.is_ok(), "checkpoint_wal should succeed: {:?}", result); + } + + // Verify data is still intact after checkpoint + let conn = Connection::open(&db_path).unwrap(); + let count: i64 = conn.query_row("SELECT COUNT(*) FROM t", [], |row| row.get(0)).unwrap(); + assert_eq!(count, 100, "all rows should survive checkpoint"); + } + + #[test] + fn test_checkpoint_wal_nonexistent_db_is_ok() { + let result = crate::checkpoint_wal(std::path::Path::new("/tmp/nonexistent_bterminal_test.db")); + assert!(result.is_ok(), "checkpoint_wal should return Ok for missing DB"); + } + #[test] fn test_contact_permissions_bidirectional() { let conn = test_db(); diff --git a/v2/src-tauri/src/lib.rs b/v2/src-tauri/src/lib.rs index cb85294..619e634 100644 --- a/v2/src-tauri/src/lib.rs +++ b/v2/src-tauri/src/lib.rs @@ -25,7 +25,7 @@ use session::SessionDb; use sidecar::{SidecarConfig, SidecarManager}; use fs_watcher::ProjectFsWatcher; use watcher::FileWatcherManager; -use std::path::Path; +use std::path::{Path, PathBuf}; use std::sync::Arc; use tauri::Manager; @@ -104,6 +104,40 @@ fn install_cli_tools(resource_dir: &Path, dev_root: &Path) { } } +/// Run `PRAGMA wal_checkpoint(TRUNCATE)` on a SQLite database to reclaim WAL file space. +/// Returns Ok(()) on success or Err with a diagnostic message. +pub(crate) fn checkpoint_wal(path: &Path) -> Result<(), String> { + use rusqlite::{Connection, OpenFlags}; + + if !path.exists() { + return Ok(()); // DB doesn't exist yet — nothing to checkpoint + } + + let conn = Connection::open_with_flags(path, OpenFlags::SQLITE_OPEN_READ_WRITE) + .map_err(|e| format!("WAL checkpoint: failed to open {}: {e}", path.display()))?; + conn.query_row("PRAGMA busy_timeout = 5000", [], |_| Ok(())) + .map_err(|e| format!("WAL checkpoint: failed to set busy_timeout: {e}"))?; + conn.query_row("PRAGMA wal_checkpoint(TRUNCATE)", [], |_| Ok(())) + .map_err(|e| format!("WAL checkpoint failed on {}: {e}", path.display()))?; + Ok(()) +} + +/// Spawn a background task that checkpoints WAL on both databases every 5 minutes. +fn spawn_wal_checkpoint_task(sessions_db_path: PathBuf, btmsg_db_path: PathBuf) { + tokio::spawn(async move { + let interval = std::time::Duration::from_secs(300); + loop { + tokio::time::sleep(interval).await; + for (label, path) in [("sessions.db", &sessions_db_path), ("btmsg.db", &btmsg_db_path)] { + match checkpoint_wal(path) { + Ok(()) => tracing::info!("WAL checkpoint completed for {label}"), + Err(e) => tracing::warn!("WAL checkpoint error for {label}: {e}"), + } + } + } + }); +} + #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { // Force dark GTK theme for native dialogs (file chooser, etc.) @@ -347,6 +381,11 @@ pub fn run() { Err(e) => log::warn!("Sidecar startup failed (agent features unavailable): {e}"), } + // Start periodic WAL checkpoint task (every 5 minutes) + let sessions_db_path = config.data_dir.join("sessions.db"); + let btmsg_db_path = config.btmsg_db_path(); + spawn_wal_checkpoint_task(sessions_db_path, btmsg_db_path); + app.manage(AppState { pty_manager, sidecar_manager,