     1→//! IPC (Inter-Process Communication) for tray application
     2→//!
     3→//! Provides a named pipe server (Windows) or Unix socket (Unix) for the
     4→//! tray application to communicate with the agent service.
     5→
     6→use serde::{Deserialize, Serialize};
     7→use std::sync::Arc;
     8→use tokio::sync::{broadcast, RwLock};
     9→use tracing::{debug, error, info, warn};
    10→
    11→/// Pipe name for Windows named pipe
    12→#[cfg(windows)]
    13→pub const PIPE_NAME: &str = r"\\.\pipe\gururmm-agent";
    14→
    15→/// Socket path for Unix domain socket
    16→#[cfg(unix)]
    17→pub const SOCKET_PATH: &str = "/var/run/gururmm/agent.sock";
    18→
    19→/// Tray policy - controls what the tray app can show and do
    20→#[derive(Debug, Clone, Serialize, Deserialize, Default)]
    21→pub struct TrayPolicy {
    22→    /// Whether to show the tray icon at all
    23→    pub enabled: bool,
    24→
    25→    /// Show connection status in tooltip/menu
    26→    pub show_status: bool,
    27→
    28→    /// Show agent info (version, hostname, etc.) in menu
    29→    pub show_info: bool,
    30→
    31→    /// Allow user to trigger manual check-in
    32→    pub allow_force_checkin: bool,
    33→
    34→    /// Allow user to view agent logs
    35→    pub allow_view_logs: bool,
    36→
    37→    /// Allow user to open web dashboard
    38→    pub allow_open_dashboard: bool,
    39→
    40→    /// Allow user to stop the agent (dangerous!)
    41→    pub allow_stop_agent: bool,
    42→
    43→    /// Dashboard URL (if allow_open_dashboard is true)
    44→    pub dashboard_url: Option<String>,
    45→
    46→    /// Custom tooltip text
    47→    pub tooltip_text: Option<String>,
    48→
    49→    /// Custom icon (base64 encoded PNG)
    50→    pub custom_icon: Option<String>,
    51→}
    52→
    53→impl TrayPolicy {
    54→    /// Default permissive policy (for development/testing)
    55→    pub fn default_permissive() -> Self {
    56→        Self {
    57→            enabled: true,
    58→            show_status: true,
    59→            show_info: true,
    60→            allow_force_checkin: true,
    61→            allow_view_logs: true,
    62→            allow_open_dashboard: true,
    63→            allow_stop_agent: false,
    64→            dashboard_url: None,
    65→            tooltip_text: None,
    66→            custom_icon: None,
    67→        }
    68→    }
    69→
    70→    /// Restrictive policy (minimal visibility)
    71→    pub fn default_restrictive() -> Self {
    72→        Self {
    73→            enabled: true,
    74→            show_status: true,
    75→            show_info: false,
    76→            allow_force_checkin: false,
    77→            allow_view_logs: false,
    78→            allow_open_dashboard: false,
    79→            allow_stop_agent: false,
    80→            dashboard_url: None,
    81→            tooltip_text: None,
    82→            custom_icon: None,
    83→        }
    84→    }
    85→}
    86→
    87→/// Agent status for tray display
    88→#[derive(Debug, Clone, Serialize, Deserialize)]
    89→pub struct AgentStatus {
    90→    /// Whether connected to server
    91→    pub connected: bool,
    92→
    93→    /// Last successful check-in time (ISO 8601)
    94→    pub last_checkin: Option<String>,
    95→
    96→    /// Server URL we're connected to
    97→    pub server_url: String,
    98→
    99→    /// Agent version
   100→    pub agent_version: String,
   101→
   102→    /// Device ID
   103→    pub device_id: String,
   104→
   105→    /// Hostname
   106→    pub hostname: String,
   107→
   108→    /// Error message (if any)
   109→    pub error: Option<String>,
   110→}
   111→
   112→/// Request from tray to agent
   113→#[derive(Debug, Clone, Serialize, Deserialize)]
   114→#[serde(tag = "type", content = "payload")]
   115→#[serde(rename_all = "snake_case")]
   116→pub enum IpcRequest {
   117→    /// Get current agent status
   118→    GetStatus,
   119→
   120→    /// Get current tray policy
   121→    GetPolicy,
   122→
   123→    /// Force immediate check-in
   124→    ForceCheckin,
   125→
   126→    /// Stop the agent service
   127→    StopAgent,
   128→
   129→    /// Subscribe to status updates
   130→    Subscribe,
   131→
   132→    /// Unsubscribe from updates
   133→    Unsubscribe,
   134→
   135→    /// Ping (health check)
   136→    Ping,
   137→}
   138→
   139→/// Response from agent to tray
   140→#[derive(Debug, Clone, Serialize, Deserialize)]
   141→#[serde(tag = "type", content = "payload")]
   142→#[serde(rename_all = "snake_case")]
   143→pub enum IpcResponse {
   144→    /// Current agent status
   145→    Status(AgentStatus),
   146→
   147→    /// Current tray policy
   148→    Policy(TrayPolicy),
   149→
   150→    /// Success acknowledgment
   151→    Ok,
   152→
   153→    /// Pong response to ping
   154→    Pong,
   155→
   156→    /// Error
   157→    Error { message: String },
   158→
   159→    /// Action denied by policy
   160→    Denied { message: String },
   161→
   162→    /// Status update (pushed to subscribers)
   163→    StatusUpdate(AgentStatus),
   164→
   165→    /// Policy update (pushed to subscribers)
   166→    PolicyUpdate(TrayPolicy),
   167→}
   168→
   169→/// Shared state for IPC server
   170→pub struct IpcState {
   171→    /// Current agent status
   172→    pub status: RwLock<AgentStatus>,
   173→
   174→    /// Current tray policy
   175→    pub policy: RwLock<TrayPolicy>,
   176→
   177→    /// Broadcast channel for status updates
   178→    pub status_tx: broadcast::Sender<AgentStatus>,
   179→
   180→    /// Broadcast channel for policy updates
   181→    pub policy_tx: broadcast::Sender<TrayPolicy>,
   182→
   183→    /// Channel to request force check-in
   184→    pub force_checkin_tx: tokio::sync::mpsc::Sender<()>,
   185→
   186→    /// Channel to request agent stop
   187→    pub stop_agent_tx: tokio::sync::mpsc::Sender<()>,
   188→}
   189→
   190→impl IpcState {
   191→    pub fn new(
   192→        initial_status: AgentStatus,
   193→        initial_policy: TrayPolicy,
   194→        force_checkin_tx: tokio::sync::mpsc::Sender<()>,
   195→        stop_agent_tx: tokio::sync::mpsc::Sender<()>,
   196→    ) -> Self {
   197→        let (status_tx, _) = broadcast::channel(16);
   198→        let (policy_tx, _) = broadcast::channel(16);
   199→
   200→        Self {
   201→            status: RwLock::new(initial_status),
   202→            policy: RwLock::new(initial_policy),
   203→            status_tx,
   204→            policy_tx,
   205→            force_checkin_tx,
   206→            stop_agent_tx,
   207→        }
   208→    }
   209→
   210→    /// Update status and broadcast to subscribers
   211→    pub async fn update_status(&self, status: AgentStatus) {
   212→        *self.status.write().await = status.clone();
   213→        let _ = self.status_tx.send(status);
   214→    }
   215→
   216→    /// Update policy and broadcast to subscribers
   217→    pub async fn update_policy(&self, policy: TrayPolicy) {
   218→        *self.policy.write().await = policy.clone();
   219→        let _ = self.policy_tx.send(policy);
   220→    }
   221→
   222→    /// Handle an IPC request
   223→    pub async fn handle_request(&self, request: IpcRequest) -> IpcResponse {
   224→        match request {
   225→            IpcRequest::GetStatus => {
   226→                let status = self.status.read().await.clone();
   227→                IpcResponse::Status(status)
   228→            }
   229→
   230→            IpcRequest::GetPolicy => {
   231→                let policy = self.policy.read().await.clone();
   232→                IpcResponse::Policy(policy)
   233→            }
   234→
   235→            IpcRequest::ForceCheckin => {
   236→                let policy = self.policy.read().await;
   237→                if !policy.allow_force_checkin {
   238→                    return IpcResponse::Denied {
   239→                        message: "Force check-in not allowed by policy".to_string(),
   240→                    };
   241→                }
   242→                drop(policy);
   243→
   244→                match self.force_checkin_tx.send(()).await {
   245→                    Ok(_) => IpcResponse::Ok,
   246→                    Err(_) => IpcResponse::Error {
   247→                        message: "Failed to trigger check-in".to_string(),
   248→                    },
   249→                }
   250→            }
   251→
   252→            IpcRequest::StopAgent => {
   253→                let policy = self.policy.read().await;
   254→                if !policy.allow_stop_agent {
   255→                    return IpcResponse::Denied {
   256→                        message: "Stopping agent not allowed by policy".to_string(),
   257→                    };
   258→                }
   259→                drop(policy);
   260→
   261→                match self.stop_agent_tx.send(()).await {
   262→                    Ok(_) => IpcResponse::Ok,
   263→                    Err(_) => IpcResponse::Error {
   264→                        message: "Failed to request agent stop".to_string(),
   265→                    },
   266→                }
   267→            }
   268→
   269→            IpcRequest::Subscribe => {
   270→                // Subscription is handled at the connection level
   271→                IpcResponse::Ok
   272→            }
   273→
   274→            IpcRequest::Unsubscribe => {
   275→                IpcResponse::Ok
   276→            }
   277→
   278→            IpcRequest::Ping => IpcResponse::Pong,
   279→        }
   280→    }
   281→}
   282→
   283→// ============================================================================
   284→// Windows Named Pipe Server
   285→// ============================================================================
   286→
   287→#[cfg(windows)]
   288→pub mod server {
   289→    use super::*;
   290→    use std::ffi::OsStr;
   291→    use std::os::windows::ffi::OsStrExt;
   292→    use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
   293→    use tokio::net::windows::named_pipe::{ServerOptions, NamedPipeServer};
   294→
   295→    /// Run the IPC server (Windows named pipe)
   296→    pub async fn run_ipc_server(state: Arc<IpcState>) -> anyhow::Result<()> {
   297→        info!("Starting IPC server on {}", super::PIPE_NAME);
   298→
   299→        loop {
   300→            // Create a new pipe instance
   301→            let server = match ServerOptions::new()
   302→                .first_pipe_instance(false)
   303→                .create(super::PIPE_NAME)
   304→            {
   305→                Ok(s) => s,
   306→                Err(e) => {
   307→                    // First instance needs different handling
   308→                    if e.kind() == std::io::ErrorKind::NotFound {
   309→                        ServerOptions::new()
   310→                            .first_pipe_instance(true)
   311→                            .create(super::PIPE_NAME)?
   312→                    } else {
   313→                        error!("Failed to create pipe: {}", e);
   314→                        tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
   315→                        continue;
   316→                    }
   317→                }
   318→            };
   319→
   320→            // Wait for a client to connect
   321→            if let Err(e) = server.connect().await {
   322→                error!("Failed to accept pipe connection: {}", e);
   323→                continue;
   324→            }
   325→
   326→            info!("IPC client connected");
   327→
   328→            // Spawn a task to handle this client
   329→            let state = Arc::clone(&state);
   330→            tokio::spawn(async move {
   331→                if let Err(e) = handle_client(server, state).await {
   332→                    debug!("IPC client disconnected: {}", e);
   333→                }
   334→            });
   335→        }
   336→    }
   337→
   338→    async fn handle_client(
   339→        pipe: NamedPipeServer,
   340→        state: Arc<IpcState>,
   341→    ) -> anyhow::Result<()> {
   342→        let (reader, mut writer) = tokio::io::split(pipe);
   343→        let mut reader = BufReader::new(reader);
   344→        let mut line = String::new();
   345→
   346→        // Subscribe to updates
   347→        let mut status_rx = state.status_tx.subscribe();
   348→        let mut policy_rx = state.policy_tx.subscribe();
   349→        let mut subscribed = false;
   350→
   351→        loop {
   352→            tokio::select! {
   353→                // Read request from client
   354→                result = reader.read_line(&mut line) => {
   355→                    match result {
   356→                        Ok(0) => break, // EOF
   357→                        Ok(_) => {
   358→                            let trimmed = line.trim();
   359→                            if trimmed.is_empty() {
   360→                                line.clear();
   361→                                continue;
   362→                            }
   363→
   364→                            match serde_json::from_str::<IpcRequest>(trimmed) {
   365→                                Ok(request) => {
   366→                                    if matches!(request, IpcRequest::Subscribe) {
   367→                                        subscribed = true;
   368→                                    } else if matches!(request, IpcRequest::Unsubscribe) {
   369→                                        subscribed = false;
   370→                                    }
   371→
   372→                                    let response = state.handle_request(request).await;
   373→                                    let response_json = serde_json::to_string(&response)?;
   374→                                    writer.write_all(response_json.as_bytes()).await?;
   375→                                    writer.write_all(b"\n").await?;
   376→                                    writer.flush().await?;
   377→                                }
   378→                                Err(e) => {
   379→                                    warn!("Invalid IPC request: {}", e);
   380→                                    let response = IpcResponse::Error {
   381→                                        message: format!("Invalid request: {}", e),
   382→                                    };
   383→                                    let response_json = serde_json::to_string(&response)?;
   384→                                    writer.write_all(response_json.as_bytes()).await?;
   385→                                    writer.write_all(b"\n").await?;
   386→                                    writer.flush().await?;
   387→                                }
   388→                            }
   389→                            line.clear();
   390→                        }
   391→                        Err(e) => {
   392→                            return Err(e.into());
   393→                        }
   394→                    }
   395→                }
   396→
   397→                // Push status updates to subscribed clients
   398→                result = status_rx.recv(), if subscribed => {
   399→                    if let Ok(status) = result {
   400→                        let response = IpcResponse::StatusUpdate(status);
   401→                        let response_json = serde_json::to_string(&response)?;
   402→                        writer.write_all(response_json.as_bytes()).await?;
   403→                        writer.write_all(b"\n").await?;
   404→                        writer.flush().await?;
   405→                    }
   406→                }
   407→
   408→                // Push policy updates to subscribed clients
   409→                result = policy_rx.recv(), if subscribed => {
   410→                    if let Ok(policy) = result {
   411→                        let response = IpcResponse::PolicyUpdate(policy);
   412→                        let response_json = serde_json::to_string(&response)?;
   413→                        writer.write_all(response_json.as_bytes()).await?;
   414→                        writer.write_all(b"\n").await?;
   415→                        writer.flush().await?;
   416→                    }
   417→                }
   418→            }
   419→        }
   420→
   421→        Ok(())
   422→    }
   423→}
   424→
   425→// ============================================================================
   426→// Unix Domain Socket Server (placeholder for future)
   427→// ============================================================================
   428→
   429→#[cfg(unix)]
   430→pub mod server {
   431→    use super::*;
   432→
   433→    /// Run the IPC server (Unix domain socket)
   434→    pub async fn run_ipc_server(_state: Arc<IpcState>) -> anyhow::Result<()> {
   435→        // TODO: Implement Unix socket server for macOS/Linux
   436→        info!("Unix IPC server not yet implemented");
   437→
   438→        // For now, just sleep forever
   439→        loop {
   440→            tokio::time::sleep(tokio::time::Duration::from_secs(3600)).await;
   441→        }
   442→    }
   443→}
   444→
   445→#[cfg(test)]
   446→mod tests {
   447→    use super::*;
   448→
   449→    #[test]
   450→    fn test_tray_policy_serialization() {
   451→        let policy = TrayPolicy::default_permissive();
   452→        let json = serde_json::to_string(&policy).unwrap();
   453→        let parsed: TrayPolicy = serde_json::from_str(&json).unwrap();
   454→        assert_eq!(parsed.enabled, policy.enabled);
   455→        assert_eq!(parsed.allow_stop_agent, policy.allow_stop_agent);
   456→    }
   457→
   458→    #[test]
   459→    fn test_ipc_request_serialization() {
   460→        let request = IpcRequest::GetStatus;
   461→        let json = serde_json::to_string(&request).unwrap();
   462→        assert!(json.contains("get_status"));
   463→
   464→        let parsed: IpcRequest = serde_json::from_str(&json).unwrap();
   465→        assert!(matches!(parsed, IpcRequest::GetStatus));
   466→    }
   467→
   468→    #[test]
   469→    fn test_ipc_response_serialization() {
   470→        let response = IpcResponse::Denied {
   471→            message: "Not allowed".to_string(),
   472→        };
   473→        let json = serde_json::to_string(&response).unwrap();
   474→        assert!(json.contains("denied"));
   475→
   476→        let parsed: IpcResponse = serde_json::from_str(&json).unwrap();
   477→        assert!(matches!(parsed, IpcResponse::Denied { .. }));
   478→    }
   479→}
   480→

