nexara-core 0.1.1

Core types, policy, registry, broker, and audit schema for Nexara
Documentation
use crate::policy::{ActionClass, TrustTier};
use crate::tool::ToolDescriptor;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum PolicySourceKind {
    OneLine,
    Json,
    HostProvided,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct PolicySource {
    pub kind: PolicySourceKind,
    pub text: String,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct PolicyContract {
    pub id: String,
    pub version: String,
    pub source: PolicySource,
    pub rules: Vec<PolicyRule>,
    pub defaults: PolicyDefaults,
    #[serde(default)]
    pub diagnostics: Vec<PolicyDiagnostic>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub created_at: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct PolicyDefaults {
    pub deny_by_default: bool,
    pub deny_missing_capabilities: bool,
}

impl Default for PolicyDefaults {
    fn default() -> Self {
        Self {
            deny_by_default: true,
            deny_missing_capabilities: true,
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct PolicyRule {
    pub id: String,
    pub effect: PolicyEffect,
    pub selector: PolicySelector,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub condition: Option<PolicyCondition>,
    pub reason: String,
}

#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum PolicyEffect {
    Allow,
    Deny,
    RequireConfirmation,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
pub struct PolicySelector {
    #[serde(default)]
    pub capabilities: Vec<CapabilityPattern>,
    #[serde(default)]
    pub resources: Vec<ResourcePattern>,
    #[serde(default)]
    pub actions: Vec<ActionPattern>,
    #[serde(default)]
    pub tools: Vec<ToolPattern>,
    #[serde(default)]
    pub scopes: Vec<ScopePattern>,
    #[serde(default)]
    pub trust_tiers: Vec<TrustTier>,
    #[serde(default)]
    pub action_classes: Vec<ActionClass>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct CapabilityPattern(pub String);

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ResourcePattern(pub String);

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ActionPattern(pub String);

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ToolPattern(pub String);

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ScopePattern(pub String);

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct PolicyCondition {
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub requires_confirmation: Option<bool>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub max_payload_bytes: Option<usize>,
    #[serde(default)]
    pub allowed_hosts: Vec<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub trust_tier_at_most: Option<TrustTier>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub action_class_is: Option<ActionClass>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct PolicyDiagnostic {
    pub severity: PolicyDiagnosticSeverity,
    pub code: PolicyDiagnosticCode,
    pub message: String,
}

#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum PolicyDiagnosticSeverity {
    Info,
    Warning,
    Error,
}

#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum PolicyDiagnosticCode {
    UnknownResource,
    UnknownOperation,
    AmbiguousVerb,
    ConflictingRule,
    OverBroadAllow,
    MissingCatalogMapping,
    DenyOverridesAllow,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct MatchedPolicyRule {
    pub rule_id: String,
    pub effect: PolicyEffect,
    pub reason: String,
}

#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum PolicyEvaluationDecision {
    Allowed,
    Denied,
    ConfirmationRequired,
    NoMatchingAllow,
    DescriptorMissingCapabilities,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct PolicyEvaluation {
    pub decision: PolicyEvaluationDecision,
    #[serde(default)]
    pub matched_rules: Vec<MatchedPolicyRule>,
    pub explanation: String,
}

impl PolicyContract {
    pub fn evaluate_tool(&self, tool: &ToolDescriptor, confirmed: bool) -> PolicyEvaluation {
        if self.defaults.deny_missing_capabilities && tool.capabilities.is_empty() {
            return PolicyEvaluation {
                decision: PolicyEvaluationDecision::DescriptorMissingCapabilities,
                matched_rules: Vec::new(),
                explanation: format!(
                    "Denied because '{}' does not declare semantic capability metadata.",
                    tool.name
                ),
            };
        }

        let mut matches = Vec::new();
        for rule in &self.rules {
            if rule.selector.matches_tool(tool) {
                matches.push(MatchedPolicyRule {
                    rule_id: rule.id.clone(),
                    effect: rule.effect,
                    reason: rule.reason.clone(),
                });
            }
        }

        if let Some(rule) = matches
            .iter()
            .find(|rule| matches!(rule.effect, PolicyEffect::Deny))
            .cloned()
        {
            return PolicyEvaluation {
                decision: PolicyEvaluationDecision::Denied,
                matched_rules: matches,
                explanation: format!("Denied by policy rule '{}': {}", rule.rule_id, rule.reason),
            };
        }

        if let Some(rule) = matches
            .iter()
            .find(|rule| matches!(rule.effect, PolicyEffect::RequireConfirmation))
            .cloned()
            && !confirmed
        {
            return PolicyEvaluation {
                decision: PolicyEvaluationDecision::ConfirmationRequired,
                matched_rules: matches,
                explanation: format!(
                    "Confirmation required by policy rule '{}': {}",
                    rule.rule_id, rule.reason
                ),
            };
        }

        if matches
            .iter()
            .any(|rule| matches!(rule.effect, PolicyEffect::Allow))
        {
            return PolicyEvaluation {
                decision: PolicyEvaluationDecision::Allowed,
                matched_rules: matches,
                explanation: format!("Allowed by policy contract '{}'.", self.id),
            };
        }

        if self.defaults.deny_by_default {
            PolicyEvaluation {
                decision: PolicyEvaluationDecision::NoMatchingAllow,
                matched_rules: matches,
                explanation: format!(
                    "Denied because '{}' did not match any allow rule in policy contract '{}'.",
                    tool.name, self.id
                ),
            }
        } else {
            PolicyEvaluation {
                decision: PolicyEvaluationDecision::Allowed,
                matched_rules: matches,
                explanation: format!(
                    "Allowed by policy contract '{}' default allow behavior.",
                    self.id
                ),
            }
        }
    }
}

impl PolicySelector {
    pub fn matches_tool(&self, tool: &ToolDescriptor) -> bool {
        self.matches_tools(tool)
            && self.matches_action_classes(tool)
            && self.matches_trust_tiers(tool)
            && self.matches_scopes(tool)
            && self.matches_capabilities(tool)
            && self.matches_resources(tool)
            && self.matches_actions(tool)
    }

    fn matches_tools(&self, tool: &ToolDescriptor) -> bool {
        self.tools.is_empty()
            || self
                .tools
                .iter()
                .any(|pattern| pattern_matches(&pattern.0, &tool.name))
    }

    fn matches_action_classes(&self, tool: &ToolDescriptor) -> bool {
        self.action_classes.is_empty() || self.action_classes.contains(&tool.action_class)
    }

    fn matches_trust_tiers(&self, tool: &ToolDescriptor) -> bool {
        self.trust_tiers.is_empty() || self.trust_tiers.contains(&tool.trust_tier)
    }

    fn matches_scopes(&self, tool: &ToolDescriptor) -> bool {
        self.scopes.is_empty()
            || self.scopes.iter().any(|pattern| {
                tool.scopes
                    .iter()
                    .any(|scope| pattern_matches(&pattern.0, scope))
            })
    }

    fn matches_capabilities(&self, tool: &ToolDescriptor) -> bool {
        self.capabilities.is_empty()
            || self.capabilities.iter().any(|pattern| {
                tool.capabilities
                    .iter()
                    .any(|capability| pattern_matches(&pattern.0, &capability.id))
            })
    }

    fn matches_resources(&self, tool: &ToolDescriptor) -> bool {
        self.resources.is_empty()
            || self.resources.iter().any(|pattern| {
                tool.capabilities.iter().any(|capability| {
                    pattern_matches(&pattern.0, &capability.resource_path.join("."))
                        || pattern_matches(&pattern.0, &capability.resource)
                })
            })
    }

    fn matches_actions(&self, tool: &ToolDescriptor) -> bool {
        self.actions.is_empty()
            || self.actions.iter().any(|pattern| {
                tool.capabilities
                    .iter()
                    .any(|capability| pattern_matches(&pattern.0, &capability.operation))
            })
    }
}

pub fn pattern_matches(pattern: &str, value: &str) -> bool {
    let pattern = pattern.trim();
    let value = value.trim();
    if pattern == "*" || pattern == value {
        return true;
    }
    if let Some(prefix) = pattern.strip_suffix(".*") {
        return value == prefix || value.starts_with(&format!("{prefix}."));
    }
    if let Some(suffix) = pattern.strip_prefix("*.") {
        return value == suffix || value.ends_with(&format!(".{suffix}"));
    }
    false
}

pub fn matched_rules_metadata(rules: &[MatchedPolicyRule]) -> BTreeMap<String, String> {
    let mut metadata = BTreeMap::new();
    if !rules.is_empty() {
        metadata.insert(
            "policy.matched_rules".to_string(),
            rules
                .iter()
                .map(|rule| rule.rule_id.as_str())
                .collect::<Vec<_>>()
                .join(","),
        );
    }
    metadata
}