Skip to main content

macp_core/policy/
rules.rs

1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
3
4// ── Decision Policy Rules ───────────────────────────────────────────
5
6#[derive(Clone, Debug, Serialize, Deserialize, Default)]
7pub struct DecisionPolicyRules {
8    #[serde(default)]
9    pub voting: VotingRules,
10    #[serde(default)]
11    pub objection_handling: ObjectionHandlingRules,
12    #[serde(default)]
13    pub evaluation: EvaluationRules,
14    #[serde(default)]
15    pub commitment: CommitmentRules,
16}
17
18#[derive(Clone, Debug, Serialize, Deserialize)]
19pub struct VotingRules {
20    #[serde(default = "default_algorithm")]
21    pub algorithm: String,
22    #[serde(default = "default_threshold")]
23    pub threshold: f64,
24    #[serde(default)]
25    pub quorum: QuorumRules,
26    #[serde(default)]
27    pub weights: HashMap<String, f64>,
28}
29
30impl Default for VotingRules {
31    fn default() -> Self {
32        Self {
33            algorithm: default_algorithm(),
34            threshold: default_threshold(),
35            quorum: QuorumRules::default(),
36            weights: HashMap::new(),
37        }
38    }
39}
40
41fn default_algorithm() -> String {
42    "none".into()
43}
44
45fn default_threshold() -> f64 {
46    0.5
47}
48
49/// Quorum rules used inside Decision mode's `voting.quorum`.
50#[derive(Clone, Debug, Serialize, Deserialize)]
51pub struct QuorumRules {
52    #[serde(default = "default_quorum_type", rename = "type")]
53    pub quorum_type: String,
54    #[serde(default)]
55    pub value: f64,
56}
57
58impl Default for QuorumRules {
59    fn default() -> Self {
60        Self {
61            quorum_type: default_quorum_type(),
62            value: 0.0,
63        }
64    }
65}
66
67fn default_quorum_type() -> String {
68    "count".into()
69}
70
71#[derive(Clone, Debug, Serialize, Deserialize)]
72pub struct ObjectionHandlingRules {
73    /// RFC-MACP-0012: objections with severity "critical" trigger veto logic.
74    #[serde(default, alias = "critical_severity_vetoes")]
75    pub critical_severity_vetoes: bool,
76    #[serde(default = "default_veto_threshold")]
77    pub veto_threshold: u32,
78    /// What a triggered critical-objection veto does to a commitment.
79    /// Defaults to [`CriticalObjectionAction::Deny`] — the historical hard-stop
80    /// that blocks every commitment. Operators in adverse-action domains
81    /// (claims/lending) generally want `deny` or `hold`; `finalize_decline` is
82    /// opt-in because auto-finalizing a denial off a single critical objection
83    /// is itself a regulated adverse action.
84    #[serde(default)]
85    pub critical_objection_action: CriticalObjectionAction,
86}
87
88impl Default for ObjectionHandlingRules {
89    fn default() -> Self {
90        Self {
91            critical_severity_vetoes: false,
92            veto_threshold: default_veto_threshold(),
93            critical_objection_action: CriticalObjectionAction::default(),
94        }
95    }
96}
97
98fn default_veto_threshold() -> u32 {
99    1
100}
101
102/// How a triggered critical-objection veto resolves a commitment attempt.
103#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
104#[serde(rename_all = "snake_case")]
105pub enum CriticalObjectionAction {
106    /// Hard-stop: the veto blocks every commitment, positive or negative
107    /// (historical behavior, conservative default).
108    #[default]
109    Deny,
110    /// The veto permits a *negative* commitment (`outcome_positive = false`) to
111    /// finalize, while still blocking a positive one.
112    FinalizeDecline,
113    /// The veto blocks the commitment but signals the session should be held
114    /// open for human escalation rather than treated as a permanent denial.
115    /// At the evaluator layer this denies the commitment (leaving the session
116    /// open); the distinct reason string marks it as an escalation hold.
117    Hold,
118}
119
120#[derive(Clone, Debug, Serialize, Deserialize)]
121pub struct EvaluationRules {
122    #[serde(default)]
123    pub required_before_voting: bool,
124    #[serde(default)]
125    pub minimum_confidence: f64,
126}
127
128impl Default for EvaluationRules {
129    fn default() -> Self {
130        Self {
131            required_before_voting: false,
132            minimum_confidence: 0.0,
133        }
134    }
135}
136
137// `CommitmentRules` is shared with the modes (which read it directly to
138// authorize commitments), so it lives in `macp-core`. Re-exported here so the
139// per-mode rule structs below and `crate::rules::CommitmentRules` keep
140// resolving.
141pub use super::CommitmentRules;
142
143// ── Proposal Policy Rules (RFC-MACP-0012 Section 4.3) ──────────────
144
145#[derive(Clone, Debug, Serialize, Deserialize, Default)]
146pub struct ProposalPolicyRules {
147    #[serde(default)]
148    pub acceptance: ProposalAcceptanceRules,
149    #[serde(default)]
150    pub counter_proposal: CounterProposalRules,
151    #[serde(default)]
152    pub rejection: RejectionRules,
153    #[serde(default)]
154    pub commitment: CommitmentRules,
155}
156
157#[derive(Clone, Debug, Serialize, Deserialize)]
158pub struct ProposalAcceptanceRules {
159    #[serde(default = "default_acceptance_criterion")]
160    pub criterion: String,
161}
162
163impl Default for ProposalAcceptanceRules {
164    fn default() -> Self {
165        Self {
166            criterion: default_acceptance_criterion(),
167        }
168    }
169}
170
171fn default_acceptance_criterion() -> String {
172    "all_parties".into()
173}
174
175#[derive(Clone, Debug, Default, Serialize, Deserialize)]
176pub struct CounterProposalRules {
177    #[serde(default)]
178    pub max_rounds: usize,
179}
180
181#[derive(Clone, Debug, Default, Serialize, Deserialize)]
182pub struct RejectionRules {
183    #[serde(default)]
184    pub terminal_on_any_reject: bool,
185}
186
187// ── Task Policy Rules (RFC-MACP-0012 Section 4.4) ──────────────────
188
189#[derive(Clone, Debug, Serialize, Deserialize, Default)]
190pub struct TaskPolicyRules {
191    #[serde(default)]
192    pub assignment: TaskAssignmentRules,
193    #[serde(default)]
194    pub completion: TaskCompletionRules,
195    #[serde(default)]
196    pub commitment: CommitmentRules,
197}
198
199#[derive(Clone, Debug, Default, Serialize, Deserialize)]
200pub struct TaskAssignmentRules {
201    #[serde(default)]
202    pub allow_reassignment_on_reject: bool,
203}
204
205#[derive(Clone, Debug, Default, Serialize, Deserialize)]
206pub struct TaskCompletionRules {
207    #[serde(default)]
208    pub require_output: bool,
209}
210
211// ── Handoff Policy Rules (RFC-MACP-0012 Section 4.5) ───────────────
212
213#[derive(Clone, Debug, Serialize, Deserialize, Default)]
214pub struct HandoffPolicyRules {
215    #[serde(default)]
216    pub acceptance: HandoffAcceptanceRules,
217    #[serde(default)]
218    pub commitment: CommitmentRules,
219}
220
221#[derive(Clone, Debug, Default, Serialize, Deserialize)]
222pub struct HandoffAcceptanceRules {
223    #[serde(default)]
224    pub implicit_accept_timeout_ms: u64,
225}
226
227// ── Quorum Policy Rules (RFC-MACP-0012 Section 4.2) ────────────────
228
229#[derive(Clone, Debug, Serialize, Deserialize, Default)]
230pub struct QuorumPolicyRules {
231    #[serde(default)]
232    pub threshold: QuorumThreshold,
233    #[serde(default)]
234    pub abstention: AbstentionRules,
235    #[serde(default)]
236    pub commitment: CommitmentRules,
237}
238
239/// Threshold rules for Quorum mode (distinct from `QuorumRules` used in Decision mode's `voting.quorum`).
240#[derive(Clone, Debug, Serialize, Deserialize)]
241pub struct QuorumThreshold {
242    #[serde(default = "default_threshold_type", rename = "type")]
243    pub threshold_type: String,
244    #[serde(default)]
245    pub value: f64,
246}
247
248impl Default for QuorumThreshold {
249    fn default() -> Self {
250        Self {
251            threshold_type: default_threshold_type(),
252            value: 0.0,
253        }
254    }
255}
256
257fn default_threshold_type() -> String {
258    "n_of_m".into()
259}
260
261#[derive(Clone, Debug, Serialize, Deserialize)]
262pub struct AbstentionRules {
263    #[serde(default)]
264    pub counts_toward_quorum: bool,
265    #[serde(default = "default_interpretation")]
266    pub interpretation: String,
267}
268
269impl Default for AbstentionRules {
270    fn default() -> Self {
271        Self {
272            counts_toward_quorum: false,
273            interpretation: default_interpretation(),
274        }
275    }
276}
277
278fn default_interpretation() -> String {
279    "neutral".into()
280}
281
282#[cfg(test)]
283mod tests {
284    use super::*;
285
286    #[test]
287    fn decision_policy_rules_defaults() {
288        let rules = DecisionPolicyRules::default();
289        assert_eq!(rules.voting.algorithm, "none");
290        assert!((rules.voting.threshold - 0.5).abs() < f64::EPSILON);
291        assert_eq!(rules.voting.quorum.quorum_type, "count");
292        assert!(!rules.objection_handling.critical_severity_vetoes);
293        assert_eq!(rules.objection_handling.veto_threshold, 1);
294        assert_eq!(
295            rules.objection_handling.critical_objection_action,
296            CriticalObjectionAction::Deny
297        );
298        assert!(!rules.commitment.allow_decline_over_approval);
299        assert!(!rules.evaluation.required_before_voting);
300        assert!((rules.evaluation.minimum_confidence).abs() < f64::EPSILON);
301        assert_eq!(rules.commitment.authority, "initiator_only");
302        assert!(rules.commitment.designated_roles.is_empty());
303        assert!(!rules.commitment.require_vote_quorum);
304    }
305
306    #[test]
307    fn decision_policy_rules_deserialization() {
308        let json = serde_json::json!({
309            "voting": {
310                "algorithm": "majority",
311                "threshold": 0.6,
312                "quorum": { "type": "percentage", "value": 75.0 },
313                "weights": { "agent://fraud": 2.0, "agent://growth": 1.0 }
314            },
315            "objection_handling": {
316                "critical_severity_vetoes": true,
317                "veto_threshold": 2
318            },
319            "evaluation": {
320                "required_before_voting": true,
321                "minimum_confidence": 0.8
322            },
323            "commitment": {
324                "authority": "designated_role",
325                "designated_roles": ["agent://lead"],
326                "require_vote_quorum": true
327            }
328        });
329
330        let rules: DecisionPolicyRules = serde_json::from_value(json).unwrap();
331        assert_eq!(rules.voting.algorithm, "majority");
332        assert!((rules.voting.threshold - 0.6).abs() < f64::EPSILON);
333        assert_eq!(rules.voting.quorum.quorum_type, "percentage");
334        assert!((rules.voting.quorum.value - 75.0).abs() < f64::EPSILON);
335        assert_eq!(*rules.voting.weights.get("agent://fraud").unwrap(), 2.0);
336        assert!(rules.objection_handling.critical_severity_vetoes);
337        assert_eq!(rules.objection_handling.veto_threshold, 2);
338        assert!(rules.evaluation.required_before_voting);
339        assert!((rules.evaluation.minimum_confidence - 0.8).abs() < f64::EPSILON);
340        assert_eq!(rules.commitment.authority, "designated_role");
341        assert_eq!(rules.commitment.designated_roles, vec!["agent://lead"]);
342        assert!(rules.commitment.require_vote_quorum);
343    }
344
345    #[test]
346    fn partial_deserialization_fills_defaults() {
347        let json = serde_json::json!({
348            "voting": { "algorithm": "unanimous" }
349        });
350        let rules: DecisionPolicyRules = serde_json::from_value(json).unwrap();
351        assert_eq!(rules.voting.algorithm, "unanimous");
352        assert!((rules.voting.threshold - 0.5).abs() < f64::EPSILON);
353        assert!(!rules.objection_handling.critical_severity_vetoes);
354        assert_eq!(rules.objection_handling.veto_threshold, 1);
355    }
356
357    #[test]
358    fn proposal_policy_rules_defaults() {
359        let rules = ProposalPolicyRules::default();
360        assert_eq!(rules.acceptance.criterion, "all_parties");
361        assert_eq!(rules.counter_proposal.max_rounds, 0);
362        assert!(!rules.rejection.terminal_on_any_reject);
363        assert_eq!(rules.commitment.authority, "initiator_only");
364    }
365
366    #[test]
367    fn proposal_policy_rules_deserialization() {
368        let json = serde_json::json!({
369            "acceptance": { "criterion": "counterparty" },
370            "counter_proposal": { "max_rounds": 3 },
371            "rejection": { "terminal_on_any_reject": true },
372            "commitment": { "authority": "any_participant" }
373        });
374        let rules: ProposalPolicyRules = serde_json::from_value(json).unwrap();
375        assert_eq!(rules.acceptance.criterion, "counterparty");
376        assert_eq!(rules.counter_proposal.max_rounds, 3);
377        assert!(rules.rejection.terminal_on_any_reject);
378        assert_eq!(rules.commitment.authority, "any_participant");
379    }
380
381    #[test]
382    fn task_policy_rules_defaults() {
383        let rules = TaskPolicyRules::default();
384        assert!(!rules.assignment.allow_reassignment_on_reject);
385        assert!(!rules.completion.require_output);
386        assert_eq!(rules.commitment.authority, "initiator_only");
387    }
388
389    #[test]
390    fn task_policy_rules_deserialization() {
391        let json = serde_json::json!({
392            "assignment": { "allow_reassignment_on_reject": true },
393            "completion": { "require_output": true },
394            "commitment": { "authority": "initiator_only" }
395        });
396        let rules: TaskPolicyRules = serde_json::from_value(json).unwrap();
397        assert!(rules.assignment.allow_reassignment_on_reject);
398        assert!(rules.completion.require_output);
399    }
400
401    #[test]
402    fn handoff_policy_rules_defaults() {
403        let rules = HandoffPolicyRules::default();
404        assert_eq!(rules.acceptance.implicit_accept_timeout_ms, 0);
405        assert_eq!(rules.commitment.authority, "initiator_only");
406    }
407
408    #[test]
409    fn handoff_policy_rules_deserialization() {
410        let json = serde_json::json!({
411            "acceptance": { "implicit_accept_timeout_ms": 5000 },
412            "commitment": { "authority": "any_participant" }
413        });
414        let rules: HandoffPolicyRules = serde_json::from_value(json).unwrap();
415        assert_eq!(rules.acceptance.implicit_accept_timeout_ms, 5000);
416        assert_eq!(rules.commitment.authority, "any_participant");
417    }
418
419    #[test]
420    fn quorum_policy_rules_defaults() {
421        let rules = QuorumPolicyRules::default();
422        assert_eq!(rules.threshold.threshold_type, "n_of_m");
423        assert!((rules.threshold.value).abs() < f64::EPSILON);
424        assert!(!rules.abstention.counts_toward_quorum);
425        assert_eq!(rules.abstention.interpretation, "neutral");
426        assert_eq!(rules.commitment.authority, "initiator_only");
427    }
428
429    #[test]
430    fn quorum_policy_rules_deserialization() {
431        let json = serde_json::json!({
432            "threshold": { "type": "percentage", "value": 75.0 },
433            "abstention": { "counts_toward_quorum": true, "interpretation": "implicit_reject" },
434            "commitment": { "authority": "initiator_only" }
435        });
436        let rules: QuorumPolicyRules = serde_json::from_value(json).unwrap();
437        assert_eq!(rules.threshold.threshold_type, "percentage");
438        assert!((rules.threshold.value - 75.0).abs() < f64::EPSILON);
439        assert!(rules.abstention.counts_toward_quorum);
440        assert_eq!(rules.abstention.interpretation, "implicit_reject");
441    }
442}