<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>

//! IPC (Inter-Process Communication) for tray application
//!
//! Provides a named pipe server (Windows) or Unix socket (Unix) for the
//! tray application to communicate with the agent service.

use serde::{Deserialize, Serialize};
use std::sync::Arc;
use tokio::sync::{broadcast, RwLock};
use tracing::{debug, error, info, warn};

/// Pipe name for Windows named pipe
#[cfg(windows)]
pub const PIPE_NAME: &str = r"\\.\pipe\gururmm-agent";

/// Socket path for Unix domain socket
#[cfg(unix)]
pub const SOCKET_PATH: &str = "/var/run/gururmm/agent.sock";

/// Tray policy - controls what the tray app can show and do
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct TrayPolicy {
    /// Whether to show the tray icon at all
    pub enabled: bool,

    /// Show connection status in tooltip/menu
    pub show_status: bool,

    /// Show agent info (version, hostname, etc.) in menu
    pub show_info: bool,

    /// Allow user to trigger manual check-in
    pub allow_force_checkin: bool,

    /// Allow user to view agent logs
    pub allow_view_logs: bool,

    /// Allow user to open web dashboard
    pub allow_open_dashboard: bool,

    /// Allow user to stop the agent (dangerous!)
    pub allow_stop_agent: bool,

    /// Dashboard URL (if allow_open_dashboard is true)
    pub dashboard_url: Option<String>,

    /// Custom tooltip text
    pub tooltip_text: Option<String>,

    /// Custom icon (base64 encoded PNG)
    pub custom_icon: Option<String>,
}

