use std::path::Path;
use tokio::process::Command;
use tracing::warn;
const SANDBOX_EXEC: &str = "/usr/bin/sandbox-exec";
fn build_profile(data_dir: &Path, profile: &crate::SandboxProfile) -> String {
let data_data = data_dir.join("data");
let data_data_str = data_data.display();
let config_file = data_dir.join("config.toml");
let config_str = config_file.display();
let mut deny_writes = format!(
r#" (subpath "/System")
(subpath "/bin")
(subpath "/sbin")
(subpath "/usr/bin")
(subpath "/usr/sbin")
(subpath "/usr/lib")
(subpath "/usr/libexec")
(subpath "/private/etc")
(subpath "/Library")
(subpath "{data_data_str}")
(literal "{config_str}")
"#
);
let mut deny_reads = format!(
r#" (subpath "{data_data_str}")
(literal "{config_str}")
"#
);
for blocked in &profile.blocked_paths {
let blocked_str = blocked.display();
deny_writes.push_str(&format!(" (subpath \"{blocked_str}\")\n"));
deny_reads.push_str(&format!(" (subpath \"{blocked_str}\")\n"));
}
format!(
"(version 1)\n\
(allow default)\n\
(deny file-write*\n{deny_writes})\n\
(deny file-read*\n{deny_reads})\n"
)
}
pub(crate) fn protected_command(
program: &str,
data_dir: &Path,
profile: &crate::SandboxProfile,
) -> Command {
if !Path::new(SANDBOX_EXEC).exists() {
warn!("sandbox-exec not found at {SANDBOX_EXEC}; falling back to code-level protection");
return Command::new(program);
}
let built_profile = build_profile(data_dir, profile);
let mut cmd = Command::new(SANDBOX_EXEC);
cmd.arg("-p").arg(built_profile).arg("--").arg(program);
cmd
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
#[test]
fn test_profile_blocks_system_dirs() {
let data_dir = PathBuf::from("/home/user/.kernex");
let profile_obj = crate::SandboxProfile::default();
let profile = build_profile(&data_dir, &profile_obj);
assert!(profile.contains("(deny file-write*"));
assert!(profile.contains(r#"(subpath "/System")"#));
assert!(profile.contains(r#"(subpath "/bin")"#));
assert!(profile.contains(r#"(subpath "/sbin")"#));
assert!(profile.contains(r#"(subpath "/usr/bin")"#));
assert!(profile.contains(r#"(subpath "/usr/sbin")"#));
assert!(profile.contains(r#"(subpath "/usr/lib")"#));
assert!(profile.contains(r#"(subpath "/usr/libexec")"#));
assert!(profile.contains(r#"(subpath "/private/etc")"#));
assert!(profile.contains(r#"(subpath "/Library")"#));
}
#[test]
fn test_profile_blocks_data_dir() {
let data_dir = PathBuf::from("/home/user/.kernex");
let profile_obj = crate::SandboxProfile::default();
let profile = build_profile(&data_dir, &profile_obj);
assert!(
profile.contains("/home/user/.kernex/data"),
"should block data dir (memory.db)"
);
}
#[test]
fn test_profile_allows_usr_local() {
let data_dir = PathBuf::from("/tmp/ws");
let profile_obj = crate::SandboxProfile::default();
let profile = build_profile(&data_dir, &profile_obj);
assert!(
!profile.contains(r#"(subpath "/usr/local")"#),
"/usr/local should not be blocked"
);
}
#[test]
fn test_profile_allows_by_default() {
let data_dir = PathBuf::from("/tmp/ws");
let profile_obj = crate::SandboxProfile::default();
let profile = build_profile(&data_dir, &profile_obj);
assert!(
profile.contains("(allow default)"),
"should allow everything by default"
);
}
#[test]
fn test_profile_blocks_data_dir_reads() {
let data_dir = PathBuf::from("/home/user/.kernex");
let profile_obj = crate::SandboxProfile::default();
let profile = build_profile(&data_dir, &profile_obj);
assert!(
profile.contains("(deny file-read*"),
"should have file-read* deny"
);
let read_deny_pos = profile.find("(deny file-read*").unwrap();
let after_read = &profile[read_deny_pos..];
assert!(
after_read.contains("/home/user/.kernex/data"),
"should block reads to data dir"
);
}
#[test]
fn test_profile_blocks_config_writes() {
let data_dir = PathBuf::from("/home/user/.kernex");
let profile_obj = crate::SandboxProfile::default();
let profile = build_profile(&data_dir, &profile_obj);
let write_deny_pos = profile.find("(deny file-write*").unwrap();
let read_deny_pos = profile.find("(deny file-read*").unwrap();
let write_section = &profile[write_deny_pos..read_deny_pos];
assert!(
write_section.contains("config.toml"),
"should block writes to config.toml"
);
}
#[test]
fn test_profile_blocks_config_reads() {
let data_dir = PathBuf::from("/home/user/.kernex");
let profile_obj = crate::SandboxProfile::default();
let profile = build_profile(&data_dir, &profile_obj);
assert!(
profile.contains(r#"(literal "/home/user/.kernex/config.toml")"#),
"should block reads to config.toml"
);
}
#[test]
fn test_command_structure() {
let data_dir = PathBuf::from("/tmp/ws");
let profile = crate::SandboxProfile::default();
let cmd = protected_command("claude", &data_dir, &profile);
let program = cmd.as_std().get_program().to_string_lossy().to_string();
assert!(
program.contains("sandbox-exec") || program.contains("claude"),
"unexpected program: {program}"
);
}
}