Skip to main content

vtcode_core/exec_policy/
approval.rs

1//! Approval requirement types for execution policy.
2
3use serde::{Deserialize, Serialize};
4
5/// Fine-grained rejection controls for approval prompts.
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
7#[serde(default)]
8pub struct RejectConfig {
9    /// Reject approval prompts related to sandbox escalation, including
10    /// `with_additional_permissions`.
11    pub sandbox_approval: bool,
12    /// Reject prompts triggered by policy `prompt` rules.
13    pub rules: bool,
14    /// Reject built-in permission request prompts that are separate from
15    /// sandbox approval.
16    pub request_permissions: bool,
17    /// Reject MCP elicitation prompts.
18    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/// Policy for when to ask for approval before executing commands.
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
41#[serde(rename_all = "snake_case")]
42pub enum AskForApproval {
43    /// Never ask for approval (autonomous mode).
44    Never,
45
46    /// Ask only when explicitly requested by policy.
47    OnRequest,
48
49    /// Ask unless the command is in the trusted list.
50    #[default]
51    UnlessTrusted,
52
53    /// Ask only on failure (retry with approval).
54    OnFailure,
55
56    /// Fine-grained rejection controls for approval prompts.
57    Reject(RejectConfig),
58}
59
60impl AskForApproval {
61    /// Check if this policy requires asking for unknown commands.
62    pub fn requires_approval_for_unknown(&self) -> bool {
63        matches!(
64            self,
65            Self::UnlessTrusted | Self::OnRequest | Self::Reject(_)
66        )
67    }
68
69    /// Check whether rule-triggered approval prompts are rejected.
70    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    /// Check whether sandbox-related approval prompts are rejected.
79    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    /// Check whether built-in permission request prompts are rejected.
88    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    /// Check whether MCP elicitation prompts are rejected.
97    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/// Compute the default approval requirement for a tool invocation.
107///
108/// `requires_sandbox_approval_prompt` should be `true` when the selected
109/// sandbox mode still requires an approval prompt, and `false` when the tool is
110/// already running unsandboxed or under an external sandbox that should not
111/// trigger an additional sandbox approval prompt.
112#[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/// A proposed amendment to the execution policy.
136///
137/// When a command requires approval but isn't explicitly forbidden,
138/// this amendment can be proposed to allow similar commands in the future.
139#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
140pub struct ExecPolicyAmendment {
141    /// The command pattern to add to the policy.
142    pub pattern: Vec<String>,
143}
144
145impl ExecPolicyAmendment {
146    /// Create a new policy amendment.
147    pub fn new(pattern: Vec<String>) -> Self {
148        Self { pattern }
149    }
150
151    /// Create from a single command prefix.
152    pub fn from_prefix(prefix: impl Into<String>) -> Self {
153        Self {
154            pattern: vec![prefix.into()],
155        }
156    }
157
158    /// Check if a command matches this amendment pattern.
159    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    /// Convert the amendment to a policy rule string.
170    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    /// Get the command pattern for Codex compatibility.
176    pub fn command_pattern(&self) -> &[String] {
177        &self.pattern
178    }
179}
180
181/// Requirement for approval before executing a command.
182///
183/// This enum represents the outcome of evaluating a command against the
184/// execution policy, indicating whether the command can proceed, needs
185/// approval, or is forbidden.
186#[derive(Debug, Clone, PartialEq, Eq)]
187pub enum ExecApprovalRequirement {
188    /// Command can be executed without approval.
189    Skip {
190        /// Whether to bypass the sandbox for this command.
191        bypass_sandbox: bool,
192        /// Proposed policy amendment if the user wants to trust this command.
193        proposed_execpolicy_amendment: Option<ExecPolicyAmendment>,
194    },
195
196    /// Command requires user approval before execution.
197    NeedsApproval {
198        /// Reason for requiring approval.
199        reason: Option<String>,
200        /// Proposed policy amendment to skip future approvals.
201        proposed_execpolicy_amendment: Option<ExecPolicyAmendment>,
202    },
203
204    /// Command is forbidden by policy and cannot be executed.
205    Forbidden {
206        /// Reason for forbidding the command.
207        reason: String,
208    },
209}
210
211impl ExecApprovalRequirement {
212    /// Create a skip requirement.
213    pub fn skip() -> Self {
214        Self::Skip {
215            bypass_sandbox: false,
216            proposed_execpolicy_amendment: None,
217        }
218    }
219
220    /// Create a skip requirement with sandbox bypass.
221    pub fn skip_with_bypass() -> Self {
222        Self::Skip {
223            bypass_sandbox: true,
224            proposed_execpolicy_amendment: None,
225        }
226    }
227
228    /// Create an approval requirement.
229    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    /// Create a needs approval requirement with an amendment.
237    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    /// Create a forbidden requirement.
248    pub fn forbidden(reason: impl Into<String>) -> Self {
249        Self::Forbidden {
250            reason: reason.into(),
251        }
252    }
253
254    /// Check if approval is needed.
255    pub fn requires_approval(&self) -> bool {
256        matches!(self, Self::NeedsApproval { .. })
257    }
258
259    /// Check if the command is forbidden.
260    pub fn is_forbidden(&self) -> bool {
261        matches!(self, Self::Forbidden { .. })
262    }
263
264    /// Check if the command can proceed (skip or approved).
265    pub fn can_proceed(&self) -> bool {
266        matches!(self, Self::Skip { .. })
267    }
268
269    /// Get the proposed amendment, if any.
270    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    /// Get the proposed exec policy amendment if any (Codex-compatible name).
285    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}