impl TrayPolicy {
    /// Default permissive policy (for development/testing)
    pub fn default_permissive() -> Self {
        Self {
            enabled: true,
            show_status: true,
            show_info: true,
            allow_force_checkin: true,
            allow_view_logs: true,
            allow_open_dashboard: true,
            allow_stop_agent: false,
            dashboard_url: None,
            tooltip_text: None,
            custom_icon: None,
        }
    }

    /// Restrictive policy (minimal visibility)
    pub fn default_restrictive() -> Self {
        Self {
            enabled: true,
            show_status: true,
            show_info: false,
            allow_force_checkin: false,
            allow_view_logs: false,
            allow_open_dashboard: false,
            allow_stop_agent: false,
            dashboard_url: None,
            tooltip_text: None,
            custom_icon: None,
        }
    }
}

/// Agent status for tray display
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentStatus {
    /// Whether connected to server
    pub connected: bool,

    /// Last successful check-in time (ISO 8601)
    pub last_checkin: Option<String>,

    /// Server URL we're connected to
    pub server_url: String,

    /// Agent version
    pub agent_version: String,

    /// Device ID
    pub device_id: String,

    /// Hostname
    pub hostname: String,

    /// Error message (if any)
    pub error: Option<String>,
}

/// Request from tray to agent
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", content = "payload")]
#[serde(rename_all = "snake_case")]
pub enum IpcRequest {
    /// Get current agent status
    GetStatus,

    /// Get current tray policy
    GetPolicy,

    /// Force immediate check-in
    ForceCheckin,

    /// Stop the agent service
    StopAgent,

    /// Subscribe to status updates
    Subscribe,

    /// Unsubscribe from updates
    Unsubscribe,

    /// Ping (health check)
    Ping,
}

/// Response from agent to tray
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", content = "payload")]
#[serde(rename_all = "snake_case")]
pub enum IpcResponse {
    /// Current agent status
    Status(AgentStatus),

    /// Current tray policy
    Policy(TrayPolicy),

    /// Success acknowledgment
    Ok,

    /// Pong response to ping
    Pong,

    /// Error
    Error { message: String },

    /// Action denied by policy
    Denied { message: String },

    /// Status update (pushed to subscribers)
    StatusUpdate(AgentStatus),

    /// Policy update (pushed to subscribers)
    PolicyUpdate(TrayPolicy),
}

