     1→//! WebSocket client for server communication
     2→//!
     3→//! Handles the WebSocket connection lifecycle including:
     4→//! - Connection establishment
     5→//! - Authentication handshake
     6→//! - Message sending/receiving
     7→//! - Heartbeat maintenance
     8→//! - Command handling
     9→
    10→use std::sync::Arc;
    11→use std::time::Duration;
    12→
    13→use anyhow::{Context, Result};
    14→use futures_util::{SinkExt, StreamExt};
    15→use tokio::sync::mpsc;
    16→use tokio::time::{interval, timeout};
    17→use tokio_tungstenite::{connect_async, tungstenite::Message};
    18→use tracing::{debug, error, info, warn};
    19→
    20→use super::{AgentMessage, AuthPayload, CommandPayload, ServerMessage, UpdatePayload, UpdateResultPayload, UpdateStatus};
    21→use crate::metrics::NetworkState;
    22→use crate::updater::{AgentUpdater, UpdaterConfig};
    23→use crate::AppState;
    24→
    25→/// WebSocket client for communicating with the GuruRMM server
    26→pub struct WebSocketClient;
    27→
    28→impl WebSocketClient {
    29→    /// Connect to the server and run the message loop
    30→    ///
    31→    /// This function will return when the connection is closed or an error occurs.
    32→    /// The caller should handle reconnection logic.
    33→    pub async fn connect_and_run(state: Arc<AppState>) -> Result<()> {
    34→        let url = &state.config.server.url;
    35→
    36→        // Connect to WebSocket server
    37→        info!("Connecting to {}", url);
    38→        let (ws_stream, response) = connect_async(url)
    39→            .await
    40→            .context("Failed to connect to WebSocket server")?;
    41→
    42→        info!(
    43→            "WebSocket connected (HTTP status: {})",
    44→            response.status()
    45→        );
    46→
    47→        let (mut write, mut read) = ws_stream.split();
    48→
    49→        // Check for pending update (from previous update attempt)
    50→        let updater_config = UpdaterConfig::default();
    51→        let pending_update = AgentUpdater::load_pending_update(&updater_config).await;
    52→
    53→        // If we have pending update info, we just restarted after an update
    54→        let (previous_version, pending_update_id) = if let Some(ref info) = pending_update {
    55→            info!(
    56→                "Found pending update info: {} -> {} (id: {})",
    57→                info.old_version, info.target_version, info.update_id
    58→            );
    59→            (Some(info.old_version.clone()), Some(info.update_id))
    60→        } else {
    61→            (None, None)
    62→        };
    63→
    64→        // Send authentication message
    65→        let auth_msg = AgentMessage::Auth(AuthPayload {
    66→            api_key: state.config.server.api_key.clone(),
    67→            device_id: crate::device_id::get_device_id(),
    68→            hostname: state.config.get_hostname(),
    69→            os_type: std::env::consts::OS.to_string(),
    70→            os_version: sysinfo::System::os_version().unwrap_or_else(|| "unknown".to_string()),
    71→            agent_version: env!("CARGO_PKG_VERSION").to_string(),
    72→            architecture: Self::get_architecture().to_string(),
    73→            previous_version,
    74→            pending_update_id,
    75→        });
    76→
    77→        let auth_json = serde_json::to_string(&auth_msg)?;
    78→        write.send(Message::Text(auth_json)).await?;
    79→        debug!("Sent authentication message");
    80→
    81→        // Wait for auth response with timeout
    82→        let auth_response = timeout(Duration::from_secs(10), read.next())
    83→            .await
    84→            .context("Authentication timeout")?
    85→            .ok_or_else(|| anyhow::anyhow!("Connection closed before auth response"))?
    86→            .context("Failed to receive auth response")?;
    87→
    88→        // Parse auth response
    89→        if let Message::Text(text) = auth_response {
    90→            let server_msg: ServerMessage =
    91→                serde_json::from_str(&text).context("Failed to parse auth response")?;
    92→
    93→            match server_msg {
    94→                ServerMessage::AuthAck(ack) => {
    95→                    if ack.success {
    96→                        info!("Authentication successful, agent_id: {:?}", ack.agent_id);
    97→                        *state.connected.write().await = true;
    98→
    99→                        // Send initial network state immediately after auth
   100→                        let network_state = NetworkState::collect();
   101→                        info!(
   102→                            "Sending initial network state ({} interfaces)",
   103→                            network_state.interfaces.len()
   104→                        );
   105→                        let network_msg = AgentMessage::NetworkState(network_state);
   106→                        let network_json = serde_json::to_string(&network_msg)?;
   107→                        write.send(Message::Text(network_json)).await?;
   108→                    } else {
   109→                        error!("Authentication failed: {:?}", ack.error);
   110→                        return Err(anyhow::anyhow!(
   111→                            "Authentication failed: {}",
   112→                            ack.error.unwrap_or_else(|| "Unknown error".to_string())
   113→                        ));
   114→                    }
   115→                }
   116→                ServerMessage::Error { code, message } => {
   117→                    error!("Server error during auth: {} - {}", code, message);
   118→                    return Err(anyhow::anyhow!("Server error: {} - {}", code, message));
   119→                }
   120→                _ => {
   121→                    warn!("Unexpected message during auth: {:?}", server_msg);
   122→                }
   123→            }
   124→        }
   125→
   126→        // Create channel for outgoing messages
   127→        let (tx, mut rx) = mpsc::channel::<AgentMessage>(100);
   128→
   129→        // Spawn metrics sender task
   130→        let metrics_tx = tx.clone();
   131→        let metrics_state = Arc::clone(&state);
   132→        let metrics_interval = state.config.metrics.interval_seconds;
   133→
   134→        let metrics_task = tokio::spawn(async move {
   135→            let mut timer = interval(Duration::from_secs(metrics_interval));
   136→
   137→            loop {
   138→                timer.tick().await;
   139→
   140→                let metrics = metrics_state.metrics_collector.collect().await;
   141→                if metrics_tx.send(AgentMessage::Metrics(metrics)).await.is_err() {
   142→                    debug!("Metrics channel closed");
   143→                    break;
   144→                }
   145→            }
   146→        });
   147→
   148→        // Spawn network state monitor task (checks for changes every 30 seconds)
   149→        let network_tx = tx.clone();
   150→        let network_task = tokio::spawn(async move {
   151→            // Check for network changes every 30 seconds
   152→            let mut timer = interval(Duration::from_secs(30));
   153→            let mut last_state = NetworkState::collect();
   154→
   155→            loop {
   156→                timer.tick().await;
   157→
   158→                let current_state = NetworkState::collect();
   159→                if current_state.has_changed(&last_state) {
   160→                    info!(
   161→                        "Network state changed (hash: {} -> {}), sending update",
   162→                        last_state.state_hash, current_state.state_hash
   163→                    );
   164→
   165→                    // Log the changes for debugging
   166→                    for iface in &current_state.interfaces {
   167→                        debug!(
   168→                            "  Interface {}: IPv4={:?}",
   169→                            iface.name, iface.ipv4_addresses
   170→                        );
   171→                    }
   172→
   173→                    if network_tx
   174→                        .send(AgentMessage::NetworkState(current_state.clone()))
   175→                        .await
   176→                        .is_err()
   177→                    {
   178→                        debug!("Network channel closed");
   179→                        break;
   180→                    }
   181→                    last_state = current_state;
   182→                }
   183→            }
   184→        });
   185→
   186→        // Spawn heartbeat task
   187→        let heartbeat_tx = tx.clone();
   188→        let heartbeat_task = tokio::spawn(async move {
   189→            let mut timer = interval(Duration::from_secs(30));
   190→
   191→            loop {
   192→                timer.tick().await;
   193→
   194→                if heartbeat_tx.send(AgentMessage::Heartbeat).await.is_err() {
   195→                    debug!("Heartbeat channel closed");
   196→                    break;
   197→                }
   198→            }
   199→        });
   200→
   201→        // Main message loop
   202→        let result: Result<()> = loop {
   203→            tokio::select! {
   204→                // Handle outgoing messages
   205→                Some(msg) = rx.recv() => {
   206→                    let json = serde_json::to_string(&msg)?;
   207→                    if let Err(e) = write.send(Message::Text(json)).await {
   208→                        break Err(e.into());
   209→                    }
   210→
   211→                    match &msg {
   212→                        AgentMessage::Metrics(m) => {
   213→                            debug!("Sent metrics: CPU={:.1}%", m.cpu_percent);
   214→                        }
   215→                        AgentMessage::NetworkState(n) => {
   216→                            debug!("Sent network state: {} interfaces, hash={}",
   217→                                n.interfaces.len(), n.state_hash);
   218→                        }
   219→                        AgentMessage::Heartbeat => {
   220→                            debug!("Sent heartbeat");
   221→                        }
   222→                        _ => {
   223→                            debug!("Sent message: {:?}", std::mem::discriminant(&msg));
   224→                        }
   225→                    }
   226→                }
   227→
   228→                // Handle incoming messages
   229→                Some(msg_result) = read.next() => {
   230→                    match msg_result {
   231→                        Ok(Message::Text(text)) => {
   232→                            if let Err(e) = Self::handle_server_message(&text, &tx).await {
   233→                                error!("Error handling message: {}", e);
   234→                            }
   235→                        }
   236→                        Ok(Message::Ping(data)) => {
   237→                            if let Err(e) = write.send(Message::Pong(data)).await {
   238→                                break Err(e.into());
   239→                            }
   240→                        }
   241→                        Ok(Message::Pong(_)) => {
   242→                            debug!("Received pong");
   243→                        }
   244→                        Ok(Message::Close(frame)) => {
   245→                            info!("Server closed connection: {:?}", frame);
   246→                            break Ok(());
   247→                        }
   248→                        Ok(Message::Binary(_)) => {
   249→                            warn!("Received unexpected binary message");
   250→                        }
   251→                        Ok(Message::Frame(_)) => {
   252→                            // Raw frame, usually not seen
   253→                        }
   254→                        Err(e) => {
   255→                            error!("WebSocket error: {}", e);
   256→                            break Err(e.into());
   257→                        }
   258→                    }
   259→                }
   260→
   261→                // Connection timeout (no activity)
   262→                _ = tokio::time::sleep(Duration::from_secs(90)) => {
   263→                    warn!("Connection timeout, no activity for 90 seconds");
   264→                    break Err(anyhow::anyhow!("Connection timeout"));
   265→                }
   266→            }
   267→        };
   268→
   269→        // Cleanup
   270→        metrics_task.abort();
   271→        network_task.abort();
   272→        heartbeat_task.abort();
   273→        *state.connected.write().await = false;
   274→
   275→        result
   276→    }
   277→
   278→    /// Handle a message received from the server
   279→    async fn handle_server_message(
   280→        text: &str,
   281→        tx: &mpsc::Sender<AgentMessage>,
   282→    ) -> Result<()> {
   283→        let msg: ServerMessage =
   284→            serde_json::from_str(text).context("Failed to parse server message")?;
   285→
   286→        match msg {
   287→            ServerMessage::Command(cmd) => {
   288→                info!("Received command: {:?} (id: {})", cmd.command_type, cmd.id);
   289→                Self::execute_command(cmd, tx.clone()).await;
   290→            }
   291→            ServerMessage::ConfigUpdate(update) => {
   292→                info!("Received config update: {:?}", update);
   293→                // Config updates will be handled in a future phase
   294→            }
   295→            ServerMessage::Ack { message_id } => {
   296→                debug!("Received ack for message: {:?}", message_id);
   297→            }
   298→            ServerMessage::AuthAck(_) => {
   299→                // Already handled during initial auth
   300→            }
   301→            ServerMessage::Error { code, message } => {
   302→                error!("Server error: {} - {}", code, message);
   303→            }
   304→            ServerMessage::Update(payload) => {
   305→                info!(
   306→                    "Received update command: {} -> {} (id: {})",
   307→                    env!("CARGO_PKG_VERSION"),
   308→                    payload.target_version,
   309→                    payload.update_id
   310→                );
   311→                Self::handle_update(payload, tx.clone()).await;
   312→            }
   313→            ServerMessage::TrayPolicyUpdate(policy) => {
   314→                info!("Received tray policy update: enabled={}", policy.enabled);
   315→                // Tray policy updates are handled via IPC broadcast
   316→                // The IPC server will pick this up and notify connected tray apps
   317→            }
   318→        }
   319→
   320→        Ok(())
   321→    }
   322→
   323→    /// Handle an update command from the server
   324→    async fn handle_update(payload: UpdatePayload, tx: mpsc::Sender<AgentMessage>) {
   325→        // Send starting status
   326→        let starting_result = UpdateResultPayload {
   327→            update_id: payload.update_id,
   328→            status: UpdateStatus::Starting,
   329→            old_version: env!("CARGO_PKG_VERSION").to_string(),
   330→            new_version: None,
   331→            error: None,
   332→        };
   333→        let _ = tx.send(AgentMessage::UpdateResult(starting_result)).await;
   334→
   335→        // Spawn update in background (it will restart the service)
   336→        tokio::spawn(async move {
   337→            let config = UpdaterConfig::default();
   338→            let updater = AgentUpdater::new(config);
   339→            let result = updater.perform_update(payload).await;
   340→
   341→            // If we reach here, the update failed (successful update restarts the process)
   342→            let _ = tx.send(AgentMessage::UpdateResult(result)).await;
   343→        });
   344→    }
   345→
   346→    /// Get the current architecture
   347→    fn get_architecture() -> &'static str {
   348→        #[cfg(target_arch = "x86_64")]
   349→        { "amd64" }
   350→        #[cfg(target_arch = "aarch64")]
   351→        { "arm64" }
   352→        #[cfg(target_arch = "x86")]
   353→        { "386" }
   354→        #[cfg(target_arch = "arm")]
   355→        { "arm" }
   356→        #[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64", target_arch = "x86", target_arch = "arm")))]
   357→        { "unknown" }
   358→    }
   359→
   360→    /// Execute a command received from the server
   361→    async fn execute_command(cmd: CommandPayload, tx: mpsc::Sender<AgentMessage>) {
   362→        let command_id = cmd.id;
   363→
   364→        // Spawn command execution in background
   365→        tokio::spawn(async move {
   366→            let start = std::time::Instant::now();
   367→
   368→            let result = Self::run_command(&cmd).await;
   369→            let duration_ms = start.elapsed().as_millis() as u64;
   370→
   371→            let (exit_code, stdout, stderr) = match result {
   372→                Ok((code, out, err)) => (code, out, err),
   373→                Err(e) => (-1, String::new(), format!("Execution error: {}", e)),
   374→            };
   375→
   376→            let result_msg = AgentMessage::CommandResult(super::CommandResultPayload {
   377→                command_id,
   378→                exit_code,
   379→                stdout,
   380→                stderr,
   381→                duration_ms,
   382→            });
   383→
   384→            if tx.send(result_msg).await.is_err() {
   385→                error!("Failed to send command result");
   386→            }
   387→        });
   388→    }
   389→
   390→    /// Run a command and capture output
   391→    async fn run_command(cmd: &CommandPayload) -> Result<(i32, String, String)> {
   392→        use tokio::process::Command;
   393→
   394→        let timeout_secs = cmd.timeout_seconds.unwrap_or(300); // 5 minute default
   395→
   396→        let mut command = match &cmd.command_type {
   397→            super::CommandType::Shell => {
   398→                #[cfg(windows)]
   399→                {
   400→                    let mut c = Command::new("cmd");
   401→                    c.args(["/C", &cmd.command]);
   402→                    c
   403→                }
   404→                #[cfg(unix)]
   405→                {
   406→                    let mut c = Command::new("sh");
   407→                    c.args(["-c", &cmd.command]);
   408→                    c
   409→                }
   410→            }
   411→            super::CommandType::PowerShell => {
   412→                let mut c = Command::new("powershell");
   413→                c.args(["-NoProfile", "-NonInteractive", "-Command", &cmd.command]);
   414→                c
   415→            }
   416→            super::CommandType::Python => {
   417→                let mut c = Command::new("python");
   418→                c.args(["-c", &cmd.command]);
   419→                c
   420→            }
   421→            super::CommandType::Script { interpreter } => {
   422→                let mut c = Command::new(interpreter);
   423→                c.args(["-c", &cmd.command]);
   424→                c
   425→            }
   426→        };
   427→
   428→        // Capture output
   429→        command.stdout(std::process::Stdio::piped());
   430→        command.stderr(std::process::Stdio::piped());
   431→
   432→        // Execute with timeout
   433→        let output = timeout(Duration::from_secs(timeout_secs), command.output())
   434→            .await
   435→            .context("Command timeout")?
   436→            .context("Failed to execute command")?;
   437→
   438→        let exit_code = output.status.code().unwrap_or(-1);
   439→        let stdout = String::from_utf8_lossy(&output.stdout).to_string();
   440→        let stderr = String::from_utf8_lossy(&output.stderr).to_string();
   441→
   442→        Ok((exit_code, stdout, stderr))
   443→    }
   444→}
   445→

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

