vtcode_safety/exec_policy/
approval.rs1use 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 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#[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!(self, Self::UnlessTrusted | Self::OnRequest | Self::Reject(_))
64 }
65
66 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 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 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 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#[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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
134pub struct ExecPolicyAmendment {
135 pattern: Vec<String>,
137}
138
139impl ExecPolicyAmendment {
140 fn new(pattern: Vec<String>) -> Self {
142 Self { pattern }
143 }
144
145 pub(crate) fn from_prefix(prefix: impl Into<String>) -> Self {
147 Self { pattern: vec![prefix.into()] }
148 }
149
150 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 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 pub fn command_pattern(&self) -> &[String] {
166 &self.pattern
167 }
168}
169
170#[derive(Debug, Clone, PartialEq, Eq)]
176pub enum ExecApprovalRequirement {
177 Skip {
179 bypass_sandbox: bool,
181 proposed_execpolicy_amendment: Option<ExecPolicyAmendment>,
183 },
184
185 NeedsApproval {
187 reason: Option<String>,
189 proposed_execpolicy_amendment: Option<ExecPolicyAmendment>,
191 },
192
193 Forbidden {
195 reason: String,
197 },
198}
199
200impl ExecApprovalRequirement {
201 pub(crate) fn skip() -> Self {
203 Self::Skip {
204 bypass_sandbox: false,
205 proposed_execpolicy_amendment: None,
206 }
207 }
208
209 pub fn skip_with_bypass() -> Self {
211 Self::Skip {
212 bypass_sandbox: true,
213 proposed_execpolicy_amendment: None,
214 }
215 }
216
217 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 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 pub(crate) fn forbidden(reason: impl Into<String>) -> Self {
235 Self::Forbidden { reason: reason.into() }
236 }
237
238 pub(crate) fn requires_approval(&self) -> bool {
240 matches!(self, Self::NeedsApproval { .. })
241 }
242
243 pub(crate) fn is_forbidden(&self) -> bool {
245 matches!(self, Self::Forbidden { .. })
246 }
247
248 pub(crate) fn can_proceed(&self) -> bool {
250 matches!(self, Self::Skip { .. })
251 }
252
253 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 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}