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.
151    pub(crate) fn matches(&self, command: &[String]) -> bool {
152        if command.len() < self.pattern.len() {
153            return false;
154        }
155        self.pattern.iter().zip(command.iter()).all(|(pattern, cmd)| pattern == cmd)
156    }
157
158    /// Convert the amendment to a policy rule string.
159    fn to_rule_string(&self) -> String {
160        let pattern_json = serde_json::to_string(&self.pattern).unwrap_or_default();
161        format!("prefix_rule(pattern={pattern_json}, decision=\"allow\")")
162    }
163
164    /// Get the command pattern for Codex compatibility.
165    pub fn command_pattern(&self) -> &[String] {
166        &self.pattern
167    }
168}
169
170/// Requirement for approval before executing a command.
171///
172/// This enum represents the outcome of evaluating a command against the
173/// execution policy, indicating whether the command can proceed, needs
174/// approval, or is forbidden.
175#[derive(Debug, Clone, PartialEq, Eq)]
176pub enum ExecApprovalRequirement {
177    /// Command can be executed without approval.
178    Skip {
179        /// Whether to bypass the sandbox for this command.
180        bypass_sandbox: bool,
181        /// Proposed policy amendment if the user wants to trust this command.
182        proposed_execpolicy_amendment: Option<ExecPolicyAmendment>,
183    },
184
185    /// Command requires user approval before execution.
186    NeedsApproval {
187        /// Reason for requiring approval.
188        reason: Option<String>,
189        /// Proposed policy amendment to skip future approvals.
190        proposed_execpolicy_amendment: Option<ExecPolicyAmendment>,
191    },
192
193    /// Command is forbidden by policy and cannot be executed.
194    Forbidden {
195        /// Reason for forbidding the command.
196        reason: String,
197    },
198}
199
200impl ExecApprovalRequirement {
201    /// Create a skip requirement.
202    pub(crate) fn skip() -> Self {
203        Self::Skip {
204            bypass_sandbox: false,
205            proposed_execpolicy_amendment: None,
206        }
207    }
208
209    /// Create a skip requirement with sandbox bypass.
210    pub fn skip_with_bypass() -> Self {
211        Self::Skip {
212            bypass_sandbox: true,
213            proposed_execpolicy_amendment: None,
214        }
215    }
216
217    /// Create an approval requirement.
218    pub(crate) fn needs_approval(reason: impl Into<String>) -> Self {
219        Self::NeedsApproval {
220            reason: Some(reason.into()),
221            proposed_execpolicy_amendment: None,
222        }
223    }
224
225    /// Create a needs approval requirement with an amendment.
226    pub fn needs_approval_with_amendment(reason: Option<String>, amendment: ExecPolicyAmendment) -> Self {
227        Self::NeedsApproval {
228            reason,
229            proposed_execpolicy_amendment: Some(amendment),
230        }
231    }
232
233    /// Create a forbidden requirement.
234    pub(crate) fn forbidden(reason: impl Into<String>) -> Self {
235        Self::Forbidden { reason: reason.into() }
236    }
237
238    /// Check if approval is needed.
239    pub(crate) fn requires_approval(&self) -> bool {
240        matches!(self, Self::NeedsApproval { .. })
241    }
242
243    /// Check if the command is forbidden.
244    pub(crate) fn is_forbidden(&self) -> bool {
245        matches!(self, Self::Forbidden { .. })
246    }
247
248    /// Check if the command can proceed (skip or approved).
249    pub(crate) fn can_proceed(&self) -> bool {
250        matches!(self, Self::Skip { .. })
251    }
252
253    /// Get the proposed amendment, if any.
254    pub fn get_amendment(&self) -> Option<&ExecPolicyAmendment> {
255        match self {
256            Self::Skip { proposed_execpolicy_amendment, .. } => proposed_execpolicy_amendment.as_ref(),
257            Self::NeedsApproval { proposed_execpolicy_amendment, .. } => proposed_execpolicy_amendment.as_ref(),
258            Self::Forbidden { .. } => None,
259        }
260    }
261
262    /// Get the proposed exec policy amendment if any (Codex-compatible name).
263    pub fn proposed_execpolicy_amendment(&self) -> Option<&ExecPolicyAmendment> {
264        self.get_amendment()
265    }
266}
267
268#[cfg(test)]
269mod tests {
270    use super::*;
271    use serde_json::json;
272
273    #[test]
274    fn test_skip_requirement() {
275        let req = ExecApprovalRequirement::skip();
276        assert!(req.can_proceed());
277        assert!(!req.requires_approval());
278        assert!(!req.is_forbidden());
279    }
280
281    #[test]
282    fn test_needs_approval_requirement() {
283        let req = ExecApprovalRequirement::needs_approval("dangerous command");
284        assert!(!req.can_proceed());
285        assert!(req.requires_approval());
286        assert!(!req.is_forbidden());
287    }
288
289    #[test]
290    fn test_forbidden_requirement() {
291        let req = ExecApprovalRequirement::forbidden("policy violation");
292        assert!(!req.can_proceed());
293        assert!(!req.requires_approval());
294        assert!(req.is_forbidden());
295    }
296
297    #[test]
298    fn test_amendment() {
299        let amendment = ExecPolicyAmendment::new(vec!["cargo".to_string(), "build".to_string()]);
300        let rule = amendment.to_rule_string();
301        assert!(rule.contains("cargo"));
302        assert!(rule.contains("build"));
303        assert!(rule.contains("allow"));
304    }
305
306    #[test]
307    fn test_reject_config_helpers() {
308        let config = RejectConfig {
309            sandbox_approval: true,
310            rules: false,
311            request_permissions: false,
312            mcp_elicitations: true,
313        };
314        assert!(config.rejects_sandbox_approval());
315        assert!(!config.rejects_rules_approval());
316        assert!(!config.rejects_request_permissions());
317        assert!(config.rejects_mcp_elicitations());
318    }
319
320    #[test]
321    fn test_ask_for_approval_rejection_helpers() {
322        assert!(AskForApproval::Never.rejects_rule_prompt());
323        assert!(AskForApproval::Never.rejects_sandbox_prompt());
324        assert!(AskForApproval::Never.rejects_request_permission_prompt());
325        assert!(AskForApproval::Never.rejects_mcp_elicitation());
326
327        assert!(!AskForApproval::OnRequest.rejects_rule_prompt());
328        assert!(!AskForApproval::OnRequest.rejects_sandbox_prompt());
329        assert!(!AskForApproval::OnRequest.rejects_request_permission_prompt());
330        assert!(!AskForApproval::OnRequest.rejects_mcp_elicitation());
331
332        let sandbox_reject_policy = AskForApproval::Reject(RejectConfig {
333            sandbox_approval: true,
334            rules: false,
335            request_permissions: false,
336            mcp_elicitations: true,
337        });
338        assert!(!sandbox_reject_policy.rejects_rule_prompt());
339        assert!(sandbox_reject_policy.rejects_sandbox_prompt());
340        assert!(!sandbox_reject_policy.rejects_request_permission_prompt());
341        assert!(sandbox_reject_policy.rejects_mcp_elicitation());
342
343        let request_permissions_reject_policy = AskForApproval::Reject(RejectConfig {
344            sandbox_approval: false,
345            rules: false,
346            request_permissions: true,
347            mcp_elicitations: false,
348        });
349        assert!(!request_permissions_reject_policy.rejects_rule_prompt());
350        assert!(!request_permissions_reject_policy.rejects_sandbox_prompt());
351        assert!(request_permissions_reject_policy.rejects_request_permission_prompt());
352        assert!(!request_permissions_reject_policy.rejects_mcp_elicitation());
353    }
354
355    #[test]
356    fn test_reject_policy_serde_roundtrip() {
357        let value = json!({
358            "reject": {
359                "sandbox_approval": true,
360                "rules": false,
361                "mcp_elicitations": true
362            }
363        });
364        let policy: AskForApproval = serde_json::from_value(value).expect("deserialize policy");
365        assert_eq!(
366            policy,
367            AskForApproval::Reject(RejectConfig {
368                sandbox_approval: true,
369                rules: false,
370                request_permissions: false,
371                mcp_elicitations: true,
372            })
373        );
374
375        let serialized = serde_json::to_value(policy).expect("serialize policy");
376        assert_eq!(
377            serialized,
378            json!({
379                "reject": {
380                    "sandbox_approval": true,
381                    "rules": false,
382                    "request_permissions": false,
383                    "mcp_elicitations": true
384                }
385            })
386        );
387    }
388
389    #[test]
390    fn test_reject_policy_defaults_missing_request_permissions_to_false() {
391        let policy: AskForApproval = serde_json::from_value(json!({
392            "reject": {
393                "sandbox_approval": true,
394                "rules": false,
395                "mcp_elicitations": true
396            }
397        }))
398        .expect("deserialize legacy reject policy");
399
400        assert_eq!(
401            policy,
402            AskForApproval::Reject(RejectConfig {
403                sandbox_approval: true,
404                rules: false,
405                request_permissions: false,
406                mcp_elicitations: true,
407            })
408        );
409    }
410
411    #[test]
412    fn default_exec_approval_requirement_skips_for_never() {
413        let requirement = default_exec_approval_requirement(AskForApproval::Never, true);
414
415        assert_eq!(requirement, ExecApprovalRequirement::skip());
416    }
417
418    #[test]
419    fn default_exec_approval_requirement_skips_for_on_failure() {
420        let requirement = default_exec_approval_requirement(AskForApproval::OnFailure, true);
421
422        assert_eq!(requirement, ExecApprovalRequirement::skip());
423    }
424
425    #[test]
426    fn default_exec_approval_requirement_requires_approval_for_on_request() {
427        let requirement = default_exec_approval_requirement(AskForApproval::OnRequest, true);
428
429        assert_eq!(
430            requirement,
431            ExecApprovalRequirement::NeedsApproval { reason: None, proposed_execpolicy_amendment: None }
432        );
433    }
434
435    #[test]
436    fn default_exec_approval_requirement_skips_on_request_without_prompt() {
437        let requirement = default_exec_approval_requirement(AskForApproval::OnRequest, false);
438
439        assert_eq!(requirement, ExecApprovalRequirement::skip());
440    }
441
442    #[test]
443    fn default_exec_approval_requirement_requires_approval_for_unless_trusted() {
444        let requirement = default_exec_approval_requirement(AskForApproval::UnlessTrusted, false);
445
446        assert_eq!(
447            requirement,
448            ExecApprovalRequirement::NeedsApproval { reason: None, proposed_execpolicy_amendment: None }
449        );
450    }
451
452    #[test]
453    fn default_exec_approval_requirement_rejects_sandbox_prompt_when_configured() {
454        let policy = AskForApproval::Reject(RejectConfig {
455            sandbox_approval: true,
456            rules: false,
457            request_permissions: false,
458            mcp_elicitations: false,
459        });
460
461        let requirement = default_exec_approval_requirement(policy, true);
462
463        assert_eq!(
464            requirement,
465            ExecApprovalRequirement::Forbidden {
466                reason: "approval policy rejected sandbox approval prompt".to_string(),
467            }
468        );
469    }
470
471    #[test]
472    fn default_exec_approval_requirement_ignores_request_permission_rejection() {
473        let policy = AskForApproval::Reject(RejectConfig {
474            sandbox_approval: false,
475            rules: false,
476            request_permissions: true,
477            mcp_elicitations: false,
478        });
479
480        let requirement = default_exec_approval_requirement(policy, false);
481
482        assert_eq!(requirement, ExecApprovalRequirement::skip());
483    }
484
485    #[test]
486    fn default_exec_approval_requirement_keeps_prompt_when_rejection_disabled() {
487        let policy = AskForApproval::Reject(RejectConfig {
488            sandbox_approval: false,
489            rules: true,
490            request_permissions: false,
491            mcp_elicitations: true,
492        });
493
494        let requirement = default_exec_approval_requirement(policy, true);
495
496        assert_eq!(
497            requirement,
498            ExecApprovalRequirement::NeedsApproval { reason: None, proposed_execpolicy_amendment: None }
499        );
500    }
501}