Skip to main content

macp_core/policy/
mod.rs

1//! Policy vocabulary and the pluggable evaluation trait.
2//!
3//! Core holds the types modes and the kernel must name: the policy
4//! definition/decision/error, the per-mode [`rules`] schemas (read by modes to
5//! drive policy-parameterized behavior and by evaluators to decide commitments),
6//! and the [`PolicyEvaluator`] trait that modes call through. The concrete
7//! default evaluator lives in the `macp-policy` crate; a third party can supply
8//! its own `PolicyEvaluator` and inject it without forking the kernel.
9
10pub 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/// Commitment rules shared across all mode policy schemas (RFC-MACP-0012).
50///
51/// This `commitment` sub-object appears in every mode's rule schema and is read
52/// directly by the modes (to authorize who may emit a `Commitment`), so it
53/// lives in core rather than in `macp-policy`.
54#[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
78/// Extract the `commitment` section from any mode's policy rules JSON.
79/// All RFC mode schemas include a `commitment` sub-object with `authority` and
80/// `designated_roles`.
81pub 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
88/// Governance policy evaluation at commitment time.
89///
90/// The runtime resolves a [`PolicyDefinition`] at `SessionStart` and stores it
91/// on the session; at commitment time a mode calls the matching method here.
92/// The default implementation lives in `macp-policy`
93/// (`macp_policy::DefaultPolicyEvaluator`); consumers may provide their own.
94pub 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}