     1→//! Database operations for agent updates
     2→
     3→use anyhow::Result;
     4→use sqlx::{PgPool, Row};
     5→use uuid::Uuid;
     6→
     7→/// Agent update record
     8→#[derive(Debug, Clone, sqlx::FromRow)]
     9→pub struct AgentUpdateRecord {
    10→    pub id: Uuid,
    11→    pub agent_id: Uuid,
    12→    pub update_id: Uuid,
    13→    pub old_version: String,
    14→    pub target_version: String,
    15→    pub download_url: Option<String>,
    16→    pub checksum_sha256: Option<String>,
    17→    pub status: Option<String>,
    18→    pub started_at: Option<chrono::DateTime<chrono::Utc>>,
    19→    pub completed_at: Option<chrono::DateTime<chrono::Utc>>,
    20→    pub error_message: Option<String>,
    21→}
    22→
    23→/// Create a new agent update record
    24→pub async fn create_agent_update(
    25→    pool: &PgPool,
    26→    agent_id: Uuid,
    27→    update_id: Uuid,
    28→    old_version: &str,
    29→    target_version: &str,
    30→    download_url: &str,
    31→    checksum_sha256: &str,
    32→) -> Result<()> {
    33→    sqlx::query(
    34→        r#"
    35→        INSERT INTO agent_updates (agent_id, update_id, old_version, target_version, download_url, checksum_sha256, status)
    36→        VALUES ($1, $2, $3, $4, $5, $6, 'pending')
    37→        "#,
    38→    )
    39→    .bind(agent_id)
    40→    .bind(update_id)
    41→    .bind(old_version)
    42→    .bind(target_version)
    43→    .bind(download_url)
    44→    .bind(checksum_sha256)
    45→    .execute(pool)
    46→    .await?;
    47→
    48→    Ok(())
    49→}
    50→
    51→/// Mark an agent update as completed
    52→pub async fn complete_agent_update(
    53→    pool: &PgPool,
    54→    update_id: Uuid,
    55→    new_version: Option<&str>,
    56→) -> Result<()> {
    57→    sqlx::query(
    58→        r#"
    59→        UPDATE agent_updates
    60→        SET status = 'completed', completed_at = NOW()
    61→        WHERE update_id = $1
    62→        "#,
    63→    )
    64→    .bind(update_id)
    65→    .execute(pool)
    66→    .await?;
    67→
    68→    // If new_version provided, update the agent's version
    69→    if let Some(version) = new_version {
    70→        sqlx::query(
    71→            r#"
    72→            UPDATE agents
    73→            SET agent_version = $2, updated_at = NOW()
    74→            WHERE id = (SELECT agent_id FROM agent_updates WHERE update_id = $1)
    75→            "#,
    76→        )
    77→        .bind(update_id)
    78→        .bind(version)
    79→        .execute(pool)
    80→        .await?;
    81→    }
    82→
    83→    Ok(())
    84→}
    85→
    86→/// Mark an agent update as failed
    87→pub async fn fail_agent_update(
    88→    pool: &PgPool,
    89→    update_id: Uuid,
    90→    error_message: Option<&str>,
    91→) -> Result<()> {
    92→    sqlx::query(
    93→        r#"
    94→        UPDATE agent_updates
    95→        SET status = 'failed', completed_at = NOW(), error_message = $2
    96→        WHERE update_id = $1
    97→        "#,
    98→    )
    99→    .bind(update_id)
   100→    .bind(error_message)
   101→    .execute(pool)
   102→    .await?;
   103→
   104→    Ok(())
   105→}
   106→
   107→/// Update the status of an agent update (for progress tracking)
   108→pub async fn update_agent_update_status(
   109→    pool: &PgPool,
   110→    update_id: Uuid,
   111→    status: &str,
   112→) -> Result<()> {
   113→    sqlx::query(
   114→        r#"
   115→        UPDATE agent_updates
   116→        SET status = $2
   117→        WHERE update_id = $1
   118→        "#,
   119→    )
   120→    .bind(update_id)
   121→    .bind(status)
   122→    .execute(pool)
   123→    .await?;
   124→
   125→    Ok(())
   126→}
   127→
   128→/// Get pending update for an agent (if any)
   129→pub async fn get_pending_update(
   130→    pool: &PgPool,
   131→    agent_id: Uuid,
   132→) -> Result<Option<AgentUpdateRecord>> {
   133→    let record = sqlx::query_as::<_, AgentUpdateRecord>(
   134→        r#"
   135→        SELECT id, agent_id, update_id, old_version, target_version,
   136→               download_url, checksum_sha256, status, started_at, completed_at, error_message
   137→        FROM agent_updates
   138→        WHERE agent_id = $1 AND status IN ('pending', 'downloading', 'installing')
   139→        ORDER BY started_at DESC
   140→        LIMIT 1
   141→        "#,
   142→    )
   143→    .bind(agent_id)
   144→    .fetch_optional(pool)
   145→    .await?;
   146→
   147→    Ok(record)
   148→}
   149→
   150→/// Get stale updates (started but not completed within timeout)
   151→pub async fn get_stale_updates(
   152→    pool: &PgPool,
   153→    timeout_secs: i64,
   154→) -> Result<Vec<AgentUpdateRecord>> {
   155→    let records = sqlx::query_as::<_, AgentUpdateRecord>(
   156→        r#"
   157→        SELECT id, agent_id, update_id, old_version, target_version,
   158→               download_url, checksum_sha256, status, started_at, completed_at, error_message
   159→        FROM agent_updates
   160→        WHERE status IN ('pending', 'downloading', 'installing')
   161→          AND started_at < NOW() - INTERVAL '1 second' * $1
   162→        "#,
   163→    )
   164→    .bind(timeout_secs as f64)
   165→    .fetch_all(pool)
   166→    .await?;
   167→
   168→    Ok(records)
   169→}
   170→
   171→/// Complete a pending update by matching agent reconnection
   172→/// Called when an agent reconnects with a previous_version different from agent_version
   173→pub async fn complete_update_by_agent(
   174→    pool: &PgPool,
   175→    agent_id: Uuid,
   176→    pending_update_id: Option<Uuid>,
   177→    old_version: &str,
   178→    new_version: &str,
   179→) -> Result<bool> {
   180→    // First try by update_id if provided
   181→    if let Some(update_id) = pending_update_id {
   182→        let result = sqlx::query(
   183→            r#"
   184→            UPDATE agent_updates
   185→            SET status = 'completed', completed_at = NOW()
   186→            WHERE update_id = $1 AND agent_id = $2 AND status IN ('pending', 'downloading', 'installing')
   187→            "#,
   188→        )
   189→        .bind(update_id)
   190→        .bind(agent_id)
   191→        .execute(pool)
   192→        .await?;
   193→
   194→        if result.rows_affected() > 0 {
   195→            return Ok(true);
   196→        }
   197→    }
   198→
   199→    // Fall back to finding by old_version match
   200→    let result = sqlx::query(
   201→        r#"
   202→        UPDATE agent_updates
   203→        SET status = 'completed', completed_at = NOW()
   204→        WHERE agent_id = $1
   205→          AND old_version = $2
   206→          AND target_version = $3
   207→          AND status IN ('pending', 'downloading', 'installing')
   208→        "#,
   209→    )
   210→    .bind(agent_id)
   211→    .bind(old_version)
   212→    .bind(new_version)
   213→    .execute(pool)
   214→    .await?;
   215→
   216→    Ok(result.rows_affected() > 0)
   217→}
   218→

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

