enact-core 0.0.2

Core agent runtime for Enact - Graph-Native AI agents
Documentation
//! Tenant Context - Multi-tenant isolation boundary
//!
//! TenantContext is a **required** component for every execution.
//! Every invocation must run within a tenant boundary for:
//! - Resource isolation
//! - Billing attribution
//! - Audit compliance
//! - Data segregation
//!
//! ## Usage
//! ```ignore
//! let tenant = TenantContext::new(TenantId::from("tenant_acme"))
//!     .with_user(UserId::from("usr_alice"))
//!     .with_limits(ResourceLimits::default());
//! ```

use crate::kernel::{TenantId, UserId};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;

/// Resource limits enforced by the runtime per tenant
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResourceLimits {
    /// Maximum number of steps in an execution
    pub max_steps: u32,
    /// Maximum total tokens for LLM calls
    pub max_tokens: u32,
    /// Maximum wall time in milliseconds
    pub max_wall_time_ms: u64,
    /// Maximum memory in megabytes (optional)
    pub max_memory_mb: Option<u32>,
    /// Maximum concurrent executions
    pub max_concurrent_executions: Option<u32>,
}

impl Default for ResourceLimits {
    fn default() -> Self {
        Self {
            max_steps: 100,
            max_tokens: 100_000,
            max_wall_time_ms: 300_000, // 5 minutes
            max_memory_mb: None,
            max_concurrent_executions: None,
        }
    }
}

/// TenantContext - Required context for every execution
///
/// This is the multi-tenant isolation boundary. Every execution MUST
/// have a TenantContext with a valid TenantId.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TenantContext {
    /// Tenant ID (REQUIRED)
    pub tenant_id: TenantId,

    /// User ID (optional - system executions may not have a user)
    pub user_id: Option<UserId>,

    /// Resource limits for this tenant
    pub limits: ResourceLimits,

    /// Tenant-specific feature flags
    pub features: HashMap<String, bool>,

    /// Tenant metadata
    pub metadata: HashMap<String, serde_json::Value>,
}

impl TenantContext {
    /// Create a new TenantContext (TenantId is required)
    pub fn new(tenant_id: TenantId) -> Self {
        Self {
            tenant_id,
            user_id: None,
            limits: ResourceLimits::default(),
            features: HashMap::new(),
            metadata: HashMap::new(),
        }
    }

    /// Add user context
    pub fn with_user(mut self, user_id: UserId) -> Self {
        self.user_id = Some(user_id);
        self
    }

    /// Set resource limits
    pub fn with_limits(mut self, limits: ResourceLimits) -> Self {
        self.limits = limits;
        self
    }

    /// Enable a feature flag
    pub fn with_feature(mut self, feature: impl Into<String>, enabled: bool) -> Self {
        self.features.insert(feature.into(), enabled);
        self
    }

    /// Add metadata
    pub fn with_metadata(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
        self.metadata.insert(key.into(), value);
        self
    }

    /// Check if a feature is enabled
    pub fn is_feature_enabled(&self, feature: &str) -> bool {
        self.features.get(feature).copied().unwrap_or(false)
    }

    /// Get the tenant ID
    pub fn tenant_id(&self) -> &TenantId {
        &self.tenant_id
    }

    /// Get the user ID
    pub fn user_id(&self) -> Option<&UserId> {
        self.user_id.as_ref()
    }

    /// Create a child TenantContext for sub-agent execution
    /// (inherits tenant, limits, features but can override user)
    pub fn child_context(&self, user_id: Option<UserId>) -> Self {
        Self {
            tenant_id: self.tenant_id.clone(),
            user_id: user_id.or_else(|| self.user_id.clone()),
            limits: self.limits.clone(),
            features: self.features.clone(),
            metadata: HashMap::new(), // Child starts fresh
        }
    }
}

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

    #[test]
    fn test_tenant_context_required_id() {
        let tenant = TenantContext::new(TenantId::from("tenant_123"));
        assert_eq!(tenant.tenant_id().as_str(), "tenant_123");
    }

    #[test]
    fn test_tenant_context_with_user() {
        let tenant =
            TenantContext::new(TenantId::from("tenant_123")).with_user(UserId::from("usr_456"));

        assert_eq!(tenant.user_id().unwrap().as_str(), "usr_456");
    }

    #[test]
    fn test_feature_flags() {
        let tenant = TenantContext::new(TenantId::from("tenant_123"))
            .with_feature("beta_tools", true)
            .with_feature("experimental", false);

        assert!(tenant.is_feature_enabled("beta_tools"));
        assert!(!tenant.is_feature_enabled("experimental"));
        assert!(!tenant.is_feature_enabled("nonexistent"));
    }

    #[test]
    fn test_child_context() {
        let parent = TenantContext::new(TenantId::from("tenant_123"))
            .with_user(UserId::from("usr_456"))
            .with_feature("beta", true);

        let child = parent.child_context(Some(UserId::from("usr_789")));

        assert_eq!(child.tenant_id().as_str(), "tenant_123");
        assert_eq!(child.user_id().unwrap().as_str(), "usr_789");
        assert!(child.is_feature_enabled("beta"));
    }
}