rivox 1.0.0

Universal polyglot build coordination layer for Python, Rust, and Node monorepos
Documentation
use crate::graph::GraphNode;
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::Path;

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct PolicyConfig {
    pub allowed_ecosystems: Vec<String>,
    pub blocked_packages: Vec<String>,
    pub require_sandbox: bool,
    pub allow_network: bool,
    pub require_provenance: bool,
    pub max_artifact_size_mb: u64,
}

impl Default for PolicyConfig {
    fn default() -> Self {
        Self {
            allowed_ecosystems: vec![
                "python".to_string(),
                "rust".to_string(),
                "node".to_string(),
                "go".to_string(),
                "gradle".to_string(),
            ],
            blocked_packages: Vec::new(),
            require_sandbox: true,
            allow_network: false,
            require_provenance: true,
            max_artifact_size_mb: 500,
        }
    }
}

#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
pub enum PolicyStatus {
    Pass,
    Warn,
    Fail,
}

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct PolicyViolation {
    pub rule: String,
    pub severity: String, // "ERROR" or "WARNING"
    pub message: String,
}

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct PolicyEvaluationResult {
    pub status: PolicyStatus,
    pub violations: Vec<PolicyViolation>,
}

pub struct PolicyEngine {
    pub config: PolicyConfig,
}

impl PolicyEngine {
    pub fn new(config: PolicyConfig) -> Self {
        Self { config }
    }

    pub fn load_from_file(policy_path: &Path) -> Result<Self> {
        if !policy_path.exists() {
            return Ok(Self::new(PolicyConfig::default()));
        }

        let content = fs::read_to_string(policy_path)
            .with_context(|| format!("Failed to read policy file at {}", policy_path.display()))?;

        let config: PolicyConfig = toml::from_str(&content)
            .with_context(|| format!("Failed to parse policy TOML at {}", policy_path.display()))?;

        Ok(Self::new(config))
    }

    pub fn evaluate_graph(&self, nodes: &[GraphNode]) -> PolicyEvaluationResult {
        let mut violations = Vec::new();

        for node in nodes {
            // Check allowed ecosystems
            if !self.config.allowed_ecosystems.is_empty()
                && !self.config.allowed_ecosystems.contains(&node.ecosystem)
            {
                violations.push(PolicyViolation {
                    rule: "allowed_ecosystems".to_string(),
                    severity: "ERROR".to_string(),
                    message: format!(
                        "Ecosystem [{}] is not permitted by policy for package [{}]",
                        node.ecosystem, node.package_name
                    ),
                });
            }

            // Check blocked packages
            if self.config.blocked_packages.contains(&node.package_name) {
                violations.push(PolicyViolation {
                    rule: "blocked_packages".to_string(),
                    severity: "ERROR".to_string(),
                    message: format!(
                        "Package [{}] is explicitly blocked by organization policy",
                        node.package_name
                    ),
                });
            }
        }

        let has_errors = violations.iter().any(|v| v.severity == "ERROR");
        let has_warnings = violations.iter().any(|v| v.severity == "WARNING");

        let status = if has_errors {
            PolicyStatus::Fail
        } else if has_warnings {
            PolicyStatus::Warn
        } else {
            PolicyStatus::Pass
        };

        PolicyEvaluationResult { status, violations }
    }
}