     1→use std::io::Result;
     2→use std::process::Command;
     3→
     4→fn main() -> Result<()> {
     5→    // Compile protobuf definitions
     6→    prost_build::compile_protos(&["../proto/guruconnect.proto"], &["../proto/"])?;
     7→
     8→    // Rerun if proto changes
     9→    println!("cargo:rerun-if-changed=../proto/guruconnect.proto");
    10→
    11→    // Rerun if git HEAD changes (new commits)
    12→    println!("cargo:rerun-if-changed=../.git/HEAD");
    13→    println!("cargo:rerun-if-changed=../.git/index");
    14→
    15→    // Build timestamp (UTC)
    16→    let build_timestamp = chrono::Utc::now().format("%Y-%m-%d %H:%M:%S UTC").to_string();
    17→    println!("cargo:rustc-env=BUILD_TIMESTAMP={}", build_timestamp);
    18→
    19→    // Git commit hash (short)
    20→    let git_hash = Command::new("git")
    21→        .args(["rev-parse", "--short=8", "HEAD"])
    22→        .output()
    23→        .ok()
    24→        .and_then(|o| String::from_utf8(o.stdout).ok())
    25→        .map(|s| s.trim().to_string())
    26→        .unwrap_or_else(|| "unknown".to_string());
    27→    println!("cargo:rustc-env=GIT_HASH={}", git_hash);
    28→
    29→    // Git commit hash (full)
    30→    let git_hash_full = Command::new("git")
    31→        .args(["rev-parse", "HEAD"])
    32→        .output()
    33→        .ok()
    34→        .and_then(|o| String::from_utf8(o.stdout).ok())
    35→        .map(|s| s.trim().to_string())
    36→        .unwrap_or_else(|| "unknown".to_string());
    37→    println!("cargo:rustc-env=GIT_HASH_FULL={}", git_hash_full);
    38→
    39→    // Git branch name
    40→    let git_branch = Command::new("git")
    41→        .args(["rev-parse", "--abbrev-ref", "HEAD"])
    42→        .output()
    43→        .ok()
    44→        .and_then(|o| String::from_utf8(o.stdout).ok())
    45→        .map(|s| s.trim().to_string())
    46→        .unwrap_or_else(|| "unknown".to_string());
    47→    println!("cargo:rustc-env=GIT_BRANCH={}", git_branch);
    48→
    49→    // Git dirty state (uncommitted changes)
    50→    let git_dirty = Command::new("git")
    51→        .args(["status", "--porcelain"])
    52→        .output()
    53→        .ok()
    54→        .map(|o| !o.stdout.is_empty())
    55→        .unwrap_or(false);
    56→    println!("cargo:rustc-env=GIT_DIRTY={}", if git_dirty { "dirty" } else { "clean" });
    57→
    58→    // Git commit date
    59→    let git_commit_date = Command::new("git")
    60→        .args(["log", "-1", "--format=%ci"])
    61→        .output()
    62→        .ok()
    63→        .and_then(|o| String::from_utf8(o.stdout).ok())
    64→        .map(|s| s.trim().to_string())
    65→        .unwrap_or_else(|| "unknown".to_string());
    66→    println!("cargo:rustc-env=GIT_COMMIT_DATE={}", git_commit_date);
    67→
    68→    // Build profile (debug/release)
    69→    let profile = std::env::var("PROFILE").unwrap_or_else(|_| "unknown".to_string());
    70→    println!("cargo:rustc-env=BUILD_PROFILE={}", profile);
    71→
    72→    // Target triple
    73→    let target = std::env::var("TARGET").unwrap_or_else(|_| "unknown".to_string());
    74→    println!("cargo:rustc-env=BUILD_TARGET={}", target);
    75→
    76→    // On Windows, embed the manifest for UAC elevation
    77→    #[cfg(target_os = "windows")]
    78→    {
    79→        println!("cargo:rerun-if-changed=guruconnect.manifest");
    80→
    81→        let mut res = winres::WindowsResource::new();
    82→        res.set_manifest_file("guruconnect.manifest");
    83→        res.set("ProductName", "GuruConnect Agent");
    84→        res.set("FileDescription", "GuruConnect Remote Desktop Agent");
    85→        res.set("LegalCopyright", "Copyright (c) AZ Computer Guru");
    86→        res.set_icon("guruconnect.ico"); // Optional: add icon if available
    87→
    88→        // Only compile if the manifest exists
    89→        if std::path::Path::new("guruconnect.manifest").exists() {
    90→            if let Err(e) = res.compile() {
    91→                // Don't fail the build if resource compilation fails
    92→                eprintln!("Warning: Failed to compile Windows resources: {}", e);
    93→            }
    94→        }
    95→    }
    96→
    97→    Ok(())
    98→}
    99→

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

use std::io::Result;
use std::process::Command;

