car-desktop 0.15.1

OS-level screen capture, accessibility inspection, and input synthesis for Common Agent Runtime
Documentation
//! Error types for `car-desktop`.
//!
//! Every failure path in the crate surfaces as one of these variants.
//! No `todo!()`, no `unimplemented!()`, no silent no-ops — if the code
//! can't do the thing, it says so with a structured error the caller
//! can route on.

use thiserror::Error;

/// The canonical error returned by every `DesktopBackend` method.
#[derive(Debug, Error)]
pub enum CarDesktopError {
    /// The current platform has no implemented backend for this operation.
    /// Windows and Linux are in this state for the entire v1; see
    /// docs/CAR_DESKTOP.md for the Q2/Q3 roadmap.
    #[error("platform `{platform}` is not supported in this build")]
    PlatformUnsupported { platform: &'static str },

    /// A feature is not yet implemented in the current task delivery
    /// sequence. The `task_id` points at the task in the plan
    /// (docs/CAR_DESKTOP.md) that will land the implementation. This
    /// variant SHOULD be empty in production builds — if it trips, the
    /// plan is mid-flight and the caller is asking for a capability
    /// scheduled for a later sprint.
    #[error("feature `{feature}` not yet implemented (scheduled: {task_id})")]
    NotYetImplemented {
        feature: &'static str,
        task_id: &'static str,
    },

    /// OS refused the operation because the required permission is
    /// not granted by the user. On macOS this covers both Screen
    /// Recording and Accessibility TCC scopes.
    #[error("permission denied: {permission:?} — user must grant in System Settings and relaunch")]
    PermissionDenied { permission: Permission },

    /// The target OS version is too old for the APIs this backend
    /// relies on (e.g. ScreenCaptureKit requires macOS 12.3+).
    #[error("OS version too old: {detail}")]
    OsTooOld { detail: String },

    /// A lookup for a window by PID / bundle ID / title filter found
    /// no match, or the window referenced by a handle no longer exists.
    #[error("window not found: {detail}")]
    WindowNotFound { detail: String },

    /// Input synthesis rejected because the computed screen point lies
    /// outside the target window's frame. This is a safety guard —
    /// clicks that miss the target window are always a bug and never
    /// silently tolerated.
    #[error("click point {x},{y} lies outside target window frame {frame:?}")]
    OutOfTargetWindow {
        x: f64,
        y: f64,
        frame: crate::models::WindowFrame,
    },

    /// The requested click target matched the destructive-action word
    /// list (delete / quit / publish / submit / pay / etc.) and the
    /// request did not carry the explicit `unsafe_ok` opt-in.
    #[error("destructive action `{label}` requires explicit `unsafe_ok: true`")]
    DestructiveActionGated { label: String },

    /// The global Esc-Esc kill switch fired while an in-flight mission
    /// was running. The caller should treat this as a user-requested
    /// abort and unwind cleanly.
    #[error("mission aborted by kill switch")]
    KillSwitchActivated,

    /// The input synthesis rate limiter rejected a burst. Rate is
    /// 8 events/sec/window by design — programmatic speeds faster than
    /// that are never a real human UI interaction and likely a bug.
    #[error("rate limit: target window has exceeded 8 events/sec")]
    RateLimited,

    /// An underlying OS API returned an unexpected error. The inner
    /// message comes from the platform layer; `source` attaches the
    /// concrete error type where available.
    #[error("OS API failure: {detail}")]
    OsApi {
        detail: String,
        #[source]
        source: Option<Box<dyn std::error::Error + Send + Sync>>,
    },

    /// A wait-for-condition step timed out without observing the
    /// requested state.
    #[error("timed out after {elapsed_ms}ms waiting for {condition}")]
    WaitTimeout { condition: String, elapsed_ms: u64 },

    /// Attempted to use an element_id that does not appear in the
    /// most-recently-observed UI map.
    #[error("element id `{element_id}` not present in the last observation")]
    UnknownElement { element_id: String },

    /// Input text contained a character outside the supported set.
    /// Currently the macOS backend accepts any Unicode scalar, but
    /// this variant is reserved for future backends with restricted
    /// keyboard coverage.
    #[error("unsupported character in typed text: U+{codepoint:04X}")]
    UnsupportedCharacter { codepoint: u32 },
}

/// The high-level permission scopes `car-desktop` cares about. On
/// macOS these map to the TCC services `kTCCServiceScreenCapture` and
/// `kTCCServiceAccessibility`.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Permission {
    ScreenRecording,
    Accessibility,
}

impl Permission {
    pub fn tcc_service_name(self) -> &'static str {
        match self {
            Self::ScreenRecording => "kTCCServiceScreenCapture",
            Self::Accessibility => "kTCCServiceAccessibility",
        }
    }
}

/// Result alias used by every public API in the crate.
pub type Result<T> = std::result::Result<T, CarDesktopError>;

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn platform_unsupported_renders_helpful_message() {
        let e = CarDesktopError::PlatformUnsupported { platform: "linux" };
        let msg = e.to_string();
        assert!(msg.contains("linux"));
    }

    #[test]
    fn permission_maps_to_tcc_service_name() {
        assert_eq!(
            Permission::ScreenRecording.tcc_service_name(),
            "kTCCServiceScreenCapture"
        );
        assert_eq!(
            Permission::Accessibility.tcc_service_name(),
            "kTCCServiceAccessibility"
        );
    }

    #[test]
    fn out_of_target_window_carries_coords_and_frame() {
        let e = CarDesktopError::OutOfTargetWindow {
            x: 100.0,
            y: 200.0,
            frame: crate::models::WindowFrame {
                x: 0.0,
                y: 0.0,
                width: 50.0,
                height: 50.0,
            },
        };
        let msg = e.to_string();
        assert!(msg.contains("100"));
        assert!(msg.contains("200"));
    }
}