a very good jj gui
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

chore: add Tauri v2 skills documentation

Add comprehensive skills for Claude to reference when working with Tauri:
- Project setup and configuration
- Frontend integration (Vite, Next.js, Nuxt, SvelteKit, Qwik)
- IPC and events communication
- Plugin development
- Security (capabilities, permissions, CSP, scopes)
- Deployment (macOS, Linux, GitHub Actions)
- Debugging and testing
- Binary optimization

+13972
+481
.agents/skills/adding-tauri-splashscreen/SKILL.md
··· 1 + --- 2 + name: adding-tauri-splashscreen 3 + description: Guides the user through adding a Tauri splashscreen, splash screen, loading screen, or startup screen to their application. Covers configuration, custom splash HTML, closing the splash when ready, and styling. 4 + --- 5 + 6 + # Tauri Splashscreen Implementation 7 + 8 + This skill covers implementing splash screens in Tauri v2 applications. A splash screen displays during application startup while the main window loads and initializes. 9 + 10 + ## Overview 11 + 12 + The splash screen pattern involves: 13 + 1. Showing a splash window immediately on launch 14 + 2. Hiding the main window until ready 15 + 3. Performing initialization tasks (frontend and backend) 16 + 4. Closing splash and showing main window when complete 17 + 18 + ## Configuration 19 + 20 + ### Window Configuration 21 + 22 + Configure both windows in `tauri.conf.json`: 23 + 24 + ```json 25 + { 26 + "app": { 27 + "windows": [ 28 + { 29 + "label": "main", 30 + "title": "My Application", 31 + "width": 1200, 32 + "height": 800, 33 + "visible": false 34 + }, 35 + { 36 + "label": "splashscreen", 37 + "title": "Loading", 38 + "url": "splashscreen.html", 39 + "width": 400, 40 + "height": 300, 41 + "center": true, 42 + "resizable": false, 43 + "decorations": false, 44 + "transparent": true, 45 + "alwaysOnTop": true 46 + } 47 + ] 48 + } 49 + } 50 + ``` 51 + 52 + Key settings: 53 + - `"visible": false` on main window - hides it until ready 54 + - `"url": "splashscreen.html"` - points to splash screen HTML 55 + - `"decorations": false` - removes window chrome for cleaner look 56 + - `"transparent": true` - enables transparent backgrounds 57 + - `"alwaysOnTop": true` - keeps splash visible during loading 58 + 59 + ## Splash Screen HTML 60 + 61 + Create `splashscreen.html` in your frontend source directory (e.g., `src/` or `public/`): 62 + 63 + ```html 64 + <!DOCTYPE html> 65 + <html lang="en"> 66 + <head> 67 + <meta charset="UTF-8"> 68 + <meta name="viewport" content="width=device-width, initial-scale=1.0"> 69 + <title>Loading</title> 70 + <style> 71 + * { 72 + margin: 0; 73 + padding: 0; 74 + box-sizing: border-box; 75 + } 76 + 77 + html, body { 78 + height: 100%; 79 + overflow: hidden; 80 + background: transparent; 81 + } 82 + 83 + .splash-container { 84 + display: flex; 85 + flex-direction: column; 86 + align-items: center; 87 + justify-content: center; 88 + height: 100vh; 89 + background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); 90 + border-radius: 12px; 91 + color: white; 92 + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; 93 + } 94 + 95 + .logo { 96 + width: 80px; 97 + height: 80px; 98 + margin-bottom: 24px; 99 + } 100 + 101 + .app-name { 102 + font-size: 24px; 103 + font-weight: 600; 104 + margin-bottom: 16px; 105 + } 106 + 107 + .loading-spinner { 108 + width: 40px; 109 + height: 40px; 110 + border: 3px solid rgba(255, 255, 255, 0.3); 111 + border-top-color: #4f46e5; 112 + border-radius: 50%; 113 + animation: spin 1s linear infinite; 114 + } 115 + 116 + @keyframes spin { 117 + to { transform: rotate(360deg); } 118 + } 119 + 120 + .loading-text { 121 + margin-top: 16px; 122 + font-size: 14px; 123 + color: rgba(255, 255, 255, 0.7); 124 + } 125 + </style> 126 + </head> 127 + <body> 128 + <div class="splash-container"> 129 + <!-- Replace with your logo --> 130 + <svg class="logo" viewBox="0 0 100 100" fill="none"> 131 + <circle cx="50" cy="50" r="45" stroke="#4f46e5" stroke-width="4"/> 132 + <path d="M30 50 L45 65 L70 35" stroke="#4f46e5" stroke-width="4" fill="none"/> 133 + </svg> 134 + <div class="app-name">My Application</div> 135 + <div class="loading-spinner"></div> 136 + <div class="loading-text">Loading...</div> 137 + </div> 138 + </body> 139 + </html> 140 + ``` 141 + 142 + ## Frontend Setup 143 + 144 + ### TypeScript/JavaScript Implementation 145 + 146 + In your main entry file (e.g., `src/main.ts`): 147 + 148 + ```typescript 149 + import { invoke } from '@tauri-apps/api/core'; 150 + 151 + // Helper function for delays 152 + function sleep(seconds: number): Promise<void> { 153 + return new Promise(resolve => setTimeout(resolve, seconds * 1000)); 154 + } 155 + 156 + // Frontend initialization 157 + async function initializeFrontend(): Promise<void> { 158 + // Perform frontend setup tasks here: 159 + // - Load configuration 160 + // - Initialize state management 161 + // - Set up routing 162 + // - Preload critical assets 163 + 164 + // Example: simulate initialization time 165 + await sleep(1); 166 + 167 + // Notify backend that frontend is ready 168 + await invoke('set_complete', { task: 'frontend' }); 169 + } 170 + 171 + // Start initialization when DOM is ready 172 + document.addEventListener('DOMContentLoaded', () => { 173 + initializeFrontend().catch(console.error); 174 + }); 175 + ``` 176 + 177 + ### Alternative: Using Window Events 178 + 179 + ```typescript 180 + import { invoke } from '@tauri-apps/api/core'; 181 + import { getCurrentWindow } from '@tauri-apps/api/window'; 182 + 183 + async function initializeFrontend(): Promise<void> { 184 + // Your initialization logic 185 + const config = await loadConfig(); 186 + await setupRouter(); 187 + await preloadAssets(); 188 + 189 + // Signal completion 190 + await invoke('set_complete', { task: 'frontend' }); 191 + } 192 + 193 + // Wait for window to be fully ready 194 + getCurrentWindow().once('tauri://created', () => { 195 + initializeFrontend(); 196 + }); 197 + ``` 198 + 199 + ## Backend Setup 200 + 201 + ### Add Tokio Dependency 202 + 203 + ```bash 204 + cargo add tokio --features time 205 + ``` 206 + 207 + ### Rust Implementation 208 + 209 + In `src-tauri/src/lib.rs`: 210 + 211 + ```rust 212 + use std::sync::Mutex; 213 + use tauri::{AppHandle, Manager, State}; 214 + 215 + // Track initialization state 216 + struct SetupState { 217 + frontend_task: bool, 218 + backend_task: bool, 219 + } 220 + 221 + impl Default for SetupState { 222 + fn default() -> Self { 223 + Self { 224 + frontend_task: false, 225 + backend_task: false, 226 + } 227 + } 228 + } 229 + 230 + // Command to mark tasks complete 231 + #[tauri::command] 232 + async fn set_complete( 233 + app: AppHandle, 234 + state: State<'_, Mutex<SetupState>>, 235 + task: String, 236 + ) -> Result<(), String> { 237 + let mut state = state.lock().map_err(|e| e.to_string())?; 238 + 239 + match task.as_str() { 240 + "frontend" => state.frontend_task = true, 241 + "backend" => state.backend_task = true, 242 + _ => return Err(format!("Unknown task: {}", task)), 243 + } 244 + 245 + // Check if all tasks are complete 246 + if state.frontend_task && state.backend_task { 247 + // Close splash and show main window 248 + if let Some(splash) = app.get_webview_window("splashscreen") { 249 + splash.close().map_err(|e| e.to_string())?; 250 + } 251 + 252 + if let Some(main) = app.get_webview_window("main") { 253 + main.show().map_err(|e| e.to_string())?; 254 + main.set_focus().map_err(|e| e.to_string())?; 255 + } 256 + } 257 + 258 + Ok(()) 259 + } 260 + 261 + // Backend initialization 262 + async fn setup_backend(app: AppHandle) { 263 + // IMPORTANT: Use tokio::time::sleep, NOT std::thread::sleep 264 + // std::thread::sleep blocks the entire async runtime 265 + 266 + // Perform backend initialization: 267 + // - Database connections 268 + // - Load configuration 269 + // - Initialize services 270 + 271 + // Example: simulate work 272 + tokio::time::sleep(tokio::time::Duration::from_secs(2)).await; 273 + 274 + // Mark backend as complete 275 + if let Some(state) = app.try_state::<Mutex<SetupState>>() { 276 + let _ = set_complete( 277 + app.clone(), 278 + state, 279 + "backend".to_string(), 280 + ).await; 281 + } 282 + } 283 + 284 + #[cfg_attr(mobile, tauri::mobile_entry_point)] 285 + pub fn run() { 286 + tauri::Builder::default() 287 + .manage(Mutex::new(SetupState::default())) 288 + .invoke_handler(tauri::generate_handler![set_complete]) 289 + .setup(|app| { 290 + let handle = app.handle().clone(); 291 + 292 + // Spawn backend initialization 293 + tauri::async_runtime::spawn(async move { 294 + setup_backend(handle).await; 295 + }); 296 + 297 + Ok(()) 298 + }) 299 + .run(tauri::generate_context!()) 300 + .expect("error while running tauri application"); 301 + } 302 + ``` 303 + 304 + ## Simple Implementation 305 + 306 + For simpler cases where you only need to wait for the frontend: 307 + 308 + ### Configuration 309 + 310 + ```json 311 + { 312 + "app": { 313 + "windows": [ 314 + { 315 + "label": "main", 316 + "visible": false 317 + }, 318 + { 319 + "label": "splashscreen", 320 + "url": "splashscreen.html", 321 + "width": 400, 322 + "height": 300, 323 + "decorations": false 324 + } 325 + ] 326 + } 327 + } 328 + ``` 329 + 330 + ### Frontend 331 + 332 + ```typescript 333 + import { invoke } from '@tauri-apps/api/core'; 334 + 335 + async function init() { 336 + // Initialize your app 337 + await setupApp(); 338 + 339 + // Close splash, show main 340 + await invoke('close_splashscreen'); 341 + } 342 + 343 + document.addEventListener('DOMContentLoaded', init); 344 + ``` 345 + 346 + ### Backend 347 + 348 + ```rust 349 + use tauri::{AppHandle, Manager}; 350 + 351 + #[tauri::command] 352 + async fn close_splashscreen(app: AppHandle) -> Result<(), String> { 353 + if let Some(splash) = app.get_webview_window("splashscreen") { 354 + splash.close().map_err(|e| e.to_string())?; 355 + } 356 + 357 + if let Some(main) = app.get_webview_window("main") { 358 + main.show().map_err(|e| e.to_string())?; 359 + main.set_focus().map_err(|e| e.to_string())?; 360 + } 361 + 362 + Ok(()) 363 + } 364 + 365 + #[cfg_attr(mobile, tauri::mobile_entry_point)] 366 + pub fn run() { 367 + tauri::Builder::default() 368 + .invoke_handler(tauri::generate_handler![close_splashscreen]) 369 + .run(tauri::generate_context!()) 370 + .expect("error while running tauri application"); 371 + } 372 + ``` 373 + 374 + ## Styling Variations 375 + 376 + ### Minimal Splash 377 + 378 + ```html 379 + <style> 380 + .splash-container { 381 + display: flex; 382 + align-items: center; 383 + justify-content: center; 384 + height: 100vh; 385 + background: #ffffff; 386 + } 387 + 388 + .logo { 389 + width: 120px; 390 + animation: pulse 2s ease-in-out infinite; 391 + } 392 + 393 + @keyframes pulse { 394 + 0%, 100% { opacity: 1; transform: scale(1); } 395 + 50% { opacity: 0.7; transform: scale(0.95); } 396 + } 397 + </style> 398 + ``` 399 + 400 + ### Progress Bar Splash 401 + 402 + ```html 403 + <style> 404 + .progress-container { 405 + width: 200px; 406 + height: 4px; 407 + background: rgba(255, 255, 255, 0.2); 408 + border-radius: 2px; 409 + overflow: hidden; 410 + margin-top: 20px; 411 + } 412 + 413 + .progress-bar { 414 + height: 100%; 415 + background: #4f46e5; 416 + animation: progress 2s ease-in-out infinite; 417 + } 418 + 419 + @keyframes progress { 420 + 0% { width: 0%; } 421 + 50% { width: 70%; } 422 + 100% { width: 100%; } 423 + } 424 + </style> 425 + 426 + <div class="progress-container"> 427 + <div class="progress-bar"></div> 428 + </div> 429 + ``` 430 + 431 + ### Dark Theme with Glow 432 + 433 + ```html 434 + <style> 435 + .splash-container { 436 + background: #0a0a0a; 437 + color: #ffffff; 438 + } 439 + 440 + .logo { 441 + filter: drop-shadow(0 0 20px rgba(79, 70, 229, 0.5)); 442 + } 443 + 444 + .app-name { 445 + text-shadow: 0 0 20px rgba(79, 70, 229, 0.5); 446 + } 447 + </style> 448 + ``` 449 + 450 + ## Important Notes 451 + 452 + 1. **Async Sleep**: Always use `tokio::time::sleep` in async Rust code, never `std::thread::sleep`. The latter blocks the entire runtime. 453 + 454 + 2. **Window Labels**: Ensure window labels in code match those in `tauri.conf.json`. 455 + 456 + 3. **Error Handling**: The splash screen should handle errors gracefully. If initialization fails, show the main window anyway with an error state. 457 + 458 + 4. **Timing**: Keep splash screen visible long enough for branding but not so long it frustrates users. Aim for 1-3 seconds minimum. 459 + 460 + 5. **Transparent Windows**: When using `transparent: true`, ensure your HTML has `background: transparent` on `html` and `body` elements. 461 + 462 + 6. **Mobile Considerations**: On mobile platforms, splash screens work differently. Consider using platform-native splash screens for iOS and Android. 463 + 464 + ## Troubleshooting 465 + 466 + **Splash screen doesn't appear:** 467 + - Verify the URL path is correct in `tauri.conf.json` 468 + - Check that the HTML file exists in the correct location 469 + 470 + **Main window shows too early:** 471 + - Ensure `visible: false` is set on the main window 472 + - Verify the `set_complete` command is being called correctly 473 + 474 + **Transparent background not working:** 475 + - Set `transparent: true` in window config 476 + - Set `background: transparent` in CSS for html and body 477 + - On some platforms, you may need `decorations: false` 478 + 479 + **Window position issues:** 480 + - Use `center: true` for centered splash screens 481 + - Or specify explicit `x` and `y` coordinates
+413
.agents/skills/adding-tauri-system-tray/SKILL.md
··· 1 + --- 2 + name: adding-tauri-system-tray 3 + description: Guides the user through implementing Tauri system tray functionality, including tray icon setup, tray menu creation, handling tray events, and updating the tray at runtime in the notification area. 4 + --- 5 + 6 + # Tauri System Tray Implementation 7 + 8 + This skill covers implementing system tray (notification area) functionality in Tauri v2 applications. 9 + 10 + ## Configuration 11 + 12 + Enable the tray-icon feature in `src-tauri/Cargo.toml`: 13 + 14 + ```toml 15 + [dependencies] 16 + tauri = { version = "2", features = ["tray-icon"] } 17 + ``` 18 + 19 + ## Basic Tray Setup 20 + 21 + Create a tray icon in `src-tauri/src/lib.rs`: 22 + 23 + ```rust 24 + use tauri::tray::TrayIconBuilder; 25 + 26 + #[cfg_attr(mobile, tauri::mobile_entry_point)] 27 + pub fn run() { 28 + tauri::Builder::default() 29 + .setup(|app| { 30 + // Use TrayIconBuilder::with_id() to reference the tray later 31 + let tray = TrayIconBuilder::with_id(app, "main-tray") 32 + .icon(app.default_window_icon().unwrap().clone()) 33 + .tooltip("My Tauri App") 34 + .build(app)?; 35 + Ok(()) 36 + }) 37 + .run(tauri::generate_context!()) 38 + .expect("error while running tauri application"); 39 + } 40 + ``` 41 + 42 + ## Tray Menu 43 + 44 + ### Basic Menu with Items 45 + 46 + ```rust 47 + use tauri::{ 48 + menu::{Menu, MenuItem}, 49 + tray::TrayIconBuilder, 50 + }; 51 + 52 + #[cfg_attr(mobile, tauri::mobile_entry_point)] 53 + pub fn run() { 54 + tauri::Builder::default() 55 + .setup(|app| { 56 + let show_item = MenuItem::with_id(app, "show", "Show Window", true, None::<&str>)?; 57 + let hide_item = MenuItem::with_id(app, "hide", "Hide Window", true, None::<&str>)?; 58 + let quit_item = MenuItem::with_id(app, "quit", "Quit", true, None::<&str>)?; 59 + 60 + let menu = Menu::with_items(app, &[&show_item, &hide_item, &quit_item])?; 61 + 62 + let tray = TrayIconBuilder::new() 63 + .icon(app.default_window_icon().unwrap().clone()) 64 + .menu(&menu) 65 + .menu_on_left_click(false) // Only show menu on right-click 66 + .build(app)?; 67 + 68 + Ok(()) 69 + }) 70 + .run(tauri::generate_context!()) 71 + .expect("error while running tauri application"); 72 + } 73 + ``` 74 + 75 + ### Menu with Separators and Submenus 76 + 77 + ```rust 78 + use tauri::{ 79 + menu::{Menu, MenuItem, PredefinedMenuItem, Submenu}, 80 + tray::TrayIconBuilder, 81 + }; 82 + 83 + #[cfg_attr(mobile, tauri::mobile_entry_point)] 84 + pub fn run() { 85 + tauri::Builder::default() 86 + .setup(|app| { 87 + let option1 = MenuItem::with_id(app, "option1", "Option 1", true, None::<&str>)?; 88 + let option2 = MenuItem::with_id(app, "option2", "Option 2", true, None::<&str>)?; 89 + let options_submenu = Submenu::with_items(app, "Options", true, &[&option1, &option2])?; 90 + 91 + let show_item = MenuItem::with_id(app, "show", "Show Window", true, None::<&str>)?; 92 + let separator = PredefinedMenuItem::separator(app)?; 93 + let quit_item = MenuItem::with_id(app, "quit", "Quit", true, None::<&str>)?; 94 + 95 + let menu = Menu::with_items( 96 + app, 97 + &[&show_item, &options_submenu, &separator, &quit_item], 98 + )?; 99 + 100 + let tray = TrayIconBuilder::new() 101 + .icon(app.default_window_icon().unwrap().clone()) 102 + .menu(&menu) 103 + .build(app)?; 104 + 105 + Ok(()) 106 + }) 107 + .run(tauri::generate_context!()) 108 + .expect("error while running tauri application"); 109 + } 110 + ``` 111 + 112 + ## Handling Tray Events 113 + 114 + ### Menu Item Events 115 + 116 + ```rust 117 + use tauri::{ 118 + menu::{Menu, MenuItem}, 119 + tray::TrayIconBuilder, 120 + Manager, 121 + }; 122 + 123 + #[cfg_attr(mobile, tauri::mobile_entry_point)] 124 + pub fn run() { 125 + tauri::Builder::default() 126 + .setup(|app| { 127 + let show_item = MenuItem::with_id(app, "show", "Show Window", true, None::<&str>)?; 128 + let hide_item = MenuItem::with_id(app, "hide", "Hide Window", true, None::<&str>)?; 129 + let quit_item = MenuItem::with_id(app, "quit", "Quit", true, None::<&str>)?; 130 + let menu = Menu::with_items(app, &[&show_item, &hide_item, &quit_item])?; 131 + 132 + let tray = TrayIconBuilder::new() 133 + .icon(app.default_window_icon().unwrap().clone()) 134 + .menu(&menu) 135 + .on_menu_event(|app, event| { 136 + match event.id.as_ref() { 137 + "show" => { 138 + if let Some(window) = app.get_webview_window("main") { 139 + let _ = window.show(); 140 + let _ = window.set_focus(); 141 + } 142 + } 143 + "hide" => { 144 + if let Some(window) = app.get_webview_window("main") { 145 + let _ = window.hide(); 146 + } 147 + } 148 + "quit" => app.exit(0), 149 + _ => println!("Unhandled menu item: {:?}", event.id), 150 + } 151 + }) 152 + .build(app)?; 153 + 154 + Ok(()) 155 + }) 156 + .run(tauri::generate_context!()) 157 + .expect("error while running tauri application"); 158 + } 159 + ``` 160 + 161 + ### Tray Icon Mouse Events 162 + 163 + ```rust 164 + use tauri::{ 165 + tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent}, 166 + Manager, 167 + }; 168 + 169 + #[cfg_attr(mobile, tauri::mobile_entry_point)] 170 + pub fn run() { 171 + tauri::Builder::default() 172 + .setup(|app| { 173 + let tray = TrayIconBuilder::new() 174 + .icon(app.default_window_icon().unwrap().clone()) 175 + .on_tray_icon_event(|tray, event| { 176 + match event { 177 + TrayIconEvent::Click { 178 + button: MouseButton::Left, 179 + button_state: MouseButtonState::Up, 180 + .. 181 + } => { 182 + let app = tray.app_handle(); 183 + if let Some(window) = app.get_webview_window("main") { 184 + let _ = window.unminimize(); 185 + let _ = window.show(); 186 + let _ = window.set_focus(); 187 + } 188 + } 189 + TrayIconEvent::DoubleClick { button: MouseButton::Left, .. } => { 190 + let app = tray.app_handle(); 191 + if let Some(window) = app.get_webview_window("main") { 192 + if window.is_visible().unwrap_or(false) { 193 + let _ = window.hide(); 194 + } else { 195 + let _ = window.show(); 196 + let _ = window.set_focus(); 197 + } 198 + } 199 + } 200 + TrayIconEvent::Enter { .. } => println!("Mouse entered tray"), 201 + TrayIconEvent::Leave { .. } => println!("Mouse left tray"), 202 + _ => {} 203 + } 204 + }) 205 + .build(app)?; 206 + 207 + Ok(()) 208 + }) 209 + .run(tauri::generate_context!()) 210 + .expect("error while running tauri application"); 211 + } 212 + ``` 213 + 214 + Note: `Enter`, `Move`, and `Leave` events are not supported on Linux. 215 + 216 + ## Updating Tray at Runtime 217 + 218 + ### Update Icon and Tooltip 219 + 220 + ```rust 221 + use tauri::{image::Image, tray::TrayIconBuilder, Manager}; 222 + 223 + #[tauri::command] 224 + fn update_tray_icon(app: tauri::AppHandle, icon_path: String) -> Result<(), String> { 225 + if let Some(tray) = app.tray_by_id("main-tray") { 226 + let icon = Image::from_path(&icon_path).map_err(|e| e.to_string())?; 227 + tray.set_icon(Some(icon)).map_err(|e| e.to_string())?; 228 + } 229 + Ok(()) 230 + } 231 + 232 + #[tauri::command] 233 + fn update_tray_tooltip(app: tauri::AppHandle, tooltip: String) -> Result<(), String> { 234 + if let Some(tray) = app.tray_by_id("main-tray") { 235 + tray.set_tooltip(Some(&tooltip)).map_err(|e| e.to_string())?; 236 + } 237 + Ok(()) 238 + } 239 + ``` 240 + 241 + ### Update Menu Items Dynamically 242 + 243 + ```rust 244 + use std::sync::Mutex; 245 + use tauri::{ 246 + menu::{Menu, MenuItem, MenuItemKind}, 247 + tray::TrayIconBuilder, 248 + Manager, 249 + }; 250 + 251 + struct AppState { 252 + menu: Mutex<Option<Menu<tauri::Wry>>>, 253 + } 254 + 255 + #[tauri::command] 256 + fn toggle_menu_item(app: tauri::AppHandle, item_id: String, enabled: bool) -> Result<(), String> { 257 + let state = app.state::<AppState>(); 258 + if let Some(menu) = state.menu.lock().unwrap().as_ref() { 259 + if let Some(MenuItemKind::MenuItem(item)) = menu.get(&item_id) { 260 + item.set_enabled(enabled).map_err(|e| e.to_string())?; 261 + } 262 + } 263 + Ok(()) 264 + } 265 + 266 + #[tauri::command] 267 + fn update_menu_text(app: tauri::AppHandle, item_id: String, text: String) -> Result<(), String> { 268 + let state = app.state::<AppState>(); 269 + if let Some(menu) = state.menu.lock().unwrap().as_ref() { 270 + if let Some(MenuItemKind::MenuItem(item)) = menu.get(&item_id) { 271 + item.set_text(&text).map_err(|e| e.to_string())?; 272 + } 273 + } 274 + Ok(()) 275 + } 276 + ``` 277 + 278 + ### Replace Entire Menu 279 + 280 + ```rust 281 + use tauri::{menu::{Menu, MenuItem}, Manager}; 282 + 283 + #[tauri::command] 284 + fn set_connected_menu(app: tauri::AppHandle) -> Result<(), String> { 285 + if let Some(tray) = app.tray_by_id("main-tray") { 286 + let disconnect = MenuItem::with_id(&app, "disconnect", "Disconnect", true, None::<&str>) 287 + .map_err(|e| e.to_string())?; 288 + let status = MenuItem::with_id(&app, "status", "Connected", false, None::<&str>) 289 + .map_err(|e| e.to_string())?; 290 + let quit = MenuItem::with_id(&app, "quit", "Quit", true, None::<&str>) 291 + .map_err(|e| e.to_string())?; 292 + 293 + let menu = Menu::with_items(&app, &[&status, &disconnect, &quit]) 294 + .map_err(|e| e.to_string())?; 295 + tray.set_menu(Some(menu)).map_err(|e| e.to_string())?; 296 + } 297 + Ok(()) 298 + } 299 + ``` 300 + 301 + ## Complete Example 302 + 303 + ```rust 304 + use std::sync::Mutex; 305 + use tauri::{ 306 + menu::{Menu, MenuItem, PredefinedMenuItem}, 307 + tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent}, 308 + Manager, 309 + }; 310 + 311 + struct TrayState { 312 + is_paused: Mutex<bool>, 313 + } 314 + 315 + #[tauri::command] 316 + fn get_tray_status(state: tauri::State<TrayState>) -> bool { 317 + *state.is_paused.lock().unwrap() 318 + } 319 + 320 + #[cfg_attr(mobile, tauri::mobile_entry_point)] 321 + pub fn run() { 322 + tauri::Builder::default() 323 + .manage(TrayState { is_paused: Mutex::new(false) }) 324 + .setup(|app| { 325 + let show = MenuItem::with_id(app, "show", "Show Window", true, None::<&str>)?; 326 + let hide = MenuItem::with_id(app, "hide", "Hide Window", true, None::<&str>)?; 327 + let sep = PredefinedMenuItem::separator(app)?; 328 + let pause = MenuItem::with_id(app, "pause", "Pause", true, None::<&str>)?; 329 + let quit = MenuItem::with_id(app, "quit", "Quit", true, None::<&str>)?; 330 + 331 + let menu = Menu::with_items(app, &[&show, &hide, &sep, &pause, &quit])?; 332 + 333 + let _tray = TrayIconBuilder::with_id(app, "main-tray") 334 + .icon(app.default_window_icon().unwrap().clone()) 335 + .tooltip("My Tauri App - Running") 336 + .menu(&menu) 337 + .menu_on_left_click(false) 338 + .on_menu_event(|app, event| { 339 + match event.id.as_ref() { 340 + "show" => { 341 + if let Some(w) = app.get_webview_window("main") { 342 + let _ = w.show(); 343 + let _ = w.set_focus(); 344 + } 345 + } 346 + "hide" => { 347 + if let Some(w) = app.get_webview_window("main") { 348 + let _ = w.hide(); 349 + } 350 + } 351 + "pause" => { 352 + let state = app.state::<TrayState>(); 353 + let mut paused = state.is_paused.lock().unwrap(); 354 + *paused = !*paused; 355 + if let Some(tray) = app.tray_by_id("main-tray") { 356 + let tip = if *paused { "Paused" } else { "Running" }; 357 + let _ = tray.set_tooltip(Some(tip)); 358 + } 359 + } 360 + "quit" => app.exit(0), 361 + _ => {} 362 + } 363 + }) 364 + .on_tray_icon_event(|tray, event| { 365 + if let TrayIconEvent::Click { 366 + button: MouseButton::Left, 367 + button_state: MouseButtonState::Up, .. 368 + } = event { 369 + let app = tray.app_handle(); 370 + if let Some(w) = app.get_webview_window("main") { 371 + if w.is_visible().unwrap_or(false) { 372 + let _ = w.hide(); 373 + } else { 374 + let _ = w.show(); 375 + let _ = w.set_focus(); 376 + } 377 + } 378 + } 379 + }) 380 + .build(app)?; 381 + 382 + Ok(()) 383 + }) 384 + .invoke_handler(tauri::generate_handler![get_tray_status]) 385 + .run(tauri::generate_context!()) 386 + .expect("error while running tauri application"); 387 + } 388 + ``` 389 + 390 + ## Platform Notes 391 + 392 + | Platform | Support | 393 + |----------|---------| 394 + | Windows | Full support for all tray events | 395 + | macOS | Full support for all tray events | 396 + | Linux | `Enter`, `Move`, `Leave` events not supported | 397 + 398 + ## Troubleshooting 399 + 400 + **Tray icon not appearing:** 401 + - Ensure `tray-icon` feature is enabled in `Cargo.toml` 402 + - Verify the icon is valid and accessible 403 + - Check that `build()` is called and result is stored 404 + 405 + **Menu not showing:** 406 + - Confirm menu is attached with `.menu(&menu)` 407 + - Check `menu_on_left_click` setting 408 + - Verify menu items are created correctly 409 + 410 + **Events not firing:** 411 + - Ensure event handlers are attached before `build()` 412 + - Check pattern matching in event handlers 413 + - Verify tray ID matches when using `tray_by_id()`
+426
.agents/skills/building-tauri-with-github-actions/SKILL.md
··· 1 + --- 2 + name: building-tauri-with-github-actions 3 + description: Guides users through setting up Tauri GitHub Actions CI/CD pipelines and workflows for automated building, testing, and releasing cross-platform desktop applications. 4 + --- 5 + 6 + # Building Tauri Apps with GitHub Actions 7 + 8 + This skill covers CI/CD pipeline configuration for Tauri applications using GitHub Actions and the official `tauri-apps/tauri-action`. 9 + 10 + ## Overview 11 + 12 + GitHub Actions enables automated building, testing, and releasing of Tauri applications across Windows, macOS, and Linux platforms. The `tauri-action` handles the complexity of cross-platform builds and release management. 13 + 14 + ## Workflow Triggers 15 + 16 + ### Push to Release Branch 17 + 18 + ```yaml 19 + name: 'publish' 20 + on: 21 + workflow_dispatch: 22 + push: 23 + branches: 24 + - release 25 + ``` 26 + 27 + ### Tag-Based Releases 28 + 29 + ```yaml 30 + name: 'publish' 31 + on: 32 + push: 33 + tags: 34 + - 'app-v*' 35 + ``` 36 + 37 + ## Platform Matrix Configuration 38 + 39 + ### Standard Multi-Platform Matrix 40 + 41 + ```yaml 42 + jobs: 43 + publish-tauri: 44 + permissions: 45 + contents: write 46 + strategy: 47 + fail-fast: false 48 + matrix: 49 + include: 50 + - platform: 'macos-latest' 51 + args: '--target aarch64-apple-darwin' 52 + - platform: 'macos-latest' 53 + args: '--target x86_64-apple-darwin' 54 + - platform: 'ubuntu-22.04' 55 + args: '' 56 + - platform: 'windows-latest' 57 + args: '' 58 + 59 + runs-on: ${{ matrix.platform }} 60 + ``` 61 + 62 + ### With ARM Linux Support (Public Repos Only) 63 + 64 + Add `ubuntu-22.04-arm` to the matrix for native ARM64 Linux builds (public repositories only). 65 + 66 + ## Complete Workflow Example 67 + 68 + ```yaml 69 + name: 'publish' 70 + 71 + on: 72 + workflow_dispatch: 73 + push: 74 + branches: 75 + - release 76 + 77 + jobs: 78 + publish-tauri: 79 + permissions: 80 + contents: write 81 + strategy: 82 + fail-fast: false 83 + matrix: 84 + include: 85 + - platform: 'macos-latest' 86 + args: '--target aarch64-apple-darwin' 87 + - platform: 'macos-latest' 88 + args: '--target x86_64-apple-darwin' 89 + - platform: 'ubuntu-22.04' 90 + args: '' 91 + - platform: 'windows-latest' 92 + args: '' 93 + 94 + runs-on: ${{ matrix.platform }} 95 + steps: 96 + - uses: actions/checkout@v4 97 + 98 + - name: Install dependencies (Ubuntu only) 99 + if: matrix.platform == 'ubuntu-22.04' 100 + run: | 101 + sudo apt-get update 102 + sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf 103 + 104 + - name: Setup Node.js 105 + uses: actions/setup-node@v4 106 + with: 107 + node-version: lts/* 108 + cache: 'npm' 109 + 110 + - name: Setup Rust toolchain 111 + uses: dtolnay/rust-toolchain@stable 112 + with: 113 + targets: ${{ matrix.platform == 'macos-latest' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }} 114 + 115 + - name: Rust cache 116 + uses: swatinem/rust-cache@v2 117 + with: 118 + workspaces: './src-tauri -> target' 119 + 120 + - name: Install frontend dependencies 121 + run: npm ci 122 + 123 + - name: Build and release 124 + uses: tauri-apps/tauri-action@v0 125 + env: 126 + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 127 + with: 128 + tagName: app-v__VERSION__ 129 + releaseName: 'App v__VERSION__' 130 + releaseBody: 'See the assets to download this version and install.' 131 + releaseDraft: true 132 + prerelease: false 133 + args: ${{ matrix.args }} 134 + ``` 135 + 136 + ## Package Manager Variants 137 + 138 + ### pnpm 139 + 140 + ```yaml 141 + - name: Setup pnpm 142 + uses: pnpm/action-setup@v4 143 + with: 144 + version: latest 145 + 146 + - name: Setup Node.js 147 + uses: actions/setup-node@v4 148 + with: 149 + node-version: lts/* 150 + cache: 'pnpm' 151 + 152 + - name: Install frontend dependencies 153 + run: pnpm install 154 + ``` 155 + 156 + ### Yarn 157 + 158 + ```yaml 159 + - name: Setup Node.js 160 + uses: actions/setup-node@v4 161 + with: 162 + node-version: lts/* 163 + cache: 'yarn' 164 + 165 + - name: Install frontend dependencies 166 + run: yarn install --frozen-lockfile 167 + ``` 168 + 169 + ## Caching Strategies 170 + 171 + ### Rust Artifact Caching 172 + 173 + ```yaml 174 + - name: Rust cache 175 + uses: swatinem/rust-cache@v2 176 + with: 177 + workspaces: './src-tauri -> target' 178 + ``` 179 + 180 + ### Node.js Dependency Caching 181 + 182 + Configure via the `cache` parameter in `actions/setup-node@v4`: `'npm'`, `'yarn'`, or `'pnpm'`. 183 + 184 + ## Linux Dependencies 185 + 186 + Ubuntu requires WebKit and related libraries: 187 + 188 + ```yaml 189 + - name: Install dependencies (Ubuntu only) 190 + if: matrix.platform == 'ubuntu-22.04' 191 + run: | 192 + sudo apt-get update 193 + sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf 194 + ``` 195 + 196 + ## Release Automation 197 + 198 + ### tauri-action Configuration 199 + 200 + ```yaml 201 + - uses: tauri-apps/tauri-action@v0 202 + env: 203 + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 204 + with: 205 + tagName: app-v__VERSION__ 206 + releaseName: 'App v__VERSION__' 207 + releaseBody: 'See the assets to download this version and install.' 208 + releaseDraft: true 209 + prerelease: false 210 + args: ${{ matrix.args }} 211 + ``` 212 + 213 + ### Version Placeholders 214 + 215 + The action automatically replaces `__VERSION__` with the app version from `tauri.conf.json`. 216 + 217 + ### Release Options 218 + 219 + | Option | Description | 220 + |--------|-------------| 221 + | `tagName` | Git tag for the release (supports `__VERSION__` placeholder) | 222 + | `releaseName` | Display name for the release | 223 + | `releaseBody` | Release notes content | 224 + | `releaseDraft` | Create as draft release (`true`/`false`) | 225 + | `prerelease` | Mark as prerelease (`true`/`false`) | 226 + 227 + ## Artifact Uploads 228 + 229 + ### Upload Build Artifacts Without Release 230 + 231 + ```yaml 232 + - name: Build Tauri app 233 + uses: tauri-apps/tauri-action@v0 234 + env: 235 + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 236 + id: tauri 237 + 238 + - name: Upload artifacts 239 + uses: actions/upload-artifact@v4 240 + with: 241 + name: tauri-build-${{ matrix.platform }} 242 + path: src-tauri/target/release/bundle/ 243 + ``` 244 + 245 + ## GitHub Token Configuration 246 + 247 + The `GITHUB_TOKEN` requires write permissions for release creation. 248 + 249 + ### Repository Settings 250 + 251 + 1. Go to Settings > Actions > General 252 + 2. Under "Workflow permissions" 253 + 3. Select "Read and write permissions" 254 + 4. Save 255 + 256 + ### Workflow Permissions 257 + 258 + ```yaml 259 + jobs: 260 + publish-tauri: 261 + permissions: 262 + contents: write 263 + ``` 264 + 265 + ## Non-Root Project Configuration 266 + 267 + For projects where Tauri is not at the repository root: 268 + 269 + ```yaml 270 + - uses: tauri-apps/tauri-action@v0 271 + env: 272 + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 273 + with: 274 + projectPath: './apps/desktop' 275 + tagName: app-v__VERSION__ 276 + releaseName: 'App v__VERSION__' 277 + ``` 278 + 279 + ## ARM Build Emulation (Alternative) 280 + 281 + For ARM builds in private repositories or when native ARM runners are unavailable: 282 + 283 + ```yaml 284 + jobs: 285 + build-arm: 286 + runs-on: ubuntu-latest 287 + steps: 288 + - uses: actions/checkout@v4 289 + 290 + - name: Build ARM64 291 + uses: pguyot/arm-runner-action@v2.6.5 292 + with: 293 + base_image: dietpi:rpi_armv8_bullseye 294 + commands: | 295 + apt-get update 296 + apt-get install -y curl build-essential libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf 297 + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y 298 + source $HOME/.cargo/env 299 + curl -fsSL https://deb.nodesource.com/setup_lts.x | bash - 300 + apt-get install -y nodejs 301 + npm ci 302 + npm run tauri build 303 + ``` 304 + 305 + Note: ARM emulation builds take approximately one hour for fresh projects. 306 + 307 + ## Code Signing Integration 308 + 309 + ### macOS Signing Environment Variables 310 + 311 + ```yaml 312 + - uses: tauri-apps/tauri-action@v0 313 + env: 314 + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 315 + APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} 316 + APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} 317 + APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} 318 + APPLE_ID: ${{ secrets.APPLE_ID }} 319 + APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} 320 + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} 321 + ``` 322 + 323 + ### Windows Signing Environment Variables 324 + 325 + ```yaml 326 + - uses: tauri-apps/tauri-action@v0 327 + env: 328 + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 329 + TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} 330 + TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} 331 + ``` 332 + 333 + ## CI-Only Workflow (No Release) 334 + 335 + For pull request validation without creating releases: 336 + 337 + ```yaml 338 + name: 'CI Build' 339 + 340 + on: 341 + pull_request: 342 + branches: 343 + - main 344 + 345 + jobs: 346 + build: 347 + strategy: 348 + fail-fast: false 349 + matrix: 350 + include: 351 + - platform: 'macos-latest' 352 + args: '--target aarch64-apple-darwin' 353 + - platform: 'ubuntu-22.04' 354 + args: '' 355 + - platform: 'windows-latest' 356 + args: '' 357 + 358 + runs-on: ${{ matrix.platform }} 359 + steps: 360 + - uses: actions/checkout@v4 361 + 362 + - name: Install dependencies (Ubuntu only) 363 + if: matrix.platform == 'ubuntu-22.04' 364 + run: | 365 + sudo apt-get update 366 + sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf 367 + 368 + - name: Setup Node.js 369 + uses: actions/setup-node@v4 370 + with: 371 + node-version: lts/* 372 + cache: 'npm' 373 + 374 + - name: Setup Rust toolchain 375 + uses: dtolnay/rust-toolchain@stable 376 + with: 377 + targets: ${{ matrix.platform == 'macos-latest' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }} 378 + 379 + - name: Rust cache 380 + uses: swatinem/rust-cache@v2 381 + with: 382 + workspaces: './src-tauri -> target' 383 + 384 + - name: Install frontend dependencies 385 + run: npm ci 386 + 387 + - name: Build Tauri app 388 + run: npm run tauri build -- ${{ matrix.args }} 389 + ``` 390 + 391 + ## Troubleshooting 392 + 393 + ### Permission Denied for Release Creation 394 + 395 + Ensure workflow has write permissions and verify repository settings allow Actions to create releases: 396 + 397 + ```yaml 398 + jobs: 399 + publish-tauri: 400 + permissions: 401 + contents: write 402 + ``` 403 + 404 + ### Linux Build Failures 405 + 406 + Verify all dependencies are installed with the Ubuntu dependency installation step. 407 + 408 + ### macOS Target Not Found 409 + 410 + Ensure Rust targets are installed for cross-compilation: 411 + 412 + ```yaml 413 + - uses: dtolnay/rust-toolchain@stable 414 + with: 415 + targets: 'aarch64-apple-darwin,x86_64-apple-darwin' 416 + ``` 417 + 418 + ### Cache Not Working 419 + 420 + Verify the workspace path matches your project structure: 421 + 422 + ```yaml 423 + - uses: swatinem/rust-cache@v2 424 + with: 425 + workspaces: './src-tauri -> target' 426 + ```
+379
.agents/skills/calling-frontend-from-tauri-rust/SKILL.md
··· 1 + --- 2 + name: calling-frontend-from-tauri-rust 3 + description: Guides developers through Tauri v2 event system for calling frontend from Rust, covering emit functions, event payloads, IPC channels, and JavaScript evaluation for bi-directional Rust-frontend communication. 4 + --- 5 + 6 + # Calling Frontend from Tauri Rust 7 + 8 + Tauri provides three mechanisms for Rust to communicate with the frontend: the event system, channels, and JavaScript evaluation. 9 + 10 + ## Event System Overview 11 + 12 + The event system enables bi-directional communication between Rust and frontend. Best for small data transfers and multi-consumer patterns. Not designed for low latency or high throughput. 13 + 14 + ### Required Imports 15 + 16 + ```rust 17 + use tauri::{AppHandle, Emitter, Manager, Listener, EventTarget}; 18 + use serde::Serialize; 19 + ``` 20 + 21 + ```typescript 22 + import { listen, once, emit } from '@tauri-apps/api/event'; 23 + import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'; 24 + ``` 25 + 26 + ## Emitting Events from Rust 27 + 28 + ### Global Events (All Listeners) 29 + 30 + Use `AppHandle::emit()` to broadcast to all listeners: 31 + 32 + ```rust 33 + use tauri::{AppHandle, Emitter}; 34 + 35 + #[tauri::command] 36 + fn download(app: AppHandle, url: String) { 37 + app.emit("download-started", &url).unwrap(); 38 + for progress in [1, 15, 50, 80, 100] { 39 + app.emit("download-progress", progress).unwrap(); 40 + } 41 + app.emit("download-finished", &url).unwrap(); 42 + } 43 + ``` 44 + 45 + ### Webview-Specific Events 46 + 47 + Target specific webviews with `emit_to()`: 48 + 49 + ```rust 50 + use tauri::{AppHandle, Emitter}; 51 + 52 + #[tauri::command] 53 + fn login(app: AppHandle, user: String, password: String) { 54 + let authenticated = user == "tauri-apps" && password == "tauri"; 55 + let result = if authenticated { "loggedIn" } else { "invalidCredentials" }; 56 + app.emit_to("login", "login-result", result).unwrap(); 57 + } 58 + ``` 59 + 60 + ### Filtered Events (Multiple Webviews) 61 + 62 + Use `emit_filter()` for conditional targeting: 63 + 64 + ```rust 65 + use tauri::{AppHandle, Emitter, EventTarget}; 66 + 67 + #[tauri::command] 68 + fn open_file(app: AppHandle, path: std::path::PathBuf) { 69 + app.emit_filter("open-file", path, |target| match target { 70 + EventTarget::WebviewWindow { label } => label == "main" || label == "file-viewer", 71 + _ => false, 72 + }).unwrap(); 73 + } 74 + ``` 75 + 76 + ## Event Payloads 77 + 78 + Custom payloads must implement `Serialize` and `Clone`: 79 + 80 + ```rust 81 + use serde::Serialize; 82 + use tauri::{AppHandle, Emitter}; 83 + 84 + #[derive(Clone, Serialize)] 85 + #[serde(rename_all = "camelCase")] 86 + struct DownloadProgress { 87 + download_id: usize, 88 + chunk_length: usize, 89 + total_size: usize, 90 + } 91 + 92 + #[tauri::command] 93 + fn download(app: AppHandle, url: String) { 94 + app.emit("download-progress", DownloadProgress { 95 + download_id: 1, 96 + chunk_length: 150, 97 + total_size: 1000, 98 + }).unwrap(); 99 + } 100 + ``` 101 + 102 + ## Listening in Frontend 103 + 104 + ### Global Event Listeners 105 + 106 + ```typescript 107 + import { listen } from '@tauri-apps/api/event'; 108 + 109 + type DownloadStarted = { 110 + url: string; 111 + downloadId: number; 112 + contentLength: number; 113 + }; 114 + 115 + listen<DownloadStarted>('download-started', (event) => { 116 + console.log(`downloading ${event.payload.contentLength} bytes from ${event.payload.url}`); 117 + }); 118 + ``` 119 + 120 + ### Webview-Specific Listeners 121 + 122 + ```typescript 123 + import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'; 124 + 125 + const appWebview = getCurrentWebviewWindow(); 126 + appWebview.listen<string>('logged-in', (event) => { 127 + localStorage.setItem('session-token', event.payload); 128 + }); 129 + ``` 130 + 131 + ### Managing Listeners 132 + 133 + ```typescript 134 + import { listen, once } from '@tauri-apps/api/event'; 135 + 136 + // Unlisten to prevent memory leaks 137 + const unlisten = await listen('download-started', (event) => { 138 + console.log('download started'); 139 + }); 140 + unlisten(); // Stop listening when done 141 + 142 + // Listen once for one-time events 143 + once('app-ready', (event) => { 144 + console.log('App is ready:', event.payload); 145 + }); 146 + ``` 147 + 148 + ## Listening in Rust 149 + 150 + ### Global and Webview Listeners 151 + 152 + ```rust 153 + use tauri::{Listener, Manager}; 154 + 155 + tauri::Builder::default() 156 + .setup(|app| { 157 + // Global listener 158 + app.listen("download-started", |event| { 159 + println!("event received: {}", event.payload()); 160 + }); 161 + 162 + // Webview-specific listener 163 + let webview = app.get_webview_window("main").unwrap(); 164 + webview.listen("logged-in", |event| { 165 + println!("User logged in: {}", event.payload()); 166 + }); 167 + Ok(()) 168 + }) 169 + .run(tauri::generate_context!()) 170 + .expect("error while running tauri application") 171 + ``` 172 + 173 + ### Unlisten and Listen Once 174 + 175 + ```rust 176 + use tauri::Listener; 177 + 178 + // Store event ID to unlisten later 179 + let event_id = app.listen("download-started", |event| { 180 + println!("download started"); 181 + }); 182 + app.unlisten(event_id); 183 + 184 + // Conditional unlisten 185 + let handle = app.handle().clone(); 186 + app.listen("status-changed", move |event| { 187 + if event.payload() == "\"ready\"" { 188 + handle.unlisten(event.id()); 189 + } 190 + }); 191 + 192 + // Listen once 193 + app.once("ready", |event| { 194 + println!("app is ready: {}", event.payload()); 195 + }); 196 + ``` 197 + 198 + ## Channels (High-Throughput Streaming) 199 + 200 + For better performance than events, use channels: 201 + 202 + ### Rust Channel Setup 203 + 204 + ```rust 205 + use tauri::{AppHandle, ipc::Channel}; 206 + use serde::Serialize; 207 + 208 + #[derive(Clone, Serialize)] 209 + #[serde(rename_all = "camelCase", tag = "event", content = "data")] 210 + enum DownloadEvent<'a> { 211 + #[serde(rename_all = "camelCase")] 212 + Started { url: &'a str, download_id: usize, content_length: usize }, 213 + #[serde(rename_all = "camelCase")] 214 + Progress { download_id: usize, chunk_length: usize }, 215 + #[serde(rename_all = "camelCase")] 216 + Finished { download_id: usize }, 217 + } 218 + 219 + #[tauri::command] 220 + fn download(app: AppHandle, url: String, on_event: Channel<DownloadEvent>) { 221 + on_event.send(DownloadEvent::Started { 222 + url: &url, 223 + download_id: 1, 224 + content_length: 1000, 225 + }).unwrap(); 226 + 227 + for _ in 0..10 { 228 + on_event.send(DownloadEvent::Progress { 229 + download_id: 1, 230 + chunk_length: 100, 231 + }).unwrap(); 232 + } 233 + 234 + on_event.send(DownloadEvent::Finished { download_id: 1 }).unwrap(); 235 + } 236 + ``` 237 + 238 + ### Frontend Channel Usage 239 + 240 + ```typescript 241 + import { invoke, Channel } from '@tauri-apps/api/core'; 242 + 243 + type DownloadEvent = 244 + | { event: 'started'; data: { url: string; downloadId: number; contentLength: number } } 245 + | { event: 'progress'; data: { downloadId: number; chunkLength: number } } 246 + | { event: 'finished'; data: { downloadId: number } }; 247 + 248 + const onEvent = new Channel<DownloadEvent>(); 249 + 250 + onEvent.onmessage = (message) => { 251 + switch (message.event) { 252 + case 'started': 253 + console.log(`Download started: ${message.data.url}`); 254 + break; 255 + case 'progress': 256 + console.log(`Progress: ${message.data.chunkLength} bytes`); 257 + break; 258 + case 'finished': 259 + console.log('Download complete!'); 260 + break; 261 + } 262 + }; 263 + 264 + await invoke('download', { url: 'https://example.com/file.json', onEvent }); 265 + ``` 266 + 267 + ## JavaScript Evaluation 268 + 269 + Execute JavaScript directly from Rust: 270 + 271 + ### Basic Evaluation 272 + 273 + ```rust 274 + use tauri::Manager; 275 + 276 + tauri::Builder::default() 277 + .setup(|app| { 278 + let webview = app.get_webview_window("main").unwrap(); 279 + webview.eval("console.log('hello from Rust')")?; 280 + Ok(()) 281 + }) 282 + ``` 283 + 284 + ### Evaluation with Data 285 + 286 + ```rust 287 + use tauri::Manager; 288 + 289 + #[tauri::command] 290 + fn notify_frontend(app: tauri::AppHandle, message: String) { 291 + if let Some(webview) = app.get_webview_window("main") { 292 + let script = format!("window.showNotification('{}')", message); 293 + webview.eval(&script).unwrap(); 294 + } 295 + } 296 + ``` 297 + 298 + ### Complex Data with serialize-to-javascript 299 + 300 + ```toml 301 + # Cargo.toml 302 + [dependencies] 303 + serialize-to-javascript = "0.1" 304 + ``` 305 + 306 + ```rust 307 + use serialize_to_javascript::Serialized; 308 + use tauri::Manager; 309 + 310 + #[derive(serde::Serialize)] 311 + struct AppState { user: String, logged_in: bool } 312 + 313 + #[tauri::command] 314 + fn sync_state(app: tauri::AppHandle) { 315 + let state = AppState { user: "john".to_string(), logged_in: true }; 316 + if let Some(webview) = app.get_webview_window("main") { 317 + let serialized = Serialized::new(&state, &Default::default()).into_string(); 318 + webview.eval(&format!("window.updateState({})", serialized)).unwrap(); 319 + } 320 + } 321 + ``` 322 + 323 + ## Choosing the Right Method 324 + 325 + | Method | Use Case | Performance | 326 + |--------|----------|-------------| 327 + | Events (`emit`) | Multi-consumer, broadcast | Moderate | 328 + | Channels | High-throughput streaming, single consumer | High | 329 + | JS Eval | Direct DOM manipulation, no response needed | Low overhead | 330 + 331 + **Events**: Notifying multiple windows, loose coupling, simple status updates. 332 + 333 + **Channels**: File downloads/uploads with progress, real-time streaming, high-frequency updates. 334 + 335 + **JS Eval**: One-off DOM updates, triggering frontend functions directly. 336 + 337 + ## Complete Example: File Watcher 338 + 339 + ### Rust Side 340 + 341 + ```rust 342 + use tauri::{AppHandle, Emitter}; 343 + use serde::Serialize; 344 + use std::path::PathBuf; 345 + 346 + #[derive(Clone, Serialize)] 347 + #[serde(rename_all = "camelCase")] 348 + struct FileChange { path: String, event_type: String } 349 + 350 + #[tauri::command] 351 + fn watch_directory(app: AppHandle, path: PathBuf) { 352 + std::thread::spawn(move || { 353 + loop { 354 + app.emit("file-changed", FileChange { 355 + path: path.to_string_lossy().to_string(), 356 + event_type: "modified".to_string(), 357 + }).unwrap(); 358 + std::thread::sleep(std::time::Duration::from_secs(5)); 359 + } 360 + }); 361 + } 362 + ``` 363 + 364 + ### Frontend Side 365 + 366 + ```typescript 367 + import { listen } from '@tauri-apps/api/event'; 368 + import { invoke } from '@tauri-apps/api/core'; 369 + 370 + type FileChange = { path: string; eventType: string }; 371 + 372 + await invoke('watch_directory', { path: '/some/directory' }); 373 + 374 + const unlisten = await listen<FileChange>('file-changed', (event) => { 375 + console.log(`File ${event.payload.eventType}: ${event.payload.path}`); 376 + }); 377 + 378 + // Cleanup when component unmounts: unlisten(); 379 + ```
+482
.agents/skills/calling-rust-from-tauri-frontend/SKILL.md
··· 1 + --- 2 + name: calling-rust-from-tauri-frontend 3 + description: Guides the user through calling Rust backend functions from the Tauri frontend using the invoke function, defining commands with the #[tauri::command] attribute, passing arguments, returning values, handling errors, and implementing async IPC communication. 4 + --- 5 + 6 + # Calling Rust from Tauri Frontend 7 + 8 + This skill covers how to call Rust backend functions from your Tauri v2 frontend using the command system and invoke function. 9 + 10 + ## Overview 11 + 12 + Tauri provides two IPC mechanisms: 13 + - **Commands** (recommended): Type-safe function calls with serialized arguments/return values 14 + - **Events**: Dynamic, one-way communication (not covered here) 15 + 16 + ## Basic Commands 17 + 18 + ### Defining a Command in Rust 19 + 20 + Use the `#[tauri::command]` attribute macro: 21 + 22 + ```rust 23 + // src-tauri/src/lib.rs 24 + #[tauri::command] 25 + fn greet(name: String) -> String { 26 + format!("Hello, {}!", name) 27 + } 28 + ``` 29 + 30 + ### Registering Commands 31 + 32 + Commands must be registered with the invoke handler: 33 + 34 + ```rust 35 + #[cfg_attr(mobile, tauri::mobile_entry_point)] 36 + pub fn run() { 37 + tauri::Builder::default() 38 + .invoke_handler(tauri::generate_handler![greet, login, fetch_data]) 39 + .run(tauri::generate_context!()) 40 + .expect("error while running tauri application") 41 + } 42 + ``` 43 + 44 + ### Invoking from JavaScript/TypeScript 45 + 46 + ```typescript 47 + import { invoke } from '@tauri-apps/api/core'; 48 + 49 + const greeting = await invoke('greet', { name: 'World' }); 50 + console.log(greeting); // "Hello, World!" 51 + ``` 52 + 53 + Or with the global Tauri object (when `app.withGlobalTauri` is enabled): 54 + 55 + ```javascript 56 + const { invoke } = window.__TAURI__.core; 57 + const greeting = await invoke('greet', { name: 'World' }); 58 + ``` 59 + 60 + ## Passing Arguments 61 + 62 + ### Argument Naming Convention 63 + 64 + By default, Rust snake_case arguments map to JavaScript camelCase: 65 + 66 + ```rust 67 + #[tauri::command] 68 + fn create_user(user_name: String, user_age: u32) -> String { 69 + format!("{} is {} years old", user_name, user_age) 70 + } 71 + ``` 72 + 73 + ```typescript 74 + await invoke('create_user', { userName: 'Alice', userAge: 30 }); 75 + ``` 76 + 77 + Use `rename_all` to change the naming convention: 78 + 79 + ```rust 80 + #[tauri::command(rename_all = "snake_case")] 81 + fn create_user(user_name: String, user_age: u32) -> String { 82 + format!("{} is {} years old", user_name, user_age) 83 + } 84 + ``` 85 + 86 + ### Complex Arguments 87 + 88 + Arguments must implement `serde::Deserialize`: 89 + 90 + ```rust 91 + use serde::Deserialize; 92 + 93 + #[derive(Deserialize)] 94 + struct UserData { 95 + name: String, 96 + email: String, 97 + age: u32, 98 + } 99 + 100 + #[tauri::command] 101 + fn register_user(user: UserData) -> String { 102 + format!("Registered {} ({}) age {}", user.name, user.email, user.age) 103 + } 104 + ``` 105 + 106 + ```typescript 107 + await invoke('register_user', { 108 + user: { name: 'Alice', email: 'alice@example.com', age: 30 } 109 + }); 110 + ``` 111 + 112 + ## Returning Values 113 + 114 + ### Simple Return Types 115 + 116 + Return types must implement `serde::Serialize`: 117 + 118 + ```rust 119 + #[tauri::command] 120 + fn get_count() -> i32 { 42 } 121 + 122 + #[tauri::command] 123 + fn get_message() -> String { "Hello from Rust!".into() } 124 + ``` 125 + 126 + ```typescript 127 + const count: number = await invoke('get_count'); 128 + const message: string = await invoke('get_message'); 129 + ``` 130 + 131 + ### Returning Complex Types 132 + 133 + ```rust 134 + use serde::Serialize; 135 + 136 + #[derive(Serialize)] 137 + struct AppConfig { 138 + theme: String, 139 + language: String, 140 + notifications_enabled: bool, 141 + } 142 + 143 + #[tauri::command] 144 + fn get_config() -> AppConfig { 145 + AppConfig { 146 + theme: "dark".into(), 147 + language: "en".into(), 148 + notifications_enabled: true, 149 + } 150 + } 151 + ``` 152 + 153 + ```typescript 154 + interface AppConfig { 155 + theme: string; 156 + language: string; 157 + notificationsEnabled: boolean; 158 + } 159 + const config: AppConfig = await invoke('get_config'); 160 + ``` 161 + 162 + ### Returning Binary Data 163 + 164 + For large binary data, use `tauri::ipc::Response` to bypass JSON serialization: 165 + 166 + ```rust 167 + use tauri::ipc::Response; 168 + 169 + #[tauri::command] 170 + fn read_file(path: String) -> Response { 171 + let data = std::fs::read(&path).unwrap(); 172 + Response::new(data) 173 + } 174 + ``` 175 + 176 + ```typescript 177 + const data: ArrayBuffer = await invoke('read_file', { path: '/path/to/file' }); 178 + ``` 179 + 180 + ## Error Handling 181 + 182 + ### Using Result Types 183 + 184 + Return `Result<T, E>` where `E` implements `Serialize` or is a `String`: 185 + 186 + ```rust 187 + #[tauri::command] 188 + fn divide(a: f64, b: f64) -> Result<f64, String> { 189 + if b == 0.0 { 190 + Err("Cannot divide by zero".into()) 191 + } else { 192 + Ok(a / b) 193 + } 194 + } 195 + ``` 196 + 197 + ```typescript 198 + try { 199 + const result = await invoke('divide', { a: 10, b: 0 }); 200 + } catch (error) { 201 + console.error('Error:', error); // "Cannot divide by zero" 202 + } 203 + ``` 204 + 205 + ### Custom Error Types with thiserror 206 + 207 + ```rust 208 + use serde::Serialize; 209 + use thiserror::Error; 210 + 211 + #[derive(Debug, Error)] 212 + enum AppError { 213 + #[error("File not found: {0}")] 214 + FileNotFound(String), 215 + #[error("Permission denied")] 216 + PermissionDenied, 217 + #[error("IO error: {0}")] 218 + Io(#[from] std::io::Error), 219 + } 220 + 221 + impl Serialize for AppError { 222 + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> 223 + where S: serde::ser::Serializer { 224 + serializer.serialize_str(self.to_string().as_ref()) 225 + } 226 + } 227 + 228 + #[tauri::command] 229 + fn open_file(path: String) -> Result<String, AppError> { 230 + if !std::path::Path::new(&path).exists() { 231 + return Err(AppError::FileNotFound(path)); 232 + } 233 + let content = std::fs::read_to_string(&path)?; 234 + Ok(content) 235 + } 236 + ``` 237 + 238 + ### Structured Error Responses 239 + 240 + ```rust 241 + use serde::Serialize; 242 + 243 + #[derive(Debug, Serialize)] 244 + struct ErrorResponse { code: String, message: String } 245 + 246 + #[tauri::command] 247 + fn validate_input(input: String) -> Result<String, ErrorResponse> { 248 + if input.is_empty() { 249 + return Err(ErrorResponse { 250 + code: "EMPTY_INPUT".into(), 251 + message: "Input cannot be empty".into(), 252 + }); 253 + } 254 + Ok(input.to_uppercase()) 255 + } 256 + ``` 257 + 258 + ```typescript 259 + interface ErrorResponse { code: string; message: string; } 260 + 261 + try { 262 + const result = await invoke('validate_input', { input: '' }); 263 + } catch (error) { 264 + const err = error as ErrorResponse; 265 + console.error(`Error ${err.code}: ${err.message}`); 266 + } 267 + ``` 268 + 269 + ## Async Commands 270 + 271 + ### Defining Async Commands 272 + 273 + Use the `async` keyword: 274 + 275 + ```rust 276 + #[tauri::command] 277 + async fn fetch_data(url: String) -> Result<String, String> { 278 + let response = reqwest::get(&url).await.map_err(|e| e.to_string())?; 279 + let body = response.text().await.map_err(|e| e.to_string())?; 280 + Ok(body) 281 + } 282 + ``` 283 + 284 + ### Async with Borrowed Types Limitation 285 + 286 + Async commands cannot use borrowed types like `&str` directly: 287 + 288 + ```rust 289 + // Will NOT compile: 290 + // async fn bad_command(value: &str) -> String { ... } 291 + 292 + // Use owned types instead: 293 + #[tauri::command] 294 + async fn good_command(value: String) -> String { 295 + some_async_operation(&value).await; 296 + value 297 + } 298 + 299 + // Or wrap in Result as workaround: 300 + #[tauri::command] 301 + async fn with_borrowed(value: &str) -> Result<String, ()> { 302 + some_async_operation(value).await; 303 + Ok(value.to_string()) 304 + } 305 + ``` 306 + 307 + ### Frontend Invocation 308 + 309 + Async commands work identically to sync since `invoke` returns a Promise: 310 + 311 + ```typescript 312 + const result = await invoke('fetch_data', { url: 'https://api.example.com/data' }); 313 + ``` 314 + 315 + ## Accessing Tauri Internals 316 + 317 + ### WebviewWindow, AppHandle, and State 318 + 319 + ```rust 320 + use std::sync::Mutex; 321 + 322 + struct AppState { counter: Mutex<i32> } 323 + 324 + #[tauri::command] 325 + async fn get_window_label(window: tauri::WebviewWindow) -> String { 326 + window.label().to_string() 327 + } 328 + 329 + #[tauri::command] 330 + async fn get_app_version(app: tauri::AppHandle) -> String { 331 + app.package_info().version.to_string() 332 + } 333 + 334 + #[tauri::command] 335 + fn increment_counter(state: tauri::State<AppState>) -> i32 { 336 + let mut counter = state.counter.lock().unwrap(); 337 + *counter += 1; 338 + *counter 339 + } 340 + 341 + pub fn run() { 342 + tauri::Builder::default() 343 + .manage(AppState { counter: Mutex::new(0) }) 344 + .invoke_handler(tauri::generate_handler![ 345 + get_window_label, get_app_version, increment_counter 346 + ]) 347 + .run(tauri::generate_context!()) 348 + .expect("error while running tauri application") 349 + } 350 + ``` 351 + 352 + ## Advanced Features 353 + 354 + ### Raw Request Access 355 + 356 + Access headers and raw body: 357 + 358 + ```rust 359 + use tauri::ipc::{Request, InvokeBody}; 360 + 361 + #[tauri::command] 362 + fn upload(request: Request) -> Result<String, String> { 363 + let InvokeBody::Raw(data) = request.body() else { 364 + return Err("Expected raw body".into()); 365 + }; 366 + let auth = request.headers() 367 + .get("Authorization") 368 + .and_then(|v| v.to_str().ok()) 369 + .ok_or("Missing Authorization header")?; 370 + Ok(format!("Received {} bytes", data.len())) 371 + } 372 + ``` 373 + 374 + ```typescript 375 + const data = new Uint8Array([1, 2, 3, 4, 5]); 376 + await invoke('upload', data, { headers: { Authorization: 'Bearer token123' } }); 377 + ``` 378 + 379 + ### Channels for Streaming 380 + 381 + ```rust 382 + use tauri::ipc::Channel; 383 + use tokio::io::AsyncReadExt; 384 + 385 + #[tauri::command] 386 + async fn stream_file(path: String, channel: Channel<Vec<u8>>) -> Result<(), String> { 387 + let mut file = tokio::fs::File::open(&path).await.map_err(|e| e.to_string())?; 388 + let mut buffer = vec![0u8; 4096]; 389 + loop { 390 + let len = file.read(&mut buffer).await.map_err(|e| e.to_string())?; 391 + if len == 0 { break; } 392 + channel.send(buffer[..len].to_vec()).map_err(|e| e.to_string())?; 393 + } 394 + Ok(()) 395 + } 396 + ``` 397 + 398 + ```typescript 399 + import { Channel } from '@tauri-apps/api/core'; 400 + 401 + const channel = new Channel<Uint8Array>(); 402 + channel.onmessage = (chunk) => console.log('Received:', chunk.length, 'bytes'); 403 + await invoke('stream_file', { path: '/path/to/file', channel }); 404 + ``` 405 + 406 + ## Organizing Commands in Modules 407 + 408 + ```rust 409 + // src-tauri/src/commands/user.rs 410 + use serde::{Deserialize, Serialize}; 411 + 412 + #[derive(Deserialize)] 413 + pub struct CreateUserRequest { pub name: String, pub email: String } 414 + 415 + #[derive(Serialize)] 416 + pub struct User { pub id: u32, pub name: String, pub email: String } 417 + 418 + #[tauri::command] 419 + pub fn create_user(request: CreateUserRequest) -> User { 420 + User { id: 1, name: request.name, email: request.email } 421 + } 422 + ``` 423 + 424 + ```rust 425 + // src-tauri/src/commands/mod.rs 426 + pub mod user; 427 + ``` 428 + 429 + ```rust 430 + // src-tauri/src/lib.rs 431 + mod commands; 432 + 433 + pub fn run() { 434 + tauri::Builder::default() 435 + .invoke_handler(tauri::generate_handler![commands::user::create_user]) 436 + .run(tauri::generate_context!()) 437 + .expect("error while running tauri application") 438 + } 439 + ``` 440 + 441 + ## TypeScript Type Safety 442 + 443 + Create a typed wrapper: 444 + 445 + ```typescript 446 + import { invoke } from '@tauri-apps/api/core'; 447 + 448 + export interface User { id: number; name: string; email: string; } 449 + export interface CreateUserRequest { name: string; email: string; } 450 + 451 + export const commands = { 452 + createUser: (request: CreateUserRequest): Promise<User> => 453 + invoke('create_user', { request }), 454 + greet: (name: string): Promise<string> => 455 + invoke('greet', { name }), 456 + }; 457 + 458 + // Usage 459 + const user = await commands.createUser({ name: 'Bob', email: 'bob@example.com' }); 460 + ``` 461 + 462 + ## Quick Reference 463 + 464 + | Task | Rust | JavaScript | 465 + |------|------|------------| 466 + | Define command | `#[tauri::command] fn name() {}` | - | 467 + | Register command | `tauri::generate_handler![name]` | - | 468 + | Invoke command | - | `await invoke('name', { args })` | 469 + | Return value | `-> T` where T: Serialize | `const result = await invoke(...)` | 470 + | Return error | `-> Result<T, E>` | `try/catch` | 471 + | Async command | `async fn name()` | Same as sync | 472 + | Access window | `window: tauri::WebviewWindow` | - | 473 + | Access app | `app: tauri::AppHandle` | - | 474 + | Access state | `state: tauri::State<T>` | - | 475 + 476 + ## Key Constraints 477 + 478 + 1. **Command names must be unique** across the entire application 479 + 2. **Commands in `lib.rs` cannot be `pub`** (use modules for organization) 480 + 3. **All commands must be registered** in a single `generate_handler!` call 481 + 4. **Async commands cannot use borrowed types** like `&str` directly 482 + 5. **Arguments must implement `Deserialize`**, return types must implement `Serialize`
+431
.agents/skills/configuring-tauri-apps/SKILL.md
··· 1 + --- 2 + name: configuring-tauri-apps 3 + description: Guides developers through Tauri v2 configuration including tauri.conf.json structure, Cargo.toml settings, environment-specific configs, and common configuration options for desktop and mobile applications. 4 + --- 5 + 6 + # Tauri Configuration Files 7 + 8 + Tauri v2 applications use three primary configuration files to manage application behavior, dependencies, and build processes. 9 + 10 + ## Configuration File Overview 11 + 12 + | File | Purpose | Format | 13 + |------|---------|--------| 14 + | `tauri.conf.json` | Tauri-specific settings | JSON, JSON5, or TOML | 15 + | `Cargo.toml` | Rust dependencies and metadata | TOML | 16 + | `package.json` | Frontend dependencies and scripts | JSON | 17 + 18 + ## tauri.conf.json 19 + 20 + The main configuration file located in `src-tauri/`. Defines application metadata, window behavior, bundling options, and plugin settings. 21 + 22 + ### Supported Formats 23 + 24 + - **JSON** (default): `tauri.conf.json` 25 + - **JSON5**: `tauri.conf.json5` (requires `config-json5` Cargo feature) 26 + - **TOML**: `Tauri.toml` (requires `config-toml` Cargo feature) 27 + 28 + ### Complete Configuration Structure 29 + 30 + ```json 31 + { 32 + "$schema": "https://schema.tauri.app/config/2", 33 + "productName": "MyApp", 34 + "version": "1.0.0", 35 + "identifier": "com.company.myapp", 36 + "mainBinaryName": "my-app", 37 + "build": { 38 + "devUrl": "http://localhost:3000", 39 + "frontendDist": "../dist", 40 + "beforeDevCommand": "npm run dev", 41 + "beforeBuildCommand": "npm run build", 42 + "features": ["custom-feature"], 43 + "removeUnusedCommands": true 44 + }, 45 + "app": { 46 + "withGlobalTauri": false, 47 + "macOSPrivateApi": false, 48 + "windows": [ 49 + { 50 + "title": "My Application", 51 + "width": 1200, 52 + "height": 800, 53 + "minWidth": 800, 54 + "minHeight": 600, 55 + "resizable": true, 56 + "fullscreen": false, 57 + "center": true, 58 + "visible": true, 59 + "decorations": true, 60 + "transparent": false, 61 + "alwaysOnTop": false, 62 + "focus": true, 63 + "url": "index.html" 64 + } 65 + ], 66 + "security": { 67 + "capabilities": [], 68 + "assetProtocol": { 69 + "enable": true, 70 + "scope": ["$APPDATA/**"] 71 + }, 72 + "pattern": { "use": "brownfield" }, 73 + "freezePrototype": false 74 + }, 75 + "trayIcon": { 76 + "id": "main-tray", 77 + "iconPath": "icons/tray.png", 78 + "iconAsTemplate": true 79 + } 80 + }, 81 + "bundle": { 82 + "active": true, 83 + "targets": "all", 84 + "icon": ["icons/32x32.png", "icons/128x128.png", "icons/icon.icns", "icons/icon.ico"], 85 + "resources": ["assets/**/*"], 86 + "copyright": "Copyright 2024", 87 + "category": "Utility", 88 + "shortDescription": "A short app description", 89 + "longDescription": "A longer description", 90 + "licenseFile": "../LICENSE", 91 + "windows": { 92 + "certificateThumbprint": null, 93 + "timestampUrl": "http://timestamp.digicert.com", 94 + "nsis": { "license": "../LICENSE", "installerIcon": "icons/icon.ico", "installMode": "currentUser" } 95 + }, 96 + "macOS": { 97 + "minimumSystemVersion": "10.13", 98 + "signingIdentity": null, 99 + "dmg": { "appPosition": { "x": 180, "y": 170 }, "applicationFolderPosition": { "x": 480, "y": 170 } } 100 + }, 101 + "linux": { 102 + "appimage": { "bundleMediaFramework": false }, 103 + "deb": { "depends": ["libwebkit2gtk-4.1-0"] }, 104 + "rpm": { "depends": ["webkit2gtk4.1"] } 105 + }, 106 + "android": { "minSdkVersion": 24 }, 107 + "iOS": { "minimumSystemVersion": "13.0" } 108 + }, 109 + "plugins": { 110 + "updater": { 111 + "pubkey": "YOUR_PUBLIC_KEY", 112 + "endpoints": ["https://releases.example.com/{{target}}/{{arch}}/{{current_version}}"] 113 + } 114 + } 115 + } 116 + ``` 117 + 118 + ### Root-Level Fields 119 + 120 + | Field | Type | Required | Description | 121 + |-------|------|----------|-------------| 122 + | `productName` | string | No | Application display name | 123 + | `version` | string | No | Semver version or path to package.json | 124 + | `identifier` | string | Yes | Reverse domain identifier (e.g., `com.tauri.example`) | 125 + | `mainBinaryName` | string | No | Override the main binary filename | 126 + 127 + ### Build Configuration Fields 128 + 129 + | Field | Type | Description | 130 + |-------|------|-------------| 131 + | `devUrl` | string | Development server URL for hot-reload | 132 + | `frontendDist` | string | Path to built frontend assets or remote URL | 133 + | `beforeDevCommand` | string | Script to run before `tauri dev` | 134 + | `beforeBuildCommand` | string | Script to run before `tauri build` | 135 + | `features` | string[] | Cargo features to enable during build | 136 + | `removeUnusedCommands` | boolean | Strip unused plugin commands from binary | 137 + 138 + ### Window Configuration Options 139 + 140 + | Field | Type | Default | Description | 141 + |-------|------|---------|-------------| 142 + | `title` | string | `"Tauri"` | Window title | 143 + | `width` / `height` | number | `800` / `600` | Window dimensions in pixels | 144 + | `minWidth` / `minHeight` | number | - | Minimum dimensions | 145 + | `maxWidth` / `maxHeight` | number | - | Maximum dimensions | 146 + | `x` / `y` | number | - | Window position | 147 + | `resizable` | boolean | `true` | Allow window resizing | 148 + | `fullscreen` | boolean | `false` | Start in fullscreen | 149 + | `center` | boolean | `false` | Center window on screen | 150 + | `visible` | boolean | `true` | Window visibility on start | 151 + | `decorations` | boolean | `true` | Show window decorations | 152 + | `transparent` | boolean | `false` | Enable window transparency | 153 + | `alwaysOnTop` | boolean | `false` | Keep window above others | 154 + | `url` | string | `"index.html"` | Initial URL to load | 155 + 156 + ### Security Configuration 157 + 158 + | Field | Type | Description | 159 + |-------|------|-------------| 160 + | `capabilities` | string[] | Permission capabilities for the application | 161 + | `assetProtocol.enable` | boolean | Enable custom asset protocol | 162 + | `assetProtocol.scope` | string[] | Allowed paths for asset protocol | 163 + | `pattern.use` | string | Security pattern (`"brownfield"` default) | 164 + | `freezePrototype` | boolean | Prevent prototype mutation | 165 + 166 + ### Bundle Targets by Platform 167 + 168 + | Platform | Targets | 169 + |----------|---------| 170 + | Windows | `nsis`, `msi` | 171 + | macOS | `app`, `dmg` | 172 + | Linux | `appimage`, `deb`, `rpm` | 173 + | Android | `apk`, `aab` | 174 + | iOS | `app` | 175 + 176 + ## Platform-Specific Configuration 177 + 178 + Create platform-specific files that override base configuration using JSON Merge Patch (RFC 7396). 179 + 180 + | Platform | Filename | 181 + |----------|----------| 182 + | Linux | `tauri.linux.conf.json` | 183 + | Windows | `tauri.windows.conf.json` | 184 + | macOS | `tauri.macos.conf.json` | 185 + | Android | `tauri.android.conf.json` | 186 + | iOS | `tauri.ios.conf.json` | 187 + 188 + Example `src-tauri/tauri.windows.conf.json`: 189 + 190 + ```json 191 + { 192 + "app": { 193 + "windows": [{ "title": "My App - Windows Edition" }] 194 + }, 195 + "bundle": { 196 + "windows": { "nsis": { "installMode": "perMachine" } } 197 + } 198 + } 199 + ``` 200 + 201 + Example `src-tauri/tauri.macos.conf.json`: 202 + 203 + ```json 204 + { 205 + "app": { "macOSPrivateApi": true }, 206 + "bundle": { 207 + "macOS": { "minimumSystemVersion": "11.0", "entitlements": "entitlements.plist" } 208 + } 209 + } 210 + ``` 211 + 212 + ## CLI Configuration Override 213 + 214 + ```bash 215 + # Development with custom config 216 + tauri dev --config src-tauri/tauri.dev.conf.json 217 + 218 + # Build with beta configuration 219 + tauri build --config src-tauri/tauri.beta.conf.json 220 + 221 + # Inline configuration override 222 + tauri build --config '{"bundle":{"identifier":"com.company.myapp.beta"}}' 223 + ``` 224 + 225 + ## Cargo.toml Configuration 226 + 227 + Located in `src-tauri/Cargo.toml`, manages Rust dependencies. 228 + 229 + ```toml 230 + [package] 231 + name = "my-app" 232 + version = "1.0.0" 233 + edition = "2021" 234 + 235 + [build-dependencies] 236 + tauri-build = { version = "2.0", features = [] } 237 + 238 + [dependencies] 239 + serde = { version = "1.0", features = ["derive"] } 240 + serde_json = "1.0" 241 + tauri = { version = "2.0", features = [] } 242 + tauri-plugin-shell = "2.0" 243 + tauri-plugin-opener = "2.0" 244 + 245 + [features] 246 + default = ["custom-protocol"] 247 + custom-protocol = ["tauri/custom-protocol"] 248 + ``` 249 + 250 + ### Common Tauri Features 251 + 252 + ```toml 253 + [dependencies] 254 + tauri = { version = "2.0", features = [ 255 + "config-json5", # Enable JSON5 config format 256 + "config-toml", # Enable TOML config format 257 + "devtools", # Enable WebView devtools 258 + "macos-private-api", # Enable macOS private APIs 259 + "tray-icon", # Enable system tray support 260 + "image-png", # PNG image support 261 + "image-ico", # ICO image support 262 + "protocol-asset" # Custom asset protocol 263 + ] } 264 + ``` 265 + 266 + ### Version Management 267 + 268 + ```toml 269 + tauri = { version = "2.0" } # Semver-compatible (recommended) 270 + tauri = { version = "=2.0.0" } # Exact version 271 + tauri = { version = "~2.0.0" } # Patch updates only 272 + ``` 273 + 274 + ## package.json Integration 275 + 276 + ```json 277 + { 278 + "name": "my-tauri-app", 279 + "version": "1.0.0", 280 + "scripts": { 281 + "dev": "vite", 282 + "build": "vite build", 283 + "tauri": "tauri" 284 + }, 285 + "dependencies": { "@tauri-apps/api": "^2.0.0" }, 286 + "devDependencies": { "@tauri-apps/cli": "^2.0.0" } 287 + } 288 + ``` 289 + 290 + ## Environment-Specific Configurations 291 + 292 + ### Development (`src-tauri/tauri.dev.conf.json`) 293 + 294 + ```json 295 + { 296 + "build": { "devUrl": "http://localhost:5173" }, 297 + "app": { 298 + "withGlobalTauri": true, 299 + "windows": [{ "title": "My App [DEV]" }] 300 + } 301 + } 302 + ``` 303 + 304 + ### Production (`src-tauri/tauri.prod.conf.json`) 305 + 306 + ```json 307 + { 308 + "build": { "frontendDist": "../dist", "removeUnusedCommands": true }, 309 + "bundle": { "active": true, "targets": "all" } 310 + } 311 + ``` 312 + 313 + ### Beta (`src-tauri/tauri.beta.conf.json`) 314 + 315 + ```json 316 + { 317 + "identifier": "com.company.myapp.beta", 318 + "productName": "MyApp Beta", 319 + "plugins": { 320 + "updater": { 321 + "endpoints": ["https://beta-releases.example.com/{{target}}/{{arch}}/{{current_version}}"] 322 + } 323 + } 324 + } 325 + ``` 326 + 327 + ## TOML Configuration Format 328 + 329 + When using `Tauri.toml`, configuration uses kebab-case: 330 + 331 + ```toml 332 + [build] 333 + dev-url = "http://localhost:3000" 334 + before-dev-command = "npm run dev" 335 + before-build-command = "npm run build" 336 + 337 + [app] 338 + with-global-tauri = false 339 + 340 + [[app.windows]] 341 + title = "My Application" 342 + width = 1200 343 + height = 800 344 + resizable = true 345 + center = true 346 + 347 + [app.security] 348 + freeze-prototype = false 349 + 350 + [app.security.asset-protocol] 351 + enable = true 352 + scope = ["$APPDATA/**"] 353 + 354 + [bundle] 355 + active = true 356 + targets = "all" 357 + icon = ["icons/32x32.png", "icons/128x128.png", "icons/icon.icns", "icons/icon.ico"] 358 + 359 + [plugins.updater] 360 + pubkey = "YOUR_PUBLIC_KEY" 361 + endpoints = ["https://releases.example.com/{{target}}/{{arch}}/{{current_version}}"] 362 + ``` 363 + 364 + ## Common Configuration Patterns 365 + 366 + ### Multi-Window Application 367 + 368 + ```json 369 + { 370 + "app": { 371 + "windows": [ 372 + { "label": "main", "title": "Main Window", "width": 1200, "height": 800, "url": "index.html" }, 373 + { "label": "settings", "title": "Settings", "width": 600, "height": 400, "url": "settings.html", "visible": false } 374 + ] 375 + } 376 + } 377 + ``` 378 + 379 + ### System Tray Application 380 + 381 + ```json 382 + { 383 + "app": { 384 + "trayIcon": { "id": "main-tray", "iconPath": "icons/tray.png", "iconAsTemplate": true }, 385 + "windows": [{ "visible": false }] 386 + } 387 + } 388 + ``` 389 + 390 + ### Plugin Configuration 391 + 392 + ```json 393 + { 394 + "plugins": { 395 + "updater": { 396 + "pubkey": "YOUR_PUBLIC_KEY", 397 + "endpoints": ["https://releases.example.com/{{target}}/{{arch}}/{{current_version}}"], 398 + "windows": { "installMode": "passive" } 399 + }, 400 + "shell": { 401 + "open": true, 402 + "scope": [{ "name": "open-url", "cmd": "open", "args": [{ "validator": "\\S+" }] }] 403 + }, 404 + "deep-link": { 405 + "mobile": ["myapp"], 406 + "desktop": { "schemes": ["myapp"] } 407 + } 408 + } 409 + } 410 + ``` 411 + 412 + ## Lock Files 413 + 414 + Commit lock files for reproducible builds: 415 + 416 + | File | Purpose | 417 + |------|---------| 418 + | `Cargo.lock` | Locks Rust dependency versions | 419 + | `package-lock.json` | Locks npm dependency versions | 420 + | `yarn.lock` | Locks Yarn dependency versions | 421 + | `pnpm-lock.yaml` | Locks pnpm dependency versions | 422 + 423 + ## Configuration Validation 424 + 425 + Use the JSON schema for editor autocompletion: 426 + 427 + ```json 428 + { "$schema": "https://schema.tauri.app/config/2" } 429 + ``` 430 + 431 + Run `tauri info` to verify configuration and environment setup.
+414
.agents/skills/configuring-tauri-capabilities/SKILL.md
··· 1 + --- 2 + name: configuring-tauri-capabilities 3 + description: Guides users through configuring Tauri capabilities for security and access control, covering capability files, permissions, per-window security boundaries, and platform-specific configurations. 4 + --- 5 + 6 + # Tauri Capabilities Configuration 7 + 8 + Capabilities are Tauri's permission management system that granularly controls which APIs and commands the frontend can access. They define security boundaries by specifying which permissions apply to which windows or webviews. 9 + 10 + ## What Are Capabilities? 11 + 12 + Capabilities serve as the bridge between permissions and windows/webviews. They: 13 + 14 + - Define which permissions are granted or denied for specific windows 15 + - Enable developers to minimize the impact of frontend compromises 16 + - Create security boundaries based on window labels (not titles) 17 + - Support platform-specific targeting (desktop vs mobile) 18 + 19 + ## Capability File Location 20 + 21 + Capability files reside in `src-tauri/capabilities/` and use JSON or TOML format. 22 + 23 + ## Capability File Structure 24 + 25 + A capability file contains: 26 + 27 + | Field | Required | Description | 28 + |-------|----------|-------------| 29 + | `identifier` | Yes | Unique capability name | 30 + | `description` | No | Purpose explanation | 31 + | `windows` | Yes | Target window labels (supports wildcards) | 32 + | `permissions` | Yes | Array of allowed/denied operations | 33 + | `platforms` | No | Target platforms (linux, macOS, windows, iOS, android) | 34 + | `remote` | No | Remote URL access configuration | 35 + | `$schema` | No | Reference to generated schema for IDE support | 36 + 37 + ## Basic Capability Example 38 + 39 + Create `src-tauri/capabilities/main.json`: 40 + 41 + ```json 42 + { 43 + "$schema": "../gen/schemas/desktop-schema.json", 44 + "identifier": "main-capability", 45 + "description": "Capability for the main window", 46 + "windows": ["main"], 47 + "permissions": [ 48 + "core:path:default", 49 + "core:event:default", 50 + "core:window:default", 51 + "core:app:default", 52 + "core:resources:default", 53 + "core:menu:default", 54 + "core:tray:default" 55 + ] 56 + } 57 + ``` 58 + 59 + ## Default Capabilities 60 + 61 + All capabilities in `src-tauri/capabilities/` are automatically enabled by default. No additional configuration is required. 62 + 63 + To explicitly control which capabilities are active, configure them in `tauri.conf.json`: 64 + 65 + ```json 66 + { 67 + "app": { 68 + "security": { 69 + "capabilities": ["main-capability", "editor-capability"] 70 + } 71 + } 72 + } 73 + ``` 74 + 75 + When explicitly configured, only the listed capabilities apply. 76 + 77 + ## Configuration Methods 78 + 79 + ### Method 1: Separate Files (Recommended) 80 + 81 + Store individual capability files in the capabilities directory: 82 + 83 + ``` 84 + src-tauri/ 85 + capabilities/ 86 + main.json 87 + editor.json 88 + settings.json 89 + ``` 90 + 91 + Reference by identifier in `tauri.conf.json`: 92 + 93 + ```json 94 + { 95 + "app": { 96 + "security": { 97 + "capabilities": ["main-capability", "editor-capability", "settings-capability"] 98 + } 99 + } 100 + } 101 + ``` 102 + 103 + ### Method 2: Inline Definition 104 + 105 + Embed capabilities directly in `tauri.conf.json`: 106 + 107 + ```json 108 + { 109 + "app": { 110 + "security": { 111 + "capabilities": [ 112 + { 113 + "identifier": "my-capability", 114 + "description": "Capability used for all windows", 115 + "windows": ["*"], 116 + "permissions": ["fs:default", "core:window:default"] 117 + } 118 + ] 119 + } 120 + } 121 + } 122 + ``` 123 + 124 + ### Method 3: Mixed Approach 125 + 126 + Combine file-based and inline capabilities: 127 + 128 + ```json 129 + { 130 + "app": { 131 + "security": { 132 + "capabilities": [ 133 + { 134 + "identifier": "inline-capability", 135 + "windows": ["*"], 136 + "permissions": ["fs:default"] 137 + }, 138 + "file-based-capability" 139 + ] 140 + } 141 + } 142 + } 143 + ``` 144 + 145 + ## Per-Window Capabilities 146 + 147 + Assign different permissions to different windows using window labels: 148 + 149 + ### Single Window 150 + 151 + ```json 152 + { 153 + "identifier": "main-capability", 154 + "windows": ["main"], 155 + "permissions": ["core:window:default", "fs:default"] 156 + } 157 + ``` 158 + 159 + ### Multiple Specific Windows 160 + 161 + ```json 162 + { 163 + "identifier": "editor-capability", 164 + "windows": ["editor", "preview"], 165 + "permissions": ["fs:read-files", "core:event:default"] 166 + } 167 + ``` 168 + 169 + ### Wildcard (All Windows) 170 + 171 + ```json 172 + { 173 + "identifier": "global-capability", 174 + "windows": ["*"], 175 + "permissions": ["core:event:default"] 176 + } 177 + ``` 178 + 179 + ### Pattern Matching 180 + 181 + ```json 182 + { 183 + "identifier": "dialog-capability", 184 + "windows": ["dialog-*"], 185 + "permissions": ["core:window:allow-close"] 186 + } 187 + ``` 188 + 189 + ## Permission Syntax 190 + 191 + Permissions follow a naming convention: 192 + 193 + | Pattern | Description | 194 + |---------|-------------| 195 + | `<plugin>:default` | Default permission set for a plugin | 196 + | `<plugin>:allow-<command>` | Allow a specific command | 197 + | `<plugin>:deny-<command>` | Deny a specific command | 198 + 199 + ### Core Permissions 200 + 201 + ```json 202 + { 203 + "permissions": [ 204 + "core:path:default", 205 + "core:event:default", 206 + "core:window:default", 207 + "core:window:allow-set-title", 208 + "core:window:allow-close", 209 + "core:app:default", 210 + "core:resources:default", 211 + "core:menu:default", 212 + "core:tray:default" 213 + ] 214 + } 215 + ``` 216 + 217 + ### Plugin Permissions 218 + 219 + ```json 220 + { 221 + "permissions": [ 222 + "fs:default", 223 + "fs:allow-read-file", 224 + "fs:allow-write-file", 225 + "shell:allow-open", 226 + "dialog:allow-open", 227 + "dialog:allow-save", 228 + "http:default", 229 + "clipboard-manager:allow-read", 230 + "clipboard-manager:allow-write" 231 + ] 232 + } 233 + ``` 234 + 235 + ## Platform-Specific Capabilities 236 + 237 + Target specific platforms using the `platforms` array. 238 + 239 + ### Desktop-Only Capability 240 + 241 + ```json 242 + { 243 + "$schema": "../gen/schemas/desktop-schema.json", 244 + "identifier": "desktop-capability", 245 + "windows": ["main"], 246 + "platforms": ["linux", "macOS", "windows"], 247 + "permissions": [ 248 + "global-shortcut:allow-register", 249 + "global-shortcut:allow-unregister", 250 + "shell:allow-execute" 251 + ] 252 + } 253 + ``` 254 + 255 + ### Mobile-Only Capability 256 + 257 + ```json 258 + { 259 + "$schema": "../gen/schemas/mobile-schema.json", 260 + "identifier": "mobile-capability", 261 + "windows": ["main"], 262 + "platforms": ["iOS", "android"], 263 + "permissions": [ 264 + "nfc:allow-scan", 265 + "biometric:allow-authenticate", 266 + "barcode-scanner:allow-scan" 267 + ] 268 + } 269 + ``` 270 + 271 + ### Separate Files for Platform Variants 272 + 273 + Create platform-specific capability files: 274 + 275 + `src-tauri/capabilities/desktop.json`: 276 + ```json 277 + { 278 + "identifier": "desktop-features", 279 + "windows": ["main"], 280 + "platforms": ["linux", "macOS", "windows"], 281 + "permissions": ["global-shortcut:default", "shell:default"] 282 + } 283 + ``` 284 + 285 + `src-tauri/capabilities/mobile.json`: 286 + ```json 287 + { 288 + "identifier": "mobile-features", 289 + "windows": ["main"], 290 + "platforms": ["iOS", "android"], 291 + "permissions": ["haptics:default", "biometric:default"] 292 + } 293 + ``` 294 + 295 + ## Remote API Access 296 + 297 + Allow remote URLs to access Tauri commands (use with caution): 298 + 299 + ```json 300 + { 301 + "$schema": "../gen/schemas/remote-schema.json", 302 + "identifier": "remote-capability", 303 + "windows": ["main"], 304 + "remote": { 305 + "urls": ["https://*.example.com"] 306 + }, 307 + "permissions": ["http:default"] 308 + } 309 + ``` 310 + 311 + ## Custom Capabilities Example 312 + 313 + A multi-window application with different permission levels: 314 + 315 + `src-tauri/capabilities/main.json`: 316 + ```json 317 + { 318 + "$schema": "../gen/schemas/desktop-schema.json", 319 + "identifier": "main-window", 320 + "description": "Full access for main application window", 321 + "windows": ["main"], 322 + "permissions": [ 323 + "core:default", 324 + "fs:default", 325 + "shell:allow-open", 326 + "dialog:default", 327 + "http:default", 328 + "clipboard-manager:default" 329 + ] 330 + } 331 + ``` 332 + 333 + `src-tauri/capabilities/settings.json`: 334 + ```json 335 + { 336 + "$schema": "../gen/schemas/desktop-schema.json", 337 + "identifier": "settings-window", 338 + "description": "Limited access for settings window", 339 + "windows": ["settings"], 340 + "permissions": [ 341 + "core:window:allow-close", 342 + "core:event:default", 343 + "fs:allow-read-file", 344 + "fs:allow-write-file" 345 + ] 346 + } 347 + ``` 348 + 349 + `src-tauri/capabilities/preview.json`: 350 + ```json 351 + { 352 + "$schema": "../gen/schemas/desktop-schema.json", 353 + "identifier": "preview-window", 354 + "description": "Read-only access for preview window", 355 + "windows": ["preview"], 356 + "permissions": [ 357 + "core:window:default", 358 + "core:event:default", 359 + "fs:allow-read-file" 360 + ] 361 + } 362 + ``` 363 + 364 + ## Security Boundaries 365 + 366 + Capabilities protect against: 367 + 368 + - Frontend compromise impact 369 + - Unintended system interface exposure 370 + - Frontend-to-backend privilege escalation 371 + 372 + Capabilities do NOT protect against: 373 + 374 + - Malicious Rust code 375 + - Overly permissive scopes 376 + - WebView vulnerabilities 377 + 378 + ## Best Practices 379 + 380 + 1. **Principle of Least Privilege**: Grant only the permissions each window needs 381 + 2. **Separate Capabilities by Window**: Create distinct capability files for different windows 382 + 3. **Use Descriptive Identifiers**: Name capabilities clearly (e.g., `main-window`, `editor-readonly`) 383 + 4. **Document Capabilities**: Include descriptions explaining the purpose 384 + 5. **Review Remote Access**: Carefully audit any remote URL access configurations 385 + 6. **Test Permission Boundaries**: Verify windows cannot access unpermitted APIs 386 + 387 + ## Schema Support 388 + 389 + Generated schemas provide IDE autocompletion. Reference them in capability files: 390 + 391 + ```json 392 + { 393 + "$schema": "../gen/schemas/desktop-schema.json" 394 + } 395 + ``` 396 + 397 + Available schemas after build: 398 + - `desktop-schema.json` - Desktop platforms 399 + - `mobile-schema.json` - Mobile platforms 400 + - `remote-schema.json` - Remote access capabilities 401 + 402 + ## Troubleshooting 403 + 404 + ### Permission Denied Errors 405 + 406 + Check that the capability includes the required permission and targets the correct window label. 407 + 408 + ### Capability Not Applied 409 + 410 + Verify the capability file is in `src-tauri/capabilities/` or explicitly listed in `tauri.conf.json`. 411 + 412 + ### Window Label Mismatch 413 + 414 + Window labels in capabilities must match the labels defined when creating windows in Rust code. Labels are case-sensitive.
+422
.agents/skills/configuring-tauri-csp/SKILL.md
··· 1 + --- 2 + name: configuring-tauri-csp 3 + description: Guides users through configuring Content Security Policy (CSP) in Tauri v2 applications to prevent XSS attacks and enhance security by restricting resource loading. 4 + --- 5 + 6 + # Tauri Content Security Policy (CSP) Configuration 7 + 8 + This skill covers Content Security Policy configuration for Tauri v2 desktop applications. 9 + 10 + ## Why CSP Matters in Tauri 11 + 12 + CSP is a security mechanism that mitigates common web vulnerabilities in Tauri applications: 13 + 14 + 1. **XSS Prevention**: Restricts which scripts can execute, blocking injected malicious code 15 + 2. **Resource Control**: Limits where the WebView can load assets from (scripts, styles, images, fonts) 16 + 3. **Trust Boundaries**: Strengthens the isolation between frontend WebView and backend Rust code 17 + 4. **Attack Surface Reduction**: Prevents unauthorized network connections and resource loading 18 + 19 + Tauri operates on a trust boundary model where frontend code has limited access to system resources through a well-defined IPC layer. CSP adds an additional layer of protection within the frontend trust zone. 20 + 21 + ## How Tauri Implements CSP 22 + 23 + Tauri uses a two-part protection strategy: 24 + 25 + 1. **Local Scripts**: Protected through cryptographic hashing at compile time 26 + 2. **Styles and External Scripts**: Verified using nonces 27 + 28 + Tauri automatically appends nonces and hashes to bundled code during compilation. Developers only need to configure application-specific trusted sources. 29 + 30 + **Important**: CSP protection only activates when explicitly configured in the Tauri configuration file. 31 + 32 + ## Default CSP Behavior 33 + 34 + By default, Tauri does not apply a CSP. You must explicitly configure it in `tauri.conf.json` under the `security` section: 35 + 36 + ```json 37 + { 38 + "security": { 39 + "csp": null 40 + } 41 + } 42 + ``` 43 + 44 + When `csp` is `null` or omitted, no Content Security Policy is enforced. 45 + 46 + ## Basic CSP Configuration 47 + 48 + ### Minimal Secure Configuration 49 + 50 + ```json 51 + { 52 + "security": { 53 + "csp": { 54 + "default-src": "'self'" 55 + } 56 + } 57 + } 58 + ``` 59 + 60 + This restricts all resources to the same origin only. 61 + 62 + ### Recommended Configuration 63 + 64 + ```json 65 + { 66 + "security": { 67 + "csp": { 68 + "default-src": "'self' customprotocol: asset:", 69 + "connect-src": "ipc: http://ipc.localhost", 70 + "font-src": ["https://fonts.gstatic.com"], 71 + "img-src": "'self' asset: http://asset.localhost blob: data:", 72 + "style-src": "'unsafe-inline' 'self' https://fonts.googleapis.com" 73 + } 74 + } 75 + } 76 + ``` 77 + 78 + ## Common CSP Directives for Tauri 79 + 80 + ### default-src 81 + 82 + Fallback policy for all resource types not explicitly defined. 83 + 84 + ```json 85 + "default-src": "'self' customprotocol: asset:" 86 + ``` 87 + 88 + Common values: 89 + - `'self'` - Same origin only 90 + - `'none'` - Block all resources 91 + - `customprotocol:` - Tauri custom protocol 92 + - `asset:` - Tauri asset protocol 93 + 94 + ### script-src 95 + 96 + Controls which scripts can execute. 97 + 98 + ```json 99 + "script-src": "'self'" 100 + ``` 101 + 102 + For WebAssembly or Rust-based frontends (Leptos, Yew, Dioxus): 103 + 104 + ```json 105 + "script-src": "'self' 'wasm-unsafe-eval'" 106 + ``` 107 + 108 + **Warning**: Never use `'unsafe-eval'` unless absolutely required. 109 + 110 + ### style-src 111 + 112 + Controls stylesheet sources. 113 + 114 + ```json 115 + "style-src": "'self' 'unsafe-inline' https://fonts.googleapis.com" 116 + ``` 117 + 118 + Note: `'unsafe-inline'` is often needed for CSS-in-JS libraries but reduces security. 119 + 120 + ### connect-src 121 + 122 + Controls allowed connection destinations for fetch, WebSocket, etc. 123 + 124 + ```json 125 + "connect-src": "ipc: http://ipc.localhost https://api.example.com" 126 + ``` 127 + 128 + Tauri-specific: 129 + - `ipc:` - Inter-process communication with Rust backend 130 + - `http://ipc.localhost` - Alternative IPC endpoint 131 + 132 + ### img-src 133 + 134 + Controls image loading sources. 135 + 136 + ```json 137 + "img-src": "'self' asset: http://asset.localhost blob: data:" 138 + ``` 139 + 140 + Common values: 141 + - `blob:` - Blob URLs (for dynamically created images) 142 + - `data:` - Data URLs (base64 encoded images) 143 + - `asset:` - Tauri asset protocol 144 + 145 + ### font-src 146 + 147 + Controls font loading sources. 148 + 149 + ```json 150 + "font-src": "'self' https://fonts.gstatic.com" 151 + ``` 152 + 153 + ### frame-src 154 + 155 + Controls iframe sources. 156 + 157 + ```json 158 + "frame-src": "'none'" 159 + ``` 160 + 161 + Recommended to block all frames unless specifically needed. 162 + 163 + ### object-src 164 + 165 + Controls plugin content (Flash, Java, etc.). 166 + 167 + ```json 168 + "object-src": "'none'" 169 + ``` 170 + 171 + Always set to `'none'` for modern applications. 172 + 173 + ## Configuration Format Options 174 + 175 + ### Object Format (Recommended) 176 + 177 + ```json 178 + { 179 + "security": { 180 + "csp": { 181 + "default-src": "'self'", 182 + "script-src": "'self' 'wasm-unsafe-eval'", 183 + "style-src": "'self' 'unsafe-inline'" 184 + } 185 + } 186 + } 187 + ``` 188 + 189 + ### Array Format for Multiple Sources 190 + 191 + ```json 192 + { 193 + "security": { 194 + "csp": { 195 + "font-src": ["'self'", "https://fonts.gstatic.com", "https://fonts.googleapis.com"] 196 + } 197 + } 198 + } 199 + ``` 200 + 201 + ### String Format 202 + 203 + ```json 204 + { 205 + "security": { 206 + "csp": "default-src 'self'; script-src 'self'" 207 + } 208 + } 209 + ``` 210 + 211 + ## Framework-Specific Configurations 212 + 213 + ### React/Vue/Svelte (Standard JS Frameworks) 214 + 215 + ```json 216 + { 217 + "security": { 218 + "csp": { 219 + "default-src": "'self'", 220 + "script-src": "'self'", 221 + "style-src": "'self' 'unsafe-inline'", 222 + "img-src": "'self' data: blob:", 223 + "font-src": "'self'", 224 + "connect-src": "ipc: http://ipc.localhost" 225 + } 226 + } 227 + } 228 + ``` 229 + 230 + ### Leptos/Yew/Dioxus (Rust/WASM Frameworks) 231 + 232 + ```json 233 + { 234 + "security": { 235 + "csp": { 236 + "default-src": "'self'", 237 + "script-src": "'self' 'wasm-unsafe-eval'", 238 + "style-src": "'self' 'unsafe-inline'", 239 + "img-src": "'self' data: blob:", 240 + "font-src": "'self'", 241 + "connect-src": "ipc: http://ipc.localhost" 242 + } 243 + } 244 + } 245 + ``` 246 + 247 + ### With External APIs 248 + 249 + ```json 250 + { 251 + "security": { 252 + "csp": { 253 + "default-src": "'self'", 254 + "script-src": "'self'", 255 + "connect-src": "ipc: http://ipc.localhost https://api.example.com wss://ws.example.com", 256 + "img-src": "'self' https://cdn.example.com" 257 + } 258 + } 259 + } 260 + ``` 261 + 262 + ## Security Best Practices 263 + 264 + ### 1. Avoid Remote Scripts 265 + 266 + Never load scripts from CDNs in production: 267 + 268 + ```json 269 + // AVOID - introduces attack vector 270 + "script-src": "'self' https://cdn.jsdelivr.net" 271 + 272 + // PREFERRED - bundle all dependencies 273 + "script-src": "'self'" 274 + ``` 275 + 276 + ### 2. Minimize unsafe-inline 277 + 278 + Only use `'unsafe-inline'` when required by your framework: 279 + 280 + ```json 281 + // More secure 282 + "style-src": "'self'" 283 + 284 + // Less secure but sometimes necessary 285 + "style-src": "'self' 'unsafe-inline'" 286 + ``` 287 + 288 + ### 3. Use Restrictive Defaults 289 + 290 + Start restrictive and add permissions as needed: 291 + 292 + ```json 293 + { 294 + "security": { 295 + "csp": { 296 + "default-src": "'none'", 297 + "script-src": "'self'", 298 + "style-src": "'self'", 299 + "img-src": "'self'", 300 + "font-src": "'self'", 301 + "connect-src": "ipc: http://ipc.localhost" 302 + } 303 + } 304 + } 305 + ``` 306 + 307 + ### 4. Block Dangerous Features 308 + 309 + Always block unused dangerous features: 310 + 311 + ```json 312 + { 313 + "security": { 314 + "csp": { 315 + "object-src": "'none'", 316 + "base-uri": "'self'", 317 + "form-action": "'self'" 318 + } 319 + } 320 + } 321 + ``` 322 + 323 + ## Advanced Configuration 324 + 325 + ### Disabling CSP Modifications 326 + 327 + If you need full control over CSP (not recommended): 328 + 329 + ```json 330 + { 331 + "security": { 332 + "csp": { 333 + "default-src": "'self'" 334 + }, 335 + "dangerousDisableAssetCspModification": true 336 + } 337 + } 338 + ``` 339 + 340 + **Warning**: This disables Tauri's automatic nonce and hash injection. 341 + 342 + ### Freeze Prototype 343 + 344 + Additional XSS protection by freezing JavaScript prototypes: 345 + 346 + ```json 347 + { 348 + "security": { 349 + "csp": { 350 + "default-src": "'self'" 351 + }, 352 + "freezePrototype": true 353 + } 354 + } 355 + ``` 356 + 357 + ## Troubleshooting 358 + 359 + ### Resources Blocked by CSP 360 + 361 + Check browser DevTools console for CSP violation messages. They indicate which directive is blocking the resource. 362 + 363 + Example error: 364 + ``` 365 + Refused to load the script 'https://example.com/script.js' because it violates the following Content Security Policy directive: "script-src 'self'" 366 + ``` 367 + 368 + Solution: Add the domain to the appropriate directive: 369 + ```json 370 + "script-src": "'self' https://example.com" 371 + ``` 372 + 373 + ### WebAssembly Not Loading 374 + 375 + Add `'wasm-unsafe-eval'` to script-src: 376 + ```json 377 + "script-src": "'self' 'wasm-unsafe-eval'" 378 + ``` 379 + 380 + ### Inline Styles Not Working 381 + 382 + For CSS-in-JS libraries, add `'unsafe-inline'` to style-src: 383 + ```json 384 + "style-src": "'self' 'unsafe-inline'" 385 + ``` 386 + 387 + ### IPC Not Working 388 + 389 + Ensure connect-src includes Tauri IPC endpoints: 390 + ```json 391 + "connect-src": "ipc: http://ipc.localhost" 392 + ``` 393 + 394 + ## Complete Example Configuration 395 + 396 + ```json 397 + { 398 + "productName": "my-tauri-app", 399 + "version": "1.0.0", 400 + "security": { 401 + "csp": { 402 + "default-src": "'self' customprotocol: asset:", 403 + "script-src": "'self'", 404 + "style-src": "'self' 'unsafe-inline'", 405 + "img-src": "'self' asset: http://asset.localhost blob: data:", 406 + "font-src": "'self'", 407 + "connect-src": "ipc: http://ipc.localhost", 408 + "object-src": "'none'", 409 + "base-uri": "'self'", 410 + "form-action": "'self'", 411 + "frame-ancestors": "'none'" 412 + }, 413 + "freezePrototype": true 414 + } 415 + } 416 + ``` 417 + 418 + ## References 419 + 420 + - [Tauri CSP Documentation](https://v2.tauri.app/security/csp) 421 + - [Tauri Security Overview](https://v2.tauri.app/security/) 422 + - [Tauri SecurityConfig Reference](https://v2.tauri.app/reference/config/#securityconfig)
+357
.agents/skills/configuring-tauri-http-headers/SKILL.md
··· 1 + --- 2 + name: configuring-tauri-http-headers 3 + description: Guides developers through configuring HTTP headers security in Tauri v2 applications, covering security headers, custom headers, and CORS configuration for secure cross-origin resource handling. 4 + --- 5 + 6 + # Tauri HTTP Headers Security Configuration 7 + 8 + This skill covers HTTP headers configuration in Tauri v2.1.0+, enabling developers to set security headers in webview responses. 9 + 10 + ## Overview 11 + 12 + Tauri allows configuring HTTP headers that are included in responses to the webview. These headers apply to production builds and do not affect IPC messages or error responses. 13 + 14 + ## Supported Headers (Allowlist) 15 + 16 + Tauri restricts header configuration to a specific allowlist for security: 17 + 18 + ### CORS Headers 19 + - `Access-Control-Allow-Credentials` 20 + - `Access-Control-Allow-Headers` 21 + - `Access-Control-Allow-Methods` 22 + - `Access-Control-Expose-Headers` 23 + - `Access-Control-Max-Age` 24 + 25 + ### Cross-Origin Policies 26 + - `Cross-Origin-Embedder-Policy` 27 + - `Cross-Origin-Opener-Policy` 28 + - `Cross-Origin-Resource-Policy` 29 + 30 + ### Security Headers 31 + - `X-Content-Type-Options` 32 + - `Permissions-Policy` 33 + - `Timing-Allow-Origin` 34 + - `Service-Worker-Allowed` 35 + 36 + ### Testing Only 37 + - `Tauri-Custom-Header` (not for production use) 38 + 39 + ## Configuration in tauri.conf.json 40 + 41 + Headers are configured under `app.security.headers` in `src-tauri/tauri.conf.json`. 42 + 43 + ### Value Formats 44 + 45 + 1. **String**: Direct assignment 46 + 2. **Array**: Items joined by commas 47 + 3. **Object**: Key-value pairs formatted as "key value", joined by semicolons 48 + 4. **Null**: Header is ignored 49 + 50 + ### Basic Configuration Example 51 + 52 + ```json 53 + { 54 + "app": { 55 + "security": { 56 + "headers": { 57 + "Cross-Origin-Opener-Policy": "same-origin", 58 + "Cross-Origin-Embedder-Policy": "require-corp", 59 + "X-Content-Type-Options": "nosniff" 60 + } 61 + } 62 + } 63 + } 64 + ``` 65 + 66 + ### Comprehensive Configuration Example 67 + 68 + ```json 69 + { 70 + "app": { 71 + "security": { 72 + "headers": { 73 + "Cross-Origin-Opener-Policy": "same-origin", 74 + "Cross-Origin-Embedder-Policy": "require-corp", 75 + "Cross-Origin-Resource-Policy": "same-origin", 76 + "Timing-Allow-Origin": [ 77 + "https://example.com", 78 + "https://api.example.com" 79 + ], 80 + "X-Content-Type-Options": "nosniff", 81 + "Permissions-Policy": { 82 + "camera": "()", 83 + "microphone": "()", 84 + "geolocation": "(self)" 85 + }, 86 + "Access-Control-Allow-Methods": ["GET", "POST", "PUT", "DELETE"], 87 + "Access-Control-Allow-Headers": ["Content-Type", "Authorization"], 88 + "Access-Control-Max-Age": "86400" 89 + }, 90 + "csp": "default-src 'self'; connect-src ipc: http://ipc.localhost" 91 + } 92 + } 93 + } 94 + ``` 95 + 96 + ## Enabling SharedArrayBuffer 97 + 98 + `SharedArrayBuffer` requires specific cross-origin policies. Configure both headers together: 99 + 100 + ```json 101 + { 102 + "app": { 103 + "security": { 104 + "headers": { 105 + "Cross-Origin-Opener-Policy": "same-origin", 106 + "Cross-Origin-Embedder-Policy": "require-corp" 107 + } 108 + } 109 + } 110 + } 111 + ``` 112 + 113 + ## CORS Configuration Examples 114 + 115 + ### Restrictive CORS (Recommended for Production) 116 + 117 + ```json 118 + { 119 + "app": { 120 + "security": { 121 + "headers": { 122 + "Cross-Origin-Resource-Policy": "same-origin", 123 + "Access-Control-Allow-Credentials": "false", 124 + "Access-Control-Allow-Methods": ["GET"], 125 + "Access-Control-Max-Age": "3600" 126 + } 127 + } 128 + } 129 + } 130 + ``` 131 + 132 + ### Permissive CORS (Development/API Scenarios) 133 + 134 + ```json 135 + { 136 + "app": { 137 + "security": { 138 + "headers": { 139 + "Cross-Origin-Resource-Policy": "cross-origin", 140 + "Access-Control-Allow-Methods": ["GET", "POST", "PUT", "DELETE", "OPTIONS"], 141 + "Access-Control-Allow-Headers": ["Content-Type", "Authorization", "X-Requested-With"], 142 + "Access-Control-Expose-Headers": ["Content-Length", "X-Request-Id"], 143 + "Access-Control-Max-Age": "86400" 144 + } 145 + } 146 + } 147 + } 148 + ``` 149 + 150 + ## Development Server Configuration 151 + 152 + Development frameworks require separate header configuration for their dev servers. Tauri header injection only applies to production builds. 153 + 154 + ### Vite (React, Vue, Svelte, Solid, Qwik) 155 + 156 + ```typescript 157 + // vite.config.ts 158 + import { defineConfig } from 'vite'; 159 + 160 + export default defineConfig({ 161 + server: { 162 + headers: { 163 + 'Cross-Origin-Opener-Policy': 'same-origin', 164 + 'Cross-Origin-Embedder-Policy': 'require-corp', 165 + 'X-Content-Type-Options': 'nosniff' 166 + } 167 + } 168 + }); 169 + ``` 170 + 171 + ### Angular 172 + 173 + ```json 174 + // angular.json 175 + { 176 + "projects": { 177 + "your-app": { 178 + "architect": { 179 + "serve": { 180 + "options": { 181 + "headers": { 182 + "Cross-Origin-Opener-Policy": "same-origin", 183 + "Cross-Origin-Embedder-Policy": "require-corp" 184 + } 185 + } 186 + } 187 + } 188 + } 189 + } 190 + } 191 + ``` 192 + 193 + ### Nuxt 194 + 195 + ```typescript 196 + // nuxt.config.ts 197 + export default defineNuxtConfig({ 198 + vite: { 199 + server: { 200 + headers: { 201 + 'Cross-Origin-Opener-Policy': 'same-origin', 202 + 'Cross-Origin-Embedder-Policy': 'require-corp' 203 + } 204 + } 205 + } 206 + }); 207 + ``` 208 + 209 + ### Next.js 210 + 211 + ```javascript 212 + // next.config.js 213 + module.exports = { 214 + async headers() { 215 + return [ 216 + { 217 + source: '/(.*)', 218 + headers: [ 219 + { 220 + key: 'Cross-Origin-Opener-Policy', 221 + value: 'same-origin' 222 + }, 223 + { 224 + key: 'Cross-Origin-Embedder-Policy', 225 + value: 'require-corp' 226 + } 227 + ] 228 + } 229 + ]; 230 + } 231 + }; 232 + ``` 233 + 234 + ### Trunk (Yew, Leptos) 235 + 236 + ```toml 237 + # Trunk.toml 238 + [serve] 239 + headers = { "Cross-Origin-Opener-Policy" = "same-origin", "Cross-Origin-Embedder-Policy" = "require-corp" } 240 + ``` 241 + 242 + ## Security Headers Reference 243 + 244 + ### Cross-Origin-Opener-Policy (COOP) 245 + 246 + Controls window opener relationships: 247 + 248 + | Value | Description | 249 + |-------|-------------| 250 + | `unsafe-none` | Default, allows opener access | 251 + | `same-origin` | Isolates browsing context to same-origin | 252 + | `same-origin-allow-popups` | Same-origin but allows popups | 253 + 254 + ### Cross-Origin-Embedder-Policy (COEP) 255 + 256 + Controls resource embedding: 257 + 258 + | Value | Description | 259 + |-------|-------------| 260 + | `unsafe-none` | Default, no restrictions | 261 + | `require-corp` | Requires CORP or CORS for cross-origin resources | 262 + | `credentialless` | Cross-origin requests without credentials | 263 + 264 + ### Cross-Origin-Resource-Policy (CORP) 265 + 266 + Controls who can load your resources: 267 + 268 + | Value | Description | 269 + |-------|-------------| 270 + | `same-site` | Only same-site requests | 271 + | `same-origin` | Only same-origin requests | 272 + | `cross-origin` | Allows cross-origin requests | 273 + 274 + ### X-Content-Type-Options 275 + 276 + Prevents MIME type sniffing: 277 + 278 + ```json 279 + { 280 + "X-Content-Type-Options": "nosniff" 281 + } 282 + ``` 283 + 284 + ### Permissions-Policy 285 + 286 + Controls browser feature access: 287 + 288 + ```json 289 + { 290 + "Permissions-Policy": { 291 + "camera": "()", 292 + "microphone": "()", 293 + "geolocation": "(self)", 294 + "fullscreen": "(self)" 295 + } 296 + } 297 + ``` 298 + 299 + ## Best Practices 300 + 301 + 1. **Configure Both Dev and Prod**: Set headers in both your framework's dev server config and `tauri.conf.json` for consistent behavior. 302 + 303 + 2. **Use Restrictive Defaults**: Start with restrictive policies and loosen only as needed. 304 + 305 + 3. **Enable COOP/COEP Together**: For `SharedArrayBuffer` support, both headers must be configured. 306 + 307 + 4. **Separate CSP Configuration**: Content-Security-Policy is configured under `app.security.csp`, not in the headers section. 308 + 309 + 5. **Avoid Tauri-Custom-Header in Production**: This header is for testing purposes only. 310 + 311 + 6. **Test Cross-Origin Scenarios**: Verify that CORS headers work correctly with your API endpoints. 312 + 313 + ## Troubleshooting 314 + 315 + ### SharedArrayBuffer Not Available 316 + 317 + Ensure both headers are set: 318 + ```json 319 + { 320 + "Cross-Origin-Opener-Policy": "same-origin", 321 + "Cross-Origin-Embedder-Policy": "require-corp" 322 + } 323 + ``` 324 + 325 + ### Headers Not Applied in Development 326 + 327 + Headers in `tauri.conf.json` only apply to production builds. Configure your dev server separately. 328 + 329 + ### CORS Errors with External APIs 330 + 331 + Add required headers for cross-origin requests: 332 + ```json 333 + { 334 + "Access-Control-Allow-Methods": ["GET", "POST", "OPTIONS"], 335 + "Access-Control-Allow-Headers": ["Content-Type", "Authorization"] 336 + } 337 + ``` 338 + 339 + ### Custom Headers Not Visible 340 + 341 + Expose custom headers via: 342 + ```json 343 + { 344 + "Access-Control-Expose-Headers": ["X-Custom-Header", "X-Request-Id"] 345 + } 346 + ``` 347 + 348 + ## Version Requirements 349 + 350 + - Tauri v2.1.0 or later required for HTTP headers configuration 351 + - Headers feature is not available in earlier versions 352 + 353 + ## Related Configuration 354 + 355 + - **CSP**: Configure under `app.security.csp` for Content Security Policy 356 + - **Capabilities**: Use Tauri's capability system for fine-grained permissions 357 + - **IPC Security**: Headers do not affect IPC message handling
+435
.agents/skills/configuring-tauri-permissions/SKILL.md
··· 1 + --- 2 + name: configuring-tauri-permissions 3 + description: Guides the user through configuring Tauri permissions, including the security permission system, allow and deny lists, plugin permissions, permission identifiers, scopes, and capability integration. 4 + --- 5 + 6 + # Tauri Permissions Configuration 7 + 8 + This skill covers the Tauri v2 permission system for controlling frontend access to backend commands and system resources. 9 + 10 + ## Permission System Overview 11 + 12 + Permissions in Tauri are explicit privileges that grant or deny access to specific commands. They form the security boundary between frontend code and system resources. 13 + 14 + ### Core Components 15 + 16 + | Component | Purpose | 17 + |-----------|---------| 18 + | Permission | Defines access to specific commands | 19 + | Scope | Restricts commands to specific paths/resources | 20 + | Capability | Links permissions to windows/webviews | 21 + | Identifier | Unique name referencing a permission | 22 + 23 + ### Security Model 24 + 25 + - Frontend code cannot access commands without explicit permission 26 + - Deny rules always take precedence over allow rules 27 + - Permissions must be linked to capabilities to be active 28 + - Each window/webview can have different permissions 29 + 30 + ## Permission Identifiers 31 + 32 + ### Naming Convention 33 + 34 + Format: `<plugin-name>:<permission-type>` 35 + 36 + | Pattern | Example | Description | 37 + |---------|---------|-------------| 38 + | `<name>:default` | `fs:default` | Default permission set | 39 + | `<name>:allow-<command>` | `fs:allow-read-file` | Allow specific command | 40 + | `<name>:deny-<command>` | `fs:deny-write-file` | Deny specific command | 41 + | `<name>:allow-<scope>` | `fs:allow-app-read` | Allow with predefined scope | 42 + 43 + ### Identifier Rules 44 + 45 + - Lowercase ASCII letters only: `[a-z]` 46 + - Maximum length: 116 characters 47 + - Plugin prefixes (`tauri-plugin-`) added automatically at compile time 48 + 49 + ## Directory Structure 50 + 51 + ### Application Structure 52 + 53 + ``` 54 + src-tauri/ 55 + ├── capabilities/ 56 + │ ├── default.json # Main capability file 57 + │ └── admin.toml # Additional capabilities 58 + ├── permissions/ 59 + │ └── custom-permission.toml # Custom app permissions 60 + └── tauri.conf.json 61 + ``` 62 + 63 + ### Plugin Structure 64 + 65 + ``` 66 + tauri-plugin-example/ 67 + ├── permissions/ 68 + │ ├── default.toml # Default permission set 69 + │ ├── autogenerated/ # Auto-generated from commands 70 + │ │ └── commands/ 71 + │ └── custom-scope.toml # Custom scopes 72 + └── src/ 73 + ├── commands.rs 74 + └── build.rs 75 + ``` 76 + 77 + ## Capability Configuration 78 + 79 + Capabilities link permissions to windows and define what frontend contexts can access. 80 + 81 + ### JSON Format (Recommended for Apps) 82 + 83 + ```json 84 + { 85 + "$schema": "../gen/schemas/desktop-schema.json", 86 + "identifier": "main-capability", 87 + "description": "Main window permissions", 88 + "windows": ["main"], 89 + "permissions": [ 90 + "core:default", 91 + "fs:default", 92 + "fs:allow-read-text-file", 93 + { 94 + "identifier": "fs:allow-write-text-file", 95 + "allow": [{ "path": "$APPDATA/*" }] 96 + } 97 + ] 98 + } 99 + ``` 100 + 101 + ### TOML Format 102 + 103 + ```toml 104 + "$schema" = "../gen/schemas/desktop-schema.json" 105 + identifier = "main-capability" 106 + description = "Main window permissions" 107 + windows = ["main"] 108 + permissions = [ 109 + "core:default", 110 + "fs:default", 111 + "fs:allow-read-text-file" 112 + ] 113 + 114 + [[permissions]] 115 + identifier = "fs:allow-write-text-file" 116 + allow = [{ path = "$APPDATA/*" }] 117 + ``` 118 + 119 + ### Window Targeting 120 + 121 + ```json 122 + { 123 + "identifier": "admin-capability", 124 + "windows": ["admin", "settings"], 125 + "permissions": ["fs:allow-write-all"] 126 + } 127 + ``` 128 + 129 + Use `"*"` to target all windows: 130 + 131 + ```json 132 + { 133 + "windows": ["*"], 134 + "permissions": ["core:default"] 135 + } 136 + ``` 137 + 138 + ### Platform-Specific Capabilities 139 + 140 + ```json 141 + { 142 + "identifier": "desktop-capability", 143 + "platforms": ["linux", "macOS", "windows"], 144 + "windows": ["main"], 145 + "permissions": ["fs:allow-app-read-recursive"] 146 + } 147 + ``` 148 + 149 + ```json 150 + { 151 + "identifier": "mobile-capability", 152 + "platforms": ["iOS", "android"], 153 + "windows": ["main"], 154 + "permissions": ["fs:allow-app-read"] 155 + } 156 + ``` 157 + 158 + ## Allow and Deny Lists 159 + 160 + ### Basic Scope Configuration 161 + 162 + ```json 163 + { 164 + "identifier": "fs:allow-read-file", 165 + "allow": [ 166 + { "path": "$HOME/Documents/*" }, 167 + { "path": "$APPDATA/**" } 168 + ], 169 + "deny": [ 170 + { "path": "$HOME/Documents/secrets/*" } 171 + ] 172 + } 173 + ``` 174 + 175 + ### Scope Variables 176 + 177 + | Variable | Description | 178 + |----------|-------------| 179 + | `$APP` | Application install directory | 180 + | `$APPCONFIG` | App config directory | 181 + | `$APPDATA` | App data directory | 182 + | `$APPLOCALDATA` | App local data directory | 183 + | `$APPCACHE` | App cache directory | 184 + | `$APPLOG` | App log directory | 185 + | `$HOME` | User home directory | 186 + | `$DESKTOP` | Desktop directory | 187 + | `$DOCUMENT` | Documents directory | 188 + | `$DOWNLOAD` | Downloads directory | 189 + | `$RESOURCE` | App resource directory | 190 + | `$TEMP` | Temporary directory | 191 + 192 + ### Glob Patterns 193 + 194 + | Pattern | Matches | 195 + |---------|---------| 196 + | `*` | Any file in directory | 197 + | `**` | Recursive (all subdirectories) | 198 + | `*.txt` | Files with .txt extension | 199 + 200 + ### Deny Precedence 201 + 202 + Deny rules always override allow rules: 203 + 204 + ```json 205 + { 206 + "permissions": [ 207 + { 208 + "identifier": "fs:allow-read-file", 209 + "allow": [{ "path": "$HOME/**" }], 210 + "deny": [{ "path": "$HOME/.ssh/**" }] 211 + } 212 + ] 213 + } 214 + ``` 215 + 216 + ## Plugin Permissions 217 + 218 + ### Using Default Plugin Permissions 219 + 220 + ```json 221 + { 222 + "permissions": [ 223 + "fs:default", 224 + "shell:default", 225 + "http:default", 226 + "dialog:default" 227 + ] 228 + } 229 + ``` 230 + 231 + ### Common Plugin Permission Patterns 232 + 233 + #### Filesystem Plugin 234 + 235 + ```json 236 + { 237 + "permissions": [ 238 + "fs:default", 239 + "fs:allow-read-text-file", 240 + "fs:allow-write-text-file", 241 + "fs:allow-app-read-recursive", 242 + "fs:allow-app-write-recursive", 243 + "fs:deny-default" 244 + ] 245 + } 246 + ``` 247 + 248 + #### HTTP Plugin 249 + 250 + ```json 251 + { 252 + "permissions": [ 253 + "http:default", 254 + { 255 + "identifier": "http:default", 256 + "allow": [{ "url": "https://api.example.com/*" }], 257 + "deny": [{ "url": "https://api.example.com/admin/*" }] 258 + } 259 + ] 260 + } 261 + ``` 262 + 263 + #### Shell Plugin 264 + 265 + ```json 266 + { 267 + "permissions": [ 268 + "shell:allow-open", 269 + { 270 + "identifier": "shell:allow-execute", 271 + "allow": [ 272 + { "name": "git", "cmd": "git", "args": true } 273 + ] 274 + } 275 + ] 276 + } 277 + ``` 278 + 279 + ### Directory-Specific Filesystem Permissions 280 + 281 + | Permission | Access | 282 + |------------|--------| 283 + | `fs:allow-appdata-read` | Read $APPDATA (non-recursive) | 284 + | `fs:allow-appdata-read-recursive` | Read $APPDATA (recursive) | 285 + | `fs:allow-appdata-write` | Write $APPDATA (non-recursive) | 286 + | `fs:allow-appdata-write-recursive` | Write $APPDATA (recursive) | 287 + | `fs:allow-home-read-recursive` | Read $HOME (recursive) | 288 + | `fs:allow-temp-write` | Write to temp directory | 289 + 290 + ## Custom Permission Definition 291 + 292 + ### TOML Permission File 293 + 294 + Create `src-tauri/permissions/my-permission.toml`: 295 + 296 + ```toml 297 + [[permission]] 298 + identifier = "my-app:config-access" 299 + description = "Access to app configuration files" 300 + commands.allow = ["read_config", "write_config"] 301 + 302 + [[scope.allow]] 303 + path = "$APPCONFIG/*" 304 + 305 + [[scope.deny]] 306 + path = "$APPCONFIG/secrets.json" 307 + ``` 308 + 309 + ### Permission Sets 310 + 311 + Group multiple permissions: 312 + 313 + ```toml 314 + [[set]] 315 + identifier = "my-app:full-access" 316 + description = "Full application access" 317 + permissions = [ 318 + "my-app:config-access", 319 + "fs:allow-app-read-recursive", 320 + "fs:allow-app-write-recursive" 321 + ] 322 + ``` 323 + 324 + ### Auto-Generated Command Permissions 325 + 326 + In plugin `src/build.rs`: 327 + 328 + ```rust 329 + const COMMANDS: &[&str] = &["get_user", "save_user", "delete_user"]; 330 + 331 + fn main() { 332 + tauri_plugin::Builder::new(COMMANDS) 333 + .build(); 334 + } 335 + ``` 336 + 337 + This generates: 338 + - `allow-get-user` / `deny-get-user` 339 + - `allow-save-user` / `deny-save-user` 340 + - `allow-delete-user` / `deny-delete-user` 341 + 342 + ### Default Permission Set 343 + 344 + Create `permissions/default.toml`: 345 + 346 + ```toml 347 + [default] 348 + description = "Default permissions for my-plugin" 349 + permissions = [ 350 + "allow-get-user", 351 + "allow-save-user" 352 + ] 353 + ``` 354 + 355 + ## Remote Access Configuration 356 + 357 + Allow remote URLs to access Tauri APIs (use with caution): 358 + 359 + ```json 360 + { 361 + "identifier": "remote-capability", 362 + "windows": ["main"], 363 + "remote": { 364 + "urls": ["https://*.myapp.com"] 365 + }, 366 + "permissions": [ 367 + "core:default" 368 + ] 369 + } 370 + ``` 371 + 372 + **Security Warning**: Linux and Android cannot distinguish iframe requests from window requests. 373 + 374 + ## Configuration in tauri.conf.json 375 + 376 + Reference capabilities by identifier: 377 + 378 + ```json 379 + { 380 + "app": { 381 + "security": { 382 + "capabilities": ["main-capability", "admin-capability"] 383 + } 384 + } 385 + } 386 + ``` 387 + 388 + Or inline capabilities directly: 389 + 390 + ```json 391 + { 392 + "app": { 393 + "security": { 394 + "capabilities": [ 395 + { 396 + "identifier": "inline-capability", 397 + "windows": ["*"], 398 + "permissions": ["core:default"] 399 + } 400 + ] 401 + } 402 + } 403 + } 404 + ``` 405 + 406 + ## Troubleshooting 407 + 408 + ### Common Errors 409 + 410 + **"Not allowed on this command"** 411 + - Verify command permission is in capability 412 + - Check scope includes the target path 413 + - Ensure capability targets correct window 414 + 415 + **Permission not found** 416 + - Check identifier spelling (lowercase only) 417 + - Verify plugin is installed 418 + - Run `cargo build` to regenerate permissions 419 + 420 + ### Debugging Permissions 421 + 422 + 1. Check generated schema: `src-tauri/gen/schemas/` 423 + 2. Review capability files load correctly 424 + 3. Verify window names match capability targets 425 + 4. Check deny rules are not blocking access 426 + 427 + ## Best Practices 428 + 429 + 1. **Principle of Least Privilege**: Only grant permissions actually needed 430 + 2. **Use Specific Scopes**: Prefer `$APPDATA/*` over `$HOME/**` 431 + 3. **Deny Sensitive Paths**: Always deny access to `.ssh`, credentials, etc. 432 + 4. **Separate Capabilities**: Use different capabilities for different window types 433 + 5. **Document Custom Permissions**: Include clear descriptions 434 + 6. **Review Plugin Defaults**: Understand what default permissions grant 435 + 7. **Platform-Specific Config**: Use platform targeting for OS-specific needs
+441
.agents/skills/configuring-tauri-scopes/SKILL.md
··· 1 + --- 2 + name: configuring-tauri-scopes 3 + description: Guides users through configuring Tauri command scopes for security, including filesystem restrictions, URL patterns, dynamic scope management, and capability-based access control. 4 + --- 5 + 6 + # Tauri Command Scopes 7 + 8 + This skill covers configuring scopes in Tauri v2 applications to control fine-grained access to commands and resources. 9 + 10 + ## What Are Scopes? 11 + 12 + Scopes are a granular authorization mechanism in Tauri that controls what specific operations a command can perform. They function as fine-grained permission boundaries beyond basic command access. 13 + 14 + ### Key Characteristics 15 + 16 + - **Allow scopes**: Explicitly permit certain operations 17 + - **Deny scopes**: Explicitly restrict certain operations 18 + - **Deny takes precedence**: When both exist, deny rules always win 19 + - **Command responsibility**: The command implementation must validate and enforce scope restrictions 20 + 21 + ### How Scopes Work 22 + 23 + The scope is passed to the command during execution. The command implementation is responsible for validating against the scope and enforcing restrictions. This means developers must carefully implement scope validation to prevent bypasses. 24 + 25 + ## Scope Configuration Location 26 + 27 + Scopes are configured in capability files located at: 28 + - `src-tauri/capabilities/default.json` (primary) 29 + - `src-tauri/capabilities/*.json` (additional capability files) 30 + 31 + ## Filesystem Scopes 32 + 33 + The filesystem plugin uses glob-compatible path patterns to define accessible paths. 34 + 35 + ### Basic Filesystem Scope Configuration 36 + 37 + ```json 38 + { 39 + "$schema": "../gen/schemas/desktop-schema.json", 40 + "identifier": "default", 41 + "description": "Default capability for the application", 42 + "windows": ["main"], 43 + "permissions": [ 44 + { 45 + "identifier": "fs:scope", 46 + "allow": [{ "path": "$APPDATA" }, { "path": "$APPDATA/**" }] 47 + } 48 + ] 49 + } 50 + ``` 51 + 52 + ### Command-Specific Scopes 53 + 54 + Restrict individual filesystem operations rather than global access: 55 + 56 + ```json 57 + { 58 + "permissions": [ 59 + { 60 + "identifier": "fs:allow-read-text-file", 61 + "allow": [{ "path": "$DOCUMENT/**" }] 62 + }, 63 + { 64 + "identifier": "fs:allow-write-text-file", 65 + "allow": [{ "path": "$HOME/notes.txt" }] 66 + } 67 + ] 68 + } 69 + ``` 70 + 71 + ### Combined Allow and Deny Scopes 72 + 73 + ```json 74 + { 75 + "permissions": [ 76 + { 77 + "identifier": "fs:allow-rename", 78 + "allow": [{ "path": "$HOME/**" }], 79 + "deny": [{ "path": "$HOME/.config/**" }] 80 + } 81 + ] 82 + } 83 + ``` 84 + 85 + ### Available Path Variables 86 + 87 + Tauri provides runtime-injected variables for common system directories: 88 + 89 + | Variable | Description | 90 + |----------|-------------| 91 + | `$APPCONFIG` | Application config directory | 92 + | `$APPDATA` | Application data directory | 93 + | `$APPLOCALDATA` | Application local data directory | 94 + | `$APPCACHE` | Application cache directory | 95 + | `$APPLOG` | Application log directory | 96 + | `$AUDIO` | User audio directory | 97 + | `$CACHE` | System cache directory | 98 + | `$CONFIG` | System config directory | 99 + | `$DATA` | System data directory | 100 + | `$DESKTOP` | User desktop directory | 101 + | `$DOCUMENT` | User documents directory | 102 + | `$DOWNLOAD` | User downloads directory | 103 + | `$EXE` | Application executable directory | 104 + | `$HOME` | User home directory | 105 + | `$PICTURE` | User pictures directory | 106 + | `$PUBLIC` | Public directory | 107 + | `$RESOURCE` | Application resource directory | 108 + | `$TEMP` | Temporary directory | 109 + | `$VIDEO` | User video directory | 110 + 111 + ## Scope Patterns 112 + 113 + Scopes support glob patterns for flexible path matching. 114 + 115 + ### Pattern Examples 116 + 117 + ```json 118 + { 119 + "permissions": [ 120 + { 121 + "identifier": "fs:scope", 122 + "allow": [ 123 + { "path": "$APPDATA/databases/*" }, 124 + { "path": "$DOCUMENT/**/*.txt" }, 125 + { "path": "$HOME/project/src/**" } 126 + ], 127 + "deny": [ 128 + { "path": "$HOME/.ssh/**" }, 129 + { "path": "$HOME/.gnupg/**" } 130 + ] 131 + } 132 + ] 133 + } 134 + ``` 135 + 136 + ### Pattern Syntax 137 + 138 + | Pattern | Meaning | 139 + |---------|---------| 140 + | `*` | Matches any characters except path separator | 141 + | `**` | Matches any characters including path separator (recursive) | 142 + | `?` | Matches a single character | 143 + | `[abc]` | Matches any character in brackets | 144 + 145 + ### Path Traversal Prevention 146 + 147 + Tauri prevents path traversal attacks. These paths are NOT allowed: 148 + - `/usr/path/to/../file` 149 + - `../path/to/file` 150 + 151 + ## HTTP Plugin Scopes 152 + 153 + The HTTP plugin uses URL patterns to control network access. 154 + 155 + ### URL Scope Configuration 156 + 157 + ```json 158 + { 159 + "permissions": [ 160 + { 161 + "identifier": "http:default", 162 + "allow": [{ "url": "https://*.tauri.app" }], 163 + "deny": [{ "url": "https://private.tauri.app" }] 164 + } 165 + ] 166 + } 167 + ``` 168 + 169 + ### URL Pattern Examples 170 + 171 + ```json 172 + { 173 + "permissions": [ 174 + { 175 + "identifier": "http:default", 176 + "allow": [ 177 + { "url": "https://api.example.com/*" }, 178 + { "url": "https://*.cdn.example.com/**" } 179 + ] 180 + } 181 + ] 182 + } 183 + ``` 184 + 185 + ## Defining Custom Permissions with Scopes (TOML) 186 + 187 + For plugins or custom commands, define permissions in TOML files. 188 + 189 + ### Basic Permission with Scope 190 + 191 + ```toml 192 + # permissions/my-permission.toml 193 + [[permission]] 194 + identifier = "scope-appdata-recursive" 195 + description = "Recursive access to APPDATA folder" 196 + 197 + [[permission.scope.allow]] 198 + path = "$APPDATA/**" 199 + ``` 200 + 201 + ### Permission with Deny Scope 202 + 203 + ```toml 204 + [[permission]] 205 + identifier = "deny-sensitive-data" 206 + description = "Denies access to sensitive directories" 207 + platforms = ["linux", "macos"] 208 + 209 + [[permission.scope.deny]] 210 + path = "$HOME/.ssh/**" 211 + 212 + [[permission.scope.deny]] 213 + path = "$HOME/.gnupg/**" 214 + ``` 215 + 216 + ### Permission Sets 217 + 218 + Combine permissions into reusable sets: 219 + 220 + ```toml 221 + [[set]] 222 + identifier = "safe-appdata-access" 223 + description = "Allows APPDATA access while denying sensitive folders" 224 + permissions = ["scope-appdata-recursive", "deny-sensitive-data"] 225 + ``` 226 + 227 + ## Dynamic Scopes (Runtime Management) 228 + 229 + Tauri allows runtime scope modification using the `FsExt` trait from Rust. 230 + 231 + ### Basic Runtime Scope Expansion 232 + 233 + ```rust 234 + use tauri_plugin_fs::FsExt; 235 + 236 + pub fn run() { 237 + tauri::Builder::default() 238 + .plugin(tauri_plugin_fs::init()) 239 + .setup(|app| { 240 + let scope = app.fs_scope(); 241 + // Allow a specific directory (non-recursive) 242 + scope.allow_directory("/path/to/directory", false)?; 243 + // Check what's currently allowed 244 + dbg!(scope.allowed()); 245 + Ok(()) 246 + }) 247 + .run(tauri::generate_context!()) 248 + .expect("error while running tauri application"); 249 + } 250 + ``` 251 + 252 + ### Tauri Command for Scope Expansion 253 + 254 + ```rust 255 + use tauri_plugin_fs::FsExt; 256 + 257 + #[tauri::command] 258 + fn expand_scope( 259 + app_handle: tauri::AppHandle, 260 + folder_path: std::path::PathBuf 261 + ) -> Result<(), String> { 262 + // Verify path before expanding scope 263 + if !folder_path.exists() { 264 + return Err("Path does not exist".to_string()); 265 + } 266 + 267 + // true = allow inner directories recursively 268 + app_handle 269 + .fs_scope() 270 + .allow_directory(&folder_path, true) 271 + .map_err(|err| err.to_string()) 272 + } 273 + ``` 274 + 275 + ### Allow Specific File 276 + 277 + ```rust 278 + #[tauri::command] 279 + fn allow_file( 280 + app_handle: tauri::AppHandle, 281 + file_path: std::path::PathBuf 282 + ) -> Result<(), String> { 283 + app_handle 284 + .fs_scope() 285 + .allow_file(&file_path) 286 + .map_err(|err| err.to_string()) 287 + } 288 + ``` 289 + 290 + ### Security Warning 291 + 292 + Dynamic scope expansion should be used carefully: 293 + - Validate paths before expanding scope 294 + - Prefer static configuration when possible 295 + - Never expand scope based on unvalidated user input 296 + 297 + ## Remote URL Scopes (Capabilities) 298 + 299 + Control which remote URLs can access your application's commands. 300 + 301 + ```json 302 + { 303 + "identifier": "remote-api-access", 304 + "description": "Allow remote access from specific domains", 305 + "windows": ["main"], 306 + "remote": { 307 + "urls": ["https://*.mydomain.dev", "https://app.example.com"] 308 + }, 309 + "permissions": ["core:default"] 310 + } 311 + ``` 312 + 313 + ## Complete Capability File Example 314 + 315 + ```json 316 + { 317 + "$schema": "../gen/schemas/desktop-schema.json", 318 + "identifier": "default", 319 + "description": "Default capability for desktop application", 320 + "windows": ["main", "settings"], 321 + "platforms": ["linux", "macos", "windows"], 322 + "permissions": [ 323 + "core:default", 324 + "core:window:allow-set-title", 325 + { 326 + "identifier": "fs:default" 327 + }, 328 + { 329 + "identifier": "fs:allow-read-text-file", 330 + "allow": [ 331 + { "path": "$DOCUMENT/**/*.md" }, 332 + { "path": "$DOCUMENT/**/*.txt" } 333 + ] 334 + }, 335 + { 336 + "identifier": "fs:allow-write-text-file", 337 + "allow": [{ "path": "$APPDATA/notes/**" }], 338 + "deny": [{ "path": "$APPDATA/notes/.secret/**" }] 339 + }, 340 + { 341 + "identifier": "http:default", 342 + "allow": [{ "url": "https://api.example.com/*" }] 343 + } 344 + ] 345 + } 346 + ``` 347 + 348 + ## Security Best Practices 349 + 350 + 1. **Minimize scope**: Only allow paths and URLs that are absolutely necessary 351 + 2. **Use deny rules**: Explicitly block sensitive directories even within allowed paths 352 + 3. **Prefer command-specific scopes**: Use `fs:allow-read-text-file` over global `fs:scope` 353 + 4. **Validate dynamic scopes**: Always verify paths before runtime scope expansion 354 + 5. **Audit scope enforcement**: Command developers must implement proper scope validation 355 + 6. **Use path variables**: Prefer `$APPDATA` over hardcoded paths for portability 356 + 357 + ## Common Scope Patterns 358 + 359 + ### Read-Only Application Data 360 + 361 + ```json 362 + { 363 + "permissions": [ 364 + { 365 + "identifier": "fs:allow-read-text-file", 366 + "allow": [{ "path": "$APPDATA/**" }] 367 + }, 368 + { 369 + "identifier": "fs:allow-exists", 370 + "allow": [{ "path": "$APPDATA/**" }] 371 + } 372 + ] 373 + } 374 + ``` 375 + 376 + ### User Document Access 377 + 378 + ```json 379 + { 380 + "permissions": [ 381 + { 382 + "identifier": "fs:scope", 383 + "allow": [{ "path": "$DOCUMENT/**" }], 384 + "deny": [ 385 + { "path": "$DOCUMENT/.hidden/**" }, 386 + { "path": "$DOCUMENT/**/*.key" } 387 + ] 388 + } 389 + ] 390 + } 391 + ``` 392 + 393 + ### API-Only HTTP Access 394 + 395 + ```json 396 + { 397 + "permissions": [ 398 + { 399 + "identifier": "http:default", 400 + "allow": [ 401 + { "url": "https://api.myapp.com/v1/*" }, 402 + { "url": "https://cdn.myapp.com/**" } 403 + ], 404 + "deny": [ 405 + { "url": "https://api.myapp.com/v1/admin/*" } 406 + ] 407 + } 408 + ] 409 + } 410 + ``` 411 + 412 + ## Troubleshooting 413 + 414 + ### "Path not allowed on the configured scope" 415 + 416 + This error indicates the requested path is outside the configured scope. Solutions: 417 + 418 + 1. Add the path to your capability's allow list 419 + 2. Check for typos in path variables 420 + 3. Verify glob patterns match the intended paths 421 + 4. Check if a deny rule is blocking the path 422 + 423 + ### Testing Scope Configuration 424 + 425 + Run in development mode to test permissions: 426 + 427 + ```bash 428 + pnpm tauri dev 429 + # or 430 + cargo tauri dev 431 + ``` 432 + 433 + Permission errors will appear in the console indicating which permissions need configuration. 434 + 435 + ## References 436 + 437 + - [Command Scopes Documentation](https://v2.tauri.app/security/scope/) 438 + - [Permissions Overview](https://v2.tauri.app/security/permissions/) 439 + - [Capabilities Reference](https://v2.tauri.app/reference/acl/capability/) 440 + - [Filesystem Plugin](https://v2.tauri.app/plugin/file-system/) 441 + - [HTTP Plugin](https://v2.tauri.app/plugin/http-client/)
+401
.agents/skills/customizing-tauri-windows/SKILL.md
··· 1 + --- 2 + name: customizing-tauri-windows 3 + description: Guides users through Tauri window customization including custom titlebar implementation, transparent windows, window decorations, drag regions, window menus, submenus, and menu keyboard shortcuts for desktop applications. 4 + --- 5 + 6 + # Tauri Window Customization 7 + 8 + Covers window customization in Tauri v2: custom titlebars, transparent windows, and window menus. 9 + 10 + ## Configuration Methods 11 + 12 + - **tauri.conf.json** - Static configuration at build time 13 + - **JavaScript Window API** - Runtime modifications from frontend 14 + - **Rust Window struct** - Runtime modifications from backend 15 + 16 + ## Window Configuration (tauri.conf.json) 17 + 18 + ```json 19 + { 20 + "app": { 21 + "windows": [{ 22 + "title": "My App", 23 + "width": 800, 24 + "height": 600, 25 + "decorations": true, 26 + "transparent": false, 27 + "alwaysOnTop": false, 28 + "center": true 29 + }] 30 + } 31 + } 32 + ``` 33 + 34 + ## Custom Titlebar Implementation 35 + 36 + ### Step 1: Disable Decorations 37 + 38 + ```json 39 + { "app": { "windows": [{ "decorations": false }] } } 40 + ``` 41 + 42 + ### Step 2: Configure Permissions (src-tauri/capabilities/default.json) 43 + 44 + ```json 45 + { 46 + "identifier": "main-capability", 47 + "windows": ["main"], 48 + "permissions": [ 49 + "core:window:default", 50 + "core:window:allow-start-dragging", 51 + "core:window:allow-close", 52 + "core:window:allow-minimize", 53 + "core:window:allow-toggle-maximize" 54 + ] 55 + } 56 + ``` 57 + 58 + ### Step 3: HTML Structure 59 + 60 + ```html 61 + <div class="titlebar"> 62 + <div class="titlebar-drag" data-tauri-drag-region> 63 + <span class="title">My Application</span> 64 + </div> 65 + <div class="titlebar-controls"> 66 + <button id="titlebar-minimize">-</button> 67 + <button id="titlebar-maximize">[]</button> 68 + <button id="titlebar-close">x</button> 69 + </div> 70 + </div> 71 + <main class="content"><!-- App content --></main> 72 + ``` 73 + 74 + ### Step 4: CSS Styling 75 + 76 + ```css 77 + .titlebar { 78 + height: 30px; 79 + background: #329ea3; 80 + position: fixed; 81 + top: 0; 82 + left: 0; 83 + right: 0; 84 + display: grid; 85 + grid-template-columns: 1fr auto; 86 + user-select: none; 87 + } 88 + 89 + .titlebar-drag { 90 + display: flex; 91 + align-items: center; 92 + padding-left: 12px; 93 + } 94 + 95 + .titlebar-controls { display: flex; } 96 + 97 + .titlebar-controls button { 98 + width: 46px; 99 + height: 30px; 100 + border: none; 101 + background: transparent; 102 + color: white; 103 + cursor: pointer; 104 + } 105 + 106 + .titlebar-controls button:hover { background: rgba(255,255,255,0.1); } 107 + .titlebar-controls button#titlebar-close:hover { background: #e81123; } 108 + .content { margin-top: 30px; padding: 16px; } 109 + ``` 110 + 111 + ### Step 5: JavaScript Controls 112 + 113 + ```typescript 114 + import { getCurrentWindow } from '@tauri-apps/api/window'; 115 + 116 + const appWindow = getCurrentWindow(); 117 + 118 + document.getElementById('titlebar-minimize') 119 + ?.addEventListener('click', () => appWindow.minimize()); 120 + document.getElementById('titlebar-maximize') 121 + ?.addEventListener('click', () => appWindow.toggleMaximize()); 122 + document.getElementById('titlebar-close') 123 + ?.addEventListener('click', () => appWindow.close()); 124 + ``` 125 + 126 + ### Drag Region Behavior 127 + 128 + The `data-tauri-drag-region` attribute applies only to its element, not children. This preserves button interactivity. Add the attribute to each draggable child if needed. 129 + 130 + ### Manual Drag with Double-Click Maximize 131 + 132 + ```typescript 133 + document.getElementById('titlebar')?.addEventListener('mousedown', (e) => { 134 + if (e.buttons === 1 && e.target === e.currentTarget) { 135 + e.detail === 2 ? appWindow.toggleMaximize() : appWindow.startDragging(); 136 + } 137 + }); 138 + ``` 139 + 140 + ## macOS Transparent Titlebar 141 + 142 + ### Cargo.toml 143 + 144 + ```toml 145 + [target."cfg(target_os = \"macos\")".dependencies] 146 + cocoa = "0.26" 147 + ``` 148 + 149 + ### Rust Implementation 150 + 151 + ```rust 152 + use tauri::{TitleBarStyle, WebviewUrl, WebviewWindowBuilder}; 153 + 154 + pub fn run() { 155 + tauri::Builder::default() 156 + .setup(|app| { 157 + let win_builder = WebviewWindowBuilder::new(app, "main", WebviewUrl::default()) 158 + .title("Transparent Titlebar Window") 159 + .inner_size(800.0, 600.0); 160 + 161 + #[cfg(target_os = "macos")] 162 + let win_builder = win_builder.title_bar_style(TitleBarStyle::Transparent); 163 + 164 + let window = win_builder.build().unwrap(); 165 + 166 + #[cfg(target_os = "macos")] 167 + { 168 + use cocoa::appkit::{NSColor, NSWindow}; 169 + use cocoa::base::{id, nil}; 170 + let ns_window = window.ns_window().unwrap() as id; 171 + unsafe { 172 + let bg_color = NSColor::colorWithRed_green_blue_alpha_( 173 + nil, 50.0/255.0, 158.0/255.0, 163.5/255.0, 1.0 174 + ); 175 + ns_window.setBackgroundColor_(bg_color); 176 + } 177 + } 178 + Ok(()) 179 + }) 180 + .run(tauri::generate_context!()) 181 + .expect("error while running tauri application") 182 + } 183 + ``` 184 + 185 + **Note**: Custom titlebars on macOS lose native features like window snapping. Transparent titlebar preserves these. 186 + 187 + ## Window Menus 188 + 189 + ### Menu Item Types 190 + 191 + | Type | Description | 192 + |------|-------------| 193 + | Text | Basic labeled menu option | 194 + | Check | Toggleable entry with checked state | 195 + | Separator | Visual divider between sections | 196 + | Icon | Entry with custom icon (Tauri 2.8.0+) | 197 + 198 + ### Creating Menus (JavaScript/TypeScript) 199 + 200 + ```typescript 201 + import { Menu, MenuItem, Submenu, PredefinedMenuItem, CheckMenuItem } from '@tauri-apps/api/menu'; 202 + 203 + const fileSubmenu = await Submenu.new({ 204 + text: 'File', 205 + items: [ 206 + await MenuItem.new({ 207 + id: 'new', text: 'New', accelerator: 'CmdOrCtrl+N', 208 + action: () => console.log('New') 209 + }), 210 + await MenuItem.new({ 211 + id: 'open', text: 'Open', accelerator: 'CmdOrCtrl+O', 212 + action: () => console.log('Open') 213 + }), 214 + await MenuItem.new({ 215 + id: 'save', text: 'Save', accelerator: 'CmdOrCtrl+S', 216 + action: () => console.log('Save') 217 + }), 218 + { type: 'Separator' }, 219 + await MenuItem.new({ 220 + id: 'quit', text: 'Quit', accelerator: 'CmdOrCtrl+Q', 221 + action: () => console.log('Quit') 222 + }) 223 + ] 224 + }); 225 + 226 + const editSubmenu = await Submenu.new({ 227 + text: 'Edit', 228 + items: [ 229 + await PredefinedMenuItem.new({ item: 'Undo' }), 230 + await PredefinedMenuItem.new({ item: 'Redo' }), 231 + await PredefinedMenuItem.new({ item: 'Separator' }), 232 + await PredefinedMenuItem.new({ item: 'Cut' }), 233 + await PredefinedMenuItem.new({ item: 'Copy' }), 234 + await PredefinedMenuItem.new({ item: 'Paste' }) 235 + ] 236 + }); 237 + 238 + const viewSubmenu = await Submenu.new({ 239 + text: 'View', 240 + items: [ 241 + await CheckMenuItem.new({ 242 + id: 'sidebar', text: 'Show Sidebar', checked: true, 243 + action: async (item) => console.log('Sidebar:', await item.isChecked()) 244 + }) 245 + ] 246 + }); 247 + 248 + const menu = await Menu.new({ items: [fileSubmenu, editSubmenu, viewSubmenu] }); 249 + await menu.setAsAppMenu(); 250 + ``` 251 + 252 + ### Creating Menus (Rust) 253 + 254 + ```rust 255 + use tauri::menu::{MenuBuilder, SubmenuBuilder}; 256 + 257 + let file_menu = SubmenuBuilder::new(app, "File") 258 + .text("new", "New") 259 + .text("open", "Open") 260 + .text("save", "Save") 261 + .separator() 262 + .text("quit", "Quit") 263 + .build()?; 264 + 265 + let edit_menu = SubmenuBuilder::new(app, "Edit") 266 + .undo() 267 + .redo() 268 + .separator() 269 + .cut() 270 + .copy() 271 + .paste() 272 + .build()?; 273 + 274 + let menu = MenuBuilder::new(app) 275 + .items(&[&file_menu, &edit_menu]) 276 + .build()?; 277 + 278 + app.set_menu(menu)?; 279 + ``` 280 + 281 + **macOS Note**: All menu items must be grouped under submenus. Top-level items are ignored. 282 + 283 + ### Handling Menu Events (Rust) 284 + 285 + ```rust 286 + app.on_menu_event(|_app_handle, event| { 287 + match event.id().0.as_str() { 288 + "new" => println!("New file"), 289 + "open" => println!("Open file"), 290 + "save" => println!("Save file"), 291 + "quit" => std::process::exit(0), 292 + _ => {} 293 + } 294 + }); 295 + ``` 296 + 297 + ### Dynamic Menu Updates 298 + 299 + **JavaScript:** 300 + ```typescript 301 + const statusItem = await menu.get('status'); 302 + if (statusItem) await statusItem.setText('Status: Ready'); 303 + ``` 304 + 305 + **Rust:** 306 + ```rust 307 + menu.get("status").unwrap().as_menuitem_unchecked().set_text("Status: Ready")?; 308 + ``` 309 + 310 + ## Keyboard Shortcuts (Accelerators) 311 + 312 + | Shortcut | Accelerator String | 313 + |----------|-------------------| 314 + | Ctrl+S / Cmd+S | `CmdOrCtrl+S` | 315 + | Ctrl+Shift+S | `CmdOrCtrl+Shift+S` | 316 + | Alt+F4 | `Alt+F4` | 317 + | F11 | `F11` | 318 + 319 + ## Complete Example 320 + 321 + ### main.rs 322 + 323 + ```rust 324 + use tauri::menu::{MenuBuilder, SubmenuBuilder}; 325 + 326 + pub fn run() { 327 + tauri::Builder::default() 328 + .setup(|app| { 329 + let file_menu = SubmenuBuilder::new(app, "File") 330 + .text("new", "New") 331 + .text("open", "Open") 332 + .separator() 333 + .text("quit", "Quit") 334 + .build()?; 335 + 336 + let edit_menu = SubmenuBuilder::new(app, "Edit") 337 + .undo().redo().separator().cut().copy().paste() 338 + .build()?; 339 + 340 + let menu = MenuBuilder::new(app) 341 + .items(&[&file_menu, &edit_menu]) 342 + .build()?; 343 + app.set_menu(menu)?; 344 + Ok(()) 345 + }) 346 + .on_menu_event(|_app, event| { 347 + match event.id().0.as_str() { 348 + "quit" => std::process::exit(0), 349 + id => println!("Menu event: {}", id), 350 + } 351 + }) 352 + .run(tauri::generate_context!()) 353 + .expect("error while running tauri application") 354 + } 355 + ``` 356 + 357 + ### React Component 358 + 359 + ```tsx 360 + import { useEffect } from 'react'; 361 + import { getCurrentWindow } from '@tauri-apps/api/window'; 362 + 363 + function App() { 364 + useEffect(() => { 365 + const appWindow = getCurrentWindow(); 366 + const minimize = () => appWindow.minimize(); 367 + const maximize = () => appWindow.toggleMaximize(); 368 + const close = () => appWindow.close(); 369 + 370 + document.getElementById('titlebar-minimize')?.addEventListener('click', minimize); 371 + document.getElementById('titlebar-maximize')?.addEventListener('click', maximize); 372 + document.getElementById('titlebar-close')?.addEventListener('click', close); 373 + 374 + return () => { 375 + document.getElementById('titlebar-minimize')?.removeEventListener('click', minimize); 376 + document.getElementById('titlebar-maximize')?.removeEventListener('click', maximize); 377 + document.getElementById('titlebar-close')?.removeEventListener('click', close); 378 + }; 379 + }, []); 380 + 381 + return ( 382 + <> 383 + <div className="titlebar"> 384 + <div className="titlebar-drag" data-tauri-drag-region> 385 + <span>My Tauri App</span> 386 + </div> 387 + <div className="titlebar-controls"> 388 + <button id="titlebar-minimize">-</button> 389 + <button id="titlebar-maximize">[]</button> 390 + <button id="titlebar-close">x</button> 391 + </div> 392 + </div> 393 + <main className="content"> 394 + <h1>Welcome to Tauri</h1> 395 + </main> 396 + </> 397 + ); 398 + } 399 + 400 + export default App; 401 + ```
+477
.agents/skills/debugging-tauri-apps/SKILL.md
··· 1 + --- 2 + name: debugging-tauri-apps 3 + description: Helps users debug Tauri v2 applications across VS Code, RustRover, IntelliJ, and Neovim. Covers console debugging, WebView DevTools, Rust backtrace, CrabNebula DevTools integration, and IDE-specific launch configurations. 4 + --- 5 + 6 + # Debugging Tauri Applications 7 + 8 + This skill covers debugging Tauri v2 applications including console debugging, WebView inspection, IDE configurations, and CrabNebula DevTools. 9 + 10 + ## Development-Only Code 11 + 12 + Use conditional compilation to exclude debug code from production builds: 13 + 14 + ```rust 15 + // Only runs during `tauri dev` 16 + #[cfg(dev)] 17 + { 18 + // Development-only code 19 + } 20 + 21 + // Runtime check 22 + if cfg!(dev) { 23 + // tauri dev code 24 + } else { 25 + // tauri build code 26 + } 27 + 28 + // Programmatic check 29 + let is_dev: bool = tauri::is_dev(); 30 + 31 + // Debug builds and `tauri build --debug` 32 + #[cfg(debug_assertions)] 33 + { 34 + // Debug-only code 35 + } 36 + ``` 37 + 38 + ## Console Debugging 39 + 40 + ### Rust Print Macros 41 + 42 + Print messages to the terminal where `tauri dev` runs: 43 + 44 + ```rust 45 + println!("Message from Rust: {}", msg); 46 + dbg!(&variable); // Prints variable with file:line info 47 + ``` 48 + 49 + ### Enable Backtraces 50 + 51 + For detailed error information: 52 + 53 + ```bash 54 + # Linux/macOS 55 + RUST_BACKTRACE=1 tauri dev 56 + 57 + # Windows PowerShell 58 + $env:RUST_BACKTRACE=1 59 + tauri dev 60 + ``` 61 + 62 + ## WebView DevTools 63 + 64 + ### Opening DevTools 65 + 66 + - Right-click and select "Inspect Element" 67 + - `Ctrl + Shift + i` (Linux/Windows) 68 + - `Cmd + Option + i` (macOS) 69 + 70 + Platform-specific inspectors: WebKit (Linux), Safari (macOS), Edge DevTools (Windows). 71 + 72 + ### Programmatic Control 73 + 74 + ```rust 75 + tauri::Builder::default() 76 + .setup(|app| { 77 + #[cfg(debug_assertions)] 78 + { 79 + let window = app.get_webview_window("main").unwrap(); 80 + window.open_devtools(); 81 + // window.close_devtools(); 82 + } 83 + Ok(()) 84 + }) 85 + ``` 86 + 87 + ### Production DevTools 88 + 89 + Create a debug build for testing: 90 + 91 + ```bash 92 + tauri build --debug 93 + ``` 94 + 95 + To permanently enable devtools in production, add to `src-tauri/Cargo.toml`: 96 + 97 + ```toml 98 + [dependencies] 99 + tauri = { version = "...", features = ["...", "devtools"] } 100 + ``` 101 + 102 + > WARNING: Using the devtools feature enables private macOS APIs that prevent App Store acceptance. 103 + 104 + --- 105 + 106 + ## VS Code Setup 107 + 108 + ### Required Extensions 109 + 110 + | Extension | Platform | Purpose | 111 + |-----------|----------|---------| 112 + | [vscode-lldb](https://marketplace.visualstudio.com/items?itemName=vadimcn.vscode-lldb) | All | LLDB debugger | 113 + | [C/C++](https://marketplace.visualstudio.com/items?itemName=ms-vscode.cpptools) | Windows | Visual Studio debugger | 114 + 115 + ### launch.json Configuration 116 + 117 + Create `.vscode/launch.json`: 118 + 119 + ```json 120 + { 121 + "version": "0.2.0", 122 + "configurations": [ 123 + { 124 + "type": "lldb", 125 + "request": "launch", 126 + "name": "Tauri Development Debug", 127 + "cargo": { 128 + "args": [ 129 + "build", 130 + "--manifest-path=./src-tauri/Cargo.toml", 131 + "--no-default-features" 132 + ] 133 + }, 134 + "preLaunchTask": "ui:dev" 135 + }, 136 + { 137 + "type": "lldb", 138 + "request": "launch", 139 + "name": "Tauri Production Debug", 140 + "cargo": { 141 + "args": [ 142 + "build", 143 + "--release", 144 + "--manifest-path=./src-tauri/Cargo.toml" 145 + ] 146 + }, 147 + "preLaunchTask": "ui:build" 148 + } 149 + ] 150 + } 151 + ``` 152 + 153 + ### Windows Visual Studio Debugger 154 + 155 + For faster Windows debugging with better enum support: 156 + 157 + ```json 158 + { 159 + "version": "0.2.0", 160 + "configurations": [ 161 + { 162 + "name": "Launch App Debug", 163 + "type": "cppvsdbg", 164 + "request": "launch", 165 + "program": "${workspaceRoot}/src-tauri/target/debug/your-app-name.exe", 166 + "cwd": "${workspaceRoot}", 167 + "preLaunchTask": "dev" 168 + } 169 + ] 170 + } 171 + ``` 172 + 173 + ### tasks.json Configuration 174 + 175 + Create `.vscode/tasks.json`: 176 + 177 + ```json 178 + { 179 + "version": "2.0.0", 180 + "tasks": [ 181 + { 182 + "label": "ui:dev", 183 + "type": "shell", 184 + "isBackground": true, 185 + "command": "npm", 186 + "args": ["run", "dev"] 187 + }, 188 + { 189 + "label": "ui:build", 190 + "type": "shell", 191 + "command": "npm", 192 + "args": ["run", "build"] 193 + }, 194 + { 195 + "label": "build:debug", 196 + "type": "cargo", 197 + "command": "build", 198 + "options": { 199 + "cwd": "${workspaceRoot}/src-tauri" 200 + } 201 + }, 202 + { 203 + "label": "dev", 204 + "dependsOn": ["build:debug", "ui:dev"], 205 + "group": { 206 + "kind": "build" 207 + } 208 + } 209 + ] 210 + } 211 + ``` 212 + 213 + ### Debugging Workflow 214 + 215 + 1. Set breakpoints by clicking the line number margin in Rust files 216 + 2. Press `F5` or select debug configuration from Run menu 217 + 3. The `preLaunchTask` runs the dev server automatically 218 + 4. Debugger attaches and stops at breakpoints 219 + 220 + > NOTE: LLDB bypasses the Tauri CLI, so `beforeDevCommand` and `beforeBuildCommand` must be configured as tasks. 221 + 222 + --- 223 + 224 + ## RustRover / IntelliJ Setup 225 + 226 + ### Project Configuration 227 + 228 + If your project lacks a top-level `Cargo.toml`, create a workspace file: 229 + 230 + ```toml 231 + [workspace] 232 + members = ["src-tauri"] 233 + ``` 234 + 235 + Or attach `src-tauri/Cargo.toml` via the Cargo tool window. 236 + 237 + ### Run Configurations 238 + 239 + Create two configurations in **Run | Edit Configurations**: 240 + 241 + #### 1. Tauri App Configuration (Cargo) 242 + 243 + - **Command**: `run` 244 + - **Additional arguments**: `--no-default-features` 245 + 246 + The `--no-default-features` flag is critical - it tells Tauri to load assets from the dev server instead of bundling them. 247 + 248 + #### 2. Development Server Configuration 249 + 250 + For Node-based projects: 251 + - Create an npm Run Configuration 252 + - Set package manager (npm/pnpm/yarn) 253 + - Set script to `dev` 254 + 255 + For Rust WASM (Trunk): 256 + - Create a Shell Script configuration 257 + - Command: `trunk serve` 258 + 259 + ### Debugging Workflow 260 + 261 + 1. Start the development server configuration first 262 + 2. Click Debug on the Tauri App configuration 263 + 3. RustRover halts at Rust breakpoints automatically 264 + 4. Inspect variables and step through code 265 + 266 + --- 267 + 268 + ## Neovim Setup 269 + 270 + ### Required Plugins 271 + 272 + - **nvim-dap** - Debug Adapter Protocol client 273 + - **nvim-dap-ui** - Debugger UI 274 + - **nvim-nio** - Async dependency for nvim-dap-ui 275 + - **overseer.nvim** (recommended) - Task management 276 + 277 + ### Prerequisites 278 + 279 + Download `codelldb` from [GitHub releases](https://github.com/vadimcn/codelldb/releases) and note the installation path. 280 + 281 + ### DAP Configuration 282 + 283 + Add to your Neovim config (init.lua or equivalent): 284 + 285 + ```lua 286 + local dap = require("dap") 287 + 288 + -- Configure codelldb adapter 289 + dap.adapters.codelldb = { 290 + type = 'server', 291 + port = "${port}", 292 + executable = { 293 + command = '/path/to/codelldb/adapter/codelldb', 294 + args = {"--port", "${port}"}, 295 + } 296 + } 297 + 298 + -- Launch configuration for Rust/Tauri 299 + dap.configurations.rust = { 300 + { 301 + name = "Launch Tauri App", 302 + type = "codelldb", 303 + request = "launch", 304 + program = function() 305 + return vim.fn.input('Path to executable: ', vim.fn.getcwd() .. '/target/debug/', 'file') 306 + end, 307 + cwd = '${workspaceFolder}', 308 + stopOnEntry = false 309 + }, 310 + } 311 + ``` 312 + 313 + ### UI Integration 314 + 315 + ```lua 316 + local dapui = require("dapui") 317 + dapui.setup() 318 + 319 + -- Auto-open/close UI 320 + dap.listeners.before.attach.dapui_config = function() 321 + dapui.open() 322 + end 323 + dap.listeners.before.launch.dapui_config = function() 324 + dapui.open() 325 + end 326 + dap.listeners.before.event_terminated.dapui_config = function() 327 + dapui.close() 328 + end 329 + dap.listeners.before.event_exited.dapui_config = function() 330 + dapui.close() 331 + end 332 + ``` 333 + 334 + ### Visual Indicators 335 + 336 + ```lua 337 + vim.fn.sign_define('DapBreakpoint', { 338 + text = 'B', 339 + texthl = 'DapBreakpoint', 340 + linehl = '', 341 + numhl = '' 342 + }) 343 + vim.fn.sign_define('DapStopped', { 344 + text = '>', 345 + texthl = 'DapStopped', 346 + linehl = 'DapStopped', 347 + numhl = '' 348 + }) 349 + ``` 350 + 351 + ### Keybindings 352 + 353 + ```lua 354 + vim.keymap.set('n', '<F5>', function() dap.continue() end) 355 + vim.keymap.set('n', '<F6>', function() dap.disconnect({ terminateDebuggee = true }) end) 356 + vim.keymap.set('n', '<F10>', function() dap.step_over() end) 357 + vim.keymap.set('n', '<F11>', function() dap.step_into() end) 358 + vim.keymap.set('n', '<F12>', function() dap.step_out() end) 359 + vim.keymap.set('n', '<Leader>b', function() dap.toggle_breakpoint() end) 360 + vim.keymap.set('n', '<Leader>o', function() overseer.toggle() end) 361 + vim.keymap.set('n', '<Leader>R', function() overseer.run_template() end) 362 + ``` 363 + 364 + ### Development Server Task 365 + 366 + Create `.vscode/tasks.json` for overseer.nvim compatibility: 367 + 368 + ```json 369 + { 370 + "version": "2.0.0", 371 + "tasks": [ 372 + { 373 + "type": "process", 374 + "label": "dev server", 375 + "command": "npm", 376 + "args": ["run", "dev"], 377 + "isBackground": true, 378 + "presentation": { 379 + "revealProblems": "onProblem" 380 + }, 381 + "problemMatcher": { 382 + "pattern": { 383 + "regexp": "^error:.*", 384 + "file": 1, 385 + "line": 2 386 + }, 387 + "background": { 388 + "activeOnStart": false, 389 + "beginsPattern": ".*Rebuilding.*", 390 + "endsPattern": ".*listening.*" 391 + } 392 + } 393 + } 394 + ] 395 + } 396 + ``` 397 + 398 + > NOTE: The development server does not start automatically when bypassing Tauri CLI. Use overseer.nvim or start it manually. 399 + 400 + --- 401 + 402 + ## CrabNebula DevTools 403 + 404 + CrabNebula DevTools provides real-time application instrumentation including log inspection, performance monitoring, and Tauri event/command analysis. 405 + 406 + ### Features 407 + 408 + - Inspect log events (including dependency logs) 409 + - Monitor command execution performance 410 + - Analyze Tauri events with payloads and responses 411 + - Real-time visualization 412 + 413 + ### Installation 414 + 415 + ```bash 416 + cargo add tauri-plugin-devtools@2.0.0 417 + ``` 418 + 419 + ### Setup 420 + 421 + Initialize DevTools as early as possible in `src-tauri/src/main.rs`: 422 + 423 + ```rust 424 + fn main() { 425 + // Initialize DevTools only in debug builds 426 + #[cfg(debug_assertions)] 427 + let devtools = tauri_plugin_devtools::init(); 428 + 429 + let mut builder = tauri::Builder::default(); 430 + 431 + #[cfg(debug_assertions)] 432 + { 433 + builder = builder.plugin(devtools); 434 + } 435 + 436 + builder 437 + .run(tauri::generate_context!()) 438 + .expect("error while running tauri application") 439 + } 440 + ``` 441 + 442 + ### Usage 443 + 444 + When running `tauri dev`, DevTools automatically opens a web-based interface showing: 445 + - Application logs with filtering 446 + - IPC command calls with timing 447 + - Event payloads and responses 448 + - Performance spans 449 + 450 + For full documentation, see [CrabNebula DevTools docs](https://docs.crabnebula.dev/devtools/get-started/). 451 + 452 + --- 453 + 454 + ## Quick Reference 455 + 456 + | Task | Command/Action | 457 + |------|----------------| 458 + | Enable backtraces | `RUST_BACKTRACE=1 tauri dev` | 459 + | Open WebView DevTools | `Ctrl+Shift+i` / `Cmd+Option+i` | 460 + | Debug build | `tauri build --debug` | 461 + | Add DevTools plugin | `cargo add tauri-plugin-devtools@2.0.0` | 462 + 463 + ### IDE Comparison 464 + 465 + | Feature | VS Code | RustRover | Neovim | 466 + |---------|---------|-----------|--------| 467 + | Extension/Plugin | vscode-lldb | Built-in | nvim-dap + codelldb | 468 + | Windows Alt | cppvsdbg | Built-in | codelldb | 469 + | Task Runner | tasks.json | Run configs | overseer.nvim | 470 + | Setup Complexity | Medium | Low | High | 471 + 472 + ### Common Issues 473 + 474 + 1. **Breakpoints not hit**: Ensure `--no-default-features` is set when building 475 + 2. **Dev server not starting**: Configure `preLaunchTask` or start manually 476 + 3. **App not loading frontend**: Dev server must be running before Tauri app starts 477 + 4. **Windows enum display issues**: Use cppvsdbg instead of LLDB
+420
.agents/skills/developing-tauri-plugins/SKILL.md
··· 1 + --- 2 + name: developing-tauri-plugins 3 + description: Guides the user through Tauri plugin development, including creating plugin extensions, configuring permissions, and building mobile plugins for iOS and Android platforms. 4 + --- 5 + 6 + # Developing Tauri Plugins 7 + 8 + Tauri plugins extend application functionality through modular Rust crates with optional JavaScript bindings and native mobile implementations. 9 + 10 + ## Plugin Architecture 11 + 12 + A complete plugin includes: 13 + - **Rust crate** (`tauri-plugin-{name}`) - Core logic 14 + - **JavaScript bindings** (`@scope/plugin-{name}`) - NPM package 15 + - **Android library** (Kotlin) - Optional 16 + - **iOS package** (Swift) - Optional 17 + 18 + ## Creating a Plugin 19 + 20 + ```bash 21 + npx @tauri-apps/cli plugin new my-plugin # Basic 22 + npx @tauri-apps/cli plugin new my-plugin --android --ios # With mobile 23 + npx @tauri-apps/cli plugin android add # Add to existing 24 + npx @tauri-apps/cli plugin ios add 25 + ``` 26 + 27 + ### Project Structure 28 + 29 + ``` 30 + tauri-plugin-my-plugin/ 31 + ├── src/ 32 + │ ├── lib.rs, commands.rs, desktop.rs, mobile.rs, error.rs 33 + ├── permissions/ # Permission TOML files 34 + ├── guest-js/index.ts # TypeScript API 35 + ├── android/, ios/ # Native mobile code 36 + ├── build.rs, Cargo.toml 37 + ``` 38 + 39 + ## Plugin Implementation 40 + 41 + ### Main Plugin File (lib.rs) 42 + 43 + ```rust 44 + use tauri::{plugin::{Builder, TauriPlugin}, Manager, Runtime}; 45 + mod commands; 46 + mod error; 47 + pub use error::{Error, Result}; 48 + 49 + #[cfg(desktop)] mod desktop; 50 + #[cfg(mobile)] mod mobile; 51 + #[cfg(desktop)] use desktop::MyPlugin; 52 + #[cfg(mobile)] use mobile::MyPlugin; 53 + 54 + pub struct MyPluginState<R: Runtime>(pub MyPlugin<R>); 55 + 56 + pub fn init<R: Runtime>() -> TauriPlugin<R> { 57 + Builder::new("my-plugin") 58 + .invoke_handler(tauri::generate_handler![commands::do_something]) 59 + .setup(|app, api| { 60 + app.manage(MyPluginState(MyPlugin::new(app, api)?)); 61 + Ok(()) 62 + }) 63 + .build() 64 + } 65 + ``` 66 + 67 + ### Plugin with Configuration 68 + 69 + ```rust 70 + use serde::Deserialize; 71 + 72 + #[derive(Debug, Deserialize)] 73 + pub struct Config { pub timeout: Option<u64>, pub enabled: bool } 74 + 75 + pub fn init<R: Runtime>() -> TauriPlugin<R, Config> { 76 + Builder::<R, Config>::new("my-plugin") 77 + .setup(|app, api| { 78 + let config = api.config(); 79 + Ok(()) 80 + }) 81 + .build() 82 + } 83 + ``` 84 + 85 + ### Commands (commands.rs) 86 + 87 + ```rust 88 + use tauri::{command, ipc::Channel, Runtime, State}; 89 + use crate::{MyPluginState, Result}; 90 + 91 + #[command] 92 + pub async fn do_something<R: Runtime>( 93 + state: State<'_, MyPluginState<R>>, input: String, 94 + ) -> Result<String> { 95 + state.0.do_something(input).await 96 + } 97 + 98 + #[command] 99 + pub async fn upload<R: Runtime>(path: String, on_progress: Channel<u32>) -> Result<()> { 100 + for i in 0..=100 { on_progress.send(i)?; } 101 + Ok(()) 102 + } 103 + ``` 104 + 105 + ### Desktop Implementation (desktop.rs) 106 + 107 + ```rust 108 + use tauri::{AppHandle, Runtime}; 109 + use crate::Result; 110 + 111 + pub struct MyPlugin<R: Runtime> { app: AppHandle<R> } 112 + 113 + impl<R: Runtime> MyPlugin<R> { 114 + pub fn new(app: &AppHandle<R>, _api: tauri::plugin::PluginApi<R, ()>) -> Result<Self> { 115 + Ok(Self { app: app.clone() }) 116 + } 117 + pub async fn do_something(&self, input: String) -> Result<String> { 118 + Ok(format!("Desktop: {}", input)) 119 + } 120 + } 121 + ``` 122 + 123 + ### Mobile Implementation (mobile.rs) 124 + 125 + ```rust 126 + use tauri::{AppHandle, Runtime}; 127 + use serde::{Deserialize, Serialize}; 128 + use crate::Result; 129 + 130 + #[derive(Serialize)] struct MobileRequest { value: String } 131 + #[derive(Deserialize)] struct MobileResponse { result: String } 132 + 133 + pub struct MyPlugin<R: Runtime> { app: AppHandle<R> } 134 + 135 + impl<R: Runtime> MyPlugin<R> { 136 + pub fn new(app: &AppHandle<R>, _api: tauri::plugin::PluginApi<R, ()>) -> Result<Self> { 137 + Ok(Self { app: app.clone() }) 138 + } 139 + pub async fn do_something(&self, input: String) -> Result<String> { 140 + let response: MobileResponse = self.app 141 + .run_mobile_plugin("doSomething", MobileRequest { value: input }) 142 + .map_err(|e| crate::Error::Mobile(e.to_string()))?; 143 + Ok(response.result) 144 + } 145 + } 146 + ``` 147 + 148 + ### Error Handling (error.rs) 149 + 150 + ```rust 151 + use serde::{Serialize, Serializer}; 152 + 153 + #[derive(Debug, thiserror::Error)] 154 + pub enum Error { 155 + #[error("IO error: {0}")] Io(#[from] std::io::Error), 156 + #[error("Mobile error: {0}")] Mobile(String), 157 + } 158 + 159 + impl Serialize for Error { 160 + fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error> 161 + where S: Serializer { serializer.serialize_str(self.to_string().as_str()) } 162 + } 163 + pub type Result<T> = std::result::Result<T, Error>; 164 + ``` 165 + 166 + ## Lifecycle Events 167 + 168 + ```rust 169 + Builder::new("my-plugin") 170 + .setup(|app, api| { Ok(()) }) // Plugin init 171 + .on_navigation(|window, url| url.scheme() != "dangerous") // Block nav 172 + .on_webview_ready(|window| {}) // Window created 173 + .on_event(|app, event| { match event { tauri::RunEvent::Exit => {} _ => {} }}) 174 + .on_drop(|app| {}) // Cleanup 175 + .build() 176 + ``` 177 + 178 + ## JavaScript Bindings (guest-js/index.ts) 179 + 180 + ```typescript 181 + import { invoke, Channel } from '@tauri-apps/api/core'; 182 + 183 + export async function doSomething(input: string): Promise<string> { 184 + return invoke('plugin:my-plugin|do_something', { input }); 185 + } 186 + 187 + export async function upload(path: string, onProgress: (p: number) => void): Promise<void> { 188 + const channel = new Channel<number>(); 189 + channel.onmessage = onProgress; 190 + return invoke('plugin:my-plugin|upload', { path, onProgress: channel }); 191 + } 192 + ``` 193 + 194 + ## Plugin Permissions 195 + 196 + ### Permission File (permissions/default.toml) 197 + 198 + ```toml 199 + [default] 200 + description = "Default permissions" 201 + permissions = ["allow-do-something"] 202 + 203 + [[permission]] 204 + identifier = "allow-do-something" 205 + description = "Allows do_something command" 206 + commands.allow = ["do_something"] 207 + 208 + [[permission]] 209 + identifier = "allow-upload" 210 + description = "Allows upload command" 211 + commands.allow = ["upload"] 212 + 213 + [[set]] 214 + identifier = "full-access" 215 + description = "Full plugin access" 216 + permissions = ["allow-do-something", "allow-upload"] 217 + ``` 218 + 219 + ### Build Script (build.rs) 220 + 221 + ```rust 222 + const COMMANDS: &[&str] = &["do_something", "upload"]; 223 + fn main() { tauri_plugin::Builder::new(COMMANDS).build(); } 224 + ``` 225 + 226 + ### Scoped Permissions 227 + 228 + ```rust 229 + use tauri::ipc::CommandScope; 230 + use serde::Deserialize; 231 + 232 + #[derive(Debug, Deserialize)] 233 + pub struct PathScope { pub path: String } 234 + 235 + #[command] 236 + pub async fn read_file(path: String, scope: CommandScope<'_, PathScope>) -> Result<String> { 237 + let allowed = scope.allows().iter().any(|s| path.starts_with(&s.path)); 238 + let denied = scope.denies().iter().any(|s| path.starts_with(&s.path)); 239 + if denied || !allowed { return Err(Error::PermissionDenied); } 240 + // Read file... 241 + } 242 + ``` 243 + 244 + ## Android Plugin (Kotlin) 245 + 246 + ```kotlin 247 + package com.example.myplugin 248 + 249 + import android.app.Activity 250 + import app.tauri.annotation.Command 251 + import app.tauri.annotation.InvokeArg 252 + import app.tauri.annotation.TauriPlugin 253 + import app.tauri.plugin.Invoke 254 + import app.tauri.plugin.JSObject 255 + import app.tauri.plugin.Plugin 256 + import kotlinx.coroutines.CoroutineScope 257 + import kotlinx.coroutines.Dispatchers 258 + import kotlinx.coroutines.launch 259 + 260 + @InvokeArg 261 + class DoSomethingArgs { 262 + lateinit var value: String // Required 263 + var optional: String? = null // Optional 264 + var withDefault: Int = 42 // Default value 265 + } 266 + 267 + @TauriPlugin 268 + class MyPlugin(private val activity: Activity) : Plugin(activity) { 269 + @Command 270 + fun doSomething(invoke: Invoke) { 271 + val args = invoke.parseArgs(DoSomethingArgs::class.java) 272 + CoroutineScope(Dispatchers.IO).launch { // Use IO for blocking ops 273 + try { 274 + invoke.resolve(JSObject().apply { put("result", "Android: ${args.value}") }) 275 + } catch (e: Exception) { invoke.reject(e.message) } 276 + } 277 + } 278 + } 279 + ``` 280 + 281 + ### Android Permissions 282 + 283 + ```kotlin 284 + @TauriPlugin(permissions = [ 285 + Permission(strings = [android.Manifest.permission.CAMERA], alias = "camera") 286 + ]) 287 + class MyPlugin(private val activity: Activity) : Plugin(activity) { 288 + @Command override fun checkPermissions(invoke: Invoke) { super.checkPermissions(invoke) } 289 + @Command override fun requestPermissions(invoke: Invoke) { super.requestPermissions(invoke) } 290 + } 291 + ``` 292 + 293 + ### Android Events & JNI 294 + 295 + ```kotlin 296 + // Emit event 297 + trigger("dataReceived", JSObject().apply { put("data", "value") }) 298 + 299 + // Lifecycle 300 + override fun onNewIntent(intent: Intent) { 301 + trigger("newIntent", JSObject().apply { put("action", intent.action) }) 302 + } 303 + 304 + // Call Rust via JNI 305 + companion object { init { System.loadLibrary("my_plugin") } } 306 + external fun processData(input: String): String // Java_com_example_myplugin_MyPlugin_processData 307 + ``` 308 + 309 + ## iOS Plugin (Swift) 310 + 311 + ```swift 312 + import SwiftRs 313 + import Tauri 314 + import UIKit 315 + 316 + class DoSomethingArgs: Decodable { 317 + let value: String // Required 318 + var optional: String? // Optional 319 + } 320 + 321 + class MyPlugin: Plugin { 322 + @objc public func doSomething(_ invoke: Invoke) throws { 323 + let args = try invoke.parseArgs(DoSomethingArgs.self) 324 + invoke.resolve(["result": "iOS: \(args.value)"]) 325 + } 326 + } 327 + 328 + @_cdecl("init_plugin_my_plugin") 329 + func initPlugin() -> Plugin { return MyPlugin() } 330 + ``` 331 + 332 + ### iOS Permissions 333 + 334 + ```swift 335 + import AVFoundation 336 + 337 + class MyPlugin: Plugin { 338 + @objc override func checkPermissions(_ invoke: Invoke) { 339 + var result: [String: String] = [:] 340 + switch AVCaptureDevice.authorizationStatus(for: .video) { 341 + case .authorized: result["camera"] = "granted" 342 + case .denied, .restricted: result["camera"] = "denied" 343 + default: result["camera"] = "prompt" 344 + } 345 + invoke.resolve(result) 346 + } 347 + 348 + @objc override func requestPermissions(_ invoke: Invoke) { 349 + AVCaptureDevice.requestAccess(for: .video) { _ in self.checkPermissions(invoke) } 350 + } 351 + } 352 + ``` 353 + 354 + ### iOS Events & FFI 355 + 356 + ```swift 357 + // Emit event 358 + trigger("dataReceived", data: ["data": "value"]) 359 + 360 + // Call Rust via FFI 361 + @_silgen_name("process_data_ffi") 362 + private static func processDataFFI(_ input: UnsafePointer<CChar>) -> UnsafeMutablePointer<CChar>? 363 + 364 + @objc public func hybrid(_ invoke: Invoke) throws { 365 + let args = try invoke.parseArgs(DoSomethingArgs.self) 366 + guard let ptr = MyPlugin.processDataFFI(args.value) else { invoke.reject("FFI failed"); return } 367 + invoke.resolve(["result": String(cString: ptr)]) 368 + ptr.deallocate() 369 + } 370 + ``` 371 + 372 + ## Using the Plugin 373 + 374 + ### Register (src-tauri/src/lib.rs) 375 + 376 + ```rust 377 + pub fn run() { 378 + tauri::Builder::default() 379 + .plugin(tauri_plugin_my_plugin::init()) 380 + .run(tauri::generate_context!()) 381 + .expect("error running application"); 382 + } 383 + ``` 384 + 385 + ### Configure (tauri.conf.json) 386 + 387 + ```json 388 + { "plugins": { "my-plugin": { "timeout": 60, "enabled": true } } } 389 + ``` 390 + 391 + ### Permissions (capabilities/default.json) 392 + 393 + ```json 394 + { "identifier": "default", "windows": ["main"], "permissions": ["my-plugin:default"] } 395 + ``` 396 + 397 + ### Frontend Usage 398 + 399 + ```typescript 400 + import { doSomething, upload } from '@myorg/plugin-my-plugin'; 401 + const result = await doSomething('hello'); 402 + await upload('/path/to/file', (p) => console.log(`${p}%`)); 403 + ``` 404 + 405 + ## Best Practices 406 + 407 + - Separate platform code in `desktop.rs` and `mobile.rs` 408 + - Use `thiserror` for structured error handling 409 + - Use async for I/O operations; request only necessary permissions 410 + - Android: Commands run on main thread - use coroutines for blocking work 411 + - iOS: Clean up FFI resources properly; use `invoke.reject()`/`invoke.resolve()` 412 + 413 + ## Android 16KB Page Size 414 + 415 + For NDK < 28, add to `.cargo/config.toml`: 416 + 417 + ```toml 418 + [target.aarch64-linux-android] 419 + rustflags = ["-C", "link-arg=-Wl,-z,max-page-size=16384"] 420 + ```
+449
.agents/skills/distributing-tauri-for-macos/SKILL.md
··· 1 + --- 2 + name: distributing-tauri-for-macos 3 + description: Guides users through distributing Tauri applications on macOS, including creating DMG installers, configuring app bundles, setting up entitlements, and customizing Info.plist files for proper macOS distribution. 4 + --- 5 + 6 + # Tauri macOS Distribution 7 + 8 + This skill covers distributing Tauri v2 applications on macOS, including DMG installers and application bundle configuration. 9 + 10 + ## Overview 11 + 12 + macOS distribution for Tauri apps involves two primary formats: 13 + 14 + 1. **Application Bundle (.app)** - The executable directory containing all app components 15 + 2. **DMG Installer (.dmg)** - A disk image that wraps the app bundle for easy drag-and-drop installation 16 + 17 + ## Building for macOS 18 + 19 + ### Build Commands 20 + 21 + Generate specific bundle types using the Tauri CLI: 22 + 23 + ```bash 24 + # Build app bundle only 25 + npm run tauri build -- --bundles app 26 + yarn tauri build --bundles app 27 + pnpm tauri build --bundles app 28 + cargo tauri build --bundles app 29 + 30 + # Build DMG installer only 31 + npm run tauri build -- --bundles dmg 32 + yarn tauri build --bundles dmg 33 + pnpm tauri build --bundles dmg 34 + cargo tauri build --bundles dmg 35 + 36 + # Build both 37 + npm run tauri build -- --bundles app,dmg 38 + ``` 39 + 40 + ## Application Bundle Structure 41 + 42 + The `.app` directory follows macOS conventions: 43 + 44 + ``` 45 + <productName>.app/ 46 + Contents/ 47 + Info.plist # App metadata and configuration 48 + MacOS/ 49 + <app-name> # Main executable 50 + Resources/ 51 + icon.icns # App icon 52 + [bundled resources] # Additional resources 53 + _CodeSignature/ # Code signature files 54 + Frameworks/ # Bundled frameworks 55 + PlugIns/ # App plugins 56 + SharedSupport/ # Support files 57 + ``` 58 + 59 + ## DMG Installer Configuration 60 + 61 + Configure DMG appearance in `tauri.conf.json`: 62 + 63 + ### Complete DMG Configuration Example 64 + 65 + ```json 66 + { 67 + "bundle": { 68 + "macOS": { 69 + "dmg": { 70 + "background": "./images/dmg-background.png", 71 + "windowSize": { 72 + "width": 660, 73 + "height": 400 74 + }, 75 + "windowPosition": { 76 + "x": 400, 77 + "y": 400 78 + }, 79 + "appPosition": { 80 + "x": 180, 81 + "y": 220 82 + }, 83 + "applicationFolderPosition": { 84 + "x": 480, 85 + "y": 220 86 + } 87 + } 88 + } 89 + } 90 + } 91 + ``` 92 + 93 + ### DMG Configuration Options 94 + 95 + | Option | Type | Default | Description | 96 + |--------|------|---------|-------------| 97 + | `background` | string | - | Path to background image relative to `src-tauri` | 98 + | `windowSize.width` | number | 660 | DMG window width in pixels | 99 + | `windowSize.height` | number | 400 | DMG window height in pixels | 100 + | `windowPosition.x` | number | - | Initial window X position on screen | 101 + | `windowPosition.y` | number | - | Initial window Y position on screen | 102 + | `appPosition.x` | number | 180 | App icon X position in window | 103 + | `appPosition.y` | number | 220 | App icon Y position in window | 104 + | `applicationFolderPosition.x` | number | 480 | Applications folder X position | 105 + | `applicationFolderPosition.y` | number | 480 | Applications folder Y position | 106 + 107 + **Note:** Icon sizes and positions may not apply correctly when building on CI/CD platforms due to a known issue with headless environments. 108 + 109 + ## Info.plist Customization 110 + 111 + ### Creating a Custom Info.plist 112 + 113 + Create `src-tauri/Info.plist` to extend the default configuration. The Tauri CLI automatically merges this with generated values. 114 + 115 + ```xml 116 + <?xml version="1.0" encoding="UTF-8"?> 117 + <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" 118 + "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> 119 + <plist version="1.0"> 120 + <dict> 121 + <!-- Privacy Usage Descriptions --> 122 + <key>NSCameraUsageDescription</key> 123 + <string>This app requires camera access for video calls</string> 124 + 125 + <key>NSMicrophoneUsageDescription</key> 126 + <string>This app requires microphone access for audio recording</string> 127 + 128 + <key>NSLocationUsageDescription</key> 129 + <string>This app requires location access for mapping features</string> 130 + 131 + <key>NSPhotoLibraryUsageDescription</key> 132 + <string>This app requires photo library access to import images</string> 133 + 134 + <!-- Document Types --> 135 + <key>CFBundleDocumentTypes</key> 136 + <array> 137 + <dict> 138 + <key>CFBundleTypeName</key> 139 + <string>My Document</string> 140 + <key>CFBundleTypeExtensions</key> 141 + <array> 142 + <string>mydoc</string> 143 + </array> 144 + <key>CFBundleTypeRole</key> 145 + <string>Editor</string> 146 + </dict> 147 + </array> 148 + 149 + <!-- URL Schemes --> 150 + <key>CFBundleURLTypes</key> 151 + <array> 152 + <dict> 153 + <key>CFBundleURLName</key> 154 + <string>com.example.myapp</string> 155 + <key>CFBundleURLSchemes</key> 156 + <array> 157 + <string>myapp</string> 158 + </array> 159 + </dict> 160 + </array> 161 + </dict> 162 + </plist> 163 + ``` 164 + 165 + ### Common Info.plist Keys 166 + 167 + | Key | Description | 168 + |-----|-------------| 169 + | `NSCameraUsageDescription` | Camera access explanation | 170 + | `NSMicrophoneUsageDescription` | Microphone access explanation | 171 + | `NSLocationUsageDescription` | Location access explanation | 172 + | `NSPhotoLibraryUsageDescription` | Photo library access explanation | 173 + | `NSAppleEventsUsageDescription` | AppleScript/automation access | 174 + | `CFBundleDocumentTypes` | Supported document types | 175 + | `CFBundleURLTypes` | Custom URL schemes | 176 + | `LSMinimumSystemVersion` | Minimum macOS version (prefer tauri.conf.json) | 177 + 178 + ### Info.plist Localization 179 + 180 + Support multiple languages with localized strings: 181 + 182 + **Directory structure:** 183 + ``` 184 + src-tauri/ 185 + infoplist/ 186 + en.lproj/ 187 + InfoPlist.strings 188 + de.lproj/ 189 + InfoPlist.strings 190 + fr.lproj/ 191 + InfoPlist.strings 192 + es.lproj/ 193 + InfoPlist.strings 194 + ``` 195 + 196 + **Example `InfoPlist.strings` (German):** 197 + ``` 198 + "NSCameraUsageDescription" = "Diese App benötigt Kamerazugriff für Videoanrufe"; 199 + "NSMicrophoneUsageDescription" = "Diese App benötigt Mikrofonzugriff für Audioaufnahmen"; 200 + ``` 201 + 202 + **Configure in `tauri.conf.json`:** 203 + ```json 204 + { 205 + "bundle": { 206 + "resources": { 207 + "infoplist/**": "./" 208 + } 209 + } 210 + } 211 + ``` 212 + 213 + ## Entitlements Configuration 214 + 215 + Entitlements grant special capabilities when your app is code-signed. 216 + 217 + ### Creating Entitlements.plist 218 + 219 + Create `src-tauri/Entitlements.plist`: 220 + 221 + ```xml 222 + <?xml version="1.0" encoding="UTF-8"?> 223 + <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" 224 + "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> 225 + <plist version="1.0"> 226 + <dict> 227 + <!-- App Sandbox (required for App Store) --> 228 + <key>com.apple.security.app-sandbox</key> 229 + <true/> 230 + 231 + <!-- Network Access --> 232 + <key>com.apple.security.network.client</key> 233 + <true/> 234 + <key>com.apple.security.network.server</key> 235 + <true/> 236 + 237 + <!-- File Access --> 238 + <key>com.apple.security.files.user-selected.read-write</key> 239 + <true/> 240 + <key>com.apple.security.files.downloads.read-write</key> 241 + <true/> 242 + 243 + <!-- Hardware Access --> 244 + <key>com.apple.security.device.camera</key> 245 + <true/> 246 + <key>com.apple.security.device.microphone</key> 247 + <true/> 248 + 249 + <!-- Hardened Runtime --> 250 + <key>com.apple.security.cs.allow-jit</key> 251 + <true/> 252 + <key>com.apple.security.cs.allow-unsigned-executable-memory</key> 253 + <true/> 254 + </dict> 255 + </plist> 256 + ``` 257 + 258 + ### Configure Entitlements in tauri.conf.json 259 + 260 + ```json 261 + { 262 + "bundle": { 263 + "macOS": { 264 + "entitlements": "./Entitlements.plist" 265 + } 266 + } 267 + } 268 + ``` 269 + 270 + ### Common Entitlements Reference 271 + 272 + **Sandbox Entitlements:** 273 + | Entitlement | Description | 274 + |-------------|-------------| 275 + | `com.apple.security.app-sandbox` | Enable app sandbox (required for App Store) | 276 + | `com.apple.security.network.client` | Outbound network connections | 277 + | `com.apple.security.network.server` | Incoming network connections | 278 + | `com.apple.security.files.user-selected.read-write` | Access user-selected files | 279 + | `com.apple.security.files.downloads.read-write` | Access Downloads folder | 280 + 281 + **Hardware Entitlements:** 282 + | Entitlement | Description | 283 + |-------------|-------------| 284 + | `com.apple.security.device.camera` | Camera access | 285 + | `com.apple.security.device.microphone` | Microphone access | 286 + | `com.apple.security.device.usb` | USB device access | 287 + | `com.apple.security.device.bluetooth` | Bluetooth access | 288 + 289 + **Hardened Runtime Entitlements:** 290 + | Entitlement | Description | 291 + |-------------|-------------| 292 + | `com.apple.security.cs.allow-jit` | Allow JIT compilation | 293 + | `com.apple.security.cs.allow-unsigned-executable-memory` | Allow unsigned executable memory | 294 + | `com.apple.security.cs.disable-library-validation` | Load arbitrary plugins | 295 + 296 + ## macOS Bundle Configuration 297 + 298 + ### Complete macOS Configuration Example 299 + 300 + ```json 301 + { 302 + "bundle": { 303 + "icon": ["icons/icon.icns"], 304 + "macOS": { 305 + "minimumSystemVersion": "10.13", 306 + "entitlements": "./Entitlements.plist", 307 + "frameworks": [ 308 + "CoreAudio", 309 + "./libs/libcustom.dylib", 310 + "./frameworks/CustomFramework.framework" 311 + ], 312 + "files": { 313 + "embedded.provisionprofile": "./profiles/distribution.provisionprofile", 314 + "SharedSupport/README.md": "./docs/README.md" 315 + }, 316 + "dmg": { 317 + "background": "./images/dmg-background.png", 318 + "windowSize": { 319 + "width": 660, 320 + "height": 400 321 + }, 322 + "appPosition": { 323 + "x": 180, 324 + "y": 220 325 + }, 326 + "applicationFolderPosition": { 327 + "x": 480, 328 + "y": 220 329 + } 330 + } 331 + } 332 + } 333 + } 334 + ``` 335 + 336 + ### Minimum System Version 337 + 338 + Set the minimum supported macOS version: 339 + 340 + ```json 341 + { 342 + "bundle": { 343 + "macOS": { 344 + "minimumSystemVersion": "12.0" 345 + } 346 + } 347 + } 348 + ``` 349 + 350 + Default: macOS 10.13 (High Sierra) 351 + 352 + ### Including Frameworks and Libraries 353 + 354 + Bundle system frameworks or custom dylib files: 355 + 356 + ```json 357 + { 358 + "bundle": { 359 + "macOS": { 360 + "frameworks": [ 361 + "CoreAudio", 362 + "AVFoundation", 363 + "./libs/libmsodbcsql.18.dylib", 364 + "./frameworks/Sparkle.framework" 365 + ] 366 + } 367 + } 368 + } 369 + ``` 370 + 371 + - **System frameworks:** Specify name only (e.g., `"CoreAudio"`) 372 + - **Custom frameworks/dylibs:** Provide path relative to `src-tauri` 373 + 374 + ### Adding Custom Files to Bundle 375 + 376 + Include additional files in the bundle's Contents directory: 377 + 378 + ```json 379 + { 380 + "bundle": { 381 + "macOS": { 382 + "files": { 383 + "embedded.provisionprofile": "./profile.provisionprofile", 384 + "SharedSupport/docs/guide.pdf": "./assets/guide.pdf", 385 + "Resources/config.json": "./config/default.json" 386 + } 387 + } 388 + } 389 + } 390 + ``` 391 + 392 + Format: `"destination": "source"` where paths are relative to `tauri.conf.json` 393 + 394 + ## Troubleshooting 395 + 396 + ### Common Issues 397 + 398 + **DMG icons not positioned correctly on CI/CD:** 399 + - This is a known issue with headless environments 400 + - Consider building DMGs locally or accepting default positioning 401 + 402 + **App rejected due to missing usage descriptions:** 403 + - Add all required `NS*UsageDescription` keys to `Info.plist` 404 + - Ensure descriptions clearly explain why access is needed 405 + 406 + **Entitlements not applied:** 407 + - Verify the entitlements file path in `tauri.conf.json` 408 + - Ensure the app is properly code-signed 409 + 410 + **Framework not found at runtime:** 411 + - Check framework path is correct relative to `src-tauri` 412 + - Verify framework is properly signed 413 + 414 + ### Verification Commands 415 + 416 + ```bash 417 + # Check Info.plist contents 418 + plutil -p path/to/App.app/Contents/Info.plist 419 + 420 + # Verify entitlements 421 + codesign -d --entitlements - path/to/App.app 422 + 423 + # Check code signature 424 + codesign -vvv --deep --strict path/to/App.app 425 + 426 + # View bundle structure 427 + find path/to/App.app -type f | head -50 428 + ``` 429 + 430 + ## Quick Reference 431 + 432 + ### File Locations 433 + 434 + | File | Location | Purpose | 435 + |------|----------|---------| 436 + | `Info.plist` | `src-tauri/Info.plist` | App metadata extensions | 437 + | `Entitlements.plist` | `src-tauri/Entitlements.plist` | Capability entitlements | 438 + | `DMG Background` | Any path in project | DMG window background | 439 + | `Localized strings` | `src-tauri/infoplist/<lang>.lproj/` | Localized Info.plist values | 440 + 441 + ### Build Output Locations 442 + 443 + ``` 444 + src-tauri/target/release/bundle/ 445 + macos/ 446 + <ProductName>.app # Application bundle 447 + dmg/ 448 + <ProductName>_<version>_<arch>.dmg # DMG installer 449 + ```
+436
.agents/skills/embedding-tauri-sidecars/SKILL.md
··· 1 + --- 2 + name: embedding-tauri-sidecars 3 + description: Teaches the assistant how to embed and execute external binaries (sidecars) in Tauri applications, including configuration, cross-platform executable naming, and Rust/JavaScript APIs for spawning sidecar processes. 4 + --- 5 + 6 + # Tauri Sidecars: Embedding External Binaries 7 + 8 + This skill covers embedding and executing external binaries (sidecars) in Tauri applications, including configuration, cross-platform considerations, and execution from Rust and JavaScript. 9 + 10 + ## Overview 11 + 12 + Sidecars are external binaries embedded within Tauri applications to extend functionality or eliminate the need for users to install dependencies. They can be executables written in any programming language. 13 + 14 + **Common Use Cases:** 15 + - Python CLI applications packaged with PyInstaller 16 + - Go or Rust compiled binaries for specific tasks 17 + - Node.js applications bundled as executables 18 + - API servers or background services 19 + 20 + ## Plugin Dependency 21 + 22 + Sidecars require the shell plugin: 23 + 24 + **Cargo.toml:** 25 + ```toml 26 + [dependencies] 27 + tauri-plugin-shell = "2" 28 + ``` 29 + 30 + **Register in main.rs:** 31 + ```rust 32 + fn main() { 33 + tauri::Builder::default() 34 + .plugin(tauri_plugin_shell::init()) 35 + .run(tauri::generate_context!()) 36 + .expect("error while running tauri application"); 37 + } 38 + ``` 39 + 40 + **Frontend package:** 41 + ```bash 42 + npm install @tauri-apps/plugin-shell 43 + ``` 44 + 45 + ## Configuration 46 + 47 + ### Registering Sidecars 48 + 49 + Configure sidecars in `tauri.conf.json` under `bundle.externalBin`. Paths are relative to `src-tauri`: 50 + 51 + ```json 52 + { 53 + "bundle": { 54 + "externalBin": [ 55 + "binaries/my-sidecar", 56 + "../external/processor" 57 + ] 58 + } 59 + } 60 + ``` 61 + 62 + **Important:** The path is a stem. Tauri appends the target triple suffix at build time. 63 + 64 + ### Cross-Platform Binary Naming 65 + 66 + Each sidecar requires platform-specific variants with target triple suffixes: 67 + 68 + | Platform | Architecture | Required Filename | 69 + |----------|--------------|-------------------| 70 + | Linux | x86_64 | `my-sidecar-x86_64-unknown-linux-gnu` | 71 + | Linux | ARM64 | `my-sidecar-aarch64-unknown-linux-gnu` | 72 + | macOS | Intel | `my-sidecar-x86_64-apple-darwin` | 73 + | macOS | Apple Silicon | `my-sidecar-aarch64-apple-darwin` | 74 + | Windows | x86_64 | `my-sidecar-x86_64-pc-windows-msvc.exe` | 75 + 76 + **Determine your target triple:** 77 + ```bash 78 + rustc --print host-tuple # Rust 1.84.0+ 79 + rustc -Vv | grep host # Older versions 80 + ``` 81 + 82 + ### Directory Structure 83 + 84 + ``` 85 + src-tauri/ 86 + binaries/ 87 + my-sidecar-x86_64-unknown-linux-gnu 88 + my-sidecar-aarch64-apple-darwin 89 + my-sidecar-x86_64-apple-darwin 90 + my-sidecar-x86_64-pc-windows-msvc.exe 91 + tauri.conf.json 92 + src/main.rs 93 + ``` 94 + 95 + ## Executing Sidecars from Rust 96 + 97 + ### Basic Execution 98 + 99 + ```rust 100 + use tauri_plugin_shell::ShellExt; 101 + 102 + #[tauri::command] 103 + async fn run_sidecar(app: tauri::AppHandle) -> Result<String, String> { 104 + let output = app 105 + .shell() 106 + .sidecar("my-sidecar") 107 + .map_err(|e| e.to_string())? 108 + .output() 109 + .await 110 + .map_err(|e| e.to_string())?; 111 + 112 + if output.status.success() { 113 + Ok(String::from_utf8_lossy(&output.stdout).to_string()) 114 + } else { 115 + Err(String::from_utf8_lossy(&output.stderr).to_string()) 116 + } 117 + } 118 + ``` 119 + 120 + **Note:** Pass only the filename to `sidecar()`, not the full path from configuration. 121 + 122 + ### With Arguments 123 + 124 + ```rust 125 + #[tauri::command] 126 + async fn process_file(app: tauri::AppHandle, file_path: String) -> Result<String, String> { 127 + let output = app 128 + .shell() 129 + .sidecar("processor") 130 + .map_err(|e| e.to_string())? 131 + .args(["--input", &file_path, "--format", "json"]) 132 + .output() 133 + .await 134 + .map_err(|e| e.to_string())?; 135 + 136 + Ok(String::from_utf8_lossy(&output.stdout).to_string()) 137 + } 138 + ``` 139 + 140 + ### Spawning Long-Running Processes 141 + 142 + For sidecars that run continuously (API servers, watchers): 143 + 144 + ```rust 145 + use tauri_plugin_shell::{ShellExt, process::CommandEvent}; 146 + 147 + #[tauri::command] 148 + async fn start_server(app: tauri::AppHandle) -> Result<u32, String> { 149 + let (mut rx, child) = app 150 + .shell() 151 + .sidecar("api-server") 152 + .map_err(|e| e.to_string())? 153 + .args(["--port", "8080"]) 154 + .spawn() 155 + .map_err(|e| e.to_string())?; 156 + 157 + let pid = child.pid(); 158 + 159 + tauri::async_runtime::spawn(async move { 160 + while let Some(event) = rx.recv().await { 161 + match event { 162 + CommandEvent::Stdout(line) => println!("{}", String::from_utf8_lossy(&line)), 163 + CommandEvent::Stderr(line) => eprintln!("{}", String::from_utf8_lossy(&line)), 164 + CommandEvent::Terminated(payload) => { 165 + println!("Terminated: {:?}", payload.code); 166 + break; 167 + } 168 + _ => {} 169 + } 170 + } 171 + }); 172 + 173 + Ok(pid) 174 + } 175 + ``` 176 + 177 + ### Managing Sidecar Lifecycle 178 + 179 + ```rust 180 + use std::sync::Mutex; 181 + use tauri::State; 182 + use tauri_plugin_shell::{ShellExt, process::CommandChild}; 183 + 184 + struct SidecarState { 185 + child: Mutex<Option<CommandChild>>, 186 + } 187 + 188 + #[tauri::command] 189 + async fn start_sidecar(app: tauri::AppHandle, state: State<'_, SidecarState>) -> Result<(), String> { 190 + let (_, child) = app.shell().sidecar("service").map_err(|e| e.to_string())? 191 + .spawn().map_err(|e| e.to_string())?; 192 + *state.child.lock().unwrap() = Some(child); 193 + Ok(()) 194 + } 195 + 196 + #[tauri::command] 197 + async fn stop_sidecar(state: State<'_, SidecarState>) -> Result<(), String> { 198 + if let Some(child) = state.child.lock().unwrap().take() { 199 + child.kill().map_err(|e| e.to_string())?; 200 + } 201 + Ok(()) 202 + } 203 + ``` 204 + 205 + ## Executing Sidecars from JavaScript 206 + 207 + ### Permission Configuration 208 + 209 + Grant shell execution permissions in `src-tauri/capabilities/default.json`: 210 + 211 + ```json 212 + { 213 + "$schema": "../gen/schemas/desktop-schema.json", 214 + "identifier": "default", 215 + "windows": ["main"], 216 + "permissions": [ 217 + "core:default", 218 + { 219 + "identifier": "shell:allow-execute", 220 + "allow": [{ "name": "binaries/my-sidecar", "sidecar": true }] 221 + } 222 + ] 223 + } 224 + ``` 225 + 226 + ### Basic Execution 227 + 228 + ```typescript 229 + import { Command } from '@tauri-apps/plugin-shell'; 230 + 231 + async function runSidecar(): Promise<string> { 232 + const command = Command.sidecar('binaries/my-sidecar'); 233 + const output = await command.execute(); 234 + if (output.code === 0) return output.stdout; 235 + throw new Error(output.stderr); 236 + } 237 + ``` 238 + 239 + ### With Arguments 240 + 241 + ```typescript 242 + async function processFile(filePath: string): Promise<string> { 243 + const command = Command.sidecar('binaries/processor', [ 244 + '--input', filePath, '--format', 'json' 245 + ]); 246 + const output = await command.execute(); 247 + return output.stdout; 248 + } 249 + ``` 250 + 251 + ### Handling Streaming Output 252 + 253 + ```typescript 254 + import { Command, Child } from '@tauri-apps/plugin-shell'; 255 + 256 + async function runWithStreaming(): Promise<Child> { 257 + const command = Command.sidecar('binaries/long-task'); 258 + 259 + command.on('close', (data) => console.log(`Finished: ${data.code}`)); 260 + command.on('error', (error) => console.error(error)); 261 + command.stdout.on('data', (line) => console.log(line)); 262 + command.stderr.on('data', (line) => console.error(line)); 263 + 264 + return await command.spawn(); 265 + } 266 + ``` 267 + 268 + ### Managing Long-Running Processes 269 + 270 + ```typescript 271 + let serverProcess: Child | null = null; 272 + 273 + async function startServer(): Promise<number> { 274 + const command = Command.sidecar('binaries/api-server', ['--port', '8080']); 275 + command.stdout.on('data', console.log); 276 + serverProcess = await command.spawn(); 277 + return serverProcess.pid; 278 + } 279 + 280 + async function stopServer(): Promise<void> { 281 + if (serverProcess) { 282 + await serverProcess.kill(); 283 + serverProcess = null; 284 + } 285 + } 286 + ``` 287 + 288 + ## Argument Validation 289 + 290 + Configure argument validation in capabilities: 291 + 292 + ```json 293 + { 294 + "identifier": "shell:allow-execute", 295 + "allow": [{ 296 + "name": "binaries/my-sidecar", 297 + "sidecar": true, 298 + "args": [ 299 + "-o", 300 + "--verbose", 301 + { "validator": "\\S+" } 302 + ] 303 + }] 304 + } 305 + ``` 306 + 307 + **Argument types:** 308 + - **Static string**: Exact match required (`-o`, `--verbose`) 309 + - **Validator object**: Regex pattern for dynamic values 310 + - **`true`**: Allow any argument (use with caution) 311 + 312 + ## Cross-Platform Considerations 313 + 314 + ### Building Platform-Specific Binaries 315 + 316 + **Rust sidecars:** 317 + ```bash 318 + cargo build --release --target x86_64-unknown-linux-gnu 319 + cp target/x86_64-unknown-linux-gnu/release/my-tool \ 320 + src-tauri/binaries/my-tool-x86_64-unknown-linux-gnu 321 + ``` 322 + 323 + **Python with PyInstaller:** 324 + ```bash 325 + pyinstaller --onefile my_script.py 326 + mv dist/my_script dist/my_script-x86_64-unknown-linux-gnu 327 + ``` 328 + 329 + ### Platform Notes 330 + 331 + **Windows:** 332 + - Executables must have `.exe` extension 333 + - Handle line endings in text file processing 334 + 335 + **macOS:** 336 + - Use `lipo` for universal binaries (Intel + Apple Silicon) 337 + - Code signing may be required for distribution 338 + - Gatekeeper may block unsigned sidecars 339 + 340 + **Linux:** 341 + - Mark binaries as executable (`chmod +x`) 342 + - Consider glibc version compatibility 343 + - Static linking reduces dependency issues 344 + 345 + ## Complete Example 346 + 347 + **tauri.conf.json:** 348 + ```json 349 + { 350 + "productName": "My App", 351 + "version": "1.0.0", 352 + "identifier": "com.example.myapp", 353 + "bundle": { 354 + "externalBin": ["binaries/data-processor"] 355 + } 356 + } 357 + ``` 358 + 359 + **capabilities/default.json:** 360 + ```json 361 + { 362 + "$schema": "../gen/schemas/desktop-schema.json", 363 + "identifier": "default", 364 + "windows": ["main"], 365 + "permissions": [ 366 + "core:default", 367 + { 368 + "identifier": "shell:allow-execute", 369 + "allow": [{ 370 + "name": "binaries/data-processor", 371 + "sidecar": true, 372 + "args": [ 373 + "--input", { "validator": "^[a-zA-Z0-9_\\-./]+$" }, 374 + "--output", { "validator": "^[a-zA-Z0-9_\\-./]+$" } 375 + ] 376 + }] 377 + } 378 + ] 379 + } 380 + ``` 381 + 382 + **src/main.rs:** 383 + ```rust 384 + use tauri_plugin_shell::ShellExt; 385 + 386 + #[tauri::command] 387 + async fn process_data(app: tauri::AppHandle, input: String, output: String) -> Result<String, String> { 388 + let result = app.shell().sidecar("data-processor").map_err(|e| e.to_string())? 389 + .args(["--input", &input, "--output", &output]) 390 + .output().await.map_err(|e| e.to_string())?; 391 + 392 + if result.status.success() { 393 + Ok(String::from_utf8_lossy(&result.stdout).to_string()) 394 + } else { 395 + Err(String::from_utf8_lossy(&result.stderr).to_string()) 396 + } 397 + } 398 + 399 + fn main() { 400 + tauri::Builder::default() 401 + .plugin(tauri_plugin_shell::init()) 402 + .invoke_handler(tauri::generate_handler![process_data]) 403 + .run(tauri::generate_context!()) 404 + .expect("error while running tauri application"); 405 + } 406 + ``` 407 + 408 + **Frontend (App.tsx):** 409 + ```tsx 410 + import { invoke } from '@tauri-apps/api/core'; 411 + 412 + function App() { 413 + const handleProcess = async () => { 414 + try { 415 + const result = await invoke('process_data', { 416 + input: '/path/to/input.txt', 417 + output: '/path/to/output.txt' 418 + }); 419 + console.log('Result:', result); 420 + } catch (error) { 421 + console.error('Error:', error); 422 + } 423 + }; 424 + 425 + return <button onClick={handleProcess}>Process Data</button>; 426 + } 427 + ``` 428 + 429 + ## Best Practices 430 + 431 + 1. **Validate all sidecar paths**: Never pass untrusted paths to sidecars 432 + 2. **Use argument validators**: Restrict allowed arguments in capabilities 433 + 3. **Handle errors gracefully**: Sidecars may fail or be missing 434 + 4. **Clean up processes**: Kill spawned processes on app exit 435 + 5. **Test on all platforms**: Binary naming and execution varies 436 + 6. **Consider binary size**: Sidecars increase bundle size significantly
+463
.agents/skills/integrating-tauri-js-frontends/SKILL.md
··· 1 + --- 2 + name: integrating-tauri-js-frontends 3 + description: Guides Claude through configuring JavaScript frontend frameworks for Tauri v2 desktop applications, including Next.js, Nuxt, Qwik, SvelteKit, and Vite with proper SSG setup, tauri.conf.json settings, and framework-specific configurations. 4 + --- 5 + 6 + # Tauri v2 JavaScript Frontend Integration 7 + 8 + This skill covers integrating JavaScript frontend frameworks with Tauri v2 for desktop application development. 9 + 10 + ## Core Architecture 11 + 12 + Tauri functions as a static web host, serving HTML, CSS, JavaScript, and WASM files through a native webview. The framework is frontend-agnostic but requires specific configurations for optimal integration. 13 + 14 + ### Supported Application Types 15 + 16 + - Static Site Generation (SSG) 17 + - Single-Page Applications (SPA) 18 + - Multi-Page Applications (MPA) 19 + 20 + ### Not Supported 21 + 22 + Server-side rendering (SSR) in its native form. All frameworks must be configured for static output. 23 + 24 + ## General Requirements 25 + 26 + ### Key Principles 27 + 28 + 1. **Static Output Required**: Tauri cannot run a Node.js server - all content must be pre-built static files 29 + 2. **Client-Server Architecture**: Implement proper client-server relationships between app and APIs (no hybrid SSR solutions) 30 + 3. **Mobile Development**: Requires a development server hosting the frontend on your internal IP 31 + 32 + ### Common tauri.conf.json Structure 33 + 34 + ```json 35 + { 36 + "build": { 37 + "beforeDevCommand": "<package-manager> dev", 38 + "beforeBuildCommand": "<package-manager> build", 39 + "devUrl": "http://localhost:<port>", 40 + "frontendDist": "../<output-dir>" 41 + } 42 + } 43 + ``` 44 + 45 + Replace `<package-manager>` with `npm run`, `yarn`, `pnpm`, or `deno task`. 46 + 47 + --- 48 + 49 + ## Framework Configurations 50 + 51 + ### Vite (React, Vue, Svelte, Solid) 52 + 53 + Vite is the recommended choice for SPA frameworks due to its fast development experience and simple configuration. 54 + 55 + #### package.json 56 + 57 + ```json 58 + { 59 + "scripts": { 60 + "dev": "vite", 61 + "build": "tsc && vite build", 62 + "preview": "vite preview", 63 + "tauri": "tauri" 64 + } 65 + } 66 + ``` 67 + 68 + #### vite.config.ts 69 + 70 + ```typescript 71 + import { defineConfig } from 'vite'; 72 + 73 + export default defineConfig({ 74 + clearScreen: false, 75 + server: { 76 + port: 5173, 77 + strictPort: true, 78 + host: process.env.TAURI_DEV_HOST || 'localhost', 79 + watch: { 80 + ignored: ['**/src-tauri/**'], 81 + }, 82 + }, 83 + envPrefix: ['VITE_', 'TAURI_ENV_*'], 84 + build: { 85 + target: process.env.TAURI_ENV_PLATFORM === 'windows' ? 'chrome105' : 'safari13', 86 + minify: process.env.TAURI_ENV_DEBUG ? false : 'esbuild', 87 + sourcemap: !!process.env.TAURI_ENV_DEBUG, 88 + }, 89 + }); 90 + ``` 91 + 92 + #### tauri.conf.json 93 + 94 + ```json 95 + { 96 + "build": { 97 + "beforeDevCommand": "npm run dev", 98 + "beforeBuildCommand": "npm run build", 99 + "devUrl": "http://localhost:5173", 100 + "frontendDist": "../dist" 101 + } 102 + } 103 + ``` 104 + 105 + --- 106 + 107 + ### Next.js 108 + 109 + Next.js requires static export mode since Tauri cannot run Node.js servers. 110 + 111 + #### Critical Requirements 112 + 113 + - Must use `output: 'export'` in next.config 114 + - Images must be unoptimized for static export 115 + - Asset prefix required for development server 116 + 117 + #### next.config.mjs 118 + 119 + ```javascript 120 + const isProd = process.env.NODE_ENV === 'production'; 121 + const internalHost = process.env.TAURI_DEV_HOST || 'localhost'; 122 + 123 + /** @type {import('next').NextConfig} */ 124 + const nextConfig = { 125 + output: 'export', 126 + images: { 127 + unoptimized: true, 128 + }, 129 + assetPrefix: isProd ? undefined : `http://${internalHost}:3000`, 130 + }; 131 + 132 + export default nextConfig; 133 + ``` 134 + 135 + #### package.json 136 + 137 + ```json 138 + { 139 + "scripts": { 140 + "dev": "next dev", 141 + "build": "next build", 142 + "start": "next start", 143 + "tauri": "tauri" 144 + } 145 + } 146 + ``` 147 + 148 + #### tauri.conf.json 149 + 150 + ```json 151 + { 152 + "build": { 153 + "beforeDevCommand": "npm run dev", 154 + "beforeBuildCommand": "npm run build", 155 + "devUrl": "http://localhost:3000", 156 + "frontendDist": "../out" 157 + } 158 + } 159 + ``` 160 + 161 + #### SSG Considerations for Next.js 162 + 163 + - The `out` directory contains static exports 164 + - Dynamic routes require `generateStaticParams()` 165 + - API routes are not supported - use Tauri commands instead 166 + - `next/image` optimization is disabled; use standard `<img>` or configure unoptimized 167 + 168 + --- 169 + 170 + ### Nuxt 171 + 172 + Nuxt must run in SSG mode with `ssr: false` for Tauri compatibility. 173 + 174 + #### nuxt.config.ts 175 + 176 + ```typescript 177 + export default defineNuxtConfig({ 178 + ssr: false, 179 + telemetry: false, 180 + devServer: { 181 + host: '0.0.0.0', // Required for iOS device compatibility 182 + }, 183 + vite: { 184 + clearScreen: false, 185 + envPrefix: ['VITE_', 'TAURI_'], 186 + server: { 187 + strictPort: true, 188 + watch: { 189 + ignored: ['**/src-tauri/**'], 190 + }, 191 + }, 192 + }, 193 + }); 194 + ``` 195 + 196 + #### package.json 197 + 198 + ```json 199 + { 200 + "scripts": { 201 + "dev": "nuxt dev", 202 + "build": "nuxt build", 203 + "generate": "nuxt generate", 204 + "tauri": "tauri" 205 + } 206 + } 207 + ``` 208 + 209 + #### tauri.conf.json 210 + 211 + ```json 212 + { 213 + "build": { 214 + "beforeDevCommand": "npm run dev", 215 + "beforeBuildCommand": "npm run generate", 216 + "devUrl": "http://localhost:3000", 217 + "frontendDist": "../dist" 218 + } 219 + } 220 + ``` 221 + 222 + #### SSG Considerations for Nuxt 223 + 224 + - Use `nuxt generate` for production builds (creates static files) 225 + - Server routes (`/server/api`) are not available - use Tauri commands 226 + - Nitro server functionality is disabled in SSG mode 227 + 228 + --- 229 + 230 + ### SvelteKit 231 + 232 + SvelteKit requires the static adapter and SSR must be disabled. 233 + 234 + #### Installation 235 + 236 + ```bash 237 + npm install --save-dev @sveltejs/adapter-static 238 + ``` 239 + 240 + #### svelte.config.js 241 + 242 + ```javascript 243 + import adapter from '@sveltejs/adapter-static'; 244 + import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; 245 + 246 + /** @type {import('@sveltejs/kit').Config} */ 247 + const config = { 248 + preprocess: vitePreprocess(), 249 + kit: { 250 + adapter: adapter({ 251 + fallback: 'index.html', 252 + }), 253 + }, 254 + }; 255 + 256 + export default config; 257 + ``` 258 + 259 + #### src/routes/+layout.ts 260 + 261 + ```typescript 262 + export const ssr = false; 263 + export const prerender = true; 264 + ``` 265 + 266 + #### vite.config.ts 267 + 268 + ```typescript 269 + import { sveltekit } from '@sveltejs/kit/vite'; 270 + import { defineConfig } from 'vite'; 271 + 272 + export default defineConfig({ 273 + plugins: [sveltekit()], 274 + clearScreen: false, 275 + server: { 276 + port: 5173, 277 + strictPort: true, 278 + host: process.env.TAURI_DEV_HOST || 'localhost', 279 + watch: { 280 + ignored: ['**/src-tauri/**'], 281 + }, 282 + }, 283 + envPrefix: ['VITE_', 'TAURI_ENV_*'], 284 + }); 285 + ``` 286 + 287 + #### tauri.conf.json 288 + 289 + ```json 290 + { 291 + "build": { 292 + "beforeDevCommand": "npm run dev", 293 + "beforeBuildCommand": "npm run build", 294 + "devUrl": "http://localhost:5173", 295 + "frontendDist": "../build" 296 + } 297 + } 298 + ``` 299 + 300 + #### SSG Considerations for SvelteKit 301 + 302 + - SPA mode (without prerendering) is recommended for full Tauri API access in load functions 303 + - With prerendering, load functions execute at build time without Tauri API access 304 + - The `fallback: 'index.html'` enables SPA routing 305 + - Output directory is `build/` by default 306 + 307 + --- 308 + 309 + ### Qwik 310 + 311 + Qwik requires the static adapter for Tauri compatibility. 312 + 313 + #### Setup 314 + 315 + ```bash 316 + # Create project 317 + npm create qwik@latest 318 + cd <project> 319 + 320 + # Add static adapter 321 + npm run qwik add static 322 + 323 + # Add Tauri CLI 324 + npm install -D @tauri-apps/cli@latest 325 + 326 + # Initialize Tauri 327 + npm run tauri init 328 + ``` 329 + 330 + #### package.json 331 + 332 + ```json 333 + { 334 + "scripts": { 335 + "dev": "vite", 336 + "build": "qwik build", 337 + "tauri": "tauri" 338 + } 339 + } 340 + ``` 341 + 342 + #### tauri.conf.json 343 + 344 + ```json 345 + { 346 + "build": { 347 + "beforeDevCommand": "npm run dev", 348 + "beforeBuildCommand": "npm run build", 349 + "devUrl": "http://localhost:5173", 350 + "frontendDist": "../dist" 351 + } 352 + } 353 + ``` 354 + 355 + #### SSG Considerations for Qwik 356 + 357 + - The static adapter is mandatory 358 + - Qwik City server functions are not available 359 + - Use Tauri commands for backend functionality 360 + 361 + --- 362 + 363 + ## Quick Reference Table 364 + 365 + | Framework | Output Dir | Dev Port | Build Command | Key Config | 366 + |------------|------------|----------|-------------------|-------------------------------| 367 + | Vite | `dist` | 5173 | `vite build` | Standard Vite config | 368 + | Next.js | `out` | 3000 | `next build` | `output: 'export'` | 369 + | Nuxt | `dist` | 3000 | `nuxt generate` | `ssr: false` | 370 + | SvelteKit | `build` | 5173 | `vite build` | `adapter-static` | 371 + | Qwik | `dist` | 5173 | `qwik build` | Static adapter | 372 + 373 + --- 374 + 375 + ## Common Issues and Solutions 376 + 377 + ### Issue: Assets Not Loading in Development 378 + 379 + **Cause**: Asset prefix not configured for development server. 380 + 381 + **Solution**: Configure asset prefix to point to dev server (see Next.js config example). 382 + 383 + ### Issue: Tauri APIs Unavailable 384 + 385 + **Cause**: Code executing during SSG build time instead of runtime. 386 + 387 + **Solution**: 388 + - Disable prerendering for pages using Tauri APIs 389 + - Use dynamic imports with `ssr: false` 390 + - Check for `window.__TAURI__` before API calls 391 + 392 + ### Issue: Hot Reload Not Working 393 + 394 + **Cause**: File watcher including `src-tauri` directory. 395 + 396 + **Solution**: Add `**/src-tauri/**` to watch ignore list in Vite config. 397 + 398 + ### Issue: Mobile Development Fails 399 + 400 + **Cause**: Dev server not accessible on network. 401 + 402 + **Solution**: Set host to `0.0.0.0` or use `TAURI_DEV_HOST` environment variable. 403 + 404 + ### Issue: Build Fails with SSR Errors 405 + 406 + **Cause**: Framework attempting server-side rendering. 407 + 408 + **Solution**: Ensure SSR is disabled: 409 + - Next.js: `output: 'export'` 410 + - Nuxt: `ssr: false` 411 + - SvelteKit: `export const ssr = false` in layout 412 + - Qwik: Use static adapter 413 + 414 + --- 415 + 416 + ## Environment Variables 417 + 418 + ### Tauri-Provided Variables 419 + 420 + | Variable | Description | 421 + |-----------------------|--------------------------------------------------| 422 + | `TAURI_DEV_HOST` | Host IP for mobile development | 423 + | `TAURI_ENV_PLATFORM` | Target platform (windows, macos, linux, ios, android) | 424 + | `TAURI_ENV_DEBUG` | Set during debug builds | 425 + 426 + ### Recommended envPrefix 427 + 428 + ```typescript 429 + envPrefix: ['VITE_', 'TAURI_ENV_*'] 430 + ``` 431 + 432 + --- 433 + 434 + ## Build Targets 435 + 436 + Configure build targets based on platform webview capabilities: 437 + 438 + ```typescript 439 + build: { 440 + target: process.env.TAURI_ENV_PLATFORM === 'windows' 441 + ? 'chrome105' // WebView2 on Windows 442 + : 'safari13', // WebKit on macOS/Linux 443 + } 444 + ``` 445 + 446 + --- 447 + 448 + ## Mobile Development 449 + 450 + For iOS and Android development, the dev server must be accessible on the network: 451 + 452 + ```typescript 453 + server: { 454 + host: process.env.TAURI_DEV_HOST || '0.0.0.0', 455 + strictPort: true, 456 + } 457 + ``` 458 + 459 + Run with the appropriate mobile command: 460 + ```bash 461 + npm run tauri ios dev 462 + npm run tauri android dev 463 + ```
+431
.agents/skills/integrating-tauri-rust-frontends/SKILL.md
··· 1 + --- 2 + name: integrating-tauri-rust-frontends 3 + description: Guides the user through integrating Rust-based WASM frontend frameworks with Tauri v2, covering Leptos and Trunk setup, WASM compilation configuration, Cargo.toml dependencies, Trunk.toml bundler settings, and withGlobalTauri API access. 4 + --- 5 + 6 + # Tauri Rust/WASM Frontend Integration 7 + 8 + This skill covers integrating Rust-based frontend frameworks with Tauri v2 for building desktop and mobile applications with WASM. 9 + 10 + ## Supported Frameworks 11 + 12 + | Framework | Description | Bundler | 13 + |-----------|-------------|---------| 14 + | Leptos | Reactive Rust framework for building web UIs | Trunk | 15 + | Yew | Component-based Rust framework | Trunk | 16 + | Dioxus | Cross-platform UI framework | Trunk | 17 + | Sycamore | Reactive library for Rust | Trunk | 18 + 19 + All Rust/WASM frontends use **Trunk** as the bundler/dev server. 20 + 21 + ## Critical Requirements 22 + 23 + 1. **Static Site Generation (SSG) Only** - Tauri does not support server-based solutions (SSR). Use SSG, SPA, or MPA approaches. 24 + 2. **withGlobalTauri** - Must be enabled for WASM frontends to access Tauri APIs via `window.__TAURI__` and `wasm-bindgen`. 25 + 3. **WebSocket Protocol** - Configure `ws_protocol = "ws"` for hot-reload on mobile development. 26 + 27 + ## Project Structure 28 + 29 + ``` 30 + my-tauri-app/ 31 + ├── src/ 32 + │ ├── main.rs # Rust frontend entry point 33 + │ └── app.rs # Application component 34 + ├── src-tauri/ 35 + │ ├── src/ 36 + │ │ └── main.rs # Tauri backend 37 + │ ├── Cargo.toml # Tauri dependencies 38 + │ └── tauri.conf.json # Tauri configuration 39 + ├── index.html # HTML entry point for Trunk 40 + ├── Cargo.toml # Frontend dependencies 41 + ├── Trunk.toml # Trunk bundler configuration 42 + └── dist/ # Build output (generated) 43 + ``` 44 + 45 + ## Configuration Files 46 + 47 + ### Tauri Configuration (src-tauri/tauri.conf.json) 48 + 49 + ```json 50 + { 51 + "build": { 52 + "beforeDevCommand": "trunk serve", 53 + "devUrl": "http://localhost:1420", 54 + "beforeBuildCommand": "trunk build", 55 + "frontendDist": "../dist" 56 + }, 57 + "app": { 58 + "withGlobalTauri": true 59 + } 60 + } 61 + ``` 62 + 63 + **Key settings:** 64 + - `beforeDevCommand`: Runs Trunk dev server before Tauri 65 + - `devUrl`: URL where Trunk serves the frontend (default: 1420 for Leptos, 8080 for plain Trunk) 66 + - `beforeBuildCommand`: Builds WASM bundle before packaging 67 + - `frontendDist`: Path to built frontend assets 68 + - `withGlobalTauri`: **Required for WASM** - Exposes `window.__TAURI__` for API access 69 + 70 + ### Trunk Configuration (Trunk.toml) 71 + 72 + ```toml 73 + [build] 74 + target = "./index.html" 75 + dist = "./dist" 76 + 77 + [watch] 78 + ignore = ["./src-tauri"] 79 + 80 + [serve] 81 + port = 1420 82 + open = false 83 + 84 + [serve.ws] 85 + ws_protocol = "ws" 86 + ``` 87 + 88 + **Key settings:** 89 + - `target`: HTML entry point with Trunk directives 90 + - `ignore`: Prevents watching Tauri backend changes 91 + - `port`: Must match `devUrl` in tauri.conf.json 92 + - `open = false`: Prevents browser auto-open (Tauri handles display) 93 + - `ws_protocol = "ws"`: Required for mobile hot-reload 94 + 95 + ### Frontend Cargo.toml (Root) 96 + 97 + ```toml 98 + [package] 99 + name = "my-app-frontend" 100 + version = "0.1.0" 101 + edition = "2021" 102 + 103 + [lib] 104 + crate-type = ["cdylib", "rlib"] 105 + 106 + [dependencies] 107 + # Core WASM dependencies 108 + wasm-bindgen = "0.2" 109 + wasm-bindgen-futures = "0.4" 110 + js-sys = "0.3" 111 + web-sys = { version = "0.3", features = ["Window", "Document"] } 112 + 113 + # Tauri API bindings for WASM 114 + tauri-wasm = { version = "2", features = ["all"] } 115 + 116 + # Choose your framework: 117 + # For Leptos: 118 + leptos = { version = "0.6", features = ["csr"] } 119 + # For Yew: 120 + # yew = { version = "0.21", features = ["csr"] } 121 + # For Dioxus: 122 + # dioxus = { version = "0.5", features = ["web"] } 123 + 124 + [profile.release] 125 + opt-level = "z" 126 + lto = true 127 + codegen-units = 1 128 + panic = "abort" 129 + ``` 130 + 131 + **Key settings:** 132 + - `crate-type = ["cdylib", "rlib"]`: Required for WASM compilation 133 + - `tauri-wasm`: Provides Rust bindings to Tauri APIs 134 + - `features = ["csr"]`: Client-side rendering for framework 135 + - Release profile optimized for small WASM binary size 136 + 137 + ### HTML Entry Point (index.html) 138 + 139 + ```html 140 + <!DOCTYPE html> 141 + <html> 142 + <head> 143 + <meta charset="utf-8" /> 144 + <meta name="viewport" content="width=device-width, initial-scale=1" /> 145 + <title>My Tauri App</title> 146 + <link data-trunk rel="css" href="styles.css" /> 147 + </head> 148 + <body> 149 + <div id="app"></div> 150 + <link data-trunk rel="rust" href="." data-wasm-opt="z" /> 151 + </body> 152 + </html> 153 + ``` 154 + 155 + **Trunk directives:** 156 + - `data-trunk rel="css"`: Include CSS files 157 + - `data-trunk rel="rust"`: Compile Rust crate to WASM 158 + - `data-wasm-opt="z"`: Optimize for size 159 + 160 + ## Leptos Setup 161 + 162 + ### Leptos-Specific Cargo.toml 163 + 164 + ```toml 165 + [package] 166 + name = "my-leptos-app" 167 + version = "0.1.0" 168 + edition = "2021" 169 + 170 + [lib] 171 + crate-type = ["cdylib", "rlib"] 172 + 173 + [dependencies] 174 + leptos = { version = "0.6", features = ["csr"] } 175 + wasm-bindgen = "0.2" 176 + wasm-bindgen-futures = "0.4" 177 + console_error_panic_hook = "0.1" 178 + tauri-wasm = { version = "2", features = ["all"] } 179 + 180 + [profile.release] 181 + opt-level = "z" 182 + lto = true 183 + ``` 184 + 185 + ### Leptos Main Entry (src/main.rs) 186 + 187 + ```rust 188 + use leptos::*; 189 + 190 + mod app; 191 + use app::App; 192 + 193 + fn main() { 194 + console_error_panic_hook::set_once(); 195 + mount_to_body(|| view! { <App /> }); 196 + } 197 + ``` 198 + 199 + ### Leptos App Component (src/app.rs) 200 + 201 + ```rust 202 + use leptos::*; 203 + use wasm_bindgen::prelude::*; 204 + use wasm_bindgen_futures::spawn_local; 205 + 206 + #[wasm_bindgen] 207 + extern "C" { 208 + #[wasm_bindgen(js_namespace = ["window", "__TAURI__", "core"])] 209 + async fn invoke(cmd: &str, args: JsValue) -> JsValue; 210 + } 211 + 212 + #[component] 213 + pub fn App() -> impl IntoView { 214 + let (message, set_message) = create_signal(String::new()); 215 + 216 + let greet = move |_| { 217 + spawn_local(async move { 218 + let args = serde_json::json!({ "name": "World" }); 219 + let args_js = serde_wasm_bindgen::to_value(&args).unwrap(); 220 + let result = invoke("greet", args_js).await; 221 + let greeting: String = serde_wasm_bindgen::from_value(result).unwrap(); 222 + set_message.set(greeting); 223 + }); 224 + }; 225 + 226 + view! { 227 + <main> 228 + <h1>"Welcome to Tauri + Leptos"</h1> 229 + <button on:click=greet>"Greet"</button> 230 + <p>{message}</p> 231 + </main> 232 + } 233 + } 234 + ``` 235 + 236 + ### Alternative: Using tauri-wasm Crate 237 + 238 + ```rust 239 + use leptos::*; 240 + use tauri_wasm::api::core::invoke; 241 + 242 + #[component] 243 + pub fn App() -> impl IntoView { 244 + let (message, set_message) = create_signal(String::new()); 245 + 246 + let greet = move |_| { 247 + spawn_local(async move { 248 + let result: String = invoke("greet", &serde_json::json!({ "name": "World" })) 249 + .await 250 + .unwrap(); 251 + set_message.set(result); 252 + }); 253 + }; 254 + 255 + view! { 256 + <main> 257 + <button on:click=greet>"Greet"</button> 258 + <p>{message}</p> 259 + </main> 260 + } 261 + } 262 + ``` 263 + 264 + ## Tauri Backend Command 265 + 266 + In `src-tauri/src/main.rs`: 267 + 268 + ```rust 269 + #[tauri::command] 270 + fn greet(name: &str) -> String { 271 + format!("Hello, {}! You've been greeted from Rust!", name) 272 + } 273 + 274 + fn main() { 275 + tauri::Builder::default() 276 + .invoke_handler(tauri::generate_handler![greet]) 277 + .run(tauri::generate_context!()) 278 + .expect("error while running tauri application"); 279 + } 280 + ``` 281 + 282 + ## Development Commands 283 + 284 + ```bash 285 + # Install Trunk 286 + cargo install trunk 287 + 288 + # Add WASM target 289 + rustup target add wasm32-unknown-unknown 290 + 291 + # Development (runs Trunk + Tauri) 292 + cd src-tauri && cargo tauri dev 293 + 294 + # Build for production 295 + cd src-tauri && cargo tauri build 296 + 297 + # Trunk only (for frontend debugging) 298 + trunk serve --port 1420 299 + 300 + # Build WASM only 301 + trunk build --release 302 + ``` 303 + 304 + ## Mobile Development 305 + 306 + For mobile platforms, additional configuration is needed: 307 + 308 + ### Trunk.toml for Mobile 309 + 310 + ```toml 311 + [serve] 312 + port = 1420 313 + open = false 314 + address = "0.0.0.0" # Listen on all interfaces for mobile 315 + 316 + [serve.ws] 317 + ws_protocol = "ws" # Required for mobile hot-reload 318 + ``` 319 + 320 + ### tauri.conf.json for Mobile 321 + 322 + ```json 323 + { 324 + "build": { 325 + "beforeDevCommand": "trunk serve --address 0.0.0.0", 326 + "devUrl": "http://YOUR_LOCAL_IP:1420" 327 + } 328 + } 329 + ``` 330 + 331 + Replace `YOUR_LOCAL_IP` with your machine's local IP (e.g., `192.168.1.100`). 332 + 333 + ## Accessing Tauri APIs from WASM 334 + 335 + ### Method 1: Direct wasm-bindgen (Recommended for control) 336 + 337 + ```rust 338 + use wasm_bindgen::prelude::*; 339 + use serde::{Serialize, Deserialize}; 340 + 341 + #[wasm_bindgen] 342 + extern "C" { 343 + // Core invoke 344 + #[wasm_bindgen(js_namespace = ["window", "__TAURI__", "core"], catch)] 345 + async fn invoke(cmd: &str, args: JsValue) -> Result<JsValue, JsValue>; 346 + 347 + // Event system 348 + #[wasm_bindgen(js_namespace = ["window", "__TAURI__", "event"])] 349 + async fn listen(event: &str, handler: &Closure<dyn Fn(JsValue)>) -> JsValue; 350 + 351 + #[wasm_bindgen(js_namespace = ["window", "__TAURI__", "event"])] 352 + async fn emit(event: &str, payload: JsValue); 353 + } 354 + 355 + // Usage 356 + async fn call_backend() -> Result<String, String> { 357 + let args = serde_wasm_bindgen::to_value(&serde_json::json!({ 358 + "path": "/some/path" 359 + })).map_err(|e| e.to_string())?; 360 + 361 + let result = invoke("read_file", args) 362 + .await 363 + .map_err(|e| format!("{:?}", e))?; 364 + 365 + serde_wasm_bindgen::from_value(result) 366 + .map_err(|e| e.to_string()) 367 + } 368 + ``` 369 + 370 + ### Method 2: Using tauri-wasm Crate 371 + 372 + ```rust 373 + use tauri_wasm::api::{core, event, dialog, fs}; 374 + 375 + // Invoke command 376 + let result: MyResponse = core::invoke("my_command", &my_args).await?; 377 + 378 + // Listen to events 379 + event::listen("my-event", |payload| { 380 + // Handle event 381 + }).await; 382 + 383 + // Emit events 384 + event::emit("my-event", &payload).await; 385 + 386 + // File dialogs 387 + let file = dialog::open(dialog::OpenDialogOptions::default()).await?; 388 + 389 + // File system (requires permissions) 390 + let contents = fs::read_text_file("path/to/file").await?; 391 + ``` 392 + 393 + ## Troubleshooting 394 + 395 + ### WASM not loading 396 + - Verify `withGlobalTauri: true` in tauri.conf.json 397 + - Check browser console for WASM errors 398 + - Ensure `wasm32-unknown-unknown` target is installed 399 + 400 + ### Hot-reload not working on mobile 401 + - Set `ws_protocol = "ws"` in Trunk.toml 402 + - Use `address = "0.0.0.0"` for mobile access 403 + - Verify firewall allows connections on dev port 404 + 405 + ### Tauri APIs undefined 406 + - `withGlobalTauri` must be `true` 407 + - Check `window.__TAURI__` exists in browser console 408 + - Verify tauri-wasm version matches Tauri version 409 + 410 + ### Large WASM binary size 411 + - Enable release profile optimizations 412 + - Use `opt-level = "z"` for size optimization 413 + - Enable LTO with `lto = true` 414 + - Consider `wasm-opt` post-processing 415 + 416 + ### Trunk build fails 417 + - Check Cargo.toml has `crate-type = ["cdylib", "rlib"]` 418 + - Verify index.html has correct `data-trunk` directives 419 + - Ensure no server-side features enabled in framework 420 + 421 + ## Version Compatibility 422 + 423 + | Component | Version | 424 + |-----------|---------| 425 + | Tauri | 2.x | 426 + | Trunk | 0.17+ | 427 + | Leptos | 0.6+ | 428 + | wasm-bindgen | 0.2.x | 429 + | tauri-wasm | 2.x | 430 + 431 + Always match `tauri-wasm` version with your Tauri version.
+471
.agents/skills/listening-to-tauri-events/SKILL.md
··· 1 + --- 2 + name: listening-to-tauri-events 3 + description: Teaches how to subscribe to and listen for Tauri events in the frontend using the events API, including typed event handlers and cleanup patterns. 4 + --- 5 + 6 + # Listening to Tauri Events in the Frontend 7 + 8 + This skill covers how to subscribe to events emitted from Rust in a Tauri v2 application using the `@tauri-apps/api/event` module. 9 + 10 + ## Core Concepts 11 + 12 + Tauri provides two event scopes: 13 + 14 + 1. **Global events** - Broadcast to all listeners across all webviews 15 + 2. **Webview-specific events** - Targeted to specific webview windows 16 + 17 + **Important:** Webview-specific events are NOT received by global listeners. Use the appropriate listener type based on how events are emitted from Rust. 18 + 19 + ## Installation 20 + 21 + The event API is part of the core Tauri API package: 22 + 23 + ```bash 24 + npm install @tauri-apps/api 25 + # or 26 + pnpm add @tauri-apps/api 27 + # or 28 + yarn add @tauri-apps/api 29 + ``` 30 + 31 + ## Global Event Listening 32 + 33 + ### Basic Listen Pattern 34 + 35 + ```typescript 36 + import { listen } from '@tauri-apps/api/event'; 37 + 38 + // Listen for a global event 39 + const unlisten = await listen('download-started', (event) => { 40 + console.log('Event received:', event.payload); 41 + }); 42 + 43 + // Clean up when done 44 + unlisten(); 45 + ``` 46 + 47 + ### Typed Event Payloads 48 + 49 + Define TypeScript interfaces matching your Rust payload structures: 50 + 51 + ```typescript 52 + import { listen } from '@tauri-apps/api/event'; 53 + 54 + // Define the payload type (matches Rust struct with camelCase) 55 + interface DownloadStarted { 56 + url: string; 57 + downloadId: number; 58 + contentLength: number; 59 + } 60 + 61 + // Use generic type parameter for type safety 62 + const unlisten = await listen<DownloadStarted>('download-started', (event) => { 63 + console.log(`Downloading from ${event.payload.url}`); 64 + console.log(`Content length: ${event.payload.contentLength} bytes`); 65 + console.log(`Download ID: ${event.payload.downloadId}`); 66 + }); 67 + ``` 68 + 69 + ### Event Object Structure 70 + 71 + The event callback receives an `Event<T>` object: 72 + 73 + ```typescript 74 + interface Event<T> { 75 + /** Event name */ 76 + event: string; 77 + /** Event identifier */ 78 + id: number; 79 + /** Event payload (your custom type) */ 80 + payload: T; 81 + } 82 + ``` 83 + 84 + ## Webview-Specific Event Listening 85 + 86 + For events emitted with `emit_to` or `emit_filter` targeting specific webviews: 87 + 88 + ```typescript 89 + import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'; 90 + 91 + const appWebview = getCurrentWebviewWindow(); 92 + 93 + // Listen for events targeted to this specific webview 94 + const unlisten = await appWebview.listen<string>('login-result', (event) => { 95 + if (event.payload === 'loggedIn') { 96 + localStorage.setItem('authenticated', 'true'); 97 + } else { 98 + console.error('Login failed:', event.payload); 99 + } 100 + }); 101 + ``` 102 + 103 + ## One-Time Event Listeners 104 + 105 + Use `once` for events that should only be handled a single time: 106 + 107 + ```typescript 108 + import { once } from '@tauri-apps/api/event'; 109 + import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'; 110 + 111 + // Global one-time listener 112 + await once('app-ready', (event) => { 113 + console.log('Application is ready'); 114 + }); 115 + 116 + // Webview-specific one-time listener 117 + const appWebview = getCurrentWebviewWindow(); 118 + await appWebview.once('initialized', (event) => { 119 + console.log('Webview initialized'); 120 + }); 121 + ``` 122 + 123 + ## Cleanup and Unsubscription 124 + 125 + ### Manual Cleanup 126 + 127 + Always unsubscribe listeners when they are no longer needed: 128 + 129 + ```typescript 130 + import { listen } from '@tauri-apps/api/event'; 131 + 132 + const unlisten = await listen('my-event', (event) => { 133 + // Handle event 134 + }); 135 + 136 + // Later, when done listening 137 + unlisten(); 138 + ``` 139 + 140 + ### React Component Cleanup 141 + 142 + ```typescript 143 + import { useEffect } from 'react'; 144 + import { listen } from '@tauri-apps/api/event'; 145 + 146 + interface ProgressPayload { 147 + percent: number; 148 + message: string; 149 + } 150 + 151 + function DownloadProgress() { 152 + useEffect(() => { 153 + let unlisten: (() => void) | undefined; 154 + 155 + const setupListener = async () => { 156 + unlisten = await listen<ProgressPayload>('download-progress', (event) => { 157 + console.log(`Progress: ${event.payload.percent}%`); 158 + }); 159 + }; 160 + 161 + setupListener(); 162 + 163 + // Cleanup when component unmounts 164 + return () => { 165 + if (unlisten) { 166 + unlisten(); 167 + } 168 + }; 169 + }, []); 170 + 171 + return <div>Listening for progress...</div>; 172 + } 173 + ``` 174 + 175 + ### Vue Composition API Cleanup 176 + 177 + ```typescript 178 + import { onMounted, onUnmounted } from 'vue'; 179 + import { listen } from '@tauri-apps/api/event'; 180 + 181 + interface NotificationPayload { 182 + title: string; 183 + body: string; 184 + } 185 + 186 + export default { 187 + setup() { 188 + let unlisten: (() => void) | undefined; 189 + 190 + onMounted(async () => { 191 + unlisten = await listen<NotificationPayload>('notification', (event) => { 192 + console.log(`${event.payload.title}: ${event.payload.body}`); 193 + }); 194 + }); 195 + 196 + onUnmounted(() => { 197 + if (unlisten) { 198 + unlisten(); 199 + } 200 + }); 201 + } 202 + }; 203 + ``` 204 + 205 + ### Svelte Cleanup 206 + 207 + ```svelte 208 + <script lang="ts"> 209 + import { onMount, onDestroy } from 'svelte'; 210 + import { listen } from '@tauri-apps/api/event'; 211 + 212 + interface StatusPayload { 213 + status: 'idle' | 'loading' | 'complete'; 214 + } 215 + 216 + let unlisten: (() => void) | undefined; 217 + 218 + onMount(async () => { 219 + unlisten = await listen<StatusPayload>('status-change', (event) => { 220 + console.log('Status:', event.payload.status); 221 + }); 222 + }); 223 + 224 + onDestroy(() => { 225 + if (unlisten) { 226 + unlisten(); 227 + } 228 + }); 229 + </script> 230 + ``` 231 + 232 + ## Automatic Listener Cleanup 233 + 234 + Tauri automatically clears listeners in these scenarios: 235 + 236 + - **Page reload** - All listeners are cleared 237 + - **URL navigation** - Listeners are cleared (except in SPA routers) 238 + 239 + **Warning:** Single Page Application routers that do not trigger full page reloads will NOT automatically clean up listeners. You must manually unsubscribe. 240 + 241 + ## Multiple Event Listeners 242 + 243 + ### Listening to Multiple Events 244 + 245 + ```typescript 246 + import { listen } from '@tauri-apps/api/event'; 247 + 248 + interface DownloadEvent { 249 + url: string; 250 + } 251 + 252 + interface ProgressEvent { 253 + percent: number; 254 + } 255 + 256 + interface CompleteEvent { 257 + path: string; 258 + size: number; 259 + } 260 + 261 + async function setupDownloadListeners() { 262 + const unlisteners: Array<() => void> = []; 263 + 264 + unlisteners.push( 265 + await listen<DownloadEvent>('download-started', (event) => { 266 + console.log(`Started downloading: ${event.payload.url}`); 267 + }) 268 + ); 269 + 270 + unlisteners.push( 271 + await listen<ProgressEvent>('download-progress', (event) => { 272 + console.log(`Progress: ${event.payload.percent}%`); 273 + }) 274 + ); 275 + 276 + unlisteners.push( 277 + await listen<CompleteEvent>('download-complete', (event) => { 278 + console.log(`Complete: ${event.payload.path} (${event.payload.size} bytes)`); 279 + }) 280 + ); 281 + 282 + // Return cleanup function 283 + return () => { 284 + unlisteners.forEach((unlisten) => unlisten()); 285 + }; 286 + } 287 + 288 + // Usage 289 + const cleanup = await setupDownloadListeners(); 290 + // Later... 291 + cleanup(); 292 + ``` 293 + 294 + ## Type Definitions Reference 295 + 296 + ### Creating Typed Event Helpers 297 + 298 + ```typescript 299 + import { listen, once, Event } from '@tauri-apps/api/event'; 300 + 301 + // Define all your event types in one place 302 + export interface AppEvents { 303 + 'download-started': { url: string; downloadId: number }; 304 + 'download-progress': { downloadId: number; percent: number }; 305 + 'download-complete': { downloadId: number; path: string }; 306 + 'download-error': { downloadId: number; error: string }; 307 + 'notification': { title: string; body: string }; 308 + } 309 + 310 + // Type-safe listen helper 311 + export async function listenTyped<K extends keyof AppEvents>( 312 + eventName: K, 313 + handler: (event: Event<AppEvents[K]>) => void 314 + ): Promise<() => void> { 315 + return listen<AppEvents[K]>(eventName, handler); 316 + } 317 + 318 + // Usage 319 + const unlisten = await listenTyped('download-started', (event) => { 320 + // event.payload is typed as { url: string; downloadId: number } 321 + console.log(event.payload.url); 322 + }); 323 + ``` 324 + 325 + ## Corresponding Rust Emit Code 326 + 327 + For reference, here is how events are emitted from Rust: 328 + 329 + ### Global Emit 330 + 331 + ```rust 332 + use tauri::{AppHandle, Emitter}; 333 + use serde::Serialize; 334 + 335 + #[derive(Clone, Serialize)] 336 + #[serde(rename_all = "camelCase")] 337 + struct DownloadStarted { 338 + url: String, 339 + download_id: usize, 340 + content_length: usize, 341 + } 342 + 343 + #[tauri::command] 344 + fn start_download(app: AppHandle, url: String) { 345 + app.emit("download-started", DownloadStarted { 346 + url, 347 + download_id: 1, 348 + content_length: 1024, 349 + }).unwrap(); 350 + } 351 + ``` 352 + 353 + ### Webview-Specific Emit 354 + 355 + ```rust 356 + use tauri::{AppHandle, Emitter}; 357 + 358 + #[tauri::command] 359 + fn notify_window(app: AppHandle, window_label: &str, message: String) { 360 + // Emit to a specific webview by label 361 + app.emit_to(window_label, "notification", message).unwrap(); 362 + } 363 + ``` 364 + 365 + ### Filtered Emit 366 + 367 + ```rust 368 + use tauri::{AppHandle, Emitter, EventTarget}; 369 + 370 + #[tauri::command] 371 + fn broadcast_to_editors(app: AppHandle, content: String) { 372 + app.emit_filter("content-update", content, |target| { 373 + match target { 374 + EventTarget::WebviewWindow { label } => { 375 + label.starts_with("editor-") 376 + } 377 + _ => false, 378 + } 379 + }).unwrap(); 380 + } 381 + ``` 382 + 383 + ## Best Practices 384 + 385 + 1. **Always define TypeScript interfaces** for event payloads to catch type mismatches at compile time 386 + 387 + 2. **Use `serde(rename_all = "camelCase")`** in Rust structs to match JavaScript naming conventions 388 + 389 + 3. **Store unlisten functions** and call them during cleanup to prevent memory leaks 390 + 391 + 4. **Use `once` for initialization events** that only fire a single time 392 + 393 + 5. **Match listener scope to emit scope** - use webview-specific listeners for `emit_to` events 394 + 395 + 6. **Centralize event type definitions** in a shared module for consistency 396 + 397 + ## Common Patterns 398 + 399 + ### Event Bus Pattern 400 + 401 + ```typescript 402 + import { listen, Event } from '@tauri-apps/api/event'; 403 + 404 + type EventHandler<T> = (payload: T) => void; 405 + 406 + class TauriEventBus { 407 + private unlisteners: Map<string, () => void> = new Map(); 408 + 409 + async subscribe<T>(event: string, handler: EventHandler<T>): Promise<void> { 410 + if (this.unlisteners.has(event)) { 411 + this.unlisteners.get(event)!(); 412 + } 413 + 414 + const unlisten = await listen<T>(event, (e: Event<T>) => { 415 + handler(e.payload); 416 + }); 417 + 418 + this.unlisteners.set(event, unlisten); 419 + } 420 + 421 + unsubscribe(event: string): void { 422 + const unlisten = this.unlisteners.get(event); 423 + if (unlisten) { 424 + unlisten(); 425 + this.unlisteners.delete(event); 426 + } 427 + } 428 + 429 + unsubscribeAll(): void { 430 + this.unlisteners.forEach((unlisten) => unlisten()); 431 + this.unlisteners.clear(); 432 + } 433 + } 434 + 435 + export const eventBus = new TauriEventBus(); 436 + ``` 437 + 438 + ### React Hook Pattern 439 + 440 + ```typescript 441 + import { useEffect, useState } from 'react'; 442 + import { listen, Event } from '@tauri-apps/api/event'; 443 + 444 + export function useTauriEvent<T>(eventName: string, initialValue: T): T { 445 + const [value, setValue] = useState<T>(initialValue); 446 + 447 + useEffect(() => { 448 + let unlisten: (() => void) | undefined; 449 + 450 + listen<T>(eventName, (event: Event<T>) => { 451 + setValue(event.payload); 452 + }).then((fn) => { 453 + unlisten = fn; 454 + }); 455 + 456 + return () => { 457 + if (unlisten) { 458 + unlisten(); 459 + } 460 + }; 461 + }, [eventName]); 462 + 463 + return value; 464 + } 465 + 466 + // Usage 467 + function StatusDisplay() { 468 + const status = useTauriEvent<string>('app-status', 'initializing'); 469 + return <div>Status: {status}</div>; 470 + } 471 + ```
+387
.agents/skills/managing-tauri-app-resources/SKILL.md
··· 1 + --- 2 + name: managing-tauri-app-resources 3 + description: Assists with managing Tauri application resources including app icons setup and generation, embedding static files and assets, accessing bundled resources at runtime, and implementing thread-safe state management patterns. 4 + --- 5 + 6 + # Managing Tauri App Resources 7 + 8 + ## App Icons 9 + 10 + ### Icon Generation 11 + 12 + Generate all platform-specific icons from a single source file: 13 + 14 + ```bash 15 + cargo tauri icon # Default: ./app-icon.png 16 + cargo tauri icon ./custom.png -o ./icons # Custom source/output 17 + cargo tauri icon --ios-color "#000000" # iOS background color 18 + ``` 19 + 20 + **Source requirements:** Squared PNG or SVG with transparency. 21 + 22 + ### Generated Formats 23 + 24 + | Format | Platform | 25 + |--------|----------| 26 + | `icon.icns` | macOS | 27 + | `icon.ico` | Windows | 28 + | `*.png` | Linux, Android, iOS | 29 + 30 + ### Configuration 31 + 32 + ```json 33 + { 34 + "bundle": { 35 + "icon": [ 36 + "icons/32x32.png", 37 + "icons/128x128.png", 38 + "icons/128x128@2x.png", 39 + "icons/icon.icns", 40 + "icons/icon.ico" 41 + ] 42 + } 43 + } 44 + ``` 45 + 46 + ### Platform Requirements 47 + 48 + **Windows (.ico):** Layers for 16, 24, 32, 48, 64, 256 pixels. 49 + 50 + **Android:** No transparency. Place in `src-tauri/gen/android/app/src/main/res/mipmap-*` folders. Each needs `ic_launcher.png`, `ic_launcher_round.png`, `ic_launcher_foreground.png`. 51 + 52 + **iOS:** No transparency. Place in `src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/`. Required sizes: 20, 29, 40, 60, 76, 83.5 pixels with 1x/2x/3x scales, plus 512x512@2x. 53 + 54 + --- 55 + 56 + ## Embedding Static Resources 57 + 58 + ### Configuration 59 + 60 + **Array syntax** (preserves directory structure): 61 + 62 + ```json 63 + { 64 + "bundle": { 65 + "resources": ["./file.txt", "folder/", "docs/**/*.md"] 66 + } 67 + } 68 + ``` 69 + 70 + **Map syntax** (custom destinations): 71 + 72 + ```json 73 + { 74 + "bundle": { 75 + "resources": { 76 + "path/to/source.json": "resources/dest.json", 77 + "docs/**/*.md": "website-docs/" 78 + } 79 + } 80 + } 81 + ``` 82 + 83 + ### Path Patterns 84 + 85 + | Pattern | Behavior | 86 + |---------|----------| 87 + | `"dir/file.txt"` | Single file | 88 + | `"dir/"` | Directory recursive | 89 + | `"dir/*"` | Files non-recursive | 90 + | `"dir/**/*"` | All files recursive | 91 + 92 + ### Accessing Resources - Rust 93 + 94 + ```rust 95 + use tauri::Manager; 96 + use tauri::path::BaseDirectory; 97 + 98 + #[tauri::command] 99 + fn load_resource(handle: tauri::AppHandle) -> Result<String, String> { 100 + let path = handle 101 + .path() 102 + .resolve("lang/de.json", BaseDirectory::Resource) 103 + .map_err(|e| e.to_string())?; 104 + std::fs::read_to_string(&path).map_err(|e| e.to_string()) 105 + } 106 + ``` 107 + 108 + ### Accessing Resources - JavaScript 109 + 110 + ```javascript 111 + import { resolveResource } from '@tauri-apps/api/path'; 112 + import { readTextFile } from '@tauri-apps/plugin-fs'; 113 + 114 + const resourcePath = await resolveResource('lang/de.json'); 115 + const content = await readTextFile(resourcePath); 116 + const data = JSON.parse(content); 117 + ``` 118 + 119 + ### Permissions 120 + 121 + ```json 122 + { 123 + "permissions": [ 124 + "fs:allow-read-text-file", 125 + "fs:allow-resource-read-recursive" 126 + ] 127 + } 128 + ``` 129 + 130 + Use `$RESOURCE/**/*` scope for recursive access. 131 + 132 + --- 133 + 134 + ## State Management 135 + 136 + ### Basic Setup 137 + 138 + ```rust 139 + use tauri::{Builder, Manager}; 140 + 141 + struct AppData { 142 + welcome_message: &'static str, 143 + } 144 + 145 + fn main() { 146 + Builder::default() 147 + .setup(|app| { 148 + app.manage(AppData { 149 + welcome_message: "Welcome!", 150 + }); 151 + Ok(()) 152 + }) 153 + .run(tauri::generate_context!()) 154 + .unwrap() 155 + } 156 + ``` 157 + 158 + ### Thread-Safe Mutable State 159 + 160 + ```rust 161 + use std::sync::Mutex; 162 + use tauri::{Builder, Manager}; 163 + 164 + #[derive(Default)] 165 + struct AppState { 166 + counter: u32, 167 + } 168 + 169 + fn main() { 170 + Builder::default() 171 + .setup(|app| { 172 + app.manage(Mutex::new(AppState::default())); 173 + Ok(()) 174 + }) 175 + .run(tauri::generate_context!()) 176 + .unwrap() 177 + } 178 + ``` 179 + 180 + ### Accessing State in Commands 181 + 182 + ```rust 183 + use std::sync::Mutex; 184 + use tauri::State; 185 + 186 + #[tauri::command] 187 + fn increase_counter(state: State<'_, Mutex<AppState>>) -> u32 { 188 + let mut state = state.lock().unwrap(); 189 + state.counter += 1; 190 + state.counter 191 + } 192 + 193 + #[tauri::command] 194 + fn get_counter(state: State<'_, Mutex<AppState>>) -> u32 { 195 + state.lock().unwrap().counter 196 + } 197 + ``` 198 + 199 + ### Async Commands with Tokio Mutex 200 + 201 + ```rust 202 + use tokio::sync::Mutex; 203 + use tauri::State; 204 + 205 + #[tauri::command] 206 + async fn increase_counter_async( 207 + state: State<'_, Mutex<AppState>> 208 + ) -> Result<u32, ()> { 209 + let mut state = state.lock().await; 210 + state.counter += 1; 211 + Ok(state.counter) 212 + } 213 + ``` 214 + 215 + ### Accessing State Outside Commands 216 + 217 + ```rust 218 + use std::sync::Mutex; 219 + use tauri::{Manager, Window, WindowEvent}; 220 + 221 + fn on_window_event(window: &Window, event: &WindowEvent) { 222 + let app_handle = window.app_handle(); 223 + let state = app_handle.state::<Mutex<AppState>>(); 224 + let mut state = state.lock().unwrap(); 225 + state.counter += 1; 226 + } 227 + ``` 228 + 229 + ### Type Alias Pattern 230 + 231 + Prevent runtime panics from type mismatches: 232 + 233 + ```rust 234 + use std::sync::Mutex; 235 + 236 + struct AppStateInner { 237 + counter: u32, 238 + } 239 + 240 + type AppState = Mutex<AppStateInner>; 241 + 242 + #[tauri::command] 243 + fn get_counter(state: State<'_, AppState>) -> u32 { 244 + state.lock().unwrap().counter 245 + } 246 + ``` 247 + 248 + ### Multiple State Types 249 + 250 + ```rust 251 + use std::sync::Mutex; 252 + use tauri::{Builder, Manager, State}; 253 + 254 + struct UserState { username: Option<String> } 255 + struct AppSettings { theme: String } 256 + 257 + fn main() { 258 + Builder::default() 259 + .setup(|app| { 260 + app.manage(Mutex::new(UserState { username: None })); 261 + app.manage(Mutex::new(AppSettings { theme: "dark".into() })); 262 + Ok(()) 263 + }) 264 + .run(tauri::generate_context!()) 265 + .unwrap() 266 + } 267 + 268 + #[tauri::command] 269 + fn login(user_state: State<'_, Mutex<UserState>>, username: String) { 270 + user_state.lock().unwrap().username = Some(username); 271 + } 272 + 273 + #[tauri::command] 274 + fn set_theme(settings: State<'_, Mutex<AppSettings>>, theme: String) { 275 + settings.lock().unwrap().theme = theme; 276 + } 277 + ``` 278 + 279 + ### Key Points 280 + 281 + - **Arc not required** - Tauri handles reference counting internally 282 + - **Use std::sync::Mutex** for most cases; Tokio's mutex only for holding locks across await points 283 + - **Type safety** - Wrong state types cause runtime panics, not compile errors; use type aliases 284 + 285 + --- 286 + 287 + ## Complete Example 288 + 289 + **tauri.conf.json:** 290 + 291 + ```json 292 + { 293 + "bundle": { 294 + "icon": [ 295 + "icons/32x32.png", 296 + "icons/128x128.png", 297 + "icons/icon.icns", 298 + "icons/icon.ico" 299 + ], 300 + "resources": { 301 + "assets/config.json": "config.json", 302 + "assets/translations/": "lang/" 303 + } 304 + } 305 + } 306 + ``` 307 + 308 + **src-tauri/src/main.rs:** 309 + 310 + ```rust 311 + use std::sync::Mutex; 312 + use serde::{Deserialize, Serialize}; 313 + use tauri::{Builder, Manager, State}; 314 + use tauri::path::BaseDirectory; 315 + 316 + #[derive(Default)] 317 + struct AppState { counter: u32, locale: String } 318 + type ManagedState = Mutex<AppState>; 319 + 320 + #[derive(Serialize, Deserialize)] 321 + struct Config { app_name: String, version: String } 322 + 323 + #[tauri::command] 324 + fn increment(state: State<'_, ManagedState>) -> u32 { 325 + let mut s = state.lock().unwrap(); 326 + s.counter += 1; 327 + s.counter 328 + } 329 + 330 + #[tauri::command] 331 + fn load_config(handle: tauri::AppHandle) -> Result<Config, String> { 332 + let path = handle.path() 333 + .resolve("config.json", BaseDirectory::Resource) 334 + .map_err(|e| e.to_string())?; 335 + let content = std::fs::read_to_string(&path).map_err(|e| e.to_string())?; 336 + serde_json::from_str(&content).map_err(|e| e.to_string()) 337 + } 338 + 339 + fn main() { 340 + Builder::default() 341 + .setup(|app| { 342 + app.manage(Mutex::new(AppState::default())); 343 + Ok(()) 344 + }) 345 + .invoke_handler(tauri::generate_handler![increment, load_config]) 346 + .run(tauri::generate_context!()) 347 + .unwrap() 348 + } 349 + ``` 350 + 351 + **Frontend:** 352 + 353 + ```javascript 354 + import { invoke } from '@tauri-apps/api/core'; 355 + import { resolveResource } from '@tauri-apps/api/path'; 356 + import { readTextFile } from '@tauri-apps/plugin-fs'; 357 + 358 + const newValue = await invoke('increment'); 359 + const config = await invoke('load_config'); 360 + const langPath = await resolveResource('lang/en.json'); 361 + const translations = JSON.parse(await readTextFile(langPath)); 362 + ``` 363 + 364 + --- 365 + 366 + ## Quick Reference 367 + 368 + ### Icon Commands 369 + ```bash 370 + cargo tauri icon # Generate from ./app-icon.png 371 + cargo tauri icon ./icon.png -o out # Custom source/output 372 + ``` 373 + 374 + ### Resource Patterns 375 + ```json 376 + { "resources": ["data.json"] } // Single file 377 + { "resources": ["assets/"] } // Directory recursive 378 + { "resources": { "src/x.json": "x.json" }} // Custom destination 379 + ``` 380 + 381 + ### State Patterns 382 + ```rust 383 + app.manage(Config { ... }); // Immutable 384 + app.manage(Mutex::new(State { ... })); // Mutable 385 + fn cmd(state: State<'_, Mutex<T>>) // In command 386 + app_handle.state::<Mutex<T>>() // Via AppHandle 387 + ```
+449
.agents/skills/managing-tauri-plugin-permissions/SKILL.md
··· 1 + --- 2 + name: managing-tauri-plugin-permissions 3 + description: Guides users through configuring Tauri plugin permissions, capabilities, and security. Covers platform-specific capabilities, window-targeted permissions, using official and community plugin permissions, and writing custom plugin permissions with scopes. 4 + --- 5 + 6 + # Managing Tauri Plugin Permissions 7 + 8 + Tauri's capability and permission system provides granular security control for desktop and mobile applications. This skill covers configuring capabilities for windows and platforms, using plugin permissions effectively, and writing custom plugin permissions. 9 + 10 + ## Core Concepts 11 + 12 + ### Capabilities 13 + 14 + Capabilities are JSON configuration files that assign permissions to specific windows and platforms. They follow the principle of least privilege. 15 + 16 + **Location**: `src-tauri/capabilities/` 17 + 18 + **Structure**: 19 + ```json 20 + { 21 + "identifier": "capability-name", 22 + "description": "Human-readable purpose", 23 + "local": true, 24 + "windows": ["window-label"], 25 + "permissions": ["plugin:allow-action"], 26 + "platforms": ["linux", "windows", "macos", "android", "ios"] 27 + } 28 + ``` 29 + 30 + ### Permission Levels 31 + 32 + Permissions operate at two levels: 33 + - **Commands**: Individual operations (e.g., `allow-write-text-file`) 34 + - **Scopes**: Path-based restrictions defining accessible files/directories 35 + 36 + ## Platform-Specific Capabilities 37 + 38 + ### Targeting Platforms 39 + 40 + Restrict capabilities to specific operating systems using the `platforms` field: 41 + 42 + ```json 43 + { 44 + "identifier": "desktop-fs-access", 45 + "description": "Filesystem access for desktop platforms", 46 + "windows": ["main"], 47 + "permissions": ["fs:allow-home-read"], 48 + "platforms": ["linux", "windows", "macos"] 49 + } 50 + ``` 51 + 52 + Available platforms: `linux`, `windows`, `macos`, `android`, `ios` 53 + 54 + ### Mobile-Only Capabilities 55 + 56 + ```json 57 + { 58 + "identifier": "mobile-camera", 59 + "description": "Camera access for mobile devices", 60 + "windows": ["main"], 61 + "permissions": ["camera:allow-capture"], 62 + "platforms": ["android", "ios"] 63 + } 64 + ``` 65 + 66 + ## Window-Specific Capabilities 67 + 68 + ### Configuring Multiple Windows 69 + 70 + Define windows in `tauri.conf.json`: 71 + 72 + ```json 73 + { 74 + "windows": [ 75 + { 76 + "label": "main", 77 + "title": "Main Window", 78 + "width": 800, 79 + "height": 600 80 + }, 81 + { 82 + "label": "settings", 83 + "title": "Settings", 84 + "width": 400, 85 + "height": 300 86 + } 87 + ] 88 + } 89 + ``` 90 + 91 + ### Assigning Capabilities to Windows 92 + 93 + Create separate capability files for each window's needs: 94 + 95 + **`src-tauri/capabilities/main-window.json`**: 96 + ```json 97 + { 98 + "identifier": "main-window-capabilities", 99 + "description": "Full access for main window", 100 + "local": true, 101 + "windows": ["main"], 102 + "permissions": [ 103 + "fs:allow-home-read", 104 + "fs:allow-home-write", 105 + "dialog:allow-open", 106 + "dialog:allow-save" 107 + ] 108 + } 109 + ``` 110 + 111 + **`src-tauri/capabilities/settings-window.json`**: 112 + ```json 113 + { 114 + "identifier": "settings-window-capabilities", 115 + "description": "Limited access for settings window", 116 + "local": true, 117 + "windows": ["settings"], 118 + "permissions": [ 119 + "fs:allow-app-read", 120 + "fs:allow-app-write" 121 + ] 122 + } 123 + ``` 124 + 125 + ### Shared Capabilities Across Windows 126 + 127 + ```json 128 + { 129 + "identifier": "shared-dialog", 130 + "description": "Dialog access for multiple windows", 131 + "local": true, 132 + "windows": ["main", "settings"], 133 + "permissions": ["dialog:allow-ask", "dialog:allow-message"] 134 + } 135 + ``` 136 + 137 + ## Using Plugin Permissions 138 + 139 + ### Default Permission Sets 140 + 141 + Every plugin provides a `default` permission set with baseline access. Enable it with: 142 + 143 + ```json 144 + { 145 + "permissions": ["plugin-name:default"] 146 + } 147 + ``` 148 + 149 + ### Finding Available Permissions 150 + 151 + 1. **Official plugins**: Check Tauri documentation's permission tables 152 + 2. **Plugin source**: Look in `permissions/autogenerated` directories 153 + 3. **Community plugins**: Check repository or crates.io page 154 + 155 + ### Permission Identifier Format 156 + 157 + ``` 158 + plugin-name:permission-name 159 + ``` 160 + 161 + Examples: 162 + - `fs:allow-read` 163 + - `fs:allow-write-text-file` 164 + - `dialog:allow-open` 165 + - `shell:allow-spawn` 166 + 167 + ### Configuring with Scopes 168 + 169 + Restrict permissions to specific paths: 170 + 171 + ```json 172 + { 173 + "identifier": "default", 174 + "description": "Main window capabilities", 175 + "windows": ["main"], 176 + "permissions": [ 177 + "fs:allow-write-text-file", 178 + { 179 + "identifier": "fs:allow-read", 180 + "allow": [{ "path": "$HOME/Documents/**" }] 181 + }, 182 + { 183 + "identifier": "fs:allow-write", 184 + "allow": [{ "path": "$APP/**" }] 185 + } 186 + ] 187 + } 188 + ``` 189 + 190 + ### Common Scope Variables 191 + 192 + | Variable | Description | 193 + |----------|-------------| 194 + | `$APP` | Application data directory | 195 + | `$HOME` | User home directory | 196 + | `$RESOURCE` | Application resources | 197 + | `$TEMP` | Temporary directory | 198 + | `$DESKTOP` | User desktop | 199 + | `$DOCUMENT` | User documents | 200 + | `$DOWNLOAD` | User downloads | 201 + 202 + ### Deny Permissions 203 + 204 + Explicitly deny specific operations: 205 + 206 + ```json 207 + { 208 + "permissions": [ 209 + "fs:default", 210 + "fs:deny-write-text-file" 211 + ] 212 + } 213 + ``` 214 + 215 + ## Writing Custom Plugin Permissions 216 + 217 + ### Plugin Structure 218 + 219 + Create a plugin with the Tauri CLI: 220 + 221 + ```bash 222 + cargo tauri plugin new my-plugin 223 + cd tauri-plugin-my-plugin 224 + ``` 225 + 226 + ### Implementing Commands 227 + 228 + **`src/commands.rs`**: 229 + ```rust 230 + use tauri::{command, AppHandle, Runtime}; 231 + 232 + #[command] 233 + pub(crate) async fn read_data<R: Runtime>( 234 + key: String, 235 + app: AppHandle<R>, 236 + ) -> Result<String, String> { 237 + // Implementation 238 + Ok(format!("Data for key: {}", key)) 239 + } 240 + 241 + #[command] 242 + pub(crate) async fn write_data<R: Runtime>( 243 + key: String, 244 + value: String, 245 + app: AppHandle<R>, 246 + ) -> Result<(), String> { 247 + // Implementation 248 + Ok(()) 249 + } 250 + 251 + #[command] 252 + pub(crate) async fn delete_data<R: Runtime>( 253 + key: String, 254 + app: AppHandle<R>, 255 + ) -> Result<(), String> { 256 + // Implementation 257 + Ok(()) 258 + } 259 + ``` 260 + 261 + ### Auto-Generating Permissions 262 + 263 + **`src/build.rs`**: 264 + ```rust 265 + const COMMANDS: &[&str] = &["read_data", "write_data", "delete_data"]; 266 + 267 + fn main() { 268 + tauri_plugin::Builder::new(COMMANDS) 269 + .global_api_script_path("./api-iife.js") 270 + .build(); 271 + } 272 + ``` 273 + 274 + This generates: 275 + - `allow-read-data` / `deny-read-data` 276 + - `allow-write-data` / `deny-write-data` 277 + - `allow-delete-data` / `deny-delete-data` 278 + 279 + ### Defining Default Permissions 280 + 281 + **`permissions/default.toml`**: 282 + ```toml 283 + "$schema" = "schemas/schema.json" 284 + 285 + [default] 286 + description = "Default permissions for my-plugin. Allows read operations only." 287 + permissions = ["allow-read-data"] 288 + ``` 289 + 290 + ### Creating Permission Sets 291 + 292 + **`permissions/read-write.toml`**: 293 + ```toml 294 + "$schema" = "schemas/schema.json" 295 + 296 + [[set]] 297 + identifier = "read-write" 298 + description = "Allows both read and write operations" 299 + permissions = ["allow-read-data", "allow-write-data"] 300 + ``` 301 + 302 + **`permissions/full-access.toml`**: 303 + ```toml 304 + "$schema" = "schemas/schema.json" 305 + 306 + [[set]] 307 + identifier = "full-access" 308 + description = "Allows all operations including delete" 309 + permissions = ["allow-read-data", "allow-write-data", "allow-delete-data"] 310 + ``` 311 + 312 + ### Registering Commands 313 + 314 + **`src/lib.rs`**: 315 + ```rust 316 + use tauri::{ 317 + plugin::{Builder, TauriPlugin}, 318 + Manager, Runtime, 319 + }; 320 + 321 + mod commands; 322 + 323 + pub fn init<R: Runtime>() -> TauriPlugin<R> { 324 + Builder::new("my-plugin") 325 + .invoke_handler(tauri::generate_handler![ 326 + commands::read_data, 327 + commands::write_data, 328 + commands::delete_data, 329 + ]) 330 + .build() 331 + } 332 + ``` 333 + 334 + ### Frontend JavaScript Bindings 335 + 336 + **`guest-js/index.ts`**: 337 + ```typescript 338 + import { invoke } from '@tauri-apps/api/core'; 339 + 340 + export async function readData(key: string): Promise<string> { 341 + return await invoke('plugin:my-plugin|read_data', { key }); 342 + } 343 + 344 + export async function writeData(key: string, value: string): Promise<void> { 345 + return await invoke('plugin:my-plugin|write_data', { key, value }); 346 + } 347 + 348 + export async function deleteData(key: string): Promise<void> { 349 + return await invoke('plugin:my-plugin|delete_data', { key }); 350 + } 351 + ``` 352 + 353 + ### Using Custom Plugin Permissions 354 + 355 + In your application's capability file: 356 + 357 + ```json 358 + { 359 + "identifier": "default", 360 + "windows": ["main"], 361 + "permissions": [ 362 + "my-plugin:default", 363 + "my-plugin:read-write", 364 + "my-plugin:allow-delete-data" 365 + ] 366 + } 367 + ``` 368 + 369 + ## Complete Example: Cross-Platform App 370 + 371 + **`src-tauri/capabilities/desktop.json`**: 372 + ```json 373 + { 374 + "$schema": "../gen/schemas/desktop-schema.json", 375 + "identifier": "desktop", 376 + "windows": ["main"], 377 + "platforms": ["linux", "windows", "macos"], 378 + "permissions": [ 379 + "core:default", 380 + "fs:default", 381 + { "identifier": "fs:allow-read", "allow": [{ "path": "$HOME/Documents/**" }] }, 382 + { "identifier": "fs:allow-write", "allow": [{ "path": "$APP/**" }] }, 383 + "dialog:allow-open", 384 + "dialog:allow-save", 385 + "shell:allow-open" 386 + ] 387 + } 388 + ``` 389 + 390 + **`src-tauri/capabilities/mobile.json`**: 391 + ```json 392 + { 393 + "identifier": "mobile", 394 + "windows": ["main"], 395 + "platforms": ["android", "ios"], 396 + "permissions": [ 397 + "fs:allow-app-read", 398 + "fs:allow-app-write", 399 + "notification:default" 400 + ] 401 + } 402 + ``` 403 + 404 + ## Best Practices 405 + 406 + ### Security 407 + 408 + 1. **Principle of least privilege**: Grant only required permissions 409 + 2. **Use scopes**: Restrict file access to specific paths rather than broad permissions 410 + 3. **Separate by window**: Each window should have only the permissions it needs 411 + 4. **Platform targeting**: Avoid granting mobile permissions on desktop and vice versa 412 + 413 + ### Organization 414 + 415 + 1. **Organize by function**: Group capabilities by feature area 416 + 2. **Use descriptive identifiers**: Make capability purposes clear 417 + 3. **Document permissions**: Include descriptions explaining why each permission is needed 418 + 419 + ### Plugin Development 420 + 421 + 1. **Minimal defaults**: Default permission sets should be restrictive 422 + 2. **Create permission sets**: Offer tiered access levels (read-only, read-write, full) 423 + 3. **Use auto-generation**: Let Tauri generate allow/deny permissions for commands 424 + 4. **Test permissions**: Verify permission behavior with example applications 425 + 426 + ## Troubleshooting 427 + 428 + ### Permission Denied Errors 429 + 430 + If you encounter permission errors: 431 + 432 + 1. Check capability file syntax (valid JSON) 433 + 2. Verify the window label matches your configuration 434 + 3. Confirm the permission identifier is correct 435 + 4. Check if a scope is required for path-based operations 436 + 437 + ### Capability Not Applied 438 + 439 + 1. Ensure capability files are in `src-tauri/capabilities/` 440 + 2. Verify the `windows` array contains the correct window labels 441 + 3. Check `platforms` includes your target OS 442 + 4. Rebuild the application after capability changes 443 + 444 + ## References 445 + 446 + - [Tauri Capabilities Documentation](https://v2.tauri.app/learn/security/capabilities-for-windows-and-platforms) 447 + - [Using Plugin Permissions](https://v2.tauri.app/learn/security/using-plugin-permissions) 448 + - [Writing Plugin Permissions](https://v2.tauri.app/learn/security/writing-plugin-permissions) 449 + - [Capability Reference](https://v2.tauri.app/reference/acl/capability/)
+241
.agents/skills/optimizing-tauri-binary-size/SKILL.md
··· 1 + --- 2 + name: optimizing-tauri-binary-size 3 + description: Guides users through Tauri binary size optimization techniques to produce small, efficient desktop application bundles using Cargo profile settings and build configurations. 4 + --- 5 + 6 + # Tauri Binary Size Optimization 7 + 8 + This skill provides guidance on optimizing Tauri application binary sizes for production releases. 9 + 10 + ## Why Tauri Produces Small Binaries 11 + 12 + Tauri is designed from the ground up to produce minimal binaries: 13 + 14 + 1. **Native Webview**: Uses the operating system's native webview instead of bundling Chromium (unlike Electron) 15 + 2. **Rust Backend**: Compiles to efficient native code with no runtime overhead 16 + 3. **Tree Shaking**: Only includes code that is actually used 17 + 4. **No V8 Engine**: Leverages existing system components rather than bundling a JavaScript engine 18 + 19 + ### Size Comparison 20 + 21 + | Framework | Minimum Binary Size | 22 + |-----------|---------------------| 23 + | Tauri | ~3-6 MB | 24 + | Electron | ~120-180 MB | 25 + | NW.js | ~80-100 MB | 26 + 27 + The dramatic size difference comes from Tauri's architectural decision to use native system webviews rather than bundling a full browser engine. 28 + 29 + ## Cargo.toml Optimization Settings 30 + 31 + Configure release profile settings in `src-tauri/Cargo.toml` to minimize binary size. 32 + 33 + ### Recommended Stable Toolchain Configuration 34 + 35 + ```toml 36 + [profile.release] 37 + codegen-units = 1 # Compile crates one at a time for better LLVM optimization 38 + lto = true # Enable link-time optimization across all crates 39 + opt-level = "s" # Optimize for binary size (alternative: "z" for even smaller) 40 + panic = "abort" # Remove panic unwinding code 41 + strip = true # Strip debug symbols from final binary 42 + ``` 43 + 44 + ### Configuration Options Explained 45 + 46 + | Option | Values | Description | 47 + |--------|--------|-------------| 48 + | `codegen-units` | `1` | Reduces parallelism but allows LLVM to perform better whole-program optimization | 49 + | `lto` | `true`, `"thin"`, `"fat"` | Link-time optimization; `true` or `"fat"` produces smallest binaries | 50 + | `opt-level` | `"s"`, `"z"`, `"3"` | `"s"` balances size/speed, `"z"` prioritizes size, `"3"` prioritizes speed | 51 + | `panic` | `"abort"` | Removes panic handler code, reducing binary size | 52 + | `strip` | `true`, `"symbols"`, `"debuginfo"` | Removes symbols and debug information from binary | 53 + 54 + ### Nightly Toolchain Options 55 + 56 + For projects using the nightly Rust toolchain, additional optimizations are available: 57 + 58 + ```toml 59 + [profile.release] 60 + codegen-units = 1 61 + lto = true 62 + opt-level = "s" 63 + panic = "abort" 64 + strip = true 65 + trim-paths = "all" # Remove file path information from binary 66 + 67 + [profile.release.build-override] 68 + opt-level = "s" # Also optimize build scripts 69 + ``` 70 + 71 + You can also set rustflags for additional control: 72 + 73 + ```toml 74 + [profile.release] 75 + rustflags = ["-Cdebuginfo=0", "-Zthreads=8"] 76 + ``` 77 + 78 + ## Tauri Build Configuration 79 + 80 + ### Remove Unused Commands (Tauri 2.4+) 81 + 82 + Tauri 2.4 introduced the ability to automatically remove code for commands not permitted in your Access Control List (ACL). Add this to `tauri.conf.json`: 83 + 84 + ```json 85 + { 86 + "build": { 87 + "removeUnusedCommands": true 88 + } 89 + } 90 + ``` 91 + 92 + This feature: 93 + - Analyzes your ACL configuration 94 + - Removes Tauri command handlers that are not allowed 95 + - Reduces binary size without changing functionality 96 + - Works automatically during release builds 97 + 98 + ### Minimal Feature Set 99 + 100 + Only enable Tauri features you actually need in `src-tauri/Cargo.toml`: 101 + 102 + ```toml 103 + [dependencies] 104 + tauri = { version = "2", features = ["macos-private-api"] } 105 + # Avoid enabling unnecessary features like: 106 + # - "devtools" in production 107 + # - "protocol-asset" if not serving local assets 108 + # - "tray-icon" if not using system tray 109 + ``` 110 + 111 + ## Frontend Optimization 112 + 113 + While this skill focuses on Rust/Tauri optimization, frontend bundle size also affects the final application: 114 + 115 + 1. **Use a bundler**: Vite, webpack, or similar with tree shaking 116 + 2. **Code splitting**: Load features on demand 117 + 3. **Minimize dependencies**: Audit and remove unused npm packages 118 + 4. **Compress assets**: Optimize images and other static assets 119 + 120 + ## Build Commands 121 + 122 + ### Standard Release Build 123 + 124 + ```bash 125 + cd src-tauri 126 + cargo tauri build --release 127 + ``` 128 + 129 + ### Using Nightly Toolchain 130 + 131 + ```bash 132 + cd src-tauri 133 + cargo +nightly tauri build --release 134 + ``` 135 + 136 + ### Check Binary Size 137 + 138 + After building, check your binary size: 139 + 140 + ```bash 141 + # macOS 142 + ls -lh src-tauri/target/release/bundle/macos/*.app/Contents/MacOS/* 143 + 144 + # Linux 145 + ls -lh src-tauri/target/release/bundle/appimage/*.AppImage 146 + 147 + # Windows 148 + dir src-tauri\target\release\bundle\msi\*.msi 149 + ``` 150 + 151 + ## Complete Example Configuration 152 + 153 + Here is a complete `src-tauri/Cargo.toml` optimized for minimal binary size: 154 + 155 + ```toml 156 + [package] 157 + name = "my-tauri-app" 158 + version = "0.1.0" 159 + edition = "2021" 160 + 161 + [dependencies] 162 + tauri = { version = "2", features = [] } 163 + serde = { version = "1", features = ["derive"] } 164 + serde_json = "1" 165 + 166 + [build-dependencies] 167 + tauri-build = { version = "2", features = [] } 168 + 169 + [profile.release] 170 + codegen-units = 1 171 + lto = true 172 + opt-level = "s" 173 + panic = "abort" 174 + strip = true 175 + 176 + [profile.release.package."*"] 177 + opt-level = "s" 178 + ``` 179 + 180 + And corresponding `tauri.conf.json`: 181 + 182 + ```json 183 + { 184 + "productName": "my-tauri-app", 185 + "version": "0.1.0", 186 + "identifier": "com.example.my-tauri-app", 187 + "build": { 188 + "removeUnusedCommands": true, 189 + "beforeBuildCommand": "npm run build", 190 + "frontendDist": "../dist" 191 + }, 192 + "bundle": { 193 + "active": true, 194 + "targets": "all", 195 + "icon": ["icons/icon.png"] 196 + } 197 + } 198 + ``` 199 + 200 + ## Optimization Trade-offs 201 + 202 + | Setting | Size Impact | Build Time | Runtime Performance | 203 + |---------|-------------|------------|---------------------| 204 + | `codegen-units = 1` | Smaller | Slower | Better | 205 + | `lto = true` | Smaller | Much slower | Better | 206 + | `opt-level = "s"` | Smaller | Similar | Slightly slower | 207 + | `opt-level = "z"` | Smallest | Similar | Slower | 208 + | `panic = "abort"` | Smaller | Faster | No unwinding | 209 + | `strip = true` | Smaller | Similar | No impact | 210 + 211 + ## Troubleshooting 212 + 213 + ### Binary Still Large 214 + 215 + 1. Check for debug builds: Ensure you are using `--release` flag 216 + 2. Audit dependencies: Run `cargo tree` to see dependency graph 217 + 3. Check for duplicate dependencies: Different versions of same crate 218 + 4. Verify strip is working: Use `file` command to check for debug symbols 219 + 220 + ### Build Failures with LTO 221 + 222 + If `lto = true` causes build failures: 223 + - Try `lto = "thin"` as a fallback 224 + - Ensure sufficient memory (LTO is memory-intensive) 225 + - Update Rust toolchain to latest version 226 + 227 + ### Nightly Features Not Working 228 + 229 + Ensure nightly is installed and active: 230 + 231 + ```bash 232 + rustup install nightly 233 + rustup default nightly 234 + # Or use +nightly flag with cargo commands 235 + ``` 236 + 237 + ## References 238 + 239 + - [Tauri Size Documentation](https://v2.tauri.app/concept/size) 240 + - [Cargo Profile Settings](https://doc.rust-lang.org/cargo/reference/profiles.html) 241 + - [Rust Performance Book](https://nnethercote.github.io/perf-book/)
+390
.agents/skills/packaging-tauri-for-linux/SKILL.md
··· 1 + --- 2 + name: packaging-tauri-for-linux 3 + description: Guides users through packaging Tauri v2 applications for Linux distributions including AppImage, Debian (.deb), RPM, Flatpak, Snap, and AUR submission. 4 + --- 5 + 6 + # Packaging Tauri Applications for Linux 7 + 8 + ## Critical Build Requirement 9 + 10 + **glibc Compatibility**: Build on the oldest base system you intend to support (e.g., Ubuntu 18.04 rather than 22.04). Use Docker or GitHub Actions for reproducible builds. 11 + 12 + ## Format Overview 13 + 14 + | Format | Use Case | Auto-Update | Installation | 15 + |--------|----------|-------------|--------------| 16 + | AppImage | Universal, portable | Manual | Run directly | 17 + | Debian | Debian/Ubuntu | Via repos | `dpkg -i` | 18 + | RPM | Fedora/RHEL/openSUSE | Via repos | `rpm -i` | 19 + | Flatpak | Sandboxed, Flathub | Built-in | `flatpak install` | 20 + | Snap | Ubuntu Store | Built-in | `snap install` | 21 + | AUR | Arch Linux | Via helpers | `makepkg -si` | 22 + 23 + --- 24 + 25 + ## AppImage 26 + 27 + Self-contained executable bundles requiring no installation. 28 + 29 + ```json 30 + { 31 + "bundle": { 32 + "linux": { 33 + "appimage": { 34 + "bundleMediaFramework": true, 35 + "files": { "/usr/share/README.md": "../README.md" } 36 + } 37 + } 38 + } 39 + } 40 + ``` 41 + 42 + | Option | Description | 43 + |--------|-------------| 44 + | `bundleMediaFramework` | Include GStreamer (increases size, licensing concerns with `ugly` plugins) | 45 + | `files` | Additional files (paths must start with `/usr/`) | 46 + 47 + ```bash 48 + npm run tauri build -- --bundles appimage 49 + ``` 50 + 51 + **ARM**: No cross-compilation support. Use GitHub `ubuntu-22.04-arm` runners. 52 + 53 + --- 54 + 55 + ## Debian Packages (.deb) 56 + 57 + ```json 58 + { 59 + "bundle": { 60 + "linux": { 61 + "deb": { 62 + "depends": ["libssl3", "libasound2"], 63 + "files": { "/usr/share/doc/myapp/README": "../README.md" }, 64 + "section": "utils", 65 + "priority": "optional" 66 + } 67 + } 68 + } 69 + } 70 + ``` 71 + 72 + Default dependencies: `libwebkit2gtk-4.1-0`, `libgtk-3-0`, `libappindicator3-1` (if tray used). 73 + 74 + ```bash 75 + npm run tauri build -- --bundles deb 76 + ``` 77 + 78 + ### ARM Cross-Compilation 79 + 80 + ```bash 81 + # ARM64 82 + rustup target add aarch64-unknown-linux-gnu 83 + sudo apt install gcc-aarch64-linux-gnu 84 + ``` 85 + 86 + `.cargo/config.toml`: 87 + ```toml 88 + [target.aarch64-unknown-linux-gnu] 89 + linker = "aarch64-linux-gnu-gcc" 90 + ``` 91 + 92 + ```bash 93 + PKG_CONFIG_SYSROOT_DIR=/usr/aarch64-linux-gnu cargo tauri build --target aarch64-unknown-linux-gnu 94 + ``` 95 + 96 + --- 97 + 98 + ## RPM Packages 99 + 100 + ```json 101 + { 102 + "bundle": { 103 + "linux": { 104 + "rpm": { 105 + "depends": ["openssl"], 106 + "conflicts": ["oldapp"], 107 + "obsoletes": ["legacyapp < 2.0"], 108 + "provides": ["myapp-bin"], 109 + "license": "MIT", 110 + "preInstallScript": "scripts/pre-install.sh", 111 + "postInstallScript": "scripts/post-install.sh" 112 + } 113 + } 114 + } 115 + } 116 + ``` 117 + 118 + Scripts receive: `1` (install), `2` (upgrade), `0` (uninstall). 119 + 120 + ```bash 121 + npm run tauri build -- --bundles rpm 122 + ``` 123 + 124 + ### Signing 125 + 126 + ```bash 127 + gpg --gen-key && gpg --export-secret-keys --armor > private.key 128 + export TAURI_SIGNING_RPM_KEY=$(cat private.key) 129 + export TAURI_SIGNING_RPM_KEY_PASSPHRASE="passphrase" 130 + ``` 131 + 132 + ### Debug Commands 133 + 134 + ```bash 135 + rpm -qip pkg.rpm # Info 136 + rpm -qlp pkg.rpm # Files 137 + rpm -qp --scripts pkg.rpm # Scripts 138 + rpm -Kv pkg.rpm # Verify signature 139 + ``` 140 + 141 + --- 142 + 143 + ## Flatpak 144 + 145 + ### Prerequisites 146 + 147 + ```bash 148 + sudo apt install flatpak flatpak-builder # Ubuntu/Debian 149 + flatpak install flathub org.gnome.Platform//46 org.gnome.Sdk//46 150 + ``` 151 + 152 + ### Manifest (flatpak-builder.yaml) 153 + 154 + ```yaml 155 + id: com.example.myapp 156 + runtime: org.gnome.Platform 157 + runtime-version: '46' 158 + sdk: org.gnome.Sdk 159 + command: myapp 160 + 161 + finish-args: 162 + - --socket=wayland 163 + - --socket=fallback-x11 164 + - --device=dri 165 + - --share=ipc 166 + - --share=network 167 + - --talk-name=org.kde.StatusNotifierWatcher 168 + - --filesystem=xdg-run/tray-icon:create 169 + 170 + modules: 171 + - name: binary 172 + buildsystem: simple 173 + sources: 174 + - type: file 175 + path: flatpak.metainfo.xml 176 + - type: file 177 + url: https://github.com/user/repo/releases/download/v1.0.0/myapp_1.0.0_amd64.deb 178 + sha256: <hash> 179 + only-arches: [x86_64] 180 + build-commands: 181 + - mkdir deb-extract && ar -x *.deb --output deb-extract 182 + - tar -C deb-extract -xf deb-extract/data.tar.gz 183 + - install -Dm755 deb-extract/usr/bin/myapp /app/bin/myapp 184 + - sed -i 's/^Icon=.*/Icon=com.example.myapp/' deb-extract/usr/share/applications/myapp.desktop 185 + - install -Dm644 deb-extract/usr/share/applications/myapp.desktop /app/share/applications/com.example.myapp.desktop 186 + - install -Dm644 deb-extract/usr/share/icons/hicolor/128x128/apps/myapp.png /app/share/icons/hicolor/128x128/apps/com.example.myapp.png 187 + - install -Dm644 flatpak.metainfo.xml /app/share/metainfo/com.example.myapp.metainfo.xml 188 + ``` 189 + 190 + ### MetaInfo (flatpak.metainfo.xml) 191 + 192 + ```xml 193 + <?xml version="1.0" encoding="UTF-8"?> 194 + <component type="desktop-application"> 195 + <id>com.example.myapp</id> 196 + <name>My App</name> 197 + <summary>Short description</summary> 198 + <metadata_license>CC0-1.0</metadata_license> 199 + <project_license>MIT</project_license> 200 + <description><p>Longer description.</p></description> 201 + <launchable type="desktop-id">com.example.myapp.desktop</launchable> 202 + <url type="homepage">https://example.com</url> 203 + <releases><release version="1.0.0" date="2025-01-01"/></releases> 204 + </component> 205 + ``` 206 + 207 + ### Build and Test 208 + 209 + ```bash 210 + flatpak-builder --force-clean --user --install flatpak flatpak-builder.yaml 211 + flatpak run com.example.myapp 212 + ``` 213 + 214 + ### Tray Icon Fix 215 + 216 + ```rust 217 + TrayIconBuilder::new() 218 + .icon(app.default_window_icon().unwrap().clone()) 219 + .temp_dir_path(app.path().app_cache_dir().unwrap()) 220 + .build().unwrap(); 221 + ``` 222 + 223 + ### Flathub Submission 224 + 225 + 1. Fork https://github.com/flathub/flathub 226 + 2. Create branch from `new-pr`, add manifest 227 + 3. Open PR, address feedback, receive repo access 228 + 229 + --- 230 + 231 + ## Snap Packages 232 + 233 + ### Prerequisites 234 + 235 + ```bash 236 + sudo apt install snapd 237 + sudo snap install core22 238 + sudo snap install snapcraft --classic 239 + ``` 240 + 241 + Register app at https://snapcraft.io 242 + 243 + ### snapcraft.yaml 244 + 245 + ```yaml 246 + name: myapp 247 + base: core22 248 + version: '1.0.0' 249 + summary: Short description (max 79 chars) 250 + description: Longer description. 251 + grade: stable 252 + confinement: strict 253 + 254 + layout: 255 + /usr/lib/x86_64-linux-gnu/webkit2gtk-4.1: 256 + bind: $SNAP/usr/lib/x86_64-linux-gnu/webkit2gtk-4.1 257 + 258 + apps: 259 + myapp: 260 + command: usr/bin/myapp 261 + desktop: usr/share/applications/myapp.desktop 262 + extensions: [gnome] 263 + plugs: [home, network, audio-playback, desktop, wayland, x11, opengl] 264 + 265 + parts: 266 + myapp: 267 + plugin: nil 268 + source: . 269 + build-packages: [nodejs, npm, curl, libwebkit2gtk-4.1-dev, libssl-dev, libayatana-appindicator3-dev] 270 + stage-packages: [libwebkit2gtk-4.1-0, libayatana-appindicator3-1] 271 + override-build: | 272 + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y 273 + . "$HOME/.cargo/env" 274 + npm install && npm run tauri build -- --bundles deb 275 + mkdir -p deb-extract && ar -x src-tauri/target/release/bundle/deb/*.deb --output deb-extract 276 + tar -C deb-extract -xf deb-extract/data.tar.gz 277 + cp -r deb-extract/usr $SNAPCRAFT_PART_INSTALL/ 278 + ``` 279 + 280 + ### Build and Publish 281 + 282 + ```bash 283 + sudo snapcraft 284 + snap run myapp 285 + snapcraft login && snapcraft upload --release=stable myapp_1.0.0_amd64.snap 286 + ``` 287 + 288 + --- 289 + 290 + ## AUR (Arch User Repository) 291 + 292 + ### Setup 293 + 294 + ```bash 295 + # Create account at https://aur.archlinux.org, configure SSH 296 + git clone ssh://aur@aur.archlinux.org/myapp.git 297 + ``` 298 + 299 + ### PKGBUILD (Binary) 300 + 301 + ```bash 302 + pkgname=myapp 303 + pkgver=1.0.0 304 + pkgrel=1 305 + pkgdesc="Description" 306 + arch=('x86_64' 'aarch64') 307 + url="https://github.com/user/myapp" 308 + license=('MIT') 309 + depends=('cairo' 'desktop-file-utils' 'gdk-pixbuf2' 'glib2' 'gtk3' 'hicolor-icon-theme' 'libsoup' 'pango' 'webkit2gtk-4.1') 310 + source_x86_64=("$pkgname-$pkgver.deb::https://github.com/user/myapp/releases/download/v$pkgver/myapp_${pkgver}_amd64.deb") 311 + sha256sums_x86_64=('SKIP') 312 + 313 + package() { tar -xf data.tar.gz -C "$pkgdir"; } 314 + ``` 315 + 316 + ### PKGBUILD (Source) 317 + 318 + ```bash 319 + pkgname=myapp 320 + pkgver=1.0.0 321 + pkgrel=1 322 + pkgdesc="Description" 323 + arch=('x86_64') 324 + url="https://github.com/user/myapp" 325 + license=('MIT') 326 + makedepends=('cargo' 'nodejs' 'npm' 'webkit2gtk-4.1' 'base-devel' 'openssl' 'libappindicator-gtk3') 327 + depends=('cairo' 'desktop-file-utils' 'gdk-pixbuf2' 'glib2' 'gtk3' 'hicolor-icon-theme' 'libsoup' 'pango' 'webkit2gtk-4.1') 328 + source=("$pkgname-$pkgver.tar.gz::https://github.com/user/myapp/archive/v$pkgver.tar.gz") 329 + sha256sums=('SKIP') 330 + 331 + build() { cd "$pkgname-$pkgver" && npm install && npm run tauri build -- --bundles deb; } 332 + package() { cd "$pkgname-$pkgver" && tar -xf src-tauri/target/release/bundle/deb/*.deb && tar -xf data.tar.gz -C "$pkgdir"; } 333 + ``` 334 + 335 + ### Install Script (myapp.install) 336 + 337 + ```bash 338 + post_install() { update-desktop-database -q; gtk-update-icon-cache -q -t -f usr/share/icons/hicolor; } 339 + post_upgrade() { post_install; } 340 + post_remove() { post_install; } 341 + ``` 342 + 343 + ### Submit 344 + 345 + ```bash 346 + makepkg --printsrcinfo > .SRCINFO 347 + makepkg -si # Test 348 + git add PKGBUILD .SRCINFO myapp.install && git commit -m "myapp 1.0.0" && git push 349 + ``` 350 + 351 + --- 352 + 353 + ## Environment Variables 354 + 355 + GUI apps don't inherit `$PATH`. Use `fix-path-env-rs`: 356 + 357 + ```toml 358 + [dependencies] 359 + fix-path-env = "0.1" 360 + ``` 361 + 362 + ```rust 363 + fn main() { 364 + fix_path_env::fix(); 365 + tauri::Builder::default().run(tauri::generate_context!()).expect("error"); 366 + } 367 + ``` 368 + 369 + --- 370 + 371 + ## GitHub Actions Example 372 + 373 + ```yaml 374 + name: Build Linux 375 + on: [push] 376 + jobs: 377 + build: 378 + runs-on: ubuntu-22.04 379 + steps: 380 + - uses: actions/checkout@v4 381 + - run: sudo apt update && sudo apt install -y libwebkit2gtk-4.1-dev libssl-dev libayatana-appindicator3-dev librsvg2-dev 382 + - uses: actions/setup-node@v4 383 + with: { node-version: 20 } 384 + - uses: dtolnay/rust-action@stable 385 + - run: npm install && npm run tauri build -- --bundles deb,rpm,appimage 386 + - uses: actions/upload-artifact@v4 387 + with: 388 + name: linux-bundles 389 + path: src-tauri/target/release/bundle/**/* 390 + ```
+395
.agents/skills/running-nodejs-sidecar-in-tauri/SKILL.md
··· 1 + --- 2 + name: running-nodejs-sidecar-in-tauri 3 + description: Guides users through running Node.js as a sidecar process in Tauri applications, enabling JavaScript backend functionality without requiring end-user Node.js installations. 4 + --- 5 + 6 + # Running Node.js as a Sidecar in Tauri 7 + 8 + Package and run Node.js applications as sidecar processes in Tauri desktop applications, leveraging the Node.js ecosystem without requiring users to install Node.js. 9 + 10 + ## Why Use a Node.js Sidecar 11 + 12 + - Bundle existing Node.js tools and libraries with your Tauri application 13 + - No external Node.js runtime dependency for end users 14 + - Leverage npm packages that have no Rust equivalent 15 + - Isolate Node.js logic from the main Tauri process 16 + - Cross-platform support (Windows, macOS, Linux) 17 + 18 + ## Prerequisites 19 + 20 + 1. Existing Tauri v2 application 21 + 2. Shell plugin installed and configured 22 + 3. Node.js and npm on the development machine 23 + 4. Rust toolchain (1.84.0+ recommended) 24 + 25 + ### Install the Shell Plugin 26 + 27 + ```bash 28 + npm install @tauri-apps/plugin-shell 29 + cargo add tauri-plugin-shell --manifest-path src-tauri/Cargo.toml 30 + ``` 31 + 32 + Register in `src-tauri/src/lib.rs`: 33 + 34 + ```rust 35 + pub fn run() { 36 + tauri::Builder::default() 37 + .plugin(tauri_plugin_shell::init()) 38 + .run(tauri::generate_context!()) 39 + .expect("error while running tauri application"); 40 + } 41 + ``` 42 + 43 + ## Project Structure 44 + 45 + ``` 46 + my-tauri-app/ 47 + ├── package.json 48 + ├── src-tauri/ 49 + │ ├── binaries/ 50 + │ │ └── my-sidecar-<target-triple>[.exe] 51 + │ ├── capabilities/default.json 52 + │ ├── tauri.conf.json 53 + │ └── src/lib.rs 54 + ├── sidecar/ 55 + │ ├── package.json 56 + │ ├── index.js 57 + │ └── rename.js 58 + └── src/ 59 + ``` 60 + 61 + ## Step-by-Step Setup 62 + 63 + ### 1. Create the Sidecar Directory 64 + 65 + ```bash 66 + mkdir sidecar && cd sidecar 67 + npm init -y 68 + npm add @yao-pkg/pkg --save-dev 69 + ``` 70 + 71 + ### 2. Write Sidecar Logic 72 + 73 + Create `sidecar/index.js`: 74 + 75 + ```javascript 76 + const command = process.argv[2]; 77 + const args = process.argv.slice(3); 78 + 79 + switch (command) { 80 + case 'hello': 81 + console.log(`Hello ${args[0] || 'World'}!`); 82 + break; 83 + case 'add': 84 + const [a, b] = args.map(Number); 85 + if (isNaN(a) || isNaN(b)) { 86 + console.error('Error: Both arguments must be numbers'); 87 + process.exit(1); 88 + } 89 + console.log(JSON.stringify({ result: a + b })); 90 + break; 91 + default: 92 + console.error(`Unknown command: ${command}`); 93 + process.exit(1); 94 + } 95 + ``` 96 + 97 + ### 3. Create the Rename Script 98 + 99 + Create `sidecar/rename.js`: 100 + 101 + ```javascript 102 + import { execSync } from 'child_process'; 103 + import fs from 'fs'; 104 + import path from 'path'; 105 + 106 + const ext = process.platform === 'win32' ? '.exe' : ''; 107 + 108 + let targetTriple; 109 + try { 110 + targetTriple = execSync('rustc --print host-tuple').toString().trim(); 111 + } catch { 112 + const rustInfo = execSync('rustc -vV').toString(); 113 + const match = rustInfo.match(/host: (.+)/); 114 + targetTriple = match ? match[1] : null; 115 + if (!targetTriple) { 116 + console.error('Could not determine Rust target triple'); 117 + process.exit(1); 118 + } 119 + } 120 + 121 + const destDir = path.join('..', 'src-tauri', 'binaries'); 122 + if (!fs.existsSync(destDir)) fs.mkdirSync(destDir, { recursive: true }); 123 + 124 + fs.renameSync(`my-sidecar${ext}`, path.join(destDir, `my-sidecar-${targetTriple}${ext}`)); 125 + ``` 126 + 127 + ### 4. Configure Build Scripts 128 + 129 + Update `sidecar/package.json`: 130 + 131 + ```json 132 + { 133 + "name": "my-sidecar", 134 + "type": "module", 135 + "scripts": { 136 + "build": "pkg index.js --output my-sidecar --targets node18", 137 + "postbuild": "node rename.js" 138 + }, 139 + "devDependencies": { 140 + "@yao-pkg/pkg": "^5.0.0" 141 + } 142 + } 143 + ``` 144 + 145 + ### 5. Configure Tauri 146 + 147 + Add to `src-tauri/tauri.conf.json`: 148 + 149 + ```json 150 + { 151 + "bundle": { 152 + "externalBin": ["binaries/my-sidecar"] 153 + } 154 + } 155 + ``` 156 + 157 + ### 6. Configure Permissions 158 + 159 + Update `src-tauri/capabilities/default.json`: 160 + 161 + ```json 162 + { 163 + "identifier": "default", 164 + "windows": ["main"], 165 + "permissions": [ 166 + "core:default", 167 + { 168 + "identifier": "shell:allow-execute", 169 + "allow": [{ 170 + "args": true, 171 + "name": "binaries/my-sidecar", 172 + "sidecar": true 173 + }] 174 + } 175 + ] 176 + } 177 + ``` 178 + 179 + **Restrict arguments for security:** 180 + 181 + ```json 182 + { 183 + "identifier": "shell:allow-execute", 184 + "allow": [ 185 + { 186 + "args": ["hello", { "validator": "\\w+" }], 187 + "name": "binaries/my-sidecar", 188 + "sidecar": true 189 + } 190 + ] 191 + } 192 + ``` 193 + 194 + ## Communication Patterns 195 + 196 + ### Frontend to Sidecar (TypeScript) 197 + 198 + ```typescript 199 + import { Command } from '@tauri-apps/plugin-shell'; 200 + 201 + async function sayHello(name: string): Promise<string> { 202 + const command = Command.sidecar('binaries/my-sidecar', ['hello', name]); 203 + const output = await command.execute(); 204 + if (output.code !== 0) throw new Error(output.stderr); 205 + return output.stdout.trim(); 206 + } 207 + 208 + async function addNumbers(a: number, b: number): Promise<number> { 209 + const command = Command.sidecar('binaries/my-sidecar', ['add', String(a), String(b)]); 210 + const output = await command.execute(); 211 + if (output.code !== 0) throw new Error(output.stderr); 212 + return JSON.parse(output.stdout).result; 213 + } 214 + ``` 215 + 216 + ### Backend to Sidecar (Rust) 217 + 218 + ```rust 219 + use tauri_plugin_shell::ShellExt; 220 + 221 + #[tauri::command] 222 + async fn call_sidecar( 223 + app: tauri::AppHandle, 224 + command: String, 225 + args: Vec<String>, 226 + ) -> Result<String, String> { 227 + let mut sidecar = app.shell().sidecar("my-sidecar").map_err(|e| e.to_string())?; 228 + sidecar = sidecar.arg(&command); 229 + for arg in args { 230 + sidecar = sidecar.arg(&arg); 231 + } 232 + let output = sidecar.output().await.map_err(|e| e.to_string())?; 233 + if output.status.success() { 234 + String::from_utf8(output.stdout).map_err(|e| e.to_string()) 235 + } else { 236 + Err(String::from_utf8_lossy(&output.stderr).to_string()) 237 + } 238 + } 239 + ``` 240 + 241 + Register the command: 242 + 243 + ```rust 244 + pub fn run() { 245 + tauri::Builder::default() 246 + .plugin(tauri_plugin_shell::init()) 247 + .invoke_handler(tauri::generate_handler![call_sidecar]) 248 + .run(tauri::generate_context!()) 249 + .expect("error while running tauri application"); 250 + } 251 + ``` 252 + 253 + ### Streaming Output 254 + 255 + ```typescript 256 + import { Command } from '@tauri-apps/plugin-shell'; 257 + 258 + async function runWithStreaming(args: string[]): Promise<void> { 259 + const command = Command.sidecar('binaries/my-sidecar', args); 260 + command.on('close', (data) => console.log(`Finished: ${data.code}`)); 261 + command.on('error', (error) => console.error(`Error: ${error}`)); 262 + command.stdout.on('data', (line) => console.log(`stdout: ${line}`)); 263 + command.stderr.on('data', (line) => console.error(`stderr: ${line}`)); 264 + await command.spawn(); 265 + } 266 + ``` 267 + 268 + ## Long-Running HTTP Sidecar 269 + 270 + For persistent processes, use HTTP: 271 + 272 + `sidecar/index.js`: 273 + 274 + ```javascript 275 + import http from 'http'; 276 + 277 + const PORT = process.env.SIDECAR_PORT || 3333; 278 + 279 + const server = http.createServer((req, res) => { 280 + let body = ''; 281 + req.on('data', (chunk) => (body += chunk)); 282 + req.on('end', () => { 283 + res.setHeader('Content-Type', 'application/json'); 284 + try { 285 + const data = body ? JSON.parse(body) : {}; 286 + if (req.url === '/hello') { 287 + res.end(JSON.stringify({ message: `Hello ${data.name || 'World'}!` })); 288 + } else if (req.url === '/health') { 289 + res.end(JSON.stringify({ status: 'ok' })); 290 + } else { 291 + res.statusCode = 404; 292 + res.end(JSON.stringify({ error: 'Not found' })); 293 + } 294 + } catch (err) { 295 + res.statusCode = 400; 296 + res.end(JSON.stringify({ error: err.message })); 297 + } 298 + }); 299 + }); 300 + 301 + server.listen(PORT, '127.0.0.1', () => console.log(`Listening on ${PORT}`)); 302 + process.on('SIGTERM', () => server.close(() => process.exit(0))); 303 + ``` 304 + 305 + Frontend communication: 306 + 307 + ```typescript 308 + import { Command } from '@tauri-apps/plugin-shell'; 309 + import { fetch } from '@tauri-apps/plugin-http'; 310 + 311 + let sidecarProcess: any = null; 312 + const PORT = 3333; 313 + 314 + async function startSidecar(): Promise<void> { 315 + if (sidecarProcess) return; 316 + const command = Command.sidecar('binaries/my-sidecar', [], { 317 + env: { SIDECAR_PORT: String(PORT) }, 318 + }); 319 + sidecarProcess = await command.spawn(); 320 + for (let i = 0; i < 10; i++) { 321 + try { 322 + const res = await fetch(`http://127.0.0.1:${PORT}/health`); 323 + if (res.ok) return; 324 + } catch { 325 + await new Promise((r) => setTimeout(r, 100)); 326 + } 327 + } 328 + throw new Error('Sidecar failed to start'); 329 + } 330 + 331 + async function callSidecar(endpoint: string, data?: object): Promise<any> { 332 + const res = await fetch(`http://127.0.0.1:${PORT}${endpoint}`, { 333 + method: 'POST', 334 + headers: { 'Content-Type': 'application/json' }, 335 + body: data ? JSON.stringify(data) : undefined, 336 + }); 337 + return res.json(); 338 + } 339 + 340 + async function stopSidecar(): Promise<void> { 341 + if (sidecarProcess) { 342 + await sidecarProcess.kill(); 343 + sidecarProcess = null; 344 + } 345 + } 346 + ``` 347 + 348 + ## Building for Production 349 + 350 + Update root `package.json`: 351 + 352 + ```json 353 + { 354 + "scripts": { 355 + "build:sidecar": "cd sidecar && npm run build", 356 + "dev": "npm run build:sidecar && tauri dev", 357 + "build": "npm run build:sidecar && tauri build" 358 + } 359 + } 360 + ``` 361 + 362 + Cross-platform targets: 363 + 364 + | Platform | pkg Target | Rust Triple | 365 + |----------|------------|-------------| 366 + | Windows x64 | `node18-win-x64` | `x86_64-pc-windows-msvc` | 367 + | macOS x64 | `node18-macos-x64` | `x86_64-apple-darwin` | 368 + | macOS ARM | `node18-macos-arm64` | `aarch64-apple-darwin` | 369 + | Linux x64 | `node18-linux-x64` | `x86_64-unknown-linux-gnu` | 370 + 371 + ## Security 372 + 373 + 1. Use validators instead of `"args": true` 374 + 2. Bind HTTP servers to `127.0.0.1` only 375 + 3. Validate input in both Tauri and sidecar 376 + 4. Ensure sidecars terminate when the app closes 377 + 378 + ## Troubleshooting 379 + 380 + **Binary not found:** Check target triple matches: 381 + ```bash 382 + ls -la src-tauri/binaries/ 383 + rustc --print host-tuple 384 + ``` 385 + 386 + **Permission denied (Unix):** 387 + ```bash 388 + chmod +x src-tauri/binaries/my-sidecar-* 389 + ``` 390 + 391 + **Silent crashes:** Check stderr: 392 + ```typescript 393 + const output = await command.execute(); 394 + if (output.code !== 0) console.error(output.stderr); 395 + ```
+267
.agents/skills/setting-up-tauri-projects/SKILL.md
··· 1 + --- 2 + name: setting-up-tauri-projects 3 + description: Helps users create and initialize new Tauri v2 projects for building cross-platform desktop and mobile applications. Covers system prerequisites and setup requirements for macOS, Windows, and Linux. Guides through project creation using create-tauri-app or manual Tauri CLI initialization. Explains project directory structure and configuration files. Supports vanilla JavaScript, TypeScript, React, Vue, Svelte, Angular, SolidJS, and Rust-based frontends. 4 + --- 5 + 6 + # Setting Up Tauri Projects 7 + 8 + ## What is Tauri? 9 + 10 + Tauri is a framework for building tiny, fast binaries for all major desktop and mobile platforms. It combines any frontend that compiles to HTML/JS/CSS with Rust for the backend. 11 + 12 + **Key characteristics:** 13 + - Minimal apps can be under 600KB (uses system webview, not bundled browser) 14 + - Built on Rust for memory, thread, and type safety 15 + - Supports virtually any frontend framework 16 + - Cross-platform: Windows, macOS, Linux, iOS, Android 17 + 18 + ## System Prerequisites 19 + 20 + ### macOS 21 + 22 + ```bash 23 + # For desktop + iOS development 24 + # Install Xcode from Mac App Store 25 + 26 + # For desktop-only development (lighter option) 27 + xcode-select --install 28 + ``` 29 + 30 + ### Windows 31 + 32 + 1. **Microsoft C++ Build Tools**: Download from Visual Studio, select "Desktop development with C++" 33 + 2. **WebView2**: Pre-installed on Windows 10 v1803+ (install manually if needed) 34 + 3. **Rust toolchain**: 35 + ```powershell 36 + winget install Rustlang.Rustup 37 + rustup default stable-msvc 38 + ``` 39 + 40 + ### Linux 41 + 42 + **Debian/Ubuntu:** 43 + ```bash 44 + sudo apt update 45 + sudo apt install libwebkit2gtk-4.1-dev build-essential curl wget file \ 46 + libxdo-dev libssl-dev libayatana-appindicator3-dev librsvg2-dev 47 + ``` 48 + 49 + **Arch Linux:** 50 + ```bash 51 + sudo pacman -S webkit2gtk-4.1 base-devel curl wget file openssl \ 52 + appmenu-gtk-module libappindicator-gtk3 librsvg xdotool 53 + ``` 54 + 55 + **Fedora:** 56 + ```bash 57 + sudo dnf install webkit2gtk4.1-devel openssl-devel curl wget file \ 58 + libappindicator-gtk3-devel librsvg2-devel libxdo-devel \ 59 + @development-tools 60 + ``` 61 + 62 + ### Rust (All Platforms) 63 + 64 + ```bash 65 + # macOS/Linux 66 + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh 67 + 68 + # Windows (PowerShell) 69 + winget install Rustlang.Rustup 70 + ``` 71 + 72 + ### Node.js 73 + 74 + Required only for JavaScript/TypeScript frontends. Install LTS version from nodejs.org. 75 + 76 + ### Mobile Development (Optional) 77 + 78 + **Android (all platforms):** 79 + ```bash 80 + # Install Android Studio, then add Rust targets 81 + rustup target add aarch64-linux-android armv7-linux-androideabi \ 82 + i686-linux-android x86_64-linux-android 83 + ``` 84 + 85 + Set environment variables: `JAVA_HOME`, `ANDROID_HOME`, `NDK_HOME` 86 + 87 + **iOS (macOS only):** 88 + ```bash 89 + # Requires full Xcode (not just Command Line Tools) 90 + rustup target add aarch64-apple-ios x86_64-apple-ios aarch64-apple-ios-sim 91 + 92 + # Install Cocoapods 93 + brew install cocoapods 94 + ``` 95 + 96 + ## Creating a Project 97 + 98 + ### Method 1: create-tauri-app (Recommended) 99 + 100 + Interactive scaffolding with framework templates. 101 + 102 + ```bash 103 + # npm 104 + npm create tauri-app@latest 105 + 106 + # pnpm 107 + pnpm create tauri-app 108 + 109 + # yarn 110 + yarn create tauri-app 111 + 112 + # bun 113 + bun create tauri-app 114 + 115 + # cargo (no Node.js required) 116 + cargo install create-tauri-app --locked 117 + cargo create-tauri-app 118 + 119 + # Shell scripts 120 + sh <(curl https://create.tauri.app/sh) # Bash 121 + irm https://create.tauri.app/ps | iex # PowerShell 122 + ``` 123 + 124 + **Prompts you'll see:** 125 + 1. Project name 126 + 2. Bundle identifier (e.g., `com.example.app`) 127 + 3. Frontend language: TypeScript/JavaScript, Rust, or .NET 128 + 4. Package manager 129 + 5. UI template: vanilla, Vue, Svelte, React, SolidJS, Angular, Preact, Yew, Leptos, Sycamore 130 + 131 + **After scaffolding:** 132 + ```bash 133 + cd your-project 134 + npm install 135 + npm run tauri dev 136 + ``` 137 + 138 + ### Method 2: Manual Setup (Existing Projects) 139 + 140 + Add Tauri to an existing frontend project. 141 + 142 + ```bash 143 + # 1. In your existing project, install Tauri CLI 144 + npm install -D @tauri-apps/cli@latest 145 + 146 + # 2. Initialize Tauri (creates src-tauri directory) 147 + npx tauri init 148 + ``` 149 + 150 + **tauri init prompts:** 151 + - App name 152 + - Window title 153 + - Frontend dev server URL (e.g., `http://localhost:5173`) 154 + - Frontend build output directory (e.g., `dist`) 155 + - Frontend dev command (e.g., `npm run dev`) 156 + - Frontend build command (e.g., `npm run build`) 157 + 158 + ```bash 159 + # 3. Start development 160 + npx tauri dev 161 + ``` 162 + 163 + ## Project Structure 164 + 165 + ``` 166 + my-tauri-app/ 167 + ├── package.json # Frontend dependencies 168 + ├── index.html # Frontend entry point 169 + ├── src/ # Frontend source code 170 + │ └── main.js 171 + └── src-tauri/ # Rust backend 172 + ├── Cargo.toml # Rust dependencies 173 + ├── Cargo.lock 174 + ├── build.rs # Tauri build script 175 + ├── tauri.conf.json # Main Tauri configuration 176 + ├── capabilities/ # Permission/capability definitions 177 + ├── icons/ # App icons (all formats) 178 + └── src/ 179 + ├── lib.rs # Main Rust code + mobile entry point 180 + └── main.rs # Desktop entry point 181 + ``` 182 + 183 + ### Key Files 184 + 185 + **tauri.conf.json** - Primary configuration: 186 + ```json 187 + { 188 + "productName": "my-app", 189 + "version": "0.1.0", 190 + "identifier": "com.example.my-app", 191 + "build": { 192 + "devUrl": "http://localhost:5173", 193 + "frontendDist": "../dist" 194 + }, 195 + "app": { 196 + "windows": [{ "title": "My App", "width": 800, "height": 600 }] 197 + } 198 + } 199 + ``` 200 + 201 + **src/lib.rs** - Rust application code: 202 + ```rust 203 + #[cfg_attr(mobile, tauri::mobile_entry_point)] 204 + pub fn run() { 205 + tauri::Builder::default() 206 + .run(tauri::generate_context!()) 207 + .expect("error while running tauri application"); 208 + } 209 + ``` 210 + 211 + **src/main.rs** - Desktop entry point: 212 + ```rust 213 + #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] 214 + 215 + fn main() { 216 + app_lib::run(); 217 + } 218 + ``` 219 + 220 + **capabilities/** - Define what commands JavaScript can invoke: 221 + ```json 222 + { 223 + "identifier": "main-capability", 224 + "windows": ["main"], 225 + "permissions": ["core:default"] 226 + } 227 + ``` 228 + 229 + ## Common Commands 230 + 231 + ```bash 232 + # Development with hot reload 233 + npm run tauri dev 234 + 235 + # Build production binary 236 + npm run tauri build 237 + 238 + # Generate app icons from source image 239 + npm run tauri icon path/to/icon.png 240 + 241 + # Add Android target 242 + npm run tauri android init 243 + 244 + # Add iOS target 245 + npm run tauri ios init 246 + 247 + # Run on Android 248 + npm run tauri android dev 249 + 250 + # Run on iOS simulator 251 + npm run tauri ios dev 252 + ``` 253 + 254 + ## Quick Reference: Supported Frontend Templates 255 + 256 + | Template | Languages | Notes | 257 + |----------|-----------|-------| 258 + | vanilla | JS, TS | No framework | 259 + | react | JS, TS | Vite-based | 260 + | vue | JS, TS | Vite-based | 261 + | svelte | JS, TS | Vite-based | 262 + | solid | JS, TS | Vite-based | 263 + | angular | TS | Angular CLI | 264 + | preact | JS, TS | Vite-based | 265 + | yew | Rust | Rust WASM frontend | 266 + | leptos | Rust | Rust WASM frontend | 267 + | sycamore | Rust | Rust WASM frontend |
+463
.agents/skills/signing-tauri-apps/SKILL.md
··· 1 + --- 2 + name: signing-tauri-apps 3 + description: Guides the user through Tauri application code signing and notarization for Android, iOS, Linux, macOS, and Windows platforms including certificate setup and configuration. 4 + --- 5 + 6 + # Tauri Code Signing Skill 7 + 8 + This skill provides comprehensive guidance for code signing Tauri applications across all supported platforms. 9 + 10 + ## Platform Overview 11 + 12 + | Platform | Requirement | Certificate Type | 13 + |----------|-------------|------------------| 14 + | Android | Required for Play Store | Java Keystore (JKS) | 15 + | iOS | Required for distribution | Apple Developer Certificate | 16 + | Linux | Optional (enhances trust) | GPG Key | 17 + | macOS | Required for distribution | Developer ID / Apple Distribution | 18 + | Windows | Required (SmartScreen) | OV or EV Certificate | 19 + 20 + --- 21 + 22 + ## Android Signing 23 + 24 + ### Generate Keystore 25 + 26 + **macOS/Linux:** 27 + ```bash 28 + keytool -genkey -v -keystore ~/upload-keystore.jks -keyalg RSA -keysize 2048 -validity 10000 -alias upload 29 + ``` 30 + 31 + **Windows:** 32 + ```powershell 33 + keytool -genkey -v -keystore $env:USERPROFILE\upload-keystore.jks -storetype JKS -keyalg RSA -keysize 2048 -validity 10000 -alias upload 34 + ``` 35 + 36 + ### Configuration File 37 + 38 + Create `src-tauri/gen/android/keystore.properties`: 39 + 40 + ```properties 41 + password=<your-password> 42 + keyAlias=upload 43 + storeFile=/path/to/upload-keystore.jks 44 + ``` 45 + 46 + **IMPORTANT:** Never commit `keystore.properties` to version control. 47 + 48 + ### Gradle Configuration 49 + 50 + Modify `src-tauri/gen/android/app/build.gradle.kts`: 51 + 52 + ```kotlin 53 + import java.io.FileInputStream 54 + 55 + // Add before android { } block 56 + val keystorePropertiesFile = rootProject.file("keystore.properties") 57 + val keystoreProperties = java.util.Properties() 58 + if (keystorePropertiesFile.exists()) { 59 + keystoreProperties.load(FileInputStream(keystorePropertiesFile)) 60 + } 61 + 62 + android { 63 + // ... existing config ... 64 + 65 + signingConfigs { 66 + create("release") { 67 + keyAlias = keystoreProperties["keyAlias"] as String 68 + keyPassword = keystoreProperties["password"] as String 69 + storeFile = file(keystoreProperties["storeFile"] as String) 70 + storePassword = keystoreProperties["password"] as String 71 + } 72 + } 73 + 74 + buildTypes { 75 + release { 76 + signingConfig = signingConfigs.getByName("release") 77 + // ... other release config ... 78 + } 79 + } 80 + } 81 + ``` 82 + 83 + ### CI/CD Environment Variables 84 + 85 + | Variable | Description | 86 + |----------|-------------| 87 + | `ANDROID_KEY_ALIAS` | Key alias (e.g., `upload`) | 88 + | `ANDROID_KEY_PASSWORD` | Keystore password | 89 + | `ANDROID_KEY_BASE64` | Base64-encoded keystore file | 90 + 91 + **GitHub Actions Example:** 92 + 93 + ```yaml 94 + - name: Setup Android signing 95 + run: | 96 + cd src-tauri/gen/android 97 + echo "keyAlias=${{ secrets.ANDROID_KEY_ALIAS }}" > keystore.properties 98 + echo "password=${{ secrets.ANDROID_KEY_PASSWORD }}" >> keystore.properties 99 + base64 -d <<< "${{ secrets.ANDROID_KEY_BASE64 }}" > $RUNNER_TEMP/keystore.jks 100 + echo "storeFile=$RUNNER_TEMP/keystore.jks" >> keystore.properties 101 + ``` 102 + 103 + --- 104 + 105 + ## iOS Signing 106 + 107 + ### Prerequisites 108 + 109 + - Apple Developer Program enrollment ($99/year) 110 + - Bundle identifier registered in App Store Connect 111 + - iOS code signing certificate 112 + - Mobile provisioning profile 113 + 114 + ### Automatic Signing (Recommended) 115 + 116 + For local development, authenticate through Xcode Settings > Accounts. 117 + 118 + For CI/CD, create an App Store Connect API key and set: 119 + 120 + | Variable | Description | 121 + |----------|-------------| 122 + | `APPLE_API_ISSUER` | Issuer ID from App Store Connect | 123 + | `APPLE_API_KEY` | Key ID from App Store Connect | 124 + | `APPLE_API_KEY_PATH` | Path to the `.p8` private key file | 125 + 126 + ### Manual Signing 127 + 128 + | Variable | Description | 129 + |----------|-------------| 130 + | `IOS_CERTIFICATE` | Base64-encoded `.p12` certificate | 131 + | `IOS_CERTIFICATE_PASSWORD` | Password used when exporting certificate | 132 + | `IOS_MOBILE_PROVISION` | Base64-encoded provisioning profile | 133 + 134 + ### Certificate Types by Distribution Method 135 + 136 + | Distribution | Certificate Type | 137 + |--------------|------------------| 138 + | Debugging | Apple Development or iOS App Development | 139 + | App Store | Apple Distribution or iOS Distribution | 140 + | Ad Hoc | Apple Distribution or iOS Distribution | 141 + 142 + ### Export Certificate 143 + 144 + 1. Open Keychain Access 145 + 2. Find your certificate 146 + 3. Right-click the private key 147 + 4. Select "Export" and save as `.p12` 148 + 5. Convert to base64: `base64 -i certificate.p12` 149 + 150 + ### Create Provisioning Profile 151 + 152 + 1. Register App ID with matching bundle identifier 153 + 2. Create provisioning profile for your distribution method 154 + 3. Link certificate to profile 155 + 4. Download and convert: `base64 -i profile.mobileprovision` 156 + 157 + --- 158 + 159 + ## Linux Signing (AppImage) 160 + 161 + ### Generate GPG Key 162 + 163 + ```bash 164 + gpg2 --full-gen-key 165 + ``` 166 + 167 + Back up the key securely. 168 + 169 + ### Environment Variables 170 + 171 + | Variable | Description | 172 + |----------|-------------| 173 + | `SIGN` | Set to `1` to enable signing | 174 + | `SIGN_KEY` | GPG Key ID (optional, uses default if not set) | 175 + | `APPIMAGETOOL_SIGN_PASSPHRASE` | Key password (required for CI/CD) | 176 + | `APPIMAGETOOL_FORCE_SIGN` | Set to `1` to fail build on signing error | 177 + 178 + ### Build with Signing 179 + 180 + ```bash 181 + SIGN=1 APPIMAGETOOL_SIGN_PASSPHRASE="your-passphrase" npm run tauri build 182 + ``` 183 + 184 + ### View Embedded Signature 185 + 186 + ```bash 187 + ./src-tauri/target/release/bundle/appimage/app_version_amd64.AppImage --appimage-signature 188 + ``` 189 + 190 + ### Validate Signature 191 + 192 + Download the validate tool from [AppImageUpdate releases](https://github.com/AppImageCommunity/AppImageUpdate/releases): 193 + 194 + ```bash 195 + chmod +x validate-x86_64.AppImage 196 + ./validate-x86_64.AppImage your-app.AppImage 197 + ``` 198 + 199 + **Note:** AppImage does not auto-validate signatures. Users must manually verify. 200 + 201 + --- 202 + 203 + ## macOS Signing and Notarization 204 + 205 + ### Prerequisites 206 + 207 + - Apple Developer Program enrollment ($99/year) 208 + - Mac computer for code signing 209 + - Free accounts cannot notarize applications 210 + 211 + ### Certificate Types 212 + 213 + | Certificate | Use Case | 214 + |-------------|----------| 215 + | Apple Distribution | App Store submissions | 216 + | Developer ID Application | Distribution outside App Store | 217 + 218 + ### Create Certificate 219 + 220 + 1. Generate Certificate Signing Request (CSR) from Keychain Access 221 + 2. Upload CSR at Apple Developer > Certificates, IDs & Profiles 222 + 3. Download and double-click `.cer` to install 223 + 224 + ### Configuration 225 + 226 + **tauri.conf.json:** 227 + 228 + ```json 229 + { 230 + "bundle": { 231 + "macOS": { 232 + "signingIdentity": "Developer ID Application: Your Name (TEAM_ID)" 233 + } 234 + } 235 + } 236 + ``` 237 + 238 + ### Environment Variables for CI/CD 239 + 240 + **Certificate Variables:** 241 + 242 + | Variable | Description | 243 + |----------|-------------| 244 + | `APPLE_CERTIFICATE` | Base64-encoded `.p12` certificate | 245 + | `APPLE_CERTIFICATE_PASSWORD` | Password for exported certificate | 246 + | `APPLE_SIGNING_IDENTITY` | Certificate name in keychain | 247 + 248 + **Notarization - Option 1: App Store Connect API (Recommended):** 249 + 250 + | Variable | Description | 251 + |----------|-------------| 252 + | `APPLE_API_ISSUER` | Issuer ID | 253 + | `APPLE_API_KEY` | Key ID | 254 + | `APPLE_API_KEY_PATH` | Path to `.p8` private key | 255 + 256 + **Notarization - Option 2: Apple ID:** 257 + 258 + | Variable | Description | 259 + |----------|-------------| 260 + | `APPLE_ID` | Apple ID email | 261 + | `APPLE_PASSWORD` | App-specific password | 262 + | `APPLE_TEAM_ID` | Team identifier | 263 + 264 + ### Export Certificate for CI/CD 265 + 266 + ```bash 267 + # Export from Keychain as .p12, then: 268 + base64 -i certificate.p12 | pbcopy 269 + ``` 270 + 271 + ### Ad-Hoc Signing (Testing Only) 272 + 273 + For unsigned distribution or testing without Apple credentials: 274 + 275 + ```json 276 + { 277 + "bundle": { 278 + "macOS": { 279 + "signingIdentity": "-" 280 + } 281 + } 282 + } 283 + ``` 284 + 285 + ### GitHub Actions Example 286 + 287 + ```yaml 288 + - name: Import certificate 289 + env: 290 + APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} 291 + APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} 292 + run: | 293 + echo $APPLE_CERTIFICATE | base64 --decode > certificate.p12 294 + security create-keychain -p actions temp.keychain 295 + security import certificate.p12 -k temp.keychain -P $APPLE_CERTIFICATE_PASSWORD -T /usr/bin/codesign 296 + security list-keychains -s temp.keychain 297 + security unlock-keychain -p actions temp.keychain 298 + security set-key-partition-list -S apple-tool:,apple: -s -k actions temp.keychain 299 + ``` 300 + 301 + --- 302 + 303 + ## Windows Signing 304 + 305 + ### Certificate Types 306 + 307 + | Type | SmartScreen | Availability | 308 + |------|-------------|--------------| 309 + | OV (Organization Validated) | Builds reputation over time | Before June 1, 2023 | 310 + | EV (Extended Validation) | Immediate trust | Required after June 1, 2023 | 311 + 312 + **Note:** Certificates obtained after June 1, 2023 require EV certificates for immediate SmartScreen trust. 313 + 314 + ### Configuration 315 + 316 + **tauri.conf.json:** 317 + 318 + ```json 319 + { 320 + "bundle": { 321 + "windows": { 322 + "certificateThumbprint": "A1B1A2B2A3B3A4B4A5B5A6B6A7B7A8B8A9B9A0B0", 323 + "digestAlgorithm": "sha256", 324 + "timestampUrl": "http://timestamp.sectigo.com" 325 + } 326 + } 327 + } 328 + ``` 329 + 330 + ### Find Certificate Thumbprint 331 + 332 + 1. Open certificate details 333 + 2. Go to Details tab 334 + 3. Find "Thumbprint" field 335 + 4. Copy the hex string (remove spaces) 336 + 337 + ### Common Timestamp URLs 338 + 339 + - `http://timestamp.sectigo.com` 340 + - `http://timestamp.digicert.com` 341 + - `http://timestamp.globalsign.com` 342 + 343 + ### Convert Certificate to PFX 344 + 345 + ```bash 346 + openssl pkcs12 -export -in cert.cer -inkey private-key.key -out certificate.pfx 347 + ``` 348 + 349 + ### Environment Variables for CI/CD 350 + 351 + | Variable | Description | 352 + |----------|-------------| 353 + | `WINDOWS_CERTIFICATE` | Base64-encoded `.pfx` file | 354 + | `WINDOWS_CERTIFICATE_PASSWORD` | PFX export password | 355 + 356 + ### GitHub Actions Example 357 + 358 + ```yaml 359 + - name: Import Windows certificate 360 + env: 361 + WINDOWS_CERTIFICATE: ${{ secrets.WINDOWS_CERTIFICATE }} 362 + WINDOWS_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }} 363 + run: | 364 + echo "$WINDOWS_CERTIFICATE" | base64 --decode > certificate.pfx 365 + Import-PfxCertificate -FilePath certificate.pfx -CertStoreLocation Cert:\CurrentUser\My -Password (ConvertTo-SecureString -String $env:WINDOWS_CERTIFICATE_PASSWORD -AsPlainText -Force) 366 + shell: pwsh 367 + ``` 368 + 369 + ### Azure Key Vault Signing 370 + 371 + For cloud-based signing with Azure Key Vault: 372 + 373 + | Variable | Description | 374 + |----------|-------------| 375 + | `AZURE_CLIENT_ID` | Azure AD application client ID | 376 + | `AZURE_CLIENT_SECRET` | Azure AD application secret | 377 + | `AZURE_TENANT_ID` | Azure AD tenant ID | 378 + 379 + Configure in `tauri.conf.json`: 380 + 381 + ```json 382 + { 383 + "bundle": { 384 + "windows": { 385 + "signCommand": "relic sign --key azurekeyvault --file %1" 386 + } 387 + } 388 + } 389 + ``` 390 + 391 + ### Azure Trusted Signing 392 + 393 + For Azure Code Signing service: 394 + 395 + ```json 396 + { 397 + "bundle": { 398 + "windows": { 399 + "signCommand": "trusted-signing-cli -e <endpoint> -a <account> -c <profile> %1" 400 + } 401 + } 402 + } 403 + ``` 404 + 405 + ### Custom Sign Command 406 + 407 + For other signing tools or cross-platform builds: 408 + 409 + ```json 410 + { 411 + "bundle": { 412 + "windows": { 413 + "signCommand": "your-signing-tool --sign %1" 414 + } 415 + } 416 + } 417 + ``` 418 + 419 + The `%1` placeholder is replaced with the executable path. 420 + 421 + --- 422 + 423 + ## Quick Reference: All Environment Variables 424 + 425 + ### Android 426 + - `ANDROID_KEY_ALIAS` 427 + - `ANDROID_KEY_PASSWORD` 428 + - `ANDROID_KEY_BASE64` 429 + 430 + ### iOS (Manual) 431 + - `IOS_CERTIFICATE` 432 + - `IOS_CERTIFICATE_PASSWORD` 433 + - `IOS_MOBILE_PROVISION` 434 + 435 + ### iOS/macOS (API Key) 436 + - `APPLE_API_ISSUER` 437 + - `APPLE_API_KEY` 438 + - `APPLE_API_KEY_PATH` 439 + 440 + ### macOS (Certificate) 441 + - `APPLE_CERTIFICATE` 442 + - `APPLE_CERTIFICATE_PASSWORD` 443 + - `APPLE_SIGNING_IDENTITY` 444 + 445 + ### macOS (Apple ID Notarization) 446 + - `APPLE_ID` 447 + - `APPLE_PASSWORD` 448 + - `APPLE_TEAM_ID` 449 + 450 + ### Linux 451 + - `SIGN` 452 + - `SIGN_KEY` 453 + - `APPIMAGETOOL_SIGN_PASSPHRASE` 454 + - `APPIMAGETOOL_FORCE_SIGN` 455 + 456 + ### Windows 457 + - `WINDOWS_CERTIFICATE` 458 + - `WINDOWS_CERTIFICATE_PASSWORD` 459 + 460 + ### Azure (Windows) 461 + - `AZURE_CLIENT_ID` 462 + - `AZURE_CLIENT_SECRET` 463 + - `AZURE_TENANT_ID`
+421
.agents/skills/testing-tauri-apps/SKILL.md
··· 1 + --- 2 + name: testing-tauri-apps 3 + description: Guides developers through testing Tauri applications including unit testing with mock runtime, mocking Tauri APIs, WebDriver end-to-end testing with Selenium and WebdriverIO, and CI integration with GitHub Actions. 4 + --- 5 + 6 + # Testing Tauri Applications 7 + 8 + This skill covers testing strategies for Tauri v2 applications: unit testing with mocks, end-to-end testing with WebDriver, and CI integration. 9 + 10 + ## Testing Approaches Overview 11 + 12 + Tauri supports two primary testing methodologies: 13 + 14 + 1. **Unit/Integration Testing** - Uses a mock runtime without executing native webview libraries 15 + 2. **End-to-End Testing** - Uses WebDriver protocol for browser automation 16 + 17 + ## Mocking Tauri APIs 18 + 19 + The `@tauri-apps/api/mocks` module simulates a Tauri environment during frontend testing. 20 + 21 + ### Install Mock Dependencies 22 + 23 + ```bash 24 + npm install -D vitest @tauri-apps/api 25 + ``` 26 + 27 + ### Mock IPC Commands 28 + 29 + ```javascript 30 + import { mockIPC, clearMocks } from '@tauri-apps/api/mocks'; 31 + import { invoke } from '@tauri-apps/api/core'; 32 + import { vi, describe, it, expect, afterEach } from 'vitest'; 33 + 34 + afterEach(() => { 35 + clearMocks(); 36 + }); 37 + 38 + describe('Tauri Commands', () => { 39 + it('should mock the add command', async () => { 40 + mockIPC((cmd, args) => { 41 + if (cmd === 'add') { 42 + return (args.a as number) + (args.b as number); 43 + } 44 + }); 45 + 46 + const result = await invoke('add', { a: 12, b: 15 }); 47 + expect(result).toBe(27); 48 + }); 49 + 50 + it('should verify invoke was called', async () => { 51 + mockIPC((cmd) => { 52 + if (cmd === 'greet') return 'Hello!'; 53 + }); 54 + 55 + const spy = vi.spyOn(window.__TAURI_INTERNALS__, 'invoke'); 56 + await invoke('greet', { name: 'World' }); 57 + expect(spy).toHaveBeenCalled(); 58 + }); 59 + }); 60 + ``` 61 + 62 + ### Mock Sidecar and Shell Commands 63 + 64 + ```javascript 65 + import { mockIPC } from '@tauri-apps/api/mocks'; 66 + 67 + mockIPC(async (cmd, args) => { 68 + if (args.message.cmd === 'execute') { 69 + const eventCallbackId = `_${args.message.onEventFn}`; 70 + const eventEmitter = window[eventCallbackId]; 71 + eventEmitter({ event: 'Stdout', payload: 'process output data' }); 72 + eventEmitter({ event: 'Terminated', payload: { code: 0 } }); 73 + } 74 + }); 75 + ``` 76 + 77 + ### Mock Events (v2.7.0+) 78 + 79 + ```javascript 80 + import { mockIPC } from '@tauri-apps/api/mocks'; 81 + import { emit, listen } from '@tauri-apps/api/event'; 82 + 83 + mockIPC(() => {}, { shouldMockEvents: true }); 84 + 85 + const eventHandler = vi.fn(); 86 + await listen('test-event', eventHandler); 87 + await emit('test-event', { foo: 'bar' }); 88 + expect(eventHandler).toHaveBeenCalled(); 89 + ``` 90 + 91 + ### Mock Windows 92 + 93 + ```javascript 94 + import { mockWindows } from '@tauri-apps/api/mocks'; 95 + import { getCurrent, getAll } from '@tauri-apps/api/webviewWindow'; 96 + 97 + mockWindows('main', 'second', 'third'); 98 + 99 + // First parameter is the "current" window 100 + expect(getCurrent()).toHaveProperty('label', 'main'); 101 + expect(getAll().map((w) => w.label)).toEqual(['main', 'second', 'third']); 102 + ``` 103 + 104 + ### Vitest Configuration 105 + 106 + ```javascript 107 + // vitest.config.js 108 + import { defineConfig } from 'vitest/config'; 109 + 110 + export default defineConfig({ 111 + test: { 112 + environment: 'jsdom', 113 + setupFiles: ['./test/setup.js'], 114 + }, 115 + }); 116 + 117 + // test/setup.js 118 + window.__TAURI_INTERNALS__ = { 119 + invoke: vi.fn(), 120 + transformCallback: vi.fn(), 121 + }; 122 + ``` 123 + 124 + ## WebDriver End-to-End Testing 125 + 126 + WebDriver testing uses `tauri-driver` to automate Tauri applications. 127 + 128 + ### Platform Support 129 + 130 + | Platform | Support | Notes | 131 + |----------|---------|-------| 132 + | Windows | Full | Requires Microsoft Edge Driver | 133 + | Linux | Full | Requires WebKitWebDriver | 134 + | macOS | None | WKWebView lacks WebDriver tooling | 135 + 136 + ### Install tauri-driver 137 + 138 + ```bash 139 + cargo install tauri-driver --locked 140 + ``` 141 + 142 + ### Platform Dependencies 143 + 144 + ```bash 145 + # Linux (Debian/Ubuntu) 146 + sudo apt install webkit2gtk-driver xvfb 147 + which WebKitWebDriver # Verify installation 148 + 149 + # Windows (PowerShell) 150 + cargo install --git https://github.com/chippers/msedgedriver-tool 151 + & "$HOME/.cargo/bin/msedgedriver-tool.exe" 152 + ``` 153 + 154 + ## WebdriverIO Setup 155 + 156 + ### Project Structure 157 + 158 + ``` 159 + my-tauri-app/ 160 + ├── src-tauri/ 161 + ├── src/ 162 + └── e2e-tests/ 163 + ├── package.json 164 + ├── wdio.conf.js 165 + └── specs/ 166 + └── app.spec.js 167 + ``` 168 + 169 + ### Package Configuration 170 + 171 + ```json 172 + { 173 + "name": "tauri-e2e-tests", 174 + "version": "1.0.0", 175 + "type": "module", 176 + "scripts": { "test": "wdio run wdio.conf.js" }, 177 + "dependencies": { "@wdio/cli": "^9.19.0" }, 178 + "devDependencies": { 179 + "@wdio/local-runner": "^9.19.0", 180 + "@wdio/mocha-framework": "^9.19.0", 181 + "@wdio/spec-reporter": "^9.19.0" 182 + } 183 + } 184 + ``` 185 + 186 + ### WebdriverIO Configuration 187 + 188 + ```javascript 189 + // e2e-tests/wdio.conf.js 190 + import { spawn, spawnSync } from 'child_process'; 191 + 192 + let tauriDriver; 193 + 194 + export const config = { 195 + hostname: '127.0.0.1', 196 + port: 4444, 197 + specs: ['./specs/**/*.js'], 198 + maxInstances: 1, 199 + capabilities: [{ 200 + browserName: 'wry', 201 + 'tauri:options': { 202 + application: '../src-tauri/target/debug/my-tauri-app', 203 + }, 204 + }], 205 + framework: 'mocha', 206 + reporters: ['spec'], 207 + mochaOpts: { ui: 'bdd', timeout: 60000 }, 208 + 209 + onPrepare: () => { 210 + const result = spawnSync('cargo', ['build', '--manifest-path', '../src-tauri/Cargo.toml'], { 211 + stdio: 'inherit', 212 + }); 213 + if (result.status !== 0) throw new Error('Failed to build Tauri app'); 214 + }, 215 + 216 + beforeSession: () => { 217 + tauriDriver = spawn('tauri-driver', [], { stdio: ['ignore', 'pipe', 'pipe'] }); 218 + return new Promise((resolve) => { 219 + tauriDriver.stdout.on('data', (data) => { 220 + if (data.toString().includes('listening')) resolve(); 221 + }); 222 + }); 223 + }, 224 + 225 + afterSession: () => tauriDriver?.kill(), 226 + }; 227 + ``` 228 + 229 + ### WebdriverIO Test Example 230 + 231 + ```javascript 232 + // e2e-tests/specs/app.spec.js 233 + describe('My Tauri App', () => { 234 + it('should display the header', async () => { 235 + const header = await $('body > h1'); 236 + expect(await header.getText()).toMatch(/^[hH]ello/); 237 + }); 238 + 239 + it('should interact with a button', async () => { 240 + const button = await $('#greet-button'); 241 + await button.click(); 242 + const output = await $('#greet-output'); 243 + await output.waitForExist({ timeout: 5000 }); 244 + expect(await output.getText()).toContain('Hello'); 245 + }); 246 + }); 247 + ``` 248 + 249 + ## Selenium Setup 250 + 251 + ### Package Configuration 252 + 253 + ```json 254 + { 255 + "name": "tauri-selenium-tests", 256 + "version": "1.0.0", 257 + "scripts": { "test": "mocha" }, 258 + "dependencies": { 259 + "chai": "^5.2.1", 260 + "mocha": "^11.7.1", 261 + "selenium-webdriver": "^4.34.0" 262 + } 263 + } 264 + ``` 265 + 266 + ### Selenium Test Example 267 + 268 + ```javascript 269 + // e2e-tests/test/test.js 270 + import { spawn, spawnSync } from 'child_process'; 271 + import path from 'path'; 272 + import { fileURLToPath } from 'url'; 273 + import { Builder, By } from 'selenium-webdriver'; 274 + import { expect } from 'chai'; 275 + 276 + const __dirname = path.dirname(fileURLToPath(import.meta.url)); 277 + let driver, tauriDriver; 278 + const application = path.resolve(__dirname, '../../src-tauri/target/debug/my-tauri-app'); 279 + 280 + describe('Tauri App Tests', function () { 281 + this.timeout(60000); 282 + 283 + before(async function () { 284 + spawnSync('cargo', ['build', '--manifest-path', '../../src-tauri/Cargo.toml'], { 285 + cwd: __dirname, stdio: 'inherit', 286 + }); 287 + 288 + tauriDriver = spawn('tauri-driver', [], { stdio: ['ignore', 'pipe', 'pipe'] }); 289 + await new Promise((resolve) => { 290 + tauriDriver.stdout.on('data', (data) => { 291 + if (data.toString().includes('listening')) resolve(); 292 + }); 293 + }); 294 + 295 + driver = await new Builder() 296 + .usingServer('http://127.0.0.1:4444/') 297 + .withCapabilities({ browserName: 'wry', 'tauri:options': { application } }) 298 + .build(); 299 + }); 300 + 301 + after(async function () { 302 + await driver?.quit(); 303 + tauriDriver?.kill(); 304 + }); 305 + 306 + it('should display greeting', async function () { 307 + const header = await driver.findElement(By.css('body > h1')); 308 + expect(await header.getText()).to.match(/^[hH]ello/); 309 + }); 310 + 311 + it('should click button and show output', async function () { 312 + const button = await driver.findElement(By.id('greet-button')); 313 + await button.click(); 314 + const output = await driver.findElement(By.id('greet-output')); 315 + expect(await output.getText()).to.include('Hello'); 316 + }); 317 + }); 318 + ``` 319 + 320 + ## CI Integration with GitHub Actions 321 + 322 + ```yaml 323 + # .github/workflows/e2e-tests.yml 324 + name: E2E Tests 325 + 326 + on: 327 + push: 328 + branches: [main] 329 + pull_request: 330 + branches: [main] 331 + 332 + jobs: 333 + test: 334 + strategy: 335 + fail-fast: false 336 + matrix: 337 + os: [ubuntu-latest, windows-latest] 338 + runs-on: ${{ matrix.os }} 339 + 340 + steps: 341 + - uses: actions/checkout@v4 342 + 343 + - name: Install Linux dependencies 344 + if: matrix.os == 'ubuntu-latest' 345 + run: | 346 + sudo apt-get update 347 + sudo apt-get install -y libwebkit2gtk-4.1-dev build-essential \ 348 + curl wget file libxdo-dev libssl-dev \ 349 + libayatana-appindicator3-dev librsvg2-dev \ 350 + webkit2gtk-driver xvfb 351 + 352 + - uses: dtolnay/rust-action@stable 353 + - run: cargo install tauri-driver --locked 354 + 355 + - name: Setup Windows WebDriver 356 + if: matrix.os == 'windows-latest' 357 + shell: pwsh 358 + run: | 359 + cargo install --git https://github.com/chippers/msedgedriver-tool 360 + & "$HOME/.cargo/bin/msedgedriver-tool.exe" 361 + 362 + - uses: actions/setup-node@v4 363 + with: 364 + node-version: '20' 365 + 366 + - run: npm install 367 + - run: npm run build 368 + - run: cargo build --manifest-path src-tauri/Cargo.toml 369 + 370 + - name: Run E2E tests (Linux) 371 + if: matrix.os == 'ubuntu-latest' 372 + working-directory: e2e-tests 373 + run: npm install && xvfb-run npm test 374 + 375 + - name: Run E2E tests (Windows) 376 + if: matrix.os == 'windows-latest' 377 + working-directory: e2e-tests 378 + run: npm install && npm test 379 + ``` 380 + 381 + ## Best Practices 382 + 383 + ### Mock Testing 384 + - Always call `clearMocks()` in `afterEach` to prevent state leakage 385 + - Use spies to verify IPC calls were made correctly 386 + - Mock at the right level: IPC for commands, windows for multi-window logic 387 + 388 + ### WebDriver Testing 389 + - Use debug builds for faster iteration during development 390 + - Set appropriate timeouts as Tauri apps may need time to initialize 391 + - Wait for elements explicitly rather than using implicit waits 392 + - Keep tests independent so each test works in isolation 393 + 394 + ### CI Integration 395 + - Use `xvfb-run` on Linux for headless WebDriver testing 396 + - Match Edge Driver version on Windows to avoid connection issues 397 + - Build the app before running WebDriver tests 398 + - Run unit tests before e2e tests to catch issues early 399 + 400 + ## Troubleshooting 401 + 402 + ### WebDriver Connection Timeout 403 + - Windows: Verify Edge Driver version matches installed Edge 404 + - Linux: Ensure `webkit2gtk-driver` is installed 405 + - Check `tauri-driver` is running and listening on port 4444 406 + 407 + ### Mock Not Working 408 + - Import `@tauri-apps/api/mocks` before the code under test 409 + - Call `clearMocks()` in `afterEach` to reset state 410 + - Ensure `window.__TAURI_INTERNALS__` is properly mocked in setup 411 + 412 + ### CI Failures 413 + - Linux: Add `xvfb-run` prefix to test commands 414 + - Windows: Install Edge Driver via `msedgedriver-tool` 415 + - Increase timeout for slower CI runners 416 + 417 + ## References 418 + 419 + - [Tauri Testing Documentation](https://v2.tauri.app/develop/tests/) 420 + - [tauri-driver on crates.io](https://crates.io/crates/tauri-driver) 421 + - [WebDriver Example Repository](https://github.com/tauri-apps/webdriver-example)
+423
.agents/skills/understanding-tauri-architecture/SKILL.md
··· 1 + --- 2 + name: understanding-tauri-architecture 3 + description: Teaches Claude about Tauri's core architecture, including the Rust backend, webview integration, Core-Shell design pattern, IPC mechanisms, and security model fundamentals. 4 + --- 5 + 6 + # Tauri Architecture 7 + 8 + Tauri is a polyglot toolkit for building desktop applications that combines a Rust backend with HTML/CSS/JavaScript rendered in a native webview. This document covers the fundamental architecture concepts. 9 + 10 + ## Architecture Overview 11 + 12 + ``` 13 + +------------------------------------------------------------------+ 14 + | TAURI APPLICATION | 15 + +------------------------------------------------------------------+ 16 + | | 17 + | +---------------------------+ +---------------------------+ | 18 + | | FRONTEND (Shell) | | BACKEND (Core) | | 19 + | |---------------------------| |---------------------------| | 20 + | | | | | | 21 + | | HTML / CSS / JavaScript | | Rust Code | | 22 + | | (or any web framework) | | (tauri crate + app) | | 23 + | | | | | | 24 + | | - React, Vue, Svelte, | | - System access | | 25 + | | Solid, etc. | | - File operations | | 26 + | | - Standard web APIs | | - Native features | | 27 + | | - Tauri JS API | | - Plugin system | | 28 + | | | | | | 29 + | +-------------+-------------+ +-------------+-------------+ | 30 + | | | | 31 + | | IPC (Message Passing) | | 32 + | +<------------------------------->+ | 33 + | | Commands & Events | | 34 + | | 35 + | +------------------------------------------------------------+ | 36 + | | WEBVIEW (TAO + WRY) | | 37 + | |------------------------------------------------------------| | 38 + | | - Platform-native webview (not bundled) | | 39 + | | - Windows: WebView2 (Edge/Chromium) | | 40 + | | - macOS: WKWebView (Safari/WebKit) | | 41 + | | - Linux: WebKitGTK | | 42 + | +------------------------------------------------------------+ | 43 + | | 44 + +------------------------------------------------------------------+ 45 + | 46 + v 47 + +------------------------------------------------------------------+ 48 + | OPERATING SYSTEM | 49 + | - Windows, macOS, Linux, iOS, Android | 50 + +------------------------------------------------------------------+ 51 + ``` 52 + 53 + ## Core vs Shell Design 54 + 55 + Tauri follows a **Core-Shell architecture** where the application is split into two distinct layers: 56 + 57 + ### The Core (Rust Backend) 58 + 59 + The Core is the Rust-based backend that handles all system-level operations: 60 + 61 + - **System access**: File system, network, processes 62 + - **Native features**: Notifications, dialogs, clipboard 63 + - **Security enforcement**: Permission validation, capability checking 64 + - **Plugin management**: Extending functionality through plugins 65 + - **App lifecycle**: Window management, updates, configuration 66 + 67 + The Core NEVER exposes direct system access to the frontend. All interactions go through validated IPC channels. 68 + 69 + ### The Shell (Frontend) 70 + 71 + The Shell is the user interface layer rendered in a webview: 72 + 73 + - **Web technologies**: HTML, CSS, JavaScript/TypeScript 74 + - **Framework agnostic**: Works with React, Vue, Svelte, Solid, or vanilla JS 75 + - **Sandboxed execution**: Runs in the webview's security sandbox 76 + - **Tauri API access**: Calls backend through `@tauri-apps/api` 77 + 78 + ## Key Ecosystem Components 79 + 80 + ### tauri Crate 81 + 82 + The central orchestrator that: 83 + - Reads `tauri.conf.json` at compile time 84 + - Manages script injection into the webview 85 + - Hosts the system interaction API 86 + - Handles application updates 87 + - Integrates runtimes, macros, and utilities 88 + 89 + ### tauri-runtime 90 + 91 + The glue layer between Tauri and lower-level webview libraries. Abstracts platform-specific webview interactions so the rest of Tauri can remain platform-agnostic. 92 + 93 + ### tauri-macros and tauri-codegen 94 + 95 + Generate compile-time code for: 96 + - Command handlers (`#[tauri::command]`) 97 + - Context and configuration parsing 98 + - Asset embedding and compression 99 + 100 + ### TAO (Window Management) 101 + 102 + Cross-platform window creation library (forked from Winit): 103 + - Creates and manages application windows 104 + - Handles menu bars and system trays 105 + - Supports Windows, macOS, Linux, iOS, Android 106 + 107 + ### WRY (WebView Rendering) 108 + 109 + Cross-platform WebView rendering library: 110 + - Abstracts webview implementations per platform 111 + - Handles webview-to-native communication 112 + - Manages JavaScript evaluation and event bridging 113 + 114 + ## Webview Integration 115 + 116 + Tauri uses the **operating system's native webview** rather than bundling a browser engine: 117 + 118 + ``` 119 + +-------------------+---------------------------+ 120 + | Platform | WebView Engine | 121 + +-------------------+---------------------------+ 122 + | Windows | WebView2 (Edge/Chromium) | 123 + | macOS | WKWebView (Safari/WebKit) | 124 + | Linux | WebKitGTK | 125 + | iOS | WKWebView | 126 + | Android | Android WebView | 127 + +-------------------+---------------------------+ 128 + ``` 129 + 130 + ### Benefits of Native WebViews 131 + 132 + 1. **Smaller binary size**: No bundled browser engine (~600KB vs ~150MB) 133 + 2. **Security**: OS vendors patch webview vulnerabilities faster than app developers can rebuild 134 + 3. **Performance**: Native integration with the operating system 135 + 4. **Consistency**: Users see familiar rendering behavior 136 + 137 + ### Considerations 138 + 139 + - Slight rendering differences between platforms 140 + - Feature availability depends on OS webview version 141 + - Testing should cover all target platforms 142 + 143 + ## Inter-Process Communication (IPC) 144 + 145 + Tauri implements **Asynchronous Message Passing** for communication between frontend and backend. This is safer than shared memory because the Core can reject malicious requests. 146 + 147 + ### IPC Flow Diagram 148 + 149 + ``` 150 + +------------------+ +------------------+ 151 + | Frontend | | Rust Backend | 152 + | (JavaScript) | | (Core) | 153 + +--------+---------+ +--------+---------+ 154 + | | 155 + | 1. invoke('command', {args}) | 156 + +---------------------------------------->| 157 + | | 158 + | [Request serialized as JSON-RPC] | 159 + | | 160 + | 2. Validate request | 161 + | 3. Check permissions | 162 + | 4. Execute command | 163 + | | 164 + | 5. Return Result<T, E> | 165 + |<----------------------------------------+ 166 + | | 167 + | [Response serialized as JSON] | 168 + | | 169 + ``` 170 + 171 + ### Two IPC Primitives 172 + 173 + #### Commands (Request-Response) 174 + 175 + Type-safe, frontend-to-backend function calls: 176 + 177 + ```rust 178 + // Rust backend 179 + #[tauri::command] 180 + fn greet(name: String) -> String { 181 + format!("Hello, {}!", name) 182 + } 183 + 184 + // Register in builder 185 + tauri::Builder::default() 186 + .invoke_handler(tauri::generate_handler![greet]) 187 + ``` 188 + 189 + ```javascript 190 + // JavaScript frontend 191 + import { invoke } from '@tauri-apps/api/core'; 192 + 193 + const greeting = await invoke('greet', { name: 'World' }); 194 + console.log(greeting); // "Hello, World!" 195 + ``` 196 + 197 + Key characteristics: 198 + - Built on JSON-RPC protocol 199 + - All arguments must be JSON-serializable 200 + - Returns a Promise that resolves with the result 201 + - Supports async Rust functions 202 + - Can access app state, window handle, etc. 203 + 204 + #### Events (Fire-and-Forget) 205 + 206 + Bidirectional, asynchronous notifications: 207 + 208 + ```javascript 209 + // Frontend: emit event 210 + import { emit } from '@tauri-apps/api/event'; 211 + emit('user-action', { action: 'clicked' }); 212 + 213 + // Frontend: listen for events 214 + import { listen } from '@tauri-apps/api/event'; 215 + const unlisten = await listen('download-progress', (event) => { 216 + console.log(event.payload); 217 + }); 218 + ``` 219 + 220 + ```rust 221 + // Backend: listen for events 222 + use tauri::Listener; 223 + 224 + app.listen("user-action", |event| { 225 + println!("User action: {}", event.payload()); 226 + }); 227 + 228 + // Backend: emit events 229 + app.emit("download-progress", 50)?; 230 + ``` 231 + 232 + Key characteristics: 233 + - No return value (one-way) 234 + - Both frontend and backend can emit/listen 235 + - Best for lifecycle events and state changes 236 + - Not type-checked at compile time 237 + 238 + ## Security Model Overview 239 + 240 + Tauri implements multiple layers of security to protect both the application and the user's system. 241 + 242 + ### Trust Boundary Model 243 + 244 + ``` 245 + +------------------------------------------------------------------+ 246 + | UNTRUSTED ZONE | 247 + | +------------------------------------------------------------+ | 248 + | | WebView Frontend | | 249 + | | - JavaScript code (potentially from remote sources) | | 250 + | | - User input | | 251 + | | - Third-party libraries | | 252 + | +------------------------------------------------------------+ | 253 + +------------------------------------------------------------------+ 254 + | 255 + [TRUST BOUNDARY] 256 + [IPC Layer validates all requests] 257 + | 258 + +------------------------------------------------------------------+ 259 + | TRUSTED ZONE | 260 + | +------------------------------------------------------------+ | 261 + | | Rust Backend | | 262 + | | - Your Rust code | | 263 + | | - Tauri core | | 264 + | | - System access (gated by permissions) | | 265 + | +------------------------------------------------------------+ | 266 + +------------------------------------------------------------------+ 267 + ``` 268 + 269 + ### Security Layers 270 + 271 + 1. **WebView Sandboxing**: Frontend code runs in the webview's security sandbox 272 + 2. **IPC Validation**: All messages crossing the trust boundary are validated 273 + 3. **Capabilities**: Define which permissions each window can access 274 + 4. **Permissions**: Fine-grained control over what operations are allowed 275 + 5. **Scopes**: Restrict command behavior (e.g., limit file access to specific directories) 276 + 6. **CSP**: Content Security Policy restricts what frontend code can do 277 + 278 + ### Capabilities System 279 + 280 + Capabilities control which permissions are granted to specific windows: 281 + 282 + ```json 283 + { 284 + "$schema": "../gen/schemas/desktop-schema.json", 285 + "identifier": "main-window-capability", 286 + "description": "Permissions for the main application window", 287 + "windows": ["main"], 288 + "permissions": [ 289 + "core:path:default", 290 + "core:window:allow-set-title", 291 + "fs:read-files", 292 + "fs:scope-app-data" 293 + ] 294 + } 295 + ``` 296 + 297 + Capabilities are defined in `src-tauri/capabilities/` as JSON or TOML files. 298 + 299 + ### Permission Structure 300 + 301 + ``` 302 + Capability 303 + | 304 + +-- windows: ["main", "settings"] // Which windows 305 + | 306 + +-- permissions: // What's allowed 307 + | 308 + +-- "plugin:action" // Allow specific action 309 + +-- "plugin:scope-xxx" // Scope restrictions 310 + ``` 311 + 312 + ### Default Security Posture 313 + 314 + - **Deny by default**: Commands must be explicitly permitted 315 + - **No remote access**: Only bundled code can access Tauri APIs by default 316 + - **Window isolation**: Each window has its own capability set 317 + - **Compile-time checks**: Many security configurations are validated at build time 318 + 319 + ## Rust Backend Structure 320 + 321 + A typical Tauri backend follows this structure: 322 + 323 + ``` 324 + src-tauri/ 325 + +-- Cargo.toml # Rust dependencies 326 + +-- tauri.conf.json # Tauri configuration 327 + +-- capabilities/ # Permission definitions 328 + | +-- main.json 329 + +-- src/ 330 + +-- main.rs # Entry point (desktop) 331 + +-- lib.rs # Core app logic 332 + +-- commands/ # Command modules 333 + | +-- mod.rs 334 + | +-- file_ops.rs 335 + +-- state.rs # App state management 336 + ``` 337 + 338 + ### Entry Point Pattern 339 + 340 + ```rust 341 + // src-tauri/src/lib.rs 342 + mod commands; 343 + 344 + #[cfg_attr(mobile, tauri::mobile_entry_point)] 345 + pub fn run() { 346 + tauri::Builder::default() 347 + .plugin(tauri_plugin_shell::init()) 348 + .invoke_handler(tauri::generate_handler![ 349 + commands::greet, 350 + commands::read_file, 351 + ]) 352 + .manage(AppState::default()) 353 + .run(tauri::generate_context!()) 354 + .expect("error while running tauri application"); 355 + } 356 + ``` 357 + 358 + ### Command Patterns 359 + 360 + ```rust 361 + // Basic command 362 + #[tauri::command] 363 + fn simple_command() -> String { 364 + "Hello".into() 365 + } 366 + 367 + // With arguments (camelCase from JS, snake_case in Rust) 368 + #[tauri::command] 369 + fn with_args(user_name: String, age: u32) -> String { 370 + format!("{} is {} years old", user_name, age) 371 + } 372 + 373 + // With error handling 374 + #[tauri::command] 375 + fn fallible() -> Result<String, String> { 376 + Ok("Success".into()) 377 + } 378 + 379 + // Async command 380 + #[tauri::command] 381 + async fn async_command() -> Result<String, String> { 382 + tokio::time::sleep(Duration::from_secs(1)).await; 383 + Ok("Done".into()) 384 + } 385 + 386 + // Accessing app state 387 + #[tauri::command] 388 + fn with_state(state: tauri::State<'_, AppState>) -> String { 389 + state.get_value() 390 + } 391 + 392 + // Accessing window 393 + #[tauri::command] 394 + fn with_window(window: tauri::WebviewWindow) -> String { 395 + window.label().to_string() 396 + } 397 + ``` 398 + 399 + ## No Runtime Bundled 400 + 401 + Tauri does NOT ship a runtime. The final binary: 402 + - Compiles Rust code directly into native machine code 403 + - Embeds frontend assets in the binary 404 + - Uses the system's native webview 405 + - Results in small, fast executables 406 + 407 + This makes reverse engineering Tauri apps non-trivial compared to Electron apps with bundled JavaScript. 408 + 409 + ## Summary 410 + 411 + | Component | Role | 412 + |-----------|------| 413 + | **Core (Rust)** | System access, security, business logic | 414 + | **Shell (Frontend)** | UI rendering, user interaction | 415 + | **WebView (TAO+WRY)** | Platform-native rendering bridge | 416 + | **IPC (Commands/Events)** | Safe message passing between layers | 417 + | **Capabilities** | Permission control per window | 418 + 419 + The architecture prioritizes: 420 + 1. **Security**: Multiple layers of protection, trust boundaries 421 + 2. **Performance**: Native code, no bundled runtime 422 + 3. **Size**: Minimal binary footprint 423 + 4. **Flexibility**: Any frontend framework, powerful Rust backend
+391
.agents/skills/understanding-tauri-ecosystem-security/SKILL.md
··· 1 + --- 2 + name: understanding-tauri-ecosystem-security 3 + description: Guides developers through Tauri ecosystem security practices including security auditing, dependency management, vulnerability reporting, and organizational security measures for building secure desktop applications. 4 + --- 5 + 6 + # Understanding Tauri Ecosystem Security 7 + 8 + This skill covers Tauri's organizational security practices, dependency management, vulnerability reporting, and comprehensive security auditing approaches. 9 + 10 + ## Tauri Security Philosophy 11 + 12 + Tauri operates on a principle of defense-in-depth with human-in-the-loop oversight. The framework acknowledges that "the weakest link in your application lifecycle essentially defines your security" and provides mechanisms to address threats at every stage. 13 + 14 + ### Trust Boundaries 15 + 16 + Tauri distinguishes between: 17 + 18 + - **Rust backend code**: Trusted, with full system access 19 + - **Frontend code**: Untrusted, runs in the system WebView 20 + - **IPC layer**: The communication bridge enforcing security boundaries 21 + 22 + Frontend code accesses system resources exclusively through the IPC layer, with permissions restricted by capabilities defined in application configuration. 23 + 24 + ## Organizational Security Practices 25 + 26 + ### Build Pipeline Security 27 + 28 + The Tauri organization uses highly automated GitHub Actions workflows with mandatory human review and approval before deployment. 29 + 30 + Key practices: 31 + 32 + - **Signed commits**: Core repositories enforce signed commits to mitigate impersonation risks 33 + - **Code review**: Every pull request requires approval from at least one maintainer 34 + - **Security workflows**: Default security checks run on all code changes 35 + 36 + ### Release Procedures 37 + 38 + The working group manages releases through: 39 + 40 + 1. Review code modifications and categorize PRs by scope 41 + 2. Maintain current dependencies 42 + 3. Conduct internal security audits for security-related PRs before minor and major releases 43 + 4. Tag releases on the development branch, triggering: 44 + - Core functionality validation 45 + - Test execution 46 + - Security audits of dependencies 47 + - Changelog generation 48 + - Artifact creation 49 + 5. Review and edit release notes before publication 50 + 51 + ## Dependency Security 52 + 53 + ### Auditing Dependencies 54 + 55 + Use automated tools to identify vulnerable packages: 56 + 57 + ```bash 58 + # Rust dependencies 59 + cargo audit 60 + 61 + # Node.js dependencies 62 + npm audit 63 + ``` 64 + 65 + ### Advanced Supply Chain Tools 66 + 67 + Consider emerging tools for deeper supply chain analysis: 68 + 69 + ```bash 70 + # Verify dependencies against trusted sources 71 + cargo vet 72 + 73 + # Community-driven code reviews 74 + cargo crev 75 + ``` 76 + 77 + ### Dependency Pinning 78 + 79 + For critical dependencies, pin to specific git hash revisions rather than floating versions: 80 + 81 + ```toml 82 + # Cargo.toml - pinned dependency 83 + [dependencies] 84 + critical-lib = { git = "https://github.com/org/repo", rev = "abc123def456" } 85 + ``` 86 + 87 + ### Keeping Dependencies Updated 88 + 89 + Regularly update Tauri, compilers, and related tooling: 90 + 91 + ```bash 92 + # Update Rust toolchain 93 + rustup update 94 + 95 + # Update Tauri CLI 96 + cargo install tauri-cli --locked 97 + 98 + # Check for outdated dependencies 99 + cargo outdated 100 + ``` 101 + 102 + ## Application Lifecycle Security 103 + 104 + ### Upstream Threats 105 + 106 + **Evaluate third-party libraries for:** 107 + 108 + - Trustworthiness of maintainers 109 + - Maintenance status and update frequency 110 + - Known vulnerabilities 111 + - Code quality and review practices 112 + 113 + ### Development Threats 114 + 115 + **Development server risks:** 116 + 117 + The default development server lacks encryption and authentication, exposing frontend assets to local networks. Only develop on trusted networks or implement mutual TLS (mTLS) for untrusted environments. 118 + 119 + **Machine hardening practices:** 120 + 121 + - Avoid administrative accounts for daily coding 122 + - Never store production secrets on development machines 123 + - Prevent secrets from entering version control 124 + - Use hardware security tokens 125 + - Maintain minimal installed applications 126 + - Keep systems fully patched 127 + 128 + **Source control security:** 129 + 130 + - Implement proper access controls for repositories 131 + - Require commit signing from all contributors 132 + 133 + ### Buildtime Threats 134 + 135 + **CI/CD infrastructure:** 136 + 137 + Use reputable providers or host systems on controlled hardware. Pin action versions explicitly in workflows: 138 + 139 + ```yaml 140 + # Good - pinned to specific version 141 + - uses: actions/checkout@v4.1.1 142 + 143 + # Bad - floating tag 144 + - uses: actions/checkout@latest 145 + ``` 146 + 147 + **Reproducible builds:** 148 + 149 + Current challenge: Rust and many frontend bundlers do not reliably produce reproducible builds by default. Maintain high trust in CI/CD systems until reproducibility tooling improves. 150 + 151 + ### Distribution Threats 152 + 153 + Control over manifest servers, build systems, and binary hosting is essential. Consider trusted third-party solutions for binary distribution. 154 + 155 + ### Runtime Threats 156 + 157 + Tauri assumes webview insecurity and implements protections via: 158 + 159 + - Content Security Policy (CSP) 160 + - Capabilities system 161 + - Runtime authority validation 162 + 163 + ## Content Security Policy 164 + 165 + CSP mitigates cross-site scripting (XSS) attacks. Tauri automatically handles cryptographic protections for bundled assets. 166 + 167 + ### CSP Configuration 168 + 169 + ```json 170 + { 171 + "app": { 172 + "security": { 173 + "csp": { 174 + "default-src": "'self' customprotocol: asset:", 175 + "connect-src": "ipc: http://ipc.localhost", 176 + "font-src": ["https://fonts.gstatic.com"], 177 + "img-src": "'self' asset: http://asset.localhost blob: data:", 178 + "style-src": "'unsafe-inline' 'self' https://fonts.googleapis.com" 179 + } 180 + } 181 + } 182 + } 183 + ``` 184 + 185 + ### CSP Best Practices 186 + 187 + - Make policies as restrictive as possible 188 + - Whitelist only trusted, preferably self-owned hosts 189 + - Avoid remote scripts from CDNs (they introduce attack vectors) 190 + - For WebAssembly frontends, include `'wasm-unsafe-eval'` in `script-src` 191 + 192 + ## Permissions and Capabilities 193 + 194 + ### Permission Structure 195 + 196 + Permissions describe explicit privileges governing frontend command access: 197 + 198 + ```toml 199 + # src-tauri/permissions/my-permission.toml 200 + [[permission]] 201 + identifier = "my-identifier" 202 + description = "Describes the impact and scope" 203 + commands.allow = ["read_file"] 204 + 205 + [[scope.allow]] 206 + my-scope = "$HOME/*" 207 + 208 + [[scope.deny]] 209 + my-scope = "$HOME/secret" 210 + ``` 211 + 212 + ### Capability Configuration 213 + 214 + Capabilities grant permissions to specific windows or webviews: 215 + 216 + ```json 217 + { 218 + "identifier": "main-window-capability", 219 + "description": "Capability for the main window", 220 + "windows": ["main"], 221 + "permissions": [ 222 + "core:default", 223 + "fs:read-files", 224 + "fs:scope-home" 225 + ] 226 + } 227 + ``` 228 + 229 + ### Security Boundaries 230 + 231 + **Capabilities protect against:** 232 + 233 + - Frontend compromise impact minimization 234 + - Accidental system data exposure 235 + - Privilege escalation from frontend to backend 236 + 237 + **Capabilities do NOT protect against:** 238 + 239 + - Malicious Rust code 240 + - Overly permissive scopes 241 + - WebView zero-day vulnerabilities 242 + - Supply chain attacks 243 + 244 + ### Command Scopes 245 + 246 + Scopes provide granular control with allow and deny rules (deny always supersedes allow): 247 + 248 + ```toml 249 + # Allow recursive directory access 250 + [[scope.allow]] 251 + path = "$APPLOCALDATA/**" 252 + 253 + # Deny sensitive folders 254 + [[scope.deny]] 255 + path = "$APPLOCALDATA/EBWebView" 256 + ``` 257 + 258 + Command developers must ensure no scope bypasses are possible through careful validation. 259 + 260 + ## Runtime Authority 261 + 262 + The runtime authority manages security enforcement at runtime: 263 + 264 + 1. Intercepts IPC requests from webview 265 + 2. Validates origin authorization 266 + 3. Confirms capability inclusion 267 + 4. Applies command-specific scopes 268 + 5. Permits or denies execution 269 + 270 + This multi-layer validation creates defense-in-depth against privilege escalation. 271 + 272 + ## Vulnerability Reporting 273 + 274 + ### How to Report 275 + 276 + Report vulnerabilities privately through: 277 + 278 + - **Preferred**: GitHub Private Vulnerability Disclosure feature 279 + - **Alternative**: Email to security@tauri.app 280 + 281 + ### What NOT to Do 282 + 283 + Do not disclose vulnerabilities via: 284 + 285 + - Pull requests 286 + - GitHub issues 287 + - Discord 288 + - Forum posts 289 + 290 + ### Disclosure Process 291 + 292 + The Tauri team commits to: 293 + 294 + - Triaging reports promptly 295 + - Maintaining confidentiality during investigation 296 + - Following 90-day standard for coordinated public disclosure 297 + - Offering optional public attribution 298 + 299 + ### Supported Versions 300 + 301 + Only Tauri versions greater than 1.0 receive security support. Earlier versions receive no security updates. 302 + 303 + ## Security Audit Checklist 304 + 305 + ### Pre-Release Audit 306 + 307 + ```markdown 308 + ## Dependency Audit 309 + - [ ] Run `cargo audit` - no critical vulnerabilities 310 + - [ ] Run `npm audit` - no critical vulnerabilities 311 + - [ ] Review new dependencies for trustworthiness 312 + - [ ] Check dependency update status 313 + 314 + ## Configuration Audit 315 + - [ ] CSP configured and restrictive 316 + - [ ] Capabilities follow least-privilege principle 317 + - [ ] Scopes properly deny sensitive paths 318 + - [ ] No overly permissive glob patterns 319 + 320 + ## Code Audit 321 + - [ ] IPC commands validate all inputs 322 + - [ ] No scope bypass vulnerabilities 323 + - [ ] Secrets not hardcoded or logged 324 + - [ ] Error messages do not leak sensitive info 325 + 326 + ## Build Audit 327 + - [ ] CI/CD actions pinned to specific versions 328 + - [ ] Build artifacts signed 329 + - [ ] Distribution channels secured 330 + ``` 331 + 332 + ### Periodic Security Review 333 + 334 + ```markdown 335 + ## Upstream Review 336 + - [ ] Tauri updated to latest stable 337 + - [ ] Rust toolchain updated 338 + - [ ] Frontend dependencies updated 339 + - [ ] Known CVEs addressed 340 + 341 + ## Access Control Review 342 + - [ ] Repository access appropriate 343 + - [ ] Commit signing enforced 344 + - [ ] CI/CD secrets rotated 345 + - [ ] Development machine security verified 346 + 347 + ## Runtime Review 348 + - [ ] WebView security patches applied (OS updates) 349 + - [ ] Capability configuration still appropriate 350 + - [ ] No deprecated permissions in use 351 + ``` 352 + 353 + ## Known Security Advisory Patterns 354 + 355 + Based on historical advisories, watch for: 356 + 357 + 1. **iFrame bypass vulnerabilities**: Origin checks may be circumvented 358 + 2. **Filesystem scope issues**: Glob patterns may be overly permissive 359 + 3. **Symbolic link bypasses**: File operations may follow symlinks unexpectedly 360 + 4. **Open redirect risks**: External sites may access IPC 361 + 5. **Dotfile handling**: Hidden files may bypass scope restrictions 362 + 363 + ## Security Resources 364 + 365 + ### Official Channels 366 + 367 + - Tauri Security Documentation: https://v2.tauri.app/security/ 368 + - GitHub Security Advisories: https://github.com/tauri-apps/tauri/security/advisories 369 + - Security Contact: security@tauri.app 370 + 371 + ### Recommended Tools 372 + 373 + | Tool | Purpose | 374 + |------|---------| 375 + | `cargo audit` | Rust vulnerability scanning | 376 + | `npm audit` | Node.js vulnerability scanning | 377 + | `cargo vet` | Dependency verification | 378 + | `cargo crev` | Community code reviews | 379 + | `cargo outdated` | Dependency freshness | 380 + 381 + ## Summary 382 + 383 + Tauri ecosystem security requires attention across the entire application lifecycle: 384 + 385 + 1. **Upstream**: Audit and pin dependencies 386 + 2. **Development**: Harden machines, secure source control 387 + 3. **Build**: Secure CI/CD, pin action versions 388 + 4. **Distribution**: Control hosting infrastructure 389 + 5. **Runtime**: Configure CSP, capabilities, and scopes 390 + 391 + The framework provides robust security primitives, but their effectiveness depends on proper configuration and ongoing vigilance. Regular audits, prompt vulnerability patching, and following least-privilege principles are essential for maintaining secure Tauri applications.
+423
.agents/skills/understanding-tauri-ipc/SKILL.md
··· 1 + --- 2 + name: understanding-tauri-ipc 3 + description: Teaches the assistant about Tauri IPC (Inter-Process Communication) patterns including brownfield and isolation approaches for secure message passing between frontend and Rust backend. 4 + --- 5 + 6 + # Tauri Inter-Process Communication (IPC) 7 + 8 + This skill covers Tauri's IPC system, including the brownfield and isolation patterns for secure communication between frontend and backend processes. 9 + 10 + ## Overview 11 + 12 + Tauri implements Inter-Process Communication using **Asynchronous Message Passing**. This enables isolated processes to exchange serialized requests and responses securely. 13 + 14 + **Why Message Passing?** 15 + - Safer than shared memory or direct function access 16 + - Recipients can reject or discard malicious requests 17 + - Tauri Core validates all requests before execution 18 + - Prevents unauthorized function invocation 19 + 20 + ## IPC Primitives 21 + 22 + Tauri provides two IPC primitives: 23 + 24 + ### Events 25 + 26 + - **Direction**: Bidirectional (Frontend <-> Tauri Core) 27 + - **Type**: Fire-and-forget, one-way messaging 28 + - **Best for**: Lifecycle events, state changes, notifications 29 + 30 + **Rust (emit to frontend):** 31 + ```rust 32 + use tauri::{AppHandle, Emitter}; 33 + 34 + fn emit_event(app: &AppHandle) { 35 + app.emit("backend-event", "payload data").unwrap(); 36 + } 37 + ``` 38 + 39 + **Frontend (listen):** 40 + ```typescript 41 + import { listen } from '@tauri-apps/api/event'; 42 + 43 + const unlisten = await listen('backend-event', (event) => { 44 + console.log('Received:', event.payload); 45 + }); 46 + 47 + // Call unlisten() when done 48 + ``` 49 + 50 + **Frontend (emit to backend):** 51 + ```typescript 52 + import { emit } from '@tauri-apps/api/event'; 53 + 54 + await emit('frontend-event', { data: 'value' }); 55 + ``` 56 + 57 + ### Commands 58 + 59 + - **Direction**: Frontend -> Rust backend 60 + - **Protocol**: JSON-RPC-based abstraction 61 + - **API**: Similar to browser's `fetch()` API 62 + - **Requirement**: Arguments and return data must be JSON-serializable 63 + 64 + **Rust command definition:** 65 + ```rust 66 + #[tauri::command] 67 + fn greet(name: &str) -> String { 68 + format!("Hello, {}!", name) 69 + } 70 + 71 + fn main() { 72 + tauri::Builder::default() 73 + .invoke_handler(tauri::generate_handler![greet]) 74 + .run(tauri::generate_context!()) 75 + .expect("error while running tauri application"); 76 + } 77 + ``` 78 + 79 + **Frontend invocation:** 80 + ```typescript 81 + import { invoke } from '@tauri-apps/api/core'; 82 + 83 + const greeting = await invoke('greet', { name: 'World' }); 84 + console.log(greeting); // "Hello, World!" 85 + ``` 86 + 87 + **Async command with Result:** 88 + ```rust 89 + #[tauri::command] 90 + async fn read_file(path: String) -> Result<String, String> { 91 + std::fs::read_to_string(&path) 92 + .map_err(|e| e.to_string()) 93 + } 94 + ``` 95 + 96 + ## IPC Patterns 97 + 98 + Tauri provides two IPC security patterns: **Brownfield** (default) and **Isolation**. 99 + 100 + --- 101 + 102 + ## Brownfield Pattern 103 + 104 + ### What It Is 105 + 106 + The brownfield pattern is Tauri's **default** IPC approach. It prioritizes compatibility with existing web frontend projects by requiring minimal modifications. 107 + 108 + ### When to Use 109 + 110 + - Migrating existing web applications to desktop 111 + - Rapid prototyping and development 112 + - Applications with trusted frontend code 113 + - Simple applications with limited IPC surface 114 + 115 + ### Why Use It 116 + 117 + - Zero configuration required 118 + - Minimal changes to existing web code 119 + - Direct access to Tauri APIs 120 + - Fastest development path 121 + 122 + ### Configuration 123 + 124 + Brownfield is the default. Explicit configuration is optional: 125 + 126 + ```json 127 + { 128 + "app": { 129 + "security": { 130 + "pattern": { 131 + "use": "brownfield" 132 + } 133 + } 134 + } 135 + } 136 + ``` 137 + 138 + **Note:** There are no additional configuration options for brownfield. 139 + 140 + ### Code Example 141 + 142 + **Rust backend:** 143 + ```rust 144 + #[tauri::command] 145 + fn process_data(input: String) -> Result<String, String> { 146 + // Direct processing without isolation layer 147 + Ok(format!("Processed: {}", input)) 148 + } 149 + ``` 150 + 151 + **Frontend:** 152 + ```typescript 153 + import { invoke } from '@tauri-apps/api/core'; 154 + 155 + // Direct invocation - no isolation layer 156 + const result = await invoke('process_data', { input: 'test' }); 157 + ``` 158 + 159 + ### Security Considerations 160 + 161 + - Frontend code has direct access to all exposed commands 162 + - No additional validation layer between frontend and backend 163 + - Supply chain attacks in frontend dependencies could invoke commands 164 + - Rely on command-level validation in Rust 165 + 166 + --- 167 + 168 + ## Isolation Pattern 169 + 170 + ### What It Is 171 + 172 + The isolation pattern intercepts and modifies **all** Tauri API messages from the frontend using JavaScript before they reach Tauri Core. A secure JavaScript application (the Isolation application) runs in a sandboxed iframe to validate and encrypt all IPC communications. 173 + 174 + ### When to Use 175 + 176 + - Applications with many frontend dependencies 177 + - High-security requirements 178 + - Handling sensitive data or operations 179 + - Public-facing applications 180 + - When supply chain attacks are a concern 181 + 182 + ### Why Use It 183 + 184 + **Protection against Development Threats:** 185 + - Validates all IPC calls before execution 186 + - Catches malicious or unwanted frontend calls 187 + - Mitigates supply chain attack risks 188 + - Provides a checkpoint for all communications 189 + 190 + **Tauri recommends using isolation whenever feasible.** 191 + 192 + ### How It Works 193 + 194 + 1. Tauri's IPC handler receives a message from frontend 195 + 2. Message routes to the Isolation application (sandboxed iframe) 196 + 3. Isolation hook validates and potentially modifies the message 197 + 4. Message encrypts using AES-GCM with runtime-generated keys 198 + 5. Encrypted message returns to IPC handler 199 + 6. Encrypted message passes to Tauri Core for decryption and execution 200 + 201 + **Key Security Features:** 202 + - New encryption keys generated on each application launch 203 + - Sandboxed iframe prevents isolation code manipulation 204 + - All IPC calls validated, including event-based APIs 205 + 206 + ### Configuration 207 + 208 + **tauri.conf.json:** 209 + ```json 210 + { 211 + "app": { 212 + "security": { 213 + "pattern": { 214 + "use": "isolation", 215 + "options": { 216 + "dir": "../dist-isolation" 217 + } 218 + } 219 + } 220 + } 221 + } 222 + ``` 223 + 224 + ### Code Example 225 + 226 + **Directory structure:** 227 + ``` 228 + project/ 229 + src/ # Main frontend 230 + src-tauri/ # Rust backend 231 + dist-isolation/ 232 + index.html 233 + index.js 234 + ``` 235 + 236 + **dist-isolation/index.html:** 237 + ```html 238 + <!DOCTYPE html> 239 + <html lang="en"> 240 + <head> 241 + <meta charset="UTF-8" /> 242 + <title>Isolation Secure Script</title> 243 + </head> 244 + <body> 245 + <script src="index.js"></script> 246 + </body> 247 + </html> 248 + ``` 249 + 250 + **dist-isolation/index.js:** 251 + ```javascript 252 + window.__TAURI_ISOLATION_HOOK__ = (payload) => { 253 + // Log all IPC calls for debugging 254 + console.log('IPC call intercepted:', payload); 255 + 256 + // Return payload unchanged (passthrough) 257 + return payload; 258 + }; 259 + ``` 260 + 261 + **Validation example (index.js):** 262 + ```javascript 263 + window.__TAURI_ISOLATION_HOOK__ = (payload) => { 264 + // Validate command calls 265 + if (payload.cmd === 'invoke') { 266 + const { __tauriModule, message } = payload; 267 + 268 + // Block unauthorized file system access 269 + if (message.cmd === 'readFile') { 270 + const path = message.path; 271 + if (!path.startsWith('/allowed/directory/')) { 272 + console.error('Blocked unauthorized file access:', path); 273 + return null; // Block the request 274 + } 275 + } 276 + 277 + // Validate specific commands 278 + if (message.cmd === 'deleteItem') { 279 + if (!confirm('Are you sure you want to delete this item?')) { 280 + return null; // User cancelled 281 + } 282 + } 283 + } 284 + 285 + return payload; 286 + }; 287 + ``` 288 + 289 + **Comprehensive validation example:** 290 + ```javascript 291 + const ALLOWED_COMMANDS = ['greet', 'read_config', 'save_settings']; 292 + const BLOCKED_PATHS = ['/etc/', '/usr/', '/System/']; 293 + 294 + window.__TAURI_ISOLATION_HOOK__ = (payload) => { 295 + // Validate invoke commands 296 + if (payload.cmd === 'invoke') { 297 + const commandName = payload.message?.cmd; 298 + 299 + // Whitelist approach 300 + if (!ALLOWED_COMMANDS.includes(commandName)) { 301 + console.warn('Blocked unknown command:', commandName); 302 + return null; 303 + } 304 + 305 + // Validate path arguments 306 + const args = payload.message?.args || {}; 307 + if (args.path) { 308 + for (const blocked of BLOCKED_PATHS) { 309 + if (args.path.startsWith(blocked)) { 310 + console.error('Blocked access to protected path:', args.path); 311 + return null; 312 + } 313 + } 314 + } 315 + } 316 + 317 + // Validate event emissions 318 + if (payload.cmd === 'emit') { 319 + const eventName = payload.event; 320 + // Add event validation as needed 321 + } 322 + 323 + return payload; 324 + }; 325 + ``` 326 + 327 + ### Performance Considerations 328 + 329 + - AES-GCM encryption overhead is minimal for most applications 330 + - Comparable to TLS encryption used in HTTPS 331 + - Key generation requires system entropy (handled seamlessly on modern systems) 332 + - Performance-sensitive applications may notice slight impact 333 + 334 + ### Limitations 335 + 336 + - ES Modules do not load in sandboxed iframes on Windows 337 + - Scripts must be inlined during build time 338 + - External files must be embedded rather than referenced 339 + - Avoid bundlers for the isolation application 340 + 341 + ### Best Practices 342 + 343 + 1. **Keep it simple**: Minimize isolation application dependencies 344 + 2. **No bundlers**: Skip ES Modules and complex build processes 345 + 3. **Validate inputs**: Verify IPC calls match expected parameters 346 + 4. **Whitelist commands**: Only allow known, safe commands 347 + 5. **Log suspicious activity**: Monitor for potential attacks 348 + 6. **Apply to events**: Validate events that trigger Rust code 349 + 350 + --- 351 + 352 + ## Pattern Comparison 353 + 354 + | Aspect | Brownfield | Isolation | 355 + |--------|------------|-----------| 356 + | Default | Yes | No | 357 + | Configuration | None required | Requires isolation app | 358 + | Security | Basic | Enhanced | 359 + | Validation | Command-level only | All IPC calls | 360 + | Encryption | None | AES-GCM | 361 + | Performance | Fastest | Slight overhead | 362 + | Complexity | Simple | Moderate | 363 + | Best for | Trusted code, prototypes | Production, sensitive apps | 364 + 365 + ## Security Best Practices 366 + 367 + ### For Both Patterns 368 + 369 + 1. **Validate all inputs in Rust commands** 370 + ```rust 371 + #[tauri::command] 372 + fn process_file(path: String) -> Result<String, String> { 373 + // Always validate paths 374 + if path.contains("..") || path.starts_with("/etc") { 375 + return Err("Invalid path".into()); 376 + } 377 + // Process file... 378 + Ok("Done".into()) 379 + } 380 + ``` 381 + 382 + 2. **Use typed arguments** 383 + ```rust 384 + #[derive(serde::Deserialize)] 385 + struct CreateUserArgs { 386 + name: String, 387 + email: String, 388 + } 389 + 390 + #[tauri::command] 391 + fn create_user(args: CreateUserArgs) -> Result<(), String> { 392 + // Type-safe argument handling 393 + Ok(()) 394 + } 395 + ``` 396 + 397 + 3. **Limit exposed commands**: Only expose necessary functionality 398 + 399 + 4. **Use capability-based permissions**: Configure permissions in `capabilities/` 400 + 401 + ### For Isolation Pattern 402 + 403 + 1. **Keep isolation code minimal**: Reduce attack surface 404 + 2. **Avoid external dependencies**: No npm packages in isolation app 405 + 3. **Use strict validation**: Whitelist over blacklist 406 + 4. **Test thoroughly**: Ensure validation catches edge cases 407 + 408 + ## Choosing a Pattern 409 + 410 + **Use Brownfield when:** 411 + - Building internal tools 412 + - Prototyping rapidly 413 + - Frontend code is fully trusted 414 + - Minimal security requirements 415 + 416 + **Use Isolation when:** 417 + - Building public applications 418 + - Handling sensitive user data 419 + - Using many third-party frontend packages 420 + - Security is a priority 421 + - Compliance requirements exist 422 + 423 + When in doubt, **prefer isolation** for production applications.
+377
.agents/skills/understanding-tauri-lifecycle-security/SKILL.md
··· 1 + --- 2 + name: understanding-tauri-lifecycle-security 3 + description: Assists developers with understanding Tauri application lifecycle security threats spanning development, build, distribution, and runtime phases, including threat mitigation strategies and security best practices. 4 + --- 5 + 6 + # Tauri Application Lifecycle Security 7 + 8 + Security in Tauri applications depends on systematic protection across all lifecycle stages. The weakest link in your application lifecycle essentially defines your security posture. 9 + 10 + ## Core Security Principle 11 + 12 + Tauri implements a two-tier security model: 13 + - **Rust Core**: Full system access 14 + - **WebView Frontend**: Access only through controlled IPC layer 15 + 16 + Any code executed in the WebView has only access to exposed system resources via the well-defined IPC layer. 17 + 18 + --- 19 + 20 + ## Development Phase Threats 21 + 22 + ### Upstream Dependency Risks 23 + 24 + Third-party dependencies may lack the strict oversight that Tauri maintains. 25 + 26 + **Mitigation Strategies:** 27 + 28 + ```bash 29 + # Scan Rust dependencies for vulnerabilities 30 + cargo audit 31 + 32 + # Scan npm dependencies 33 + npm audit 34 + 35 + # Advanced supply chain analysis 36 + cargo vet 37 + cargo crev 38 + cargo supply-chain 39 + ``` 40 + 41 + **Best Practices:** 42 + - Keep Tauri, `rustc`, and `nodejs` current to patch vulnerabilities 43 + - Evaluate trustworthiness of third-party libraries before integration 44 + - Prefer consuming critical dependencies via git hash revisions rather than version ranges 45 + 46 + ```toml 47 + # Cargo.toml - Pin to specific commit hash 48 + [dependencies] 49 + critical-lib = { git = "https://github.com/org/critical-lib", rev = "abc123def456" } 50 + ``` 51 + 52 + ### Development Server Exposure 53 + 54 + Development servers typically run unencrypted and unauthenticated on local networks, allowing attackers to push malicious frontend code to development devices. 55 + 56 + **Threat Scenario:** 57 + ``` 58 + Attacker on same network -> Intercepts dev server traffic -> Injects malicious frontend code 59 + ``` 60 + 61 + **Mitigation:** 62 + - Develop only on trusted networks 63 + - Implement mutual TLS (mTLS) authentication when necessary 64 + - Note: Tauri's built-in dev server lacks mutual authentication features 65 + 66 + ### Machine Hardening 67 + 68 + | Practice | Purpose | 69 + |----------|---------| 70 + | Avoid admin accounts for coding | Limit blast radius of compromise | 71 + | Block secrets from version control | Prevent credential leaks | 72 + | Use hardware security tokens | Minimize compromise impact | 73 + | Minimize installed applications | Reduce attack surface | 74 + 75 + ### Source Control Security 76 + 77 + **Required Protections:** 78 + - Implement proper access controls in version control systems 79 + - Require contributor commit signing to prevent unauthorized attribution 80 + - Use established hardening guidelines for authentication workflows 81 + 82 + ```bash 83 + # Enable commit signing 84 + git config --global commit.gpgsign true 85 + git config --global user.signingkey YOUR_KEY_ID 86 + ``` 87 + 88 + --- 89 + 90 + ## Build Phase Threats 91 + 92 + ### Build System Trust 93 + 94 + CI/CD systems access source code, secrets, and can modify builds without local verification. 95 + 96 + **Threat Vectors:** 97 + 1. Compromised CI/CD provider 98 + 2. Malicious build scripts 99 + 3. Unauthorized secret access 100 + 4. Build artifact tampering 101 + 102 + **Mitigation Options:** 103 + - Trust reputable third-party providers (GitHub Actions, GitLab CI) 104 + - Host and control your own infrastructure for sensitive applications 105 + 106 + ### Binary Signing 107 + 108 + Applications must be cryptographically signed for their target platform. 109 + 110 + **Platform Requirements:** 111 + 112 + | Platform | Signing Requirement | 113 + |----------|---------------------| 114 + | macOS | Apple Developer Certificate + Notarization | 115 + | Windows | Code Signing Certificate (EV recommended) | 116 + | Linux | GPG signing for packages | 117 + 118 + **Key Protection:** 119 + ```bash 120 + # Use hardware tokens for signing credentials 121 + # Prevents compromised build systems from leaking keys 122 + 123 + # Example: Using YubiKey for code signing 124 + pkcs11-tool --module /usr/lib/opensc-pkcs11.so --sign 125 + ``` 126 + 127 + Hardware tokens prevent key exfiltration but cannot prevent key misuse on a compromised system. 128 + 129 + ### Reproducible Builds Challenge 130 + 131 + Rust is not fully reliable at producing reproducible builds despite theoretical support. Frontend bundlers similarly struggle with reproducible output. 132 + 133 + **Implications:** 134 + - Cannot entirely eliminate reliance on build system trust 135 + - Implement multiple verification layers 136 + - Consider build provenance attestation 137 + 138 + --- 139 + 140 + ## Distribution Threats 141 + 142 + Loss of control over manifest servers, build servers, or binary hosting creates critical vulnerability points. 143 + 144 + ### Attack Vectors 145 + 146 + ``` 147 + Manifest Server Compromise -> Malicious update metadata -> Users download tampered binaries 148 + Build Server Compromise -> Injected malware at build time -> Signed malicious releases 149 + Binary Host Compromise -> Replaced binaries -> Users download malicious versions 150 + ``` 151 + 152 + ### Mitigation Strategies 153 + 154 + 1. **Secure Update Channels** 155 + - Use HTTPS for all update communications 156 + - Implement certificate pinning where possible 157 + - Verify update signatures client-side 158 + 159 + 2. **Binary Integrity** 160 + - Publish checksums alongside releases 161 + - Use signed manifests for updates 162 + - Consider transparency logs 163 + 164 + 3. **Infrastructure Security** 165 + - Multi-factor authentication for all distribution systems 166 + - Audit logging for binary access 167 + - Separate credentials for different environments 168 + 169 + --- 170 + 171 + ## Runtime Threats 172 + 173 + ### WebView Security Model 174 + 175 + Tauri assumes webview components are inherently insecure and implements multiple protection layers. 176 + 177 + **Defense Layers:** 178 + 179 + ``` 180 + +------------------+ 181 + | Untrusted | 182 + | Frontend Code | 183 + +--------+---------+ 184 + | 185 + +--------v---------+ 186 + | CSP | <- Restricts communication types 187 + +--------+---------+ 188 + | 189 + +--------v---------+ 190 + | Capabilities | <- Controls API access 191 + +--------+---------+ 192 + | 193 + +--------v---------+ 194 + | Permissions | <- Fine-grained command control 195 + +--------+---------+ 196 + | 197 + +--------v---------+ 198 + | Scopes | <- Resource-level restrictions 199 + +--------+---------+ 200 + | 201 + +--------v---------+ 202 + | Rust Backend | <- Trusted system access 203 + +------------------+ 204 + ``` 205 + 206 + ### Content Security Policy (CSP) 207 + 208 + CSP restricts webview communication types to prevent XSS and injection attacks. 209 + 210 + **Configuration in `tauri.conf.json`:** 211 + 212 + ```json 213 + { 214 + "app": { 215 + "security": { 216 + "csp": "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'" 217 + } 218 + } 219 + } 220 + ``` 221 + 222 + **CSP Best Practices:** 223 + - Start with restrictive policy, relax only as needed 224 + - Avoid `'unsafe-eval'` and `'unsafe-inline'` for scripts 225 + - Use nonces or hashes for inline scripts when required 226 + 227 + ### Capabilities Configuration 228 + 229 + Define which permissions are granted to specific windows. 230 + 231 + **Example: `src-tauri/capabilities/main.json`** 232 + 233 + ```json 234 + { 235 + "$schema": "../gen/schemas/desktop-schema.json", 236 + "identifier": "main-capability", 237 + "description": "Capability for the main window", 238 + "windows": ["main"], 239 + "permissions": [ 240 + "core:path:default", 241 + "core:window:allow-set-title", 242 + "fs:read-files" 243 + ] 244 + } 245 + ``` 246 + 247 + **Security Notes:** 248 + - Windows in multiple capabilities merge security boundaries 249 + - Security boundaries depend on window labels, not titles 250 + - Capabilities protect against frontend compromise and privilege escalation 251 + 252 + ### Permission Scopes 253 + 254 + Control resource access at a granular level. 255 + 256 + **Example: File System Scope** 257 + 258 + ```toml 259 + # src-tauri/permissions/fs-restricted.toml 260 + [[permission]] 261 + identifier = "fs-home-restricted" 262 + description = "Allow home directory access except secrets" 263 + commands.allow = ["read_file", "write_file"] 264 + 265 + [[scope.allow]] 266 + path = "$HOME/*" 267 + 268 + [[scope.deny]] 269 + path = "$HOME/.ssh/*" 270 + 271 + [[scope.deny]] 272 + path = "$HOME/.gnupg/*" 273 + 274 + [[scope.deny]] 275 + path = "$HOME/.aws/*" 276 + ``` 277 + 278 + ### Prototype Freezing 279 + 280 + Prevent JavaScript prototype pollution attacks. 281 + 282 + ```json 283 + { 284 + "app": { 285 + "security": { 286 + "freezePrototype": true 287 + } 288 + } 289 + } 290 + ``` 291 + 292 + ### Remote API Access Control 293 + 294 + Control which external URLs can access Tauri commands. 295 + 296 + ```json 297 + { 298 + "identifier": "remote-api-capability", 299 + "remote": { 300 + "urls": ["https://*.yourdomain.com"] 301 + }, 302 + "permissions": ["limited-api-access"] 303 + } 304 + ``` 305 + 306 + --- 307 + 308 + ## Threat Mitigation Quick Reference 309 + 310 + | Phase | Threat | Mitigation | 311 + |-------|--------|------------| 312 + | Development | Dependency vulnerabilities | `cargo audit`, `npm audit`, pin versions | 313 + | Development | Dev server exposure | Trusted networks, mTLS | 314 + | Development | Credential leaks | Hardware tokens, gitignore secrets | 315 + | Build | CI/CD compromise | Trusted providers, self-hosted options | 316 + | Build | Unsigned binaries | Platform signing, hardware key storage | 317 + | Distribution | Manifest tampering | HTTPS, certificate pinning | 318 + | Distribution | Binary replacement | Checksums, signed manifests | 319 + | Runtime | XSS/injection | CSP, input validation | 320 + | Runtime | Privilege escalation | Capabilities, permissions, scopes | 321 + | Runtime | Prototype pollution | `freezePrototype: true` | 322 + 323 + --- 324 + 325 + ## Security Configuration Template 326 + 327 + **Minimal Secure Configuration:** 328 + 329 + ```json 330 + { 331 + "app": { 332 + "security": { 333 + "csp": "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self'", 334 + "freezePrototype": true, 335 + "capabilities": ["main-capability"], 336 + "dangerousDisableAssetCspModification": false, 337 + "assetProtocol": { 338 + "enable": false, 339 + "scope": [] 340 + } 341 + } 342 + } 343 + } 344 + ``` 345 + 346 + **Capability File Structure:** 347 + 348 + ``` 349 + src-tauri/ 350 + ├── capabilities/ 351 + │ ├── main.json # Main window capabilities 352 + │ └── settings.json # Settings window capabilities 353 + ├── permissions/ 354 + │ └── custom-scope.toml # Custom permission scopes 355 + └── tauri.conf.json 356 + ``` 357 + 358 + --- 359 + 360 + ## Vulnerability Reporting 361 + 362 + If you discover security vulnerabilities in Tauri applications: 363 + 364 + 1. Use GitHub Vulnerability Disclosure on affected repositories 365 + 2. Email: security@tauri.app 366 + 3. Do not publicly discuss findings before coordinated resolution 367 + 4. Limited bounty consideration available 368 + 369 + --- 370 + 371 + ## Key Takeaways 372 + 373 + 1. **Defense in Depth**: No single layer provides sufficient protection 374 + 2. **Least Privilege**: Grant minimum necessary permissions 375 + 3. **Update Regularly**: WebView patches reach users faster through OS updates 376 + 4. **Trust Boundaries**: Frontend code is untrusted; validate everything in Rust 377 + 5. **Lifecycle Coverage**: Security must span development through runtime
+376
.agents/skills/understanding-tauri-process-model/SKILL.md
··· 1 + --- 2 + name: understanding-tauri-process-model 3 + description: Explains the Tauri process model architecture including the Core process, WebView process, inter-process communication, multiwindow handling, and process isolation security patterns. 4 + --- 5 + 6 + # Tauri Process Model 7 + 8 + Tauri implements a multi-process architecture similar to Electron and modern web browsers. Understanding this model is essential for building secure, performant Tauri applications. 9 + 10 + ## Architecture Overview 11 + 12 + ``` 13 + +------------------------------------------------------------------+ 14 + | TAURI APPLICATION | 15 + +------------------------------------------------------------------+ 16 + | | 17 + | +-----------------------------+ | 18 + | | CORE PROCESS | | 19 + | | (Rust) | | 20 + | | | | 21 + | | +----------------------+ | | 22 + | | | Window Manager | | | 23 + | | +----------------------+ | | 24 + | | | System Tray | | | 25 + | | +----------------------+ | | 26 + | | | Global State | | | 27 + | | +----------------------+ | | 28 + | | | IPC Router | | | 29 + | | +----------------------+ | | 30 + | | | OS Abstractions | | | 31 + | +-------------+---------------+ | 32 + | | | 33 + | | IPC (Inter-Process Communication) | 34 + | | | 35 + | +----------+----------+----------+ | 36 + | | | | | | 37 + | v v v v | 38 + | +------+ +------+ +------+ +------+ | 39 + | |WebView| |WebView| |WebView| |WebView| | 40 + | | #1 | | #2 | | #3 | | #N | | 41 + | +------+ +------+ +------+ +------+ | 42 + | | HTML | | HTML | | HTML | | HTML | | 43 + | | CSS | | CSS | | CSS | | CSS | | 44 + | | JS | | JS | | JS | | JS | | 45 + | +------+ +------+ +------+ +------+ | 46 + | | 47 + +------------------------------------------------------------------+ 48 + ``` 49 + 50 + ## The Core Process 51 + 52 + The Core process is the application's entry point and central hub. It runs Rust code and has exclusive access to operating system capabilities. 53 + 54 + ### Responsibilities 55 + 56 + | Responsibility | Description | 57 + |----------------|-------------| 58 + | Window Management | Creates and orchestrates application windows | 59 + | System Integration | Manages system tray menus and notifications | 60 + | IPC Routing | Handles all inter-process communication | 61 + | Global State | Manages application-wide settings and database connections | 62 + | OS Abstractions | Provides cross-platform APIs | 63 + 64 + ### Why Rust for the Core Process 65 + 66 + Rust powers the Core process for its memory-safety guarantees. The ownership system prevents: 67 + 68 + - Null pointer dereferences 69 + - Buffer overflows 70 + - Data races 71 + - Use-after-free bugs 72 + 73 + This is critical because the Core process has full system access. 74 + 75 + ``` 76 + +------------------------------------------+ 77 + | CORE PROCESS | 78 + | | 79 + | Memory Safety via Rust Ownership: | 80 + | - No null pointers | 81 + | - No buffer overflows | 82 + | - No data races | 83 + | - No use-after-free | 84 + | | 85 + | Full OS Access: | 86 + | - File system | 87 + | - Network | 88 + | - System APIs | 89 + | - Hardware interfaces | 90 + +------------------------------------------+ 91 + ``` 92 + 93 + ## The WebView Process 94 + 95 + WebView processes render the user interface using the operating system's native WebView library. 96 + 97 + ### Platform-Specific WebViews 98 + 99 + ``` 100 + +------------------+------------------+------------------+ 101 + | WINDOWS | MACOS | LINUX | 102 + +------------------+------------------+------------------+ 103 + | | | | 104 + | Microsoft Edge | WKWebView | webkitgtk | 105 + | WebView2 | | | 106 + | | | | 107 + | Chromium-based | Safari engine | WebKit engine | 108 + | | | | 109 + +------------------+------------------+------------------+ 110 + | | | 111 + +------------------+------------------+ 112 + | 113 + Dynamic Linking 114 + (Not bundled) 115 + | 116 + Smaller executables 117 + ``` 118 + 119 + ### Key Characteristics 120 + 121 + 1. **Dynamic Linking**: WebView libraries are linked at runtime, not bundled 122 + 2. **Web Technologies**: Execute HTML, CSS, and JavaScript 123 + 3. **Framework Support**: Works with React, Vue, Svelte, Solid, etc. 124 + 4. **Isolation**: Each WebView runs in its own process space 125 + 126 + ## Process Communication (IPC) 127 + 128 + All communication between processes flows through the Core process. 129 + 130 + ``` 131 + +----------------+ +----------------+ 132 + | WebView A | | WebView B | 133 + | | | | 134 + | invoke() ----+---->+----------------+<------+---- invoke() | 135 + | | | CORE PROCESS | | | 136 + | <---- listen |<----+ +------>| listen ----> | 137 + | | | - Validates | | | 138 + +----------------+ | - Routes | +----------------+ 139 + | - Filters | 140 + | - Transforms | 141 + +----------------+ 142 + | 143 + v 144 + +----------------+ 145 + | OS / System | 146 + | Resources | 147 + +----------------+ 148 + ``` 149 + 150 + ### IPC Flow 151 + 152 + 1. WebView calls `invoke()` with a command name and payload 153 + 2. Core process receives the message 154 + 3. Core process validates and processes the request 155 + 4. Core process may interact with OS resources 156 + 5. Core process sends response back to WebView 157 + 158 + ### Example: Basic IPC 159 + 160 + **Frontend (JavaScript)** 161 + ```javascript 162 + import { invoke } from '@tauri-apps/api/core'; 163 + 164 + // Call a Rust command 165 + const result = await invoke('greet', { name: 'World' }); 166 + ``` 167 + 168 + **Backend (Rust)** 169 + ```rust 170 + #[tauri::command] 171 + fn greet(name: &str) -> String { 172 + format!("Hello, {}!", name) 173 + } 174 + ``` 175 + 176 + ## Multiwindow Handling 177 + 178 + A single Core process manages multiple WebView processes. 179 + 180 + ``` 181 + +-------------------+ 182 + | CORE PROCESS | 183 + | | 184 + | Shared State: | 185 + | - User session | 186 + | - App config | 187 + | - DB connection | 188 + +-------------------+ 189 + /|\ 190 + / | \ 191 + / | \ 192 + / | \ 193 + / | \ 194 + v v v 195 + +------+ +------+ +------+ 196 + |Main | |Settings| |About| 197 + |Window| |Window | |Window| 198 + +------+ +------+ +------+ 199 + ``` 200 + 201 + ### Window Management Patterns 202 + 203 + **Creating Windows** 204 + ```rust 205 + use tauri::Manager; 206 + 207 + #[tauri::command] 208 + fn open_settings(app: tauri::AppHandle) { 209 + tauri::WebviewWindowBuilder::new( 210 + &app, 211 + "settings", 212 + tauri::WebviewUrl::App("settings.html".into()) 213 + ) 214 + .title("Settings") 215 + .build() 216 + .unwrap(); 217 + } 218 + ``` 219 + 220 + **Cross-Window Communication** 221 + ```rust 222 + use tauri::Manager; 223 + 224 + #[tauri::command] 225 + fn broadcast_update(app: tauri::AppHandle, data: String) { 226 + // Emit to all windows 227 + app.emit("data-updated", data).unwrap(); 228 + } 229 + ``` 230 + 231 + **Window-Specific Events** 232 + ```rust 233 + use tauri::Manager; 234 + 235 + #[tauri::command] 236 + fn notify_window(app: tauri::AppHandle, window_label: String, data: String) { 237 + if let Some(window) = app.get_webview_window(&window_label) { 238 + window.emit("notification", data).unwrap(); 239 + } 240 + } 241 + ``` 242 + 243 + ## Process Isolation and Security 244 + 245 + ### The Principle of Least Privilege 246 + 247 + > "If you have a gardener coming over to trim your hedge, you give them the key to your garden. You would not give them the keys to your house." 248 + 249 + ``` 250 + +------------------------------------------------------------------+ 251 + | SECURITY BOUNDARIES | 252 + +------------------------------------------------------------------+ 253 + | | 254 + | +---------------------------+ +---------------------------+ | 255 + | | CORE PROCESS | | WEBVIEW PROCESS | | 256 + | | (Trusted Zone) | | (Untrusted Zone) | | 257 + | +---------------------------+ +---------------------------+ | 258 + | | | | | | 259 + | | - File system access | | - Render UI only | | 260 + | | - Database connections | | - User input handling | | 261 + | | - Network requests | | - Display data | | 262 + | | - Crypto operations | | - Call allowed commands | | 263 + | | - Secrets management | | | | 264 + | | - Business logic | | NO DIRECT ACCESS TO: | | 265 + | | | | - File system | | 266 + | | | | - Network (direct) | | 267 + | | | | - System APIs | | 268 + | +---------------------------+ +---------------------------+ | 269 + | | 270 + +------------------------------------------------------------------+ 271 + ``` 272 + 273 + ### Security Benefits of Process Isolation 274 + 275 + | Benefit | Description | 276 + |---------|-------------| 277 + | Crash Containment | Failures in one process don't crash the entire app | 278 + | State Recovery | Invalid processes can be restarted independently | 279 + | Attack Surface Reduction | Compromised WebView has limited capabilities | 280 + | Resource Protection | Sensitive data stays in Core process | 281 + 282 + ### Security Best Practices 283 + 284 + **In the Frontend (WebView)** 285 + 286 + 1. Sanitize all user input 287 + 2. Never handle secrets 288 + 3. Defer business logic to Core process 289 + 4. Implement Content Security Policy (CSP) 290 + 291 + **In the Backend (Core Process)** 292 + 293 + 1. Validate all IPC inputs 294 + 2. Use the capability system to restrict commands 295 + 3. Apply principle of least privilege to each window 296 + 297 + ### Capability-Based Security 298 + 299 + Tauri uses a capability system to control what each window can access. 300 + 301 + ``` 302 + +------------------+ +------------------+ +------------------+ 303 + | Main Window | | Settings Window | | Viewer Window | 304 + +------------------+ +------------------+ +------------------+ 305 + | Capabilities: | | Capabilities: | | Capabilities: | 306 + | - read_file | | - read_config | | - read_file | 307 + | - write_file | | - write_config | | (read only) | 308 + | - network | | | | | 309 + | - notifications | | | | | 310 + +------------------+ +------------------+ +------------------+ 311 + ``` 312 + 313 + **Example: Capability Configuration** 314 + 315 + ```json 316 + { 317 + "identifier": "main-capability", 318 + "description": "Capability for the main window", 319 + "windows": ["main"], 320 + "permissions": [ 321 + "core:default", 322 + "fs:read-files", 323 + "fs:write-files", 324 + "http:default" 325 + ] 326 + } 327 + ``` 328 + 329 + ## Process Lifecycle 330 + 331 + ``` 332 + +------------------------------------------------------------------+ 333 + | APPLICATION LIFECYCLE | 334 + +------------------------------------------------------------------+ 335 + | | 336 + | 1. App Launch | 337 + | +------------------+ | 338 + | | Core Process | <-- Starts first | 339 + | | Initializes | | 340 + | +------------------+ | 341 + | | | 342 + | v | 343 + | 2. Window Creation | 344 + | +------------------+ | 345 + | | WebView Process | <-- Core creates WebViews | 346 + | | Spawned | | 347 + | +------------------+ | 348 + | | | 349 + | v | 350 + | 3. Running | 351 + | +--------+ IPC +----------+ | 352 + | | Core |<--------->| WebViews | | 353 + | +--------+ +----------+ | 354 + | | | 355 + | v | 356 + | 4. Shutdown | 357 + | +------------------+ | 358 + | | WebViews close | <-- WebViews terminate first | 359 + | | Core cleans up | <-- Core process exits last | 360 + | +------------------+ | 361 + | | 362 + +------------------------------------------------------------------+ 363 + ``` 364 + 365 + ## Summary 366 + 367 + | Aspect | Core Process | WebView Process | 368 + |--------|--------------|-----------------| 369 + | Language | Rust | JavaScript/TypeScript | 370 + | Quantity | One per app | One or more per app | 371 + | OS Access | Full | None (via IPC only) | 372 + | Role | Backend, orchestration | UI rendering | 373 + | Security | Trusted | Untrusted | 374 + | Crash Impact | App terminates | Window closes | 375 + 376 + The Tauri process model provides a secure foundation for building desktop applications by maintaining strict separation between the trusted Core process and the potentially vulnerable WebView processes. All sensitive operations should be implemented in the Core process, with the WebView serving only as a presentation layer.
+475
.agents/skills/understanding-tauri-runtime-authority/SKILL.md
··· 1 + --- 2 + name: understanding-tauri-runtime-authority 3 + description: Explains how the Tauri runtime authority enforces security policies during application execution, covering ACL-based access control, capability resolution at runtime, scope injection, and command validation for secure IPC. 4 + --- 5 + 6 + # Tauri Runtime Authority 7 + 8 + The runtime authority is a core Tauri component that enforces security policies during application execution. It validates permissions, resolves capabilities, and injects scopes before commands execute. 9 + 10 + ## What Is Runtime Authority? 11 + 12 + Runtime authority is the enforcement layer that sits between the WebView frontend and Tauri commands. It acts as a gatekeeper for all IPC (Inter-Process Communication) requests. 13 + 14 + ### Core Function 15 + 16 + When a webview invokes a Tauri command, the runtime authority: 17 + 18 + 1. Receives the invoke request from the webview 19 + 2. Validates the origin is permitted to call the requested command 20 + 3. Confirms the origin belongs to applicable capabilities 21 + 4. Injects defined scopes into the request 22 + 5. Passes the validated request to the Tauri command 23 + 24 + If the origin is not allowed, the request is denied and the command never executes. 25 + 26 + ### Trust Boundary Model 27 + 28 + Tauri implements a trust boundary separating Rust core code from WebView frontend code: 29 + 30 + | Zone | Trust Level | Access | 31 + |------|-------------|--------| 32 + | Rust Core | Full trust | Unrestricted system access | 33 + | WebView Frontend | Limited trust | Only exposed resources via IPC | 34 + 35 + The runtime authority enforces this boundary at execution time. 36 + 37 + ## Security Architecture 38 + 39 + ### How Runtime Authority Fits 40 + 41 + ``` 42 + Frontend (WebView) 43 + | 44 + v 45 + [IPC Invoke Request] 46 + | 47 + v 48 + +------------------+ 49 + | Runtime Authority| <-- Validates permissions, capabilities, scopes 50 + +------------------+ 51 + | 52 + v (if allowed) 53 + [Tauri Command Execution] 54 + | 55 + v 56 + [System Resources] 57 + ``` 58 + 59 + ### Key Components 60 + 61 + | Component | Role in Runtime | 62 + |-----------|-----------------| 63 + | Permissions | Define what commands exist and their access rules | 64 + | Capabilities | Map permissions to specific windows/webviews | 65 + | Scopes | Restrict command behavior with path/resource limits | 66 + | Runtime Authority | Enforces all of the above at execution time | 67 + 68 + ## Capability Resolution at Runtime 69 + 70 + When a command is invoked, the runtime authority resolves which capabilities apply. 71 + 72 + ### Resolution Process 73 + 74 + 1. **Identify Origin**: Determine which window/webview made the request 75 + 2. **Match Capabilities**: Find all capabilities that include this window 76 + 3. **Collect Permissions**: Aggregate all permissions from matched capabilities 77 + 4. **Check Command Access**: Verify the command is allowed 78 + 5. **Merge Scopes**: Combine all applicable scope restrictions 79 + 6. **Validate or Deny**: Either proceed with scope injection or reject 80 + 81 + ### Window Capability Merging 82 + 83 + When a window is part of multiple capabilities, security boundaries merge: 84 + 85 + ```json 86 + // capability-1.json 87 + { 88 + "identifier": "basic-access", 89 + "windows": ["main"], 90 + "permissions": ["fs:allow-read-file"] 91 + } 92 + 93 + // capability-2.json 94 + { 95 + "identifier": "write-access", 96 + "windows": ["main"], 97 + "permissions": ["fs:allow-write-file"] 98 + } 99 + ``` 100 + 101 + Result: The "main" window gets both read and write permissions. 102 + 103 + ### Platform-Specific Resolution 104 + 105 + Capabilities can target specific platforms. At runtime, only capabilities matching the current platform are considered: 106 + 107 + ```json 108 + { 109 + "identifier": "desktop-features", 110 + "platforms": ["linux", "macOS", "windows"], 111 + "windows": ["main"], 112 + "permissions": ["shell:allow-execute"] 113 + } 114 + ``` 115 + 116 + On iOS/Android, this capability is ignored at runtime. 117 + 118 + ## Access Control Enforcement 119 + 120 + ### Deny Precedence Rule 121 + 122 + When evaluating access, deny rules always take precedence: 123 + 124 + ```json 125 + { 126 + "permissions": [ 127 + { 128 + "identifier": "fs:allow-read-file", 129 + "allow": [{ "path": "$HOME/**" }], 130 + "deny": [{ "path": "$HOME/.ssh/**" }] 131 + } 132 + ] 133 + } 134 + ``` 135 + 136 + At runtime: 137 + - Request to read `$HOME/documents/file.txt` - **Allowed** 138 + - Request to read `$HOME/.ssh/id_rsa` - **Denied** (deny rule matches) 139 + 140 + ### Command-Level Validation 141 + 142 + Before any command executes: 143 + 144 + 1. Runtime authority checks if the command permission exists 145 + 2. Verifies the calling window has that permission via its capabilities 146 + 3. Validates any scope restrictions are satisfied 147 + 148 + ``` 149 + Window "editor" calls fs.readFile("/home/user/doc.txt") 150 + | 151 + v 152 + Runtime Authority checks: 153 + - Does "editor" have fs:allow-read-file? Yes 154 + - Is "/home/user/doc.txt" in allowed scope? Yes 155 + - Is it in any deny scope? No 156 + | 157 + v 158 + Command executes with scopes injected 159 + ``` 160 + 161 + ## Scope Injection 162 + 163 + ### How Scopes Work at Runtime 164 + 165 + Scopes are not just validation rules; they are injected into command execution context. Commands can access their applicable scopes to enforce restrictions. 166 + 167 + ### Scope Variables 168 + 169 + At runtime, scope variables resolve to actual paths: 170 + 171 + | Variable | Runtime Resolution | 172 + |----------|-------------------| 173 + | `$APP` | Application install directory | 174 + | `$APPDATA` | App data directory | 175 + | `$APPCONFIG` | App config directory | 176 + | `$HOME` | User home directory | 177 + | `$TEMP` | Temporary directory | 178 + | `$DOCUMENT` | Documents directory | 179 + | `$DOWNLOAD` | Downloads directory | 180 + | `$DESKTOP` | Desktop directory | 181 + 182 + ### Scope Combination Example 183 + 184 + ```json 185 + { 186 + "identifier": "main-capability", 187 + "windows": ["main"], 188 + "permissions": [ 189 + { 190 + "identifier": "fs:allow-read-file", 191 + "allow": [{ "path": "$APPDATA/*" }] 192 + }, 193 + { 194 + "identifier": "fs:allow-write-file", 195 + "allow": [{ "path": "$APPDATA/config.json" }] 196 + } 197 + ] 198 + } 199 + ``` 200 + 201 + At runtime for the "main" window: 202 + - Read operations allowed in `$APPDATA/*` 203 + - Write operations only allowed for `$APPDATA/config.json` 204 + 205 + ## Path Traversal Prevention 206 + 207 + The runtime authority includes built-in path traversal protection: 208 + 209 + ``` 210 + Request: /usr/path/to/../../../etc/passwd 211 + Result: DENIED (path traversal detected) 212 + ``` 213 + 214 + Parent directory accessors (`..`) in paths are blocked, ensuring scope restrictions cannot be bypassed. 215 + 216 + ## Configuration Examples 217 + 218 + ### Basic Runtime Security Setup 219 + 220 + `src-tauri/capabilities/default.json`: 221 + 222 + ```json 223 + { 224 + "$schema": "../gen/schemas/desktop-schema.json", 225 + "identifier": "default-capability", 226 + "description": "Default runtime permissions", 227 + "windows": ["main"], 228 + "permissions": [ 229 + "core:default", 230 + "core:event:default", 231 + "core:window:default" 232 + ] 233 + } 234 + ``` 235 + 236 + ### Scoped Filesystem Access 237 + 238 + `src-tauri/capabilities/files.json`: 239 + 240 + ```json 241 + { 242 + "$schema": "../gen/schemas/desktop-schema.json", 243 + "identifier": "file-access", 244 + "description": "Controlled filesystem access", 245 + "windows": ["main"], 246 + "permissions": [ 247 + "fs:default", 248 + { 249 + "identifier": "fs:allow-read-file", 250 + "allow": [ 251 + { "path": "$APPDATA/**" }, 252 + { "path": "$DOCUMENT/**" } 253 + ], 254 + "deny": [ 255 + { "path": "$DOCUMENT/private/**" } 256 + ] 257 + }, 258 + { 259 + "identifier": "fs:allow-write-file", 260 + "allow": [ 261 + { "path": "$APPDATA/**" } 262 + ] 263 + } 264 + ] 265 + } 266 + ``` 267 + 268 + ### Multi-Window Security Boundaries 269 + 270 + `src-tauri/capabilities/editor.json`: 271 + 272 + ```json 273 + { 274 + "$schema": "../gen/schemas/desktop-schema.json", 275 + "identifier": "editor-capability", 276 + "description": "Full editor permissions", 277 + "windows": ["editor"], 278 + "permissions": [ 279 + "core:default", 280 + "fs:default", 281 + "fs:allow-read-file", 282 + "fs:allow-write-file", 283 + "dialog:default" 284 + ] 285 + } 286 + ``` 287 + 288 + `src-tauri/capabilities/preview.json`: 289 + 290 + ```json 291 + { 292 + "$schema": "../gen/schemas/desktop-schema.json", 293 + "identifier": "preview-capability", 294 + "description": "Read-only preview permissions", 295 + "windows": ["preview"], 296 + "permissions": [ 297 + "core:window:default", 298 + "core:event:default", 299 + { 300 + "identifier": "fs:allow-read-file", 301 + "allow": [{ "path": "$TEMP/preview/**" }] 302 + } 303 + ] 304 + } 305 + ``` 306 + 307 + At runtime: 308 + - "editor" window can read/write files and open dialogs 309 + - "preview" window can only read from temp preview directory 310 + 311 + ### HTTP Request Scoping 312 + 313 + ```json 314 + { 315 + "identifier": "api-access", 316 + "windows": ["main"], 317 + "permissions": [ 318 + { 319 + "identifier": "http:default", 320 + "allow": [ 321 + { "url": "https://api.myapp.com/*" }, 322 + { "url": "https://cdn.myapp.com/*" } 323 + ], 324 + "deny": [ 325 + { "url": "https://api.myapp.com/admin/*" } 326 + ] 327 + } 328 + ] 329 + } 330 + ``` 331 + 332 + ## Runtime Security Guarantees 333 + 334 + ### What Runtime Authority Protects 335 + 336 + | Threat | Protection | 337 + |--------|------------| 338 + | Frontend compromise | Limits damage to granted permissions only | 339 + | Unauthorized command access | Commands denied without explicit capability | 340 + | Path traversal attacks | Built-in prevention at runtime | 341 + | Scope bypass attempts | All scopes enforced before command execution | 342 + | Cross-window access | Each window isolated to its capabilities | 343 + 344 + ### What Runtime Authority Does NOT Protect 345 + 346 + | Threat | Why Not Protected | 347 + |--------|-------------------| 348 + | Malicious Rust code | Rust core has full trust | 349 + | Overly permissive config | Developer responsibility | 350 + | WebView vulnerabilities | OS WebView security boundary | 351 + | Supply chain attacks | Dependency security | 352 + 353 + ## Debugging Runtime Authority 354 + 355 + ### Permission Denied Errors 356 + 357 + When a command fails with permission denied: 358 + 359 + 1. **Check Window Label**: Verify the window making the request 360 + 2. **Check Capability**: Ensure a capability targets that window 361 + 3. **Check Permission**: Verify the permission is in the capability 362 + 4. **Check Scope**: Verify the resource is in allowed scope and not denied 363 + 364 + ### Common Runtime Issues 365 + 366 + | Issue | Cause | Solution | 367 + |-------|-------|----------| 368 + | Command not allowed | Missing permission in capability | Add permission to capability | 369 + | Scope not applied | No scope defined for permission | Add allow scope | 370 + | Access denied despite allow | Deny rule takes precedence | Remove conflicting deny | 371 + | Window has no permissions | Capability not targeting window | Check window label in capability | 372 + 373 + ### Verifying Runtime Configuration 374 + 375 + Check generated schemas to see what permissions are available: 376 + 377 + ``` 378 + src-tauri/gen/schemas/desktop-schema.json 379 + src-tauri/gen/schemas/mobile-schema.json 380 + ``` 381 + 382 + ## Best Practices 383 + 384 + ### Principle of Least Privilege 385 + 386 + Grant only the permissions each window actually needs: 387 + 388 + ```json 389 + // Good: Specific permissions 390 + { 391 + "windows": ["settings"], 392 + "permissions": [ 393 + "core:window:allow-close", 394 + "fs:allow-read-file" 395 + ] 396 + } 397 + 398 + // Avoid: Overly broad permissions 399 + { 400 + "windows": ["settings"], 401 + "permissions": ["fs:default", "shell:default"] 402 + } 403 + ``` 404 + 405 + ### Always Define Scopes 406 + 407 + Never leave filesystem or network permissions unscoped: 408 + 409 + ```json 410 + // Good: Scoped access 411 + { 412 + "identifier": "fs:allow-read-file", 413 + "allow": [{ "path": "$APPDATA/**" }] 414 + } 415 + 416 + // Avoid: Unscoped access 417 + { 418 + "permissions": ["fs:allow-read-file"] 419 + } 420 + ``` 421 + 422 + ### Deny Sensitive Paths 423 + 424 + Explicitly deny access to sensitive locations: 425 + 426 + ```json 427 + { 428 + "identifier": "fs:allow-read-file", 429 + "allow": [{ "path": "$HOME/**" }], 430 + "deny": [ 431 + { "path": "$HOME/.ssh/**" }, 432 + { "path": "$HOME/.gnupg/**" }, 433 + { "path": "$HOME/.aws/**" } 434 + ] 435 + } 436 + ``` 437 + 438 + ### Separate Capabilities by Trust Level 439 + 440 + Create distinct capabilities for different security contexts: 441 + 442 + ``` 443 + capabilities/ 444 + main-trusted.json # Full access for main window 445 + plugin-limited.json # Restricted for plugin windows 446 + preview-readonly.json # Read-only for preview 447 + ``` 448 + 449 + ### Platform-Specific Security 450 + 451 + Use platform targeting for OS-specific permissions: 452 + 453 + ```json 454 + { 455 + "identifier": "desktop-shell", 456 + "platforms": ["linux", "macOS", "windows"], 457 + "windows": ["main"], 458 + "permissions": ["shell:allow-execute"] 459 + } 460 + ``` 461 + 462 + This prevents desktop-only permissions from being evaluated on mobile. 463 + 464 + ## Summary 465 + 466 + The runtime authority is Tauri's enforcement mechanism for the ACL-based security model: 467 + 468 + 1. **Every IPC request** passes through runtime authority validation 469 + 2. **Capabilities resolve** at runtime based on the calling window 470 + 3. **Scopes inject** into command execution context 471 + 4. **Deny rules** always take precedence over allow rules 472 + 5. **Path traversal** is blocked automatically 473 + 6. **Security boundaries merge** when windows have multiple capabilities 474 + 475 + Configure capabilities and permissions correctly, and the runtime authority ensures they are enforced consistently throughout application execution.
+231
.agents/skills/updating-tauri-dependencies/SKILL.md
··· 1 + --- 2 + name: updating-tauri-dependencies 3 + description: Assists users with updating Tauri dependencies including the Tauri CLI, Rust crates, JavaScript packages, and checking for outdated versions to upgrade to the latest version. 4 + --- 5 + 6 + # Updating Tauri Dependencies 7 + 8 + This skill provides guidance for updating Tauri dependencies across both the JavaScript and Rust ecosystems. 9 + 10 + ## Version Synchronization (Critical) 11 + 12 + The JavaScript `@tauri-apps/api` package and Rust `tauri` crate must maintain matching minor versions. Adding new features requires upgrading both sides to ensure compatibility. 13 + 14 + For Tauri plugins, maintain exact version parity (e.g., both `2.2.1`) for the npm package and cargo crate. 15 + 16 + --- 17 + 18 + ## Updating JavaScript Dependencies 19 + 20 + ### Using npm 21 + 22 + Update Tauri CLI and API packages: 23 + 24 + ```bash 25 + npm install @tauri-apps/cli@latest @tauri-apps/api@latest 26 + ``` 27 + 28 + Check for outdated packages: 29 + 30 + ```bash 31 + npm outdated @tauri-apps/cli 32 + npm outdated @tauri-apps/api 33 + ``` 34 + 35 + ### Using yarn 36 + 37 + Update Tauri CLI and API packages: 38 + 39 + ```bash 40 + yarn up @tauri-apps/cli @tauri-apps/api 41 + ``` 42 + 43 + Check for outdated packages: 44 + 45 + ```bash 46 + yarn outdated @tauri-apps/cli 47 + yarn outdated @tauri-apps/api 48 + ``` 49 + 50 + ### Using pnpm 51 + 52 + Update Tauri CLI and API packages: 53 + 54 + ```bash 55 + pnpm update @tauri-apps/cli @tauri-apps/api --latest 56 + ``` 57 + 58 + Check for outdated packages: 59 + 60 + ```bash 61 + pnpm outdated @tauri-apps/cli 62 + pnpm outdated @tauri-apps/api 63 + ``` 64 + 65 + --- 66 + 67 + ## Updating Rust Dependencies 68 + 69 + ### Manual Update 70 + 71 + 1. Check the latest versions on crates.io: 72 + - [tauri crate versions](https://crates.io/crates/tauri/versions) 73 + - [tauri-build crate versions](https://crates.io/crates/tauri-build/versions) 74 + 75 + 2. Edit `src-tauri/Cargo.toml` and update the version numbers: 76 + 77 + ```toml 78 + [build-dependencies] 79 + tauri-build = "2.0" 80 + 81 + [dependencies] 82 + tauri = { version = "2.0", features = [] } 83 + ``` 84 + 85 + 3. Run cargo update from the src-tauri directory: 86 + 87 + ```bash 88 + cd src-tauri && cargo update 89 + ``` 90 + 91 + ### Using cargo-edit (Automatic) 92 + 93 + Install cargo-edit if not already installed: 94 + 95 + ```bash 96 + cargo install cargo-edit 97 + ``` 98 + 99 + Upgrade Tauri dependencies automatically: 100 + 101 + ```bash 102 + cd src-tauri && cargo upgrade tauri tauri-build 103 + ``` 104 + 105 + --- 106 + 107 + ## Checking for Updates 108 + 109 + ### Check All Tauri Dependencies 110 + 111 + JavaScript packages: 112 + 113 + ```bash 114 + # npm 115 + npm outdated | grep tauri 116 + 117 + # yarn 118 + yarn outdated | grep tauri 119 + 120 + # pnpm 121 + pnpm outdated | grep tauri 122 + ``` 123 + 124 + Rust crates: 125 + 126 + ```bash 127 + cd src-tauri && cargo outdated | grep tauri 128 + ``` 129 + 130 + Note: `cargo outdated` requires the cargo-outdated tool: 131 + 132 + ```bash 133 + cargo install cargo-outdated 134 + ``` 135 + 136 + --- 137 + 138 + ## Updating Tauri Plugins 139 + 140 + When updating Tauri plugins, both the npm package and Rust crate must be updated to the same version. 141 + 142 + Example for updating a plugin (e.g., shell plugin): 143 + 144 + ### JavaScript side 145 + 146 + ```bash 147 + # npm 148 + npm install @tauri-apps/plugin-shell@latest 149 + 150 + # yarn 151 + yarn up @tauri-apps/plugin-shell 152 + 153 + # pnpm 154 + pnpm update @tauri-apps/plugin-shell --latest 155 + ``` 156 + 157 + ### Rust side 158 + 159 + Edit `src-tauri/Cargo.toml`: 160 + 161 + ```toml 162 + [dependencies] 163 + tauri-plugin-shell = "2.0" 164 + ``` 165 + 166 + Then update: 167 + 168 + ```bash 169 + cd src-tauri && cargo update 170 + ``` 171 + 172 + --- 173 + 174 + ## Complete Update Workflow 175 + 176 + To update all Tauri dependencies in a project: 177 + 178 + 1. Update JavaScript dependencies: 179 + 180 + ```bash 181 + # Using npm (adjust for your package manager) 182 + npm install @tauri-apps/cli@latest @tauri-apps/api@latest 183 + ``` 184 + 185 + 2. Update any Tauri plugins on the JavaScript side: 186 + 187 + ```bash 188 + npm install @tauri-apps/plugin-shell@latest @tauri-apps/plugin-fs@latest 189 + # Add other plugins as needed 190 + ``` 191 + 192 + 3. Update Rust dependencies in `src-tauri/Cargo.toml` 193 + 194 + 4. Run cargo update: 195 + 196 + ```bash 197 + cd src-tauri && cargo update 198 + ``` 199 + 200 + 5. Rebuild the project to verify compatibility: 201 + 202 + ```bash 203 + npm run tauri build 204 + # or 205 + cargo tauri build 206 + ``` 207 + 208 + --- 209 + 210 + ## Troubleshooting 211 + 212 + ### Version Mismatch Errors 213 + 214 + If you encounter version mismatch errors between JavaScript and Rust dependencies: 215 + 216 + 1. Verify both sides use matching minor versions 217 + 2. Check `package.json` for `@tauri-apps/api` version 218 + 3. Check `src-tauri/Cargo.toml` for `tauri` crate version 219 + 4. Ensure they align (e.g., both at 2.x) 220 + 221 + ### Cargo Lock Conflicts 222 + 223 + If `Cargo.lock` has conflicts after updating: 224 + 225 + ```bash 226 + cd src-tauri && rm Cargo.lock && cargo update 227 + ``` 228 + 229 + ### Plugin Version Mismatch 230 + 231 + For plugin version mismatches, ensure exact version parity between npm and cargo versions of the same plugin.
+1
.pi/skills/adding-tauri-splashscreen
··· 1 + ../../.agents/skills/adding-tauri-splashscreen
+1
.pi/skills/adding-tauri-system-tray
··· 1 + ../../.agents/skills/adding-tauri-system-tray
+1
.pi/skills/building-tauri-with-github-actions
··· 1 + ../../.agents/skills/building-tauri-with-github-actions
+1
.pi/skills/calling-frontend-from-tauri-rust
··· 1 + ../../.agents/skills/calling-frontend-from-tauri-rust
+1
.pi/skills/calling-rust-from-tauri-frontend
··· 1 + ../../.agents/skills/calling-rust-from-tauri-frontend
+1
.pi/skills/configuring-tauri-apps
··· 1 + ../../.agents/skills/configuring-tauri-apps
+1
.pi/skills/configuring-tauri-capabilities
··· 1 + ../../.agents/skills/configuring-tauri-capabilities
+1
.pi/skills/configuring-tauri-csp
··· 1 + ../../.agents/skills/configuring-tauri-csp
+1
.pi/skills/configuring-tauri-http-headers
··· 1 + ../../.agents/skills/configuring-tauri-http-headers
+1
.pi/skills/configuring-tauri-permissions
··· 1 + ../../.agents/skills/configuring-tauri-permissions
+1
.pi/skills/configuring-tauri-scopes
··· 1 + ../../.agents/skills/configuring-tauri-scopes
+1
.pi/skills/customizing-tauri-windows
··· 1 + ../../.agents/skills/customizing-tauri-windows
+1
.pi/skills/debugging-tauri-apps
··· 1 + ../../.agents/skills/debugging-tauri-apps
+1
.pi/skills/developing-tauri-plugins
··· 1 + ../../.agents/skills/developing-tauri-plugins
+1
.pi/skills/distributing-tauri-for-macos
··· 1 + ../../.agents/skills/distributing-tauri-for-macos
+1
.pi/skills/embedding-tauri-sidecars
··· 1 + ../../.agents/skills/embedding-tauri-sidecars
+1
.pi/skills/integrating-tauri-js-frontends
··· 1 + ../../.agents/skills/integrating-tauri-js-frontends
+1
.pi/skills/integrating-tauri-rust-frontends
··· 1 + ../../.agents/skills/integrating-tauri-rust-frontends
+1
.pi/skills/listening-to-tauri-events
··· 1 + ../../.agents/skills/listening-to-tauri-events
+1
.pi/skills/managing-tauri-app-resources
··· 1 + ../../.agents/skills/managing-tauri-app-resources
+1
.pi/skills/managing-tauri-plugin-permissions
··· 1 + ../../.agents/skills/managing-tauri-plugin-permissions
+1
.pi/skills/optimizing-tauri-binary-size
··· 1 + ../../.agents/skills/optimizing-tauri-binary-size
+1
.pi/skills/packaging-tauri-for-linux
··· 1 + ../../.agents/skills/packaging-tauri-for-linux
+1
.pi/skills/running-nodejs-sidecar-in-tauri
··· 1 + ../../.agents/skills/running-nodejs-sidecar-in-tauri
+1
.pi/skills/setting-up-tauri-projects
··· 1 + ../../.agents/skills/setting-up-tauri-projects
+1
.pi/skills/signing-tauri-apps
··· 1 + ../../.agents/skills/signing-tauri-apps
+1
.pi/skills/testing-tauri-apps
··· 1 + ../../.agents/skills/testing-tauri-apps
+1
.pi/skills/understanding-tauri-architecture
··· 1 + ../../.agents/skills/understanding-tauri-architecture
+1
.pi/skills/understanding-tauri-ecosystem-security
··· 1 + ../../.agents/skills/understanding-tauri-ecosystem-security
+1
.pi/skills/understanding-tauri-ipc
··· 1 + ../../.agents/skills/understanding-tauri-ipc
+1
.pi/skills/understanding-tauri-lifecycle-security
··· 1 + ../../.agents/skills/understanding-tauri-lifecycle-security
+1
.pi/skills/understanding-tauri-process-model
··· 1 + ../../.agents/skills/understanding-tauri-process-model
+1
.pi/skills/understanding-tauri-runtime-authority
··· 1 + ../../.agents/skills/understanding-tauri-runtime-authority
+1
.pi/skills/updating-tauri-dependencies
··· 1 + ../../.agents/skills/updating-tauri-dependencies