nexara-core 0.1.1

Core types, policy, registry, broker, and audit schema for Nexara
Documentation
use crate::error::NexaraResult;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::collections::BTreeMap;
use std::time::{SystemTime, UNIX_EPOCH};

#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum PolicyDecision {
    Allowed,
    DeniedNotFound,
    DeniedNotAllowed,
    DeniedReadOnly,
    DeniedTrustProfile,
    DeniedConfirmationRequired,
    DeniedPayloadTooLarge,
    DeniedConcurrencyLimit,
    DeniedInvalidParams,
    DeniedServiceUnavailable,
    DeniedPolicyContract,
    DeniedMissingCapabilities,
    DeniedNoMatchingAllow,
    DeniedPolicyConfirmationRequired,
}

#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum AuditOutcome {
    Success,
    Error,
    Rejected,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct AuditRecord {
    pub timestamp_unix_ms: u64,
    pub caller: String,
    pub tool: String,
    pub trust_tier: String,
    pub action_class: String,
    pub trust_profile: String,
    pub policy_decision: PolicyDecision,
    pub outcome: AuditOutcome,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub result_hash: Option<String>,
    pub duration_ms: u64,
    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
    pub metadata: BTreeMap<String, String>,
}

impl AuditRecord {
    pub fn now(
        caller: impl Into<String>,
        tool: impl Into<String>,
        policy_decision: PolicyDecision,
        outcome: AuditOutcome,
    ) -> Self {
        Self {
            timestamp_unix_ms: now_unix_ms(),
            caller: caller.into(),
            tool: tool.into(),
            trust_tier: String::new(),
            action_class: String::new(),
            trust_profile: String::new(),
            policy_decision,
            outcome,
            result_hash: None,
            duration_ms: 0,
            metadata: BTreeMap::new(),
        }
    }
}

pub trait AuditSink: Send + Sync {
    fn record(&self, record: AuditRecord) -> NexaraResult<()>;
}

#[derive(Debug, Default)]
pub struct TracingAuditSink;

impl AuditSink for TracingAuditSink {
    fn record(&self, record: AuditRecord) -> NexaraResult<()> {
        tracing::info!(
            nexara.audit = true,
            caller = %record.caller,
            tool = %record.tool,
            policy_decision = ?record.policy_decision,
            outcome = ?record.outcome,
            result_hash = record.result_hash.as_deref().unwrap_or("-"),
            duration_ms = record.duration_ms,
            "nexara audit event"
        );
        Ok(())
    }
}

pub fn hash_result(value: &serde_json::Value) -> String {
    let serialized = serde_json::to_vec(value).unwrap_or_default();
    let digest = Sha256::digest(&serialized);
    digest
        .iter()
        .fold(String::with_capacity(64), |mut acc, byte| {
            use std::fmt::Write;
            let _ = write!(acc, "{byte:02x}");
            acc
        })
}

fn now_unix_ms() -> u64 {
    SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap_or_default()
        .as_millis() as u64
}