//! Database operations for agent updates

use anyhow::Result;
use sqlx::{PgPool, Row};
use uuid::Uuid;

/// Agent update record
#[derive(Debug, Clone, sqlx::FromRow)]
pub struct AgentUpdateRecord {
    pub id: Uuid,
    pub agent_id: Uuid,
    pub update_id: Uuid,
    pub old_version: String,
    pub target_version: String,
    pub download_url: Option<String>,
    pub checksum_sha256: Option<String>,
    pub status: Option<String>,
    pub started_at: Option<chrono::DateTime<chrono::Utc>>,
    pub completed_at: Option<chrono::DateTime<chrono::Utc>>,
    pub error_message: Option<String>,
}

/// Create a new agent update record
pub async fn create_agent_update(
    pool: &PgPool,
    agent_id: Uuid,
    update_id: Uuid,
    old_version: &str,
    target_version: &str,
    download_url: &str,
    checksum_sha256: &str,
) -> Result<()> {
    sqlx::query(
        r#"
        INSERT INTO agent_updates (agent_id, update_id, old_version, target_version, download_url, checksum_sha256, status)
        VALUES ($1, $2, $3, $4, $5, $6, 'pending')
        "#,
    )
    .bind(agent_id)
    .bind(update_id)
    .bind(old_version)
    .bind(target_version)
    .bind(download_url)
    .bind(checksum_sha256)
    .execute(pool)
    .await?;

    Ok(())
}

