From 5d6691df22b6a14348b3fec36319026f3fa7f5e4 Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Sat, 7 Mar 2026 22:43:16 -0500 Subject: [PATCH] Add [sandbox.env] support to workflow configs Allow passing environment variables into sandbox command execution via `[sandbox.env]` in TOML configs. Supports literal values and host env passthrough via `${env.VARNAME}` syntax (whole-value only, missing vars are hard errors). Env vars are injected into command nodes via `cmd.envs()` and into CLI backend agents via the sandbox env file. Co-Authored-By: Claude Opus 4.6 --- crates/arc-api/src/demo/mod.rs | 6 + crates/arc-workflows/src/cli/cli_backend.rs | 24 +- crates/arc-workflows/src/cli/run.rs | 31 ++- crates/arc-workflows/src/cli/run_config.rs | 239 ++++++++++++++++++ crates/arc-workflows/src/engine.rs | 8 + crates/arc-workflows/src/handler/agent.rs | 3 + crates/arc-workflows/src/handler/command.rs | 5 +- .../arc-workflows/src/handler/conditional.rs | 1 + crates/arc-workflows/src/handler/exit.rs | 1 + crates/arc-workflows/src/handler/fan_in.rs | 1 + crates/arc-workflows/src/handler/human.rs | 1 + .../arc-workflows/src/handler/manager_loop.rs | 6 + crates/arc-workflows/src/handler/mod.rs | 2 + crates/arc-workflows/src/handler/parallel.rs | 3 + crates/arc-workflows/src/handler/prompt.rs | 1 + crates/arc-workflows/src/handler/start.rs | 1 + crates/arc-workflows/src/handler/wait.rs | 1 + 17 files changed, 324 insertions(+), 10 deletions(-) diff --git a/crates/arc-api/src/demo/mod.rs b/crates/arc-api/src/demo/mod.rs index 331215441..2c9f059de 100644 --- a/crates/arc-api/src/demo/mod.rs +++ b/crates/arc-api/src/demo/mod.rs @@ -932,6 +932,7 @@ mod runs { network: Some(arc_workflows::daytona_sandbox::DaytonaNetwork::Block), }), exe: None, + env: None, }), vars: Some(std::collections::HashMap::from([ ("repo_url".into(), "https://github.com/org/api-server".into()), @@ -1052,6 +1053,7 @@ mod workflows { network: None, }), exe: None, + env: None, }), vars: Some(std::collections::HashMap::from([ ("repo_url".into(), "https://github.com/org/service".into()), @@ -1117,6 +1119,7 @@ mod workflows { network: None, }), exe: None, + env: None, }), vars: Some(std::collections::HashMap::from([ ("spec_path".into(), "specs/feature.md".into()), @@ -1193,6 +1196,7 @@ mod workflows { network: None, }), exe: None, + env: None, }), vars: Some(std::collections::HashMap::from([ ("source_env".into(), "production".into()), @@ -1260,6 +1264,7 @@ mod workflows { network: None, }), exe: None, + env: None, }), vars: Some(std::collections::HashMap::from([ ("analytics_window".into(), "30d".into()), @@ -2653,6 +2658,7 @@ mod settings { network: Some(arc_workflows::daytona_sandbox::DaytonaNetwork::Block), }), exe: None, + env: None, }), vars: None, checkpoint: Default::default(), diff --git a/crates/arc-workflows/src/cli/cli_backend.rs b/crates/arc-workflows/src/cli/cli_backend.rs index 44ec22ae8..78597a84b 100644 --- a/crates/arc-workflows/src/cli/cli_backend.rs +++ b/crates/arc-workflows/src/cli/cli_backend.rs @@ -1,3 +1,4 @@ +use std::collections::HashMap; use std::path::Path; use std::sync::Arc; @@ -330,16 +331,32 @@ pub fn parse_cli_response(provider: Provider, output: &str) -> Option String { + val.replace('\'', "'\\''") +} + /// CLI backend that invokes external CLI tools (claude, codex, gemini) via `exec_command()`. pub struct AgentCliBackend { model: String, provider: Provider, + env: HashMap, } impl AgentCliBackend { #[must_use] pub fn new(model: String, provider: Provider) -> Self { - Self { model, provider } + Self { + model, + provider, + env: HashMap::new(), + } + } + + #[must_use] + pub fn with_env(mut self, env: HashMap) -> Self { + self.env = env; + self } /// Detect changed files by comparing git state before and after the CLI run. @@ -455,8 +472,11 @@ impl CodergenBackend for AgentCliBackend { env_lines.extend(provider.api_key_env_vars().iter().filter_map(|name| { std::env::var(name) .ok() - .map(|val| format!("export {name}='{val}'")) + .map(|val| format!("export {name}='{}'", shell_escape(&val))) })); + for (name, val) in &self.env { + env_lines.push(format!("export {name}='{}'", shell_escape(val))); + } { sandbox .write_file(&env_path, &env_lines.join("\n")) diff --git a/crates/arc-workflows/src/cli/run.rs b/crates/arc-workflows/src/cli/run.rs index 3bae083ad..4c2cf6a16 100644 --- a/crates/arc-workflows/src/cli/run.rs +++ b/crates/arc-workflows/src/cli/run.rs @@ -659,13 +659,23 @@ pub async fn run_command( ); // 7. Build engine - let registry = default_registry(interviewer.clone(), || { - if dry_run_mode { - None - } else { - let api = AgentApiBackend::new(model.clone(), provider_enum, fallback_chain.clone()); - let cli = AgentCliBackend::new(model.clone(), provider_enum); - Some(Box::new(BackendRouter::new(Box::new(api), cli))) + let sandbox_env: HashMap = run_cfg + .as_ref() + .and_then(|c| c.sandbox.as_ref()) + .and_then(|s| s.env.clone()) + .unwrap_or_default(); + let registry = default_registry(interviewer.clone(), { + let sandbox_env = sandbox_env.clone(); + let model = model.clone(); + move || { + if dry_run_mode { + None + } else { + let api = AgentApiBackend::new(model.clone(), provider_enum, fallback_chain.clone()); + let cli = AgentCliBackend::new(model.clone(), provider_enum) + .with_env(sandbox_env.clone()); + Some(Box::new(BackendRouter::new(Box::new(api), cli))) + } } }); let mut engine = WorkflowRunEngine::with_interviewer( @@ -674,6 +684,9 @@ pub async fn run_command( interviewer, Arc::clone(&sandbox), ); + if !sandbox_env.is_empty() { + engine.set_env(sandbox_env); + } // Wire up hook runner from run config if let Some(ref cfg) = run_cfg { @@ -1740,6 +1753,7 @@ mod tests { preserve: Some(false), daytona: None, exe: None, + env: None, }), vars: None, hooks: Vec::new(), @@ -1763,6 +1777,7 @@ mod tests { preserve: Some(true), daytona: None, exe: None, + env: None, }), vars: None, hooks: Vec::new(), @@ -1774,6 +1789,7 @@ mod tests { preserve: Some(false), daytona: None, exe: None, + env: None, }), ..RunDefaults::default() }; @@ -1788,6 +1804,7 @@ mod tests { preserve: Some(true), daytona: None, exe: None, + env: None, }), ..RunDefaults::default() }; diff --git a/crates/arc-workflows/src/cli/run_config.rs b/crates/arc-workflows/src/cli/run_config.rs index 7c84a811a..2b95bdf7a 100644 --- a/crates/arc-workflows/src/cli/run_config.rs +++ b/crates/arc-workflows/src/cli/run_config.rs @@ -53,6 +53,7 @@ pub struct SandboxConfig { pub preserve: Option, pub daytona: Option, pub exe: Option, + pub env: Option>, } /// Defaults for workflow runs, loaded from the server config. @@ -135,6 +136,13 @@ impl WorkflowRunConfig { (None, Some(_)) => task.daytona = default.daytona.clone(), _ => {} } + if let Some(ref default_env) = default.env { + let mut merged = default_env.clone(); + if let Some(ref task_env) = task.env { + merged.extend(task_env.clone()); + } + task.env = Some(merged); + } } (None, Some(_)) => self.sandbox = defaults.sandbox.clone(), _ => {} @@ -171,10 +179,47 @@ pub fn load_run_config(path: &Path) -> anyhow::Result { let config_dir = path.parent().unwrap_or(Path::new(".")); resolve_dockerfile(&mut config, config_dir)?; + resolve_sandbox_env(&mut config)?; Ok(config) } +/// Resolve `${env.VARNAME}` references in `[sandbox.env]` values. +/// +/// Only whole-value references are supported (no partial interpolation). +/// Missing host env vars produce a hard error. +fn resolve_sandbox_env(config: &mut WorkflowRunConfig) -> anyhow::Result<()> { + if let Some(env) = config + .sandbox + .as_mut() + .and_then(|s| s.env.as_mut()) + { + resolve_env_refs(env)?; + } + Ok(()) +} + +/// Resolve `${env.VARNAME}` patterns in a map of env vars. +/// +/// If the entire value is `${env.VARNAME}`, it is replaced with the host +/// environment variable. Any other value is left as-is. Missing host +/// variables produce an error. +pub fn resolve_env_refs(env: &mut HashMap) -> anyhow::Result<()> { + for (key, value) in env.iter_mut() { + if let Some(var_name) = value + .strip_prefix("${env.") + .and_then(|s| s.strip_suffix('}')) + { + *value = std::env::var(var_name).with_context(|| { + format!( + "sandbox.env.{key}: host environment variable {var_name:?} is not set" + ) + })?; + } + } + Ok(()) +} + /// If the config contains a `dockerfile = { path = "..." }`, read the file /// and replace it with `DockerfileSource::Inline(contents)`. fn resolve_dockerfile(config: &mut WorkflowRunConfig, config_dir: &Path) -> anyhow::Result<()> { @@ -882,6 +927,7 @@ preserve = true preserve: Some(false), daytona: None, exe: None, + env: None, }), ..RunDefaults::default() }; @@ -908,6 +954,7 @@ provider = "docker" preserve: Some(true), daytona: None, exe: None, + env: None, }), ..RunDefaults::default() }; @@ -937,6 +984,7 @@ provider = "daytona" ..DaytonaConfig::default() }), exe: None, + env: None, }), ..RunDefaults::default() }; @@ -973,6 +1021,7 @@ auto_stop_interval = 60 ..DaytonaConfig::default() }), exe: None, + env: None, }), ..RunDefaults::default() }; @@ -1008,6 +1057,7 @@ env = "from_task" ..DaytonaConfig::default() }), exe: None, + env: None, }), ..RunDefaults::default() }; @@ -1047,6 +1097,7 @@ cpu = 2 ..DaytonaConfig::default() }), exe: None, + env: None, }), ..RunDefaults::default() }; @@ -1085,6 +1136,7 @@ auto_stop_interval = 60 ..DaytonaConfig::default() }), exe: None, + env: None, }), ..RunDefaults::default() }; @@ -1318,6 +1370,7 @@ network = "block" ..DaytonaConfig::default() }), exe: None, + env: None, }), ..RunDefaults::default() }; @@ -1352,6 +1405,7 @@ auto_stop_interval = 60 ..DaytonaConfig::default() }), exe: None, + env: None, }), ..RunDefaults::default() }; @@ -1477,4 +1531,189 @@ exclude_globs = ["**/node_modules/**"] vec!["**/node_modules/**"] ); } + + #[test] + fn parse_toml_with_sandbox_env() { + let toml = r#" +version = 1 +goal = "test" +graph = "w.dot" + +[sandbox.env] +FOO = "bar" +BAZ = "${env.HOME}" +"#; + let config = parse_run_config(toml).unwrap(); + let env = config.sandbox.unwrap().env.unwrap(); + assert_eq!(env["FOO"], "bar"); + // Not yet resolved (parse_run_config doesn't resolve env refs) + assert_eq!(env["BAZ"], "${env.HOME}"); + } + + #[test] + fn parse_toml_without_sandbox_env() { + let toml = r#" +version = 1 +goal = "test" +graph = "w.dot" + +[sandbox] +provider = "daytona" +"#; + let config = parse_run_config(toml).unwrap(); + assert!(config.sandbox.unwrap().env.is_none()); + } + + #[test] + fn resolve_env_refs_literal_passthrough() { + let mut env = HashMap::from([("FOO".into(), "bar".into())]); + resolve_env_refs(&mut env).unwrap(); + assert_eq!(env["FOO"], "bar"); + } + + #[test] + fn resolve_env_refs_host_var() { + std::env::set_var("ARC_TEST_RESOLVE_VAR", "secret123"); + let mut env = HashMap::from([ + ("MY_KEY".into(), "${env.ARC_TEST_RESOLVE_VAR}".into()), + ]); + resolve_env_refs(&mut env).unwrap(); + assert_eq!(env["MY_KEY"], "secret123"); + std::env::remove_var("ARC_TEST_RESOLVE_VAR"); + } + + #[test] + fn resolve_env_refs_missing_var_errors() { + let mut env = HashMap::from([ + ("MY_KEY".into(), "${env.ARC_TEST_NONEXISTENT_VAR_12345}".into()), + ]); + let err = resolve_env_refs(&mut env).unwrap_err(); + assert!( + err.to_string().contains("ARC_TEST_NONEXISTENT_VAR_12345"), + "unexpected error: {err}" + ); + } + + #[test] + fn resolve_env_refs_partial_not_interpolated() { + let mut env = HashMap::from([ + ("MIXED".into(), "prefix_${env.HOME}_suffix".into()), + ]); + // Partial interpolation is not supported — value is left as-is + resolve_env_refs(&mut env).unwrap(); + assert_eq!(env["MIXED"], "prefix_${env.HOME}_suffix"); + } + + #[test] + fn apply_defaults_merges_sandbox_env() { + let mut cfg = parse_run_config( + r#" +version = 1 +goal = "test" +graph = "w.dot" + +[sandbox.env] +TASK_KEY = "task_val" +SHARED = "from_task" +"#, + ) + .unwrap(); + let defaults = RunDefaults { + sandbox: Some(SandboxConfig { + provider: None, + preserve: None, + daytona: None, + exe: None, + env: Some(HashMap::from([ + ("DEFAULT_KEY".into(), "default_val".into()), + ("SHARED".into(), "from_default".into()), + ])), + }), + ..RunDefaults::default() + }; + cfg.apply_defaults(&defaults); + let env = cfg.sandbox.unwrap().env.unwrap(); + assert_eq!(env["DEFAULT_KEY"], "default_val"); + assert_eq!(env["TASK_KEY"], "task_val"); + assert_eq!(env["SHARED"], "from_task"); + } + + #[test] + fn apply_defaults_sandbox_env_from_default_only() { + let mut cfg = parse_run_config( + r#" +version = 1 +goal = "test" +graph = "w.dot" + +[sandbox] +provider = "daytona" +"#, + ) + .unwrap(); + let defaults = RunDefaults { + sandbox: Some(SandboxConfig { + provider: None, + preserve: None, + daytona: None, + exe: None, + env: Some(HashMap::from([("KEY".into(), "val".into())])), + }), + ..RunDefaults::default() + }; + cfg.apply_defaults(&defaults); + let env = cfg.sandbox.unwrap().env.unwrap(); + assert_eq!(env["KEY"], "val"); + } + + #[test] + fn load_run_config_resolves_env_refs() { + std::env::set_var("ARC_TEST_LOAD_ENV", "resolved_value"); + let dir = tempfile::tempdir().unwrap(); + let toml_path = dir.path().join("run.toml"); + std::fs::write( + &toml_path, + r#" +version = 1 +goal = "test" +graph = "w.dot" + +[sandbox.env] +LITERAL = "hello" +FROM_HOST = "${env.ARC_TEST_LOAD_ENV}" +"#, + ) + .unwrap(); + + let config = load_run_config(&toml_path).unwrap(); + let env = config.sandbox.unwrap().env.unwrap(); + assert_eq!(env["LITERAL"], "hello"); + assert_eq!(env["FROM_HOST"], "resolved_value"); + std::env::remove_var("ARC_TEST_LOAD_ENV"); + } + + #[test] + fn load_run_config_missing_env_var_errors() { + let dir = tempfile::tempdir().unwrap(); + let toml_path = dir.path().join("run.toml"); + std::fs::write( + &toml_path, + r#" +version = 1 +goal = "test" +graph = "w.dot" + +[sandbox.env] +MISSING = "${env.ARC_TEST_DEFINITELY_NOT_SET_67890}" +"#, + ) + .unwrap(); + + let err = load_run_config(&toml_path).unwrap_err(); + assert!( + err.to_string() + .contains("ARC_TEST_DEFINITELY_NOT_SET_67890"), + "unexpected error: {err}" + ); + } } diff --git a/crates/arc-workflows/src/engine.rs b/crates/arc-workflows/src/engine.rs index 90e5546dc..05068b478 100644 --- a/crates/arc-workflows/src/engine.rs +++ b/crates/arc-workflows/src/engine.rs @@ -860,6 +860,7 @@ impl WorkflowRunEngine { sandbox, git_state: std::sync::RwLock::new(None), hook_runner: None, + env: HashMap::new(), }, interviewer: None, } @@ -875,6 +876,7 @@ impl WorkflowRunEngine { sandbox: Arc::clone(&services.sandbox), git_state: std::sync::RwLock::new(None), hook_runner: services.hook_runner.clone(), + env: services.env.clone(), }, interviewer: None, } @@ -895,6 +897,7 @@ impl WorkflowRunEngine { sandbox, git_state: std::sync::RwLock::new(None), hook_runner: None, + env: HashMap::new(), }, interviewer: Some(interviewer), } @@ -905,6 +908,11 @@ impl WorkflowRunEngine { self.services.hook_runner = Some(runner); } + /// Set environment variables from `[sandbox.env]` config. + pub fn set_env(&mut self, env: HashMap) { + self.services.env = env; + } + /// Run lifecycle hooks and return the merged decision. /// Returns `Proceed` if no hook runner is configured. async fn run_hooks( diff --git a/crates/arc-workflows/src/handler/agent.rs b/crates/arc-workflows/src/handler/agent.rs index f81c11cb8..f6f28e8f0 100644 --- a/crates/arc-workflows/src/handler/agent.rs +++ b/crates/arc-workflows/src/handler/agent.rs @@ -322,6 +322,7 @@ mod tests { )), git_state: std::sync::RwLock::new(None), hook_runner: None, + env: HashMap::new(), } } @@ -458,6 +459,7 @@ mod tests { )), git_state: std::sync::RwLock::new(None), hook_runner: None, + env: HashMap::new(), }; let outcome = handler @@ -519,6 +521,7 @@ mod tests { )), git_state: std::sync::RwLock::new(None), hook_runner: None, + env: HashMap::new(), }; let outcome = handler diff --git a/crates/arc-workflows/src/handler/command.rs b/crates/arc-workflows/src/handler/command.rs index fbd533569..587234d97 100644 --- a/crates/arc-workflows/src/handler/command.rs +++ b/crates/arc-workflows/src/handler/command.rs @@ -25,7 +25,7 @@ impl Handler for CommandHandler { context: &Context, _graph: &Graph, logs_root: &Path, - _services: &EngineServices, + services: &EngineServices, ) -> Result { let script = node .attrs @@ -69,11 +69,13 @@ impl Handler for CommandHandler { tokio::process::Command::new("python3") .arg("-c") .arg(script) + .envs(&services.env) .output() } else { tokio::process::Command::new("sh") .arg("-c") .arg(script) + .envs(&services.env) .output() }; @@ -183,6 +185,7 @@ mod tests { )), git_state: std::sync::RwLock::new(None), hook_runner: None, + env: std::collections::HashMap::new(), } } diff --git a/crates/arc-workflows/src/handler/conditional.rs b/crates/arc-workflows/src/handler/conditional.rs index ad76540dc..7610651d6 100644 --- a/crates/arc-workflows/src/handler/conditional.rs +++ b/crates/arc-workflows/src/handler/conditional.rs @@ -45,6 +45,7 @@ mod tests { )), git_state: std::sync::RwLock::new(None), hook_runner: None, + env: std::collections::HashMap::new(), } } diff --git a/crates/arc-workflows/src/handler/exit.rs b/crates/arc-workflows/src/handler/exit.rs index 7e571671a..ab36f4365 100644 --- a/crates/arc-workflows/src/handler/exit.rs +++ b/crates/arc-workflows/src/handler/exit.rs @@ -42,6 +42,7 @@ mod tests { )), git_state: std::sync::RwLock::new(None), hook_runner: None, + env: std::collections::HashMap::new(), } } diff --git a/crates/arc-workflows/src/handler/fan_in.rs b/crates/arc-workflows/src/handler/fan_in.rs index 34f5238e4..438800774 100644 --- a/crates/arc-workflows/src/handler/fan_in.rs +++ b/crates/arc-workflows/src/handler/fan_in.rs @@ -307,6 +307,7 @@ mod tests { )), git_state: std::sync::RwLock::new(None), hook_runner: None, + env: std::collections::HashMap::new(), } } diff --git a/crates/arc-workflows/src/handler/human.rs b/crates/arc-workflows/src/handler/human.rs index 9ab1b16d2..011660caf 100644 --- a/crates/arc-workflows/src/handler/human.rs +++ b/crates/arc-workflows/src/handler/human.rs @@ -281,6 +281,7 @@ mod tests { )), git_state: std::sync::RwLock::new(None), hook_runner: None, + env: std::collections::HashMap::new(), } } diff --git a/crates/arc-workflows/src/handler/manager_loop.rs b/crates/arc-workflows/src/handler/manager_loop.rs index f87f0a9e0..3f8245e55 100644 --- a/crates/arc-workflows/src/handler/manager_loop.rs +++ b/crates/arc-workflows/src/handler/manager_loop.rs @@ -264,6 +264,7 @@ mod tests { )), git_state: std::sync::RwLock::new(None), hook_runner: None, + env: HashMap::new(), } } @@ -396,6 +397,7 @@ mod tests { )), git_state: std::sync::RwLock::new(None), hook_runner: None, + env: HashMap::new(), }; let handler = SubWorkflowHandler; @@ -504,6 +506,7 @@ mod tests { )), git_state: std::sync::RwLock::new(None), hook_runner: None, + env: HashMap::new(), }; let handler = SubWorkflowHandler; @@ -564,6 +567,7 @@ mod tests { )), git_state: std::sync::RwLock::new(None), hook_runner: None, + env: HashMap::new(), }; let handler = SubWorkflowHandler; @@ -736,6 +740,7 @@ mod tests { )), git_state: std::sync::RwLock::new(None), hook_runner: None, + env: HashMap::new(), }; let handler = SubWorkflowHandler; @@ -820,6 +825,7 @@ mod tests { )), git_state: std::sync::RwLock::new(None), hook_runner: None, + env: HashMap::new(), }; let handler = SubWorkflowHandler; diff --git a/crates/arc-workflows/src/handler/mod.rs b/crates/arc-workflows/src/handler/mod.rs index 28d4eb098..bde39aa91 100644 --- a/crates/arc-workflows/src/handler/mod.rs +++ b/crates/arc-workflows/src/handler/mod.rs @@ -36,6 +36,8 @@ pub struct EngineServices { pub(crate) git_state: std::sync::RwLock>>, /// Hook runner for user-defined lifecycle hooks. pub hook_runner: Option>, + /// Environment variables from `[sandbox.env]` config, injected into command nodes. + pub env: HashMap, } impl EngineServices { diff --git a/crates/arc-workflows/src/handler/parallel.rs b/crates/arc-workflows/src/handler/parallel.rs index 6e6cb1a76..9a90d0fea 100644 --- a/crates/arc-workflows/src/handler/parallel.rs +++ b/crates/arc-workflows/src/handler/parallel.rs @@ -394,6 +394,7 @@ impl Handler for ParallelHandler { let registry = Arc::clone(&services.registry); let emitter = Arc::clone(&services.emitter); let hook_runner = services.hook_runner.clone(); + let env = services.env.clone(); let graph = graph.clone(); let logs_root = logs_root.to_path_buf(); let sem = Arc::clone(&semaphore); @@ -438,6 +439,7 @@ impl Handler for ParallelHandler { sandbox: Arc::clone(&setup.sandbox), git_state: std::sync::RwLock::new(None), hook_runner: hook_runner.clone(), + env: env.clone(), }; let handler = registry.resolve(target_node); let outcome = handler @@ -771,6 +773,7 @@ mod tests { )), git_state: std::sync::RwLock::new(None), hook_runner: None, + env: std::collections::HashMap::new(), } } diff --git a/crates/arc-workflows/src/handler/prompt.rs b/crates/arc-workflows/src/handler/prompt.rs index b9dd5326d..bb8e39d73 100644 --- a/crates/arc-workflows/src/handler/prompt.rs +++ b/crates/arc-workflows/src/handler/prompt.rs @@ -134,6 +134,7 @@ mod tests { )), git_state: std::sync::RwLock::new(None), hook_runner: None, + env: std::collections::HashMap::new(), } } diff --git a/crates/arc-workflows/src/handler/start.rs b/crates/arc-workflows/src/handler/start.rs index 2dffb6454..fc17235e5 100644 --- a/crates/arc-workflows/src/handler/start.rs +++ b/crates/arc-workflows/src/handler/start.rs @@ -41,6 +41,7 @@ mod tests { )), git_state: std::sync::RwLock::new(None), hook_runner: None, + env: std::collections::HashMap::new(), } } diff --git a/crates/arc-workflows/src/handler/wait.rs b/crates/arc-workflows/src/handler/wait.rs index b4d878c37..f5bdf0cfa 100644 --- a/crates/arc-workflows/src/handler/wait.rs +++ b/crates/arc-workflows/src/handler/wait.rs @@ -56,6 +56,7 @@ mod tests { )), git_state: std::sync::RwLock::new(None), hook_runner: None, + env: std::collections::HashMap::new(), } }