//! WebSocket client for server communication
//!
//! Handles the WebSocket connection lifecycle including:
//! - Connection establishment
//! - Authentication handshake
//! - Message sending/receiving
//! - Heartbeat maintenance
//! - Command handling

use std::sync::Arc;
use std::time::Duration;

use anyhow::{Context, Result};
use futures_util::{SinkExt, StreamExt};
use tokio::sync::mpsc;
use tokio::time::{interval, timeout};
use tokio_tungstenite::{connect_async, tungstenite::Message};
use tracing::{debug, error, info, warn};

use super::{AgentMessage, AuthPayload, CommandPayload, ServerMessage, UpdatePayload, UpdateResultPayload, UpdateStatus};
use crate::metrics::NetworkState;
use crate::updater::{AgentUpdater, UpdaterConfig};
use crate::AppState;

/// WebSocket client for communicating with the GuruRMM server
pub struct WebSocketClient;

impl WebSocketClient {
    /// Connect to the server and run the message loop
    ///
    /// This function will return when the connection is closed or an error occurs.
    /// The caller should handle reconnection logic.
    pub async fn connect_and_run(state: Arc<AppState>) -> Result<()> {
        let url = &state.config.server.url;

        // Connect to WebSocket server
        info!("Connecting to {}", url);
        let (ws_stream, response) = connect_async(url)
            .await
            .context("Failed to connect to WebSocket server")?;

        info!(
            "WebSocket connected (HTTP status: {})",
            response.status()
        );

        let (mut write, mut read) = ws_stream.split();

        // Check for pending update (from previous update attempt)
        let updater_config = UpdaterConfig::default();
        let pending_update = AgentUpdater::load_pending_update(&updater_config).await;

        // If we have pending update info, we just restarted after an update
        let (previous_version, pending_update_id) = if let Some(ref info) = pending_update {
            info!(
                "Found pending update info: {} -> {} (id: {})",
                info.old_version, info.target_version, info.update_id
            );
            (Some(info.old_version.clone()), Some(info.update_id))
        } else {
            (None, None)
        };

        // Send authentication message
        let auth_msg = AgentMessage::Auth(AuthPayload {
            api_key: state.config.server.api_key.clone(),
            device_id: crate::device_id::get_device_id(),
            hostname: state.config.get_hostname(),
            os_type: std::env::consts::OS.to_string(),
            os_version: sysinfo::System::os_version().unwrap_or_else(|| "unknown".to_string()),
            agent_version: env!("CARGO_PKG_VERSION").to_string(),
            architecture: Self::get_architecture().to_string(),
            previous_version,
            pending_update_id,
        });

        let auth_json = serde_json::to_string(&auth_msg)?;
        write.send(Message::Text(auth_json)).await?;
        debug!("Sent authentication message");

        // Wait for auth response with timeout
        let auth_response = timeout(Duration::from_secs(10), read.next())
            .await
            .context("Authentication timeout")?
            .ok_or_else(|| anyhow::anyhow!("Connection closed before auth response"))?
            .context("Failed to receive auth response")?;

        // Parse auth response
        if let Message::Text(text) = auth_response {
            let server_msg: ServerMessage =
                serde_json::from_str(&text).context("Failed to parse auth response")?;

            match server_msg {
                ServerMessage::AuthAck(ack) => {
                    if ack.success {
                        info!("Authentication successful, agent_id: {:?}", ack.agent_id);
                        *state.connected.write().await = true;

                        // Send initial network state immediately after auth
                        let network_state = NetworkState::collect();
                        info!(
                            "Sending initial network state ({} interfaces)",
                            network_state.interfaces.len()
                        );
                        let network_msg = AgentMessage::NetworkState(network_state);
                        let network_json = serde_json::to_string(&network_msg)?;
                        write.send(Message::Text(network_json)).await?;
                    } else {
                        error!("Authentication failed: {:?}", ack.error);
                        return Err(anyhow::anyhow!(
                            "Authentication failed: {}",
                            ack.error.unwrap_or_else(|| "Unknown error".to_string())
                        ));
                    }
                }
                ServerMessage::Error { code, message } => {
                    error!("Server error during auth: {} - {}", code, message);
                    return Err(anyhow::anyhow!("Server error: {} - {}", code, message));
                }
                _ => {
                    warn!("Unexpected message during auth: {:?}", server_msg);
                }
            }
        }

        // Create channel for outgoing messages
        let (tx, mut rx) = mpsc::channel::<AgentMessage>(100);

        // Spawn metrics sender task
        let metrics_tx = tx.clone();
        let metrics_state = Arc::clone(&state);
        let metrics_interval = state.config.metrics.interval_seconds;

        let metrics_task = tokio::spawn(async move {
            let mut timer = interval(Duration::from_secs(metrics_interval));

            loop {
                timer.tick().await;

                let metrics = metrics_state.metrics_collector.collect().await;
                if metrics_tx.send(AgentMessage::Metrics(metrics)).await.is_err() {
                    debug!("Metrics channel closed");
                    break;
                }
            }
        });

        // Spawn network state monitor task (checks for changes every 30 seconds)
        let network_tx = tx.clone();
        let network_task = tokio::spawn(async move {
            // Check for network changes every 30 seconds
            let mut timer = interval(Duration::from_secs(30));
            let mut last_state = NetworkState::collect();

            loop {
                timer.tick().await;

                let current_state = NetworkState::collect();
                if current_state.has_changed(&last_state) {
                    info!(
                        "Network state changed (hash: {} -> {}), sending update",
                        last_state.state_hash, current_state.state_hash
                    );

                    // Log the changes for debugging
                    for iface in &current_state.interfaces {
                        debug!(
                            "  Interface {}: IPv4={:?}",
                            iface.name, iface.ipv4_addresses
                        );
                    }

                    if network_tx
                        .send(AgentMessage::NetworkState(current_state.clone()))
                        .await
                        .is_err()
                    {
                        debug!("Network channel closed");
                        break;
                    }
                    last_state = current_state;
                }
            }
        });

        // Spawn heartbeat task
        let heartbeat_tx = tx.clone();
        let heartbeat_task = tokio::spawn(async move {
            let mut timer = interval(Duration::from_secs(30));

            loop {
                timer.tick().await;

                if heartbeat_tx.send(AgentMessage::Heartbeat).await.is_err() {
                    debug!("Heartbeat channel closed");
                    break;
                }
            }
        });

        // Main message loop
        let result: Result<()> = loop {
            tokio::select! {
                // Handle outgoing messages
                Some(msg) = rx.recv() => {
                    let json = serde_json::to_string(&msg)?;
                    if let Err(e) = write.send(Message::Text(json)).await {
                        break Err(e.into());
                    }

                    match &msg {
                        AgentMessage::Metrics(m) => {
                            debug!("Sent metrics: CPU={:.1}%", m.cpu_percent);
                        }
                        AgentMessage::NetworkState(n) => {
                            debug!("Sent network state: {} interfaces, hash={}",
                                n.interfaces.len(), n.state_hash);
                        }
                        AgentMessage::Heartbeat => {
                            debug!("Sent heartbeat");
                        }
                        _ => {
                            debug!("Sent message: {:?}", std::mem::discriminant(&msg));
                        }
                    }
                }

                // Handle incoming messages
                Some(msg_result) = read.next() => {
                    match msg_result {
                        Ok(Message::Text(text)) => {
                            if let Err(e) = Self::handle_server_message(&text, &tx).await {
                                error!("Error handling message: {}", e);
                            }
                        }
                        Ok(Message::Ping(data)) => {
                            if let Err(e) = write.send(Message::Pong(data)).await {
                                break Err(e.into());
                            }
                        }
                        Ok(Message::Pong(_)) => {
                            debug!("Received pong");
                        }
                        Ok(Message::Close(frame)) => {
                            info!("Server closed connection: {:?}", frame);
                            break Ok(());
                        }
                        Ok(Message::Binary(_)) => {
                            warn!("Received unexpected binary message");
                        }
                        Ok(Message::Frame(_)) => {
                            // Raw frame, usually not seen
                        }
                        Err(e) => {
                            error!("WebSocket error: {}", e);
                            break Err(e.into());
                        }
                    }
                }

                // Connection timeout (no activity)
                _ = tokio::time::sleep(Duration::from_secs(90)) => {
                    warn!("Connection timeout, no activity for 90 seconds");
                    break Err(anyhow::anyhow!("Connection timeout"));
                }
            }
        };

        // Cleanup
        metrics_task.abort();
        network_task.abort();
        heartbeat_task.abort();
        *state.connected.write().await = false;

        result
    }