/// Mark an agent update as completed
pub async fn complete_agent_update(
    pool: &PgPool,
    update_id: Uuid,
    new_version: Option<&str>,
) -> Result<()> {
    sqlx::query(
        r#"
        UPDATE agent_updates
        SET status = 'completed', completed_at = NOW()
        WHERE update_id = $1
        "#,
    )
    .bind(update_id)
    .execute(pool)
    .await?;

    // If new_version provided, update the agent's version
    if let Some(version) = new_version {
        sqlx::query(
            r#"
            UPDATE agents
            SET agent_version = $2, updated_at = NOW()
            WHERE id = (SELECT agent_id FROM agent_updates WHERE update_id = $1)
            "#,
        )
        .bind(update_id)
        .bind(version)
        .execute(pool)
        .await?;
    }

    Ok(())
}

/// Mark an agent update as failed
pub async fn fail_agent_update(
    pool: &PgPool,
    update_id: Uuid,
    error_message: Option<&str>,
) -> Result<()> {
    sqlx::query(
        r#"
        UPDATE agent_updates
        SET status = 'failed', completed_at = NOW(), error_message = $2
        WHERE update_id = $1
        "#,
    )
    .bind(update_id)
    .bind(error_message)
    .execute(pool)
    .await?;

    Ok(())
}

/// Update the status of an agent update (for progress tracking)
pub async fn update_agent_update_status(
    pool: &PgPool,
    update_id: Uuid,
    status: &str,
) -> Result<()> {
    sqlx::query(
        r#"
        UPDATE agent_updates
        SET status = $2
        WHERE update_id = $1
        "#,
    )
    .bind(update_id)
    .bind(status)
    .execute(pool)
    .await?;

    Ok(())
}

/// Get pending update for an agent (if any)
pub async fn get_pending_update(
    pool: &PgPool,
    agent_id: Uuid,
) -> Result<Option<AgentUpdateRecord>> {
    let record = sqlx::query_as::<_, AgentUpdateRecord>(
        r#"
        SELECT id, agent_id, update_id, old_version, target_version,
               download_url, checksum_sha256, status, started_at, completed_at, error_message
        FROM agent_updates
        WHERE agent_id = $1 AND status IN ('pending', 'downloading', 'installing')
        ORDER BY started_at DESC
        LIMIT 1
        "#,
    )
    .bind(agent_id)
    .fetch_optional(pool)
    .await?;

    Ok(record)
}

