openlatch-client 0.1.18

OpenLatch runtime enforcement node — the capture-and-enforce client for the AI Operations Platform
//! Hand-written extensions for generated types.
//!
//! Generated types provide data structure + serde. This module adds:
//! - Constructor methods (VerdictResponse::allow, VerdictResponse::approve,
//!   VerdictResponse::deny)
//! - Helper functions (new_event_id, current_timestamp, os_string, arch_string)

use super::{Verdict, VerdictResponse};

impl VerdictResponse {
    /// Constructs an `allow` verdict response for PreToolUse and UserPromptSubmit events.
    pub fn allow(event_id: String, latency_ms: f64) -> Self {
        Self::with_verdict(Verdict::Allow, event_id, latency_ms)
    }

    /// Constructs an `approve` verdict response for Stop events.
    pub fn approve(event_id: String, latency_ms: f64) -> Self {
        Self::with_verdict(Verdict::Approve, event_id, latency_ms)
    }

    /// Constructs a `deny` verdict response for a local policy block.
    ///
    /// The three optional fields come from the rule that **decided** — the
    /// lexicographically first rule within the deciding set, never across all
    /// matches. `reason` is what the developer actually reads when the agent
    /// renders the block ("why was this blocked?"), so it must be the deciding
    /// rule's own text and nothing else. `severity` is the rule's authored
    /// severity as a wire string (`low` / `medium` / `high` / `critical`).
    ///
    /// `schema_version` stays `"1.0"`: a policy deny uses only fields that have
    /// existed since 1.0 (`reason`, `severity`, `rule_id`). Bumping it is
    /// reserved for responses that actually carry a 1.1 field — `context` /
    /// `offline` — as `attach_alert_context` does.
    pub fn deny(
        event_id: String,
        latency_ms: f64,
        reason: Option<String>,
        severity: Option<String>,
        rule_id: Option<String>,
    ) -> Self {
        Self {
            reason,
            severity,
            rule_id,
            ..Self::with_verdict(Verdict::Deny, event_id, latency_ms)
        }
    }

    fn with_verdict(verdict: Verdict, event_id: String, latency_ms: f64) -> Self {
        Self {
            schema_version: "1.0".to_string(),
            verdict,
            event_id,
            latency_ms,
            reason: None,
            severity: None,
            threat_category: None,
            rule_id: None,
            details_url: None,
            offline: false,
            context: None,
        }
    }
}

/// Generates a new UUIDv7 event ID.
///
/// UUIDv7 IDs encode a millisecond-precision Unix timestamp in the most significant bits,
/// making them monotonically ordered when compared lexicographically. This satisfies EVNT-01.
pub fn new_event_id() -> String {
    format!("evt_{}", uuid::Uuid::now_v7())
}

/// Returns the current UTC timestamp as an RFC 3339 string with Z suffix.
///
/// Example output: `"2026-04-07T12:00:00Z"`
///
/// # PERFORMANCE: Pure in-memory — no I/O, no allocation beyond the returned String.
pub fn current_timestamp() -> String {
    chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true)
}

/// Returns the current UTC time as a `chrono::DateTime<Utc>` — the exact
/// type the generated `EventEnvelope.time` field expects for the CloudEvents
/// `time` attribute. Equivalent to `chrono::Utc::now()` but colocated with
/// the other envelope helpers so call sites can grab everything from one
/// module.
pub fn current_time_utc() -> chrono::DateTime<chrono::Utc> {
    chrono::Utc::now()
}

/// Returns the current OS name as reported by the Rust standard library.
///
/// Examples: `"linux"`, `"macos"`, `"windows"`
pub fn os_string() -> &'static str {
    std::env::consts::OS
}

/// Returns the current CPU architecture as reported by the Rust standard library.
///
/// Examples: `"x86_64"`, `"aarch64"`
pub fn arch_string() -> &'static str {
    std::env::consts::ARCH
}

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

    #[test]
    fn deny_carries_the_deciding_rules_fields() {
        let r = VerdictResponse::deny(
            "evt_1".to_string(),
            1.5,
            Some("Canary enforce".to_string()),
            Some("high".to_string()),
            Some("OL-CMD-ENF".to_string()),
        );
        assert_eq!(r.verdict, Verdict::Deny);
        assert_eq!(r.reason.as_deref(), Some("Canary enforce"));
        assert_eq!(r.severity.as_deref(), Some("high"));
        assert_eq!(r.rule_id.as_deref(), Some("OL-CMD-ENF"));
        // A policy deny uses only 1.0 fields; `context`/`offline` stay untouched.
        assert_eq!(r.schema_version, "1.0");
        assert!(r.context.is_none());
        assert!(!r.offline);
    }

    #[test]
    fn allow_and_approve_leave_the_policy_fields_empty() {
        for r in [
            VerdictResponse::allow("evt_1".to_string(), 0.0),
            VerdictResponse::approve("evt_1".to_string(), 0.0),
        ] {
            assert!(r.reason.is_none());
            assert!(r.severity.is_none());
            assert!(r.rule_id.is_none());
        }
    }
}