Skip to main content

vtcode_safety/sandboxing/
permissions.rs

1//! Sandbox permissions for fine-grained access control.
2
3use std::path::PathBuf;
4
5use serde::{Deserialize, Serialize};
6
7/// Fine-grained permissions for sandbox operations.
8///
9/// These permissions allow individual tool calls to request specific
10/// capabilities beyond the base sandbox policy.
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
12#[serde(rename_all = "snake_case")]
13pub enum SandboxPermissions {
14    /// Use the default sandbox permissions from the policy.
15    #[default]
16    UseDefault,
17
18    /// Request escalated permissions (requires approval).
19    RequireEscalated,
20
21    /// Request additional per-command sandbox permissions.
22    WithAdditionalPermissions,
23
24    /// Bypass the sandbox entirely (requires explicit approval).
25    BypassSandbox,
26}
27
28impl SandboxPermissions {
29    /// Normalize an implicit additional-permission request to its explicit
30    /// sandboxed wire mode. Escalated and bypass modes are left unchanged so
31    /// their conflict checks remain fail-closed at the execution boundary.
32    #[must_use]
33    pub fn normalized_for(self, additional_permissions: Option<&AdditionalPermissions>) -> Self {
34        if self == Self::UseDefault && additional_permissions.is_some_and(|permissions| !permissions.is_empty()) {
35            Self::WithAdditionalPermissions
36        } else {
37            self
38        }
39    }
40
41    /// Check if this permission requires approval.
42    fn requires_approval(&self) -> bool {
43        matches!(self, Self::RequireEscalated | Self::WithAdditionalPermissions | Self::BypassSandbox)
44    }
45
46    /// Check if this permission requests full unsandboxed execution.
47    pub fn requires_escalated_permissions(&self) -> bool {
48        matches!(self, Self::RequireEscalated | Self::BypassSandbox)
49    }
50
51    /// Check if this permission requests any additional privileges.
52    fn requires_additional_permissions(&self) -> bool {
53        !matches!(self, Self::UseDefault)
54    }
55
56    /// Check if this permission requests additional sandboxed permissions.
57    pub fn uses_additional_permissions(&self) -> bool {
58        matches!(self, Self::WithAdditionalPermissions)
59    }
60
61    /// Check if this permission bypasses the sandbox.
62    fn bypasses_sandbox(&self) -> bool {
63        matches!(self, Self::BypassSandbox)
64    }
65
66    /// Merge with another permission, taking the more permissive one.
67    fn merge(&self, other: &Self) -> Self {
68        use SandboxPermissions::*;
69        match (self, other) {
70            (BypassSandbox, _) | (_, BypassSandbox) => BypassSandbox,
71            (RequireEscalated, _) | (_, RequireEscalated) => RequireEscalated,
72            (WithAdditionalPermissions, _) | (_, WithAdditionalPermissions) => WithAdditionalPermissions,
73            (UseDefault, UseDefault) => UseDefault,
74        }
75    }
76}
77
78/// Additional per-command filesystem permissions.
79#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
80pub struct AdditionalPermissions {
81    /// Additional filesystem paths to grant read access.
82    #[serde(default, skip_serializing_if = "Vec::is_empty")]
83    pub fs_read: Vec<PathBuf>,
84    /// Additional filesystem paths to grant write access.
85    #[serde(default, skip_serializing_if = "Vec::is_empty")]
86    pub fs_write: Vec<PathBuf>,
87}
88
89impl AdditionalPermissions {
90    pub fn is_empty(&self) -> bool {
91        self.fs_read.is_empty() && self.fs_write.is_empty()
92    }
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98
99    #[test]
100    fn test_default_permissions() {
101        let perm = SandboxPermissions::default();
102        assert!(!perm.requires_approval());
103        assert!(!perm.bypasses_sandbox());
104    }
105
106    #[test]
107    fn test_escalated_permissions() {
108        let perm = SandboxPermissions::RequireEscalated;
109        assert!(perm.requires_approval());
110        assert!(!perm.bypasses_sandbox());
111        assert!(perm.requires_escalated_permissions());
112    }
113
114    #[test]
115    fn test_bypass_permissions() {
116        let perm = SandboxPermissions::BypassSandbox;
117        assert!(perm.requires_approval());
118        assert!(perm.bypasses_sandbox());
119        assert!(perm.requires_escalated_permissions());
120    }
121
122    #[test]
123    fn test_with_additional_permissions() {
124        let perm = SandboxPermissions::WithAdditionalPermissions;
125        assert!(perm.requires_approval());
126        assert!(perm.requires_additional_permissions());
127        assert!(perm.uses_additional_permissions());
128        assert!(!perm.requires_escalated_permissions());
129        assert!(!perm.bypasses_sandbox());
130    }
131
132    #[test]
133    fn default_normalizes_for_non_empty_additional_permissions() {
134        let additional = AdditionalPermissions {
135            fs_read: vec![PathBuf::from("/tmp/reference")],
136            fs_write: Vec::new(),
137        };
138
139        assert_eq!(
140            SandboxPermissions::UseDefault.normalized_for(Some(&additional)),
141            SandboxPermissions::WithAdditionalPermissions
142        );
143        assert_eq!(
144            SandboxPermissions::RequireEscalated.normalized_for(Some(&additional)),
145            SandboxPermissions::RequireEscalated
146        );
147        assert_eq!(
148            SandboxPermissions::UseDefault.normalized_for(Some(&AdditionalPermissions::default())),
149            SandboxPermissions::UseDefault
150        );
151    }
152
153    #[test]
154    fn test_merge_permissions() {
155        use SandboxPermissions::*;
156
157        assert_eq!(UseDefault.merge(&UseDefault), UseDefault);
158        assert_eq!(UseDefault.merge(&WithAdditionalPermissions), WithAdditionalPermissions);
159        assert_eq!(UseDefault.merge(&RequireEscalated), RequireEscalated);
160        assert_eq!(RequireEscalated.merge(&BypassSandbox), BypassSandbox);
161    }
162}