claude_agent/security/
mod.rs1pub mod bash;
12pub mod fs;
13pub mod guard;
14pub mod limits;
15pub mod path;
16pub mod policy;
17pub mod sandbox;
18
19mod error;
20
21pub use error::SecurityError;
22pub use fs::{SecureFileHandle, SecureFs};
23pub use guard::SecurityGuard;
24pub use limits::ResourceLimits;
25pub use path::SafePath;
26pub use policy::SecurityPolicy;
27pub use sandbox::{DomainCheck, NetworkConfig, NetworkSandbox, Sandbox, SandboxConfig};
28
29use std::path::{Path, PathBuf};
30use std::sync::Arc;
31
32#[derive(Clone)]
33pub struct SecurityContext {
34 pub fs: SecureFs,
35 pub bash: bash::BashAnalyzer,
36 pub limits: ResourceLimits,
37 pub policy: SecurityPolicy,
38 pub network: Arc<NetworkSandbox>,
39 pub sandbox: Arc<Sandbox>,
40}
41
42impl SecurityContext {
43 pub fn new(root: impl AsRef<Path>) -> Result<Self, SecurityError> {
44 Self::builder().root(root).build()
45 }
46
47 pub fn builder() -> SecurityContextBuilder {
48 SecurityContextBuilder::default()
49 }
50
51 pub fn permissive() -> Self {
56 Self {
57 fs: SecureFs::permissive(),
58 bash: bash::BashAnalyzer::new(bash::BashPolicy::default()),
59 limits: ResourceLimits::none(),
60 policy: SecurityPolicy::permissive(),
61 network: Arc::new(NetworkSandbox::permissive()),
62 sandbox: Arc::new(Sandbox::disabled()),
63 }
64 }
65
66 pub fn root(&self) -> &Path {
67 self.fs.root()
68 }
69
70 pub fn is_sandboxed(&self) -> bool {
71 self.sandbox.is_enabled()
72 }
73
74 pub fn should_auto_allow_bash(&self) -> bool {
75 self.sandbox.should_auto_allow_bash()
76 }
77}
78
79#[derive(Default)]
80pub struct SecurityContextBuilder {
81 root: Option<PathBuf>,
82 allowed_paths: Vec<PathBuf>,
83 denied_patterns: Vec<String>,
84 limits: Option<ResourceLimits>,
85 bash_policy: Option<bash::BashPolicy>,
86 max_symlink_depth: Option<u8>,
87 network: Option<NetworkSandbox>,
88 sandbox_config: Option<SandboxConfig>,
89}
90
91impl SecurityContextBuilder {
92 pub fn root(mut self, path: impl AsRef<Path>) -> Self {
93 self.root = Some(path.as_ref().to_path_buf());
94 self
95 }
96
97 pub fn allowed_paths(mut self, paths: Vec<PathBuf>) -> Self {
98 self.allowed_paths = paths;
99 self
100 }
101
102 pub fn denied_patterns(mut self, patterns: Vec<String>) -> Self {
103 self.denied_patterns = patterns;
104 self
105 }
106
107 pub fn limits(mut self, limits: ResourceLimits) -> Self {
108 self.limits = Some(limits);
109 self
110 }
111
112 pub fn bash_policy(mut self, policy: bash::BashPolicy) -> Self {
113 self.bash_policy = Some(policy);
114 self
115 }
116
117 pub fn max_symlink_depth(mut self, depth: u8) -> Self {
118 self.max_symlink_depth = Some(depth);
119 self
120 }
121
122 pub fn network(mut self, sandbox: NetworkSandbox) -> Self {
123 self.network = Some(sandbox);
124 self
125 }
126
127 pub fn sandbox(mut self, config: SandboxConfig) -> Self {
128 self.sandbox_config = Some(config);
129 self
130 }
131
132 pub fn sandbox_enabled(mut self, enabled: bool) -> Self {
133 let root = self
134 .root
135 .clone()
136 .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
137 self.sandbox_config = Some(if enabled {
138 SandboxConfig::new(root)
139 } else {
140 SandboxConfig::disabled()
141 });
142 self
143 }
144
145 pub fn auto_allow_bash_if_sandboxed(mut self, auto_allow: bool) -> Self {
146 let root = self
147 .root
148 .clone()
149 .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
150 let config = self
151 .sandbox_config
152 .take()
153 .unwrap_or_else(|| SandboxConfig::new(root));
154 self.sandbox_config = Some(config.with_auto_allow_bash(auto_allow));
155 self
156 }
157
158 pub fn build(self) -> Result<SecurityContext, SecurityError> {
159 let root = self
160 .root
161 .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
162
163 let fs = SecureFs::new(
164 &root,
165 self.allowed_paths.clone(),
166 &self.denied_patterns,
167 self.max_symlink_depth
168 .unwrap_or(crate::security::path::DEFAULT_MAX_SYMLINK_DEPTH),
169 )?;
170
171 let sandbox_config = self.sandbox_config.unwrap_or_else(|| {
172 SandboxConfig::disabled()
173 .with_working_dir(root)
174 .with_allowed_paths(self.allowed_paths)
175 .with_denied_paths(self.denied_patterns)
176 });
177
178 let network = self
179 .network
180 .unwrap_or_else(|| sandbox_config.to_network_sandbox());
181
182 Ok(SecurityContext {
183 fs,
184 bash: bash::BashAnalyzer::new(self.bash_policy.unwrap_or_default()),
185 limits: self.limits.unwrap_or_default(),
186 policy: SecurityPolicy::default(),
187 network: Arc::new(network),
188 sandbox: Arc::new(Sandbox::new(sandbox_config)),
189 })
190 }
191}
192
193#[cfg(test)]
194mod tests {
195 use super::*;
196 use tempfile::tempdir;
197
198 #[test]
199 fn test_security_context_new() {
200 let dir = tempdir().unwrap();
201 let security = SecurityContext::new(dir.path()).unwrap();
202 assert_eq!(security.root(), std::fs::canonicalize(dir.path()).unwrap());
203 }
204
205 #[test]
206 fn test_security_context_permissive() {
207 let security = SecurityContext::permissive();
208 assert_eq!(security.root(), Path::new("/"));
209 }
210
211 #[test]
212 fn test_builder() {
213 let dir = tempdir().unwrap();
214 let canonical_dir = std::fs::canonicalize(dir.path()).unwrap();
215 let security = SecurityContext::builder()
216 .root(&canonical_dir)
217 .max_symlink_depth(5)
218 .limits(ResourceLimits::default())
219 .build()
220 .unwrap();
221
222 assert_eq!(security.root(), canonical_dir);
223 }
224}