/// Shared state for IPC server
pub struct IpcState {
    /// Current agent status
    pub status: RwLock<AgentStatus>,

    /// Current tray policy
    pub policy: RwLock<TrayPolicy>,

    /// Broadcast channel for status updates
    pub status_tx: broadcast::Sender<AgentStatus>,

    /// Broadcast channel for policy updates
    pub policy_tx: broadcast::Sender<TrayPolicy>,

    /// Channel to request force check-in
    pub force_checkin_tx: tokio::sync::mpsc::Sender<()>,

    /// Channel to request agent stop
    pub stop_agent_tx: tokio::sync::mpsc::Sender<()>,
}

impl IpcState {
    pub fn new(
        initial_status: AgentStatus,
        initial_policy: TrayPolicy,
        force_checkin_tx: tokio::sync::mpsc::Sender<()>,
        stop_agent_tx: tokio::sync::mpsc::Sender<()>,
    ) -> Self {
        let (status_tx, _) = broadcast::channel(16);
        let (policy_tx, _) = broadcast::channel(16);

        Self {
            status: RwLock::new(initial_status),
            policy: RwLock::new(initial_policy),
            status_tx,
            policy_tx,
            force_checkin_tx,
            stop_agent_tx,
        }
    }

    /// Update status and broadcast to subscribers
    pub async fn update_status(&self, status: AgentStatus) {
        *self.status.write().await = status.clone();
        let _ = self.status_tx.send(status);
    }

    /// Update policy and broadcast to subscribers
    pub async fn update_policy(&self, policy: TrayPolicy) {
        *self.policy.write().await = policy.clone();
        let _ = self.policy_tx.send(policy);
    }

    /// Handle an IPC request
    pub async fn handle_request(&self, request: IpcRequest) -> IpcResponse {
        match request {
            IpcRequest::GetStatus => {
                let status = self.status.read().await.clone();
                IpcResponse::Status(status)
            }

            IpcRequest::GetPolicy => {
                let policy = self.policy.read().await.clone();
                IpcResponse::Policy(policy)
            }

            IpcRequest::ForceCheckin => {
                let policy = self.policy.read().await;
                if !policy.allow_force_checkin {
                    return IpcResponse::Denied {
                        message: "Force check-in not allowed by policy".to_string(),
                    };
                }
                drop(policy);

                match self.force_checkin_tx.send(()).await {
                    Ok(_) => IpcResponse::Ok,
                    Err(_) => IpcResponse::Error {
                        message: "Failed to trigger check-in".to_string(),
                    },
                }
            }

            IpcRequest::StopAgent => {
                let policy = self.policy.read().await;
                if !policy.allow_stop_agent {
                    return IpcResponse::Denied {
                        message: "Stopping agent not allowed by policy".to_string(),
                    };
                }
                drop(policy);

                match self.stop_agent_tx.send(()).await {
                    Ok(_) => IpcResponse::Ok,
                    Err(_) => IpcResponse::Error {
                        message: "Failed to request agent stop".to_string(),
                    },
                }
            }

            IpcRequest::Subscribe => {
                // Subscription is handled at the connection level
                IpcResponse::Ok
            }

            IpcRequest::Unsubscribe => {
                IpcResponse::Ok
            }

            IpcRequest::Ping => IpcResponse::Pong,
        }
    }
}

// ============================================================================
// Windows Named Pipe Server
// ============================================================================

#[cfg(windows)]
pub mod server {
    use super::*;
    use std::ffi::OsStr;
    use std::os::windows::ffi::OsStrExt;
    use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
    use tokio::net::windows::named_pipe::{ServerOptions, NamedPipeServer};

    /// Run the IPC server (Windows named pipe)
    pub async fn run_ipc_server(state: Arc<IpcState>) -> anyhow::Result<()> {
        info!("Starting IPC server on {}", super::PIPE_NAME);

        loop {
            // Create a new pipe instance
            let server = match ServerOptions::new()
                .first_pipe_instance(false)
                .create(super::PIPE_NAME)
            {
                Ok(s) => s,
                Err(e) => {
                    // First instance needs different handling
                    if e.kind() == std::io::ErrorKind::NotFound {
                        ServerOptions::new()
                            .first_pipe_instance(true)
                            .create(super::PIPE_NAME)?
                    } else {
                        error!("Failed to create pipe: {}", e);
                        tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
                        continue;
                    }
                }
            };

            // Wait for a client to connect
            if let Err(e) = server.connect().await {
                error!("Failed to accept pipe connection: {}", e);
                continue;
            }

            info!("IPC client connected");

            // Spawn a task to handle this client
            let state = Arc::clone(&state);
            tokio::spawn(async move {
                if let Err(e) = handle_client(server, state).await {
                    debug!("IPC client disconnected: {}", e);
                }
            });
        }
    }

    async fn handle_client(
        pipe: NamedPipeServer,
        state: Arc<IpcState>,
    ) -> anyhow::Result<()> {
        let (reader, mut writer) = tokio::io::split(pipe);
        let mut reader = BufReader::new(reader);
        let mut line = String::new();

        // Subscribe to updates
        let mut status_rx = state.status_tx.subscribe();
        let mut policy_rx = state.policy_tx.subscribe();
        let mut subscribed = false;

        loop {
            tokio::select! {
                // Read request from client
                result = reader.read_line(&mut line) => {
                    match result {
                        Ok(0) => break, // EOF
                        Ok(_) => {
                            let trimmed = line.trim();
                            if trimmed.is_empty() {
                                line.clear();
                                continue;
                            }

                            match serde_json::from_str::<IpcRequest>(trimmed) {
                                Ok(request) => {
                                    if matches!(request, IpcRequest::Subscribe) {
                                        subscribed = true;
                                    } else if matches!(request, IpcRequest::Unsubscribe) {
                                        subscribed = false;
                                    }

                                    let response = state.handle_request(request).await;
                                    let response_json = serde_json::to_string(&response)?;
                                    writer.write_all(response_json.as_bytes()).await?;
                                    writer.write_all(b"\n").await?;
                                    writer.flush().await?;
                                }
                                Err(e) => {
                                    warn!("Invalid IPC request: {}", e);
                                    let response = IpcResponse::Error {
                                        message: format!("Invalid request: {}", e),
                                    };
                                    let response_json = serde_json::to_string(&response)?;
                                    writer.write_all(response_json.as_bytes()).await?;
                                    writer.write_all(b"\n").await?;
                                    writer.flush().await?;
                                }
                            }
                            line.clear();
                        }
                        Err(e) => {
                            return Err(e.into());
                        }
                    }
                }

                // Push status updates to subscribed clients
                result = status_rx.recv(), if subscribed => {
                    if let Ok(status) = result {
                        let response = IpcResponse::StatusUpdate(status);
                        let response_json = serde_json::to_string(&response)?;
                        writer.write_all(response_json.as_bytes()).await?;
                        writer.write_all(b"\n").await?;
                        writer.flush().await?;
                    }
                }

                // Push policy updates to subscribed clients
                result = policy_rx.recv(), if subscribed => {
                    if let Ok(policy) = result {
                        let response = IpcResponse::PolicyUpdate(policy);
                        let response_json = serde_json::to_string(&response)?;
                        writer.write_all(response_json.as_bytes()).await?;
                        writer.write_all(b"\n").await?;
                        writer.flush().await?;
                    }
                }
            }
        }

        Ok(())
    }
}

