//! chrs-exec: Isolated Task Execution Engine for CHORUS. use bollard::container::{Config, CreateContainerOptions, StartContainerOptions, LogOutput, LogsOptions}; use bollard::Docker; use bollard::image::CreateImageOptions; use bollard::models::HostConfig; use futures_util::stream::TryStreamExt; use serde::{Deserialize, Serialize}; use thiserror::Error; use std::collections::HashMap; use chrono::Utc; use uuid::Uuid; /// Represents a request for task execution. #[derive(Debug, Serialize, Deserialize, Clone)] pub struct TaskRequest { pub language: String, pub code: Option, // Optional raw code to execute pub agent_prompt: Option, // NEW: Optional prompt for the internal agent (opencode) pub workspace_path: Option, // NEW: Path to mount into the container pub timeout_secs: u64, } /// Results of a task execution. #[derive(Debug, Serialize, Deserialize, Clone)] pub struct TaskResult { pub exit_code: i64, pub stdout: String, pub stderr: String, pub duration_ms: u64, } #[derive(Debug, Error)] pub enum ExecError { #[error("Docker error: {0}")] Docker(#[from] bollard::errors::Error), #[error("Task timeout")] Timeout, #[error("Image not found: {0}")] ImageNotFound(String), } /// Executes tasks within isolated Docker containers. pub struct DockerExecutor { docker: Docker, image_map: HashMap, } impl DockerExecutor { pub fn new() -> Result { let docker = Docker::connect_with_local_defaults()?; let mut image_map = HashMap::new(); image_map.insert("rust".into(), "anthonyrawlins/chorus-rust-dev:latest".into()); image_map.insert("python".into(), "anthonyrawlins/chorus-python-dev:latest".into()); image_map.insert("base".into(), "anthonyrawlins/chorus-base:latest".into()); Ok(Self { docker, image_map }) } /// Executes a task using the "Agent Inception" pattern if a prompt is provided. pub async fn execute(&self, task: TaskRequest) -> Result { let start_time = Utc::now(); let image = self.image_map.get(&task.language) .or_else(|| self.image_map.get("base")) .unwrap(); // 1. Ensure image is pulled self.docker.create_image( Some(CreateImageOptions { from_image: image.as_str(), ..Default::default() }), None, None ).try_collect::>().await?; // 2. Prepare Command (Agent Inception vs Raw Script) // Using Vec to avoid lifetime issues with temporary formatted strings let cmd: Vec = if let Some(prompt) = &task.agent_prompt { vec!["opencode".to_string(), "run".to_string(), "--prompt".to_string(), prompt.clone()] } else if let Some(code) = &task.code { vec![ "bash".to_string(), "-c".to_string(), format!("echo '{}' > task.code && bash task.code", code.replace("'", "'\\''")) ] } else { vec!["ls".to_string(), "-la".to_string()] }; // 3. Create Container with optional Volume mount let container_name = format!("chrs-task-{}", Uuid::new_v4()); let mut binds = Vec::new(); if let Some(path) = &task.workspace_path { binds.push(format!("{}:/workspace", path)); } let host_config = HostConfig { memory: Some(2 * 1024 * 1024 * 1024), // 2GB nano_cpus: Some(2_000_000_000), // 2 Cores network_mode: Some("none".into()), // Air-gapped for security binds: Some(binds), ..Default::default() }; let config = Config { image: Some(image.as_str()), cmd: Some(cmd.iter().map(|s| s.as_str()).collect()), working_dir: Some("/workspace"), host_config: Some(host_config), ..Default::default() }; self.docker.create_container( Some(CreateContainerOptions { name: container_name.as_str(), ..Default::default() }), config ).await?; // 4. Start and Wait self.docker.start_container(&container_name, None::>).await?; // 5. Collect Logs let logs = self.docker.logs( &container_name, Some(LogsOptions:: { stdout: true, stderr: true, follow: true, ..Default::default() }) ).try_collect::>().await?; let mut stdout = String::new(); let mut stderr = String::new(); for log in logs { match log { LogOutput::StdOut { message } => stdout.push_str(&String::from_utf8_lossy(&message)), LogOutput::StdErr { message } => stderr.push_str(&String::from_utf8_lossy(&message)), _ => {} } } // 6. Cleanup let _ = self.docker.stop_container(&container_name, None).await; let _ = self.docker.remove_container(&container_name, None).await; let end_time = Utc::now(); let duration = end_time.signed_duration_since(start_time).num_milliseconds() as u64; Ok(TaskResult { exit_code: 0, stdout, stderr, duration_ms: duration, }) } }