diff --git a/src/apps/cli/src/modes/chat.rs b/src/apps/cli/src/modes/chat.rs index d402aa9f50..b22d0cc759 100644 --- a/src/apps/cli/src/modes/chat.rs +++ b/src/apps/cli/src/modes/chat.rs @@ -794,10 +794,7 @@ impl ChatMode { ); continue; } - if let ToolEventData::ConfirmationNeeded { - identity, .. - } = tool_event - { + if let ToolEventData::ConfirmationNeeded { identity, .. } = tool_event { if self .runtime .approval_controller() diff --git a/src/apps/cli/src/modes/exec.rs b/src/apps/cli/src/modes/exec.rs index 97521129b7..1b57daea50 100644 --- a/src/apps/cli/src/modes/exec.rs +++ b/src/apps/cli/src/modes/exec.rs @@ -811,9 +811,7 @@ impl ExecMode { } if event_turn_id == &turn_id => { use bitfun_events::ToolEventData; match tool_event { - ToolEventData::ConfirmationNeeded { - identity, .. - } => { + ToolEventData::ConfirmationNeeded { identity, .. } => { let tool_id = &identity.tool_id; let tool_name = identity.effective_name(); if self.approval_mode.rejects_confirmation() { diff --git a/src/apps/cli/src/peer_host/args.rs b/src/apps/cli/src/peer_host/args.rs index 89fdb375df..25704eace1 100644 --- a/src/apps/cli/src/peer_host/args.rs +++ b/src/apps/cli/src/peer_host/args.rs @@ -36,8 +36,8 @@ pub(crate) fn optional_bool(obj: &Value, camel: &str) -> Option { pub(crate) fn get_usize(obj: &Value, camel: &str) -> Result { let snake = camel_to_snake(camel); - let value = field(obj, camel, &snake) - .ok_or_else(|| format!("Missing or invalid '{camel}' field"))?; + let value = + field(obj, camel, &snake).ok_or_else(|| format!("Missing or invalid '{camel}' field"))?; value .as_u64() .map(|n| n as usize) @@ -71,6 +71,9 @@ mod tests { let snake = json!({ "workspace_path": "/b" }); assert_eq!(get_string(&camel, "workspacePath").unwrap(), "/a"); assert_eq!(get_string(&snake, "workspacePath").unwrap(), "/b"); - assert_eq!(optional_string(&camel, "workspacePath").as_deref(), Some("/a")); + assert_eq!( + optional_string(&camel, "workspacePath").as_deref(), + Some("/a") + ); } } diff --git a/src/apps/cli/src/peer_host/commands/config.rs b/src/apps/cli/src/peer_host/commands/config.rs index dddaab700b..e114d7dffe 100644 --- a/src/apps/cli/src/peer_host/commands/config.rs +++ b/src/apps/cli/src/peer_host/commands/config.rs @@ -31,10 +31,7 @@ pub(crate) async fn get_config(args: &Value) -> Result { .await .map_err(|e| format!("Failed to get config service: {e}"))?; - match config_service - .get_config::(path.as_deref()) - .await - { + match config_service.get_config::(path.as_deref()).await { Ok(config) => Ok(config), Err(e) => { if skip_retry_on_not_found && is_expected_config_path_not_found(&e, path.as_deref()) { @@ -67,7 +64,10 @@ pub(crate) async fn get_configs(args: &Value) -> Result { if configs.contains_key(&path) { continue; } - match config_service.get_config::(Some(path.as_str())).await { + match config_service + .get_config::(Some(path.as_str())) + .await + { Ok(config) => { configs.insert(path, config); } @@ -103,13 +103,10 @@ pub(crate) async fn set_config(args: &Value) -> Result { .await .map_err(|e| format!("Failed to get config service: {e}"))?; - config_service - .set_config(&path, value) - .await - .map_err(|e| { - tracing::error!("Failed to set config: path={path}, error={e}"); - format!("Failed to set config: {e}") - })?; + config_service.set_config(&path, value).await.map_err(|e| { + tracing::error!("Failed to set config: path={path}, error={e}"); + format!("Failed to set config: {e}") + })?; Ok(json!("Configuration set successfully")) } diff --git a/src/apps/cli/src/peer_host/commands/git.rs b/src/apps/cli/src/peer_host/commands/git.rs index 863ff3d1e5..437976a2f2 100644 --- a/src/apps/cli/src/peer_host/commands/git.rs +++ b/src/apps/cli/src/peer_host/commands/git.rs @@ -12,9 +12,7 @@ pub(crate) async fn git_is_repository(args: &Value) -> Result { let is_repo = GitService::is_repository(&repository_path) .await .map_err(|e| { - tracing::error!( - "Failed to check Git repository: path={repository_path}, error={e}" - ); + tracing::error!("Failed to check Git repository: path={repository_path}, error={e}"); format!("Failed to check Git repository: {e}") })?; Ok(json!(is_repo)) diff --git a/src/apps/cli/src/peer_host/fanout.rs b/src/apps/cli/src/peer_host/fanout.rs index a57433b791..4df08c6c7c 100644 --- a/src/apps/cli/src/peer_host/fanout.rs +++ b/src/apps/cli/src/peer_host/fanout.rs @@ -3,9 +3,9 @@ use std::collections::HashSet; use std::sync::OnceLock; +use bitfun_agent_tools::effective_tool_invocation; use bitfun_core::service::remote_connect::encryption::encrypt_to_base64; use bitfun_core::service::remote_connect::remote_server::RemoteCommand; -use bitfun_agent_tools::effective_tool_invocation; use bitfun_events::{project_agentic_frontend_event, AgenticEvent, ToolEventData}; use tokio::sync::{broadcast, mpsc}; @@ -239,12 +239,9 @@ async fn handle_agentic_event(state: &PeerHostState, event: AgenticEvent) -> Res if let AgenticEvent::ToolEvent { session_id, turn_id, - tool_event: - ToolEventData::Started { - identity, - params, - .. - }, + tool_event: ToolEventData::Started { + identity, params, .. + }, .. } = &event { @@ -284,9 +281,7 @@ async fn handle_agentic_event(state: &PeerHostState, event: AgenticEvent) -> Res { let terminal_task_call = match tool_event { ToolEventData::Completed { - identity, - result, - .. + identity, result, .. } if identity.effective_name() == "Task" => Some(( identity.tool_id.as_str(), result @@ -296,12 +291,11 @@ async fn handle_agentic_event(state: &PeerHostState, event: AgenticEvent) -> Res .get("cancelled_background_tasks") .and_then(serde_json::Value::as_u64), )), - ToolEventData::Failed { - identity, .. + ToolEventData::Failed { identity, .. } | ToolEventData::Cancelled { identity, .. } + if identity.effective_name() == "Task" => + { + Some((identity.tool_id.as_str(), None, None)) } - | ToolEventData::Cancelled { - identity, .. - } if identity.effective_name() == "Task" => Some((identity.tool_id.as_str(), None, None)), _ => None, }; if let Some((tool_id, background_task_id, cancelled_background_tasks)) = terminal_task_call @@ -322,9 +316,10 @@ async fn handle_agentic_event(state: &PeerHostState, event: AgenticEvent) -> Res .. } = &event { - state - .turns - .record_confirmation(&PeerTurnKey::new(session_id, turn_id), identity.tool_id.clone())?; + state.turns.record_confirmation( + &PeerTurnKey::new(session_id, turn_id), + identity.tool_id.clone(), + )?; } let Some(projected) = project_agentic_frontend_event(event) else { diff --git a/src/apps/desktop/src/computer_use/windows_ax_shortcuts.rs b/src/apps/desktop/src/computer_use/windows_ax_shortcuts.rs index 50f9b3e27a..00c3b893e7 100644 --- a/src/apps/desktop/src/computer_use/windows_ax_shortcuts.rs +++ b/src/apps/desktop/src/computer_use/windows_ax_shortcuts.rs @@ -45,8 +45,9 @@ const MENU_BAR_SEARCH_MAX_VISITED: usize = 4_000; unsafe fn build_shortcuts_cache_request( automation: &IUIAutomation, ) -> BitFunResult { - let cache_req = automation - .CreateCacheRequest() + // SAFETY: `automation` is a live UI Automation COM interface and every + // property, pattern, and scope identifier below is a documented UIA value. + let cache_req = unsafe { automation.CreateCacheRequest() } .map_err(|e| BitFunError::tool(format!("UI Automation CreateCacheRequest: {}.", e)))?; for prop in [ UIA_ControlTypePropertyId, @@ -54,10 +55,10 @@ unsafe fn build_shortcuts_cache_request( UIA_AcceleratorKeyPropertyId, UIA_IsEnabledPropertyId, ] { - let _ = cache_req.AddProperty(prop); + let _ = unsafe { cache_req.AddProperty(prop) }; } - let _ = cache_req.AddPattern(UIA_TogglePatternId); - let _ = cache_req.SetTreeScope(TreeScope_Subtree); + let _ = unsafe { cache_req.AddPattern(UIA_TogglePatternId) }; + let _ = unsafe { cache_req.SetTreeScope(TreeScope_Subtree) }; Ok(cache_req) } diff --git a/src/apps/desktop/src/computer_use/windows_ax_ui.rs b/src/apps/desktop/src/computer_use/windows_ax_ui.rs index bbcb411f84..b87ef612d9 100644 --- a/src/apps/desktop/src/computer_use/windows_ax_ui.rs +++ b/src/apps/desktop/src/computer_use/windows_ax_ui.rs @@ -140,8 +140,9 @@ fn localized_control_type_string(elem: &IUIAutomationElement) -> String { unsafe fn build_cache_request( automation: &IUIAutomation, ) -> BitFunResult { - let cache_req = automation - .CreateCacheRequest() + // SAFETY: `automation` is a live UI Automation COM interface and all ids + // supplied below are documented properties, patterns, scopes, or filters. + let cache_req = unsafe { automation.CreateCacheRequest() } .map_err(|e| BitFunError::tool(format!("UI Automation CreateCacheRequest: {}.", e)))?; // Properties to pre-fetch (typed cached accessors read these). @@ -154,7 +155,7 @@ unsafe fn build_cache_request( UIA_IsOffscreenPropertyId, UIA_BoundingRectanglePropertyId, ] { - let _ = cache_req.AddProperty(prop); + let _ = unsafe { cache_req.AddProperty(prop) }; } // Patterns to pre-fetch (for action detection + Value read). @@ -168,16 +169,16 @@ unsafe fn build_cache_request( UIA_TextPatternId, UIA_ScrollPatternId, ] { - let _ = cache_req.AddPattern(pat); + let _ = unsafe { cache_req.AddPattern(pat) }; } // Fetch the entire subtree in one bulk RPC. - let _ = cache_req.SetTreeScope(TreeScope_Subtree); + let _ = unsafe { cache_req.SetTreeScope(TreeScope_Subtree) }; // Control-view filter (same set ControlViewWalker would walk) — drops // decorative / raw-view nodes that only add noise. - if let Ok(ctrl_cond) = automation.ControlViewCondition() { - let _ = cache_req.SetTreeFilter(&ctrl_cond); + if let Ok(ctrl_cond) = unsafe { automation.ControlViewCondition() } { + let _ = unsafe { cache_req.SetTreeFilter(&ctrl_cond) }; } Ok(cache_req) @@ -193,7 +194,9 @@ pub(crate) unsafe fn build_updated_cache_with_retry( ) -> BitFunResult { let mut attempt = 0u32; loop { - match uncached.BuildUpdatedCache(cache_req) { + // SAFETY: both COM interfaces are live for the call and `cache_req` + // was constructed by the same UI Automation instance. + match unsafe { uncached.BuildUpdatedCache(cache_req) } { Ok(e) => return Ok(e), Err(e) => { attempt += 1; @@ -422,39 +425,45 @@ unsafe fn walk_tree_full( max_elements: usize, max_depth: usize, ) -> BitFunResult<(String, Vec)> { - let _ = CoInitializeEx(None, COINIT_APARTMENTTHREADED); + // SAFETY: initializes COM for the current thread and creates the documented + // in-process UI Automation class; failures are handled below. + let _ = unsafe { CoInitializeEx(None, COINIT_APARTMENTTHREADED) }; - let automation: IUIAutomation = CoCreateInstance(&CUIAutomation, None, CLSCTX_INPROC_SERVER) - .map_err(|e| { + let automation: IUIAutomation = + unsafe { CoCreateInstance(&CUIAutomation, None, CLSCTX_INPROC_SERVER) }.map_err(|e| { BitFunError::tool(format!( "UI Automation (CoCreateInstance CUIAutomation): {}.", e )) })?; - let cache_req = build_cache_request(&automation)?; + let cache_req = unsafe { build_cache_request(&automation) }?; - let uncached = automation.ElementFromHandle(hwnd).map_err(|e| { + // SAFETY: `hwnd` is the caller-provided target handle. UIA reports an + // error for an invalid or stale handle, which is propagated here. + let uncached = unsafe { automation.ElementFromHandle(hwnd) }.map_err(|e| { BitFunError::tool(format!("UI Automation ElementFromHandle failed: {}.", e)) })?; - let root_elem = build_updated_cache_with_retry(&uncached, &cache_req)?; + let root_elem = unsafe { build_updated_cache_with_retry(&uncached, &cache_req) }?; let mut nodes: Vec = Vec::new(); let mut lines: Vec<(usize, String)> = Vec::new(); let mut counter = 0usize; let mut total = 0usize; - walk_cached_bounded( - &root_elem, - 0, - None, - &mut nodes, - &mut lines, - &mut counter, - &mut total, - max_elements, - max_depth, - ); + unsafe { + walk_cached_bounded( + &root_elem, + 0, + None, + &mut nodes, + &mut lines, + &mut counter, + &mut total, + max_elements, + max_depth, + ) + }; let tree_text = render_lines(&lines); Ok((tree_text, nodes)) @@ -555,21 +564,25 @@ unsafe fn walk_cached_bounded( } // Recurse using cached children — zero additional cross-process RPCs. - if let Ok(children) = element.GetCachedChildren() { - let len = children.Length().unwrap_or(0); + // SAFETY: `element` is a live cached UIA element, and child indices are + // bounded by the array length returned by UI Automation. + if let Ok(children) = unsafe { element.GetCachedChildren() } { + let len = unsafe { children.Length() }.unwrap_or(0); for i in 0..len { - if let Ok(child) = children.GetElement(i) { - walk_cached_bounded( - &child, - depth + 1, - emitted_parent, - nodes, - lines, - counter, - total, - max_elements, - max_depth, - ); + if let Ok(child) = unsafe { children.GetElement(i) } { + unsafe { + walk_cached_bounded( + &child, + depth + 1, + emitted_parent, + nodes, + lines, + counter, + total, + max_elements, + max_depth, + ) + }; } } } diff --git a/src/apps/desktop/src/computer_use/windows_bg_input.rs b/src/apps/desktop/src/computer_use/windows_bg_input.rs index 9c07c860c5..7a7f1037e2 100644 --- a/src/apps/desktop/src/computer_use/windows_bg_input.rs +++ b/src/apps/desktop/src/computer_use/windows_bg_input.rs @@ -440,12 +440,16 @@ fn post_char(hwnd: HWND, ch: char) -> BitFunResult<()> { /// user. Best-effort; returns whether the attribute was set. unsafe fn set_cloak(h: HWND, on: bool) -> bool { let v: BOOL = if on { TRUE } else { FALSE }; - DwmSetWindowAttribute( - h, - DWMWA_CLOAK, - &v as *const _ as *const c_void, - std::mem::size_of::() as u32, - ) + // SAFETY: `v` is a live `BOOL` whose pointer and byte length match the + // `DWMWA_CLOAK` contract; an invalid HWND is reported as an API error. + unsafe { + DwmSetWindowAttribute( + h, + DWMWA_CLOAK, + &v as *const _ as *const c_void, + std::mem::size_of::() as u32, + ) + } .is_ok() } @@ -454,24 +458,26 @@ unsafe fn set_cloak(h: HWND, on: bool) -> bool { /// honored even on a foreground-locked session without UIAccess. Single attach, /// no retry loop — bounded. Returns whether `target` actually became foreground. unsafe fn force_foreground_attached(target: HWND) -> bool { - let cur = GetForegroundWindow(); + // SAFETY: all values are opaque Win32 handles/thread ids obtained from the + // same APIs; every successful attach is paired with a detach below. + let cur = unsafe { GetForegroundWindow() }; if cur == target { return true; } - let my_tid = GetCurrentThreadId(); + let my_tid = unsafe { GetCurrentThreadId() }; let mut pid = 0u32; - let cur_tid = GetWindowThreadProcessId(cur, Some(&mut pid)); + let cur_tid = unsafe { GetWindowThreadProcessId(cur, Some(&mut pid)) }; let attached = cur_tid != 0 && cur_tid != my_tid; if attached { - let _ = AttachThreadInput(my_tid, cur_tid, 1); + let _ = unsafe { AttachThreadInput(my_tid, cur_tid, 1) }; } // `SetForegroundWindow` may return BOOL (older bindings) or `Result` // (windows 0.61); `let _ =` discards either without a must_use warning. - let _ = SetForegroundWindow(target); + let _ = unsafe { SetForegroundWindow(target) }; if attached { - let _ = AttachThreadInput(my_tid, cur_tid, 0); + let _ = unsafe { AttachThreadInput(my_tid, cur_tid, 0) }; } - GetForegroundWindow() == target + (unsafe { GetForegroundWindow() }) == target } /// Type `text` into a **background** target via real `SendInput` Unicode @@ -576,51 +582,60 @@ pub(super) fn inject_key_cloaked(hwnd: HWND, keycode: u16, modifiers: &[u16]) -> /// handle) with `TOKEN_QUERY` access for `OpenProcessToken` to succeed. unsafe fn process_integrity_rid(process: Handle) -> Option { let mut token: Handle = std::ptr::null_mut(); - if OpenProcessToken(process, TOKEN_QUERY, &mut token) == 0 { + // SAFETY: the caller supplies a process handle valid for `TOKEN_QUERY`; + // `token` is a live out-pointer for the duration of the call. + if unsafe { OpenProcessToken(process, TOKEN_QUERY, &mut token) } == 0 { return None; } // Probe the required buffer size first (the first call always fails with // ERROR_INSUFFICIENT_BUFFER and writes `needed`). let mut needed: u32 = 0; - GetTokenInformation( - token, - TOKEN_INTEGRITY_LEVEL_CLASS, - std::ptr::null_mut(), - 0, - &mut needed, - ); + unsafe { + GetTokenInformation( + token, + TOKEN_INTEGRITY_LEVEL_CLASS, + std::ptr::null_mut(), + 0, + &mut needed, + ) + }; if needed == 0 { - CloseHandle(token); + unsafe { CloseHandle(token) }; return None; } let mut buf = vec![0u8; needed as usize]; - let ok = GetTokenInformation( - token, - TOKEN_INTEGRITY_LEVEL_CLASS, - buf.as_mut_ptr(), - needed, - &mut needed, - ) != 0; - CloseHandle(token); + let ok = unsafe { + GetTokenInformation( + token, + TOKEN_INTEGRITY_LEVEL_CLASS, + buf.as_mut_ptr(), + needed, + &mut needed, + ) + } != 0; + unsafe { CloseHandle(token) }; if !ok { return None; } // The buffer holds a TOKEN_MANDATORY_LABEL { SID_AND_ATTRIBUTES { Sid, Attr } }. - let tml = &*(buf.as_ptr() as *const TOKEN_MANDATORY_LABEL); + // SAFETY: a successful `GetTokenInformation(TokenIntegrityLevel)` fills + // `buf` with a `TOKEN_MANDATORY_LABEL` and a SID owned by that buffer. + // `read_unaligned` avoids assuming that `Vec` has the struct's alignment. + let tml = unsafe { (buf.as_ptr() as *const TOKEN_MANDATORY_LABEL).read_unaligned() }; let sid = tml.label.sid as *const c_void; - let count_ptr = GetSidSubAuthorityCount(sid); + let count_ptr = unsafe { GetSidSubAuthorityCount(sid) }; if count_ptr.is_null() { return None; } - let count = *count_ptr; + let count = unsafe { *count_ptr }; if count == 0 { return None; } - let rid_ptr = GetSidSubAuthority(sid, (count - 1) as u32); + let rid_ptr = unsafe { GetSidSubAuthority(sid, (count - 1) as u32) }; if rid_ptr.is_null() { return None; } - Some(*rid_ptr) + Some(unsafe { *rid_ptr }) } /// If posting `msg` from the current process to `hwnd` would be silently @@ -897,11 +912,13 @@ unsafe fn send_unicode(text: &str) -> BitFunResult<()> { if ev.is_empty() { return Ok(()); } - let sent = SendInput( - ev.len() as u32, - ev.as_ptr(), - std::mem::size_of::() as i32, - ); + let sent = unsafe { + SendInput( + ev.len() as u32, + ev.as_ptr(), + std::mem::size_of::() as i32, + ) + }; if sent as usize != ev.len() { return Err(BitFunError::service(format!( "SendInput typed only {sent} of {} key events", @@ -919,24 +936,28 @@ unsafe fn send_unicode(text: &str) -> BitFunResult<()> { unsafe fn send_key_combo(keycode: u16, modifiers: &[u16]) -> BitFunResult<()> { let mut ev: Vec = Vec::with_capacity(modifiers.len() * 2 + 2); for &m in modifiers { - let m_scan = MapVirtualKeyW(m as u32, MAPVK_VK_TO_VSC); + // SAFETY: `MapVirtualKeyW` accepts every virtual-key value and has no + // pointer or lifetime requirements. + let m_scan = unsafe { MapVirtualKeyW(m as u32, MAPVK_VK_TO_VSC) }; ev.push(vk_event(m, m_scan, false)); } - let scan = MapVirtualKeyW(keycode as u32, MAPVK_VK_TO_VSC); + let scan = unsafe { MapVirtualKeyW(keycode as u32, MAPVK_VK_TO_VSC) }; ev.push(vk_event(keycode, scan, false)); ev.push(vk_event(keycode, scan, true)); for &m in modifiers.iter().rev() { - let m_scan = MapVirtualKeyW(m as u32, MAPVK_VK_TO_VSC); + let m_scan = unsafe { MapVirtualKeyW(m as u32, MAPVK_VK_TO_VSC) }; ev.push(vk_event(m, m_scan, true)); } if ev.is_empty() { return Ok(()); } - let sent = SendInput( - ev.len() as u32, - ev.as_ptr(), - std::mem::size_of::() as i32, - ); + let sent = unsafe { + SendInput( + ev.len() as u32, + ev.as_ptr(), + std::mem::size_of::() as i32, + ) + }; if sent as usize != ev.len() { return Err(BitFunError::service(format!( "SendInput sent only {sent} of {} key events", diff --git a/src/apps/desktop/src/computer_use/windows_capture.rs b/src/apps/desktop/src/computer_use/windows_capture.rs index f451381a67..b2715086e7 100644 --- a/src/apps/desktop/src/computer_use/windows_capture.rs +++ b/src/apps/desktop/src/computer_use/windows_capture.rs @@ -202,7 +202,9 @@ fn screenshot_window_via_wgc(hwnd: HWND) -> BitFunResult<(Vec, u32, u32)> { /// `(bgra_pixels, width, height)`. unsafe fn screenshot_via_screen_region(hwnd: HWND) -> BitFunResult<(Vec, i32, i32)> { let mut rect = RECT::default(); - GetWindowRect(hwnd, &mut rect).map_err(|e| { + // SAFETY: `rect` is a valid out-parameter and a stale/invalid HWND is + // reported by the Win32 API. + unsafe { GetWindowRect(hwnd, &mut rect) }.map_err(|e| { BitFunError::service(format!("screen-region fallback: GetWindowRect failed: {e}")) })?; // Under Per-Monitor V2 DPI awareness, GetWindowRect returns PHYSICAL pixels @@ -216,21 +218,25 @@ unsafe fn screenshot_via_screen_region(hwnd: HWND) -> BitFunResult<(Vec, i32 "screen-region fallback: window has zero/negative bounds: {w}x{h}" ))); } - let screen_dc = GetDC(None); // NULL HWND -> desktop DC - let mem_dc = CreateCompatibleDC(Some(screen_dc)); - let bitmap = CreateCompatibleBitmap(screen_dc, w, h); - let old_bitmap = SelectObject(mem_dc, bitmap.into()); - let blt_ok = BitBlt( - mem_dc, - 0, - 0, - w, - h, - Some(screen_dc), - physical_left, - physical_top, - SRCCOPY, - ); + // SAFETY: the DC and bitmap handles are created in this block, remain live + // through the copy, and are restored/released before the function returns. + let screen_dc = unsafe { GetDC(None) }; // NULL HWND -> desktop DC + let mem_dc = unsafe { CreateCompatibleDC(Some(screen_dc)) }; + let bitmap = unsafe { CreateCompatibleBitmap(screen_dc, w, h) }; + let old_bitmap = unsafe { SelectObject(mem_dc, bitmap.into()) }; + let blt_ok = unsafe { + BitBlt( + mem_dc, + 0, + 0, + w, + h, + Some(screen_dc), + physical_left, + physical_top, + SRCCOPY, + ) + }; let mut bmi = BITMAPINFO { bmiHeader: BITMAPINFOHEADER { biSize: std::mem::size_of::() as u32, @@ -246,19 +252,23 @@ unsafe fn screenshot_via_screen_region(hwnd: HWND) -> BitFunResult<(Vec, i32 }; let pixel_count = (w * h) as usize; let mut pixels = vec![0u8; pixel_count * 4]; - let ok = GetDIBits( - mem_dc, - bitmap, - 0, - h as u32, - Some(pixels.as_mut_ptr() as *mut _), - &mut bmi, - DIB_RGB_COLORS, - ); - SelectObject(mem_dc, old_bitmap); - let _ = DeleteObject(bitmap.into()); - let _ = DeleteDC(mem_dc); - ReleaseDC(None, screen_dc); + let ok = unsafe { + GetDIBits( + mem_dc, + bitmap, + 0, + h as u32, + Some(pixels.as_mut_ptr() as *mut _), + &mut bmi, + DIB_RGB_COLORS, + ) + }; + unsafe { + SelectObject(mem_dc, old_bitmap); + let _ = DeleteObject(bitmap.into()); + let _ = DeleteDC(mem_dc); + ReleaseDC(None, screen_dc); + } if blt_ok.is_err() { return Err(BitFunError::service(format!( "screen-region fallback: BitBlt failed: {blt_ok:?}" @@ -334,7 +344,8 @@ unsafe fn screenshot_window_bytes_unsafe(hwnd: HWND) -> BitFunResult BitFunResult BitFunResult = { let mut r = RECT::default(); - let hr = DwmGetWindowAttribute( - hwnd, - DWMWA_EXTENDED_FRAME_BOUNDS, - &mut r as *mut _ as *mut _, - std::mem::size_of::() as u32, - ); + // SAFETY: `r` is a live `RECT` out-buffer with the exact byte size + // required by `DWMWA_EXTENDED_FRAME_BOUNDS`. + let hr = unsafe { + DwmGetWindowAttribute( + hwnd, + DWMWA_EXTENDED_FRAME_BOUNDS, + &mut r as *mut _ as *mut _, + std::mem::size_of::() as u32, + ) + }; hr.ok().map(|_| r) }; @@ -392,20 +409,24 @@ unsafe fn screenshot_window_bytes_unsafe(hwnd: HWND) -> BitFunResult BitFunResult { // Screen-region BitBlt captures the full GetWindowRect region // (no DWM crop), so its origin is the raw window top-left. diff --git a/src/apps/desktop/src/computer_use/windows_list_apps.rs b/src/apps/desktop/src/computer_use/windows_list_apps.rs index 71c89cd0f9..b090de651a 100644 --- a/src/apps/desktop/src/computer_use/windows_list_apps.rs +++ b/src/apps/desktop/src/computer_use/windows_list_apps.rs @@ -96,12 +96,14 @@ pub(super) fn find_top_window_for_pid(pid: u32) -> Option { found: Option, } unsafe extern "system" fn cb(hwnd: HWND, lparam: LPARAM) -> BOOL { - let state = &mut *(lparam.0 as *mut FindState); - if IsWindowVisible(hwnd).0 == 0 || IsIconic(hwnd).0 != 0 { + // SAFETY: `lparam` is the unique `FindState` pointer supplied to the + // synchronous `EnumWindows` call below and remains live for the callback. + let state = unsafe { &mut *(lparam.0 as *mut FindState) }; + if unsafe { IsWindowVisible(hwnd) }.0 == 0 || unsafe { IsIconic(hwnd) }.0 != 0 { return TRUE; } let mut pid: u32 = 0; - GetWindowThreadProcessId(hwnd, Some(&mut pid)); + unsafe { GetWindowThreadProcessId(hwnd, Some(&mut pid)) }; if pid == state.target_pid { state.found = Some(hwnd.0 as isize); return windows::Win32::Foundation::FALSE; @@ -131,19 +133,21 @@ fn enumerate_windows() -> Vec { } unsafe extern "system" fn enum_windows_cb(hwnd: HWND, lparam: LPARAM) -> BOOL { - let state = &*(lparam.0 as *const Mutex); + // SAFETY: `lparam` points to the live `Mutex` supplied to the + // synchronous `EnumWindows` call in `enumerate_windows`. + let state = unsafe { &*(lparam.0 as *const Mutex) }; // Skip invisible or minimized windows. - if IsWindowVisible(hwnd).0 == 0 || IsIconic(hwnd).0 != 0 { + if unsafe { IsWindowVisible(hwnd) }.0 == 0 || unsafe { IsIconic(hwnd) }.0 != 0 { return TRUE; } - let title_len = GetWindowTextLengthW(hwnd); + let title_len = unsafe { GetWindowTextLengthW(hwnd) }; if title_len == 0 { return TRUE; } let mut buf = vec![0u16; (title_len + 1) as usize]; - let n = GetWindowTextW(hwnd, &mut buf); + let n = unsafe { GetWindowTextW(hwnd, &mut buf) }; let title = { let len = (n as usize).min(buf.len()); String::from_utf16_lossy(&buf[..len]) @@ -153,7 +157,7 @@ unsafe extern "system" fn enum_windows_cb(hwnd: HWND, lparam: LPARAM) -> BOOL { } let mut pid: u32 = 0; - GetWindowThreadProcessId(hwnd, Some(&mut pid)); + unsafe { GetWindowThreadProcessId(hwnd, Some(&mut pid)) }; if pid == 0 { return TRUE; } diff --git a/src/apps/desktop/src/computer_use/windows_msaa.rs b/src/apps/desktop/src/computer_use/windows_msaa.rs index 4b1b84bccc..c2da604814 100644 --- a/src/apps/desktop/src/computer_use/windows_msaa.rs +++ b/src/apps/desktop/src/computer_use/windows_msaa.rs @@ -115,40 +115,50 @@ unsafe fn walk_bounded( max_depth: usize, ) -> BitFunResult> { // BitFun is a Tauri GUI app; match the UIA path's apartment threading. - let _ = CoInitializeEx(None, COINIT_APARTMENTTHREADED); + // SAFETY: initializes COM for the current thread; the result is intentionally + // ignored because an already initialized apartment is acceptable here. + let _ = unsafe { CoInitializeEx(None, COINIT_APARTMENTTHREADED) }; let hwnd_win = HWND(hwnd as *mut _); let mut raw_root: *mut std::ffi::c_void = null_mut(); // `AccessibleObjectFromWindow` returns the IAccessible for the window's // client area (OBJID_CLIENT) via the IID we pass. let iid = IAccessible::IID; - let res = AccessibleObjectFromWindow( - hwnd_win, - OBJID_CLIENT, - &iid, - &mut raw_root as *mut _ as *mut _, - ); + // SAFETY: `raw_root` is a valid out-pointer for the requested `IAccessible` + // IID. The API owns initialization and reports invalid HWNDs as errors. + let res = unsafe { + AccessibleObjectFromWindow( + hwnd_win, + OBJID_CLIENT, + &iid, + &mut raw_root as *mut _ as *mut _, + ) + }; if res.is_err() || raw_root.is_null() { return Err(BitFunError::tool(format!( "MSAA AccessibleObjectFromWindow failed for hwnd 0x{hwnd:x}: {res:?}." ))); } - let root: IAccessible = IAccessible::from_raw(raw_root); + // SAFETY: success returned a non-null COM pointer for exactly + // `IAccessible::IID`; ownership is transferred into the interface wrapper. + let root: IAccessible = unsafe { IAccessible::from_raw(raw_root) }; let mut nodes: Vec = Vec::new(); let mut counter = 0usize; let mut total = 0usize; - walk( - &root, - 0, - None, - &mut nodes, - &mut counter, - &mut total, - max_depth, - max_total, - ); + unsafe { + walk( + &root, + 0, + None, + &mut nodes, + &mut counter, + &mut total, + max_depth, + max_total, + ) + }; log::debug!( "MSAA walk for hwnd 0x{hwnd:x} produced {} nodes ({} actionable).", @@ -215,20 +225,19 @@ unsafe fn walk( } *total += 1; - let self_var = child_id_variant(CHILDID_SELF); + // SAFETY: constructs the documented `VT_I4` representation used by MSAA + // for `CHILDID_SELF` and the child indices below. + let self_var = unsafe { child_id_variant(CHILDID_SELF) }; // Properties — each call wrapped to swallow per-element COM errors. - let role_int: Option = acc - .get_accRole(&self_var) + let role_int: Option = unsafe { acc.get_accRole(&self_var) } .ok() - .and_then(|v| variant_to_i32(&v)); - let name: Option = acc - .get_accName(&self_var) + .and_then(|v| unsafe { variant_to_i32(&v) }); + let name: Option = unsafe { acc.get_accName(&self_var) } .ok() .map(|b| b.to_string()) .filter(|s| !s.trim().is_empty()); - let default_action: Option = acc - .get_accDefaultAction(&self_var) + let default_action: Option = unsafe { acc.get_accDefaultAction(&self_var) } .ok() .map(|b| b.to_string()) .filter(|s| !s.trim().is_empty()); @@ -239,9 +248,7 @@ unsafe fn walk( let mut t = 0i32; let mut w = 0i32; let mut h = 0i32; - if acc - .accLocation(&mut l, &mut t, &mut w, &mut h, &self_var) - .is_ok() + if unsafe { acc.accLocation(&mut l, &mut t, &mut w, &mut h, &self_var) }.is_ok() && w > 0 && h > 0 { @@ -321,22 +328,24 @@ unsafe fn walk( nodes.push(node); // Recurse via accChildCount + get_accChild. - let child_count = acc.accChildCount().unwrap_or(0); + let child_count = unsafe { acc.accChildCount() }.unwrap_or(0); for i in 1..=child_count { - let child_var = child_id_variant(i); + let child_var = unsafe { child_id_variant(i) }; // accChild returns IDispatch — query for IAccessible. - if let Ok(child_disp) = acc.get_accChild(&child_var) { + if let Ok(child_disp) = unsafe { acc.get_accChild(&child_var) } { if let Ok(child_acc) = child_disp.cast::() { - walk( - &child_acc, - depth + 1, - next_parent, - nodes, - counter, - total, - max_depth, - max_total, - ); + unsafe { + walk( + &child_acc, + depth + 1, + next_parent, + nodes, + counter, + total, + max_depth, + max_total, + ) + }; } } } @@ -345,21 +354,23 @@ unsafe fn walk( // Non-emitting path (filtered out by !is_actionable && !has_content): still // recurse, propagating the same parent_index. - let child_count = acc.accChildCount().unwrap_or(0); + let child_count = unsafe { acc.accChildCount() }.unwrap_or(0); for i in 1..=child_count { - let child_var = child_id_variant(i); - if let Ok(child_disp) = acc.get_accChild(&child_var) { + let child_var = unsafe { child_id_variant(i) }; + if let Ok(child_disp) = unsafe { acc.get_accChild(&child_var) } { if let Ok(child_acc) = child_disp.cast::() { - walk( - &child_acc, - depth + 1, - parent_index, - nodes, - counter, - total, - max_depth, - max_total, - ); + unsafe { + walk( + &child_acc, + depth + 1, + parent_index, + nodes, + counter, + total, + max_depth, + max_total, + ) + }; } } } @@ -374,18 +385,26 @@ unsafe fn walk( /// `ManuallyDrop` is dereferenced explicitly. unsafe fn child_id_variant(id: i32) -> VARIANT { let mut var = VARIANT::default(); - (*var.Anonymous.Anonymous).vt = VT_I4; - (*var.Anonymous.Anonymous).Anonymous.lVal = id; + // SAFETY: `VARIANT::default` is initialized, and setting `vt = VT_I4` + // selects the `lVal` union member written immediately afterward. + unsafe { + (*var.Anonymous.Anonymous).vt = VT_I4; + (*var.Anonymous.Anonymous).Anonymous.lVal = id; + } var } /// Read a `VT_I4` out of a VARIANT. `get_accRole` returns `VT_I4` in practice /// (custom roles may arrive as `VT_BSTR`, which we map to `None` = unknown). unsafe fn variant_to_i32(v: &VARIANT) -> Option { - if (*v.Anonymous.Anonymous).vt == VT_I4 { - Some((*v.Anonymous.Anonymous).Anonymous.lVal) - } else { - None + // SAFETY: the `lVal` union member is read only after the discriminant is + // confirmed to be `VT_I4`. + unsafe { + if (*v.Anonymous.Anonymous).vt == VT_I4 { + Some((*v.Anonymous.Anonymous).Anonymous.lVal) + } else { + None + } } } diff --git a/src/apps/desktop/src/computer_use/windows_wgc_capture.rs b/src/apps/desktop/src/computer_use/windows_wgc_capture.rs index c601c37b83..e5fde82153 100644 --- a/src/apps/desktop/src/computer_use/windows_wgc_capture.rs +++ b/src/apps/desktop/src/computer_use/windows_wgc_capture.rs @@ -103,22 +103,12 @@ unsafe fn create_d3d11_device() -> BitFunResult<(ID3D11Device, ID3D11DeviceConte let mut context: Option = None; let flags = D3D11_CREATE_DEVICE_BGRA_SUPPORT; - if D3D11CreateDevice( - None, - D3D_DRIVER_TYPE_HARDWARE, - HMODULE::default(), - flags, - None, - D3D11_SDK_VERSION, - Some(&mut device), - None, - Some(&mut context), - ) - .is_err() - { + // SAFETY: output pointers reference live `Option` slots, and all remaining + // arguments are documented D3D11 constants or null/default handles. + if unsafe { D3D11CreateDevice( None, - D3D_DRIVER_TYPE_WARP, + D3D_DRIVER_TYPE_HARDWARE, HMODULE::default(), flags, None, @@ -127,6 +117,22 @@ unsafe fn create_d3d11_device() -> BitFunResult<(ID3D11Device, ID3D11DeviceConte None, Some(&mut context), ) + } + .is_err() + { + unsafe { + D3D11CreateDevice( + None, + D3D_DRIVER_TYPE_WARP, + HMODULE::default(), + flags, + None, + D3D11_SDK_VERSION, + Some(&mut device), + None, + Some(&mut context), + ) + } .map_err(|e| BitFunError::service(format!("D3D11CreateDevice (WARP): {e}")))?; } @@ -143,7 +149,9 @@ unsafe fn create_winrt_d3d_device(d3d_device: &ID3D11Device) -> BitFunResult() + // SAFETY: `access` is the live DXGI interface for `surface`; the requested + // interface type matches the WGC frame surface contract. + let src_texture: ID3D11Texture2D = unsafe { access.GetInterface::() } .map_err(|e| BitFunError::service(format!("GetInterface ID3D11Texture2D: {e}")))?; let mut desc = D3D11_TEXTURE2D_DESC::default(); - src_texture.GetDesc(&mut desc); + unsafe { src_texture.GetDesc(&mut desc) }; let width = desc.Width; let height = desc.Height; if width == 0 || height == 0 { @@ -188,31 +197,34 @@ unsafe fn copy_frame_to_bgra( MiscFlags: 0, }; let mut staging: Option = None; - d3d_device - .CreateTexture2D(&staging_desc, None, Some(&mut staging)) + // SAFETY: `staging_desc` is fully initialized and `staging` is a live + // output slot for the newly created texture interface. + unsafe { d3d_device.CreateTexture2D(&staging_desc, None, Some(&mut staging)) } .map_err(|e| BitFunError::service(format!("CreateTexture2D staging: {e}")))?; let staging = staging.ok_or_else(|| { BitFunError::service("CreateTexture2D returned null staging texture".to_string()) })?; - d3d_context.CopyResource(&staging, &src_texture); + unsafe { d3d_context.CopyResource(&staging, &src_texture) }; let mut mapped = windows::Win32::Graphics::Direct3D11::D3D11_MAPPED_SUBRESOURCE::default(); - d3d_context - .Map(&staging, 0, D3D11_MAP_READ, 0, Some(&mut mapped)) + unsafe { d3d_context.Map(&staging, 0, D3D11_MAP_READ, 0, Some(&mut mapped)) } .map_err(|e| BitFunError::service(format!("Map staging texture: {e}")))?; let row_pitch = mapped.RowPitch as usize; let width_bytes = (width as usize) * 4; let mut pixels = vec![0u8; (width as usize) * (height as usize) * 4]; let src = mapped.pData as *const u8; + // SAFETY: a successful `Map` exposes `height` rows at `pData`, each with + // at least `width * 4` readable bytes according to `RowPitch`. Destination + // rows are disjoint slices of the fully allocated `pixels` buffer. for y in 0..height as usize { - let src_row = src.add(y * row_pitch); - let dst_row = pixels.as_mut_ptr().add(y * width_bytes); - std::ptr::copy_nonoverlapping(src_row, dst_row, width_bytes); + let src_row = unsafe { src.add(y * row_pitch) }; + let dst_row = unsafe { pixels.as_mut_ptr().add(y * width_bytes) }; + unsafe { std::ptr::copy_nonoverlapping(src_row, dst_row, width_bytes) }; } - d3d_context.Unmap(&staging, 0); + unsafe { d3d_context.Unmap(&staging, 0) }; Ok((pixels, width, height)) } diff --git a/src/crates/adapters/webdriver/src/platform/evaluator/windows.rs b/src/crates/adapters/webdriver/src/platform/evaluator/windows.rs index 7e3ee37cb0..6626ad1835 100644 --- a/src/crates/adapters/webdriver/src/platform/evaluator/windows.rs +++ b/src/crates/adapters/webdriver/src/platform/evaluator/windows.rs @@ -172,14 +172,14 @@ impl ICoreWebView2WebMessageReceivedEventHandler_Impl for WebMessageReceivedHand unsafe fn register_message_handler(webview: &ICoreWebView2) -> Result<(), WebDriverErrorResponse> { let handler: ICoreWebView2WebMessageReceivedEventHandler = WebMessageReceivedHandler.into(); - let mut token = std::mem::zeroed(); - webview - .add_WebMessageReceived(&handler, &raw mut token) - .map_err(|error| { - WebDriverErrorResponse::unknown_error(format!( - "Failed to register WebView2 message handler: {error:?}" - )) - })?; + // SAFETY: `EventRegistrationToken` is an FFI value initialized by WebView2, + // and both COM interface references remain valid for the duration of the call. + let mut token = unsafe { std::mem::zeroed() }; + unsafe { webview.add_WebMessageReceived(&handler, &raw mut token) }.map_err(|error| { + WebDriverErrorResponse::unknown_error(format!( + "Failed to register WebView2 message handler: {error:?}" + )) + })?; std::mem::forget(handler); Ok(()) diff --git a/src/crates/assembly/core/src/agentic/tools/product_runtime/catalog.rs b/src/crates/assembly/core/src/agentic/tools/product_runtime/catalog.rs index 042f320e07..bc2fbb0cf6 100644 --- a/src/crates/assembly/core/src/agentic/tools/product_runtime/catalog.rs +++ b/src/crates/assembly/core/src/agentic/tools/product_runtime/catalog.rs @@ -782,7 +782,7 @@ mod tests { .allowed_tool_names .contains(&"ReviewPlatform".to_string())); assert!(manifest - .collapsed_tool_names + .deferred_tool_names .contains(&"ReviewPlatform".to_string())); assert!(manifest .tool_definitions diff --git a/src/crates/services/terminal/src/transcript.rs b/src/crates/services/terminal/src/transcript.rs index cf3a5f190d..ec49af3c89 100644 --- a/src/crates/services/terminal/src/transcript.rs +++ b/src/crates/services/terminal/src/transcript.rs @@ -1,5 +1,5 @@ //! Persistent plain-text transcripts for user-created terminal sessions. -//! For terminals with shell integration, the remaining text is command output: prompt and command-input rendering are omitted. +//! For terminals with shell integration, the remaining text is command output: prompt and command-input rendering are omitted. //! Terminals without shell integration retain raw terminal text. use std::collections::HashMap;