zeroclaw_runtime/security/
detect.rs1use crate::security::traits::Sandbox;
4use std::path::Path;
5use std::sync::Arc;
6use zeroclaw_config::schema::{SandboxBackend, SandboxConfig};
7
8pub fn create_sandbox(
17 sandbox: &SandboxConfig,
18 runtime_kind: &str,
19 workspace_dir: Option<&Path>,
20) -> Arc<dyn Sandbox> {
21 let backend = &sandbox.backend;
22
23 if matches!(backend, SandboxBackend::None) || sandbox.enabled == Some(false) {
25 return Arc::new(super::traits::NoopSandbox);
26 }
27
28 match backend {
30 SandboxBackend::Landlock => {
31 #[cfg(feature = "sandbox-landlock")]
32 {
33 #[cfg(target_os = "linux")]
34 {
35 if let Ok(sandbox) = super::landlock::LandlockSandbox::with_workspace(
36 workspace_dir.map(Path::to_path_buf),
37 ) {
38 return Arc::new(sandbox);
39 }
40 }
41 }
42 ::zeroclaw_log::record!(
43 WARN,
44 ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note)
45 .with_outcome(::zeroclaw_log::EventOutcome::Unknown),
46 "Landlock requested but not available, falling back to application-layer"
47 );
48 Arc::new(super::traits::NoopSandbox)
49 }
50 SandboxBackend::Firejail => {
51 #[cfg(target_os = "linux")]
52 {
53 if let Ok(sandbox) = super::firejail::FirejailSandbox::new() {
54 return Arc::new(sandbox);
55 }
56 }
57 ::zeroclaw_log::record!(
58 WARN,
59 ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note)
60 .with_outcome(::zeroclaw_log::EventOutcome::Unknown),
61 "Firejail requested but not available, falling back to application-layer"
62 );
63 Arc::new(super::traits::NoopSandbox)
64 }
65 SandboxBackend::Bubblewrap => {
66 #[cfg(feature = "sandbox-bubblewrap")]
67 {
68 #[cfg(any(target_os = "linux", target_os = "macos"))]
69 {
70 if let Ok(sandbox) = super::bubblewrap::BubblewrapSandbox::new() {
71 return Arc::new(sandbox);
72 }
73 }
74 }
75 ::zeroclaw_log::record!(
76 WARN,
77 ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note)
78 .with_outcome(::zeroclaw_log::EventOutcome::Unknown),
79 "Bubblewrap requested but not available, falling back to application-layer"
80 );
81 Arc::new(super::traits::NoopSandbox)
82 }
83 SandboxBackend::Docker => {
84 let result = if let Some(ws) = workspace_dir {
85 super::docker::DockerSandbox::with_workspace(
86 super::docker::DockerSandbox::default_image(),
87 ws.to_path_buf(),
88 )
89 } else {
90 super::docker::DockerSandbox::new()
91 };
92 if let Ok(sandbox) = result {
93 return Arc::new(sandbox);
94 }
95 ::zeroclaw_log::record!(
96 WARN,
97 ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note)
98 .with_outcome(::zeroclaw_log::EventOutcome::Unknown),
99 "Docker requested but not available, falling back to application-layer"
100 );
101 Arc::new(super::traits::NoopSandbox)
102 }
103 SandboxBackend::SandboxExec => {
104 #[cfg(target_os = "macos")]
105 {
106 if let Ok(sandbox) = super::seatbelt::SeatbeltSandbox::with_workspace(workspace_dir)
107 {
108 return Arc::new(sandbox);
109 }
110 }
111 ::zeroclaw_log::record!(
112 WARN,
113 ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note)
114 .with_outcome(::zeroclaw_log::EventOutcome::Unknown),
115 "sandbox-exec requested but not available, falling back to application-layer"
116 );
117 Arc::new(super::traits::NoopSandbox)
118 }
119 SandboxBackend::Auto | SandboxBackend::None => {
120 detect_best_sandbox(runtime_kind, workspace_dir)
122 }
123 }
124}
125
126fn detect_best_sandbox(runtime_kind: &str, workspace_dir: Option<&Path>) -> Arc<dyn Sandbox> {
132 let skip_docker = runtime_kind == "native";
133
134 #[cfg(target_os = "linux")]
135 {
136 #[cfg(feature = "sandbox-landlock")]
138 {
139 if let Ok(sandbox) = super::landlock::LandlockSandbox::with_workspace(
140 workspace_dir.map(Path::to_path_buf),
141 ) {
142 ::zeroclaw_log::record!(
143 INFO,
144 ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note),
145 "Landlock sandbox enabled (Linux kernel 5.13+)"
146 );
147 return Arc::new(sandbox);
148 }
149 }
150
151 if let Ok(sandbox) = super::firejail::FirejailSandbox::probe() {
153 ::zeroclaw_log::record!(
154 INFO,
155 ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note),
156 "Firejail sandbox enabled"
157 );
158 return Arc::new(sandbox);
159 }
160 }
161
162 #[cfg(target_os = "macos")]
163 {
164 #[cfg(feature = "sandbox-bubblewrap")]
166 {
167 if let Ok(sandbox) = super::bubblewrap::BubblewrapSandbox::probe() {
168 ::zeroclaw_log::record!(
169 INFO,
170 ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note),
171 "Bubblewrap sandbox enabled"
172 );
173 return Arc::new(sandbox);
174 }
175 }
176
177 if let Ok(sandbox) = super::seatbelt::SeatbeltSandbox::with_workspace(workspace_dir) {
179 ::zeroclaw_log::record!(
180 INFO,
181 ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note),
182 "macOS sandbox-exec (Seatbelt) enabled"
183 );
184 return Arc::new(sandbox);
185 }
186 }
187
188 if !skip_docker {
193 let docker_result = if let Some(ws) = workspace_dir {
194 super::docker::DockerSandbox::with_workspace(
195 super::docker::DockerSandbox::default_image(),
196 ws.to_path_buf(),
197 )
198 } else {
199 super::docker::DockerSandbox::probe()
200 };
201 if let Ok(sandbox) = docker_result {
202 ::zeroclaw_log::record!(
203 INFO,
204 ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note),
205 "Docker sandbox enabled"
206 );
207 return Arc::new(sandbox);
208 }
209 } else {
210 ::zeroclaw_log::record!(
211 DEBUG,
212 ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note),
213 "Docker sandbox skipped: runtime.kind = \"native\" overrides auto-detection"
214 );
215 }
216
217 ::zeroclaw_log::record!(
219 INFO,
220 ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note),
221 "No sandbox backend available, using application-layer security"
222 );
223 Arc::new(super::traits::NoopSandbox)
224}
225
226#[cfg(target_os = "linux")]
232pub fn linux_memcg_available() -> bool {
233 use std::path::Path;
234
235 if Path::new("/sys/fs/cgroup/memory.max").exists() {
236 return true;
237 }
238 if Path::new("/sys/fs/cgroup/memory/memory.limit_in_bytes").exists() {
239 return true;
240 }
241 if let Ok(content) = std::fs::read_to_string("/proc/cgroups") {
242 for line in content.lines() {
243 if line.starts_with('#') {
244 continue;
245 }
246 let mut cols = line.split_whitespace();
247 let name = cols.next().unwrap_or("");
248 let _hierarchy = cols.next();
249 let _num_cgroups = cols.next();
250 let enabled = cols.next().unwrap_or("0");
251 if name == "memory" && enabled == "1" {
252 return true;
253 }
254 }
255 }
256 false
257}
258
259#[cfg(not(target_os = "linux"))]
262pub fn linux_memcg_available() -> bool {
263 false
264}
265
266#[cfg(test)]
267mod tests {
268 use super::*;
269
270 #[test]
271 fn detect_best_sandbox_returns_something() {
272 let sandbox = detect_best_sandbox("", None);
273 assert!(sandbox.is_available());
275 }
276
277 #[test]
278 fn explicit_none_returns_noop() {
279 let sandbox_cfg = SandboxConfig {
280 enabled: Some(false),
281 backend: SandboxBackend::None,
282 firejail_args: Vec::new(),
283 };
284 let sandbox = create_sandbox(&sandbox_cfg, "", None);
285 assert_eq!(sandbox.name(), "none");
286 }
287
288 #[test]
289 fn auto_mode_detects_something() {
290 let sandbox_cfg = SandboxConfig {
291 enabled: None, backend: SandboxBackend::Auto,
293 firejail_args: Vec::new(),
294 };
295 let sandbox = create_sandbox(&sandbox_cfg, "", None);
296 assert!(sandbox.is_available());
298 }
299
300 #[test]
301 fn native_runtime_with_auto_sandbox_never_selects_docker() {
302 let sandbox = detect_best_sandbox("native", None);
306 assert_ne!(sandbox.name(), "docker");
307 }
308
309 #[test]
310 fn explicit_docker_backend_is_not_blocked_by_native_runtime() {
311 let sandbox_cfg = SandboxConfig {
314 enabled: None,
315 backend: SandboxBackend::Docker,
316 firejail_args: Vec::new(),
317 };
318 let sandbox = create_sandbox(&sandbox_cfg, "native", None);
319 assert!(sandbox.is_available());
321 }
322
323 #[test]
324 fn linux_memcg_available_returns_bool() {
325 let _result: bool = linux_memcg_available();
326 }
327
328 #[cfg(target_os = "linux")]
329 #[test]
330 fn linux_memcg_cgroup_v2_path_probe_does_not_panic() {
331 let _ = std::path::Path::new("/sys/fs/cgroup/memory.max").exists();
332 }
333
334 #[cfg(target_os = "linux")]
335 #[test]
336 fn linux_memcg_proc_cgroups_parses_without_panic() {
337 if let Ok(content) = std::fs::read_to_string("/proc/cgroups") {
338 let _found = content.lines().filter(|l| !l.starts_with('#')).any(|l| {
339 let mut f = l.split_whitespace();
340 let name = f.next().unwrap_or("");
341 let _hier = f.next();
342 let _num = f.next();
343 let enabled = f.next().unwrap_or("0");
344 name == "memory" && enabled == "1"
345 });
346 }
347 }
348}