From 2253259ec5bf62bd330e0d79f2649be1f7fbf91b Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Thu, 26 Feb 2026 18:53:51 -0500 Subject: [PATCH] Add [execution] section to TOML task config for execution environment Allow specifying execution environment (local/docker/daytona) in TOML task configs so pipelines are self-contained. CLI flag takes precedence over TOML, which takes precedence over the default (local). Co-Authored-By: Claude Opus 4.6 Entire-Checkpoint: 600b7720bbda --- crates/attractor/src/cli/mod.rs | 8 ++++---- crates/attractor/src/cli/run.rs | 16 +++++++++++++--- crates/attractor/src/cli/serve.rs | 2 +- crates/attractor/src/cli/task_config.rs | 21 +++++++++++++++++++++ 4 files changed, 39 insertions(+), 8 deletions(-) diff --git a/crates/attractor/src/cli/mod.rs b/crates/attractor/src/cli/mod.rs index 8bbaa4926..6564fad80 100644 --- a/crates/attractor/src/cli/mod.rs +++ b/crates/attractor/src/cli/mod.rs @@ -105,8 +105,8 @@ pub struct RunArgs { pub verbose: u8, /// Execution environment for agent tools - #[arg(long, value_enum, default_value_t)] - pub execution_env: ExecutionEnvKind, + #[arg(long, value_enum)] + pub execution_env: Option, } #[derive(Args)] @@ -139,8 +139,8 @@ pub struct ServeArgs { pub dry_run: bool, /// Execution environment for agent tools - #[arg(long, value_enum, default_value_t)] - pub execution_env: ExecutionEnvKind, + #[arg(long, value_enum)] + pub execution_env: Option, } /// Read a .dot file from disk. diff --git a/crates/attractor/src/cli/run.rs b/crates/attractor/src/cli/run.rs index 85924113c..04e6cfcf8 100644 --- a/crates/attractor/src/cli/run.rs +++ b/crates/attractor/src/cli/run.rs @@ -18,7 +18,7 @@ use crate::validation::Severity; use super::backend::AgentBackend; use super::task_config; -use super::{compute_stage_cost, format_cost, format_duration_human, format_event_detail, format_event_summary, format_tokens_human, print_diagnostics, read_dot_file, RunArgs}; +use super::{compute_stage_cost, format_cost, format_duration_human, format_event_detail, format_event_summary, format_tokens_human, print_diagnostics, read_dot_file, ExecutionEnvKind, RunArgs}; /// Accumulates token usage and cost across all pipeline stages. #[derive(Default)] @@ -218,7 +218,17 @@ pub async fn run_command(args: RunArgs, styles: &'static Styles) -> anyhow::Resu Arc::new(ConsoleInterviewer::new(styles)) }; - // 5. Resolve backend, model, and provider + // 5. Resolve execution environment: CLI flag > TOML > default + let toml_execution_env = task_cfg + .as_ref() + .and_then(|c| c.execution.as_ref()) + .and_then(|e| e.environment.as_deref()) + .map(|s| s.parse::()) + .transpose() + .map_err(|e| anyhow::anyhow!("Invalid execution environment in TOML: {e}"))?; + let execution_env = args.execution_env.or(toml_execution_env).unwrap_or_default(); + + // 6. Resolve backend, model, and provider let dry_run_mode = if args.dry_run { true } else { @@ -294,7 +304,7 @@ pub async fn run_command(args: RunArgs, styles: &'static Styles) -> anyhow::Resu provider.clone(), args.verbose, styles, - args.execution_env, + execution_env, setup_commands.clone(), ))) } diff --git a/crates/attractor/src/cli/serve.rs b/crates/attractor/src/cli/serve.rs index 5f727e524..17a4e5b98 100644 --- a/crates/attractor/src/cli/serve.rs +++ b/crates/attractor/src/cli/serve.rs @@ -66,7 +66,7 @@ pub async fn serve_command(args: ServeArgs, styles: &'static Styles) -> anyhow:: provider.clone(), 0, styles, - args.execution_env, + args.execution_env.unwrap_or_default(), Vec::new(), ))) } diff --git a/crates/attractor/src/cli/task_config.rs b/crates/attractor/src/cli/task_config.rs index 297038893..6c1a9cbcf 100644 --- a/crates/attractor/src/cli/task_config.rs +++ b/crates/attractor/src/cli/task_config.rs @@ -14,6 +14,7 @@ pub struct TaskConfig { pub directory: Option, pub llm: Option, pub setup: Option, + pub execution: Option, pub vars: Option>, } @@ -29,6 +30,11 @@ pub struct SetupConfig { pub timeout_ms: Option, } +#[derive(Debug, Deserialize)] +pub struct ExecutionConfig { + pub environment: Option, +} + /// Load and validate a task config from a TOML file. /// /// The `graph` path in the returned config is resolved relative to the @@ -201,6 +207,21 @@ language = "python" assert_eq!(expand_vars("costs $5", &vars).unwrap(), "costs $5"); } + #[test] + fn parse_toml_with_execution() { + let toml = r#" +version = 1 +task = "Run tests" +graph = "pipeline.dot" + +[execution] +environment = "daytona" +"#; + let config = parse_task_config(toml).unwrap(); + let execution = config.execution.unwrap(); + assert_eq!(execution.environment.as_deref(), Some("daytona")); + } + #[test] fn parse_minimal_toml() { let toml = r#"