use std::env;
use std::io;
use std::path::PathBuf;
use tokio::process::Command;
use tracing::debug;
use crate::config::{default_allowed_env_keys, RunnerConfig};
#[derive(Debug, Clone)]
pub struct SandboxPolicy {
pub allowed_env_keys: Vec<String>,
pub working_directory: PathBuf,
pub env_overrides: Vec<(String, String)>,
}
impl SandboxPolicy {
#[must_use]
pub fn new(working_directory: PathBuf) -> Self {
Self {
allowed_env_keys: default_allowed_env_keys(),
working_directory,
env_overrides: Vec::new(),
}
}
#[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());
}
}
for (key, value) in &policy.env_overrides {
cmd.env(key, value);
}
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(config: &RunnerConfig) -> io::Result<SandboxPolicy> {
let dir = match config.working_directory.as_deref() {
Some(p) if p.exists() => p.to_path_buf(),
_ => env::current_dir()?,
};
let mut policy = SandboxPolicy::new(dir).with_env_keys(config.allowed_env_keys.clone());
policy.env_overrides.clone_from(&config.env);
Ok(policy)
}
#[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(
&RunnerConfig::new(PathBuf::from("/bin/true")).with_allowed_env_keys(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(
&RunnerConfig::new(PathBuf::from("/bin/true"))
.with_allowed_env_keys(keys)
.with_working_directory(PathBuf::from("/nonexistent/path/xyz123")),
)
.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(
&RunnerConfig::new(PathBuf::from("/bin/true"))
.with_allowed_env_keys(keys)
.with_working_directory(dir.clone()),
)
.unwrap(); assert_eq!(policy.working_directory, dir);
assert_eq!(policy.allowed_env_keys, vec!["HOME"]);
}
}