    /// Handle a message received from the server
    async fn handle_server_message(
        text: &str,
        tx: &mpsc::Sender<AgentMessage>,
    ) -> Result<()> {
        let msg: ServerMessage =
            serde_json::from_str(text).context("Failed to parse server message")?;

        match msg {
            ServerMessage::Command(cmd) => {
                info!("Received command: {:?} (id: {})", cmd.command_type, cmd.id);
                Self::execute_command(cmd, tx.clone()).await;
            }
            ServerMessage::ConfigUpdate(update) => {
                info!("Received config update: {:?}", update);
                // Config updates will be handled in a future phase
            }
            ServerMessage::Ack { message_id } => {
                debug!("Received ack for message: {:?}", message_id);
            }
            ServerMessage::AuthAck(_) => {
                // Already handled during initial auth
            }
            ServerMessage::Error { code, message } => {
                error!("Server error: {} - {}", code, message);
            }
            ServerMessage::Update(payload) => {
                info!(
                    "Received update command: {} -> {} (id: {})",
                    env!("CARGO_PKG_VERSION"),
                    payload.target_version,
                    payload.update_id
                );
                Self::handle_update(payload, tx.clone()).await;
            }
            ServerMessage::TrayPolicyUpdate(policy) => {
                info!("Received tray policy update: enabled={}", policy.enabled);
                // Tray policy updates are handled via IPC broadcast
                // The IPC server will pick this up and notify connected tray apps
            }
        }

        Ok(())
    }

