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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    /// Agent version
    pub agent_version: String,

    /// Device ID
    pub device_id: String,

    /// Hostname
    pub hostname: String,

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

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

    /// Get current tray policy
    GetPolicy,

    /// Force immediate check-in
    ForceCheckin,

    /// Stop the agent service
    StopAgent,

    /// Subscribe to status updates
    Subscribe,

    /// Unsubscribe from updates
    Unsubscribe,

    /// Ping (health check)
    Ping,
}

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

    /// Current tray policy
    Policy(TrayPolicy),

    /// Success acknowledgment
    Ok,

    /// Pong response to ping
    Pong,

    /// Error
    Error { message: String },

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

            info!("IPC client connected");

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

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

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

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

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

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

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

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

        Ok(())
    }
}

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

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

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

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

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

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

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

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

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

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


     1→-- GuruRMM Initial Schema
     2→-- Creates tables for agents, metrics, commands, watchdog events, and users
     3→
     4→-- Enable UUID extension
     5→CREATE EXTENSION IF NOT EXISTS "pgcrypto";
     6→
     7→-- Agents table
     8→-- Stores registered agents and their current status
     9→CREATE TABLE agents (
    10→    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    11→    hostname VARCHAR(255) NOT NULL,
    12→    api_key_hash VARCHAR(255) NOT NULL,
    13→    os_type VARCHAR(50) NOT NULL,
    14→    os_version VARCHAR(100),
    15→    agent_version VARCHAR(50),
    16→    last_seen TIMESTAMPTZ,
    17→    status VARCHAR(20) DEFAULT 'offline',
    18→    created_at TIMESTAMPTZ DEFAULT NOW(),
    19→    updated_at TIMESTAMPTZ DEFAULT NOW()
    20→);
    21→
    22→-- Index for looking up agents by hostname
    23→CREATE INDEX idx_agents_hostname ON agents(hostname);
    24→
    25→-- Index for finding online agents
    26→CREATE INDEX idx_agents_status ON agents(status);
    27→
    28→-- Metrics table
    29→-- Time-series data for system metrics from agents
    30→CREATE TABLE metrics (
    31→    id BIGSERIAL PRIMARY KEY,
    32→    agent_id UUID NOT NULL REFERENCES agents(id) ON DELETE CASCADE,
    33→    timestamp TIMESTAMPTZ DEFAULT NOW(),
    34→    cpu_percent REAL,
    35→    memory_percent REAL,
    36→    memory_used_bytes BIGINT,
    37→    disk_percent REAL,
    38→    disk_used_bytes BIGINT,
    39→    network_rx_bytes BIGINT,
    40→    network_tx_bytes BIGINT
    41→);
    42→
    43→-- Index for querying metrics by agent and time
    44→CREATE INDEX idx_metrics_agent_time ON metrics(agent_id, timestamp DESC);
    45→
    46→-- Index for finding recent metrics
    47→CREATE INDEX idx_metrics_timestamp ON metrics(timestamp DESC);
    48→
    49→-- Users table
    50→-- Dashboard users for authentication
    51→CREATE TABLE users (
    52→    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    53→    email VARCHAR(255) UNIQUE NOT NULL,
    54→    password_hash VARCHAR(255),
    55→    name VARCHAR(255),
    56→    role VARCHAR(50) DEFAULT 'user',
    57→    sso_provider VARCHAR(50),
    58→    sso_id VARCHAR(255),
    59→    created_at TIMESTAMPTZ DEFAULT NOW(),
    60→    last_login TIMESTAMPTZ
    61→);
    62→
    63→-- Index for email lookups during login
    64→CREATE INDEX idx_users_email ON users(email);
    65→
    66→-- Index for SSO lookups
    67→CREATE INDEX idx_users_sso ON users(sso_provider, sso_id);
    68→
    69→-- Commands table
    70→-- Commands sent to agents and their results
    71→CREATE TABLE commands (
    72→    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    73→    agent_id UUID NOT NULL REFERENCES agents(id) ON DELETE CASCADE,
    74→    command_type VARCHAR(50) NOT NULL,
    75→    command_text TEXT NOT NULL,
    76→    status VARCHAR(20) DEFAULT 'pending',
    77→    exit_code INTEGER,
    78→    stdout TEXT,
    79→    stderr TEXT,
    80→    created_at TIMESTAMPTZ DEFAULT NOW(),
    81→    started_at TIMESTAMPTZ,
    82→    completed_at TIMESTAMPTZ,
    83→    created_by UUID REFERENCES users(id) ON DELETE SET NULL
    84→);
    85→
    86→-- Index for finding pending commands for an agent
    87→CREATE INDEX idx_commands_agent_status ON commands(agent_id, status);
    88→
    89→-- Index for command history queries
    90→CREATE INDEX idx_commands_created ON commands(created_at DESC);
    91→
    92→-- Watchdog events table
    93→-- Events from agent watchdog monitoring
    94→CREATE TABLE watchdog_events (
    95→    id BIGSERIAL PRIMARY KEY,
    96→    agent_id UUID NOT NULL REFERENCES agents(id) ON DELETE CASCADE,
    97→    timestamp TIMESTAMPTZ DEFAULT NOW(),
    98→    service_name VARCHAR(255) NOT NULL,
    99→    event_type VARCHAR(50) NOT NULL,
   100→    details TEXT
   101→);
   102→
   103→-- Index for querying events by agent and time
   104→CREATE INDEX idx_watchdog_agent_time ON watchdog_events(agent_id, timestamp DESC);
   105→
   106→-- Index for finding recent events
   107→CREATE INDEX idx_watchdog_timestamp ON watchdog_events(timestamp DESC);
   108→
   109→-- Function to update updated_at timestamp
   110→CREATE OR REPLACE FUNCTION update_updated_at_column()
   111→RETURNS TRIGGER AS $$
   112→BEGIN
   113→    NEW.updated_at = NOW();
   114→    RETURN NEW;
   115→END;
   116→$$ LANGUAGE plpgsql;
   117→
   118→-- Trigger for agents table
   119→CREATE TRIGGER update_agents_updated_at
   120→    BEFORE UPDATE ON agents
   121→    FOR EACH ROW
   122→    EXECUTE FUNCTION update_updated_at_column();
   123→

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