fn main() -> Result<()> {
    // Compile protobuf definitions
    prost_build::compile_protos(&["../proto/guruconnect.proto"], &["../proto/"])?;

    // Rerun if proto changes
    println!("cargo:rerun-if-changed=../proto/guruconnect.proto");

    // Rerun if git HEAD changes (new commits)
    println!("cargo:rerun-if-changed=../.git/HEAD");
    println!("cargo:rerun-if-changed=../.git/index");

    // Build timestamp (UTC)
    let build_timestamp = chrono::Utc::now().format("%Y-%m-%d %H:%M:%S UTC").to_string();
    println!("cargo:rustc-env=BUILD_TIMESTAMP={}", build_timestamp);

    // Git commit hash (short)
    let git_hash = Command::new("git")
        .args(["rev-parse", "--short=8", "HEAD"])
        .output()
        .ok()
        .and_then(|o| String::from_utf8(o.stdout).ok())
        .map(|s| s.trim().to_string())
        .unwrap_or_else(|| "unknown".to_string());
    println!("cargo:rustc-env=GIT_HASH={}", git_hash);

    // Git commit hash (full)
    let git_hash_full = Command::new("git")
        .args(["rev-parse", "HEAD"])
        .output()
        .ok()
        .and_then(|o| String::from_utf8(o.stdout).ok())
        .map(|s| s.trim().to_string())
        .unwrap_or_else(|| "unknown".to_string());
    println!("cargo:rustc-env=GIT_HASH_FULL={}", git_hash_full);

    // Git branch name
    let git_branch = Command::new("git")
        .args(["rev-parse", "--abbrev-ref", "HEAD"])
        .output()
        .ok()
        .and_then(|o| String::from_utf8(o.stdout).ok())
        .map(|s| s.trim().to_string())
        .unwrap_or_else(|| "unknown".to_string());
    println!("cargo:rustc-env=GIT_BRANCH={}", git_branch);

    // Git dirty state (uncommitted changes)
    let git_dirty = Command::new("git")
        .args(["status", "--porcelain"])
        .output()
        .ok()
        .map(|o| !o.stdout.is_empty())
        .unwrap_or(false);
    println!("cargo:rustc-env=GIT_DIRTY={}", if git_dirty { "dirty" } else { "clean" });

    // Git commit date
    let git_commit_date = Command::new("git")
        .args(["log", "-1", "--format=%ci"])
        .output()
        .ok()
        .and_then(|o| String::from_utf8(o.stdout).ok())
        .map(|s| s.trim().to_string())
        .unwrap_or_else(|| "unknown".to_string());
    println!("cargo:rustc-env=GIT_COMMIT_DATE={}", git_commit_date);

    // Build profile (debug/release)
    let profile = std::env::var("PROFILE").unwrap_or_else(|_| "unknown".to_string());
    println!("cargo:rustc-env=BUILD_PROFILE={}", profile);

    // Target triple
    let target = std::env::var("TARGET").unwrap_or_else(|_| "unknown".to_string());
    println!("cargo:rustc-env=BUILD_TARGET={}", target);

    // On Windows, embed the manifest for UAC elevation
    #[cfg(target_os = "windows")]
    {
        println!("cargo:rerun-if-changed=guruconnect.manifest");

        let mut res = winres::WindowsResource::new();
        res.set_manifest_file("guruconnect.manifest");
        res.set("ProductName", "GuruConnect Agent");
        res.set("FileDescription", "GuruConnect Remote Desktop Agent");
        res.set("LegalCopyright", "Copyright (c) AZ Computer Guru");
        res.set_icon("guruconnect.ico"); // Optional: add icon if available

        // Only compile if the manifest exists
        if std::path::Path::new("guruconnect.manifest").exists() {
            if let Err(e) = res.compile() {
                // Don't fail the build if resource compilation fails
                eprintln!("Warning: Failed to compile Windows resources: {}", e);
            }
        }
    }

    Ok(())
}


Let me read the Cargo.toml to see the current version:


     1→[package]
     2→name = "guruconnect"
     3→version = "0.1.0"
     4→edition = "2021"
     5→authors = ["AZ Computer Guru"]
     6→description = "GuruConnect Remote Desktop - Agent and Viewer"
     7→
     8→[dependencies]
     9→# CLI
    10→clap = { version = "4", features = ["derive"] }
    11→
    12→# Async runtime
    13→tokio = { version = "1", features = ["full", "sync", "time", "rt-multi-thread", "macros"] }
    14→
    15→# WebSocket
    16→tokio-tungstenite = { version = "0.24", features = ["native-tls"] }
    17→futures-util = "0.3"
    18→
    19→# Windowing (for viewer)
    20→winit = { version = "0.30", features = ["rwh_06"] }
    21→softbuffer = "0.4"
    22→raw-window-handle = "0.6"
    23→
    24→# Compression
    25→zstd = "0.13"
    26→
    27→# Protocol (protobuf)
    28→prost = "0.13"
    29→prost-types = "0.13"
    30→bytes = "1"
    31→
    32→# Serialization
    33→serde = { version = "1", features = ["derive"] }
    34→serde_json = "1"
    35→
    36→# Logging
    37→tracing = "0.1"
    38→tracing-subscriber = { version = "0.3", features = ["env-filter"] }
    39→
    40→# Error handling
    41→anyhow = "1"
    42→thiserror = "1"
    43→
    44→# Configuration
    45→toml = "0.8"
    46→
    47→# Crypto
    48→ring = "0.17"
    49→
    50→# UUID
    51→uuid = { version = "1", features = ["v4", "serde"] }
    52→
    53→# Time
    54→chrono = { version = "0.4", features = ["serde"] }
    55→
    56→# Hostname
    57→hostname = "0.4"
    58→
    59→# URL encoding
    60→urlencoding = "2"
    61→
    62→# System tray (Windows)
    63→tray-icon = "0.19"
    64→muda = "0.15"  # Menu for tray icon
    65→
    66→# Image handling for tray icon
    67→image = { version = "0.25", default-features = false, features = ["png"] }
    68→
    69→# URL parsing
    70→url = "2"
    71→
    72→[target.'cfg(windows)'.dependencies]
    73→# Windows APIs for screen capture, input, and shell operations
    74→windows = { version = "0.58", features = [
    75→    "Win32_Foundation",
    76→    "Win32_Graphics_Gdi",
    77→    "Win32_Graphics_Dxgi",
    78→    "Win32_Graphics_Dxgi_Common",
    79→    "Win32_Graphics_Direct3D",
    80→    "Win32_Graphics_Direct3D11",
    81→    "Win32_UI_Input_KeyboardAndMouse",
    82→    "Win32_UI_WindowsAndMessaging",
    83→    "Win32_UI_Shell",
    84→    "Win32_System_LibraryLoader",
    85→    "Win32_System_Threading",
    86→    "Win32_System_Registry",
    87→    "Win32_System_Console",
    88→    "Win32_System_Environment",
    89→    "Win32_Security",
    90→    "Win32_Storage_FileSystem",
    91→    "Win32_System_Pipes",
    92→    "Win32_System_SystemServices",
    93→    "Win32_System_IO",
    94→]}
    95→
    96→# Windows service support
    97→windows-service = "0.7"
    98→
    99→[build-dependencies]
   100→prost-build = "0.13"
   101→winres = "0.1"
   102→chrono = "0.4"
   103→
   104→[[bin]]
   105→name = "guruconnect"
   106→path = "src/main.rs"
   107→
   108→[[bin]]
   109→name = "guruconnect-sas-service"
   110→path = "src/bin/sas_service.rs"
   111→

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