    /// Handle an update command from the server
    async fn handle_update(payload: UpdatePayload, tx: mpsc::Sender<AgentMessage>) {
        // Send starting status
        let starting_result = UpdateResultPayload {
            update_id: payload.update_id,
            status: UpdateStatus::Starting,
            old_version: env!("CARGO_PKG_VERSION").to_string(),
            new_version: None,
            error: None,
        };
        let _ = tx.send(AgentMessage::UpdateResult(starting_result)).await;

        // Spawn update in background (it will restart the service)
        tokio::spawn(async move {
            let config = UpdaterConfig::default();
            let updater = AgentUpdater::new(config);
            let result = updater.perform_update(payload).await;

            // If we reach here, the update failed (successful update restarts the process)
            let _ = tx.send(AgentMessage::UpdateResult(result)).await;
        });
    }

    /// Get the current architecture
    fn get_architecture() -> &'static str {
        #[cfg(target_arch = "x86_64")]
        { "amd64" }
        #[cfg(target_arch = "aarch64")]
        { "arm64" }
        #[cfg(target_arch = "x86")]
        { "386" }
        #[cfg(target_arch = "arm")]
        { "arm" }
        #[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64", target_arch = "x86", target_arch = "arm")))]
        { "unknown" }
    }

    /// Execute a command received from the server
    async fn execute_command(cmd: CommandPayload, tx: mpsc::Sender<AgentMessage>) {
        let command_id = cmd.id;

        // Spawn command execution in background
        tokio::spawn(async move {
            let start = std::time::Instant::now();

            let result = Self::run_command(&cmd).await;
            let duration_ms = start.elapsed().as_millis() as u64;

            let (exit_code, stdout, stderr) = match result {
                Ok((code, out, err)) => (code, out, err),
                Err(e) => (-1, String::new(), format!("Execution error: {}", e)),
            };

            let result_msg = AgentMessage::CommandResult(super::CommandResultPayload {
                command_id,
                exit_code,
                stdout,
                stderr,
                duration_ms,
            });

            if tx.send(result_msg).await.is_err() {
                error!("Failed to send command result");
            }
        });
    }

    /// Run a command and capture output
    async fn run_command(cmd: &CommandPayload) -> Result<(i32, String, String)> {
        use tokio::process::Command;

        let timeout_secs = cmd.timeout_seconds.unwrap_or(300); // 5 minute default

        let mut command = match &cmd.command_type {
            super::CommandType::Shell => {
                #[cfg(windows)]
                {
                    let mut c = Command::new("cmd");
                    c.args(["/C", &cmd.command]);
                    c
                }
                #[cfg(unix)]
                {
                    let mut c = Command::new("sh");
                    c.args(["-c", &cmd.command]);
                    c
                }
            }
            super::CommandType::PowerShell => {
                let mut c = Command::new("powershell");
                c.args(["-NoProfile", "-NonInteractive", "-Command", &cmd.command]);
                c
            }
            super::CommandType::Python => {
                let mut c = Command::new("python");
                c.args(["-c", &cmd.command]);
                c
            }
            super::CommandType::Script { interpreter } => {
                let mut c = Command::new(interpreter);
                c.args(["-c", &cmd.command]);
                c
            }
        };

        // Capture output
        command.stdout(std::process::Stdio::piped());
        command.stderr(std::process::Stdio::piped());

        // Execute with timeout
        let output = timeout(Duration::from_secs(timeout_secs), command.output())
            .await
            .context("Command timeout")?
            .context("Failed to execute command")?;

        let exit_code = output.status.code().unwrap_or(-1);
        let stdout = String::from_utf8_lossy(&output.stdout).to_string();
        let stderr = String::from_utf8_lossy(&output.stderr).to_string();

        Ok((exit_code, stdout, stderr))
    }
}


