use super::sandbox_exec::{SandboxPlan, SandboxStep};
use crate::core::purifier;
use crate::core::types::Machine;
use crate::transport;
use std::path::Path;
#[derive(Debug, Clone)]
pub struct SandboxExecResult {
pub output_hash: String,
pub store_path: String,
pub steps_executed: Vec<(u8, String, bool)>,
pub duration_secs: f64,
}
pub fn execute_sandbox_plan(
plan: &SandboxPlan,
script: &str,
machine: &Machine,
store_dir: &Path,
timeout_secs: Option<u64>,
) -> Result<SandboxExecResult, String> {
let start = std::time::Instant::now();
let mut steps_executed = Vec::new();
for step in &plan.steps {
let success = execute_step(step, machine, timeout_secs)?;
steps_executed.push((step.step, step.description.clone(), success));
if !success {
cleanup_namespace(&plan.namespace_id, machine);
return Err(format!(
"sandbox step {} failed: {}",
step.step, step.description
));
}
}
let duration = start.elapsed().as_secs_f64();
let output_hash = compute_sandbox_output_hash(plan, script);
let hash_bare = output_hash.strip_prefix("blake3:").unwrap_or(&output_hash);
let store_path = format!("{}/{hash_bare}/content", store_dir.display());
Ok(SandboxExecResult {
output_hash,
store_path,
steps_executed,
duration_secs: duration,
})
}
fn execute_step(
step: &SandboxStep,
machine: &Machine,
timeout_secs: Option<u64>,
) -> Result<bool, String> {
let cmd = match &step.command {
Some(c) => c,
None => return Ok(true), };
purifier::validate_script(cmd)
.map_err(|e| format!("I8 violation at step {}: {e}", step.step))?;
let output = transport::exec_script_timeout(machine, cmd, timeout_secs)
.map_err(|e| format!("step {} transport error: {e}", step.step))?;
Ok(output.success())
}
fn cleanup_namespace(namespace_id: &str, machine: &Machine) {
let cleanup_cmd = format!("rm -rf '/tmp/forjar-sandbox/{namespace_id}' 2>/dev/null; true");
let _ = transport::exec_script_timeout(machine, &cleanup_cmd, Some(10));
}
fn compute_sandbox_output_hash(plan: &SandboxPlan, script: &str) -> String {
let mut components: Vec<String> = plan
.overlay
.lower_dirs
.iter()
.map(|p| p.display().to_string())
.collect();
components.sort();
components.push(script.to_string());
let refs: Vec<&str> = components.iter().map(|s| s.as_str()).collect();
crate::tripwire::hasher::composite_hash(&refs)
}
pub fn dry_run_sandbox_plan(plan: &SandboxPlan) -> Result<Vec<String>, String> {
let mut commands = Vec::new();
for step in &plan.steps {
if let Some(cmd) = &step.command {
purifier::validate_script(cmd)
.map_err(|e| format!("I8 dry-run violation at step {}: {e}", step.step))?;
commands.push(cmd.clone());
}
}
Ok(commands)
}
pub fn validate_sandbox_commands(plan: &SandboxPlan) -> Vec<String> {
let mut errors = Vec::new();
for step in &plan.steps {
if let Some(cmd) = &step.command {
if let Err(e) = purifier::validate_script(cmd) {
errors.push(format!("step {}: {e}", step.step));
}
}
}
errors
}