stackpatrol-core 0.5.2

Shared types and protocol definitions for StackPatrol CLI and control plane.
Documentation
use serde::{Deserialize, Serialize};

/// Diagnostic context attached to a critical event so the control-plane
/// rule library has something to match against. See ADR 009.
///
/// The agent collects per-probe context (last journald lines, `docker logs`,
/// `df -h`, top processes, etc.), runs every item through the redaction module,
/// caps total size to 64 KiB, and ships the result as part of the envelope.
/// Backward-compatible: old agents emit envelopes without this field, and the
/// backend treats absent `context` as "no diagnosis input."
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
pub struct ContextBundle {
    /// Independent context items — typically 1–4 per bundle. Order matters
    /// for the per-bundle truncation policy (drop from the end first).
    pub items: Vec<ContextItem>,
    /// Aggregate redaction stats across all items.
    #[serde(default)]
    pub redactions: RedactionStats,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ContextItem {
    /// Stable identifier for the source — `"journalctl"`, `"docker_logs"`,
    /// `"df_h"`, `"ps_top10_rss"`, `"dmesg"`, etc. Rule-library matchers key
    /// off this for source-aware patterns.
    pub source: String,
    /// Plain text. Always post-redaction. Trimmed to the per-item soft cap.
    pub content: String,
    /// True if `content` was truncated below what the collector originally produced.
    pub truncated: bool,
}

/// Per-category redaction counts. Useful for two things: telling the control
/// plane what kinds of secrets the bundle had (which is itself a diagnostic
/// signal — *"this log has JWTs in it, suggesting auth attempts"*), and for
/// trust-page reporting (*"we redacted 12,847 secrets last month"*).
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
pub struct RedactionStats {
    #[serde(default)]
    pub bearer_tokens: u32,
    #[serde(default)]
    pub api_keys: u32,
    #[serde(default)]
    pub jwts: u32,
    #[serde(default)]
    pub emails: u32,
    #[serde(default)]
    pub db_credentials: u32,
    #[serde(default)]
    pub env_secrets: u32,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case", tag = "kind")]
pub enum Event {
    Heartbeat {
        uptime_secs: u64,
    },
    ServiceDown {
        name: String,
    },
    ServiceUp {
        name: String,
    },
    /// Disk usage crossed the warning threshold (default 10pp below `disk_high`).
    /// Heads-up alert before things go critical. Suppressed if the value is
    /// already in critical range — `DiskHigh` covers that case.
    DiskWarning {
        mount: String,
        percent: u8,
    },
    DiskHigh {
        mount: String,
        percent: u8,
    },
    /// Memory usage crossed the warning threshold (default 10pp below `memory_high`).
    MemoryWarning {
        percent: u8,
    },
    MemoryHigh {
        percent: u8,
    },
    /// 1-minute load crossed the warning threshold (default 75% of `load_1m_high`).
    LoadWarning {
        load_1m: f32,
    },
    LoadHigh {
        load_1m: f32,
    },
    HostUnreachable,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EventEnvelope {
    pub server_name: String,
    pub timestamp: i64,
    pub event: Event,
    /// Optional diagnostic bundle attached by the agent for critical events,
    /// per ADR 009. Old agents (pre-Phase-1.5) emit envelopes without this
    /// field; the field is also `None` for events the operator opted out of
    /// (`[diagnosis]` config) or for non-critical events that don't get
    /// bundles by default. The control plane treats absent or empty bundles
    /// as "no rule-library input" and ships the alert without a diagnosis
    /// paragraph — never invents one.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub context: Option<ContextBundle>,
}