pub mod rules;
use crate::decision::DecisionState;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct PolicyDefinition {
pub policy_id: String,
pub mode: String,
pub description: String,
pub rules: serde_json::Value,
pub schema_version: u32,
}
#[derive(Clone, Debug, PartialEq)]
pub enum PolicyDecision {
Allow { reasons: Vec<String> },
Deny { reasons: Vec<String> },
}
#[derive(Clone, Debug, PartialEq)]
pub enum PolicyError {
UnknownPolicy(String),
InvalidDefinition(String),
PolicyDenied(String),
}
impl std::fmt::Display for PolicyError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
PolicyError::UnknownPolicy(id) => write!(f, "unknown policy: {}", id),
PolicyError::InvalidDefinition(msg) => write!(f, "invalid policy definition: {}", msg),
PolicyError::PolicyDenied(reason) => write!(f, "policy denied: {}", reason),
}
}
}
impl std::error::Error for PolicyError {}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct CommitmentRules {
#[serde(default = "default_authority")]
pub authority: String,
#[serde(default)]
pub designated_roles: Vec<String>,
#[serde(default)]
pub require_vote_quorum: bool,
}
impl Default for CommitmentRules {
fn default() -> Self {
Self {
authority: default_authority(),
designated_roles: Vec::new(),
require_vote_quorum: false,
}
}
}
fn default_authority() -> String {
"initiator_only".into()
}
pub fn extract_commitment_rules(rules: &serde_json::Value) -> CommitmentRules {
rules
.get("commitment")
.and_then(|c| serde_json::from_value(c.clone()).ok())
.unwrap_or_default()
}
pub trait PolicyEvaluator: Send + Sync {
fn evaluate_decision_commitment(
&self,
policy: &PolicyDefinition,
state: &DecisionState,
participants: &[String],
) -> PolicyDecision;
fn evaluate_proposal_commitment(
&self,
policy: &PolicyDefinition,
counter_proposal_count: usize,
) -> PolicyDecision;
fn evaluate_task_commitment(
&self,
policy: &PolicyDefinition,
has_output: bool,
) -> PolicyDecision;
fn evaluate_handoff_commitment(&self, policy: &PolicyDefinition) -> PolicyDecision;
fn evaluate_quorum_commitment(
&self,
policy: &PolicyDefinition,
approve_count: usize,
reject_count: usize,
abstain_count: usize,
total_participants: usize,
) -> PolicyDecision;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn policy_error_display() {
let e = PolicyError::UnknownPolicy("p1".into());
assert_eq!(e.to_string(), "unknown policy: p1");
let e = PolicyError::InvalidDefinition("bad".into());
assert_eq!(e.to_string(), "invalid policy definition: bad");
let e = PolicyError::PolicyDenied("nope".into());
assert_eq!(e.to_string(), "policy denied: nope");
}
#[test]
fn policy_definition_serialization_round_trip() {
let def = PolicyDefinition {
policy_id: "test".into(),
mode: "*".into(),
description: "test policy".into(),
rules: serde_json::json!({"voting": {"algorithm": "none"}}),
schema_version: 1,
};
let json = serde_json::to_string(&def).unwrap();
let parsed: PolicyDefinition = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.policy_id, "test");
assert_eq!(parsed.schema_version, 1);
}
#[test]
fn commitment_rules_default_is_initiator_only() {
let rules = CommitmentRules::default();
assert_eq!(rules.authority, "initiator_only");
assert!(rules.designated_roles.is_empty());
assert!(!rules.require_vote_quorum);
}
#[test]
fn extract_commitment_rules_reads_nested_object() {
let rules = serde_json::json!({
"commitment": { "authority": "designated_role", "designated_roles": ["agent://lead"] }
});
let parsed = extract_commitment_rules(&rules);
assert_eq!(parsed.authority, "designated_role");
assert_eq!(parsed.designated_roles, vec!["agent://lead".to_string()]);
}
}