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