/// Get stale updates (started but not completed within timeout)
pub async fn get_stale_updates(
    pool: &PgPool,
    timeout_secs: i64,
) -> Result<Vec<AgentUpdateRecord>> {
    let records = sqlx::query_as::<_, AgentUpdateRecord>(
        r#"
        SELECT id, agent_id, update_id, old_version, target_version,
               download_url, checksum_sha256, status, started_at, completed_at, error_message
        FROM agent_updates
        WHERE status IN ('pending', 'downloading', 'installing')
          AND started_at < NOW() - INTERVAL '1 second' * $1
        "#,
    )
    .bind(timeout_secs as f64)
    .fetch_all(pool)
    .await?;

    Ok(records)
}

/// Complete a pending update by matching agent reconnection
/// Called when an agent reconnects with a previous_version different from agent_version
pub async fn complete_update_by_agent(
    pool: &PgPool,
    agent_id: Uuid,
    pending_update_id: Option<Uuid>,
    old_version: &str,
    new_version: &str,
) -> Result<bool> {
    // First try by update_id if provided
    if let Some(update_id) = pending_update_id {
        let result = sqlx::query(
            r#"
            UPDATE agent_updates
            SET status = 'completed', completed_at = NOW()
            WHERE update_id = $1 AND agent_id = $2 AND status IN ('pending', 'downloading', 'installing')
            "#,
        )
        .bind(update_id)
        .bind(agent_id)
        .execute(pool)
        .await?;

        if result.rows_affected() > 0 {
            return Ok(true);
        }
    }

    // Fall back to finding by old_version match
    let result = sqlx::query(
        r#"
        UPDATE agent_updates
        SET status = 'completed', completed_at = NOW()
        WHERE agent_id = $1
          AND old_version = $2
          AND target_version = $3
          AND status IN ('pending', 'downloading', 'installing')
        "#,
    )
    .bind(agent_id)
    .bind(old_version)
    .bind(new_version)
    .execute(pool)
    .await?;

    Ok(result.rows_affected() > 0)
}


Perfect! I now have a comprehensive understanding of the GuruRMM database schema. Let me create a detailed report of my findings:

## GuruRMM Database Schema & Data Models Analysis

### 1. Current Table Structure

#### Core Tables:

**agents**
- `id` (UUID, PK) - Unique agent identifier
- `hostname` (VARCHAR 255) - Machine name
- `api_key_hash` (VARCHAR 255, nullable) - Authentication credential
- `os_type` (VARCHAR 50) - OS type (Windows, Linux, macOS)
- `os_version` (VARCHAR 100) - OS version
- `agent_version` (VARCHAR 50) - Agent software version
- `device_id` (VARCHAR 255, nullable) - Hardware-derived unique ID
- `site_id` (UUID, FK → sites.id, nullable) - Site association
- `architecture` (VARCHAR 20, default 'amd64') - CPU architecture
- `last_seen` (TIMESTAMPTZ) - Last heartbeat timestamp
- `status` (VARCHAR 20) - 'online' or 'offline'
- `created_at`, `updated_at` (TIMESTAMPTZ)

**clients** (organizations/companies)
- `id` (UUID, PK)
- `name` (VARCHAR 255, NOT NULL) - Company name
- `code` (VARCHAR 50, UNIQUE) - Short code like "ACME"
- `notes` (TEXT)
- `is_active` (BOOLEAN, default TRUE)
- `created_at`, `updated_at` (TIMESTAMPTZ)

**sites** (locations under a client)
- `id` (UUID, PK)
- `client_id` (UUID, FK → clients.id) - Parent client
- `name` (VARCHAR 255) - Location name
- `site_code` (VARCHAR 50, UNIQUE) - Human-friendly code like "BLUE-TIGER-4829"
- `api_key_hash` (VARCHAR 255) - Shared key for all agents at this site
- `address` (TEXT) - Physical address
- `notes` (TEXT)
- `is_active` (BOOLEAN, default TRUE)
- `created_at`, `updated_at` (TIMESTAMPTZ)

