enact-core 0.0.2

Core agent runtime for Enact - Graph-Native AI agents
Documentation
//! Protection Context
//!
//! Context types that determine how data is protected based on its destination.

use crate::kernel::TenantId;

/// Data destination - where the data is going
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DataDestination {
    /// Storage (PostgreSQL, Blob Storage, EventStore)
    /// Treatment: Encrypt sensitive fields
    Storage,

    /// Streaming to frontend (SSE, gRPC)
    /// Treatment: Mask PII, never send encrypted or raw
    Stream,

    /// Structured logs (telemetry, observability)
    /// Treatment: Hash or mask, never raw
    Log,

    /// Audit export (JSON, PDF)
    /// Treatment: Decrypt for internal, mask for external
    AuditExport,
}

impl DataDestination {
    /// Returns true if this destination is frontend-visible
    pub fn is_frontend_visible(&self) -> bool {
        matches!(self, DataDestination::Stream)
    }

    /// Returns true if this destination requires encryption
    pub fn requires_encryption(&self) -> bool {
        matches!(self, DataDestination::Storage)
    }
}

/// Protection context - information needed to make protection decisions
#[derive(Debug, Clone)]
pub struct ProtectionContext {
    /// Where the data is going
    pub destination: DataDestination,

    /// Tenant context (for tenant-specific policies)
    pub tenant_id: Option<TenantId>,

    /// Whether this is an internal audit export (can decrypt)
    pub is_internal_audit: bool,
}

impl ProtectionContext {
    /// Create a new protection context
    pub fn new(destination: DataDestination) -> Self {
        Self {
            destination,
            tenant_id: None,
            is_internal_audit: false,
        }
    }

    /// Create context for streaming to frontend
    pub fn for_stream() -> Self {
        Self::new(DataDestination::Stream)
    }

    /// Create context for storage
    pub fn for_storage() -> Self {
        Self::new(DataDestination::Storage)
    }

    /// Create context for logging
    pub fn for_log() -> Self {
        Self::new(DataDestination::Log)
    }

    /// Create context for audit export
    pub fn for_audit(is_internal: bool) -> Self {
        Self {
            destination: DataDestination::AuditExport,
            tenant_id: None,
            is_internal_audit: is_internal,
        }
    }

    /// Add tenant context
    pub fn with_tenant(mut self, tenant_id: TenantId) -> Self {
        self.tenant_id = Some(tenant_id);
        self
    }
}

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

    #[test]
    fn test_data_destination_frontend_visible() {
        assert!(DataDestination::Stream.is_frontend_visible());
        assert!(!DataDestination::Storage.is_frontend_visible());
        assert!(!DataDestination::Log.is_frontend_visible());
        assert!(!DataDestination::AuditExport.is_frontend_visible());
    }

    #[test]
    fn test_data_destination_requires_encryption() {
        assert!(DataDestination::Storage.requires_encryption());
        assert!(!DataDestination::Stream.requires_encryption());
        assert!(!DataDestination::Log.requires_encryption());
        assert!(!DataDestination::AuditExport.requires_encryption());
    }

    #[test]
    fn test_protection_context_factories() {
        let stream_ctx = ProtectionContext::for_stream();
        assert_eq!(stream_ctx.destination, DataDestination::Stream);

        let storage_ctx = ProtectionContext::for_storage();
        assert_eq!(storage_ctx.destination, DataDestination::Storage);

        let log_ctx = ProtectionContext::for_log();
        assert_eq!(log_ctx.destination, DataDestination::Log);

        let audit_ctx = ProtectionContext::for_audit(true);
        assert_eq!(audit_ctx.destination, DataDestination::AuditExport);
        assert!(audit_ctx.is_internal_audit);
    }

    #[test]
    fn test_protection_context_with_tenant() {
        let tenant_id = TenantId::from_string("tenant_123");
        let ctx = ProtectionContext::for_stream().with_tenant(tenant_id);
        assert!(ctx.tenant_id.is_some());
        assert_eq!(ctx.tenant_id.unwrap().as_str(), "tenant_123");
    }
}