use std::env;
use std::io;
use std::path::{Path, PathBuf};
use tokio::process::Command;
use tracing::debug;
use crate::config::default_allowed_env_keys;
#[derive(Debug, Clone)]
pub struct SandboxPolicy {
pub allowed_env_keys: Vec<String>,
pub working_directory: PathBuf,
}
impl SandboxPolicy {
#[must_use]
pub fn new(working_directory: PathBuf) -> Self {
Self {
allowed_env_keys: default_allowed_env_keys(),
working_directory,
}
}
#[must_use]
pub fn with_env_keys(mut self, keys: Vec<String>) -> Self {
self.allowed_env_keys = keys;
self
}
}
pub fn apply_sandbox(cmd: &mut Command, policy: &SandboxPolicy) {
cmd.env_clear();
let mut resolved = Vec::new();
let mut missing = Vec::new();
for key in &policy.allowed_env_keys {
if let Ok(value) = env::var(key) {
cmd.env(key, &value);
resolved.push(key.as_str());
} else {
missing.push(key.as_str());
}
}
cmd.current_dir(&policy.working_directory);
debug!(
cwd = %policy.working_directory.display(),
resolved_count = resolved.len(),
missing_count = missing.len(),
?missing,
"Applied sandbox policy"
);
}
pub fn build_policy(
working_dir: Option<&Path>,
allowed_env_keys: &[String],
) -> io::Result<SandboxPolicy> {
let dir = match working_dir {
Some(p) if p.exists() => p.to_path_buf(),
_ => env::current_dir()?,
};
Ok(SandboxPolicy::new(dir).with_env_keys(allowed_env_keys.to_vec()))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sandbox_policy_defaults() {
let policy = SandboxPolicy::new(PathBuf::from("/tmp"));
assert_eq!(policy.working_directory, PathBuf::from("/tmp"));
assert!(!policy.allowed_env_keys.is_empty());
}
#[test]
fn test_sandbox_policy_with_custom_keys() {
let policy =
SandboxPolicy::new(PathBuf::from("/tmp")).with_env_keys(vec!["CUSTOM_KEY".to_owned()]);
assert_eq!(policy.allowed_env_keys, vec!["CUSTOM_KEY"]);
}
#[test]
fn test_build_policy_fallback_to_cwd() {
let keys = vec!["HOME".to_owned()];
let policy = build_policy(None, &keys).unwrap();
assert_eq!(policy.working_directory, env::current_dir().unwrap());
}
#[test]
fn test_build_policy_nonexistent_dir_falls_back() {
let keys = vec!["HOME".to_owned()];
let policy = build_policy(Some(Path::new("/nonexistent/path/xyz123")), &keys).unwrap();
assert_eq!(policy.working_directory, env::current_dir().unwrap());
}
#[test]
fn test_build_policy_existing_dir() {
let keys = vec!["HOME".to_owned()];
let dir = env::temp_dir();
let policy = build_policy(Some(&dir), &keys).unwrap();
assert_eq!(policy.working_directory, dir);
assert_eq!(policy.allowed_env_keys, vec!["HOME"]);
}
}