-- GuruRMM Initial Schema
-- Creates tables for agents, metrics, commands, watchdog events, and users

-- Enable UUID extension
CREATE EXTENSION IF NOT EXISTS "pgcrypto";

-- Agents table
-- Stores registered agents and their current status
CREATE TABLE agents (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    hostname VARCHAR(255) NOT NULL,
    api_key_hash VARCHAR(255) NOT NULL,
    os_type VARCHAR(50) NOT NULL,
    os_version VARCHAR(100),
    agent_version VARCHAR(50),
    last_seen TIMESTAMPTZ,
    status VARCHAR(20) DEFAULT 'offline',
    created_at TIMESTAMPTZ DEFAULT NOW(),
    updated_at TIMESTAMPTZ DEFAULT NOW()
);

-- Index for looking up agents by hostname
CREATE INDEX idx_agents_hostname ON agents(hostname);

-- Index for finding online agents
CREATE INDEX idx_agents_status ON agents(status);

-- Metrics table
-- Time-series data for system metrics from agents
CREATE TABLE metrics (
    id BIGSERIAL PRIMARY KEY,
    agent_id UUID NOT NULL REFERENCES agents(id) ON DELETE CASCADE,
    timestamp TIMESTAMPTZ DEFAULT NOW(),
    cpu_percent REAL,
    memory_percent REAL,
    memory_used_bytes BIGINT,
    disk_percent REAL,
    disk_used_bytes BIGINT,
    network_rx_bytes BIGINT,
    network_tx_bytes BIGINT
);

-- Index for querying metrics by agent and time
CREATE INDEX idx_metrics_agent_time ON metrics(agent_id, timestamp DESC);

-- Index for finding recent metrics
CREATE INDEX idx_metrics_timestamp ON metrics(timestamp DESC);

-- Users table
-- Dashboard users for authentication
CREATE TABLE users (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    email VARCHAR(255) UNIQUE NOT NULL,
    password_hash VARCHAR(255),
    name VARCHAR(255),
    role VARCHAR(50) DEFAULT 'user',
    sso_provider VARCHAR(50),
    sso_id VARCHAR(255),
    created_at TIMESTAMPTZ DEFAULT NOW(),
    last_login TIMESTAMPTZ
);

-- Index for email lookups during login
CREATE INDEX idx_users_email ON users(email);

-- Index for SSO lookups
CREATE INDEX idx_users_sso ON users(sso_provider, sso_id);

-- Commands table
-- Commands sent to agents and their results
CREATE TABLE commands (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    agent_id UUID NOT NULL REFERENCES agents(id) ON DELETE CASCADE,
    command_type VARCHAR(50) NOT NULL,
    command_text TEXT NOT NULL,
    status VARCHAR(20) DEFAULT 'pending',
    exit_code INTEGER,
    stdout TEXT,
    stderr TEXT,
    created_at TIMESTAMPTZ DEFAULT NOW(),
    started_at TIMESTAMPTZ,
    completed_at TIMESTAMPTZ,
    created_by UUID REFERENCES users(id) ON DELETE SET NULL
);

-- Index for finding pending commands for an agent
CREATE INDEX idx_commands_agent_status ON commands(agent_id, status);

-- Index for command history queries
CREATE INDEX idx_commands_created ON commands(created_at DESC);

-- Watchdog events table
-- Events from agent watchdog monitoring
CREATE TABLE watchdog_events (
    id BIGSERIAL PRIMARY KEY,
    agent_id UUID NOT NULL REFERENCES agents(id) ON DELETE CASCADE,
    timestamp TIMESTAMPTZ DEFAULT NOW(),
    service_name VARCHAR(255) NOT NULL,
    event_type VARCHAR(50) NOT NULL,
    details TEXT
);

-- Index for querying events by agent and time
CREATE INDEX idx_watchdog_agent_time ON watchdog_events(agent_id, timestamp DESC);

-- Index for finding recent events
CREATE INDEX idx_watchdog_timestamp ON watchdog_events(timestamp DESC);

-- Function to update updated_at timestamp
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
    NEW.updated_at = NOW();
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

