Skip to main content

enact_core/policy/
mod.rs

1//! Policy - Execution constraints and guardrails
2//!
3//! Policies define what is ALLOWED during execution:
4//! - Execution policies: limits, timeouts, retries
5//! - Tool policies: permissions, network/fs access, PII handling
6//! - Tenant policies: quotas, feature flags, isolation
7//! - Input processors: pre-execution validation (PII, prompt injection)
8//!
9//! ## Architecture
10//!
11//! Policies are evaluated BEFORE execution, not during business logic.
12//! This ensures:
13//! - Clear separation of concerns
14//! - Auditable policy decisions
15//! - No policy logic scattered in execution code
16//!
17//! ```text
18//! ┌─────────────────────────────────────────────┐
19//! │              Policy Evaluator               │
20//! │  ┌─────────────────────────────────────┐    │
21//! │  │ ExecutionPolicy │ ToolPolicy │ ...  │    │
22//! │  └─────────────────────────────────────┘    │
23//! │                     │                       │
24//! │                     ▼                       │
25//! │           Allow / Deny / Warn              │
26//! └─────────────────────────────────────────────┘
27//!                       │
28//!                       ▼
29//!              ExecutionKernel
30//! ```
31//!
32//! @see docs/TECHNICAL/17-GUARDRAILS-PROTECTION.md
33//! @see docs/TECHNICAL/25-STREAM-PROCESSORS.md
34
35mod execution_policy;
36mod filters;
37mod tenant_policy;
38mod tool_policy;
39
40// Input processors (feat-09: Guardrails)
41mod input_processor;
42mod long_running;
43mod pii_input;
44
45pub use execution_policy::{ExecutionLimits, ExecutionPolicy};
46pub use filters::{ContentFilter, FilterAction, FilterResult};
47pub use long_running::{
48    CheckpointPolicy, ContextStrategy, LongRunningExecutionPolicy, WorkingMemoryPolicy,
49};
50pub use tenant_policy::{FeatureFlags, TenantLimits, TenantPolicy};
51pub use tool_policy::{ToolPermissions, ToolPolicy, ToolTrustLevel};
52
53// Input processor exports
54pub use input_processor::{InputProcessor, InputProcessorPipeline, InputProcessorResult};
55pub use pii_input::{PiiInputMode, PiiInputProcessor};
56
57/// Policy decision result
58#[derive(Debug, Clone)]
59pub enum PolicyDecision {
60    /// Action is allowed
61    Allow,
62    /// Action is denied with reason
63    Deny { reason: String },
64    /// Action is allowed but logged/warned
65    Warn { message: String },
66}
67
68impl PolicyDecision {
69    pub fn is_allowed(&self) -> bool {
70        matches!(self, PolicyDecision::Allow | PolicyDecision::Warn { .. })
71    }
72
73    pub fn is_denied(&self) -> bool {
74        matches!(self, PolicyDecision::Deny { .. })
75    }
76}
77
78/// Trait for policy evaluators
79pub trait PolicyEvaluator: Send + Sync {
80    /// Evaluate a policy for the given context
81    fn evaluate(&self, context: &PolicyContext) -> PolicyDecision;
82}
83
84/// Context for policy evaluation
85#[derive(Debug, Clone)]
86pub struct PolicyContext {
87    /// Tenant ID (if multi-tenant)
88    pub tenant_id: Option<String>,
89    /// User ID
90    pub user_id: Option<String>,
91    /// Current action being evaluated
92    pub action: PolicyAction,
93    /// Additional metadata
94    pub metadata: std::collections::HashMap<String, String>,
95}
96
97/// Actions that can be evaluated by policies
98#[derive(Debug, Clone)]
99pub enum PolicyAction {
100    /// Starting an execution
101    StartExecution { graph_id: Option<String> },
102    /// Invoking a tool
103    InvokeTool { tool_name: String },
104    /// Making an LLM call
105    LlmCall { model: String },
106    /// Accessing external resource
107    ExternalAccess { resource: String },
108    /// Outputting content
109    OutputContent { content_type: String },
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115
116    // ============ PolicyDecision Tests ============
117
118    #[test]
119    fn test_policy_decision_allow() {
120        let decision = PolicyDecision::Allow;
121        assert!(decision.is_allowed());
122        assert!(!decision.is_denied());
123    }
124
125    #[test]
126    fn test_policy_decision_deny() {
127        let decision = PolicyDecision::Deny {
128            reason: "Not authorized".to_string(),
129        };
130        assert!(!decision.is_allowed());
131        assert!(decision.is_denied());
132    }
133
134    #[test]
135    fn test_policy_decision_warn() {
136        let decision = PolicyDecision::Warn {
137            message: "Proceed with caution".to_string(),
138        };
139        // Warn is allowed but logged
140        assert!(decision.is_allowed());
141        assert!(!decision.is_denied());
142    }
143
144    // ============ PolicyContext Tests ============
145
146    #[test]
147    fn test_policy_context_creation() {
148        let mut metadata = std::collections::HashMap::new();
149        metadata.insert("key".to_string(), "value".to_string());
150
151        let context = PolicyContext {
152            tenant_id: Some("tenant-123".to_string()),
153            user_id: Some("user-456".to_string()),
154            action: PolicyAction::StartExecution {
155                graph_id: Some("graph-789".to_string()),
156            },
157            metadata,
158        };
159
160        assert_eq!(context.tenant_id.as_ref().unwrap(), "tenant-123");
161        assert_eq!(context.user_id.as_ref().unwrap(), "user-456");
162        assert!(matches!(
163            context.action,
164            PolicyAction::StartExecution { .. }
165        ));
166    }
167
168    #[test]
169    fn test_policy_action_variants() {
170        let start = PolicyAction::StartExecution { graph_id: None };
171        assert!(matches!(start, PolicyAction::StartExecution { .. }));
172
173        let invoke = PolicyAction::InvokeTool {
174            tool_name: "web_search".to_string(),
175        };
176        assert!(matches!(invoke, PolicyAction::InvokeTool { .. }));
177
178        let llm = PolicyAction::LlmCall {
179            model: "gpt-4".to_string(),
180        };
181        assert!(matches!(llm, PolicyAction::LlmCall { .. }));
182
183        let external = PolicyAction::ExternalAccess {
184            resource: "https://api.example.com".to_string(),
185        };
186        assert!(matches!(external, PolicyAction::ExternalAccess { .. }));
187
188        let output = PolicyAction::OutputContent {
189            content_type: "text/plain".to_string(),
190        };
191        assert!(matches!(output, PolicyAction::OutputContent { .. }));
192    }
193}