//! Local Runtime Module //! //! Provides local execution capabilities by combining Process and Native runtimes. //! This module serves as a facade for all local process-based execution. //! //! The `ProcessRuntime` is used for Python (and other interpreted languages), //! driven by `RuntimeExecutionConfig` rather than language-specific Rust code. use super::native::NativeRuntime; use super::process::ProcessRuntime; use super::{ExecutionContext, ExecutionResult, Runtime, RuntimeError, RuntimeResult}; use async_trait::async_trait; use attune_common::models::runtime::{ InlineExecutionConfig, InlineExecutionStrategy, InterpreterConfig, RuntimeExecutionConfig, }; use std::path::PathBuf; use tracing::{debug, info}; /// Local runtime that delegates to Process, Shell, or Native based on action type pub struct LocalRuntime { native: NativeRuntime, python: ProcessRuntime, shell: ProcessRuntime, } impl LocalRuntime { /// Create a new local runtime with default settings. /// /// Uses a default Python `RuntimeExecutionConfig` for the process runtime, /// since this is a fallback when runtimes haven't been loaded from the database. pub fn new() -> Self { let python_config = RuntimeExecutionConfig { interpreter: InterpreterConfig { binary: "python3".to_string(), args: vec![], file_extension: Some(".py".to_string()), }, inline_execution: InlineExecutionConfig::default(), environment: None, dependencies: None, env_vars: std::collections::HashMap::new(), }; let shell_config = RuntimeExecutionConfig { interpreter: InterpreterConfig { binary: "/bin/bash".to_string(), args: vec![], file_extension: Some(".sh".to_string()), }, inline_execution: InlineExecutionConfig { strategy: InlineExecutionStrategy::TempFile, extension: Some(".sh".to_string()), inject_shell_helpers: true, }, environment: None, dependencies: None, env_vars: std::collections::HashMap::new(), }; Self { native: NativeRuntime::new(), python: ProcessRuntime::new( "python".to_string(), python_config, PathBuf::from("/opt/attune/packs"), PathBuf::from("/opt/attune/runtime_envs"), ), shell: ProcessRuntime::new( "shell".to_string(), shell_config, PathBuf::from("/opt/attune/packs"), PathBuf::from("/opt/attune/runtime_envs"), ), } } /// Create a local runtime with custom runtimes pub fn with_runtimes( native: NativeRuntime, python: ProcessRuntime, shell: ProcessRuntime, ) -> Self { Self { native, python, shell, } } /// Get the appropriate runtime for the given context fn select_runtime(&self, context: &ExecutionContext) -> RuntimeResult<&dyn Runtime> { if self.native.can_execute(context) { debug!("Selected Native runtime for action: {}", context.action_ref); Ok(&self.native) } else if self.python.can_execute(context) { debug!( "Selected Python (ProcessRuntime) for action: {}", context.action_ref ); Ok(&self.python) } else if self.shell.can_execute(context) { debug!( "Selected Shell (ProcessRuntime) for action: {}", context.action_ref ); Ok(&self.shell) } else { Err(RuntimeError::RuntimeNotFound(format!( "No suitable local runtime found for action: {}", context.action_ref ))) } } } impl Default for LocalRuntime { fn default() -> Self { Self::new() } } #[async_trait] impl Runtime for LocalRuntime { fn name(&self) -> &str { "local" } fn can_execute(&self, context: &ExecutionContext) -> bool { self.native.can_execute(context) || self.python.can_execute(context) || self.shell.can_execute(context) } async fn execute(&self, context: ExecutionContext) -> RuntimeResult { info!( "Executing local action: {} (execution_id: {})", context.action_ref, context.execution_id ); let runtime = self.select_runtime(&context)?; runtime.execute(context).await } async fn setup(&self) -> RuntimeResult<()> { info!("Setting up Local runtime"); self.native.setup().await?; self.python.setup().await?; self.shell.setup().await?; info!("Local runtime setup complete"); Ok(()) } async fn cleanup(&self) -> RuntimeResult<()> { info!("Cleaning up Local runtime"); self.native.cleanup().await?; self.python.cleanup().await?; self.shell.cleanup().await?; Ok(()) } async fn validate(&self) -> RuntimeResult<()> { debug!("Validating Local runtime"); self.native.validate().await?; self.python.validate().await?; self.shell.validate().await?; Ok(()) } } #[cfg(test)] mod tests { use super::*; use crate::runtime::{OutputFormat, ParameterDelivery, ParameterFormat}; use std::collections::HashMap; #[tokio::test] async fn test_local_runtime_shell() { let runtime = LocalRuntime::new(); let context = ExecutionContext { execution_id: 2, action_ref: "test.shell_action".to_string(), parameters: HashMap::new(), env: HashMap::new(), secrets: HashMap::new(), timeout: Some(10), working_dir: None, entry_point: "run.sh".to_string(), code: Some("#!/bin/bash\necho 'hello from shell'".to_string()), code_path: None, runtime_name: Some("shell".to_string()), 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, stdout_log_path: None, stderr_log_path: None, parameter_delivery: ParameterDelivery::default(), parameter_format: ParameterFormat::default(), output_format: OutputFormat::default(), cancel_token: None, }; assert!(runtime.can_execute(&context)); let result = runtime.execute(context).await.unwrap(); assert!(result.is_success()); assert!(result.stdout.contains("hello from shell")); } #[tokio::test] async fn test_local_runtime_unknown() { let runtime = LocalRuntime::new(); let context = ExecutionContext { execution_id: 3, action_ref: "test.unknown_action".to_string(), parameters: HashMap::new(), env: HashMap::new(), secrets: HashMap::new(), timeout: Some(10), working_dir: None, entry_point: "run".to_string(), code: Some("some code".to_string()), code_path: None, runtime_name: Some("unknown".to_string()), 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, stdout_log_path: None, stderr_log_path: None, parameter_delivery: ParameterDelivery::default(), parameter_format: ParameterFormat::default(), output_format: OutputFormat::default(), cancel_token: None, }; assert!(!runtime.can_execute(&context)); } }