Now let me look at the transport module to see the message definitions:


     1→//! Transport layer for agent-server communication
     2→//!
     3→//! Handles WebSocket connection to the GuruRMM server with:
     4→//! - Auto-reconnection on disconnect
     5→//! - Authentication via API key
     6→//! - Sending metrics and receiving commands
     7→//! - Heartbeat to maintain connection
     8→
     9→mod websocket;
    10→
    11→pub use websocket::WebSocketClient;
    12→
    13→use serde::{Deserialize, Serialize};
    14→use uuid::Uuid;
    15→
    16→/// Messages sent from agent to server
    17→#[derive(Debug, Clone, Serialize, Deserialize)]
    18→#[serde(tag = "type", content = "payload")]
    19→#[serde(rename_all = "snake_case")]
    20→pub enum AgentMessage {
    21→    /// Authentication message (sent on connect)
    22→    Auth(AuthPayload),
    23→
    24→    /// Metrics report
    25→    Metrics(crate::metrics::SystemMetrics),
    26→
    27→    /// Network state update (sent on connect and when interfaces change)
    28→    NetworkState(crate::metrics::NetworkState),
    29→
    30→    /// Command execution result
    31→    CommandResult(CommandResultPayload),
    32→
    33→    /// Watchdog event (service stopped, restarted, etc.)
    34→    WatchdogEvent(WatchdogEventPayload),
    35→
    36→    /// Update result (success, failure, rollback)
    37→    UpdateResult(UpdateResultPayload),
    38→
    39→    /// Heartbeat to keep connection alive
    40→    Heartbeat,
    41→}
    42→
    43→/// Authentication payload
    44→#[derive(Debug, Clone, Serialize, Deserialize)]
    45→pub struct AuthPayload {
    46→    /// API key for this agent (or site)
    47→    pub api_key: String,
    48→
    49→    /// Unique device identifier (hardware-derived)
    50→    pub device_id: String,
    51→
    52→    /// Hostname of this machine
    53→    pub hostname: String,
    54→
    55→    /// Operating system type
    56→    pub os_type: String,
    57→
    58→    /// Operating system version
    59→    pub os_version: String,
    60→
    61→    /// Agent version
    62→    pub agent_version: String,
    63→
    64→    /// Architecture (amd64, arm64, etc.)
    65→    #[serde(default = "default_arch")]
    66→    pub architecture: String,
    67→
    68→    /// Previous version if reconnecting after update
    69→    #[serde(skip_serializing_if = "Option::is_none")]
    70→    pub previous_version: Option<String>,
    71→
    72→    /// Update ID if reconnecting after update
    73→    #[serde(skip_serializing_if = "Option::is_none")]
    74→    pub pending_update_id: Option<Uuid>,
    75→}
    76→
    77→fn default_arch() -> String {
    78→    #[cfg(target_arch = "x86_64")]
    79→    { "amd64".to_string() }
    80→    #[cfg(target_arch = "aarch64")]
    81→    { "arm64".to_string() }
    82→    #[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
    83→    { "unknown".to_string() }
    84→}
    85→
    86→/// Command execution result payload
    87→#[derive(Debug, Clone, Serialize, Deserialize)]
    88→pub struct CommandResultPayload {
    89→    /// Command ID (from the server)
    90→    pub command_id: Uuid,
    91→
    92→    /// Exit code (0 = success)
    93→    pub exit_code: i32,
    94→
    95→    /// Standard output
    96→    pub stdout: String,
    97→
    98→    /// Standard error
    99→    pub stderr: String,
   100→
   101→    /// Execution duration in milliseconds
   102→    pub duration_ms: u64,
   103→}
   104→
   105→/// Watchdog event payload
   106→#[derive(Debug, Clone, Serialize, Deserialize)]
   107→pub struct WatchdogEventPayload {
   108→    /// Service or process name
   109→    pub name: String,
   110→
   111→    /// Event type
   112→    pub event: WatchdogEvent,
   113→
   114→    /// Additional details
   115→    pub details: Option<String>,
   116→}
   117→
   118→/// Types of watchdog events
   119→#[derive(Debug, Clone, Serialize, Deserialize)]
   120→#[serde(rename_all = "snake_case")]
   121→pub enum WatchdogEvent {
   122→    /// Service/process was found stopped
   123→    Stopped,
   124→
   125→    /// Service/process was restarted by the agent
   126→    Restarted,
   127→
   128→    /// Restart attempt failed
   129→    RestartFailed,
   130→
   131→    /// Max restart attempts reached
   132→    MaxRestartsReached,
   133→
   134→    /// Service/process recovered on its own
   135→    Recovered,
   136→}
   137→
   138→/// Messages sent from server to agent
   139→#[derive(Debug, Clone, Serialize, Deserialize)]
   140→#[serde(tag = "type", content = "payload")]
   141→#[serde(rename_all = "snake_case")]
   142→pub enum ServerMessage {
   143→    /// Authentication acknowledgment
   144→    AuthAck(AuthAckPayload),
   145→
   146→    /// Command to execute
   147→    Command(CommandPayload),
   148→
   149→    /// Configuration update
   150→    ConfigUpdate(ConfigUpdatePayload),
   151→
   152→    /// Agent update command
   153→    Update(UpdatePayload),
   154→
   155→    /// Tray policy update
   156→    TrayPolicyUpdate(crate::ipc::TrayPolicy),
   157→
   158→    /// Acknowledgment of received message
   159→    Ack { message_id: Option<String> },
   160→
   161→    /// Error message
   162→    Error { code: String, message: String },
   163→}
   164→
   165→/// Authentication acknowledgment payload
   166→#[derive(Debug, Clone, Serialize, Deserialize)]
   167→pub struct AuthAckPayload {
   168→    /// Whether authentication was successful
   169→    pub success: bool,
   170→
   171→    /// Agent ID assigned by server
   172→    pub agent_id: Option<Uuid>,
   173→
   174→    /// Error message if authentication failed
   175→    pub error: Option<String>,
   176→}
   177→
   178→/// Command payload from server
   179→#[derive(Debug, Clone, Serialize, Deserialize)]
   180→pub struct CommandPayload {
   181→    /// Unique command ID
   182→    pub id: Uuid,
   183→
   184→    /// Type of command
   185→    pub command_type: CommandType,
   186→
   187→    /// Command text to execute
   188→    pub command: String,
   189→
   190→    /// Optional timeout in seconds
   191→    pub timeout_seconds: Option<u64>,
   192→
   193→    /// Whether to run as elevated/admin
   194→    pub elevated: bool,
   195→}
   196→
   197→/// Types of commands
   198→#[derive(Debug, Clone, Serialize, Deserialize)]
   199→#[serde(rename_all = "snake_case")]
   200→pub enum CommandType {
   201→    /// Shell command (cmd on Windows, bash on Unix)
   202→    Shell,
   203→
   204→    /// PowerShell command (Windows)
   205→    PowerShell,
   206→
   207→    /// Python script
   208→    Python,
   209→
   210→    /// Raw script (requires interpreter path)
   211→    Script { interpreter: String },
   212→}
   213→
   214→/// Configuration update payload
   215→#[derive(Debug, Clone, Serialize, Deserialize)]
   216→pub struct ConfigUpdatePayload {
   217→    /// New metrics interval (if changed)
   218→    pub metrics_interval_seconds: Option<u64>,
   219→
   220→    /// Updated watchdog config
   221→    pub watchdog: Option<WatchdogConfigUpdate>,
   222→}
   223→
   224→/// Watchdog configuration update
   225→#[derive(Debug, Clone, Serialize, Deserialize)]
   226→pub struct WatchdogConfigUpdate {
   227→    /// Enable/disable watchdog
   228→    pub enabled: Option<bool>,
   229→
   230→    /// Check interval
   231→    pub check_interval_seconds: Option<u64>,
   232→
   233→    // Services and processes would be included here for remote config updates
   234→}
   235→
   236→/// Update command payload from server
   237→#[derive(Debug, Clone, Serialize, Deserialize)]
   238→pub struct UpdatePayload {
   239→    /// Unique update ID for tracking
   240→    pub update_id: Uuid,
   241→
   242→    /// Target version to update to
   243→    pub target_version: String,
   244→
   245→    /// Download URL for the new binary
   246→    pub download_url: String,
   247→
   248→    /// SHA256 checksum of the binary
   249→    pub checksum_sha256: String,
   250→
   251→    /// Whether to force update (skip version check)
   252→    #[serde(default)]
   253→    pub force: bool,
   254→}
   255→
   256→/// Update result payload sent back to server
   257→#[derive(Debug, Clone, Serialize, Deserialize)]
   258→pub struct UpdateResultPayload {
   259→    /// Update ID (from the server)
   260→    pub update_id: Uuid,
   261→
   262→    /// Update status
   263→    pub status: UpdateStatus,
   264→
   265→    /// Old version before update
   266→    pub old_version: String,
   267→
   268→    /// New version after update (if successful)
   269→    pub new_version: Option<String>,
   270→
   271→    /// Error message if failed
   272→    pub error: Option<String>,
   273→}
   274→
   275→/// Update status codes
   276→#[derive(Debug, Clone, Serialize, Deserialize)]
   277→#[serde(rename_all = "snake_case")]
   278→pub enum UpdateStatus {
   279→    /// Update starting
   280→    Starting,
   281→
   282→    /// Downloading new binary
   283→    Downloading,
   284→
   285→    /// Download complete, verifying
   286→    Verifying,
   287→
   288→    /// Installing (replacing binary)
   289→    Installing,
   290→
   291→    /// Restarting service
   292→    Restarting,
   293→
   294→    /// Update completed successfully
   295→    Completed,
   296→
   297→    /// Update failed
   298→    Failed,
   299→
   300→    /// Rolled back to previous version
   301→    RolledBack,
   302→}
   303→

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