**registration_tokens** (for secure site code distribution)
- `id` (UUID, PK)
- `site_id` (UUID, FK → sites.id)
- `token_hash` (VARCHAR 255)
- `description` (VARCHAR 255)
- `uses_remaining` (INTEGER, NULL = unlimited)
- `expires_at` (TIMESTAMPTZ)
- `created_at` (TIMESTAMPTZ)
- `created_by` (UUID, FK → users.id)

**commands** (commands sent to agents)
- `id` (UUID, PK)
- `agent_id` (UUID, FK → agents.id)
- `command_type` (VARCHAR 50) - Type of command
- `command_text` (TEXT) - The actual command
- `status` (VARCHAR 20) - 'pending', 'running', 'completed', 'failed'
- `exit_code` (INTEGER)
- `stdout`, `stderr` (TEXT)
- `created_at`, `started_at`, `completed_at` (TIMESTAMPTZ)
- `created_by` (UUID, FK → users.id)

**agent_updates** (update tracking)
- `id` (UUID, PK)
- `agent_id` (UUID, FK → agents.id)
- `update_id` (UUID, UNIQUE)
- `old_version`, `target_version` (VARCHAR 50)
- `status` (VARCHAR 20) - 'pending', 'downloading', 'installing', 'completed', 'failed', 'rolled_back'
- `download_url` (TEXT)
- `checksum_sha256` (VARCHAR 64)
- `started_at`, `completed_at` (TIMESTAMPTZ)
- `error_message` (TEXT)
- `created_at` (TIMESTAMPTZ)

**metrics** (time-series system metrics)
- `id` (BIGSERIAL, PK)
- `agent_id` (UUID, FK → agents.id)
- `timestamp` (TIMESTAMPTZ)
- `cpu_percent`, `memory_percent` (REAL)
- `memory_used_bytes`, `memory_total_bytes` (BIGINT)
- `disk_percent`, `disk_used_bytes`, `disk_total_bytes` (BIGINT)
- `network_rx_bytes`, `network_tx_bytes` (BIGINT)
- `uptime_seconds`, `boot_time` (BIGINT)
- `logged_in_user` (VARCHAR 255)
- `user_idle_seconds` (BIGINT)
- `public_ip` (VARCHAR 45)
- `cpu_temp_celsius`, `gpu_temp_celsius` (REAL)
- `temperatures` (JSONB) - All sensor readings

**agent_state** (latest agent snapshot)
- `agent_id` (UUID, PK, FK → agents.id)
- `network_interfaces` (JSONB)
- `network_state_hash` (VARCHAR 32)
- `uptime_seconds`, `boot_time` (BIGINT)
- `logged_in_user` (VARCHAR 255)
- `user_idle_seconds` (BIGINT)
- `public_ip` (VARCHAR 45)
- `network_updated_at`, `metrics_updated_at` (TIMESTAMPTZ)

**watchdog_events** (agent monitoring events)
- `id` (BIGSERIAL, PK)
- `agent_id` (UUID, FK → agents.id)
- `timestamp` (TIMESTAMPTZ)
- `service_name` (VARCHAR 255)
- `event_type` (VARCHAR 50)
- `details` (TEXT)

**users** (dashboard authentication)
- `id` (UUID, PK)
- `email` (VARCHAR 255, UNIQUE)
- `password_hash` (VARCHAR 255)
- `name` (VARCHAR 255)
- `role` (VARCHAR 50, default 'user')
- `sso_provider`, `sso_id` (VARCHAR 255)
- `created_at` (TIMESTAMPTZ)
- `last_login` (TIMESTAMPTZ)

---

### 2. Agent-Site-Client Relationship Model

```
Client (1) ──────── (N) Sites
  ▲
  │
  │ (1) ──────── (N) Agents (via site_id)
  │
Site ─────────────────┘
```

**Key Relationships:**

1. **Client → Sites**: One-to-many via `sites.client_id`
2. **Sites → Agents**: One-to-many via `agents.site_id`
3. **Full Hierarchy**: Client > Site > Agents

