ricecoder_execution/
approval.rs

1//! Approval management for execution plans
2//!
3//! Wraps the ApprovalGate from workflows and provides high-level approval
4//! management for execution plans. Handles approval requests, decisions,
5//! and enforcement of approval gates based on risk level.
6
7use crate::error::{ExecutionError, ExecutionResult};
8use crate::models::{ExecutionPlan, RiskLevel};
9use ricecoder_workflows::approval::{ApprovalGate, ApprovalRequest};
10use std::collections::HashMap;
11
12/// Approval summary for a plan
13#[derive(Debug, Clone)]
14pub struct ApprovalSummary {
15    /// Plan ID
16    pub plan_id: String,
17    /// Plan name
18    pub plan_name: String,
19    /// Number of steps
20    pub step_count: usize,
21    /// Risk level
22    pub risk_level: RiskLevel,
23    /// Risk score
24    pub risk_score: f32,
25    /// Risk factors description
26    pub risk_factors: String,
27    /// Estimated duration in seconds
28    pub estimated_duration_secs: u64,
29    /// Whether approval is required
30    pub requires_approval: bool,
31}
32
33/// Approval manager for execution plans
34///
35/// Manages approval requests and decisions for execution plans.
36/// Wraps the ApprovalGate from workflows and provides plan-specific
37/// approval management.
38pub struct ApprovalManager {
39    /// Underlying approval gate
40    gate: ApprovalGate,
41    /// Map of plan IDs to approval request IDs
42    plan_requests: HashMap<String, String>,
43    /// Map of approval request IDs to plan IDs
44    request_plans: HashMap<String, String>,
45}
46
47impl Default for ApprovalManager {
48    fn default() -> Self {
49        Self::new()
50    }
51}
52
53impl ApprovalManager {
54    /// Create a new approval manager
55    pub fn new() -> Self {
56        ApprovalManager {
57            gate: ApprovalGate::new(),
58            plan_requests: HashMap::new(),
59            request_plans: HashMap::new(),
60        }
61    }
62
63    /// Request approval for a plan
64    ///
65    /// Creates an approval request for the plan and returns the request ID.
66    /// The request will timeout after 30 minutes (1800000 ms).
67    pub fn request_approval(&mut self, plan: &ExecutionPlan) -> ExecutionResult<String> {
68        let summary = ApprovalSummary::from_plan(plan);
69        let message = summary.format_message();
70
71        let request_id = self
72            .gate
73            .request_approval(plan.id.clone(), message, 1_800_000) // 30 minutes
74            .map_err(|e| {
75                ExecutionError::ValidationError(format!("Failed to request approval: {}", e))
76            })?;
77
78        self.plan_requests
79            .insert(plan.id.clone(), request_id.clone());
80        self.request_plans
81            .insert(request_id.clone(), plan.id.clone());
82
83        tracing::info!(
84            plan_id = %plan.id,
85            request_id = %request_id,
86            risk_level = ?plan.risk_score.level,
87            "Approval requested for plan"
88        );
89
90        Ok(request_id)
91    }
92
93    /// Approve a plan
94    ///
95    /// Marks the approval request as approved.
96    pub fn approve(&mut self, request_id: &str, comments: Option<String>) -> ExecutionResult<()> {
97        self.gate
98            .approve(request_id, comments.clone())
99            .map_err(|e| ExecutionError::ValidationError(format!("Failed to approve: {}", e)))?;
100
101        if let Some(plan_id) = self.request_plans.get(request_id) {
102            tracing::info!(
103                plan_id = %plan_id,
104                request_id = %request_id,
105                comments = ?comments,
106                "Plan approved"
107            );
108        }
109
110        Ok(())
111    }
112
113    /// Reject a plan
114    ///
115    /// Marks the approval request as rejected.
116    pub fn reject(&mut self, request_id: &str, comments: Option<String>) -> ExecutionResult<()> {
117        self.gate
118            .reject(request_id, comments.clone())
119            .map_err(|e| ExecutionError::ValidationError(format!("Failed to reject: {}", e)))?;
120
121        if let Some(plan_id) = self.request_plans.get(request_id) {
122            tracing::info!(
123                plan_id = %plan_id,
124                request_id = %request_id,
125                comments = ?comments,
126                "Plan rejected"
127            );
128        }
129
130        Ok(())
131    }
132
133    /// Check if a plan is approved
134    ///
135    /// Returns true if the plan has been approved, false if rejected or pending.
136    pub fn is_approved(&self, request_id: &str) -> ExecutionResult<bool> {
137        self.gate.is_approved(request_id).map_err(|e| {
138            ExecutionError::ValidationError(format!("Failed to check approval status: {}", e))
139        })
140    }
141
142    /// Check if a plan is rejected
143    ///
144    /// Returns true if the plan has been rejected, false if approved or pending.
145    pub fn is_rejected(&self, request_id: &str) -> ExecutionResult<bool> {
146        self.gate.is_rejected(request_id).map_err(|e| {
147            ExecutionError::ValidationError(format!("Failed to check rejection status: {}", e))
148        })
149    }
150
151    /// Check if a request is still pending
152    pub fn is_pending(&self, request_id: &str) -> ExecutionResult<bool> {
153        self.gate.is_pending(request_id).map_err(|e| {
154            ExecutionError::ValidationError(format!("Failed to check pending status: {}", e))
155        })
156    }
157
158    /// Get the approval request details
159    pub fn get_request(&self, request_id: &str) -> ExecutionResult<ApprovalRequest> {
160        self.gate.get_request_status(request_id).map_err(|e| {
161            ExecutionError::ValidationError(format!("Failed to get request status: {}", e))
162        })
163    }
164
165    /// Get all pending approval requests
166    pub fn get_pending_requests(&self) -> Vec<ApprovalRequest> {
167        self.gate.get_pending_requests()
168    }
169
170    /// Get the approval request ID for a plan
171    pub fn get_request_id(&self, plan_id: &str) -> Option<String> {
172        self.plan_requests.get(plan_id).cloned()
173    }
174
175    /// Determine if approval is required based on risk level
176    ///
177    /// Returns true if approval is required for the given risk level.
178    pub fn approval_required(risk_level: RiskLevel) -> bool {
179        matches!(risk_level, RiskLevel::High | RiskLevel::Critical)
180    }
181
182    /// Determine if approval is strongly recommended based on risk level
183    ///
184    /// Returns true if approval is strongly recommended (Critical risk).
185    pub fn approval_strongly_recommended(risk_level: RiskLevel) -> bool {
186        matches!(risk_level, RiskLevel::Critical)
187    }
188}
189
190impl ApprovalSummary {
191    /// Create an approval summary from a plan
192    pub fn from_plan(plan: &ExecutionPlan) -> Self {
193        let risk_factors = plan
194            .risk_score
195            .factors
196            .iter()
197            .map(|f| format!("- {}: {}", f.name, f.description))
198            .collect::<Vec<_>>()
199            .join("\n");
200
201        ApprovalSummary {
202            plan_id: plan.id.clone(),
203            plan_name: plan.name.clone(),
204            step_count: plan.steps.len(),
205            risk_level: plan.risk_score.level,
206            risk_score: plan.risk_score.score,
207            risk_factors,
208            estimated_duration_secs: plan.estimated_duration.as_secs(),
209            requires_approval: plan.requires_approval,
210        }
211    }
212
213    /// Format the approval summary as a message
214    pub fn format_message(&self) -> String {
215        format!(
216            "Plan: {}\nSteps: {}\nRisk Level: {:?}\nRisk Score: {:.2}\nEstimated Duration: {}s\n\nRisk Factors:\n{}",
217            self.plan_name,
218            self.step_count,
219            self.risk_level,
220            self.risk_score,
221            self.estimated_duration_secs,
222            self.risk_factors
223        )
224    }
225}
226
227#[cfg(test)]
228mod tests {
229    use super::*;
230    use crate::models::{ExecutionStep, RiskFactor, RiskScore, StepAction};
231
232    fn create_test_plan() -> ExecutionPlan {
233        let step = ExecutionStep::new(
234            "Test step".to_string(),
235            StepAction::CreateFile {
236                path: "test.txt".to_string(),
237                content: "test".to_string(),
238            },
239        );
240
241        let mut plan = ExecutionPlan::new("Test Plan".to_string(), vec![step]);
242        plan.risk_score = RiskScore {
243            level: RiskLevel::High,
244            score: 1.8,
245            factors: vec![RiskFactor {
246                name: "file_count".to_string(),
247                weight: 0.5,
248                description: "1 file modified".to_string(),
249            }],
250        };
251        plan.requires_approval = true;
252
253        plan
254    }
255
256    #[test]
257    fn test_create_approval_manager() {
258        let manager = ApprovalManager::new();
259        assert_eq!(manager.plan_requests.len(), 0);
260        assert_eq!(manager.request_plans.len(), 0);
261    }
262
263    #[test]
264    fn test_request_approval() {
265        let mut manager = ApprovalManager::new();
266        let plan = create_test_plan();
267
268        let request_id = manager.request_approval(&plan).unwrap();
269        assert!(!request_id.is_empty());
270        assert_eq!(manager.plan_requests.len(), 1);
271        assert_eq!(manager.request_plans.len(), 1);
272    }
273
274    #[test]
275    fn test_approve_plan() {
276        let mut manager = ApprovalManager::new();
277        let plan = create_test_plan();
278
279        let request_id = manager.request_approval(&plan).unwrap();
280        manager
281            .approve(&request_id, Some("Looks good".to_string()))
282            .unwrap();
283
284        assert!(manager.is_approved(&request_id).unwrap());
285        assert!(!manager.is_rejected(&request_id).unwrap());
286        assert!(!manager.is_pending(&request_id).unwrap());
287    }
288
289    #[test]
290    fn test_reject_plan() {
291        let mut manager = ApprovalManager::new();
292        let plan = create_test_plan();
293
294        let request_id = manager.request_approval(&plan).unwrap();
295        manager
296            .reject(&request_id, Some("Needs changes".to_string()))
297            .unwrap();
298
299        assert!(!manager.is_approved(&request_id).unwrap());
300        assert!(manager.is_rejected(&request_id).unwrap());
301        assert!(!manager.is_pending(&request_id).unwrap());
302    }
303
304    #[test]
305    fn test_get_request_id() {
306        let mut manager = ApprovalManager::new();
307        let plan = create_test_plan();
308        let plan_id = plan.id.clone();
309
310        let request_id = manager.request_approval(&plan).unwrap();
311        assert_eq!(manager.get_request_id(&plan_id), Some(request_id));
312    }
313
314    #[test]
315    fn test_approval_required() {
316        assert!(!ApprovalManager::approval_required(RiskLevel::Low));
317        assert!(!ApprovalManager::approval_required(RiskLevel::Medium));
318        assert!(ApprovalManager::approval_required(RiskLevel::High));
319        assert!(ApprovalManager::approval_required(RiskLevel::Critical));
320    }
321
322    #[test]
323    fn test_approval_strongly_recommended() {
324        assert!(!ApprovalManager::approval_strongly_recommended(
325            RiskLevel::Low
326        ));
327        assert!(!ApprovalManager::approval_strongly_recommended(
328            RiskLevel::Medium
329        ));
330        assert!(!ApprovalManager::approval_strongly_recommended(
331            RiskLevel::High
332        ));
333        assert!(ApprovalManager::approval_strongly_recommended(
334            RiskLevel::Critical
335        ));
336    }
337
338    #[test]
339    fn test_approval_summary_from_plan() {
340        let plan = create_test_plan();
341        let summary = ApprovalSummary::from_plan(&plan);
342
343        assert_eq!(summary.plan_id, plan.id);
344        assert_eq!(summary.plan_name, "Test Plan");
345        assert_eq!(summary.step_count, 1);
346        assert_eq!(summary.risk_level, RiskLevel::High);
347        assert!(summary.requires_approval);
348    }
349
350    #[test]
351    fn test_approval_summary_format_message() {
352        let plan = create_test_plan();
353        let summary = ApprovalSummary::from_plan(&plan);
354        let message = summary.format_message();
355
356        assert!(message.contains("Test Plan"));
357        assert!(message.contains("Steps: 1"));
358        assert!(message.contains("High"));
359        assert!(message.contains("Risk Factors"));
360    }
361
362    #[test]
363    fn test_get_pending_requests() {
364        let mut manager = ApprovalManager::new();
365        let plan1 = create_test_plan();
366        let plan2 = create_test_plan();
367
368        let _req1 = manager.request_approval(&plan1).unwrap();
369        let _req2 = manager.request_approval(&plan2).unwrap();
370
371        let pending = manager.get_pending_requests();
372        assert_eq!(pending.len(), 2);
373    }
374}