1pub mod rules;
11
12use crate::decision::DecisionState;
13use serde::{Deserialize, Serialize};
14
15#[derive(Clone, Debug, Serialize, Deserialize)]
16pub struct PolicyDefinition {
17 pub policy_id: String,
18 pub mode: String,
19 pub description: String,
20 pub rules: serde_json::Value,
21 pub schema_version: u32,
22}
23
24#[derive(Clone, Debug, PartialEq)]
25pub enum PolicyDecision {
26 Allow { reasons: Vec<String> },
27 Deny { reasons: Vec<String> },
28}
29
30#[derive(Clone, Debug, PartialEq)]
31pub enum PolicyError {
32 UnknownPolicy(String),
33 InvalidDefinition(String),
34 PolicyDenied(String),
35}
36
37impl std::fmt::Display for PolicyError {
38 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39 match self {
40 PolicyError::UnknownPolicy(id) => write!(f, "unknown policy: {}", id),
41 PolicyError::InvalidDefinition(msg) => write!(f, "invalid policy definition: {}", msg),
42 PolicyError::PolicyDenied(reason) => write!(f, "policy denied: {}", reason),
43 }
44 }
45}
46
47impl std::error::Error for PolicyError {}
48
49#[derive(Clone, Debug, Serialize, Deserialize)]
55pub struct CommitmentRules {
56 #[serde(default = "default_authority")]
57 pub authority: String,
58 #[serde(default)]
59 pub designated_roles: Vec<String>,
60 #[serde(default)]
61 pub require_vote_quorum: bool,
62}
63
64impl Default for CommitmentRules {
65 fn default() -> Self {
66 Self {
67 authority: default_authority(),
68 designated_roles: Vec::new(),
69 require_vote_quorum: false,
70 }
71 }
72}
73
74fn default_authority() -> String {
75 "initiator_only".into()
76}
77
78pub fn extract_commitment_rules(rules: &serde_json::Value) -> CommitmentRules {
82 rules
83 .get("commitment")
84 .and_then(|c| serde_json::from_value(c.clone()).ok())
85 .unwrap_or_default()
86}
87
88pub trait PolicyEvaluator: Send + Sync {
95 fn evaluate_decision_commitment(
96 &self,
97 policy: &PolicyDefinition,
98 state: &DecisionState,
99 participants: &[String],
100 ) -> PolicyDecision;
101
102 fn evaluate_proposal_commitment(
103 &self,
104 policy: &PolicyDefinition,
105 counter_proposal_count: usize,
106 ) -> PolicyDecision;
107
108 fn evaluate_task_commitment(
109 &self,
110 policy: &PolicyDefinition,
111 has_output: bool,
112 ) -> PolicyDecision;
113
114 fn evaluate_handoff_commitment(&self, policy: &PolicyDefinition) -> PolicyDecision;
115
116 fn evaluate_quorum_commitment(
117 &self,
118 policy: &PolicyDefinition,
119 approve_count: usize,
120 reject_count: usize,
121 abstain_count: usize,
122 total_participants: usize,
123 ) -> PolicyDecision;
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129
130 #[test]
131 fn policy_error_display() {
132 let e = PolicyError::UnknownPolicy("p1".into());
133 assert_eq!(e.to_string(), "unknown policy: p1");
134
135 let e = PolicyError::InvalidDefinition("bad".into());
136 assert_eq!(e.to_string(), "invalid policy definition: bad");
137
138 let e = PolicyError::PolicyDenied("nope".into());
139 assert_eq!(e.to_string(), "policy denied: nope");
140 }
141
142 #[test]
143 fn policy_definition_serialization_round_trip() {
144 let def = PolicyDefinition {
145 policy_id: "test".into(),
146 mode: "*".into(),
147 description: "test policy".into(),
148 rules: serde_json::json!({"voting": {"algorithm": "none"}}),
149 schema_version: 1,
150 };
151 let json = serde_json::to_string(&def).unwrap();
152 let parsed: PolicyDefinition = serde_json::from_str(&json).unwrap();
153 assert_eq!(parsed.policy_id, "test");
154 assert_eq!(parsed.schema_version, 1);
155 }
156
157 #[test]
158 fn commitment_rules_default_is_initiator_only() {
159 let rules = CommitmentRules::default();
160 assert_eq!(rules.authority, "initiator_only");
161 assert!(rules.designated_roles.is_empty());
162 assert!(!rules.require_vote_quorum);
163 }
164
165 #[test]
166 fn extract_commitment_rules_reads_nested_object() {
167 let rules = serde_json::json!({
168 "commitment": { "authority": "designated_role", "designated_roles": ["agent://lead"] }
169 });
170 let parsed = extract_commitment_rules(&rules);
171 assert_eq!(parsed.authority, "designated_role");
172 assert_eq!(parsed.designated_roles, vec!["agent://lead".to_string()]);
173 }
174}