**Agent Registration Patterns:**

There are TWO registration methods supported:

1. **Legacy/Global Registration** (api_key_hash at agent level)
   - Agent has its own `api_key_hash` in agents table
   - `site_id` is NULL
   - Used for older agents or those not assigned to a site

2. **Site-Based Registration** (modern approach)
   - Agent `site_id` points to a site
   - All agents at a site share the `api_key_hash` from sites table
   - Agent has unique `device_id` (hardware-derived)
   - Unique constraint: `(site_id, device_id)` combination
   - Device can be re-registered if agent is reimaged

**Lookup Methods:**
- By Site + Device ID: `get_agent_by_site_and_device()`
- By API Key Hash: `get_agent_by_api_key_hash()` (legacy)
- By Site: `get_agents_by_site()`
- Unassigned: `get_unassigned_agents()`

---

### 3. Existing Configuration/Settings Patterns

**Pattern 1: Command-Based Configuration (via commands table)**
- Commands are stored with `command_type` and `command_text`
- Status tracking: pending → running → completed/failed
- One-time execution per command
- Example: Setup commands, remediation tasks
- Useful for: One-off tasks, not persistent settings

**Pattern 2: JSONB Flexible Storage (agent_state table)**
- `network_interfaces` (JSONB) - Stores network configuration snapshot
- `temperatures` (JSONB in metrics) - Stores all sensor readings
- Flexible for storing variable/schema-less data
- Updated periodically, not transaction-tracked

**Pattern 3: Update Tracking (agent_updates table)**
- Tracks version transitions with status
- Includes download URL, checksum, error messages
- Status progression: pending → downloading → installing → completed
- Timeout detection for stale updates
- Re-registration detection when agent reconnects

**Pattern 4: Token-Based Configuration (registration_tokens)**
- `uses_remaining` (INTEGER) - Rate limiting
- `expires_at` - Time-based expiration
- Both patterns for constraining usage
- Already handles secure distribution

---

### 4. Design Considerations for a Policy System

**What Fits Well:**
1. **Hierarchical scoping** - Policies at Client/Site/Agent levels mirror existing structure
2. **Commands already exist** - Could extend command system for policy delivery
3. **Status tracking pattern** - Similar to agent_updates for policy compliance
4. **JSONB support** - Good for flexible policy definitions
5. **Site-based registration** - Policies can inherit from site level

**What Would Need New Tables:**
1. **policies** table - Policy definitions and metadata
2. **policy_assignments** table - Which policies apply to which agents/sites/clients
3. **policy_compliance** table - Track if agents are compliant with policies
4. **policy_versions** table - Version history and rollback capability
5. **policy_violations** table - Track when agents violate policies

**Key Design Questions:**
1. Should policies be applied at Agent, Site, or Client level (probably all three with inheritance)?
2. Should policy configuration be JSONB (flexible) or have specific columns for common policies?
3. How should policy conflicts be resolved (multiple applicable policies)?
4. Should policy changes automatically trigger agent reconfiguration commands?
5. Do you need policy compliance auditing and reporting?

---

### File Locations Summary:

- **Migrations**: `/C:\Users\MikeSwanson\claude-projects\gururmm\server\migrations/`
- **Database Models**: `/C:\Users\MikeSwanson\claude-projects\gururmm\server\src\db/`
  - agents.rs, clients.rs, sites.rs, commands.rs, updates.rs, metrics.rs, users.rs
- **API Layer**: `/C:\Users\MikeSwanson\claude-projects\gururmm\server\src\api/`
- **Database Pool & Connection**: `/C:\Users\MikeSwanson\claude-projects\gururmm\server\src\db\mod.rs`

The codebase shows a well-structured multi-tenant system with clear separation between clients, sites, and agents. The existing patterns for commands, tokens, and updates provide good templates for building a policy system.