[package]
name = "guruconnect"
version = "0.1.0"
edition = "2021"
authors = ["AZ Computer Guru"]
description = "GuruConnect Remote Desktop - Agent and Viewer"

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

# Async runtime
tokio = { version = "1", features = ["full", "sync", "time", "rt-multi-thread", "macros"] }

# WebSocket
tokio-tungstenite = { version = "0.24", features = ["native-tls"] }
futures-util = "0.3"

# Windowing (for viewer)
winit = { version = "0.30", features = ["rwh_06"] }
softbuffer = "0.4"
raw-window-handle = "0.6"

# Compression
zstd = "0.13"

# Protocol (protobuf)
prost = "0.13"
prost-types = "0.13"
bytes = "1"

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

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

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

# Configuration
toml = "0.8"

# Crypto
ring = "0.17"

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

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

# Hostname
hostname = "0.4"

# URL encoding
urlencoding = "2"

# System tray (Windows)
tray-icon = "0.19"
muda = "0.15"  # Menu for tray icon

# Image handling for tray icon
image = { version = "0.25", default-features = false, features = ["png"] }

# URL parsing
url = "2"

[target.'cfg(windows)'.dependencies]
# Windows APIs for screen capture, input, and shell operations
windows = { version = "0.58", features = [
    "Win32_Foundation",
    "Win32_Graphics_Gdi",
    "Win32_Graphics_Dxgi",
    "Win32_Graphics_Dxgi_Common",
    "Win32_Graphics_Direct3D",
    "Win32_Graphics_Direct3D11",
    "Win32_UI_Input_KeyboardAndMouse",
    "Win32_UI_WindowsAndMessaging",
    "Win32_UI_Shell",
    "Win32_System_LibraryLoader",
    "Win32_System_Threading",
    "Win32_System_Registry",
    "Win32_System_Console",
    "Win32_System_Environment",
    "Win32_Security",
    "Win32_Storage_FileSystem",
    "Win32_System_Pipes",
    "Win32_System_SystemServices",
    "Win32_System_IO",
]}

# Windows service support
windows-service = "0.7"

[build-dependencies]
prost-build = "0.13"
winres = "0.1"
chrono = "0.4"

[[bin]]
name = "guruconnect"
path = "src/main.rs"

