Skip to main content

vtcode_safety/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    const fn rejects_rules_approval(self) -> bool {
27        self.rules
28    }
29
30    const fn rejects_request_permissions(self) -> bool {
31        self.request_permissions
32    }
33
34    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 during autonomous execution.
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!(self, Self::UnlessTrusted | Self::OnRequest | Self::Reject(_))
64    }
65
66    /// Check whether rule-triggered approval prompts are rejected.
67    pub const fn rejects_rule_prompt(self) -> bool {
68        match self {
69            Self::Never => true,
70            Self::Reject(reject_config) => reject_config.rejects_rules_approval(),
71            Self::OnFailure | Self::OnRequest | Self::UnlessTrusted => false,
72        }
73    }
74
75    /// Check whether sandbox-related approval prompts are rejected.
76    pub const fn rejects_sandbox_prompt(self) -> bool {
77        match self {
78            Self::Never => true,
79            Self::Reject(reject_config) => reject_config.rejects_sandbox_approval(),
80            Self::OnFailure | Self::OnRequest | Self::UnlessTrusted => false,
81        }
82    }
83
84    /// Check whether built-in permission request prompts are rejected.
85    pub const fn rejects_request_permission_prompt(self) -> bool {
86        match self {
87            Self::Never => true,
88            Self::Reject(reject_config) => reject_config.rejects_request_permissions(),
89            Self::OnFailure | Self::OnRequest | Self::UnlessTrusted => false,
90        }
91    }
92
93    /// Check whether MCP elicitation prompts are rejected.
94    pub const fn rejects_mcp_elicitation(self) -> bool {
95        match self {
96            Self::Never => true,
97            Self::Reject(reject_config) => reject_config.rejects_mcp_elicitations(),
98            Self::OnFailure | Self::OnRequest | Self::UnlessTrusted => false,
99        }
100    }
101}
102
103/// Compute the default approval requirement for a tool invocation.
104///
105/// `requires_sandbox_approval_prompt` should be `true` when the selected
106/// sandbox policy still requires an approval prompt, and `false` when the tool is
107/// already running unsandboxed or under an external sandbox that should not
108/// trigger an additional sandbox approval prompt.
109#[must_use]
110pub fn default_exec_approval_requirement(
111    policy: AskForApproval,
112    requires_sandbox_approval_prompt: bool,
113) -> ExecApprovalRequirement {
114    let needs_approval = match policy {
115        AskForApproval::Never | AskForApproval::OnFailure => false,
116        AskForApproval::OnRequest | AskForApproval::Reject(_) => requires_sandbox_approval_prompt,
117        AskForApproval::UnlessTrusted => true,
118    };
119
120    if needs_approval && policy.rejects_sandbox_prompt() {
121        ExecApprovalRequirement::forbidden("approval policy rejected sandbox approval prompt")
122    } else if needs_approval {
123        ExecApprovalRequirement::NeedsApproval { reason: None, proposed_execpolicy_amendment: None }
124    } else {
125        ExecApprovalRequirement::skip()
126    }
127}
128
129/// A proposed amendment to the execution policy.
130///
131/// When a command requires approval but isn't explicitly forbidden,
132/// this amendment can be proposed to allow similar commands in the future.
133#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
134pub struct ExecPolicyAmendment {
135    /// The command pattern to add to the policy.
136    pattern: Vec<String>,
137}
138
139impl ExecPolicyAmendment {
140    /// Create a new policy amendment.
141    fn new(pattern: Vec<String>) -> Self {
142        Self { pattern }
143    }
144
145    /// Create from a single command prefix.
146    pub(crate) fn from_prefix(prefix: impl Into<String>) -> Self {
147        Self { pattern: vec![prefix.into()] }
148    }
149
150    /// Check if a command matches this amendment pattern. Zero-token patterns never match.
151    pub(crate) fn matches(&self, command: &[String]) -> bool {
152        super::policy::matches_prefix(&self.pattern, command)
153    }
154
155    /// Convert the amendment to a policy rule string.
156    fn to_rule_string(&self) -> String {
157        let pattern_json = serde_json::to_string(&self.pattern).unwrap_or_default();
158        format!("prefix_rule(pattern={pattern_json}, decision=\"allow\")")
159    }
160
161    /// Get the command pattern for Codex compatibility.
162    pub fn command_pattern(&self) -> &[String] {
163        &self.pattern
164    }
165}
166
167/// Requirement for approval before executing a command.
168///
169/// This enum represents the outcome of evaluating a command against the
170/// execution policy, indicating whether the command can proceed, needs
171/// approval, or is forbidden.
172#[derive(Debug, Clone, PartialEq, Eq)]
173pub enum ExecApprovalRequirement {
174    /// Command can be executed without approval.
175    Skip {
176        /// Whether to bypass the sandbox for this command.
177        bypass_sandbox: bool,
178        /// Proposed policy amendment if the user wants to trust this command.
179        proposed_execpolicy_amendment: Option<ExecPolicyAmendment>,
180    },
181
182    /// Command requires user approval before execution.
183    NeedsApproval {
184        /// Reason for requiring approval.
185        reason: Option<String>,
186        /// Proposed policy amendment to skip future approvals.
187        proposed_execpolicy_amendment: Option<ExecPolicyAmendment>,
188    },
189
190    /// Command is forbidden by policy and cannot be executed.
191    Forbidden {
192        /// Reason for forbidding the command.
193        reason: String,
194    },
195}
196
197impl ExecApprovalRequirement {
198    /// Create a skip requirement.
199    pub(crate) fn skip() -> Self {
200        Self::Skip {
201            bypass_sandbox: false,
202            proposed_execpolicy_amendment: None,
203        }
204    }
205
206    /// Create a skip requirement with sandbox bypass.
207    pub fn skip_with_bypass() -> Self {
208        Self::Skip {
209            bypass_sandbox: true,
210            proposed_execpolicy_amendment: None,
211        }
212    }
213
214    /// Create an approval requirement.
215    pub(crate) fn needs_approval(reason: impl Into<String>) -> Self {
216        Self::NeedsApproval {
217            reason: Some(reason.into()),
218            proposed_execpolicy_amendment: None,
219        }
220    }
221
222    /// Create a needs approval requirement with an amendment.
223    pub fn needs_approval_with_amendment(reason: Option<String>, amendment: ExecPolicyAmendment) -> Self {
224        Self::NeedsApproval {
225            reason,
226            proposed_execpolicy_amendment: Some(amendment),
227        }
228    }
229
230    /// Create a forbidden requirement.
231    pub(crate) fn forbidden(reason: impl Into<String>) -> Self {
232        Self::Forbidden { reason: reason.into() }
233    }
234
235    /// Check if approval is needed.
236    pub(crate) fn requires_approval(&self) -> bool {
237        matches!(self, Self::NeedsApproval { .. })
238    }
239
240    /// Check if the command is forbidden.
241    pub(crate) fn is_forbidden(&self) -> bool {
242        matches!(self, Self::Forbidden { .. })
243    }
244
245    /// Check if the command can proceed (skip or approved).
246    pub(crate) fn can_proceed(&self) -> bool {
247        matches!(self, Self::Skip { .. })
248    }
249
250    /// Get the proposed amendment, if any.
251    pub fn get_amendment(&self) -> Option<&ExecPolicyAmendment> {
252        match self {
253            Self::Skip { proposed_execpolicy_amendment, .. } => proposed_execpolicy_amendment.as_ref(),
254            Self::NeedsApproval { proposed_execpolicy_amendment, .. } => proposed_execpolicy_amendment.as_ref(),
255            Self::Forbidden { .. } => None,
256        }
257    }
258
259    /// Get the proposed exec policy amendment if any (Codex-compatible name).
260    pub fn proposed_execpolicy_amendment(&self) -> Option<&ExecPolicyAmendment> {
261        self.get_amendment()
262    }
263}
264
265#[cfg(test)]
266mod tests {
267    use super::*;
268    use serde_json::json;
269
270    #[test]
271    fn test_skip_requirement() {
272        let req = ExecApprovalRequirement::skip();
273        assert!(req.can_proceed());
274        assert!(!req.requires_approval());
275        assert!(!req.is_forbidden());
276    }
277
278    #[test]
279    fn test_needs_approval_requirement() {
280        let req = ExecApprovalRequirement::needs_approval("dangerous command");
281        assert!(!req.can_proceed());
282        assert!(req.requires_approval());
283        assert!(!req.is_forbidden());
284    }
285
286    #[test]
287    fn test_forbidden_requirement() {
288        let req = ExecApprovalRequirement::forbidden("policy violation");
289        assert!(!req.can_proceed());
290        assert!(!req.requires_approval());
291        assert!(req.is_forbidden());
292    }
293
294    #[test]
295    fn empty_amendments_never_match() {
296        let decoded: ExecPolicyAmendment = serde_json::from_str(r#"{"pattern":[]}"#).unwrap();
297        for amendment in [ExecPolicyAmendment::new(vec![]), decoded] {
298            assert!(!amendment.matches(&[]));
299            assert!(!amendment.matches(&["untrusted".into()]));
300        }
301        let amendment = ExecPolicyAmendment::new(vec!["git".into(), "".into()]);
302        assert!(amendment.matches(&["git".into(), "".into(), "tail".into()]));
303        assert!(!amendment.matches(&["".into(), "git".into()]));
304        assert!(!amendment.matches(&["git".into()]));
305    }
306
307    #[test]
308    fn test_amendment() {
309        let amendment = ExecPolicyAmendment::new(vec!["cargo".to_string(), "build".to_string()]);
310        let rule = amendment.to_rule_string();
311        assert!(rule.contains("cargo"));
312        assert!(rule.contains("build"));
313        assert!(rule.contains("allow"));
314    }
315
316    #[test]
317    fn test_reject_config_helpers() {
318        let config = RejectConfig {
319            sandbox_approval: true,
320            rules: false,
321            request_permissions: false,
322            mcp_elicitations: true,
323        };
324        assert!(config.rejects_sandbox_approval());
325        assert!(!config.rejects_rules_approval());
326        assert!(!config.rejects_request_permissions());
327        assert!(config.rejects_mcp_elicitations());
328    }
329
330    #[test]
331    fn test_ask_for_approval_rejection_helpers() {
332        assert!(AskForApproval::Never.rejects_rule_prompt());
333        assert!(AskForApproval::Never.rejects_sandbox_prompt());
334        assert!(AskForApproval::Never.rejects_request_permission_prompt());
335        assert!(AskForApproval::Never.rejects_mcp_elicitation());
336
337        assert!(!AskForApproval::OnRequest.rejects_rule_prompt());
338        assert!(!AskForApproval::OnRequest.rejects_sandbox_prompt());
339        assert!(!AskForApproval::OnRequest.rejects_request_permission_prompt());
340        assert!(!AskForApproval::OnRequest.rejects_mcp_elicitation());
341
342        let sandbox_reject_policy = AskForApproval::Reject(RejectConfig {
343            sandbox_approval: true,
344            rules: false,
345            request_permissions: false,
346            mcp_elicitations: true,
347        });
348        assert!(!sandbox_reject_policy.rejects_rule_prompt());
349        assert!(sandbox_reject_policy.rejects_sandbox_prompt());
350        assert!(!sandbox_reject_policy.rejects_request_permission_prompt());
351        assert!(sandbox_reject_policy.rejects_mcp_elicitation());
352
353        let request_permissions_reject_policy = AskForApproval::Reject(RejectConfig {
354            sandbox_approval: false,
355            rules: false,
356            request_permissions: true,
357            mcp_elicitations: false,
358        });
359        assert!(!request_permissions_reject_policy.rejects_rule_prompt());
360        assert!(!request_permissions_reject_policy.rejects_sandbox_prompt());
361        assert!(request_permissions_reject_policy.rejects_request_permission_prompt());
362        assert!(!request_permissions_reject_policy.rejects_mcp_elicitation());
363    }
364
365    #[test]
366    fn test_reject_policy_serde_roundtrip() {
367        let value = json!({
368            "reject": {
369                "sandbox_approval": true,
370                "rules": false,
371                "mcp_elicitations": true
372            }
373        });
374        let policy: AskForApproval = serde_json::from_value(value).expect("deserialize policy");
375        assert_eq!(
376            policy,
377            AskForApproval::Reject(RejectConfig {
378                sandbox_approval: true,
379                rules: false,
380                request_permissions: false,
381                mcp_elicitations: true,
382            })
383        );
384
385        let serialized = serde_json::to_value(policy).expect("serialize policy");
386        assert_eq!(
387            serialized,
388            json!({
389                "reject": {
390                    "sandbox_approval": true,
391                    "rules": false,
392                    "request_permissions": false,
393                    "mcp_elicitations": true
394                }
395            })
396        );
397    }
398
399    #[test]
400    fn test_reject_policy_defaults_missing_request_permissions_to_false() {
401        let policy: AskForApproval = serde_json::from_value(json!({
402            "reject": {
403                "sandbox_approval": true,
404                "rules": false,
405                "mcp_elicitations": true
406            }
407        }))
408        .expect("deserialize legacy reject policy");
409
410        assert_eq!(
411            policy,
412            AskForApproval::Reject(RejectConfig {
413                sandbox_approval: true,
414                rules: false,
415                request_permissions: false,
416                mcp_elicitations: true,
417            })
418        );
419    }
420
421    #[test]
422    fn default_exec_approval_requirement_skips_for_never() {
423        let requirement = default_exec_approval_requirement(AskForApproval::Never, true);
424
425        assert_eq!(requirement, ExecApprovalRequirement::skip());
426    }
427
428    #[test]
429    fn default_exec_approval_requirement_skips_for_on_failure() {
430        let requirement = default_exec_approval_requirement(AskForApproval::OnFailure, true);
431
432        assert_eq!(requirement, ExecApprovalRequirement::skip());
433    }
434
435    #[test]
436    fn default_exec_approval_requirement_requires_approval_for_on_request() {
437        let requirement = default_exec_approval_requirement(AskForApproval::OnRequest, true);
438
439        assert_eq!(
440            requirement,
441            ExecApprovalRequirement::NeedsApproval { reason: None, proposed_execpolicy_amendment: None }
442        );
443    }
444
445    #[test]
446    fn default_exec_approval_requirement_skips_on_request_without_prompt() {
447        let requirement = default_exec_approval_requirement(AskForApproval::OnRequest, false);
448
449        assert_eq!(requirement, ExecApprovalRequirement::skip());
450    }
451
452    #[test]
453    fn default_exec_approval_requirement_requires_approval_for_unless_trusted() {
454        let requirement = default_exec_approval_requirement(AskForApproval::UnlessTrusted, false);
455
456        assert_eq!(
457            requirement,
458            ExecApprovalRequirement::NeedsApproval { reason: None, proposed_execpolicy_amendment: None }
459        );
460    }
461
462    #[test]
463    fn default_exec_approval_requirement_rejects_sandbox_prompt_when_configured() {
464        let policy = AskForApproval::Reject(RejectConfig {
465            sandbox_approval: true,
466            rules: false,
467            request_permissions: false,
468            mcp_elicitations: false,
469        });
470
471        let requirement = default_exec_approval_requirement(policy, true);
472
473        assert_eq!(
474            requirement,
475            ExecApprovalRequirement::Forbidden {
476                reason: "approval policy rejected sandbox approval prompt".to_string(),
477            }
478        );
479    }
480
481    #[test]
482    fn default_exec_approval_requirement_ignores_request_permission_rejection() {
483        let policy = AskForApproval::Reject(RejectConfig {
484            sandbox_approval: false,
485            rules: false,
486            request_permissions: true,
487            mcp_elicitations: false,
488        });
489
490        let requirement = default_exec_approval_requirement(policy, false);
491
492        assert_eq!(requirement, ExecApprovalRequirement::skip());
493    }
494
495    #[test]
496    fn default_exec_approval_requirement_keeps_prompt_when_rejection_disabled() {
497        let policy = AskForApproval::Reject(RejectConfig {
498            sandbox_approval: false,
499            rules: true,
500            request_permissions: false,
501            mcp_elicitations: true,
502        });
503
504        let requirement = default_exec_approval_requirement(policy, true);
505
506        assert_eq!(
507            requirement,
508            ExecApprovalRequirement::NeedsApproval { reason: None, proposed_execpolicy_amendment: None }
509        );
510    }
511}