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}