     1→//! Agent configuration management
     2→
     3→use anyhow::{Context, Result};
     4→use serde::{Deserialize, Serialize};
     5→use std::path::PathBuf;
     6→use uuid::Uuid;
     7→
     8→/// Agent configuration
     9→#[derive(Debug, Clone, Serialize, Deserialize)]
    10→pub struct Config {
    11→    /// Server WebSocket URL (e.g., wss://connect.example.com/ws)
    12→    pub server_url: String,
    13→
    14→    /// Agent API key for authentication
    15→    pub api_key: String,
    16→
    17→    /// Unique agent identifier (generated on first run)
    18→    #[serde(default = "generate_agent_id")]
    19→    pub agent_id: String,
    20→
    21→    /// Optional hostname override
    22→    pub hostname_override: Option<String>,
    23→
    24→    /// Support code for one-time support sessions (set via command line)
    25→    #[serde(skip)]
    26→    pub support_code: Option<String>,
    27→
    28→    /// Capture settings
    29→    #[serde(default)]
    30→    pub capture: CaptureConfig,
    31→
    32→    /// Encoding settings
    33→    #[serde(default)]
    34→    pub encoding: EncodingConfig,
    35→}
    36→
    37→fn generate_agent_id() -> String {
    38→    Uuid::new_v4().to_string()
    39→}
    40→
    41→#[derive(Debug, Clone, Serialize, Deserialize)]
    42→pub struct CaptureConfig {
    43→    /// Target frames per second (1-60)
    44→    #[serde(default = "default_fps")]
    45→    pub fps: u32,
    46→
    47→    /// Use DXGI Desktop Duplication (recommended)
    48→    #[serde(default = "default_true")]
    49→    pub use_dxgi: bool,
    50→
    51→    /// Fall back to GDI if DXGI fails
    52→    #[serde(default = "default_true")]
    53→    pub gdi_fallback: bool,
    54→}
    55→
    56→#[derive(Debug, Clone, Serialize, Deserialize)]
    57→pub struct EncodingConfig {
    58→    /// Preferred codec (auto, raw, vp9, h264)
    59→    #[serde(default = "default_codec")]
    60→    pub codec: String,
    61→
    62→    /// Quality (1-100, higher = better quality, more bandwidth)
    63→    #[serde(default = "default_quality")]
    64→    pub quality: u32,
    65→
    66→    /// Use hardware encoding if available
    67→    #[serde(default = "default_true")]
    68→    pub hardware_encoding: bool,
    69→}
    70→
    71→fn default_fps() -> u32 {
    72→    30
    73→}
    74→
    75→fn default_true() -> bool {
    76→    true
    77→}
    78→
    79→fn default_codec() -> String {
    80→    "auto".to_string()
    81→}
    82→
    83→fn default_quality() -> u32 {
    84→    75
    85→}
    86→
    87→impl Default for CaptureConfig {
    88→    fn default() -> Self {
    89→        Self {
    90→            fps: default_fps(),
    91→            use_dxgi: true,
    92→            gdi_fallback: true,
    93→        }
    94→    }
    95→}
    96→
    97→impl Default for EncodingConfig {
    98→    fn default() -> Self {
    99→        Self {
   100→            codec: default_codec(),
   101→            quality: default_quality(),
   102→            hardware_encoding: true,
   103→        }
   104→    }
   105→}
   106→
   107→impl Config {
   108→    /// Load configuration from file or environment
   109→    pub fn load() -> Result<Self> {
   110→        // Try loading from config file
   111→        let config_path = Self::config_path();
   112→
   113→        if config_path.exists() {
   114→            let contents = std::fs::read_to_string(&config_path)
   115→                .with_context(|| format!("Failed to read config from {:?}", config_path))?;
   116→
   117→            let mut config: Config = toml::from_str(&contents)
   118→                .with_context(|| "Failed to parse config file")?;
   119→
   120→            // Ensure agent_id is set and saved
   121→            if config.agent_id.is_empty() {
   122→                config.agent_id = generate_agent_id();
   123→                let _ = config.save();
   124→            }
   125→
   126→            // support_code is always None when loading from file (set via CLI)
   127→            config.support_code = None;
   128→
   129→            return Ok(config);
   130→        }
   131→
   132→        // Fall back to environment variables
   133→        let server_url = std::env::var("GURUCONNECT_SERVER_URL")
   134→            .unwrap_or_else(|_| "wss://connect.azcomputerguru.com/ws/agent".to_string());
   135→
   136→        let api_key = std::env::var("GURUCONNECT_API_KEY")
   137→            .unwrap_or_else(|_| "dev-key".to_string());
   138→
   139→        let agent_id = std::env::var("GURUCONNECT_AGENT_ID")
   140→            .unwrap_or_else(|_| generate_agent_id());
   141→
   142→        let config = Config {
   143→            server_url,
   144→            api_key,
   145→            agent_id,
   146→            hostname_override: std::env::var("GURUCONNECT_HOSTNAME").ok(),
   147→            support_code: None, // Set via CLI
   148→            capture: CaptureConfig::default(),
   149→            encoding: EncodingConfig::default(),
   150→        };
   151→
   152→        // Save config with generated agent_id for persistence
   153→        let _ = config.save();
   154→
   155→        Ok(config)
   156→    }
   157→
   158→    /// Get the configuration file path
   159→    fn config_path() -> PathBuf {
   160→        // Check for config in current directory first
   161→        let local_config = PathBuf::from("guruconnect.toml");
   162→        if local_config.exists() {
   163→            return local_config;
   164→        }
   165→
   166→        // Check in program data directory (Windows)
   167→        #[cfg(windows)]
   168→        {
   169→            if let Ok(program_data) = std::env::var("ProgramData") {
   170→                let path = PathBuf::from(program_data)
   171→                    .join("GuruConnect")
   172→                    .join("agent.toml");
   173→                if path.exists() {
   174→                    return path;
   175→                }
   176→            }
   177→        }
   178→
   179→        // Default to local config
   180→        local_config
   181→    }
   182→
   183→    /// Get the hostname to use
   184→    pub fn hostname(&self) -> String {
   185→        self.hostname_override
   186→            .clone()
   187→            .unwrap_or_else(|| {
   188→                hostname::get()
   189→                    .map(|h| h.to_string_lossy().to_string())
   190→                    .unwrap_or_else(|_| "unknown".to_string())
   191→            })
   192→    }
   193→
   194→    /// Save current configuration to file
   195→    pub fn save(&self) -> Result<()> {
   196→        let config_path = Self::config_path();
   197→
   198→        // Ensure parent directory exists
   199→        if let Some(parent) = config_path.parent() {
   200→            std::fs::create_dir_all(parent)?;
   201→        }
   202→
   203→        let contents = toml::to_string_pretty(self)?;
   204→        std::fs::write(&config_path, contents)?;
   205→
   206→        Ok(())
   207→    }
   208→}
   209→
   210→/// Example configuration file content
   211→pub fn example_config() -> &'static str {
   212→    r#"# GuruConnect Agent Configuration
   213→
   214→# Server connection
   215→server_url = "wss://connect.example.com/ws"
   216→api_key = "your-agent-api-key"
   217→agent_id = "auto-generated-uuid"
   218→
   219→# Optional: override hostname
   220→# hostname_override = "custom-hostname"
   221→
   222→[capture]
   223→fps = 30
   224→use_dxgi = true
   225→gdi_fallback = true
   226→
   227→[encoding]
   228→codec = "auto"      # auto, raw, vp9, h264
   229→quality = 75        # 1-100
   230→hardware_encoding = true
   231→"#
   232→}
   233→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