// ============================================================================
// Unix Domain Socket Server (placeholder for future)
// ============================================================================

#[cfg(unix)]
pub mod server {
    use super::*;

    /// Run the IPC server (Unix domain socket)
    pub async fn run_ipc_server(_state: Arc<IpcState>) -> anyhow::Result<()> {
        // TODO: Implement Unix socket server for macOS/Linux
        info!("Unix IPC server not yet implemented");

        // For now, just sleep forever
        loop {
            tokio::time::sleep(tokio::time::Duration::from_secs(3600)).await;
        }
    }
}

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

    #[test]
    fn test_tray_policy_serialization() {
        let policy = TrayPolicy::default_permissive();
        let json = serde_json::to_string(&policy).unwrap();
        let parsed: TrayPolicy = serde_json::from_str(&json).unwrap();
        assert_eq!(parsed.enabled, policy.enabled);
        assert_eq!(parsed.allow_stop_agent, policy.allow_stop_agent);
    }

    #[test]
    fn test_ipc_request_serialization() {
        let request = IpcRequest::GetStatus;
        let json = serde_json::to_string(&request).unwrap();
        assert!(json.contains("get_status"));

        let parsed: IpcRequest = serde_json::from_str(&json).unwrap();
        assert!(matches!(parsed, IpcRequest::GetStatus));
    }

    #[test]
    fn test_ipc_response_serialization() {
        let response = IpcResponse::Denied {
            message: "Not allowed".to_string(),
        };
        let json = serde_json::to_string(&response).unwrap();
        assert!(json.contains("denied"));

        let parsed: IpcResponse = serde_json::from_str(&json).unwrap();
        assert!(matches!(parsed, IpcResponse::Denied { .. }));
    }
}


     1→//! GuruRMM Tray Application
     2→//!
     3→//! System tray interface for the GuruRMM agent.
     4→//! Communicates with the agent service via IPC (named pipe on Windows).
     5→
     6→// Hide console window on Windows release builds
     7→#![cfg_attr(
     8→    all(target_os = "windows", not(debug_assertions)),
     9→    windows_subsystem = "windows"
    10→)]
    11→
    12→mod ipc;
    13→mod menu;
    14→mod tray;
    15→
    16→use anyhow::Result;
    17→use tracing::{error, info};
    18→use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
    19→
    20→use crate::ipc::IpcClient;
    21→use crate::tray::TrayApp;
    22→
    23→fn main() -> Result<()> {
    24→    // Initialize logging
    25→    tracing_subscriber::registry()
    26→        .with(
    27→            tracing_subscriber::EnvFilter::try_from_default_env()
    28→                .unwrap_or_else(|_| "gururmm_tray=info".into()),
    29→        )
    30→        .with(tracing_subscriber::fmt::layer())
    31→        .init();
    32→
    33→    info!("GuruRMM Tray starting...");
    34→
    35→    // Run the tray application
    36→    if let Err(e) = run_tray() {
    37→        error!("Tray application error: {}", e);
    38→        return Err(e);
    39→    }
    40→
    41→    info!("GuruRMM Tray exiting");
    42→    Ok(())
    43→}
    44→
    45→fn run_tray() -> Result<()> {
    46→    // Create and run the tray application
    47→    let app = TrayApp::new()?;
    48→    app.run()
    49→}
    50→

<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>

//! GuruRMM Tray Application
//!
//! System tray interface for the GuruRMM agent.
//! Communicates with the agent service via IPC (named pipe on Windows).

// Hide console window on Windows release builds
#![cfg_attr(
    all(target_os = "windows", not(debug_assertions)),
    windows_subsystem = "windows"
)]

mod ipc;
mod menu;
mod tray;

use anyhow::Result;
use tracing::{error, info};
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};

use crate::ipc::IpcClient;
use crate::tray::TrayApp;

fn main() -> Result<()> {
    // Initialize logging
    tracing_subscriber::registry()
        .with(
            tracing_subscriber::EnvFilter::try_from_default_env()
                .unwrap_or_else(|_| "gururmm_tray=info".into()),
        )
        .with(tracing_subscriber::fmt::layer())
        .init();

    info!("GuruRMM Tray starting...");

    // Run the tray application
    if let Err(e) = run_tray() {
        error!("Tray application error: {}", e);
        return Err(e);
    }

    info!("GuruRMM Tray exiting");
    Ok(())
}

fn run_tray() -> Result<()> {
    // Create and run the tray application
    let app = TrayApp::new()?;
    app.run()
}