//! Transport layer for agent-server communication
//!
//! Handles WebSocket connection to the GuruRMM server with:
//! - Auto-reconnection on disconnect
//! - Authentication via API key
//! - Sending metrics and receiving commands
//! - Heartbeat to maintain connection

mod websocket;

pub use websocket::WebSocketClient;

use serde::{Deserialize, Serialize};
use uuid::Uuid;

/// Messages sent from agent to server
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", content = "payload")]
#[serde(rename_all = "snake_case")]
pub enum AgentMessage {
    /// Authentication message (sent on connect)
    Auth(AuthPayload),

    /// Metrics report
    Metrics(crate::metrics::SystemMetrics),

    /// Network state update (sent on connect and when interfaces change)
    NetworkState(crate::metrics::NetworkState),

    /// Command execution result
    CommandResult(CommandResultPayload),

    /// Watchdog event (service stopped, restarted, etc.)
    WatchdogEvent(WatchdogEventPayload),

    /// Update result (success, failure, rollback)
    UpdateResult(UpdateResultPayload),

    /// Heartbeat to keep connection alive
    Heartbeat,
}

/// Authentication payload
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuthPayload {
    /// API key for this agent (or site)
    pub api_key: String,

    /// Unique device identifier (hardware-derived)
    pub device_id: String,

    /// Hostname of this machine
    pub hostname: String,

    /// Operating system type
    pub os_type: String,

    /// Operating system version
    pub os_version: String,

    /// Agent version
    pub agent_version: String,

    /// Architecture (amd64, arm64, etc.)
    #[serde(default = "default_arch")]
    pub architecture: String,

    /// Previous version if reconnecting after update
    #[serde(skip_serializing_if = "Option::is_none")]
    pub previous_version: Option<String>,

    /// Update ID if reconnecting after update
    #[serde(skip_serializing_if = "Option::is_none")]
    pub pending_update_id: Option<Uuid>,
}

fn default_arch() -> String {
    #[cfg(target_arch = "x86_64")]
    { "amd64".to_string() }
    #[cfg(target_arch = "aarch64")]
    { "arm64".to_string() }
    #[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
    { "unknown".to_string() }
}

/// Command execution result payload
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CommandResultPayload {
    /// Command ID (from the server)
    pub command_id: Uuid,

    /// Exit code (0 = success)
    pub exit_code: i32,

    /// Standard output
    pub stdout: String,

    /// Standard error
    pub stderr: String,

    /// Execution duration in milliseconds
    pub duration_ms: u64,
}

/// Watchdog event payload
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WatchdogEventPayload {
    /// Service or process name
    pub name: String,

    /// Event type
    pub event: WatchdogEvent,

    /// Additional details
    pub details: Option<String>,
}

/// Types of watchdog events
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum WatchdogEvent {
    /// Service/process was found stopped
    Stopped,

    /// Service/process was restarted by the agent
    Restarted,

    /// Restart attempt failed
    RestartFailed,

    /// Max restart attempts reached
    MaxRestartsReached,

    /// Service/process recovered on its own
    Recovered,
}

/// Messages sent from server to agent
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", content = "payload")]
#[serde(rename_all = "snake_case")]
pub enum ServerMessage {
    /// Authentication acknowledgment
    AuthAck(AuthAckPayload),

    /// Command to execute
    Command(CommandPayload),

    /// Configuration update
    ConfigUpdate(ConfigUpdatePayload),

    /// Agent update command
    Update(UpdatePayload),

    /// Tray policy update
    TrayPolicyUpdate(crate::ipc::TrayPolicy),

    /// Acknowledgment of received message
    Ack { message_id: Option<String> },

    /// Error message
    Error { code: String, message: String },
}

/// Authentication acknowledgment payload
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuthAckPayload {
    /// Whether authentication was successful
    pub success: bool,

    /// Agent ID assigned by server
    pub agent_id: Option<Uuid>,

    /// Error message if authentication failed
    pub error: Option<String>,
}

/// Command payload from server
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CommandPayload {
    /// Unique command ID
    pub id: Uuid,

    /// Type of command
    pub command_type: CommandType,

    /// Command text to execute
    pub command: String,

    /// Optional timeout in seconds
    pub timeout_seconds: Option<u64>,

    /// Whether to run as elevated/admin
    pub elevated: bool,
}

/// Types of commands
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CommandType {
    /// Shell command (cmd on Windows, bash on Unix)
    Shell,

    /// PowerShell command (Windows)
    PowerShell,

    /// Python script
    Python,

    /// Raw script (requires interpreter path)
    Script { interpreter: String },
}

/// Configuration update payload
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConfigUpdatePayload {
    /// New metrics interval (if changed)
    pub metrics_interval_seconds: Option<u64>,

    /// Updated watchdog config
    pub watchdog: Option<WatchdogConfigUpdate>,
}

/// Watchdog configuration update
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WatchdogConfigUpdate {
    /// Enable/disable watchdog
    pub enabled: Option<bool>,

    /// Check interval
    pub check_interval_seconds: Option<u64>,

    // Services and processes would be included here for remote config updates
}

/// Update command payload from server
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UpdatePayload {
    /// Unique update ID for tracking
    pub update_id: Uuid,

    /// Target version to update to
    pub target_version: String,

    /// Download URL for the new binary
    pub download_url: String,

    /// SHA256 checksum of the binary
    pub checksum_sha256: String,

    /// Whether to force update (skip version check)
    #[serde(default)]
    pub force: bool,
}

/// Update result payload sent back to server
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UpdateResultPayload {
    /// Update ID (from the server)
    pub update_id: Uuid,

    /// Update status
    pub status: UpdateStatus,

    /// Old version before update
    pub old_version: String,

    /// New version after update (if successful)
    pub new_version: Option<String>,

    /// Error message if failed
    pub error: Option<String>,
}

/// Update status codes
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum UpdateStatus {
    /// Update starting
    Starting,

    /// Downloading new binary
    Downloading,

    /// Download complete, verifying
    Verifying,

    /// Installing (replacing binary)
    Installing,

    /// Restarting service
    Restarting,

    /// Update completed successfully
    Completed,

    /// Update failed
    Failed,

    /// Rolled back to previous version
    RolledBack,
}


Found 7 files
gururmm\agent\src\updater\mod.rs
gururmm\agent\src\tray_launcher.rs
gururmm\agent\src\service.rs
gururmm\agent\src\main.rs
gururmm\agent\src\transport\mod.rs
gururmm\agent\src\transport\websocket.rs
gururmm\agent\src\ipc.rs


Now let me check the IPC module to understand TrayPolicy:


     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

<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 me also check the commands module to see if there are more message patterns:


     1→//! Remote command execution module
     2→//!
     3→//! Handles execution of commands received from the server.
     4→//! Command execution is currently handled inline in transport/websocket.rs
     5→//! This module will be expanded with additional features in Phase 2.
     6→
     7→// Future additions:
     8→// - Command queue for offline execution
     9→// - Script caching
    10→// - Elevated execution handling
    11→// - Command result streaming
    12→

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

//! Remote command execution module
//!
//! Handles execution of commands received from the server.
//! Command execution is currently handled inline in transport/websocket.rs
//! This module will be expanded with additional features in Phase 2.

