Skip to main content

enact_core/context/
tenant.rs

1//! Tenant Context - Multi-tenant isolation boundary
2//!
3//! TenantContext is a **required** component for every execution.
4//! Every invocation must run within a tenant boundary for:
5//! - Resource isolation
6//! - Billing attribution
7//! - Audit compliance
8//! - Data segregation
9//!
10//! ## Usage
11//! ```ignore
12//! let tenant = TenantContext::new(TenantId::from("tenant_acme"))
13//!     .with_user(UserId::from("usr_alice"))
14//!     .with_limits(ResourceLimits::default());
15//! ```
16
17use crate::kernel::{TenantId, UserId};
18use serde::{Deserialize, Serialize};
19use std::collections::HashMap;
20
21/// Resource limits enforced by the runtime per tenant
22#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct ResourceLimits {
24    /// Maximum number of steps in an execution
25    pub max_steps: u32,
26    /// Maximum total tokens for LLM calls
27    pub max_tokens: u32,
28    /// Maximum wall time in milliseconds
29    pub max_wall_time_ms: u64,
30    /// Maximum memory in megabytes (optional)
31    pub max_memory_mb: Option<u32>,
32    /// Maximum concurrent executions
33    pub max_concurrent_executions: Option<u32>,
34}
35
36impl Default for ResourceLimits {
37    fn default() -> Self {
38        Self {
39            max_steps: 100,
40            max_tokens: 100_000,
41            max_wall_time_ms: 300_000, // 5 minutes
42            max_memory_mb: None,
43            max_concurrent_executions: None,
44        }
45    }
46}
47
48/// TenantContext - Required context for every execution
49///
50/// This is the multi-tenant isolation boundary. Every execution MUST
51/// have a TenantContext with a valid TenantId.
52#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct TenantContext {
54    /// Tenant ID (REQUIRED)
55    pub tenant_id: TenantId,
56
57    /// User ID (optional - system executions may not have a user)
58    pub user_id: Option<UserId>,
59
60    /// Resource limits for this tenant
61    pub limits: ResourceLimits,
62
63    /// Tenant-specific feature flags
64    pub features: HashMap<String, bool>,
65
66    /// Tenant metadata
67    pub metadata: HashMap<String, serde_json::Value>,
68}
69
70impl TenantContext {
71    /// Create a new TenantContext (TenantId is required)
72    pub fn new(tenant_id: TenantId) -> Self {
73        Self {
74            tenant_id,
75            user_id: None,
76            limits: ResourceLimits::default(),
77            features: HashMap::new(),
78            metadata: HashMap::new(),
79        }
80    }
81
82    /// Add user context
83    pub fn with_user(mut self, user_id: UserId) -> Self {
84        self.user_id = Some(user_id);
85        self
86    }
87
88    /// Set resource limits
89    pub fn with_limits(mut self, limits: ResourceLimits) -> Self {
90        self.limits = limits;
91        self
92    }
93
94    /// Enable a feature flag
95    pub fn with_feature(mut self, feature: impl Into<String>, enabled: bool) -> Self {
96        self.features.insert(feature.into(), enabled);
97        self
98    }
99
100    /// Add metadata
101    pub fn with_metadata(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
102        self.metadata.insert(key.into(), value);
103        self
104    }
105
106    /// Check if a feature is enabled
107    pub fn is_feature_enabled(&self, feature: &str) -> bool {
108        self.features.get(feature).copied().unwrap_or(false)
109    }
110
111    /// Get the tenant ID
112    pub fn tenant_id(&self) -> &TenantId {
113        &self.tenant_id
114    }
115
116    /// Get the user ID
117    pub fn user_id(&self) -> Option<&UserId> {
118        self.user_id.as_ref()
119    }
120
121    /// Create a child TenantContext for sub-agent execution
122    /// (inherits tenant, limits, features but can override user)
123    pub fn child_context(&self, user_id: Option<UserId>) -> Self {
124        Self {
125            tenant_id: self.tenant_id.clone(),
126            user_id: user_id.or_else(|| self.user_id.clone()),
127            limits: self.limits.clone(),
128            features: self.features.clone(),
129            metadata: HashMap::new(), // Child starts fresh
130        }
131    }
132}
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137
138    #[test]
139    fn test_tenant_context_required_id() {
140        let tenant = TenantContext::new(TenantId::from("tenant_123"));
141        assert_eq!(tenant.tenant_id().as_str(), "tenant_123");
142    }
143
144    #[test]
145    fn test_tenant_context_with_user() {
146        let tenant =
147            TenantContext::new(TenantId::from("tenant_123")).with_user(UserId::from("usr_456"));
148
149        assert_eq!(tenant.user_id().unwrap().as_str(), "usr_456");
150    }
151
152    #[test]
153    fn test_feature_flags() {
154        let tenant = TenantContext::new(TenantId::from("tenant_123"))
155            .with_feature("beta_tools", true)
156            .with_feature("experimental", false);
157
158        assert!(tenant.is_feature_enabled("beta_tools"));
159        assert!(!tenant.is_feature_enabled("experimental"));
160        assert!(!tenant.is_feature_enabled("nonexistent"));
161    }
162
163    #[test]
164    fn test_child_context() {
165        let parent = TenantContext::new(TenantId::from("tenant_123"))
166            .with_user(UserId::from("usr_456"))
167            .with_feature("beta", true);
168
169        let child = parent.child_context(Some(UserId::from("usr_789")));
170
171        assert_eq!(child.tenant_id().as_str(), "tenant_123");
172        assert_eq!(child.user_id().unwrap().as_str(), "usr_789");
173        assert!(child.is_feature_enabled("beta"));
174    }
175}