Let me also read the tray.rs and menu.rs files to get the complete picture of how the tray app works.


     1→//! Main tray application logic
     2→
     3→use anyhow::{Context, Result};
     4→use std::sync::Arc;
     5→use tokio::sync::{mpsc, RwLock};
     6→use tray_icon::{
     7→    menu::MenuEvent,
     8→    Icon, TrayIcon, TrayIconBuilder,
     9→};
    10→use tracing::{debug, error, info, warn};
    11→use winit::event_loop::{ControlFlow, EventLoop};
    12→
    13→use crate::ipc::{AgentStatus, ConnectionState, IpcClient, IpcRequest, TrayPolicy};
    14→use crate::menu::{self, MenuAction};
    15→
    16→/// Icon state
    17→#[derive(Debug, Clone, Copy, PartialEq, Eq)]
    18→pub enum IconState {
    19→    Connected,
    20→    Reconnecting,
    21→    Error,
    22→    Disabled,
    23→}
    24→
    25→/// Embedded icon data (will be replaced with actual icons)
    26→mod icons {
    27→    // Placeholder: 16x16 RGBA icons
    28→    // In production, these would be loaded from the assets folder
    29→
    30→    pub fn connected() -> Vec<u8> {
    31→        // Green circle placeholder (16x16 RGBA)
    32→        create_circle_icon(0x22, 0xc5, 0x5e, 0xff)
    33→    }
    34→
    35→    pub fn reconnecting() -> Vec<u8> {
    36→        // Yellow circle placeholder
    37→        create_circle_icon(0xea, 0xb3, 0x08, 0xff)
    38→    }
    39→
    40→    pub fn error() -> Vec<u8> {
    41→        // Red circle placeholder
    42→        create_circle_icon(0xef, 0x44, 0x44, 0xff)
    43→    }
    44→
    45→    pub fn disabled() -> Vec<u8> {
    46→        // Gray circle placeholder
    47→        create_circle_icon(0x6b, 0x72, 0x80, 0xff)
    48→    }
    49→
    50→    fn create_circle_icon(r: u8, g: u8, b: u8, a: u8) -> Vec<u8> {
    51→        let size = 32;
    52→        let mut data = vec![0u8; size * size * 4];
    53→        let center = size as f32 / 2.0;
    54→        let radius = center - 2.0;
    55→
    56→        for y in 0..size {
    57→            for x in 0..size {
    58→                let dx = x as f32 - center;
    59→                let dy = y as f32 - center;
    60→                let dist = (dx * dx + dy * dy).sqrt();
    61→
    62→                let idx = (y * size + x) * 4;
    63→                if dist <= radius {
    64→                    data[idx] = r;
    65→                    data[idx + 1] = g;
    66→                    data[idx + 2] = b;
    67→                    data[idx + 3] = a;
    68→                } else if dist <= radius + 1.0 {
    69→                    // Anti-aliased edge
    70→                    let alpha = ((radius + 1.0 - dist) * a as f32) as u8;
    71→                    data[idx] = r;
    72→                    data[idx + 1] = g;
    73→                    data[idx + 2] = b;
    74→                    data[idx + 3] = alpha;
    75→                }
    76→            }
    77→        }
    78→
    79→        data
    80→    }
    81→}
    82→
    83→/// Main tray application
    84→pub struct TrayApp {
    85→    /// Tokio runtime for async operations
    86→    runtime: tokio::runtime::Runtime,
    87→
    88→    /// IPC client state
    89→    connection_state: Arc<RwLock<ConnectionState>>,
    90→    status: Arc<RwLock<AgentStatus>>,
    91→    policy: Arc<RwLock<TrayPolicy>>,
    92→
    93→    /// Request sender
    94→    request_tx: mpsc::Sender<IpcRequest>,
    95→}
    96→
    97→impl TrayApp {
    98→    /// Create a new tray application
    99→    pub fn new() -> Result<Self> {
   100→        let runtime = tokio::runtime::Builder::new_multi_thread()
   101→            .enable_all()
   102→            .build()
   103→            .context("Failed to create tokio runtime")?;
   104→
   105→        let connection_state = Arc::new(RwLock::new(ConnectionState::Disconnected));
   106→        let status = Arc::new(RwLock::new(AgentStatus::default()));
   107→        let policy = Arc::new(RwLock::new(TrayPolicy::default_permissive()));
   108→
   109→        let (request_tx, request_rx) = mpsc::channel(32);
   110→        let (update_tx, _update_rx) = mpsc::channel(32);
   111→
   112→        // Spawn IPC connection task
   113→        let conn_state = Arc::clone(&connection_state);
   114→        let conn_status = Arc::clone(&status);
   115→        let conn_policy = Arc::clone(&policy);
   116→
   117→        runtime.spawn(async move {
   118→            crate::ipc::connection::run_connection(
   119→                conn_state,
   120→                conn_status,
   121→                conn_policy,
   122→                request_rx,
   123→                update_tx,
   124→            )
   125→            .await;
   126→        });
   127→
   128→        Ok(Self {
   129→            runtime,
   130→            connection_state,
   131→            status,
   132→            policy,
   133→            request_tx,
   134→        })
   135→    }
   136→
   137→    /// Run the tray application (blocking)
   138→    pub fn run(self) -> Result<()> {
   139→        let event_loop = EventLoop::new().context("Failed to create event loop")?;
   140→
   141→        // Create initial icon
   142→        let icon = create_icon(IconState::Reconnecting)?;
   143→
   144→        // Get initial status and policy for menu
   145→        let (status, policy) = self.runtime.block_on(async {
   146→            let s = self.status.read().await.clone();
   147→            let p = self.policy.read().await.clone();
   148→            (s, p)
   149→        });
   150→
   151→        // Build initial menu
   152→        let menu = menu::build_menu(&status, &policy);
   153→
   154→        // Create tray icon
   155→        let tooltip = policy
   156→            .tooltip_text
   157→            .clone()
   158→            .unwrap_or_else(|| "GuruRMM Agent".to_string());
   159→
   160→        let tray_icon = TrayIconBuilder::new()
   161→            .with_menu(Box::new(menu))
   162→            .with_tooltip(&tooltip)
   163→            .with_icon(icon)
   164→            .build()
   165→            .context("Failed to create tray icon")?;
   166→
   167→        info!("Tray icon created");
   168→
   169→        // Menu event receiver
   170→        let menu_channel = MenuEvent::receiver();
   171→
   172→        // Track last known state for icon updates
   173→        let mut last_icon_state = IconState::Reconnecting;
   174→        let mut last_connected = false;
   175→
   176→        // Run event loop
   177→        event_loop.run(move |_event, event_loop| {
   178→            event_loop.set_control_flow(ControlFlow::Wait);
   179→
   180→            // Check for menu events (non-blocking)
   181→            if let Ok(event) = menu_channel.try_recv() {
   182→                let action = menu::handle_menu_event(event);
   183→                debug!("Menu action: {:?}", action);
   184→
   185→                match action {
   186→                    MenuAction::ForceCheckin => {
   187→                        let tx = self.request_tx.clone();
   188→                        self.runtime.spawn(async move {
   189→                            if let Err(e) = tx.send(IpcRequest::ForceCheckin).await {
   190→                                error!("Failed to send force checkin: {}", e);
   191→                            }
   192→                        });
   193→                    }
   194→                    MenuAction::ViewLogs => {
   195→                        // Open log file location
   196→                        #[cfg(windows)]
   197→                        {
   198→                            let _ = std::process::Command::new("explorer")
   199→                                .arg(r"C:\ProgramData\GuruRMM\logs")
   200→                                .spawn();
   201→                        }
   202→                    }
   203→                    MenuAction::OpenDashboard => {
   204→                        let policy = self.runtime.block_on(async {
   205→                            self.policy.read().await.clone()
   206→                        });
   207→                        if let Some(url) = policy.dashboard_url {
   208→                            let _ = open::that(&url);
   209→                        }
   210→                    }
   211→                    MenuAction::StopAgent => {
   212→                        // Show confirmation dialog before stopping
   213→                        // For now, just send the request
   214→                        let tx = self.request_tx.clone();
   215→                        self.runtime.spawn(async move {
   216→                            if let Err(e) = tx.send(IpcRequest::StopAgent).await {
   217→                                error!("Failed to send stop agent: {}", e);
   218→                            }
   219→                        });
   220→                    }
   221→                    MenuAction::ExitTray => {
   222→                        info!("Exit requested");
   223→                        event_loop.exit();
   224→                    }
   225→                    MenuAction::Unknown(id) => {
   226→                        warn!("Unknown menu action: {}", id);
   227→                    }
   228→                }
   229→            }
   230→
   231→            // Periodically update icon and menu based on state
   232→            let (conn_state, status, policy) = self.runtime.block_on(async {
   233→                let c = *self.connection_state.read().await;
   234→                let s = self.status.read().await.clone();
   235→                let p = self.policy.read().await.clone();
   236→                (c, s, p)
   237→            });
   238→
   239→            // Determine icon state
   240→            let icon_state = match conn_state {
   241→                ConnectionState::Connected if status.connected => IconState::Connected,
   242→                ConnectionState::Connected => IconState::Error,
   243→                ConnectionState::Connecting => IconState::Reconnecting,
   244→                ConnectionState::Disconnected => IconState::Disabled,
   245→            };
   246→
   247→            // Update icon if state changed
   248→            if icon_state != last_icon_state {
   249→                last_icon_state = icon_state;
   250→                if let Ok(new_icon) = create_icon(icon_state) {
   251→                    if let Err(e) = tray_icon.set_icon(Some(new_icon)) {
   252→                        warn!("Failed to update icon: {}", e);
   253→                    }
   254→                }
   255→
   256→                // Update tooltip
   257→                let tooltip = match icon_state {
   258→                    IconState::Connected => format!("GuruRMM - Connected to {}",
   259→                        status.server_url.split('/').nth(2).unwrap_or(&status.server_url)),
   260→                    IconState::Reconnecting => "GuruRMM - Reconnecting...".to_string(),
   261→                    IconState::Error => format!("GuruRMM - Error: {}",
   262→                        status.error.as_deref().unwrap_or("Unknown")),
   263→                    IconState::Disabled => "GuruRMM - Disabled".to_string(),
   264→                };
   265→                let _ = tray_icon.set_tooltip(Some(&tooltip));
   266→            }
   267→
   268→            // Rebuild menu if connection state changed
   269→            if status.connected != last_connected {
   270→                last_connected = status.connected;
   271→                let new_menu = menu::build_menu(&status, &policy);
   272→                tray_icon.set_menu(Some(Box::new(new_menu)));
   273→            }
   274→        })?;
   275→
   276→        Ok(())
   277→    }
   278→}
   279→
   280→/// Create an icon for the given state
   281→fn create_icon(state: IconState) -> Result<Icon> {
   282→    let data = match state {
   283→        IconState::Connected => icons::connected(),
   284→        IconState::Reconnecting => icons::reconnecting(),
   285→        IconState::Error => icons::error(),
   286→        IconState::Disabled => icons::disabled(),
   287→    };
   288→
   289→    Icon::from_rgba(data, 32, 32).context("Failed to create icon")
   290→}
   291→

