zeuxis 0.2.0

Local read-only MCP screenshot server for screen/window/region capture
//! Platform permission gates for screen capture.
//!
//! macOS requires a Screen Recording preflight/request cycle, Linux currently
//! relies on backend behavior, and other platforms fail before capture work is
//! attempted.

use crate::mcp::errors::ServerError;

/// Boundary that decides whether capture work may proceed.
pub trait PermissionGate: Send + Sync {
    /// Returns `permission_denied` or `capture_unsupported_on_platform` when
    /// the current session cannot capture screens yet.
    fn ensure_capture_allowed(&self) -> Result<(), ServerError>;
}

/// Default permission gate for the current operating system.
#[derive(Debug, Clone, Default)]
pub struct PlatformPermissionGate;

impl PlatformPermissionGate {
    /// Creates a permission gate using platform APIs when available.
    pub const fn new() -> Self {
        Self
    }
}

impl PermissionGate for PlatformPermissionGate {
    fn ensure_capture_allowed(&self) -> Result<(), ServerError> {
        #[cfg(target_os = "macos")]
        {
            let api = CoreGraphicsScreenCaptureAccess;
            evaluate_macos_permission(&api)
        }

        #[cfg(target_os = "linux")]
        {
            Ok(())
        }

        #[cfg(not(any(target_os = "macos", target_os = "linux")))]
        {
            Err(ServerError::capture_unsupported_on_platform(format!(
                "capture is unsupported on platform '{}' in v1; macOS or Linux is required",
                std::env::consts::OS
            )))
        }
    }
}

#[cfg(any(target_os = "macos", test))]
trait MacScreenCaptureAccess {
    fn preflight(&self) -> bool;
    fn request(&self) -> bool;
}

#[cfg(any(target_os = "macos", test))]
fn evaluate_macos_permission(api: &dyn MacScreenCaptureAccess) -> Result<(), ServerError> {
    if api.preflight() {
        return Ok(());
    }

    // Requesting can show the system prompt, but the current capture call still
    // has to fail; clients retry after the user grants Screen Recording access.
    let _ = api.request();

    Err(ServerError::permission_denied(
        "screen capture permission is denied. Grant Screen Recording permission to your terminal app in System Settings > Privacy & Security > Screen Recording, then retry the tool call",
    ))
}

#[cfg(target_os = "macos")]
#[derive(Debug, Clone, Copy)]
struct CoreGraphicsScreenCaptureAccess;

#[cfg(target_os = "macos")]
impl MacScreenCaptureAccess for CoreGraphicsScreenCaptureAccess {
    fn preflight(&self) -> bool {
        // SAFETY: CoreGraphics exposes this permission query as a no-argument C
        // function returning a bool and does not retain Rust-managed data.
        unsafe { CGPreflightScreenCaptureAccess() }
    }

    fn request(&self) -> bool {
        // SAFETY: CoreGraphics exposes this permission request as a no-argument C
        // function returning a bool and does not retain Rust-managed data.
        unsafe { CGRequestScreenCaptureAccess() }
    }
}

#[cfg(target_os = "macos")]
#[link(name = "ApplicationServices", kind = "framework")]
unsafe extern "C" {
    fn CGPreflightScreenCaptureAccess() -> bool;
    fn CGRequestScreenCaptureAccess() -> bool;
}

#[cfg(test)]
mod tests {
    use std::sync::atomic::{AtomicUsize, Ordering};

    use super::*;

    struct MockMacPermissionApi {
        preflight_result: bool,
        request_result: bool,
        request_calls: AtomicUsize,
    }

    impl MockMacPermissionApi {
        fn new(preflight_result: bool, request_result: bool) -> Self {
            Self {
                preflight_result,
                request_result,
                request_calls: AtomicUsize::new(0),
            }
        }
    }

    impl MacScreenCaptureAccess for MockMacPermissionApi {
        fn preflight(&self) -> bool {
            self.preflight_result
        }

        fn request(&self) -> bool {
            self.request_calls.fetch_add(1, Ordering::SeqCst);
            self.request_result
        }
    }

    #[test]
    fn platform_permissions_allows_when_preflight_is_true() {
        let api = MockMacPermissionApi::new(true, false);
        let result = evaluate_macos_permission(&api);
        assert!(result.is_ok());
        assert_eq!(api.request_calls.load(Ordering::SeqCst), 0);
    }

    #[test]
    fn platform_permissions_requests_once_and_returns_denied_without_retry() {
        let api = MockMacPermissionApi::new(false, true);
        let result = evaluate_macos_permission(&api);

        let error = result.expect_err("permission must still be denied in same invocation");
        assert_eq!(error.error_code(), "permission_denied");
        assert_eq!(api.request_calls.load(Ordering::SeqCst), 1);
    }

    #[test]
    fn platform_permissions_new_constructs_gate() {
        let _gate = PlatformPermissionGate::new();
    }

    #[cfg(target_os = "linux")]
    #[test]
    fn platform_permissions_linux_returns_ok() {
        let gate = PlatformPermissionGate::new();
        let result = gate.ensure_capture_allowed();
        assert!(result.is_ok());
    }

    #[cfg(not(any(target_os = "macos", target_os = "linux")))]
    #[test]
    fn platform_permissions_non_macos_non_linux_returns_unsupported() {
        let gate = PlatformPermissionGate::new();
        let error = gate
            .ensure_capture_allowed()
            .expect_err("unsupported platforms should fail in v1");
        assert_eq!(error.error_code(), "capture_unsupported_on_platform");
    }
}