1use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
7#[serde(default)]
8pub struct RejectConfig {
9 pub sandbox_approval: bool,
12 pub rules: bool,
14 pub request_permissions: bool,
17 pub mcp_elicitations: bool,
19}
20
21impl RejectConfig {
22 pub const fn rejects_sandbox_approval(self) -> bool {
23 self.sandbox_approval
24 }
25
26 pub const fn rejects_rules_approval(self) -> bool {
27 self.rules
28 }
29
30 pub const fn rejects_request_permissions(self) -> bool {
31 self.request_permissions
32 }
33
34 pub const fn rejects_mcp_elicitations(self) -> bool {
35 self.mcp_elicitations
36 }
37}
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
41#[serde(rename_all = "snake_case")]
42pub enum AskForApproval {
43 Never,
45
46 OnRequest,
48
49 #[default]
51 UnlessTrusted,
52
53 OnFailure,
55
56 Reject(RejectConfig),
58}
59
60impl AskForApproval {
61 pub fn requires_approval_for_unknown(&self) -> bool {
63 matches!(
64 self,
65 Self::UnlessTrusted | Self::OnRequest | Self::Reject(_)
66 )
67 }
68
69 pub const fn rejects_rule_prompt(self) -> bool {
71 match self {
72 Self::Never => true,
73 Self::Reject(reject_config) => reject_config.rejects_rules_approval(),
74 Self::OnFailure | Self::OnRequest | Self::UnlessTrusted => false,
75 }
76 }
77
78 pub const fn rejects_sandbox_prompt(self) -> bool {
80 match self {
81 Self::Never => true,
82 Self::Reject(reject_config) => reject_config.rejects_sandbox_approval(),
83 Self::OnFailure | Self::OnRequest | Self::UnlessTrusted => false,
84 }
85 }
86
87 pub const fn rejects_request_permission_prompt(self) -> bool {
89 match self {
90 Self::Never => true,
91 Self::Reject(reject_config) => reject_config.rejects_request_permissions(),
92 Self::OnFailure | Self::OnRequest | Self::UnlessTrusted => false,
93 }
94 }
95
96 pub const fn rejects_mcp_elicitation(self) -> bool {
98 match self {
99 Self::Never => true,
100 Self::Reject(reject_config) => reject_config.rejects_mcp_elicitations(),
101 Self::OnFailure | Self::OnRequest | Self::UnlessTrusted => false,
102 }
103 }
104}
105
106#[must_use]
113pub fn default_exec_approval_requirement(
114 policy: AskForApproval,
115 requires_sandbox_approval_prompt: bool,
116) -> ExecApprovalRequirement {
117 let needs_approval = match policy {
118 AskForApproval::Never | AskForApproval::OnFailure => false,
119 AskForApproval::OnRequest | AskForApproval::Reject(_) => requires_sandbox_approval_prompt,
120 AskForApproval::UnlessTrusted => true,
121 };
122
123 if needs_approval && policy.rejects_sandbox_prompt() {
124 ExecApprovalRequirement::forbidden("approval policy rejected sandbox approval prompt")
125 } else if needs_approval {
126 ExecApprovalRequirement::NeedsApproval {
127 reason: None,
128 proposed_execpolicy_amendment: None,
129 }
130 } else {
131 ExecApprovalRequirement::skip()
132 }
133}
134
135#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
140pub struct ExecPolicyAmendment {
141 pub pattern: Vec<String>,
143}
144
145impl ExecPolicyAmendment {
146 pub fn new(pattern: Vec<String>) -> Self {
148 Self { pattern }
149 }
150
151 pub fn from_prefix(prefix: impl Into<String>) -> Self {
153 Self {
154 pattern: vec![prefix.into()],
155 }
156 }
157
158 pub fn matches(&self, command: &[String]) -> bool {
160 if command.len() < self.pattern.len() {
161 return false;
162 }
163 self.pattern
164 .iter()
165 .zip(command.iter())
166 .all(|(pattern, cmd)| pattern == cmd)
167 }
168
169 pub fn to_rule_string(&self) -> String {
171 let pattern_json = serde_json::to_string(&self.pattern).unwrap_or_default();
172 format!("prefix_rule(pattern={}, decision=\"allow\")", pattern_json)
173 }
174
175 pub fn command_pattern(&self) -> &[String] {
177 &self.pattern
178 }
179}
180
181#[derive(Debug, Clone, PartialEq, Eq)]
187pub enum ExecApprovalRequirement {
188 Skip {
190 bypass_sandbox: bool,
192 proposed_execpolicy_amendment: Option<ExecPolicyAmendment>,
194 },
195
196 NeedsApproval {
198 reason: Option<String>,
200 proposed_execpolicy_amendment: Option<ExecPolicyAmendment>,
202 },
203
204 Forbidden {
206 reason: String,
208 },
209}
210
211impl ExecApprovalRequirement {
212 pub fn skip() -> Self {
214 Self::Skip {
215 bypass_sandbox: false,
216 proposed_execpolicy_amendment: None,
217 }
218 }
219
220 pub fn skip_with_bypass() -> Self {
222 Self::Skip {
223 bypass_sandbox: true,
224 proposed_execpolicy_amendment: None,
225 }
226 }
227
228 pub fn needs_approval(reason: impl Into<String>) -> Self {
230 Self::NeedsApproval {
231 reason: Some(reason.into()),
232 proposed_execpolicy_amendment: None,
233 }
234 }
235
236 pub fn needs_approval_with_amendment(
238 reason: Option<String>,
239 amendment: ExecPolicyAmendment,
240 ) -> Self {
241 Self::NeedsApproval {
242 reason,
243 proposed_execpolicy_amendment: Some(amendment),
244 }
245 }
246
247 pub fn forbidden(reason: impl Into<String>) -> Self {
249 Self::Forbidden {
250 reason: reason.into(),
251 }
252 }
253
254 pub fn requires_approval(&self) -> bool {
256 matches!(self, Self::NeedsApproval { .. })
257 }
258
259 pub fn is_forbidden(&self) -> bool {
261 matches!(self, Self::Forbidden { .. })
262 }
263
264 pub fn can_proceed(&self) -> bool {
266 matches!(self, Self::Skip { .. })
267 }
268
269 pub fn get_amendment(&self) -> Option<&ExecPolicyAmendment> {
271 match self {
272 Self::Skip {
273 proposed_execpolicy_amendment,
274 ..
275 } => proposed_execpolicy_amendment.as_ref(),
276 Self::NeedsApproval {
277 proposed_execpolicy_amendment,
278 ..
279 } => proposed_execpolicy_amendment.as_ref(),
280 Self::Forbidden { .. } => None,
281 }
282 }
283
284 pub fn proposed_execpolicy_amendment(&self) -> Option<&ExecPolicyAmendment> {
286 self.get_amendment()
287 }
288}
289
290#[cfg(test)]
291mod tests {
292 use super::*;
293 use serde_json::json;
294
295 #[test]
296 fn test_skip_requirement() {
297 let req = ExecApprovalRequirement::skip();
298 assert!(req.can_proceed());
299 assert!(!req.requires_approval());
300 assert!(!req.is_forbidden());
301 }
302
303 #[test]
304 fn test_needs_approval_requirement() {
305 let req = ExecApprovalRequirement::needs_approval("dangerous command");
306 assert!(!req.can_proceed());
307 assert!(req.requires_approval());
308 assert!(!req.is_forbidden());
309 }
310
311 #[test]
312 fn test_forbidden_requirement() {
313 let req = ExecApprovalRequirement::forbidden("policy violation");
314 assert!(!req.can_proceed());
315 assert!(!req.requires_approval());
316 assert!(req.is_forbidden());
317 }
318
319 #[test]
320 fn test_amendment() {
321 let amendment = ExecPolicyAmendment::new(vec!["cargo".to_string(), "build".to_string()]);
322 let rule = amendment.to_rule_string();
323 assert!(rule.contains("cargo"));
324 assert!(rule.contains("build"));
325 assert!(rule.contains("allow"));
326 }
327
328 #[test]
329 fn test_reject_config_helpers() {
330 let config = RejectConfig {
331 sandbox_approval: true,
332 rules: false,
333 request_permissions: false,
334 mcp_elicitations: true,
335 };
336 assert!(config.rejects_sandbox_approval());
337 assert!(!config.rejects_rules_approval());
338 assert!(!config.rejects_request_permissions());
339 assert!(config.rejects_mcp_elicitations());
340 }
341
342 #[test]
343 fn test_ask_for_approval_rejection_helpers() {
344 assert!(AskForApproval::Never.rejects_rule_prompt());
345 assert!(AskForApproval::Never.rejects_sandbox_prompt());
346 assert!(AskForApproval::Never.rejects_request_permission_prompt());
347 assert!(AskForApproval::Never.rejects_mcp_elicitation());
348
349 assert!(!AskForApproval::OnRequest.rejects_rule_prompt());
350 assert!(!AskForApproval::OnRequest.rejects_sandbox_prompt());
351 assert!(!AskForApproval::OnRequest.rejects_request_permission_prompt());
352 assert!(!AskForApproval::OnRequest.rejects_mcp_elicitation());
353
354 let sandbox_reject_policy = AskForApproval::Reject(RejectConfig {
355 sandbox_approval: true,
356 rules: false,
357 request_permissions: false,
358 mcp_elicitations: true,
359 });
360 assert!(!sandbox_reject_policy.rejects_rule_prompt());
361 assert!(sandbox_reject_policy.rejects_sandbox_prompt());
362 assert!(!sandbox_reject_policy.rejects_request_permission_prompt());
363 assert!(sandbox_reject_policy.rejects_mcp_elicitation());
364
365 let request_permissions_reject_policy = AskForApproval::Reject(RejectConfig {
366 sandbox_approval: false,
367 rules: false,
368 request_permissions: true,
369 mcp_elicitations: false,
370 });
371 assert!(!request_permissions_reject_policy.rejects_rule_prompt());
372 assert!(!request_permissions_reject_policy.rejects_sandbox_prompt());
373 assert!(request_permissions_reject_policy.rejects_request_permission_prompt());
374 assert!(!request_permissions_reject_policy.rejects_mcp_elicitation());
375 }
376
377 #[test]
378 fn test_reject_policy_serde_roundtrip() {
379 let value = json!({
380 "reject": {
381 "sandbox_approval": true,
382 "rules": false,
383 "mcp_elicitations": true
384 }
385 });
386 let policy: AskForApproval = serde_json::from_value(value).expect("deserialize policy");
387 assert_eq!(
388 policy,
389 AskForApproval::Reject(RejectConfig {
390 sandbox_approval: true,
391 rules: false,
392 request_permissions: false,
393 mcp_elicitations: true,
394 })
395 );
396
397 let serialized = serde_json::to_value(policy).expect("serialize policy");
398 assert_eq!(
399 serialized,
400 json!({
401 "reject": {
402 "sandbox_approval": true,
403 "rules": false,
404 "request_permissions": false,
405 "mcp_elicitations": true
406 }
407 })
408 );
409 }
410
411 #[test]
412 fn test_reject_policy_defaults_missing_request_permissions_to_false() {
413 let policy: AskForApproval = serde_json::from_value(json!({
414 "reject": {
415 "sandbox_approval": true,
416 "rules": false,
417 "mcp_elicitations": true
418 }
419 }))
420 .expect("deserialize legacy reject policy");
421
422 assert_eq!(
423 policy,
424 AskForApproval::Reject(RejectConfig {
425 sandbox_approval: true,
426 rules: false,
427 request_permissions: false,
428 mcp_elicitations: true,
429 })
430 );
431 }
432
433 #[test]
434 fn default_exec_approval_requirement_skips_for_never() {
435 let requirement = default_exec_approval_requirement(AskForApproval::Never, true);
436
437 assert_eq!(requirement, ExecApprovalRequirement::skip());
438 }
439
440 #[test]
441 fn default_exec_approval_requirement_skips_for_on_failure() {
442 let requirement = default_exec_approval_requirement(AskForApproval::OnFailure, true);
443
444 assert_eq!(requirement, ExecApprovalRequirement::skip());
445 }
446
447 #[test]
448 fn default_exec_approval_requirement_requires_approval_for_on_request() {
449 let requirement = default_exec_approval_requirement(AskForApproval::OnRequest, true);
450
451 assert_eq!(
452 requirement,
453 ExecApprovalRequirement::NeedsApproval {
454 reason: None,
455 proposed_execpolicy_amendment: None,
456 }
457 );
458 }
459
460 #[test]
461 fn default_exec_approval_requirement_skips_on_request_without_prompt() {
462 let requirement = default_exec_approval_requirement(AskForApproval::OnRequest, false);
463
464 assert_eq!(requirement, ExecApprovalRequirement::skip());
465 }
466
467 #[test]
468 fn default_exec_approval_requirement_requires_approval_for_unless_trusted() {
469 let requirement = default_exec_approval_requirement(AskForApproval::UnlessTrusted, false);
470
471 assert_eq!(
472 requirement,
473 ExecApprovalRequirement::NeedsApproval {
474 reason: None,
475 proposed_execpolicy_amendment: None,
476 }
477 );
478 }
479
480 #[test]
481 fn default_exec_approval_requirement_rejects_sandbox_prompt_when_configured() {
482 let policy = AskForApproval::Reject(RejectConfig {
483 sandbox_approval: true,
484 rules: false,
485 request_permissions: false,
486 mcp_elicitations: false,
487 });
488
489 let requirement = default_exec_approval_requirement(policy, true);
490
491 assert_eq!(
492 requirement,
493 ExecApprovalRequirement::Forbidden {
494 reason: "approval policy rejected sandbox approval prompt".to_string(),
495 }
496 );
497 }
498
499 #[test]
500 fn default_exec_approval_requirement_ignores_request_permission_rejection() {
501 let policy = AskForApproval::Reject(RejectConfig {
502 sandbox_approval: false,
503 rules: false,
504 request_permissions: true,
505 mcp_elicitations: false,
506 });
507
508 let requirement = default_exec_approval_requirement(policy, false);
509
510 assert_eq!(requirement, ExecApprovalRequirement::skip());
511 }
512
513 #[test]
514 fn default_exec_approval_requirement_keeps_prompt_when_rejection_disabled() {
515 let policy = AskForApproval::Reject(RejectConfig {
516 sandbox_approval: false,
517 rules: true,
518 request_permissions: false,
519 mcp_elicitations: true,
520 });
521
522 let requirement = default_exec_approval_requirement(policy, true);
523
524 assert_eq!(
525 requirement,
526 ExecApprovalRequirement::NeedsApproval {
527 reason: None,
528 proposed_execpolicy_amendment: None,
529 }
530 );
531 }
532}