<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>

//! Main tray application logic

use anyhow::{Context, Result};
use std::sync::Arc;
use tokio::sync::{mpsc, RwLock};
use tray_icon::{
    menu::MenuEvent,
    Icon, TrayIcon, TrayIconBuilder,
};
use tracing::{debug, error, info, warn};
use winit::event_loop::{ControlFlow, EventLoop};

use crate::ipc::{AgentStatus, ConnectionState, IpcClient, IpcRequest, TrayPolicy};
use crate::menu::{self, MenuAction};

/// Icon state
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IconState {
    Connected,
    Reconnecting,
    Error,
    Disabled,
}

/// Embedded icon data (will be replaced with actual icons)
mod icons {
    // Placeholder: 16x16 RGBA icons
    // In production, these would be loaded from the assets folder

    pub fn connected() -> Vec<u8> {
        // Green circle placeholder (16x16 RGBA)
        create_circle_icon(0x22, 0xc5, 0x5e, 0xff)
    }

    pub fn reconnecting() -> Vec<u8> {
        // Yellow circle placeholder
        create_circle_icon(0xea, 0xb3, 0x08, 0xff)
    }

    pub fn error() -> Vec<u8> {
        // Red circle placeholder
        create_circle_icon(0xef, 0x44, 0x44, 0xff)
    }

    pub fn disabled() -> Vec<u8> {
        // Gray circle placeholder
        create_circle_icon(0x6b, 0x72, 0x80, 0xff)
    }

    fn create_circle_icon(r: u8, g: u8, b: u8, a: u8) -> Vec<u8> {
        let size = 32;
        let mut data = vec![0u8; size * size * 4];
        let center = size as f32 / 2.0;
        let radius = center - 2.0;

        for y in 0..size {
            for x in 0..size {
                let dx = x as f32 - center;
                let dy = y as f32 - center;
                let dist = (dx * dx + dy * dy).sqrt();

                let idx = (y * size + x) * 4;
                if dist <= radius {
                    data[idx] = r;
                    data[idx + 1] = g;
                    data[idx + 2] = b;
                    data[idx + 3] = a;
                } else if dist <= radius + 1.0 {
                    // Anti-aliased edge
                    let alpha = ((radius + 1.0 - dist) * a as f32) as u8;
                    data[idx] = r;
                    data[idx + 1] = g;
                    data[idx + 2] = b;
                    data[idx + 3] = alpha;
                }
            }
        }

        data
    }
}

/// Main tray application
pub struct TrayApp {
    /// Tokio runtime for async operations
    runtime: tokio::runtime::Runtime,

    /// IPC client state
    connection_state: Arc<RwLock<ConnectionState>>,
    status: Arc<RwLock<AgentStatus>>,
    policy: Arc<RwLock<TrayPolicy>>,

    /// Request sender
    request_tx: mpsc::Sender<IpcRequest>,
}

impl TrayApp {
    /// Create a new tray application
    pub fn new() -> Result<Self> {
        let runtime = tokio::runtime::Builder::new_multi_thread()
            .enable_all()
            .build()
            .context("Failed to create tokio runtime")?;

        let connection_state = Arc::new(RwLock::new(ConnectionState::Disconnected));
        let status = Arc::new(RwLock::new(AgentStatus::default()));
        let policy = Arc::new(RwLock::new(TrayPolicy::default_permissive()));

        let (request_tx, request_rx) = mpsc::channel(32);
        let (update_tx, _update_rx) = mpsc::channel(32);

        // Spawn IPC connection task
        let conn_state = Arc::clone(&connection_state);
        let conn_status = Arc::clone(&status);
        let conn_policy = Arc::clone(&policy);

        runtime.spawn(async move {
            crate::ipc::connection::run_connection(
                conn_state,
                conn_status,
                conn_policy,
                request_rx,
                update_tx,
            )
            .await;
        });

        Ok(Self {
            runtime,
            connection_state,
            status,
            policy,
            request_tx,
        })
    }