[[bin]]
name = "guruconnect-sas-service"
path = "src/bin/sas_service.rs"


     1→//! GuruConnect Server - WebSocket Relay Server
     2→//!
     3→//! Handles connections from both agents and dashboard viewers,
     4→//! relaying video frames and input events between them.
     5→
     6→mod config;
     7→mod relay;
     8→mod session;
     9→mod auth;
    10→mod api;
    11→mod db;
    12→mod support_codes;
    13→
    14→pub mod proto {
    15→    include!(concat!(env!("OUT_DIR"), "/guruconnect.rs"));
    16→}
    17→
    18→use anyhow::Result;
    19→use axum::{
    20→    Router,
    21→    routing::{get, post, put, delete},
    22→    extract::{Path, State, Json, Query, Request},
    23→    response::{Html, IntoResponse},
    24→    http::StatusCode,
    25→    middleware::{self, Next},
    26→};
    27→use std::net::SocketAddr;
    28→use std::sync::Arc;
    29→use tower_http::cors::{Any, CorsLayer};
    30→use tower_http::trace::TraceLayer;
    31→use tower_http::services::ServeDir;
    32→use tracing::{info, Level};
    33→use tracing_subscriber::FmtSubscriber;
    34→use serde::Deserialize;
    35→
    36→use support_codes::{SupportCodeManager, CreateCodeRequest, SupportCode, CodeValidation};
    37→use auth::{JwtConfig, hash_password, generate_random_password, AuthenticatedUser};
    38→
    39→/// Application state
    40→#[derive(Clone)]
    41→pub struct AppState {
    42→    sessions: session::SessionManager,
    43→    support_codes: SupportCodeManager,
    44→    db: Option<db::Database>,
    45→    pub jwt_config: Arc<JwtConfig>,
    46→    /// Optional API key for persistent agents (env: AGENT_API_KEY)
    47→    pub agent_api_key: Option<String>,
    48→}
    49→
    50→/// Middleware to inject JWT config into request extensions
    51→async fn auth_layer(
    52→    State(state): State<AppState>,
    53→    mut request: Request,
    54→    next: Next,
    55→) -> impl IntoResponse {
    56→    request.extensions_mut().insert(state.jwt_config.clone());
    57→    next.run(request).await
    58→}
    59→
    60→#[tokio::main]
    61→async fn main() -> Result<()> {
    62→    // Initialize logging
    63→    let _subscriber = FmtSubscriber::builder()
    64→        .with_max_level(Level::INFO)
    65→        .with_target(true)
    66→        .init();
    67→
    68→    info!("GuruConnect Server v{}", env!("CARGO_PKG_VERSION"));
    69→
    70→    // Load configuration
    71→    let config = config::Config::load()?;
    72→
    73→    // Use port 3002 for GuruConnect
    74→    let listen_addr = std::env::var("LISTEN_ADDR").unwrap_or_else(|_| "0.0.0.0:3002".to_string());
    75→    info!("Loaded configuration, listening on {}", listen_addr);
    76→
    77→    // JWT configuration
    78→    let jwt_secret = std::env::var("JWT_SECRET").unwrap_or_else(|_| {
    79→        tracing::warn!("JWT_SECRET not set, using default (INSECURE for production!)");
    80→        "guruconnect-dev-secret-change-me-in-production".to_string()
    81→    });
    82→    let jwt_expiry_hours = std::env::var("JWT_EXPIRY_HOURS")
    83→        .ok()
    84→        .and_then(|s| s.parse().ok())
    85→        .unwrap_or(24i64);
    86→    let jwt_config = Arc::new(JwtConfig::new(jwt_secret, jwt_expiry_hours));
    87→
    88→    // Initialize database if configured
    89→    let database = if let Some(ref db_url) = config.database_url {
    90→        match db::Database::connect(db_url, config.database_max_connections).await {
    91→            Ok(db) => {
    92→                // Run migrations
    93→                if let Err(e) = db.migrate().await {
    94→                    tracing::error!("Failed to run migrations: {}", e);
    95→                    return Err(e);
    96→                }
    97→                Some(db)
    98→            }
    99→            Err(e) => {
   100→                tracing::warn!("Failed to connect to database: {}. Running without persistence.", e);

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

//! GuruConnect Server - WebSocket Relay Server
//!
//! Handles connections from both agents and dashboard viewers,
//! relaying video frames and input events between them.

mod config;
mod relay;
mod session;
mod auth;
mod api;
mod db;
mod support_codes;

pub mod proto {
    include!(concat!(env!("OUT_DIR"), "/guruconnect.rs"));
}

use anyhow::Result;
use axum::{
    Router,
    routing::{get, post, put, delete},
    extract::{Path, State, Json, Query, Request},
    response::{Html, IntoResponse},
    http::StatusCode,
    middleware::{self, Next},
};
use std::net::SocketAddr;
use std::sync::Arc;
use tower_http::cors::{Any, CorsLayer};
use tower_http::trace::TraceLayer;
use tower_http::services::ServeDir;
use tracing::{info, Level};
use tracing_subscriber::FmtSubscriber;
use serde::Deserialize;

use support_codes::{SupportCodeManager, CreateCodeRequest, SupportCode, CodeValidation};
use auth::{JwtConfig, hash_password, generate_random_password, AuthenticatedUser};

/// Application state
#[derive(Clone)]
pub struct AppState {
    sessions: session::SessionManager,
    support_codes: SupportCodeManager,
    db: Option<db::Database>,
    pub jwt_config: Arc<JwtConfig>,
    /// Optional API key for persistent agents (env: AGENT_API_KEY)
    pub agent_api_key: Option<String>,
}

/// Middleware to inject JWT config into request extensions
async fn auth_layer(
    State(state): State<AppState>,
    mut request: Request,
    next: Next,
) -> impl IntoResponse {
    request.extensions_mut().insert(state.jwt_config.clone());
    next.run(request).await
}

#[tokio::main]
async fn main() -> Result<()> {
    // Initialize logging
    let _subscriber = FmtSubscriber::builder()
        .with_max_level(Level::INFO)
        .with_target(true)
        .init();

    info!("GuruConnect Server v{}", env!("CARGO_PKG_VERSION"));

    // Load configuration
    let config = config::Config::load()?;

    // Use port 3002 for GuruConnect
    let listen_addr = std::env::var("LISTEN_ADDR").unwrap_or_else(|_| "0.0.0.0:3002".to_string());
    info!("Loaded configuration, listening on {}", listen_addr);

    // JWT configuration
    let jwt_secret = std::env::var("JWT_SECRET").unwrap_or_else(|_| {
        tracing::warn!("JWT_SECRET not set, using default (INSECURE for production!)");
        "guruconnect-dev-secret-change-me-in-production".to_string()
    });
    let jwt_expiry_hours = std::env::var("JWT_EXPIRY_HOURS")
        .ok()
        .and_then(|s| s.parse().ok())
        .unwrap_or(24i64);
    let jwt_config = Arc::new(JwtConfig::new(jwt_secret, jwt_expiry_hours));

    // Initialize database if configured
    let database = if let Some(ref db_url) = config.database_url {
        match db::Database::connect(db_url, config.database_max_connections).await {
            Ok(db) => {
                // Run migrations
                if let Err(e) = db.migrate().await {
                    tracing::error!("Failed to run migrations: {}", e);
                    return Err(e);
                }
                Some(db)
            }
            Err(e) => {
                tracing::warn!("Failed to connect to database: {}. Running without persistence.", e);


Perfect! Now let me check how the admin commands are handled in the server to understand the server side:


Bash

find "C:\Users\MikeSwanson\Claude\guru-connect\server\src" -name "*.rs" -type f | head -20

List server source files


No files found


Let me check the relay module to see how messages are handled:


Get line count of relay module


     1→//! WebSocket relay handlers
     2→//!
     3→//! Handles WebSocket connections from agents and viewers,
     4→//! relaying video frames and input events between them.
     5→
     6→use axum::{
     7→    extract::{
     8→        ws::{Message, WebSocket, WebSocketUpgrade},
     9→        Query, State,
    10→    },
    11→    response::IntoResponse,
    12→    http::StatusCode,
    13→};
    14→use futures_util::{SinkExt, StreamExt};
    15→use prost::Message as ProstMessage;
    16→use serde::Deserialize;
    17→use tracing::{error, info, warn};
    18→use uuid::Uuid;
    19→
    20→use crate::proto;
    21→use crate::session::SessionManager;
    22→use crate::db::{self, Database};
    23→use crate::AppState;
    24→
    25→#[derive(Debug, Deserialize)]
    26→pub struct AgentParams {
    27→    agent_id: String,
    28→    #[serde(default)]
    29→    agent_name: Option<String>,
    30→    #[serde(default)]
    31→    support_code: Option<String>,
    32→    #[serde(default)]
    33→    hostname: Option<String>,
    34→    /// API key for persistent (managed) agents
    35→    #[serde(default)]
    36→    api_key: Option<String>,
    37→}
    38→
    39→#[derive(Debug, Deserialize)]
    40→pub struct ViewerParams {
    41→    session_id: String,
    42→    #[serde(default = "default_viewer_name")]
    43→    viewer_name: String,
    44→    /// JWT token for authentication (required)
    45→    #[serde(default)]
    46→    token: Option<String>,
    47→}
    48→
    49→fn default_viewer_name() -> String {
    50→    "Technician".to_string()
    51→}
    52→
    53→/// WebSocket handler for agent connections
    54→pub async fn agent_ws_handler(
    55→    ws: WebSocketUpgrade,
    56→    State(state): State<AppState>,
    57→    Query(params): Query<AgentParams>,
    58→) -> Result<impl IntoResponse, StatusCode> {
    59→    let agent_id = params.agent_id.clone();
    60→    let agent_name = params.hostname.clone().or(params.agent_name.clone()).unwrap_or_else(|| agent_id.clone());
    61→    let support_code = params.support_code.clone();
    62→    let api_key = params.api_key.clone();
    63→
    64→    // SECURITY: Agent must provide either a support code OR an API key
    65→    // Support code = ad-hoc support session (technician generated code)
    66→    // API key = persistent managed agent
    67→
    68→    if support_code.is_none() && api_key.is_none() {
    69→        warn!("Agent connection rejected: {} - no support code or API key", agent_id);
    70→        return Err(StatusCode::UNAUTHORIZED);
    71→    }
    72→
    73→    // Validate support code if provided
    74→    if let Some(ref code) = support_code {
    75→        // Check if it's a valid, pending support code
    76→        let code_info = state.support_codes.get_status(code).await;
    77→        if code_info.is_none() {
    78→            warn!("Agent connection rejected: {} - invalid support code {}", agent_id, code);
    79→            return Err(StatusCode::UNAUTHORIZED);
    80→        }
    81→        let status = code_info.unwrap();
    82→        if status != "pending" && status != "connected" {
    83→            warn!("Agent connection rejected: {} - support code {} has status {}", agent_id, code, status);
    84→            return Err(StatusCode::UNAUTHORIZED);
    85→        }
    86→        info!("Agent {} authenticated via support code {}", agent_id, code);
    87→    }
    88→
    89→    // Validate API key if provided (for persistent agents)
    90→    if let Some(ref key) = api_key {
    91→        // For now, we'll accept API keys that match the JWT secret or a configured agent key
    92→        // In production, this should validate against a database of registered agents
    93→        if !validate_agent_api_key(&state, key).await {
    94→            warn!("Agent connection rejected: {} - invalid API key", agent_id);
    95→            return Err(StatusCode::UNAUTHORIZED);
    96→        }
    97→        info!("Agent {} authenticated via API key", agent_id);
    98→    }
    99→
   100→    let sessions = state.sessions.clone();
   101→    let support_codes = state.support_codes.clone();
   102→    let db = state.db.clone();
   103→
   104→    Ok(ws.on_upgrade(move |socket| handle_agent_connection(socket, sessions, support_codes, db, agent_id, agent_name, support_code)))
   105→}
   106→
   107→/// Validate an agent API key
   108→async fn validate_agent_api_key(state: &AppState, api_key: &str) -> bool {
   109→    // Check if API key is a valid JWT (allows using dashboard token for testing)
   110→    if state.jwt_config.validate_token(api_key).is_ok() {
   111→        return true;
   112→    }
   113→
   114→    // Check against configured agent API key if set
   115→    if let Some(ref configured_key) = state.agent_api_key {
   116→        if api_key == configured_key {
   117→            return true;
   118→        }
   119→    }
   120→
   121→    // In future: validate against database of registered agents
   122→    false
   123→}
   124→
   125→/// WebSocket handler for viewer connections
   126→pub async fn viewer_ws_handler(
   127→    ws: WebSocketUpgrade,
   128→    State(state): State<AppState>,
   129→    Query(params): Query<ViewerParams>,
   130→) -> Result<impl IntoResponse, StatusCode> {
   131→    // Require JWT token for viewers
   132→    let token = params.token.ok_or_else(|| {
   133→        warn!("Viewer connection rejected: missing token");
   134→        StatusCode::UNAUTHORIZED
   135→    })?;
   136→
   137→    // Validate the token
   138→    let claims = state.jwt_config.validate_token(&token).map_err(|e| {
   139→        warn!("Viewer connection rejected: invalid token: {}", e);
   140→        StatusCode::UNAUTHORIZED
   141→    })?;
   142→
   143→    info!("Viewer {} authenticated via JWT", claims.username);
   144→
   145→    let session_id = params.session_id;
   146→    let viewer_name = params.viewer_name;
   147→    let sessions = state.sessions.clone();
   148→    let db = state.db.clone();
   149→
   150→    Ok(ws.on_upgrade(move |socket| handle_viewer_connection(socket, sessions, db, session_id, viewer_name)))

<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 relay handlers
//!
//! Handles WebSocket connections from agents and viewers,
//! relaying video frames and input events between them.

use axum::{
    extract::{
        ws::{Message, WebSocket, WebSocketUpgrade},
        Query, State,
    },
    response::IntoResponse,
    http::StatusCode,
};
use futures_util::{SinkExt, StreamExt};
use prost::Message as ProstMessage;
use serde::Deserialize;
use tracing::{error, info, warn};
use uuid::Uuid;

use crate::proto;
use crate::session::SessionManager;
use crate::db::{self, Database};
use crate::AppState;

#[derive(Debug, Deserialize)]
pub struct AgentParams {
    agent_id: String,
    #[serde(default)]
    agent_name: Option<String>,
    #[serde(default)]
    support_code: Option<String>,
    #[serde(default)]
    hostname: Option<String>,
    /// API key for persistent (managed) agents
    #[serde(default)]
    api_key: Option<String>,
}

#[derive(Debug, Deserialize)]
pub struct ViewerParams {
    session_id: String,
    #[serde(default = "default_viewer_name")]
    viewer_name: String,
    /// JWT token for authentication (required)
    #[serde(default)]
    token: Option<String>,
}

fn default_viewer_name() -> String {
    "Technician".to_string()
}

/// WebSocket handler for agent connections
pub async fn agent_ws_handler(
    ws: WebSocketUpgrade,
    State(state): State<AppState>,
    Query(params): Query<AgentParams>,
) -> Result<impl IntoResponse, StatusCode> {
    let agent_id = params.agent_id.clone();
    let agent_name = params.hostname.clone().or(params.agent_name.clone()).unwrap_or_else(|| agent_id.clone());
    let support_code = params.support_code.clone();
    let api_key = params.api_key.clone();

    // SECURITY: Agent must provide either a support code OR an API key
    // Support code = ad-hoc support session (technician generated code)
    // API key = persistent managed agent

    if support_code.is_none() && api_key.is_none() {
        warn!("Agent connection rejected: {} - no support code or API key", agent_id);
        return Err(StatusCode::UNAUTHORIZED);
    }

    // Validate support code if provided
    if let Some(ref code) = support_code {
        // Check if it's a valid, pending support code
        let code_info = state.support_codes.get_status(code).await;
        if code_info.is_none() {
            warn!("Agent connection rejected: {} - invalid support code {}", agent_id, code);
            return Err(StatusCode::UNAUTHORIZED);
        }
        let status = code_info.unwrap();
        if status != "pending" && status != "connected" {
            warn!("Agent connection rejected: {} - support code {} has status {}", agent_id, code, status);
            return Err(StatusCode::UNAUTHORIZED);
        }
        info!("Agent {} authenticated via support code {}", agent_id, code);
    }

    // Validate API key if provided (for persistent agents)
    if let Some(ref key) = api_key {
        // For now, we'll accept API keys that match the JWT secret or a configured agent key
        // In production, this should validate against a database of registered agents
        if !validate_agent_api_key(&state, key).await {
            warn!("Agent connection rejected: {} - invalid API key", agent_id);
            return Err(StatusCode::UNAUTHORIZED);
        }
        info!("Agent {} authenticated via API key", agent_id);
    }

    let sessions = state.sessions.clone();
    let support_codes = state.support_codes.clone();
    let db = state.db.clone();

    Ok(ws.on_upgrade(move |socket| handle_agent_connection(socket, sessions, support_codes, db, agent_id, agent_name, support_code)))
}

/// Validate an agent API key
async fn validate_agent_api_key(state: &AppState, api_key: &str) -> bool {
    // Check if API key is a valid JWT (allows using dashboard token for testing)
    if state.jwt_config.validate_token(api_key).is_ok() {
        return true;
    }

    // Check against configured agent API key if set
    if let Some(ref configured_key) = state.agent_api_key {
        if api_key == configured_key {
            return true;
        }
    }

    // In future: validate against database of registered agents
    false
}

/// WebSocket handler for viewer connections
pub async fn viewer_ws_handler(
    ws: WebSocketUpgrade,
    State(state): State<AppState>,
    Query(params): Query<ViewerParams>,
) -> Result<impl IntoResponse, StatusCode> {
    // Require JWT token for viewers
    let token = params.token.ok_or_else(|| {
        warn!("Viewer connection rejected: missing token");
        StatusCode::UNAUTHORIZED
    })?;

    // Validate the token
    let claims = state.jwt_config.validate_token(&token).map_err(|e| {
        warn!("Viewer connection rejected: invalid token: {}", e);
        StatusCode::UNAUTHORIZED
    })?;

    info!("Viewer {} authenticated via JWT", claims.username);

    let session_id = params.session_id;
    let viewer_name = params.viewer_name;
    let sessions = state.sessions.clone();
    let db = state.db.clone();

    Ok(ws.on_upgrade(move |socket| handle_viewer_connection(socket, sessions, db, session_id, viewer_name)))


508 C:\Users\MikeSwanson\Claude\guru-connect\server\src\relay\mod.rs


Perfect! Now I have a comprehensive understanding. Let me create a detailed analysis report of the GuruConnect codebase regarding auto-update implementation.

## GuruConnect Agent Auto-Update Implementation Analysis

Based on my exploration of the GuruConnect codebase, here's a comprehensive analysis of how auto-update could be implemented:

---

### 1. CURRENT VERSION/BUILD INFO SYSTEM

**File:** `agent/src/main.rs` (lines 32-90)

The agent already has an excellent compile-time build info system:

```rust
pub mod build_info {
    pub const VERSION: &str = env!("CARGO_PKG_VERSION");
    pub const GIT_HASH: &str = env!("GIT_HASH");
    pub const GIT_HASH_FULL: &str = env!("GIT_HASH_FULL");
    pub const GIT_BRANCH: &str = env!("GIT_BRANCH");
    pub const GIT_DIRTY: &str = env!("GIT_DIRTY");
    pub const GIT_COMMIT_DATE: &str = env!("GIT_COMMIT_DATE");
    pub const BUILD_TIMESTAMP: &str = env!("BUILD_TIMESTAMP");
    pub const BUILD_PROFILE: &str = env!("BUILD_PROFILE");
    pub const BUILD_TARGET: &str = env!("BUILD_TARGET");
}
```

**Source:** `agent/build.rs` embeds this info at compile time via git commands.

**Current version:** `0.1.0` (from Cargo.toml)

**Version string format:**
- Short: `0.1.0-48076e1` (version-gitshort)
- Full: Includes branch, commit date, timestamp, profile, target

**Available command:**
```
guruconnect version-info  # Shows detailed build info
```

---

### 2. TRANSPORT LAYER ANALYSIS

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

The agent uses WebSocket over TLS (wss://) with:
- **Connection:** Connects to `wss://connect.azcomputerguru.com/ws/agent`
- **Authentication:** Sends `agent_id`, `api_key`, `hostname`, optional `support_code`
- **Serialization:** Protobuf binary format
- **Send/Receive:** `send()` and `recv()` methods for Message objects

**Key capabilities:**
- Non-blocking `try_recv()` for polling messages
- Async `recv()` for blocking reads
- Automatic message buffering
- Connection state tracking

---

### 3. PROTOCOL INFRASTRUCTURE FOR UPDATES

**File:** `proto/guruconnect.proto` (lines 282-291)

An update command already exists in the protocol:

```protobuf
message AdminCommand {
    AdminCommandType command = 1;
    string reason = 2;              // Why the command was issued
}

enum AdminCommandType {
    ADMIN_UNINSTALL = 0;            // Uninstall agent and remove from startup
    ADMIN_RESTART = 1;              // Restart the agent process
    ADMIN_UPDATE = 2;               // Download and install update (future)
}
```

**Current status:** `ADMIN_UPDATE = 2` is defined but **not implemented** in the agent.

**Session module handling:** `agent/src/session/mod.rs` (lines 455-477) shows that:
- `AdminCommandType::AdminUninstall` is fully implemented (uninstall + remove from startup)
- `AdminCommandType::AdminRestart` is partially implemented (disconnects, reconnects)
- `AdminCommandType::AdminUpdate` has a TODO placeholder:
  ```rust
  Some(AdminCommandType::AdminUpdate) => {
      tracing::info!("Update command received (not implemented)");
  }
  ```

---

### 4. INSTALLATION/FILE REPLACEMENT SYSTEM

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

**Current install locations:**
- **System-wide (elevated):** `C:\Program Files\GuruConnect\guruconnect.exe`
- **User-only (non-elevated):** `%LOCALAPPDATA%\GuruConnect\guruconnect.exe`

**How installation works:**
1. Self-copies from current exe to install location (lines 284-289)
2. Registers protocol handler (`guruconnect://`) in registry
3. Can detect existing installations and skip copying

**Critical for updates:** `get_install_path(elevated)` provides the path structure.

---

### 5. RESTART/PERSISTENCE SYSTEM

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

**Windows persistence:**
- Adds to registry: `HKCU\Software\Microsoft\Windows\CurrentVersion\Run`
- Stores full executable path
- Agent automatically adds itself on startup (main.rs, line 431)

**Uninstall mechanism:**
- `MoveFileExW` with `MOVEFILE_DELAY_UNTIL_REBOOT` schedules deletion after reboot
- This solves the **running .exe replacement problem** - can't replace a running executable directly, so it's scheduled for deletion on reboot

**For auto-update:** This registry startup entry ensures the new binary will run on restart.

---

### 6. SESSION/MESSAGE HANDLING LOOP

**File:** `agent/src/session/mod.rs` (lines 205-401)

The agent has two operational modes:

**Idle Mode (lines 334-351):**
- Sends heartbeats every 30 seconds
- Sends status every 60 seconds
- Minimal resource usage
- Perfect for checking update availability

**Streaming Mode (lines 353-378):**
- Continuously capturing and sending video frames
- Challenge: Can't silently replace the binary while streaming
- Update should gracefully stop streaming and restart

**Message processing flow:**
1. Poll for messages: `transport.try_recv()?`
2. Handle special messages (chat, streaming control)
3. Process admin commands
4. State-specific behavior

---

### 7. CHALLENGES & SOLUTIONS FOR WINDOWS AUTO-UPDATE

**Challenge 1: Can't Replace Running .exe**
- **Problem:** The agent executable is running in memory and locked by Windows
- **Solution:** Use startup.rs's `MoveFileExW(..., MOVEFILE_DELAY_UNTIL_REBOOT)`
  - Download new binary to temporary location
  - Schedule current exe for deletion on reboot
  - Copy new binary to install location
  - Trigger restart (via AdminCommand or user action)

**Challenge 2: Update While Streaming**
- **Problem:** Can't restart while actively serving a viewer
- **Solution:**
  - Detect streaming sessions in progress
  - Queue update for after stream ends
  - Or: Send disconnect to all viewers, then update and restart

**Challenge 3: Installation Directory Permissions**
- **Problem:** Non-elevated agents install to `%LOCALAPPDATA%` (user-accessible)
  - Elevated agents install to `C:\Program Files\` (admin-only)
- **Solution:**
  - Update must respect current install location
  - Download new binary can go to temp folder (always writable)
  - Use current exe path to determine where to place update

**Challenge 4: Version Verification**
- **Problem:** How to prevent downgrade attacks or corrupted downloads
- **Solution:**
  - Server sends version + checksum (SHA256) in AdminUpdateCommand
  - Agent verifies checksum before moving files
  - Also validates version is >= current version

---

### 8. PROPOSED UPDATE MESSAGE STRUCTURE

**Should extend proto/guruconnect.proto:**

```protobuf
// New update message
message UpdateCommand {
    string version = 1;               // e.g., "0.2.0"
    string download_url = 2;          // e.g., https://...guruconnect.exe
    string checksum_sha256 = 3;       // For integrity verification
    bool restart_immediately = 4;     // If false, restart on next idle
}

// Extend AdminCommand to include update details
// OR change AdminCommand to include union of command-specific data
```

---

### 9. EXISTING INFRASTRUCTURE THAT SUPPORTS UPDATES

✓ **Version tracking system** - Embedded at compile time, easily queryable
✓ **Protocol infrastructure** - AdminCommand message structure already exists
✓ **File replacement mechanism** - MoveFileExW with reboot scheduling
✓ **Restart persistence** - Registry startup entry ensures new binary runs
✓ **Message loop architecture** - Can handle update commands during idle mode
✓ **Session state tracking** - Can detect streaming vs idle
✓ **Error handling framework** - Admin commands already return special errors
✓ **Transport reliability** - WebSocket with proper connection detection

---

### 10. INSTALLATION PATHS & DETECTION

**Current agent can:**
- Detect installation location via `std::env::current_exe()`
- Check elevation status (is_elevated())
- Determine install directory based on elevation
- Store config in ProgramData or AppData directories

**For updates:**
- Always update to same location as current binary
- Get path: `std::env::current_exe()` + determine elevation status
- Fallback to install::get_install_path(is_elevated())

---

### 11. SERVER-SIDE (What's Already Ready)

**File:** `server/src/relay/mod.rs` (lines 1-150)

The server already:
- Maintains active agent WebSocket connections
- Can relay messages bidirectionally
- Authenticates agents via API key or support code
- Has session management to track which agents are connected

**For updates, server would need to:**
- Store/track available versions
- Serve update binaries or provide download URLs
- Send AdminCommand with UpdateCommand payload
- Track update status of each agent (via database)

---

## Summary: Feasibility of Auto-Update

**Highly feasible** - The codebase has most required infrastructure:

1. **Download & Install:** Use existing install.rs paths + MoveFileExW scheduling
2. **Version Check:** Use embedded build_info + AdminCommand protocol
3. **Graceful Restart:** Leverage registry startup persistence
4. **Windows-Safe:** MoveFileExW solves the "can't replace running .exe" problem
5. **Protocol Ready:** AdminCommand structure exists, just needs UpdateCommand payload

**Key implementation points:**
- File: `agent/src/session/mod.rs` - Handle AdminUpdate command
- File: `proto/guruconnect.proto` - Extend AdminCommand or create UpdateCommand
- File: `agent/src/install.rs` - Add `perform_update()` function
- File: `agent/src/startup.rs` - Leverage existing MOVEFILE_DELAY_UNTIL_REBOOT

---

**Absolute file paths for reference:**
- C:\Users\MikeSwanson\Claude\guru-connect\agent\src\main.rs
- C:\Users\MikeSwanson\Claude\guru-connect\agent\src\install.rs
- C:\Users\MikeSwanson\Claude\guru-connect\agent\src\startup.rs
- C:\Users\MikeSwanson\Claude\guru-connect\agent\src\session\mod.rs
- C:\Users\MikeSwanson\Claude\guru-connect\agent\src\transport\websocket.rs
- C:\Users\MikeSwanson\Claude\guru-connect\proto\guruconnect.proto
- C:\Users\MikeSwanson\Claude\guru-connect\agent\build.rs
- C:\Users\MikeSwanson\Claude\guru-connect\server\src\relay\mod.rs