//! Runtime Module //! //! Provides runtime abstraction and implementations for executing actions //! in different environments. The primary runtime is `ProcessRuntime`, a //! generic, configuration-driven runtime that reads its behavior from the //! database `runtime.execution_config` JSONB column. //! //! Language-specific runtimes (Python, Node.js, etc.) are NOT implemented //! as separate Rust types. Instead, the `ProcessRuntime` handles all //! languages by using the interpreter, environment, and dependency //! configuration stored in the database. //! //! ## Runtime Version Selection //! //! When an action declares a `runtime_version_constraint` (e.g., `">=3.12"`), //! the executor resolves the best matching `RuntimeVersion` from the database //! and passes its `execution_config` through `ExecutionContext::runtime_config_override`. //! The `ProcessRuntime` uses this override instead of its built-in config, //! enabling version-specific interpreter binaries, environment commands, etc. //! //! The environment directory is also overridden to include the version suffix //! (e.g., `python-3.12` instead of `python`) so that different versions //! maintain isolated environments. pub mod dependency; pub mod local; pub mod log_writer; pub mod native; pub mod parameter_passing; pub mod process; pub mod process_executor; // Re-export runtime implementations pub use local::LocalRuntime; pub use native::NativeRuntime; pub use process::ProcessRuntime; use async_trait::async_trait; use attune_common::models::runtime::RuntimeExecutionConfig; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::path::PathBuf; use thiserror::Error; use tokio_util::sync::CancellationToken; // Re-export dependency management types pub use dependency::{ DependencyError, DependencyManager, DependencyManagerRegistry, DependencyResult, DependencySpec, EnvironmentInfo, }; pub use log_writer::{BoundedLogResult, BoundedLogWriter}; pub use parameter_passing::{ParameterDeliveryConfig, PreparedParameters}; // Re-export parameter types from common pub use attune_common::models::{OutputFormat, ParameterDelivery, ParameterFormat}; /// Runtime execution result pub type RuntimeResult = std::result::Result; /// Runtime execution errors #[derive(Debug, Error)] pub enum RuntimeError { #[error("Execution failed: {0}")] ExecutionFailed(String), #[error("Timeout after {0} seconds")] Timeout(u64), #[error("Runtime not found: {0}")] RuntimeNotFound(String), #[error("Invalid action: {0}")] InvalidAction(String), #[error("IO error: {0}")] IoError(#[from] std::io::Error), #[error("Serialization error: {0}")] SerializationError(#[from] serde_json::Error), #[error("Process error: {0}")] ProcessError(String), #[error("Setup error: {0}")] SetupError(String), #[error("Cleanup error: {0}")] CleanupError(String), } /// Action execution context #[derive(Debug, Clone)] pub struct ExecutionContext { /// Execution ID pub execution_id: i64, /// Action reference (pack.action) pub action_ref: String, /// Action parameters pub parameters: HashMap, /// Environment variables pub env: HashMap, /// Secrets (passed securely via stdin, not environment variables). /// Values are JSON — strings, objects, arrays, numbers, or booleans. pub secrets: HashMap, /// Execution timeout in seconds pub timeout: Option, /// Working directory pub working_dir: Option, /// Action entry point (script, function, etc.) pub entry_point: String, /// Action code/script content pub code: Option, /// Action code file path (alternative to code) pub code_path: Option, /// Runtime name (python, shell, etc.) - used to select the correct runtime pub runtime_name: Option, /// Optional override of the runtime's execution config, set when a specific /// runtime version has been selected (e.g., Python 3.12 vs the parent /// "Python" runtime). When present, `ProcessRuntime` uses this config /// instead of its built-in one for interpreter resolution, environment /// setup, and dependency management. pub runtime_config_override: Option, /// Optional override of the environment directory suffix. When a specific /// runtime version is selected, the env dir includes the version /// (e.g., `python-3.12` instead of `python`) for per-version isolation. /// Format: just the directory name, not the full path. pub runtime_env_dir_suffix: Option, /// The selected runtime version string for logging/diagnostics /// (e.g., "3.12.1"). `None` means the parent runtime config is used as-is. pub selected_runtime_version: Option, /// Maximum stdout size in bytes (for log truncation) pub max_stdout_bytes: usize, /// Maximum stderr size in bytes (for log truncation) pub max_stderr_bytes: usize, /// How parameters should be delivered to the action pub parameter_delivery: ParameterDelivery, /// Format for parameter serialization pub parameter_format: ParameterFormat, /// Format for output parsing pub output_format: OutputFormat, /// Optional cancellation token for process termination. /// When triggered, the executor sends SIGTERM → SIGKILL /// with a short grace period. pub cancel_token: Option, } impl ExecutionContext { /// Create a test context with default values (for tests) #[cfg(test)] pub fn test_context(action_ref: String, code: Option) -> Self { use std::collections::HashMap; Self { execution_id: 1, action_ref, parameters: HashMap::new(), env: HashMap::new(), secrets: HashMap::new(), timeout: Some(10), working_dir: None, entry_point: "run".to_string(), code, code_path: None, runtime_name: None, runtime_config_override: None, runtime_env_dir_suffix: None, selected_runtime_version: None, max_stdout_bytes: 10 * 1024 * 1024, max_stderr_bytes: 10 * 1024 * 1024, parameter_delivery: ParameterDelivery::default(), parameter_format: ParameterFormat::default(), output_format: OutputFormat::default(), cancel_token: None, } } } /// Action execution result #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ExecutionResult { /// Exit code (0 = success) pub exit_code: i32, /// Standard output pub stdout: String, /// Standard error pub stderr: String, /// Execution result data (parsed from stdout or returned by action) pub result: Option, /// Execution duration in milliseconds pub duration_ms: u64, /// Error message if execution failed pub error: Option, /// Whether stdout was truncated due to size limits #[serde(default)] pub stdout_truncated: bool, /// Whether stderr was truncated due to size limits #[serde(default)] pub stderr_truncated: bool, /// Number of bytes truncated from stdout (0 if not truncated) #[serde(default)] pub stdout_bytes_truncated: usize, /// Number of bytes truncated from stderr (0 if not truncated) #[serde(default)] pub stderr_bytes_truncated: usize, } impl ExecutionResult { /// Check if execution was successful pub fn is_success(&self) -> bool { self.exit_code == 0 && self.error.is_none() } /// Create a success result pub fn success(stdout: String, result: Option, duration_ms: u64) -> Self { Self { exit_code: 0, stdout, stderr: String::new(), result, duration_ms, error: None, stdout_truncated: false, stderr_truncated: false, stdout_bytes_truncated: 0, stderr_bytes_truncated: 0, } } /// Create a failure result pub fn failure(exit_code: i32, stderr: String, error: String, duration_ms: u64) -> Self { Self { exit_code, stdout: String::new(), stderr, result: None, duration_ms, error: Some(error), stdout_truncated: false, stderr_truncated: false, stdout_bytes_truncated: 0, stderr_bytes_truncated: 0, } } } /// Runtime trait for executing actions #[async_trait] pub trait Runtime: Send + Sync { /// Get the runtime name fn name(&self) -> &str; /// Check if this runtime can execute the given action fn can_execute(&self, context: &ExecutionContext) -> bool; /// Execute an action async fn execute(&self, context: ExecutionContext) -> RuntimeResult; /// Setup the runtime environment (called once on worker startup) async fn setup(&self) -> RuntimeResult<()> { Ok(()) } /// Cleanup the runtime environment (called on worker shutdown) async fn cleanup(&self) -> RuntimeResult<()> { Ok(()) } /// Validate the runtime is properly configured async fn validate(&self) -> RuntimeResult<()> { Ok(()) } } /// Runtime registry for managing multiple runtime implementations pub struct RuntimeRegistry { runtimes: Vec>, } impl RuntimeRegistry { /// Create a new runtime registry pub fn new() -> Self { Self { runtimes: Vec::new(), } } /// Register a runtime pub fn register(&mut self, runtime: Box) { self.runtimes.push(runtime); } /// Get a runtime that can execute the given context pub fn get_runtime(&self, context: &ExecutionContext) -> RuntimeResult<&dyn Runtime> { // If runtime_name is specified, use it to select the runtime directly if let Some(ref runtime_name) = context.runtime_name { return self .runtimes .iter() .find(|r| r.name() == runtime_name) .map(|r| r.as_ref()) .ok_or_else(|| { RuntimeError::RuntimeNotFound(format!( "Runtime '{}' not found for action: {} (available: {})", runtime_name, context.action_ref, self.list_runtimes().join(", ") )) }); } // Otherwise, fall back to can_execute check self.runtimes .iter() .find(|r| r.can_execute(context)) .map(|r| r.as_ref()) .ok_or_else(|| { RuntimeError::RuntimeNotFound(format!( "No runtime found for action: {} (available: {})", context.action_ref, self.list_runtimes().join(", ") )) }) } /// Setup all registered runtimes pub async fn setup_all(&self) -> RuntimeResult<()> { for runtime in &self.runtimes { runtime.setup().await?; } Ok(()) } /// Cleanup all registered runtimes pub async fn cleanup_all(&self) -> RuntimeResult<()> { for runtime in &self.runtimes { runtime.cleanup().await?; } Ok(()) } /// Validate all registered runtimes pub async fn validate_all(&self) -> RuntimeResult<()> { for runtime in &self.runtimes { runtime.validate().await?; } Ok(()) } /// List all registered runtimes pub fn list_runtimes(&self) -> Vec<&str> { self.runtimes.iter().map(|r| r.name()).collect() } } impl Default for RuntimeRegistry { fn default() -> Self { Self::new() } }