-- Trigger for agents table
CREATE TRIGGER update_agents_updated_at
    BEFORE UPDATE ON agents
    FOR EACH ROW
    EXECUTE FUNCTION update_updated_at_column();


     1→-- GuruRMM Clients and Sites Schema
     2→-- Adds multi-tenant support with clients, sites, and site-based agent registration
     3→
     4→-- Clients table (organizations/companies)
     5→CREATE TABLE clients (
     6→    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
     7→    name VARCHAR(255) NOT NULL,
     8→    code VARCHAR(50) UNIQUE, -- Optional short code like "ACME"
     9→    notes TEXT,
    10→    is_active BOOLEAN DEFAULT TRUE,
    11→    created_at TIMESTAMPTZ DEFAULT NOW(),
    12→    updated_at TIMESTAMPTZ DEFAULT NOW()
    13→);
    14→
    15→CREATE INDEX idx_clients_name ON clients(name);
    16→CREATE INDEX idx_clients_code ON clients(code);
    17→
    18→-- Trigger for clients updated_at
    19→CREATE TRIGGER update_clients_updated_at
    20→    BEFORE UPDATE ON clients
    21→    FOR EACH ROW
    22→    EXECUTE FUNCTION update_updated_at_column();
    23→
    24→-- Sites table (locations under a client)
    25→CREATE TABLE sites (
    26→    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    27→    client_id UUID NOT NULL REFERENCES clients(id) ON DELETE CASCADE,
    28→    name VARCHAR(255) NOT NULL,
    29→    -- Site code: human-friendly, used for agent registration (e.g., "BLUE-TIGER-4829")
    30→    site_code VARCHAR(50) UNIQUE NOT NULL,
    31→    -- API key hash for this site (all agents at site share this key)
    32→    api_key_hash VARCHAR(255) NOT NULL,
    33→    address TEXT,
    34→    notes TEXT,
    35→    is_active BOOLEAN DEFAULT TRUE,
    36→    created_at TIMESTAMPTZ DEFAULT NOW(),
    37→    updated_at TIMESTAMPTZ DEFAULT NOW()
    38→);
    39→
    40→CREATE INDEX idx_sites_client ON sites(client_id);
    41→CREATE INDEX idx_sites_code ON sites(site_code);
    42→CREATE INDEX idx_sites_api_key ON sites(api_key_hash);
    43→
    44→-- Trigger for sites updated_at
    45→CREATE TRIGGER update_sites_updated_at
    46→    BEFORE UPDATE ON sites
    47→    FOR EACH ROW
    48→    EXECUTE FUNCTION update_updated_at_column();
    49→
    50→-- Add new columns to agents table
    51→-- device_id: unique hardware-derived identifier for the machine
    52→ALTER TABLE agents ADD COLUMN device_id VARCHAR(255);
    53→
    54→-- site_id: which site this agent belongs to (nullable for legacy agents)
    55→ALTER TABLE agents ADD COLUMN site_id UUID REFERENCES sites(id) ON DELETE SET NULL;
    56→
    57→-- Make api_key_hash nullable (new agents will use site's api_key)
    58→ALTER TABLE agents ALTER COLUMN api_key_hash DROP NOT NULL;
    59→
    60→-- Index for looking up agents by device_id within a site
    61→CREATE UNIQUE INDEX idx_agents_site_device ON agents(site_id, device_id) WHERE site_id IS NOT NULL AND device_id IS NOT NULL;
    62→
    63→-- Index for site lookups
    64→CREATE INDEX idx_agents_site ON agents(site_id);
    65→
    66→-- Registration tokens table (optional: for secure site code distribution)
    67→CREATE TABLE registration_tokens (
    68→    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    69→    site_id UUID NOT NULL REFERENCES sites(id) ON DELETE CASCADE,
    70→    token_hash VARCHAR(255) NOT NULL,
    71→    description VARCHAR(255),
    72→    uses_remaining INTEGER, -- NULL = unlimited
    73→    expires_at TIMESTAMPTZ,
    74→    created_at TIMESTAMPTZ DEFAULT NOW(),
    75→    created_by UUID REFERENCES users(id) ON DELETE SET NULL
    76→);
    77→
    78→CREATE INDEX idx_reg_tokens_site ON registration_tokens(site_id);
    79→CREATE INDEX idx_reg_tokens_hash ON registration_tokens(token_hash);
    80→
    81→-- Function to generate a random site code (WORD-WORD-####)
    82→-- This is just a helper; actual generation should be in application code
    83→-- for better word lists
    84→CREATE OR REPLACE FUNCTION generate_site_code() RETURNS VARCHAR(50) AS $$
    85→DECLARE
    86→    words TEXT[] := ARRAY['ALPHA', 'BETA', 'GAMMA', 'DELTA', 'ECHO', 'FOXTROT',
    87→                          'BLUE', 'GREEN', 'RED', 'GOLD', 'SILVER', 'IRON',
    88→                          'HAWK', 'EAGLE', 'TIGER', 'LION', 'WOLF', 'BEAR',
    89→                          'NORTH', 'SOUTH', 'EAST', 'WEST', 'PEAK', 'VALLEY',
    90→                          'RIVER', 'OCEAN', 'STORM', 'CLOUD', 'STAR', 'MOON'];
    91→    word1 TEXT;
    92→    word2 TEXT;
    93→    num INTEGER;
    94→BEGIN
    95→    word1 := words[1 + floor(random() * array_length(words, 1))::int];
    96→    word2 := words[1 + floor(random() * array_length(words, 1))::int];
    97→    num := 1000 + floor(random() * 9000)::int;
    98→    RETURN word1 || '-' || word2 || '-' || num::text;
    99→END;
   100→$$ LANGUAGE plpgsql;
   101→

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

-- GuruRMM Clients and Sites Schema
-- Adds multi-tenant support with clients, sites, and site-based agent registration

-- Clients table (organizations/companies)
CREATE TABLE clients (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    name VARCHAR(255) NOT NULL,
    code VARCHAR(50) UNIQUE, -- Optional short code like "ACME"
    notes TEXT,
    is_active BOOLEAN DEFAULT TRUE,
    created_at TIMESTAMPTZ DEFAULT NOW(),
    updated_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX idx_clients_name ON clients(name);
CREATE INDEX idx_clients_code ON clients(code);

-- Trigger for clients updated_at
CREATE TRIGGER update_clients_updated_at
    BEFORE UPDATE ON clients
    FOR EACH ROW
    EXECUTE FUNCTION update_updated_at_column();

-- Sites table (locations under a client)
CREATE TABLE sites (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    client_id UUID NOT NULL REFERENCES clients(id) ON DELETE CASCADE,
    name VARCHAR(255) NOT NULL,
    -- Site code: human-friendly, used for agent registration (e.g., "BLUE-TIGER-4829")
    site_code VARCHAR(50) UNIQUE NOT NULL,
    -- API key hash for this site (all agents at site share this key)
    api_key_hash VARCHAR(255) NOT NULL,
    address TEXT,
    notes TEXT,
    is_active BOOLEAN DEFAULT TRUE,
    created_at TIMESTAMPTZ DEFAULT NOW(),
    updated_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX idx_sites_client ON sites(client_id);
CREATE INDEX idx_sites_code ON sites(site_code);
CREATE INDEX idx_sites_api_key ON sites(api_key_hash);

-- Trigger for sites updated_at
CREATE TRIGGER update_sites_updated_at
    BEFORE UPDATE ON sites
    FOR EACH ROW
    EXECUTE FUNCTION update_updated_at_column();

-- Add new columns to agents table
-- device_id: unique hardware-derived identifier for the machine
ALTER TABLE agents ADD COLUMN device_id VARCHAR(255);

-- site_id: which site this agent belongs to (nullable for legacy agents)
ALTER TABLE agents ADD COLUMN site_id UUID REFERENCES sites(id) ON DELETE SET NULL;

-- Make api_key_hash nullable (new agents will use site's api_key)
ALTER TABLE agents ALTER COLUMN api_key_hash DROP NOT NULL;

-- Index for looking up agents by device_id within a site
CREATE UNIQUE INDEX idx_agents_site_device ON agents(site_id, device_id) WHERE site_id IS NOT NULL AND device_id IS NOT NULL;

-- Index for site lookups
CREATE INDEX idx_agents_site ON agents(site_id);

-- Registration tokens table (optional: for secure site code distribution)
CREATE TABLE registration_tokens (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    site_id UUID NOT NULL REFERENCES sites(id) ON DELETE CASCADE,
    token_hash VARCHAR(255) NOT NULL,
    description VARCHAR(255),
    uses_remaining INTEGER, -- NULL = unlimited
    expires_at TIMESTAMPTZ,
    created_at TIMESTAMPTZ DEFAULT NOW(),
    created_by UUID REFERENCES users(id) ON DELETE SET NULL
);

CREATE INDEX idx_reg_tokens_site ON registration_tokens(site_id);
CREATE INDEX idx_reg_tokens_hash ON registration_tokens(token_hash);

-- Function to generate a random site code (WORD-WORD-####)
-- This is just a helper; actual generation should be in application code
-- for better word lists
CREATE OR REPLACE FUNCTION generate_site_code() RETURNS VARCHAR(50) AS $$
DECLARE
    words TEXT[] := ARRAY['ALPHA', 'BETA', 'GAMMA', 'DELTA', 'ECHO', 'FOXTROT',
                          'BLUE', 'GREEN', 'RED', 'GOLD', 'SILVER', 'IRON',
                          'HAWK', 'EAGLE', 'TIGER', 'LION', 'WOLF', 'BEAR',
                          'NORTH', 'SOUTH', 'EAST', 'WEST', 'PEAK', 'VALLEY',
                          'RIVER', 'OCEAN', 'STORM', 'CLOUD', 'STAR', 'MOON'];
    word1 TEXT;
    word2 TEXT;
    num INTEGER;
BEGIN
    word1 := words[1 + floor(random() * array_length(words, 1))::int];
    word2 := words[1 + floor(random() * array_length(words, 1))::int];
    num := 1000 + floor(random() * 9000)::int;
    RETURN word1 || '-' || word2 || '-' || num::text;
END;
$$ LANGUAGE plpgsql;


<tool_use_error>File does not exist.</tool_use_error>


     1→-- Extended metrics and agent state
     2→-- Adds columns for uptime, user info, IPs, and network state storage
     3→
     4→-- Add extended columns to metrics table
     5→ALTER TABLE metrics ADD COLUMN IF NOT EXISTS uptime_seconds BIGINT;
     6→ALTER TABLE metrics ADD COLUMN IF NOT EXISTS boot_time BIGINT;
     7→ALTER TABLE metrics ADD COLUMN IF NOT EXISTS logged_in_user VARCHAR(255);
     8→ALTER TABLE metrics ADD COLUMN IF NOT EXISTS user_idle_seconds BIGINT;
     9→ALTER TABLE metrics ADD COLUMN IF NOT EXISTS public_ip VARCHAR(45);  -- Supports IPv6
    10→
    11→-- Agent state table for current/latest agent information
    12→-- This stores the latest snapshot of extended agent info (not time-series)
    13→CREATE TABLE IF NOT EXISTS agent_state (
    14→    agent_id UUID PRIMARY KEY REFERENCES agents(id) ON DELETE CASCADE,
    15→    -- Network state
    16→    network_interfaces JSONB,
    17→    network_state_hash VARCHAR(32),
    18→    -- Latest extended metrics (cached for quick access)
    19→    uptime_seconds BIGINT,
    20→    boot_time BIGINT,
    21→    logged_in_user VARCHAR(255),
    22→    user_idle_seconds BIGINT,
    23→    public_ip VARCHAR(45),
    24→    -- Timestamps
    25→    network_updated_at TIMESTAMPTZ,
    26→    metrics_updated_at TIMESTAMPTZ DEFAULT NOW()
    27→);
    28→
    29→-- Index for finding agents by public IP (useful for diagnostics)
    30→CREATE INDEX IF NOT EXISTS idx_agent_state_public_ip ON agent_state(public_ip);
    31→
    32→-- Add memory_total_bytes and disk_total_bytes to metrics for completeness
    33→ALTER TABLE metrics ADD COLUMN IF NOT EXISTS memory_total_bytes BIGINT;
    34→ALTER TABLE metrics ADD COLUMN IF NOT EXISTS disk_total_bytes BIGINT;
    35→

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

-- Extended metrics and agent state
-- Adds columns for uptime, user info, IPs, and network state storage

-- Add extended columns to metrics table
ALTER TABLE metrics ADD COLUMN IF NOT EXISTS uptime_seconds BIGINT;
ALTER TABLE metrics ADD COLUMN IF NOT EXISTS boot_time BIGINT;
ALTER TABLE metrics ADD COLUMN IF NOT EXISTS logged_in_user VARCHAR(255);
ALTER TABLE metrics ADD COLUMN IF NOT EXISTS user_idle_seconds BIGINT;
ALTER TABLE metrics ADD COLUMN IF NOT EXISTS public_ip VARCHAR(45);  -- Supports IPv6

-- Agent state table for current/latest agent information
-- This stores the latest snapshot of extended agent info (not time-series)
CREATE TABLE IF NOT EXISTS agent_state (
    agent_id UUID PRIMARY KEY REFERENCES agents(id) ON DELETE CASCADE,
    -- Network state
    network_interfaces JSONB,
    network_state_hash VARCHAR(32),
    -- Latest extended metrics (cached for quick access)
    uptime_seconds BIGINT,
    boot_time BIGINT,
    logged_in_user VARCHAR(255),
    user_idle_seconds BIGINT,
    public_ip VARCHAR(45),
    -- Timestamps
    network_updated_at TIMESTAMPTZ,
    metrics_updated_at TIMESTAMPTZ DEFAULT NOW()
);

-- Index for finding agents by public IP (useful for diagnostics)
CREATE INDEX IF NOT EXISTS idx_agent_state_public_ip ON agent_state(public_ip);

-- Add memory_total_bytes and disk_total_bytes to metrics for completeness
ALTER TABLE metrics ADD COLUMN IF NOT EXISTS memory_total_bytes BIGINT;
ALTER TABLE metrics ADD COLUMN IF NOT EXISTS disk_total_bytes BIGINT;


C:\Users\MikeSwanson\claude-projects\gururmm\server\src\main.rs:37:pub struct AppState {
C:\Users\MikeSwanson\claude-projects\gururmm\server\src\main.rs:110:    let state = AppState {
C:\Users\MikeSwanson\claude-projects\gururmm\server\src\main.rs:131:fn build_router(state: AppState) -> Router {
C:\Users\MikeSwanson\claude-projects\gururmm\server\src\auth\mod.rs:16:use crate::AppState;
C:\Users\MikeSwanson\claude-projects\gururmm\server\src\auth\mod.rs:110:impl FromRequestParts<AppState> for AuthUser {
C:\Users\MikeSwanson\claude-projects\gururmm\server\src\auth\mod.rs:113:    async fn from_request_parts(parts: &mut Parts, state: &AppState) -> Result<Self, Self::Rejection> {
C:\Users\MikeSwanson\claude-projects\gururmm\server\src\api\agents.rs:16:use crate::AppState;
C:\Users\MikeSwanson\claude-projects\gururmm\server\src\api\agents.rs:36:    State(state): State<AppState>,
C:\Users\MikeSwanson\claude-projects\gururmm\server\src\api\agents.rs:66:    State(state): State<AppState>,
C:\Users\MikeSwanson\claude-projects\gururmm\server\src\api\agents.rs:78:    State(state): State<AppState>,
C:\Users\MikeSwanson\claude-projects\gururmm\server\src\api\agents.rs:91:    State(state): State<AppState>,
C:\Users\MikeSwanson\claude-projects\gururmm\server\src\api\agents.rs:113:    State(state): State<AppState>,
C:\Users\MikeSwanson\claude-projects\gururmm\server\src\api\agents.rs:130:    State(state): State<AppState>,
C:\Users\MikeSwanson\claude-projects\gururmm\server\src\api\agents.rs:156:    State(state): State<AppState>,
C:\Users\MikeSwanson\claude-projects\gururmm\server\src\api\agents.rs:167:    State(state): State<AppState>,
C:\Users\MikeSwanson\claude-projects\gururmm\server\src\api\agents.rs:179:    State(state): State<AppState>,
C:\Users\MikeSwanson\claude-projects\gururmm\server\src\api\agents.rs:217:    State(state): State<AppState>,
C:\Users\MikeSwanson\claude-projects\gururmm\server\src\api\agents.rs:296:    State(state): State<AppState>,
C:\Users\MikeSwanson\claude-projects\gururmm\server\src\api\agents.rs:325:    State(_state): State<AppState>,
C:\Users\MikeSwanson\claude-projects\gururmm\server\src\api\agents.rs:345:    State(state): State<AppState>,
C:\Users\MikeSwanson\claude-projects\gururmm\server\src\api\clients.rs:13:use crate::AppState;
C:\Users\MikeSwanson\claude-projects\gururmm\server\src\api\clients.rs:47:    State(state): State<AppState>,
C:\Users\MikeSwanson\claude-projects\gururmm\server\src\api\clients.rs:72:    State(state): State<AppState>,
C:\Users\MikeSwanson\claude-projects\gururmm\server\src\api\clients.rs:99:    State(state): State<AppState>,
C:\Users\MikeSwanson\claude-projects\gururmm\server\src\api\clients.rs:126:    State(state): State<AppState>,
C:\Users\MikeSwanson\claude-projects\gururmm\server\src\api\clients.rs:156:    State(state): State<AppState>,
C:\Users\MikeSwanson\claude-projects\gururmm\server\src\ws\mod.rs:26:use crate::AppState;
C:\Users\MikeSwanson\claude-projects\gururmm\server\src\ws\mod.rs:264:pub async fn ws_handler(ws: WebSocketUpgrade, State(state): State<AppState>) -> Response {
C:\Users\MikeSwanson\claude-projects\gururmm\server\src\ws\mod.rs:269:async fn handle_socket(socket: WebSocket, state: AppState) {
C:\Users\MikeSwanson\claude-projects\gururmm\server\src\ws\mod.rs:432:    state: &AppState,
C:\Users\MikeSwanson\claude-projects\gururmm\server\src\ws\mod.rs:558:    state: &AppState,
C:\Users\MikeSwanson\claude-projects\gururmm\server\src\api\auth.rs:12:use crate::AppState;
C:\Users\MikeSwanson\claude-projects\gururmm\server\src\api\auth.rs:46:    State(state): State<AppState>,
C:\Users\MikeSwanson\claude-projects\gururmm\server\src\api\auth.rs:85:    State(state): State<AppState>,
C:\Users\MikeSwanson\claude-projects\gururmm\server\src\api\auth.rs:142:    State(state): State<AppState>,
C:\Users\MikeSwanson\claude-projects\gururmm\server\src\api\mod.rs:24:use crate::AppState;
C:\Users\MikeSwanson\claude-projects\gururmm\server\src\api\mod.rs:27:pub fn routes() -> Router<AppState> {
C:\Users\MikeSwanson\claude-projects\gururmm\server\src\api\sso.rs:16:use crate::AppState;
C:\Users\MikeSwanson\claude-projects\gururmm\server\src\api\sso.rs:33:    State(state): State<AppState>,
C:\Users\MikeSwanson\claude-projects\gururmm\server\src\api\sso.rs:104:    State(state): State<AppState>,
C:\Users\MikeSwanson\claude-projects\gururmm\server\src\api\metrics.rs:13:use crate::AppState;
C:\Users\MikeSwanson\claude-projects\gururmm\server\src\api\metrics.rs:30:    State(state): State<AppState>,
C:\Users\MikeSwanson\claude-projects\gururmm\server\src\api\metrics.rs:58:    State(state): State<AppState>,
C:\Users\MikeSwanson\claude-projects\gururmm\server\src\api\sites.rs:14:use crate::AppState;
C:\Users\MikeSwanson\claude-projects\gururmm\server\src\api\sites.rs:61:    State(state): State<AppState>,
C:\Users\MikeSwanson\claude-projects\gururmm\server\src\api\sites.rs:89:    State(state): State<AppState>,
C:\Users\MikeSwanson\claude-projects\gururmm\server\src\api\sites.rs:118:    State(state): State<AppState>,
C:\Users\MikeSwanson\claude-projects\gururmm\server\src\api\sites.rs:169:    State(state): State<AppState>,
C:\Users\MikeSwanson\claude-projects\gururmm\server\src\api\sites.rs:203:    State(state): State<AppState>,
C:\Users\MikeSwanson\claude-projects\gururmm\server\src\api\sites.rs:242:    State(state): State<AppState>,
C:\Users\MikeSwanson\claude-projects\gururmm\server\src\api\sites.rs:268:    State(state): State<AppState>,
C:\Users\MikeSwanson\claude-projects\gururmm\server\src\api\commands.rs:18:use crate::AppState;
C:\Users\MikeSwanson\claude-projects\gururmm\server\src\api\commands.rs:54:    State(state): State<AppState>,
C:\Users\MikeSwanson\claude-projects\gururmm\server\src\api\commands.rs:121:    State(state): State<AppState>,
C:\Users\MikeSwanson\claude-projects\gururmm\server\src\api\commands.rs:138:    State(state): State<AppState>,
C:\Users\MikeSwanson\claude-projects\gururmm\server\src\api\users.rs:16:use crate::AppState;
C:\Users\MikeSwanson\claude-projects\gururmm\server\src\api\users.rs:20:    State(state): State<AppState>,
C:\Users\MikeSwanson\claude-projects\gururmm\server\src\api\users.rs:51:    State(state): State<AppState>,
C:\Users\MikeSwanson\claude-projects\gururmm\server\src\api\users.rs:108:    State(state): State<AppState>,
C:\Users\MikeSwanson\claude-projects\gururmm\server\src\api\users.rs:127:    State(state): State<AppState>,


     1→//! GuruRMM Server - RMM Management Server
     2→//!
     3→//! Provides the backend API and WebSocket endpoint for GuruRMM agents.
     4→//! Features:
     5→//! - Agent registration and management
     6→//! - Real-time WebSocket communication with agents
     7→//! - Metrics storage and retrieval
     8→//! - Command execution via agents
     9→//! - Dashboard authentication
    10→
    11→mod api;
    12→mod auth;
    13→mod config;
    14→mod db;
    15→mod updates;
    16→mod ws;
    17→
    18→use std::sync::Arc;
    19→
    20→use anyhow::Result;
    21→use axum::{
    22→    routing::{get, post},
    23→    Router,
    24→};
    25→use sqlx::postgres::PgPoolOptions;
    26→use tokio::sync::RwLock;
    27→use tower_http::cors::{Any, CorsLayer};
    28→use tower_http::trace::TraceLayer;
    29→use tracing::info;
    30→
    31→use crate::config::ServerConfig;
    32→use crate::updates::UpdateManager;
    33→use crate::ws::AgentConnections;
    34→
    35→/// Shared application state
    36→#[derive(Clone)]
    37→pub struct AppState {
    38→    /// Database connection pool
    39→    pub db: sqlx::PgPool,
    40→
    41→    /// Server configuration
    42→    pub config: Arc<ServerConfig>,
    43→
    44→    /// Connected agents (WebSocket connections)
    45→    pub agents: Arc<RwLock<AgentConnections>>,
    46→
    47→    /// Agent update manager
    48→    pub updates: Arc<UpdateManager>,
    49→}
    50→
    51→#[tokio::main]
    52→async fn main() -> Result<()> {
    53→    // Load environment variables from .env file
    54→    dotenvy::dotenv().ok();
    55→
    56→    // Initialize logging
    57→    tracing_subscriber::fmt()
    58→        .with_env_filter(
    59→            tracing_subscriber::EnvFilter::from_default_env()
    60→                .add_directive("gururmm_server=info".parse()?)
    61→                .add_directive("tower_http=debug".parse()?)
    62→                .add_directive("info".parse()?),
    63→        )
    64→        .init();
    65→
    66→    info!("GuruRMM Server starting...");
    67→
    68→    // Load configuration
    69→    let config = ServerConfig::from_env()?;
    70→    info!("Server configuration loaded");
    71→
    72→    // Connect to database
    73→    info!("Connecting to database...");
    74→    let db = PgPoolOptions::new()
    75→        .max_connections(config.database.max_connections)
    76→        .connect(&config.database.url)
    77→        .await?;
    78→    info!("Database connected");
    79→
    80→    // Run migrations
    81→    info!("Running database migrations...");
    82→    sqlx::migrate!("./migrations").run(&db).await?;
    83→    info!("Migrations complete");
    84→
    85→    // Initialize update manager
    86→    let update_manager = UpdateManager::new(
    87→        config.updates.downloads_dir.clone(),
    88→        config.updates.downloads_base_url.clone(),
    89→        config.updates.auto_update_enabled,
    90→        config.updates.update_timeout_secs,
    91→    );
    92→
    93→    // Initial scan for available versions
    94→    info!("Scanning for available agent versions...");
    95→    if let Err(e) = update_manager.scan_versions().await {
    96→        tracing::warn!("Failed to scan agent versions: {} (continuing without auto-update)", e);
    97→    }
    98→
    99→    let update_manager = Arc::new(update_manager);
   100→
   101→    // Spawn background scanner (handle is intentionally not awaited - runs until server shutdown)
   102→    let _scanner_handle = update_manager.spawn_scanner(config.updates.scan_interval_secs);
   103→    info!(
   104→        "Auto-update: {} (scan interval: {}s)",
   105→        if config.updates.auto_update_enabled { "enabled" } else { "disabled" },
   106→        config.updates.scan_interval_secs
   107→    );
   108→
   109→    // Create shared state
   110→    let state = AppState {
   111→        db,
   112→        config: Arc::new(config.clone()),
   113→        agents: Arc::new(RwLock::new(AgentConnections::new())),
   114→        updates: update_manager,
   115→    };
   116→
   117→    // Build router
   118→    let app = build_router(state);
   119→
   120→    // Start server
   121→    let addr = format!("{}:{}", config.server.host, config.server.port);
   122→    info!("Starting server on {}", addr);
   123→
   124→    let listener = tokio::net::TcpListener::bind(&addr).await?;
   125→    axum::serve(listener, app).await?;
   126→
   127→    Ok(())
   128→}
   129→
   130→/// Build the application router
   131→fn build_router(state: AppState) -> Router {
   132→    // CORS configuration (allow dashboard access)
   133→    let cors = CorsLayer::new()
   134→        .allow_origin(Any)
   135→        .allow_methods(Any)
   136→        .allow_headers(Any);
   137→
   138→    Router::new()
   139→        // Health check
   140→        .route("/health", get(health_check))
   141→        // WebSocket endpoint for agents
   142→        .route("/ws", get(ws::ws_handler))
   143→        // API routes
   144→        .nest("/api", api::routes())
   145→        // Middleware
   146→        .layer(TraceLayer::new_for_http())
   147→        .layer(cors)
   148→        // State
   149→        .with_state(state)
   150→}
   151→
   152→/// Health check endpoint
   153→async fn health_check() -> &'static str {
   154→    "OK"
   155→}
   156→

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

//! GuruRMM Server - RMM Management Server
//!
//! Provides the backend API and WebSocket endpoint for GuruRMM agents.
//! Features:
//! - Agent registration and management
//! - Real-time WebSocket communication with agents
//! - Metrics storage and retrieval
//! - Command execution via agents
//! - Dashboard authentication

mod api;
mod auth;
mod config;
mod db;
mod updates;
mod ws;

use std::sync::Arc;

use anyhow::Result;
use axum::{
    routing::{get, post},
    Router,
};
use sqlx::postgres::PgPoolOptions;
use tokio::sync::RwLock;
use tower_http::cors::{Any, CorsLayer};
use tower_http::trace::TraceLayer;
use tracing::info;

use crate::config::ServerConfig;
use crate::updates::UpdateManager;
use crate::ws::AgentConnections;

/// Shared application state
#[derive(Clone)]
pub struct AppState {
    /// Database connection pool
    pub db: sqlx::PgPool,

    /// Server configuration
    pub config: Arc<ServerConfig>,

    /// Connected agents (WebSocket connections)
    pub agents: Arc<RwLock<AgentConnections>>,

    /// Agent update manager
    pub updates: Arc<UpdateManager>,
}

#[tokio::main]
async fn main() -> Result<()> {
    // Load environment variables from .env file
    dotenvy::dotenv().ok();

    // Initialize logging
    tracing_subscriber::fmt()
        .with_env_filter(
            tracing_subscriber::EnvFilter::from_default_env()
                .add_directive("gururmm_server=info".parse()?)
                .add_directive("tower_http=debug".parse()?)
                .add_directive("info".parse()?),
        )
        .init();

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

    // Load configuration
    let config = ServerConfig::from_env()?;
    info!("Server configuration loaded");

    // Connect to database
    info!("Connecting to database...");
    let db = PgPoolOptions::new()
        .max_connections(config.database.max_connections)
        .connect(&config.database.url)
        .await?;
    info!("Database connected");

    // Run migrations
    info!("Running database migrations...");
    sqlx::migrate!("./migrations").run(&db).await?;
    info!("Migrations complete");

    // Initialize update manager
    let update_manager = UpdateManager::new(
        config.updates.downloads_dir.clone(),
        config.updates.downloads_base_url.clone(),
        config.updates.auto_update_enabled,
        config.updates.update_timeout_secs,
    );

    // Initial scan for available versions
    info!("Scanning for available agent versions...");
    if let Err(e) = update_manager.scan_versions().await {
        tracing::warn!("Failed to scan agent versions: {} (continuing without auto-update)", e);
    }

    let update_manager = Arc::new(update_manager);

    // Spawn background scanner (handle is intentionally not awaited - runs until server shutdown)
    let _scanner_handle = update_manager.spawn_scanner(config.updates.scan_interval_secs);
    info!(
        "Auto-update: {} (scan interval: {}s)",
        if config.updates.auto_update_enabled { "enabled" } else { "disabled" },
        config.updates.scan_interval_secs
    );

    // Create shared state
    let state = AppState {
        db,
        config: Arc::new(config.clone()),
        agents: Arc::new(RwLock::new(AgentConnections::new())),
        updates: update_manager,
    };

    // Build router
    let app = build_router(state);

    // Start server
    let addr = format!("{}:{}", config.server.host, config.server.port);
    info!("Starting server on {}", addr);

    let listener = tokio::net::TcpListener::bind(&addr).await?;
    axum::serve(listener, app).await?;

    Ok(())
}

/// Build the application router
fn build_router(state: AppState) -> Router {
    // CORS configuration (allow dashboard access)
    let cors = CorsLayer::new()
        .allow_origin(Any)
        .allow_methods(Any)
        .allow_headers(Any);

    Router::new()
        // Health check
        .route("/health", get(health_check))
        // WebSocket endpoint for agents
        .route("/ws", get(ws::ws_handler))
        // API routes
        .nest("/api", api::routes())
        // Middleware
        .layer(TraceLayer::new_for_http())
        .layer(cors)
        // State
        .with_state(state)
}

/// Health check endpoint
async fn health_check() -> &'static str {
    "OK"
}