safe_shell_sandbox/
lib.rs1mod macos;
2pub mod proxy;
3
4use std::collections::HashMap;
5use std::path::PathBuf;
6use std::process::ExitStatus;
7
8pub struct SandboxResult {
10 pub status: ExitStatus,
11 pub file_reads_blocked: usize,
12 pub network_requests_blocked: usize,
13}
14
15pub struct SandboxConfig {
17 pub command: Vec<String>,
18 pub env: HashMap<String, String>,
19 pub cwd: PathBuf,
20 pub allow_write: Vec<String>,
21 pub deny_read: Vec<String>,
22 pub network_allow: Vec<String>,
23 pub quiet: bool,
24}
25
26pub fn resolve_shell() -> Result<String, Box<dyn std::error::Error>> {
34 if let Ok(path) = which::which("bash") {
36 return Ok(path.to_string_lossy().to_string());
37 }
38
39 for path in ["/bin/bash", "/usr/bin/bash", "/opt/homebrew/bin/bash"] {
41 if std::path::Path::new(path).exists() {
42 return Ok(path.to_string());
43 }
44 }
45
46 if let Ok(path) = which::which("sh") {
48 return Ok(path.to_string_lossy().to_string());
49 }
50
51 Err("Could not find bash or sh.".into())
52}
53
54pub fn ensure_path(env: &mut HashMap<String, String>) {
57 if !env.contains_key("PATH") {
58 env.insert(
59 "PATH".to_string(),
60 "/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin".to_string(),
61 );
62 }
63}
64
65pub fn resolve_path(path: &str, cwd: &std::path::Path) -> String {
67 if path.starts_with("~/") || path == "~" {
68 if let Some(home) = dirs::home_dir() {
69 return path.replacen('~', &home.to_string_lossy(), 1);
70 }
71 }
72
73 if path == "." {
74 return cwd.to_string_lossy().to_string();
75 }
76
77 if let Some(stripped) = path.strip_prefix("./") {
78 return cwd.join(stripped).to_string_lossy().to_string();
79 }
80
81 if path.starts_with('/') {
82 return path.to_string();
83 }
84
85 cwd.join(path).to_string_lossy().to_string()
87}
88
89pub fn execute_sandboxed(
91 command: &str,
92 env: &HashMap<String, String>,
93) -> Result<SandboxResult, Box<dyn std::error::Error>> {
94 let config = SandboxConfig {
95 command: vec![command.to_string()],
96 env: env.clone(),
97 cwd: std::env::current_dir()?,
98 allow_write: vec![],
99 deny_read: vec![],
100 network_allow: vec![],
101 quiet: false,
102 };
103
104 execute_with_config(&config)
105}
106
107pub fn execute_with_config(
109 config: &SandboxConfig,
110) -> Result<SandboxResult, Box<dyn std::error::Error>> {
111 #[cfg(target_os = "macos")]
112 {
113 macos::execute(config)
114 }
115
116 #[cfg(not(target_os = "macos"))]
117 {
118 Err("Unsupported platform. safe-shell currently supports macOS only. Linux support coming soon — see https://github.com/claudexai/safe-shell/issues".into())
119 }
120}
121
122#[cfg(test)]
123mod tests {
124 use super::*;
125
126 #[test]
127 fn resolve_path_home() {
128 let cwd = std::path::Path::new("/project");
129 let resolved = resolve_path("~/.aws", cwd);
130 let home = dirs::home_dir().unwrap();
131 assert_eq!(resolved, format!("{}/.aws", home.display()));
132 }
133
134 #[test]
135 fn resolve_path_dot() {
136 let cwd = std::path::Path::new("/project");
137 let resolved = resolve_path("./node_modules", cwd);
138 assert_eq!(resolved, "/project/node_modules");
139 }
140
141 #[test]
142 fn resolve_path_dot_alone() {
143 let cwd = std::path::Path::new("/project");
144 let resolved = resolve_path(".", cwd);
145 assert_eq!(resolved, "/project");
146 }
147
148 #[test]
149 fn resolve_path_absolute() {
150 let cwd = std::path::Path::new("/project");
151 let resolved = resolve_path("/tmp", cwd);
152 assert_eq!(resolved, "/tmp");
153 }
154
155 #[test]
156 fn resolve_path_relative() {
157 let cwd = std::path::Path::new("/project");
158 let resolved = resolve_path("target", cwd);
159 assert_eq!(resolved, "/project/target");
160 }
161}