//! Agent configuration management

use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use uuid::Uuid;

/// Agent configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Config {
    /// Server WebSocket URL (e.g., wss://connect.example.com/ws)
    pub server_url: String,

    /// Agent API key for authentication
    pub api_key: String,

    /// Unique agent identifier (generated on first run)
    #[serde(default = "generate_agent_id")]
    pub agent_id: String,

    /// Optional hostname override
    pub hostname_override: Option<String>,

    /// Support code for one-time support sessions (set via command line)
    #[serde(skip)]
    pub support_code: Option<String>,

    /// Capture settings
    #[serde(default)]
    pub capture: CaptureConfig,

    /// Encoding settings
    #[serde(default)]
    pub encoding: EncodingConfig,
}

fn generate_agent_id() -> String {
    Uuid::new_v4().to_string()
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CaptureConfig {
    /// Target frames per second (1-60)
    #[serde(default = "default_fps")]
    pub fps: u32,

    /// Use DXGI Desktop Duplication (recommended)
    #[serde(default = "default_true")]
    pub use_dxgi: bool,

    /// Fall back to GDI if DXGI fails
    #[serde(default = "default_true")]
    pub gdi_fallback: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EncodingConfig {
    /// Preferred codec (auto, raw, vp9, h264)
    #[serde(default = "default_codec")]
    pub codec: String,

    /// Quality (1-100, higher = better quality, more bandwidth)
    #[serde(default = "default_quality")]
    pub quality: u32,

    /// Use hardware encoding if available
    #[serde(default = "default_true")]
    pub hardware_encoding: bool,
}

fn default_fps() -> u32 {
    30
}

fn default_true() -> bool {
    true
}

fn default_codec() -> String {
    "auto".to_string()
}

fn default_quality() -> u32 {
    75
}

impl Default for CaptureConfig {
    fn default() -> Self {
        Self {
            fps: default_fps(),
            use_dxgi: true,
            gdi_fallback: true,
        }
    }
}

impl Default for EncodingConfig {
    fn default() -> Self {
        Self {
            codec: default_codec(),
            quality: default_quality(),
            hardware_encoding: true,
        }
    }
}

impl Config {
    /// Load configuration from file or environment
    pub fn load() -> Result<Self> {
        // Try loading from config file
        let config_path = Self::config_path();

        if config_path.exists() {
            let contents = std::fs::read_to_string(&config_path)
                .with_context(|| format!("Failed to read config from {:?}", config_path))?;

            let mut config: Config = toml::from_str(&contents)
                .with_context(|| "Failed to parse config file")?;

            // Ensure agent_id is set and saved
            if config.agent_id.is_empty() {
                config.agent_id = generate_agent_id();
                let _ = config.save();
            }

            // support_code is always None when loading from file (set via CLI)
            config.support_code = None;

            return Ok(config);
        }

        // Fall back to environment variables
        let server_url = std::env::var("GURUCONNECT_SERVER_URL")
            .unwrap_or_else(|_| "wss://connect.azcomputerguru.com/ws/agent".to_string());

        let api_key = std::env::var("GURUCONNECT_API_KEY")
            .unwrap_or_else(|_| "dev-key".to_string());

        let agent_id = std::env::var("GURUCONNECT_AGENT_ID")
            .unwrap_or_else(|_| generate_agent_id());

        let config = Config {
            server_url,
            api_key,
            agent_id,
            hostname_override: std::env::var("GURUCONNECT_HOSTNAME").ok(),
            support_code: None, // Set via CLI
            capture: CaptureConfig::default(),
            encoding: EncodingConfig::default(),
        };

        // Save config with generated agent_id for persistence
        let _ = config.save();

        Ok(config)
    }

    /// Get the configuration file path
    fn config_path() -> PathBuf {
        // Check for config in current directory first
        let local_config = PathBuf::from("guruconnect.toml");
        if local_config.exists() {
            return local_config;
        }

        // Check in program data directory (Windows)
        #[cfg(windows)]
        {
            if let Ok(program_data) = std::env::var("ProgramData") {
                let path = PathBuf::from(program_data)
                    .join("GuruConnect")
                    .join("agent.toml");
                if path.exists() {
                    return path;
                }
            }
        }

        // Default to local config
        local_config
    }

    /// Get the hostname to use
    pub fn hostname(&self) -> String {
        self.hostname_override
            .clone()
            .unwrap_or_else(|| {
                hostname::get()
                    .map(|h| h.to_string_lossy().to_string())
                    .unwrap_or_else(|_| "unknown".to_string())
            })
    }

    /// Save current configuration to file
    pub fn save(&self) -> Result<()> {
        let config_path = Self::config_path();

        // Ensure parent directory exists
        if let Some(parent) = config_path.parent() {
            std::fs::create_dir_all(parent)?;
        }

        let contents = toml::to_string_pretty(self)?;
        std::fs::write(&config_path, contents)?;

        Ok(())
    }
}

/// Example configuration file content
pub fn example_config() -> &'static str {
    r#"# GuruConnect Agent Configuration

# Server connection
server_url = "wss://connect.example.com/ws"
api_key = "your-agent-api-key"
agent_id = "auto-generated-uuid"

# Optional: override hostname
# hostname_override = "custom-hostname"

[capture]
fps = 30
use_dxgi = true
gdi_fallback = true

[encoding]
codec = "auto"      # auto, raw, vp9, h264
quality = 75        # 1-100
hardware_encoding = true
"#
}


     1→//! System tray icon and menu for the agent
     2→//!
     3→//! Provides a tray icon with menu options:
     4→//! - Connection status
     5→//! - Machine name
     6→//! - End session
     7→
     8→use anyhow::Result;
     9→use muda::{Menu, MenuEvent, MenuItem, PredefinedMenuItem, Submenu};
    10→use std::sync::atomic::{AtomicBool, Ordering};
    11→use std::sync::Arc;
    12→use tray_icon::{Icon, TrayIcon, TrayIconBuilder, TrayIconEvent};
    13→use tracing::{info, warn};
    14→
    15→#[cfg(windows)]
    16→use windows::Win32::UI::WindowsAndMessaging::{
    17→    PeekMessageW, TranslateMessage, DispatchMessageW, MSG, PM_REMOVE,
    18→};
    19→
    20→/// Events that can be triggered from the tray menu
    21→#[derive(Debug, Clone)]
    22→pub enum TrayAction {
    23→    EndSession,
    24→    ShowDetails,
    25→    ShowDebugWindow,
    26→}
    27→
    28→/// Tray icon controller
    29→pub struct TrayController {
    30→    _tray_icon: TrayIcon,
    31→    menu: Menu,
    32→    end_session_item: MenuItem,
    33→    debug_item: MenuItem,
    34→    status_item: MenuItem,
    35→    exit_requested: Arc<AtomicBool>,
    36→}
    37→
    38→impl TrayController {
    39→    /// Create a new tray controller
    40→    /// `allow_end_session` - If true, show "End Session" menu item (only for support sessions)
    41→    pub fn new(machine_name: &str, support_code: Option<&str>, allow_end_session: bool) -> Result<Self> {
    42→        // Create menu items
    43→        let status_text = if let Some(code) = support_code {
    44→            format!("Support Session: {}", code)
    45→        } else {
    46→            "Persistent Agent".to_string()
    47→        };
    48→
    49→        let status_item = MenuItem::new(&status_text, false, None);
    50→        let machine_item = MenuItem::new(format!("Machine: {}", machine_name), false, None);
    51→        let separator = PredefinedMenuItem::separator();
    52→
    53→        // Only show "End Session" for support sessions
    54→        // Persistent agents can only be removed by admin
    55→        let end_session_item = if allow_end_session {
    56→            MenuItem::new("End Session", true, None)
    57→        } else {
    58→            MenuItem::new("Managed by Administrator", false, None)
    59→        };
    60→
    61→        // Debug window option (always available)
    62→        let debug_item = MenuItem::new("Show Debug Window", true, None);
    63→
    64→        // Build menu
    65→        let menu = Menu::new();
    66→        menu.append(&status_item)?;
    67→        menu.append(&machine_item)?;
    68→        menu.append(&separator)?;
    69→        menu.append(&debug_item)?;
    70→        menu.append(&end_session_item)?;
    71→
    72→        // Create tray icon
    73→        let icon = create_default_icon()?;
    74→
    75→        let tray_icon = TrayIconBuilder::new()
    76→            .with_menu(Box::new(menu.clone()))
    77→            .with_tooltip(format!("GuruConnect - {}", machine_name))
    78→            .with_icon(icon)
    79→            .build()?;
    80→
    81→        let exit_requested = Arc::new(AtomicBool::new(false));
    82→
    83→        Ok(Self {
    84→            _tray_icon: tray_icon,
    85→            menu,
    86→            end_session_item,
    87→            debug_item,
    88→            status_item,
    89→            exit_requested,
    90→        })
    91→    }
    92→
    93→    /// Check if exit has been requested
    94→    pub fn exit_requested(&self) -> bool {
    95→        self.exit_requested.load(Ordering::SeqCst)
    96→    }
    97→
    98→    /// Update the connection status display
    99→    pub fn update_status(&self, status: &str) {
   100→        self.status_item.set_text(status);

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

//! System tray icon and menu for the agent
//!
//! Provides a tray icon with menu options:
//! - Connection status
//! - Machine name
//! - End session

use anyhow::Result;
use muda::{Menu, MenuEvent, MenuItem, PredefinedMenuItem, Submenu};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use tray_icon::{Icon, TrayIcon, TrayIconBuilder, TrayIconEvent};
use tracing::{info, warn};

#[cfg(windows)]
use windows::Win32::UI::WindowsAndMessaging::{
    PeekMessageW, TranslateMessage, DispatchMessageW, MSG, PM_REMOVE,
};

/// Events that can be triggered from the tray menu
#[derive(Debug, Clone)]
pub enum TrayAction {
    EndSession,
    ShowDetails,
    ShowDebugWindow,
}

/// Tray icon controller
pub struct TrayController {
    _tray_icon: TrayIcon,
    menu: Menu,
    end_session_item: MenuItem,
    debug_item: MenuItem,
    status_item: MenuItem,
    exit_requested: Arc<AtomicBool>,
}

impl TrayController {
    /// Create a new tray controller
    /// `allow_end_session` - If true, show "End Session" menu item (only for support sessions)
    pub fn new(machine_name: &str, support_code: Option<&str>, allow_end_session: bool) -> Result<Self> {
        // Create menu items
        let status_text = if let Some(code) = support_code {
            format!("Support Session: {}", code)
        } else {
            "Persistent Agent".to_string()
        };

        let status_item = MenuItem::new(&status_text, false, None);
        let machine_item = MenuItem::new(format!("Machine: {}", machine_name), false, None);
        let separator = PredefinedMenuItem::separator();

        // Only show "End Session" for support sessions
        // Persistent agents can only be removed by admin
        let end_session_item = if allow_end_session {
            MenuItem::new("End Session", true, None)
        } else {
            MenuItem::new("Managed by Administrator", false, None)
        };

        // Debug window option (always available)
        let debug_item = MenuItem::new("Show Debug Window", true, None);

        // Build menu
        let menu = Menu::new();
        menu.append(&status_item)?;
        menu.append(&machine_item)?;
        menu.append(&separator)?;
        menu.append(&debug_item)?;
        menu.append(&end_session_item)?;

        // Create tray icon
        let icon = create_default_icon()?;

        let tray_icon = TrayIconBuilder::new()
            .with_menu(Box::new(menu.clone()))
            .with_tooltip(format!("GuruConnect - {}", machine_name))
            .with_icon(icon)
            .build()?;

        let exit_requested = Arc::new(AtomicBool::new(false));

        Ok(Self {
            _tray_icon: tray_icon,
            menu,
            end_session_item,
            debug_item,
            status_item,
            exit_requested,
        })
    }

    /// Check if exit has been requested
    pub fn exit_requested(&self) -> bool {
        self.exit_requested.load(Ordering::SeqCst)
    }

    /// Update the connection status display
    pub fn update_status(&self, status: &str) {
        self.status_item.set_text(status);


Perfect! Now let me check the GuruRMM agent for comparison on service patterns:


     1→//! Windows Service implementation for GuruRMM Agent
     2→//!
     3→//! This module implements the Windows Service Control Manager (SCM) protocol,
     4→//! allowing the agent to run as a native Windows service without third-party wrappers.
     5→
     6→#[cfg(all(windows, feature = "native-service"))]
     7→pub mod windows {
     8→    use std::ffi::OsString;
     9→    use std::path::PathBuf;
    10→    use std::sync::mpsc;
    11→    use std::time::Duration;
    12→
    13→    use anyhow::{Context, Result};
    14→    use tracing::{error, info, warn};
    15→    use windows_service::{
    16→        define_windows_service,
    17→        service::{
    18→            ServiceAccess, ServiceControl, ServiceControlAccept, ServiceErrorControl,
    19→            ServiceExitCode, ServiceInfo, ServiceStartType, ServiceState, ServiceStatus,
    20→            ServiceType,
    21→        },
    22→        service_control_handler::{self, ServiceControlHandlerResult},
    23→        service_dispatcher, service_manager::{ServiceManager, ServiceManagerAccess},
    24→    };
    25→
    26→    pub const SERVICE_NAME: &str = "GuruRMMAgent";
    27→    pub const SERVICE_DISPLAY_NAME: &str = "GuruRMM Agent";
    28→    pub const SERVICE_DESCRIPTION: &str =
    29→        "GuruRMM Agent - Remote Monitoring and Management service";
    30→    pub const INSTALL_DIR: &str = r"C:\Program Files\GuruRMM";
    31→    pub const CONFIG_DIR: &str = r"C:\ProgramData\GuruRMM";
    32→
    33→    // Generate the Windows service boilerplate
    34→    define_windows_service!(ffi_service_main, service_main);
    35→
    36→    /// Entry point called by the Windows Service Control Manager
    37→    pub fn run_as_service() -> Result<()> {
    38→        // This function is called when Windows starts the service.
    39→        // It blocks until the service is stopped.
    40→        service_dispatcher::start(SERVICE_NAME, ffi_service_main)
    41→            .context("Failed to start service dispatcher")?;
    42→        Ok(())
    43→    }
    44→
    45→    /// Main service function called by the SCM
    46→    fn service_main(arguments: Vec<OsString>) {
    47→        if let Err(e) = run_service(arguments) {
    48→            error!("Service error: {}", e);
    49→        }
    50→    }
    51→
    52→    /// The actual service implementation
    53→    fn run_service(_arguments: Vec<OsString>) -> Result<()> {
    54→        // Create a channel to receive stop events
    55→        let (shutdown_tx, shutdown_rx) = mpsc::channel();
    56→
    57→        // Create the service control handler
    58→        let event_handler = move |control_event| -> ServiceControlHandlerResult {
    59→            match control_event {
    60→                ServiceControl::Stop => {
    61→                    info!("Received stop command from SCM");
    62→                    let _ = shutdown_tx.send(());
    63→                    ServiceControlHandlerResult::NoError
    64→                }
    65→                ServiceControl::Interrogate => ServiceControlHandlerResult::NoError,
    66→                ServiceControl::Shutdown => {
    67→                    info!("Received shutdown command from SCM");
    68→                    let _ = shutdown_tx.send(());
    69→                    ServiceControlHandlerResult::NoError
    70→                }
    71→                _ => ServiceControlHandlerResult::NotImplemented,
    72→            }
    73→        };
    74→
    75→        // Register the service control handler
    76→        let status_handle = service_control_handler::register(SERVICE_NAME, event_handler)
    77→            .context("Failed to register service control handler")?;
    78→
    79→        // Report that we're starting
    80→        status_handle
    81→            .set_service_status(ServiceStatus {
    82→                service_type: ServiceType::OWN_PROCESS,
    83→                current_state: ServiceState::StartPending,
    84→                controls_accepted: ServiceControlAccept::empty(),
    85→                exit_code: ServiceExitCode::Win32(0),
    86→                checkpoint: 0,
    87→                wait_hint: Duration::from_secs(10),
    88→                process_id: None,
    89→            })
    90→            .context("Failed to set StartPending status")?;
    91→
    92→        // Determine config path
    93→        let config_path = PathBuf::from(format!(r"{}\\agent.toml", CONFIG_DIR));
    94→
    95→        // Create the tokio runtime for the agent
    96→        let runtime = tokio::runtime::Runtime::new().context("Failed to create tokio runtime")?;
    97→
    98→        // Start the agent in the runtime
    99→        let agent_result = runtime.block_on(async {
   100→            // Load configuration
   101→            let config = match crate::config::AgentConfig::load(&config_path) {
   102→                Ok(c) => c,
   103→                Err(e) => {
   104→                    error!("Failed to load config from {:?}: {}", config_path, e);
   105→                    return Err(anyhow::anyhow!("Config load failed: {}", e));
   106→                }
   107→            };
   108→
   109→            info!("GuruRMM Agent service starting...");
   110→            info!("Config loaded from {:?}", config_path);
   111→            info!("Server URL: {}", config.server.url);
   112→
   113→            // Initialize metrics collector
   114→            let metrics_collector = crate::metrics::MetricsCollector::new();
   115→            info!("Metrics collector initialized");
   116→
   117→            // Create shared state
   118→            let state = std::sync::Arc::new(crate::AppState {
   119→                config: config.clone(),
   120→                metrics_collector,
   121→                connected: tokio::sync::RwLock::new(false),
   122→            });
   123→
   124→            // Report that we're running
   125→            status_handle
   126→                .set_service_status(ServiceStatus {
   127→                    service_type: ServiceType::OWN_PROCESS,
   128→                    current_state: ServiceState::Running,
   129→                    controls_accepted: ServiceControlAccept::STOP | ServiceControlAccept::SHUTDOWN,
   130→                    exit_code: ServiceExitCode::Win32(0),
   131→                    checkpoint: 0,
   132→                    wait_hint: Duration::default(),
   133→                    process_id: None,
   134→                })
   135→                .context("Failed to set Running status")?;
   136→
   137→            // Start WebSocket client task
   138→            let ws_state = std::sync::Arc::clone(&state);
   139→            let ws_handle = tokio::spawn(async move {
   140→                loop {
   141→                    info!("Connecting to server...");
   142→                    match crate::transport::WebSocketClient::connect_and_run(std::sync::Arc::clone(
   143→                        &ws_state,
   144→                    ))
   145→                    .await
   146→                    {
   147→                        Ok(_) => {
   148→                            warn!("WebSocket connection closed normally, reconnecting...");
   149→                        }
   150→                        Err(e) => {

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

//! Windows Service implementation for GuruRMM Agent
//!
//! This module implements the Windows Service Control Manager (SCM) protocol,
//! allowing the agent to run as a native Windows service without third-party wrappers.

#[cfg(all(windows, feature = "native-service"))]
pub mod windows {
    use std::ffi::OsString;
    use std::path::PathBuf;
    use std::sync::mpsc;
    use std::time::Duration;

    use anyhow::{Context, Result};
    use tracing::{error, info, warn};
    use windows_service::{
        define_windows_service,
        service::{
            ServiceAccess, ServiceControl, ServiceControlAccept, ServiceErrorControl,
            ServiceExitCode, ServiceInfo, ServiceStartType, ServiceState, ServiceStatus,
            ServiceType,
        },
        service_control_handler::{self, ServiceControlHandlerResult},
        service_dispatcher, service_manager::{ServiceManager, ServiceManagerAccess},
    };

    pub const SERVICE_NAME: &str = "GuruRMMAgent";
    pub const SERVICE_DISPLAY_NAME: &str = "GuruRMM Agent";
    pub const SERVICE_DESCRIPTION: &str =
        "GuruRMM Agent - Remote Monitoring and Management service";
    pub const INSTALL_DIR: &str = r"C:\Program Files\GuruRMM";
    pub const CONFIG_DIR: &str = r"C:\ProgramData\GuruRMM";

    // Generate the Windows service boilerplate
    define_windows_service!(ffi_service_main, service_main);

    /// Entry point called by the Windows Service Control Manager
    pub fn run_as_service() -> Result<()> {
        // This function is called when Windows starts the service.
        // It blocks until the service is stopped.
        service_dispatcher::start(SERVICE_NAME, ffi_service_main)
            .context("Failed to start service dispatcher")?;
        Ok(())
    }

    /// Main service function called by the SCM
    fn service_main(arguments: Vec<OsString>) {
        if let Err(e) = run_service(arguments) {
            error!("Service error: {}", e);
        }
    }

    /// The actual service implementation
    fn run_service(_arguments: Vec<OsString>) -> Result<()> {
        // Create a channel to receive stop events
        let (shutdown_tx, shutdown_rx) = mpsc::channel();

        // Create the service control handler
        let event_handler = move |control_event| -> ServiceControlHandlerResult {
            match control_event {
                ServiceControl::Stop => {
                    info!("Received stop command from SCM");
                    let _ = shutdown_tx.send(());
                    ServiceControlHandlerResult::NoError
                }
                ServiceControl::Interrogate => ServiceControlHandlerResult::NoError,
                ServiceControl::Shutdown => {
                    info!("Received shutdown command from SCM");
                    let _ = shutdown_tx.send(());
                    ServiceControlHandlerResult::NoError
                }
                _ => ServiceControlHandlerResult::NotImplemented,
            }
        };

        // Register the service control handler
        let status_handle = service_control_handler::register(SERVICE_NAME, event_handler)
            .context("Failed to register service control handler")?;

        // Report that we're starting
        status_handle
            .set_service_status(ServiceStatus {
                service_type: ServiceType::OWN_PROCESS,
                current_state: ServiceState::StartPending,
                controls_accepted: ServiceControlAccept::empty(),
                exit_code: ServiceExitCode::Win32(0),
                checkpoint: 0,
                wait_hint: Duration::from_secs(10),
                process_id: None,
            })
            .context("Failed to set StartPending status")?;

        // Determine config path
        let config_path = PathBuf::from(format!(r"{}\\agent.toml", CONFIG_DIR));

        // Create the tokio runtime for the agent
        let runtime = tokio::runtime::Runtime::new().context("Failed to create tokio runtime")?;

        // Start the agent in the runtime
        let agent_result = runtime.block_on(async {
            // Load configuration
            let config = match crate::config::AgentConfig::load(&config_path) {
                Ok(c) => c,
                Err(e) => {
                    error!("Failed to load config from {:?}: {}", config_path, e);
                    return Err(anyhow::anyhow!("Config load failed: {}", e));
                }
            };

            info!("GuruRMM Agent service starting...");
            info!("Config loaded from {:?}", config_path);
            info!("Server URL: {}", config.server.url);

            // Initialize metrics collector
            let metrics_collector = crate::metrics::MetricsCollector::new();
            info!("Metrics collector initialized");

            // Create shared state
            let state = std::sync::Arc::new(crate::AppState {
                config: config.clone(),
                metrics_collector,
                connected: tokio::sync::RwLock::new(false),
            });

            // Report that we're running
            status_handle
                .set_service_status(ServiceStatus {
                    service_type: ServiceType::OWN_PROCESS,
                    current_state: ServiceState::Running,
                    controls_accepted: ServiceControlAccept::STOP | ServiceControlAccept::SHUTDOWN,
                    exit_code: ServiceExitCode::Win32(0),
                    checkpoint: 0,
                    wait_hint: Duration::default(),
                    process_id: None,
                })
                .context("Failed to set Running status")?;

            // Start WebSocket client task
            let ws_state = std::sync::Arc::clone(&state);
            let ws_handle = tokio::spawn(async move {
                loop {
                    info!("Connecting to server...");
                    match crate::transport::WebSocketClient::connect_and_run(std::sync::Arc::clone(
                        &ws_state,
                    ))
                    .await
                    {
                        Ok(_) => {
                            warn!("WebSocket connection closed normally, reconnecting...");
                        }
                        Err(e) => {


     1→[package]
     2→name = "gururmm-agent"
     3→version = "0.3.5"
     4→edition = "2021"
     5→description = "GuruRMM Agent - Cross-platform RMM agent"
     6→authors = ["GuruRMM"]
     7→
     8→[features]
     9→default = ["native-service"]
    10→# Modern Windows (10+, Server 2016+): Native Windows Service integration
    11→native-service = ["dep:windows-service", "dep:windows"]
    12→# Legacy Windows (7, Server 2008 R2): Console mode, use NSSM for service wrapper
    13→legacy = []
    14→
    15→[dependencies]
    16→# Async runtime
    17→tokio = { version = "1", features = ["full"] }
    18→
    19→# System information (cross-platform metrics)
    20→sysinfo = "0.31"
    21→
    22→# WebSocket client (native-tls for Windows 7/2008R2 compatibility)
    23→tokio-tungstenite = { version = "0.24", features = ["native-tls"] }
    24→futures-util = "0.3"
    25→
    26→# HTTP client (fallback/registration) - native-tls for Windows 7/2008R2 compatibility
    27→reqwest = { version = "0.12", default-features = false, features = ["json", "native-tls"] }
    28→
    29→# Serialization
    30→serde = { version = "1", features = ["derive"] }
    31→serde_json = "1"
    32→toml = "0.8"
    33→
    34→# CLI arguments
    35→clap = { version = "4", features = ["derive"] }
    36→
    37→# Logging
    38→tracing = "0.1"
    39→tracing-subscriber = { version = "0.3", features = ["env-filter"] }
    40→
    41→# Error handling
    42→anyhow = "1"
    43→thiserror = "1"
    44→
    45→# UUID for identifiers
    46→uuid = { version = "1", features = ["v4", "serde"] }
    47→
    48→# SHA256 checksums for update verification
    49→sha2 = "0.10"
    50→
    51→# Time handling
    52→chrono = { version = "0.4", features = ["serde"] }
    53→
    54→# Hostname detection
    55→hostname = "0.4"
    56→
    57→# Network interface enumeration (LAN IPs)
    58→local-ip-address = "0.6"
    59→
    60→# Async file operations
    61→tokio-util = "0.7"
    62→
    63→[target.'cfg(windows)'.dependencies]
    64→# Windows service support (optional, only for native-service feature)
    65→windows-service = { version = "0.7", optional = true }
    66→# Windows-specific APIs for service management (optional)
    67→windows = { version = "0.58", optional = true, features = [
    68→    "Win32_System_Services",
    69→    "Win32_Foundation",
    70→    "Win32_Security",
    71→] }
    72→
    73→[target.'cfg(unix)'.dependencies]
    74→# Unix signal handling and user detection
    75→nix = { version = "0.29", features = ["signal", "user"] }
    76→
    77→[profile.release]
    78→# Optimize for size while maintaining performance
    79→opt-level = "z"
    80→lto = true
    81→codegen-units = 1
    82→strip = true
    83→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

[package]
name = "gururmm-agent"
version = "0.3.5"
edition = "2021"
description = "GuruRMM Agent - Cross-platform RMM agent"
authors = ["GuruRMM"]

[features]
default = ["native-service"]
# Modern Windows (10+, Server 2016+): Native Windows Service integration
native-service = ["dep:windows-service", "dep:windows"]
# Legacy Windows (7, Server 2008 R2): Console mode, use NSSM for service wrapper
legacy = []

[dependencies]
# Async runtime
tokio = { version = "1", features = ["full"] }

# System information (cross-platform metrics)
sysinfo = "0.31"

# WebSocket client (native-tls for Windows 7/2008R2 compatibility)
tokio-tungstenite = { version = "0.24", features = ["native-tls"] }
futures-util = "0.3"

# HTTP client (fallback/registration) - native-tls for Windows 7/2008R2 compatibility
reqwest = { version = "0.12", default-features = false, features = ["json", "native-tls"] }

# Serialization
serde = { version = "1", features = ["derive"] }
serde_json = "1"
toml = "0.8"

# CLI arguments
clap = { version = "4", features = ["derive"] }

# Logging
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }

# Error handling
anyhow = "1"
thiserror = "1"

# UUID for identifiers
uuid = { version = "1", features = ["v4", "serde"] }

# SHA256 checksums for update verification
sha2 = "0.10"

# Time handling
chrono = { version = "0.4", features = ["serde"] }

# Hostname detection
hostname = "0.4"

# Network interface enumeration (LAN IPs)
local-ip-address = "0.6"

# Async file operations
tokio-util = "0.7"

[target.'cfg(windows)'.dependencies]
# Windows service support (optional, only for native-service feature)
windows-service = { version = "0.7", optional = true }
# Windows-specific APIs for service management (optional)
windows = { version = "0.58", optional = true, features = [
    "Win32_System_Services",
    "Win32_Foundation",
    "Win32_Security",
] }

[target.'cfg(unix)'.dependencies]
# Unix signal handling and user detection
nix = { version = "0.29", features = ["signal", "user"] }

[profile.release]
# Optimize for size while maintaining performance
opt-level = "z"
lto = true
codegen-units = 1
strip = true


Perfect! Now I have all the information needed. Let me create a comprehensive exploration report.

## GuruConnect Agent Codebase Exploration Report

Based on my thorough exploration of the GuruConnect agent codebase, here's what I found:

### 1. **Current Agent Structure** (`C:\Users\MikeSwanson\Claude\guru-connect\agent\src\`)

**Key Modules:**
- **main.rs** - Entry point with elevation checks, support code extraction, configuration loading
- **input/keyboard.rs** - Keyboard input via SendInput API
- **input/mod.rs** - Combined input controller (mouse + keyboard)
- **startup.rs** - Registry-based persistence via `HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run`
- **session/mod.rs** - Core session manager with idle/streaming states
- **transport/websocket.rs** - WebSocket client with protobuf serialization
- **config.rs** - TOML-based configuration (server_url, api_key, agent_id, capture/encoding settings)
- **tray/mod.rs** - System tray with menu (status, end session, debug window)
- **capture/** - DXGI + GDI screen capture implementation
- **encoder/** - Raw + Zstd compression with dirty rectangle detection

### 2. **SendSAS() Implementation** (keyboard.rs, lines 130-178)

**Current Approach:**
```rust
// Tries sas.dll first (dynamic loading):
// 1. LoadLibraryW("sas.dll")
// 2. GetProcAddress("SendSAS")
// 3. Call SendSAS(0) - FALSE for Ctrl+Alt+Del
// Falls back to manual key sending (won't work without privileges)
```

**Critical Issues:**
- Requires administrator privileges to work via sas.dll
- Fallback to SendInput won't bypass Windows security restrictions
- Designed to run as the logged-in user, NOT as SYSTEM

### 3. **Startup Persistence** (startup.rs)

**Pattern Used:**
- Adds executable to `HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run`
- Runs in user context, not SYSTEM
- Handles removal and scheduled deletion via `MoveFileExW` with `MOVEFILE_DELAY_UNTIL_REBOOT`

### 4. **Cargo.toml Dependencies**

**Relevant for your service design:**
```toml
# Already included:
windows = "0.58" with features for:
  - Win32_UI_Input_KeyboardAndMouse (SendInput)
  - Win32_System_Registry (startup)
  - Win32_System_Threading (token elevation checks)
  - Win32_System_Console
  - Win32_Storage_FileSystem
windows-service = "0.7"  # Already available!
```

### 5. **IPC/Communication Patterns**

**Current:**
- WebSocket over TLS to server (remote)
- No local IPC currently implemented
- Uses protobuf for message serialization

**Proto Message Types (guruconnect.proto):**
- SessionRequest/Response
- MouseEvent / KeyEvent
- VideoFrame (with display_id)
- ChatMessage
- Heartbeat / HeartbeatAck

### 6. **Reference Implementation: GuruRMM Agent Service**

The GuruRMM agent (`C:\Users\MikeSwanson\Claude\gururmm\agent\src\service.rs`) provides excellent patterns:

**Windows Service Implementation:**
```rust
// Uses windows-service crate:
define_windows_service!(ffi_service_main, service_main);
service_dispatcher::start(SERVICE_NAME, ffi_service_main)

// Service lifecycle:
// 1. ServiceState::StartPending (with 10s wait_hint)
// 2. ServiceState::Running (accepts STOP + SHUTDOWN)
// 3. Handles ServiceControl::Stop, Interrogate, Shutdown events

// Key files:
// SERVICE_NAME = "GuruRMMAgent"
// INSTALL_DIR = r"C:\Program Files\GuruRMM"
// CONFIG_DIR = r"C:\ProgramData\GuruRMM"
```

---

## Design Recommendations for Windows Service

Based on these patterns, here's what I recommend:

### **Service Architecture:**

1. **Create `sas-service` as separate binary** (similar to how GuruConnect is structured)
   - Runs as SYSTEM
   - Runs as Windows Service (using `windows-service` crate already in Cargo.toml)
   - Minimal footprint - only handles SAS calls

2. **IPC via Named Pipes** (Windows standard for this)
   ```
   Agent (user context)
   └─> \\.\pipe\guruconnect-sas (named pipe)
       └─> SAS Service (SYSTEM context)
           └─> SendSAS() via sas.dll
   ```

3. **Service Control Flow:**
   - Agent calls `CreateFile()` on named pipe
   - Sends request: `{"action": "send_sas"}`
   - Service calls `SendSAS(0)`
   - Returns success/error

4. **Installation/Uninstallment:**
   - Agent calls: `sc.exe create GuruConnectSAS ...`
   - Agent calls: `sc.exe start GuruConnectSAS`
   - On uninstall: `sc.exe delete GuruConnectSAS`

### **Key Integration Points:**

1. **Modify keyboard.rs**: Add method to call service instead of direct sas.dll
2. **New module**: `sas_service/` - Named pipe server running as Windows Service
3. **Update startup.rs**: Install/uninstall the service alongside agent
4. **Update Cargo.toml**: Already has `windows-service` and `windows` crates needed

### **Files to Reference:**
- GuruRMM service pattern: `C:\Users\MikeSwanson\Claude\gururmm\agent\src\service.rs`
- Current keyboard: `C:\Users\MikeSwanson\Claude\guru-connect\agent\src\input\keyboard.rs`
- Current startup: `C:\Users\MikeSwanson\Claude\guru-connect\agent\src\startup.rs`

This approach maintains the clean separation of concerns while leveraging existing crate dependencies and proven Windows service patterns from the GuruRMM agent.