Skip to main content

deepseek_execpolicy/
lib.rs

1use std::collections::HashSet;
2
3use anyhow::Result;
4use deepseek_protocol::{NetworkPolicyAmendment, NetworkPolicyRuleAction};
5use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
8#[serde(rename_all = "snake_case")]
9pub enum AskForApproval {
10    UnlessTrusted,
11    OnFailure,
12    OnRequest,
13    Reject {
14        sandbox_approval: bool,
15        rules: bool,
16        mcp_elicitations: bool,
17    },
18    Never,
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
22pub struct ExecPolicyAmendment {
23    pub prefixes: Vec<String>,
24}
25
26#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
27pub enum ExecApprovalRequirement {
28    Skip {
29        bypass_sandbox: bool,
30        proposed_execpolicy_amendment: Option<ExecPolicyAmendment>,
31    },
32    NeedsApproval {
33        reason: String,
34        proposed_execpolicy_amendment: Option<ExecPolicyAmendment>,
35        proposed_network_policy_amendments: Vec<NetworkPolicyAmendment>,
36    },
37    Forbidden {
38        reason: String,
39    },
40}
41
42impl ExecApprovalRequirement {
43    pub fn reason(&self) -> &str {
44        match self {
45            ExecApprovalRequirement::Skip { .. } => "Execution allowed by policy.",
46            ExecApprovalRequirement::NeedsApproval { reason, .. } => reason,
47            ExecApprovalRequirement::Forbidden { reason } => reason,
48        }
49    }
50
51    pub fn phase(&self) -> &'static str {
52        match self {
53            ExecApprovalRequirement::Skip { .. } => "allowed",
54            ExecApprovalRequirement::NeedsApproval { .. } => "needs_approval",
55            ExecApprovalRequirement::Forbidden { .. } => "forbidden",
56        }
57    }
58}
59
60#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
61pub struct ExecPolicyDecision {
62    pub allow: bool,
63    pub requires_approval: bool,
64    pub requirement: ExecApprovalRequirement,
65    pub matched_rule: Option<String>,
66}
67
68impl ExecPolicyDecision {
69    pub fn reason(&self) -> &str {
70        self.requirement.reason()
71    }
72}
73
74#[derive(Debug, Clone)]
75pub struct ExecPolicyContext<'a> {
76    pub command: &'a str,
77    pub cwd: &'a str,
78    pub ask_for_approval: AskForApproval,
79    pub sandbox_mode: Option<&'a str>,
80}
81
82#[derive(Debug, Clone, Default)]
83pub struct ExecPolicyEngine {
84    trusted_prefixes: Vec<String>,
85    denied_prefixes: Vec<String>,
86    approved_for_session: HashSet<String>,
87}
88
89impl ExecPolicyEngine {
90    pub fn new(trusted_prefixes: Vec<String>, denied_prefixes: Vec<String>) -> Self {
91        Self {
92            trusted_prefixes,
93            denied_prefixes,
94            approved_for_session: HashSet::new(),
95        }
96    }
97
98    pub fn remember_session_approval(&mut self, approval_key: String) {
99        self.approved_for_session.insert(approval_key);
100    }
101
102    pub fn is_session_approved(&self, approval_key: &str) -> bool {
103        self.approved_for_session.contains(approval_key)
104    }
105
106    pub fn check(&self, ctx: ExecPolicyContext<'_>) -> Result<ExecPolicyDecision> {
107        let normalized = normalize_command(ctx.command);
108        if let Some(rule) = self
109            .denied_prefixes
110            .iter()
111            .find(|rule| normalized.starts_with(&normalize_command(rule)))
112        {
113            return Ok(ExecPolicyDecision {
114                allow: false,
115                requires_approval: false,
116                matched_rule: Some(rule.clone()),
117                requirement: ExecApprovalRequirement::Forbidden {
118                    reason: format!("Command blocked by denied prefix rule '{rule}'"),
119                },
120            });
121        }
122
123        let trusted_rule = self
124            .trusted_prefixes
125            .iter()
126            .find(|rule| normalized.starts_with(&normalize_command(rule)))
127            .cloned();
128        let is_trusted = trusted_rule.is_some();
129
130        let requirement = match ctx.ask_for_approval {
131            AskForApproval::Never => ExecApprovalRequirement::Skip {
132                bypass_sandbox: false,
133                proposed_execpolicy_amendment: None,
134            },
135            AskForApproval::UnlessTrusted if is_trusted => ExecApprovalRequirement::Skip {
136                bypass_sandbox: false,
137                proposed_execpolicy_amendment: None,
138            },
139            AskForApproval::OnFailure => ExecApprovalRequirement::Skip {
140                bypass_sandbox: false,
141                proposed_execpolicy_amendment: None,
142            },
143            AskForApproval::Reject { rules, .. } if rules => ExecApprovalRequirement::Forbidden {
144                reason: "Policy is configured to reject rule-exceptions.".to_string(),
145            },
146            _ => ExecApprovalRequirement::NeedsApproval {
147                reason: if is_trusted {
148                    "Approval requested by policy mode.".to_string()
149                } else {
150                    "Unmatched command prefix requires approval.".to_string()
151                },
152                proposed_execpolicy_amendment: if is_trusted {
153                    None
154                } else {
155                    Some(ExecPolicyAmendment {
156                        prefixes: vec![first_token(ctx.command)],
157                    })
158                },
159                proposed_network_policy_amendments: vec![NetworkPolicyAmendment {
160                    host: ctx.cwd.to_string(),
161                    action: NetworkPolicyRuleAction::Allow,
162                }],
163            },
164        };
165
166        let (allow, requires_approval) = match requirement {
167            ExecApprovalRequirement::Skip { .. } => (true, false),
168            ExecApprovalRequirement::NeedsApproval { .. } => (true, true),
169            ExecApprovalRequirement::Forbidden { .. } => (false, false),
170        };
171
172        Ok(ExecPolicyDecision {
173            allow,
174            requires_approval,
175            matched_rule: trusted_rule,
176            requirement,
177        })
178    }
179}
180
181fn normalize_command(value: &str) -> String {
182    value.trim().to_ascii_lowercase()
183}
184
185fn first_token(command: &str) -> String {
186    command
187        .split_whitespace()
188        .next()
189        .unwrap_or_default()
190        .to_string()
191}