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 super::policy::matches_prefix(&self.pattern, command)
153 }
154
155 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 pub fn command_pattern(&self) -> &[String] {
163 &self.pattern
164 }
165}
166
167#[derive(Debug, Clone, PartialEq, Eq)]
173pub enum ExecApprovalRequirement {
174 Skip {
176 bypass_sandbox: bool,
178 proposed_execpolicy_amendment: Option<ExecPolicyAmendment>,
180 },
181
182 NeedsApproval {
184 reason: Option<String>,
186 proposed_execpolicy_amendment: Option<ExecPolicyAmendment>,
188 },
189
190 Forbidden {
192 reason: String,
194 },
195}
196
197impl ExecApprovalRequirement {
198 pub(crate) fn skip() -> Self {
200 Self::Skip {
201 bypass_sandbox: false,
202 proposed_execpolicy_amendment: None,
203 }
204 }
205
206 pub fn skip_with_bypass() -> Self {
208 Self::Skip {
209 bypass_sandbox: true,
210 proposed_execpolicy_amendment: None,
211 }
212 }
213
214 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 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 pub(crate) fn forbidden(reason: impl Into<String>) -> Self {
232 Self::Forbidden { reason: reason.into() }
233 }
234
235 pub(crate) fn requires_approval(&self) -> bool {
237 matches!(self, Self::NeedsApproval { .. })
238 }
239
240 pub(crate) fn is_forbidden(&self) -> bool {
242 matches!(self, Self::Forbidden { .. })
243 }
244
245 pub(crate) fn can_proceed(&self) -> bool {
247 matches!(self, Self::Skip { .. })
248 }
249
250 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 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}