// Future additions:
// - Command queue for offline execution
// - Script caching
// - Elevated execution handling
// - Command result streaming


     1→//! System metrics collection module
     2→//!
     3→//! Uses the `sysinfo` crate for cross-platform system metrics collection.
     4→//! Collects CPU, memory, disk, and network statistics.
     5→//! Uses `local-ip-address` for network interface enumeration.
     6→
     7→use chrono::{DateTime, Utc};
     8→use local_ip_address::list_afinet_netifas;
     9→use serde::{Deserialize, Serialize};
    10→use std::collections::HashMap;
    11→use std::net::IpAddr;
    12→use std::sync::Mutex;
    13→use sysinfo::{Components, CpuRefreshKind, Disks, MemoryRefreshKind, Networks, RefreshKind, System, Users};
    14→
    15→/// System metrics data structure
    16→#[derive(Debug, Clone, Serialize, Deserialize)]
    17→pub struct SystemMetrics {
    18→    /// Timestamp when metrics were collected
    19→    pub timestamp: DateTime<Utc>,
    20→
    21→    /// CPU usage percentage (0-100)
    22→    pub cpu_percent: f32,
    23→
    24→    /// Memory usage percentage (0-100)
    25→    pub memory_percent: f32,
    26→
    27→    /// Memory used in bytes
    28→    pub memory_used_bytes: u64,
    29→
    30→    /// Total memory in bytes
    31→    pub memory_total_bytes: u64,
    32→
    33→    /// Disk usage percentage (0-100) - primary disk
    34→    pub disk_percent: f32,
    35→
    36→    /// Disk used in bytes - primary disk
    37→    pub disk_used_bytes: u64,
    38→
    39→    /// Total disk space in bytes - primary disk
    40→    pub disk_total_bytes: u64,
    41→
    42→    /// Network bytes received since last collection
    43→    pub network_rx_bytes: u64,
    44→
    45→    /// Network bytes transmitted since last collection
    46→    pub network_tx_bytes: u64,
    47→
    48→    /// Operating system type
    49→    pub os_type: String,
    50→
    51→    /// Operating system version
    52→    pub os_version: String,
    53→
    54→    /// System hostname
    55→    pub hostname: String,
    56→
    57→    /// System uptime in seconds
    58→    #[serde(default)]
    59→    pub uptime_seconds: u64,
    60→
    61→    /// Boot time as Unix timestamp
    62→    #[serde(default)]
    63→    pub boot_time: i64,
    64→
    65→    /// Logged in username (if available)
    66→    #[serde(default)]
    67→    pub logged_in_user: Option<String>,
    68→
    69→    /// User idle time in seconds (time since last input)
    70→    #[serde(default)]
    71→    pub user_idle_seconds: Option<u64>,
    72→
    73→    /// Public/WAN IP address (fetched periodically)
    74→    #[serde(default)]
    75→    pub public_ip: Option<String>,
    76→
    77→    /// CPU temperature in Celsius (average across all CPU sensors)
    78→    #[serde(default)]
    79→    pub cpu_temp_celsius: Option<f32>,
    80→
    81→    /// GPU temperature in Celsius (primary GPU if available)
    82→    #[serde(default)]
    83→    pub gpu_temp_celsius: Option<f32>,
    84→
    85→    /// All temperature sensors with their labels and values
    86→    #[serde(default)]
    87→    pub temperatures: Vec<TemperatureSensor>,
    88→}
    89→
    90→/// Temperature sensor reading
    91→#[derive(Debug, Clone, Serialize, Deserialize)]
    92→pub struct TemperatureSensor {
    93→    /// Sensor label/name (e.g., "CPU Package", "GPU", "nvme0")
    94→    pub label: String,
    95→
    96→    /// Temperature in Celsius
    97→    pub temp_celsius: f32,
    98→
    99→    /// Critical temperature threshold (if available)
   100→    #[serde(default)]
   101→    pub critical_celsius: Option<f32>,
   102→}
   103→
   104→/// Metrics collector using sysinfo
   105→pub struct MetricsCollector {
   106→    /// System info instance (needs to be refreshed for each collection)
   107→    system: Mutex<System>,
   108→
   109→    /// Previous network stats for delta calculation
   110→    prev_network_rx: Mutex<u64>,
   111→    prev_network_tx: Mutex<u64>,
   112→
   113→    /// Cached public IP (refreshed less frequently)
   114→    cached_public_ip: Mutex<Option<String>>,
   115→
   116→    /// Last time public IP was fetched
   117→    last_public_ip_fetch: Mutex<Option<std::time::Instant>>,
   118→}
   119→
   120→impl MetricsCollector {
   121→    /// Create a new metrics collector
   122→    pub fn new() -> Self {
   123→        // Create system with minimal initial refresh
   124→        let system = System::new_with_specifics(
   125→            RefreshKind::new()
   126→                .with_cpu(CpuRefreshKind::everything())
   127→                .with_memory(MemoryRefreshKind::everything()),
   128→        );
   129→
   130→        Self {
   131→            system: Mutex::new(system),
   132→            prev_network_rx: Mutex::new(0),
   133→            prev_network_tx: Mutex::new(0),
   134→            cached_public_ip: Mutex::new(None),
   135→            last_public_ip_fetch: Mutex::new(None),
   136→        }
   137→    }
   138→
   139→    /// Collect current system metrics
   140→    pub async fn collect(&self) -> SystemMetrics {
   141→        // Collect CPU - need to do two refreshes with delay for accurate reading
   142→        // We release the lock between operations to avoid holding MutexGuard across await
   143→        {
   144→            let mut system = self.system.lock().unwrap();
   145→            system.refresh_cpu_all();
   146→        }
   147→
   148→        // Small delay for CPU measurement accuracy
   149→        tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;
   150→

<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 metrics collection module
//!
//! Uses the `sysinfo` crate for cross-platform system metrics collection.
//! Collects CPU, memory, disk, and network statistics.
//! Uses `local-ip-address` for network interface enumeration.

use chrono::{DateTime, Utc};
use local_ip_address::list_afinet_netifas;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::net::IpAddr;
use std::sync::Mutex;
use sysinfo::{Components, CpuRefreshKind, Disks, MemoryRefreshKind, Networks, RefreshKind, System, Users};

/// System metrics data structure
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SystemMetrics {
    /// Timestamp when metrics were collected
    pub timestamp: DateTime<Utc>,

    /// CPU usage percentage (0-100)
    pub cpu_percent: f32,

    /// Memory usage percentage (0-100)
    pub memory_percent: f32,

    /// Memory used in bytes
    pub memory_used_bytes: u64,

    /// Total memory in bytes
    pub memory_total_bytes: u64,

    /// Disk usage percentage (0-100) - primary disk
    pub disk_percent: f32,

    /// Disk used in bytes - primary disk
    pub disk_used_bytes: u64,

    /// Total disk space in bytes - primary disk
    pub disk_total_bytes: u64,

    /// Network bytes received since last collection
    pub network_rx_bytes: u64,

    /// Network bytes transmitted since last collection
    pub network_tx_bytes: u64,

    /// Operating system type
    pub os_type: String,

    /// Operating system version
    pub os_version: String,

    /// System hostname
    pub hostname: String,

    /// System uptime in seconds
    #[serde(default)]
    pub uptime_seconds: u64,

    /// Boot time as Unix timestamp
    #[serde(default)]
    pub boot_time: i64,

    /// Logged in username (if available)
    #[serde(default)]
    pub logged_in_user: Option<String>,

    /// User idle time in seconds (time since last input)
    #[serde(default)]
    pub user_idle_seconds: Option<u64>,

    /// Public/WAN IP address (fetched periodically)
    #[serde(default)]
    pub public_ip: Option<String>,

    /// CPU temperature in Celsius (average across all CPU sensors)
    #[serde(default)]
    pub cpu_temp_celsius: Option<f32>,

    /// GPU temperature in Celsius (primary GPU if available)
    #[serde(default)]
    pub gpu_temp_celsius: Option<f32>,

    /// All temperature sensors with their labels and values
    #[serde(default)]
    pub temperatures: Vec<TemperatureSensor>,
}

/// Temperature sensor reading
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TemperatureSensor {
    /// Sensor label/name (e.g., "CPU Package", "GPU", "nvme0")
    pub label: String,

    /// Temperature in Celsius
    pub temp_celsius: f32,

    /// Critical temperature threshold (if available)
    #[serde(default)]
    pub critical_celsius: Option<f32>,
}

/// Metrics collector using sysinfo
pub struct MetricsCollector {
    /// System info instance (needs to be refreshed for each collection)
    system: Mutex<System>,

    /// Previous network stats for delta calculation
    prev_network_rx: Mutex<u64>,
    prev_network_tx: Mutex<u64>,

    /// Cached public IP (refreshed less frequently)
    cached_public_ip: Mutex<Option<String>>,

    /// Last time public IP was fetched
    last_public_ip_fetch: Mutex<Option<std::time::Instant>>,
}

impl MetricsCollector {
    /// Create a new metrics collector
    pub fn new() -> Self {
        // Create system with minimal initial refresh
        let system = System::new_with_specifics(
            RefreshKind::new()
                .with_cpu(CpuRefreshKind::everything())
                .with_memory(MemoryRefreshKind::everything()),
        );

        Self {
            system: Mutex::new(system),
            prev_network_rx: Mutex::new(0),
            prev_network_tx: Mutex::new(0),
            cached_public_ip: Mutex::new(None),
            last_public_ip_fetch: Mutex::new(None),
        }
    }

    /// Collect current system metrics
    pub async fn collect(&self) -> SystemMetrics {
        // Collect CPU - need to do two refreshes with delay for accurate reading
        // We release the lock between operations to avoid holding MutexGuard across await
        {
            let mut system = self.system.lock().unwrap();
            system.refresh_cpu_all();
        }

        // Small delay for CPU measurement accuracy
        tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;


Perfect! I have a comprehensive understanding of the agent's configuration handling. Let me compile my findings:

## GuruRMM Agent Configuration & Policy Analysis

Based on my exploration of the agent codebase, here's a detailed report on how the agent currently handles configuration and how policies can be designed:

### 1. CURRENT CONFIG STRUCTURE

**File**: `agent/src/config.rs`

The agent has a hierarchical configuration structure loaded from TOML files:

```
AgentConfig {
  ServerConfig {
    - url: String (WebSocket URL)
    - api_key: String (authentication)
    - hostname_override: Option<String>
  }

  MetricsConfig {
    - interval_seconds: u64 (default: 60)
    - collect_cpu: bool
    - collect_memory: bool
    - collect_disk: bool
    - collect_network: bool
  }

  WatchdogConfig {
    - enabled: bool
    - check_interval_seconds: u64 (default: 30)
    - services: Vec<ServiceWatch> {
        name: String
        action: WatchAction (Alert|Restart|Ignore)
        max_restarts: u32
        restart_cooldown_seconds: u64
      }
    - processes: Vec<ProcessWatch> {
        name: String
        action: WatchAction
        start_command: Option<String>
      }
  }
}
```

**Key characteristics**:
- Configuration is static (loaded at startup from TOML file)
- No built-in dynamic update mechanism currently
- Validation enforces: URL scheme (ws/wss), non-empty API key, minimum intervals (10s for metrics, 5s for watchdog)

---

### 2. HOW AGENT RECEIVES UPDATES FROM SERVER

**File**: `agent/src/transport/mod.rs` and `agent/src/transport/websocket.rs`

The server-to-agent communication uses a tagged JSON enum called `ServerMessage`:

```rust
enum ServerMessage {
  AuthAck(AuthAckPayload)           // Initial authentication response
  Command(CommandPayload)            // Execute shell/PS/Python commands
  ConfigUpdate(ConfigUpdatePayload)  // SETTINGS UPDATE MECHANISM
  Update(UpdatePayload)              // Agent binary updates
  TrayPolicyUpdate(TrayPolicy)       // UI policy updates
  Ack { message_id }                 // Acknowledgment
  Error { code, message }            // Error messages
}
```

**ConfigUpdatePayload** (lines 214-234):
```rust
pub struct ConfigUpdatePayload {
  pub metrics_interval_seconds: Option<u64>,
  pub watchdog: Option<WatchdogConfigUpdate> {
    pub enabled: Option<bool>,
    pub check_interval_seconds: Option<u64>,
    // Note: services/processes would be added here for remote config
  }
}
```

**Status**: The message structure exists, but **handling is incomplete** (line 291-293 in websocket.rs):
```rust
ServerMessage::ConfigUpdate(update) => {
  info!("Received config update: {:?}", update);
  // Config updates will be handled in a future phase
}
```

---

### 3. MESSAGE PROTOCOL PATTERNS

**Authentication Handshake**:
1. Agent connects and sends `AgentMessage::Auth` with credentials
2. Server responds with `ServerMessage::AuthAck`
3. Agent immediately sends `AgentMessage::NetworkState` after successful auth

**Ongoing Communication**:
- **Metrics**: Agent sends `AgentMessage::Metrics` at configured interval (default 60s)
- **Network State**: Sent on connect and when interfaces change (checked every 30s)
- **Heartbeat**: `AgentMessage::Heartbeat` sent every 30s to keep connection alive
- **Watchdog Events**: `AgentMessage::WatchdogEvent` for service/process changes
- **Command Results**: `AgentMessage::CommandResult` after executing remote commands
- **Update Results**: `AgentMessage::UpdateResult` tracking agent binary updates

**Message Serialization**:
- Format: JSON with tagged unions (`#[serde(tag = "type", content = "payload")]`)
- Example pattern:
  ```json
  { "type": "metrics", "payload": { ...metrics data... } }
  { "type": "config_update", "payload": { ...update data... } }
  ```

---

### 4. TRAY POLICY (UI CONTROL)

**File**: `agent/src/ipc.rs`

There's already a policy mechanism for the tray application that demonstrates how policies work:

```rust
pub struct TrayPolicy {
  pub enabled: bool,
  pub show_status: bool,
  pub show_info: bool,
  pub allow_force_checkin: bool,
  pub allow_view_logs: bool,
  pub allow_open_dashboard: bool,
  pub allow_stop_agent: bool,
  pub dashboard_url: Option<String>,
  pub tooltip_text: Option<String>,
  pub custom_icon: Option<String>,
}
```

This is received via `ServerMessage::TrayPolicyUpdate(policy)` and broadcast to tray apps via IPC (Windows named pipes or Unix sockets).

---

### 5. KEY ARCHITECTURAL PATTERNS FOR POLICY DESIGN

**Strengths to leverage**:
1. **Optional fields**: Use `Option<T>` for partial updates (only changed fields)
2. **Enum-based actions**: `WatchAction` pattern (Alert|Restart|Ignore) shows how to offer discrete policy choices
3. **Broadcast mechanism**: TrayPolicy uses broadcast channels - could be reused for agent policies
4. **IPC pattern**: Shows how to push updates to connected clients (could extend to agents)

**Current Gaps**:
1. **No dynamic config reload** - ConfigUpdate received but not applied
2. **No per-device targeting** - policies would need device_id/hostname filtering at server
3. **No version compatibility** - policies don't specify minimum agent version
4. **No audit logging** - no record of which policies were applied when

---

### 6. DESIGN RECOMMENDATIONS FOR POLICIES

Based on the codebase patterns, policies should be structured as:

```rust
// Similar to ConfigUpdatePayload pattern
pub struct PolicyUpdatePayload {
  pub policy_id: Uuid,
  pub policies: Vec<Policy>,
  pub applies_to: Vec<String>, // device IDs or hostnames
  pub version_constraint: Option<VersionConstraint>,
}

pub enum Policy {
  MetricsCollection { ... },      // extends MetricsConfig
  ServiceMonitoring { ... },       // extends WatchdogConfig
  CommandExecution { ... },        // restrict command types/patterns
  SystemAccess { ... },            // restrict file/registry access
  // etc.
}
```

The agent would:
1. Receive `ServerMessage::PolicyUpdate(payload)` via WebSocket
2. Validate policy compatibility
3. Apply policies dynamically (reload without restart if possible)
4. Send `AgentMessage::PolicyApplied(result)` back to server
5. Store policy state for startup recovery

---

**File Paths for Reference**:
- `/C:\Users\MikeSwanson\claude-projects\gururmm\agent\src\config.rs` - Config structure
- `/C:\Users\MikeSwanson\claude-projects\gururmm\agent\src\main.rs` - Startup/lifecycle
- `/C:\Users\MikeSwanson\claude-projects\gururmm\agent\src\transport\websocket.rs` - Message handling
- `/C:\Users\MikeSwanson\claude-projects\gururmm\agent\src\transport\mod.rs` - Message types
- `/C:\Users\MikeSwanson\claude-projects\gururmm\agent\src\ipc.rs` - Policy broadcast example