enact-core 0.0.1

Core agent runtime for Enact - Graph-Native AI agents
Documentation
//! Policy - Execution constraints and guardrails
//!
//! Policies define what is ALLOWED during execution:
//! - Execution policies: limits, timeouts, retries
//! - Tool policies: permissions, network/fs access, PII handling
//! - Tenant policies: quotas, feature flags, isolation
//! - Input processors: pre-execution validation (PII, prompt injection)
//!
//! ## Architecture
//!
//! Policies are evaluated BEFORE execution, not during business logic.
//! This ensures:
//! - Clear separation of concerns
//! - Auditable policy decisions
//! - No policy logic scattered in execution code
//!
//! ```text
//! ┌─────────────────────────────────────────────┐
//! │              Policy Evaluator               │
//! │  ┌─────────────────────────────────────┐    │
//! │  │ ExecutionPolicy │ ToolPolicy │ ...  │    │
//! │  └─────────────────────────────────────┘    │
//! │                     │                       │
//! │                     ▼                       │
//! │           Allow / Deny / Warn              │
//! └─────────────────────────────────────────────┘
//!//!//!              ExecutionKernel
//! ```
//!
//! @see docs/TECHNICAL/17-GUARDRAILS-PROTECTION.md
//! @see docs/TECHNICAL/25-STREAM-PROCESSORS.md

mod execution_policy;
mod filters;
mod tenant_policy;
mod tool_policy;

// Input processors (feat-09: Guardrails)
mod input_processor;
mod long_running;
mod pii_input;

pub use execution_policy::{ExecutionLimits, ExecutionPolicy};
pub use filters::{ContentFilter, FilterAction, FilterResult};
pub use long_running::{
    CheckpointPolicy, ContextStrategy, LongRunningExecutionPolicy, WorkingMemoryPolicy,
};
pub use tenant_policy::{FeatureFlags, TenantLimits, TenantPolicy};
pub use tool_policy::{ToolPermissions, ToolPolicy, ToolTrustLevel};

// Input processor exports
pub use input_processor::{InputProcessor, InputProcessorPipeline, InputProcessorResult};
pub use pii_input::{PiiInputMode, PiiInputProcessor};

/// Policy decision result
#[derive(Debug, Clone)]
pub enum PolicyDecision {
    /// Action is allowed
    Allow,
    /// Action is denied with reason
    Deny { reason: String },
    /// Action is allowed but logged/warned
    Warn { message: String },
}

impl PolicyDecision {
    pub fn is_allowed(&self) -> bool {
        matches!(self, PolicyDecision::Allow | PolicyDecision::Warn { .. })
    }

    pub fn is_denied(&self) -> bool {
        matches!(self, PolicyDecision::Deny { .. })
    }
}

/// Trait for policy evaluators
pub trait PolicyEvaluator: Send + Sync {
    /// Evaluate a policy for the given context
    fn evaluate(&self, context: &PolicyContext) -> PolicyDecision;
}

/// Context for policy evaluation
#[derive(Debug, Clone)]
pub struct PolicyContext {
    /// Tenant ID (if multi-tenant)
    pub tenant_id: Option<String>,
    /// User ID
    pub user_id: Option<String>,
    /// Current action being evaluated
    pub action: PolicyAction,
    /// Additional metadata
    pub metadata: std::collections::HashMap<String, String>,
}

/// Actions that can be evaluated by policies
#[derive(Debug, Clone)]
pub enum PolicyAction {
    /// Starting an execution
    StartExecution { graph_id: Option<String> },
    /// Invoking a tool
    InvokeTool { tool_name: String },
    /// Making an LLM call
    LlmCall { model: String },
    /// Accessing external resource
    ExternalAccess { resource: String },
    /// Outputting content
    OutputContent { content_type: String },
}

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

    // ============ PolicyDecision Tests ============

    #[test]
    fn test_policy_decision_allow() {
        let decision = PolicyDecision::Allow;
        assert!(decision.is_allowed());
        assert!(!decision.is_denied());
    }

    #[test]
    fn test_policy_decision_deny() {
        let decision = PolicyDecision::Deny {
            reason: "Not authorized".to_string(),
        };
        assert!(!decision.is_allowed());
        assert!(decision.is_denied());
    }

    #[test]
    fn test_policy_decision_warn() {
        let decision = PolicyDecision::Warn {
            message: "Proceed with caution".to_string(),
        };
        // Warn is allowed but logged
        assert!(decision.is_allowed());
        assert!(!decision.is_denied());
    }

    // ============ PolicyContext Tests ============

    #[test]
    fn test_policy_context_creation() {
        let mut metadata = std::collections::HashMap::new();
        metadata.insert("key".to_string(), "value".to_string());

        let context = PolicyContext {
            tenant_id: Some("tenant-123".to_string()),
            user_id: Some("user-456".to_string()),
            action: PolicyAction::StartExecution {
                graph_id: Some("graph-789".to_string()),
            },
            metadata,
        };

        assert_eq!(context.tenant_id.as_ref().unwrap(), "tenant-123");
        assert_eq!(context.user_id.as_ref().unwrap(), "user-456");
        assert!(matches!(
            context.action,
            PolicyAction::StartExecution { .. }
        ));
    }

    #[test]
    fn test_policy_action_variants() {
        let start = PolicyAction::StartExecution { graph_id: None };
        assert!(matches!(start, PolicyAction::StartExecution { .. }));

        let invoke = PolicyAction::InvokeTool {
            tool_name: "web_search".to_string(),
        };
        assert!(matches!(invoke, PolicyAction::InvokeTool { .. }));

        let llm = PolicyAction::LlmCall {
            model: "gpt-4".to_string(),
        };
        assert!(matches!(llm, PolicyAction::LlmCall { .. }));

        let external = PolicyAction::ExternalAccess {
            resource: "https://api.example.com".to_string(),
        };
        assert!(matches!(external, PolicyAction::ExternalAccess { .. }));

        let output = PolicyAction::OutputContent {
            content_type: "text/plain".to_string(),
        };
        assert!(matches!(output, PolicyAction::OutputContent { .. }));
    }
}