    /// Run the tray application (blocking)
    pub fn run(self) -> Result<()> {
        let event_loop = EventLoop::new().context("Failed to create event loop")?;

        // Create initial icon
        let icon = create_icon(IconState::Reconnecting)?;

        // Get initial status and policy for menu
        let (status, policy) = self.runtime.block_on(async {
            let s = self.status.read().await.clone();
            let p = self.policy.read().await.clone();
            (s, p)
        });

        // Build initial menu
        let menu = menu::build_menu(&status, &policy);

        // Create tray icon
        let tooltip = policy
            .tooltip_text
            .clone()
            .unwrap_or_else(|| "GuruRMM Agent".to_string());

        let tray_icon = TrayIconBuilder::new()
            .with_menu(Box::new(menu))
            .with_tooltip(&tooltip)
            .with_icon(icon)
            .build()
            .context("Failed to create tray icon")?;

        info!("Tray icon created");

        // Menu event receiver
        let menu_channel = MenuEvent::receiver();

        // Track last known state for icon updates
        let mut last_icon_state = IconState::Reconnecting;
        let mut last_connected = false;

        // Run event loop
        event_loop.run(move |_event, event_loop| {
            event_loop.set_control_flow(ControlFlow::Wait);

            // Check for menu events (non-blocking)
            if let Ok(event) = menu_channel.try_recv() {
                let action = menu::handle_menu_event(event);
                debug!("Menu action: {:?}", action);

                match action {
                    MenuAction::ForceCheckin => {
                        let tx = self.request_tx.clone();
                        self.runtime.spawn(async move {
                            if let Err(e) = tx.send(IpcRequest::ForceCheckin).await {
                                error!("Failed to send force checkin: {}", e);
                            }
                        });
                    }
                    MenuAction::ViewLogs => {
                        // Open log file location
                        #[cfg(windows)]
                        {
                            let _ = std::process::Command::new("explorer")
                                .arg(r"C:\ProgramData\GuruRMM\logs")
                                .spawn();
                        }
                    }
                    MenuAction::OpenDashboard => {
                        let policy = self.runtime.block_on(async {
                            self.policy.read().await.clone()
                        });
                        if let Some(url) = policy.dashboard_url {
                            let _ = open::that(&url);
                        }
                    }
                    MenuAction::StopAgent => {
                        // Show confirmation dialog before stopping
                        // For now, just send the request
                        let tx = self.request_tx.clone();
                        self.runtime.spawn(async move {
                            if let Err(e) = tx.send(IpcRequest::StopAgent).await {
                                error!("Failed to send stop agent: {}", e);
                            }
                        });
                    }
                    MenuAction::ExitTray => {
                        info!("Exit requested");
                        event_loop.exit();
                    }
                    MenuAction::Unknown(id) => {
                        warn!("Unknown menu action: {}", id);
                    }
                }
            }

            // Periodically update icon and menu based on state
            let (conn_state, status, policy) = self.runtime.block_on(async {
                let c = *self.connection_state.read().await;
                let s = self.status.read().await.clone();
                let p = self.policy.read().await.clone();
                (c, s, p)
            });

            // Determine icon state
            let icon_state = match conn_state {
                ConnectionState::Connected if status.connected => IconState::Connected,
                ConnectionState::Connected => IconState::Error,
                ConnectionState::Connecting => IconState::Reconnecting,
                ConnectionState::Disconnected => IconState::Disabled,
            };

            // Update icon if state changed
            if icon_state != last_icon_state {
                last_icon_state = icon_state;
                if let Ok(new_icon) = create_icon(icon_state) {
                    if let Err(e) = tray_icon.set_icon(Some(new_icon)) {
                        warn!("Failed to update icon: {}", e);
                    }
                }

                // Update tooltip
                let tooltip = match icon_state {
                    IconState::Connected => format!("GuruRMM - Connected to {}",
                        status.server_url.split('/').nth(2).unwrap_or(&status.server_url)),
                    IconState::Reconnecting => "GuruRMM - Reconnecting...".to_string(),
                    IconState::Error => format!("GuruRMM - Error: {}",
                        status.error.as_deref().unwrap_or("Unknown")),
                    IconState::Disabled => "GuruRMM - Disabled".to_string(),
                };
                let _ = tray_icon.set_tooltip(Some(&tooltip));
            }

            // Rebuild menu if connection state changed
            if status.connected != last_connected {
                last_connected = status.connected;
                let new_menu = menu::build_menu(&status, &policy);
                tray_icon.set_menu(Some(Box::new(new_menu)));
            }
        })?;

        Ok(())
    }
}

/// Create an icon for the given state
fn create_icon(state: IconState) -> Result<Icon> {
    let data = match state {
        IconState::Connected => icons::connected(),
        IconState::Reconnecting => icons::reconnecting(),
        IconState::Error => icons::error(),
        IconState::Disabled => icons::disabled(),
    };

    Icon::from_rgba(data, 32, 32).context("Failed to create icon")
}


Now I have a comprehensive picture of how the agent and tray work together. Let me summarize this for the user.


## How the Agent and Tray Work Together

### Architecture

```
┌─────────────────────┐                    ┌─────────────────────┐
│    RMM Server       │◄───WebSocket──────►│   Agent Service     │
│ (rmm-api.azcomp...) │                    │ (runs as SYSTEM)    │
└─────────────────────┘                    └──────────┬──────────┘
                                                      │
                                           Named Pipe │ \\.\pipe\gururmm-agent
                                           (JSON/line)│
                                                      ▼
                                           ┌─────────────────────┐
                                           │     Tray App        │
                                           │ (runs in user       │
                                           │  session)           │
                                           └─────────────────────┘
```

### Key Design Points

**1. Separation of Privileges**
- **Agent**: Runs as SYSTEM service (elevated, always running)
- **Tray**: Runs in user session (normal user, per-login)

**2. IPC Protocol** (`agent/src/ipc.rs:116-167`)
- Uses Windows Named Pipe: `\\.\pipe\gururmm-agent`
- JSON messages, newline-delimited
- **Requests** (tray → agent): `GetStatus`, `GetPolicy`, `ForceCheckin`, `StopAgent`, `Subscribe`, `Ping`
- **Responses** (agent → tray): `Status`, `Policy`, `Ok`, `Denied`, `StatusUpdate`, `PolicyUpdate`

**3. Policy-Controlled Features** (`TrayPolicy` struct)
| Setting | Controls |
|---------|----------|
| `enabled` | Show tray icon at all |
| `show_status` | Display connection status |
| `show_info` | Show version/hostname in menu |
| `allow_force_checkin` | User can trigger immediate check-in |
| `allow_view_logs` | User can open log folder |
| `allow_open_dashboard` | User can open web dashboard |
| `allow_stop_agent` | User can stop service (dangerous) |

**4. Icon States** (`tray/src/tray.rs:17-23`)
| State | Color | Meaning |
|-------|-------|---------|
| Connected | Green | Agent connected to server |
| Reconnecting | Yellow | Connecting/retrying |
| Error | Red | Connection error |
| Disabled | Gray | Policy disabled or agent not running |

**5. Subscription Model**
- Tray sends `Subscribe` request to agent
- Agent pushes `StatusUpdate` and `PolicyUpdate` whenever state changes
- Tray updates icon/tooltip/menu dynamically

### What's NOT Implemented Yet

1. **Tray Launcher** - Agent should auto-launch tray in each user session (via `WTSEnumerateSessions` + `CreateProcessAsUser`)
2. **Server-side TrayPolicy** - Policy comes from server, stored per-site
3. **Unix IPC** - Mac/Linux use Unix domain socket (placeholder exists at `/var/run/gururmm/agent.sock`)

### Current State

Right now, the tray must be launched manually. It will:
1. Connect to agent via named pipe
2. Request status/policy
3. Display icon based on agent's connection state
4. Rebuild menu based on policy permissions


How does the agent get installed on the endpoint?


The user wants to understand how the agent gets installed on endpoints. Let me look at the service installation code in the agent.