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#[non_exhaustive]
27#[derive(Clone, Debug, PartialEq)]
28pub enum PolicyDecision {
29 Allow { reasons: Vec<String> },
30 Deny { reasons: Vec<String> },
31}
32
33#[non_exhaustive]
34#[derive(Clone, Debug, PartialEq)]
35pub enum PolicyError {
36 UnknownPolicy(String),
37 InvalidDefinition(String),
38 PolicyDenied(String),
39}
40
41impl std::fmt::Display for PolicyError {
42 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43 match self {
44 PolicyError::UnknownPolicy(id) => write!(f, "unknown policy: {}", id),
45 PolicyError::InvalidDefinition(msg) => write!(f, "invalid policy definition: {}", msg),
46 PolicyError::PolicyDenied(reason) => write!(f, "policy denied: {}", reason),
47 }
48 }
49}
50
51impl std::error::Error for PolicyError {}
52
53#[derive(Clone, Debug, Serialize, Deserialize)]
59pub struct CommitmentRules {
60 #[serde(default = "default_authority")]
61 pub authority: String,
62 #[serde(default)]
63 pub designated_roles: Vec<String>,
64 #[serde(default)]
65 pub require_vote_quorum: bool,
66 #[serde(default)]
72 pub allow_decline_over_approval: bool,
73}
74
75impl Default for CommitmentRules {
76 fn default() -> Self {
77 Self {
78 authority: default_authority(),
79 designated_roles: Vec::new(),
80 require_vote_quorum: false,
81 allow_decline_over_approval: false,
82 }
83 }
84}
85
86fn default_authority() -> String {
87 "initiator_only".into()
88}
89
90pub fn extract_commitment_rules(rules: &serde_json::Value) -> CommitmentRules {
94 rules
95 .get("commitment")
96 .and_then(|c| serde_json::from_value(c.clone()).ok())
97 .unwrap_or_default()
98}
99
100pub struct CommitmentContext<'a> {
108 pub policy: &'a PolicyDefinition,
109 pub participants: &'a [String],
110 pub outcome_positive: bool,
111 pub mode: CommitmentMode<'a>,
112}
113
114#[non_exhaustive]
121pub enum CommitmentMode<'a> {
122 Decision {
123 state: &'a DecisionState,
124 },
125 Proposal {
126 counter_proposal_count: usize,
127 },
128 Task {
129 has_output: bool,
130 },
131 Handoff,
132 Quorum {
133 approve_count: usize,
134 reject_count: usize,
135 abstain_count: usize,
136 },
137}
138
139pub trait PolicyEvaluator: Send + Sync {
150 fn evaluate_commitment(&self, ctx: &CommitmentContext<'_>) -> PolicyDecision;
154
155 #[deprecated(note = "build a CommitmentContext and call evaluate_commitment")]
156 fn evaluate_decision_commitment(
157 &self,
158 policy: &PolicyDefinition,
159 state: &DecisionState,
160 participants: &[String],
161 ) -> PolicyDecision {
162 self.evaluate_commitment(&CommitmentContext {
163 policy,
164 participants,
165 outcome_positive: true,
166 mode: CommitmentMode::Decision { state },
167 })
168 }
169
170 #[deprecated(note = "build a CommitmentContext and call evaluate_commitment")]
171 fn evaluate_decision_commitment_outcome(
172 &self,
173 policy: &PolicyDefinition,
174 state: &DecisionState,
175 participants: &[String],
176 outcome_positive: bool,
177 ) -> PolicyDecision {
178 self.evaluate_commitment(&CommitmentContext {
179 policy,
180 participants,
181 outcome_positive,
182 mode: CommitmentMode::Decision { state },
183 })
184 }
185
186 #[deprecated(note = "build a CommitmentContext and call evaluate_commitment")]
187 fn evaluate_proposal_commitment(
188 &self,
189 policy: &PolicyDefinition,
190 counter_proposal_count: usize,
191 ) -> PolicyDecision {
192 self.evaluate_commitment(&CommitmentContext {
193 policy,
194 participants: &[],
195 outcome_positive: true,
196 mode: CommitmentMode::Proposal {
197 counter_proposal_count,
198 },
199 })
200 }
201
202 #[deprecated(note = "build a CommitmentContext and call evaluate_commitment")]
203 fn evaluate_task_commitment(
204 &self,
205 policy: &PolicyDefinition,
206 has_output: bool,
207 ) -> PolicyDecision {
208 self.evaluate_commitment(&CommitmentContext {
209 policy,
210 participants: &[],
211 outcome_positive: true,
212 mode: CommitmentMode::Task { has_output },
213 })
214 }
215
216 #[deprecated(note = "build a CommitmentContext and call evaluate_commitment")]
217 fn evaluate_handoff_commitment(&self, policy: &PolicyDefinition) -> PolicyDecision {
218 self.evaluate_commitment(&CommitmentContext {
219 policy,
220 participants: &[],
221 outcome_positive: true,
222 mode: CommitmentMode::Handoff,
223 })
224 }
225
226 #[deprecated(note = "build a CommitmentContext and call evaluate_commitment")]
227 fn evaluate_quorum_commitment(
228 &self,
229 policy: &PolicyDefinition,
230 approve_count: usize,
231 reject_count: usize,
232 abstain_count: usize,
233 total_participants: usize,
234 ) -> PolicyDecision {
235 let _ = total_participants;
241 self.evaluate_commitment(&CommitmentContext {
242 policy,
243 participants: &[],
244 outcome_positive: true,
245 mode: CommitmentMode::Quorum {
246 approve_count,
247 reject_count,
248 abstain_count,
249 },
250 })
251 }
252}
253
254#[cfg(test)]
255mod tests {
256 use super::*;
257
258 #[test]
259 fn policy_error_display() {
260 let e = PolicyError::UnknownPolicy("p1".into());
261 assert_eq!(e.to_string(), "unknown policy: p1");
262
263 let e = PolicyError::InvalidDefinition("bad".into());
264 assert_eq!(e.to_string(), "invalid policy definition: bad");
265
266 let e = PolicyError::PolicyDenied("nope".into());
267 assert_eq!(e.to_string(), "policy denied: nope");
268 }
269
270 #[test]
271 fn policy_definition_serialization_round_trip() {
272 let def = PolicyDefinition {
273 policy_id: "test".into(),
274 mode: "*".into(),
275 description: "test policy".into(),
276 rules: serde_json::json!({"voting": {"algorithm": "none"}}),
277 schema_version: 1,
278 };
279 let json = serde_json::to_string(&def).unwrap();
280 let parsed: PolicyDefinition = serde_json::from_str(&json).unwrap();
281 assert_eq!(parsed.policy_id, "test");
282 assert_eq!(parsed.schema_version, 1);
283 }
284
285 #[test]
286 fn commitment_rules_default_is_initiator_only() {
287 let rules = CommitmentRules::default();
288 assert_eq!(rules.authority, "initiator_only");
289 assert!(rules.designated_roles.is_empty());
290 assert!(!rules.require_vote_quorum);
291 }
292
293 #[test]
294 fn extract_commitment_rules_reads_nested_object() {
295 let rules = serde_json::json!({
296 "commitment": { "authority": "designated_role", "designated_roles": ["agent://lead"] }
297 });
298 let parsed = extract_commitment_rules(&rules);
299 assert_eq!(parsed.authority, "designated_role");
300 assert_eq!(parsed.designated_roles, vec!["agent://lead".to_string()]);
301 }
302}