working out the worker/execution interface

This commit is contained in:
2026-02-08 12:55:33 -06:00
parent c62f41669d
commit a74e13fa0b
108 changed files with 21162 additions and 674 deletions

View File

@@ -7,6 +7,7 @@ pub mod dependency;
pub mod local;
pub mod log_writer;
pub mod native;
pub mod parameter_passing;
pub mod python;
pub mod python_venv;
pub mod shell;
@@ -18,6 +19,7 @@ pub use python::PythonRuntime;
pub use shell::ShellRuntime;
use async_trait::async_trait;
use attune_common::models::{ParameterDelivery, ParameterFormat};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
@@ -29,6 +31,7 @@ pub use dependency::{
DependencySpec, EnvironmentInfo,
};
pub use log_writer::{BoundedLogResult, BoundedLogWriter};
pub use parameter_passing::{ParameterDeliveryConfig, PreparedParameters};
pub use python_venv::PythonVenvManager;
/// Runtime execution result
@@ -108,6 +111,14 @@ pub struct ExecutionContext {
/// Maximum stderr size in bytes (for log truncation)
#[serde(default = "default_max_log_bytes")]
pub max_stderr_bytes: usize,
/// How parameters should be delivered to the action
#[serde(default)]
pub parameter_delivery: ParameterDelivery,
/// Format for parameter serialization
#[serde(default)]
pub parameter_format: ParameterFormat,
}
fn default_max_log_bytes() -> usize {
@@ -133,6 +144,8 @@ impl ExecutionContext {
runtime_name: None,
max_stdout_bytes: 10 * 1024 * 1024,
max_stderr_bytes: 10 * 1024 * 1024,
parameter_delivery: ParameterDelivery::default(),
parameter_format: ParameterFormat::default(),
}
}
}

View File

@@ -4,14 +4,16 @@
//! This runtime is used for Rust binaries and other compiled executables.
use super::{
parameter_passing::{self, ParameterDeliveryConfig},
BoundedLogWriter, ExecutionContext, ExecutionResult, Runtime, RuntimeError, RuntimeResult,
};
use async_trait::async_trait;
use std::path::PathBuf;
use std::process::Stdio;
use std::time::Instant;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::process::Command;
use tokio::time::{timeout, Duration};
use tokio::time::Duration;
use tracing::{debug, info, warn};
/// Native runtime for executing compiled binaries
@@ -35,11 +37,11 @@ impl NativeRuntime {
/// Execute a native binary with parameters and environment variables
async fn execute_binary(
&self,
binary_path: std::path::PathBuf,
parameters: &std::collections::HashMap<String, serde_json::Value>,
binary_path: PathBuf,
secrets: &std::collections::HashMap<String, String>,
env: &std::collections::HashMap<String, String>,
exec_timeout: Option<u64>,
parameters_stdin: Option<&str>,
timeout: Option<u64>,
max_stdout_bytes: usize,
max_stderr_bytes: usize,
) -> RuntimeResult<ExecutionResult> {
@@ -76,22 +78,11 @@ impl NativeRuntime {
cmd.current_dir(work_dir);
}
// Add environment variables
// Add environment variables (including parameter delivery metadata)
for (key, value) in env {
cmd.env(key, value);
}
// Add parameters as environment variables with ATTUNE_ACTION_ prefix
for (key, value) in parameters {
let value_str = match value {
serde_json::Value::String(s) => s.clone(),
serde_json::Value::Number(n) => n.to_string(),
serde_json::Value::Bool(b) => b.to_string(),
_ => serde_json::to_string(value)?,
};
cmd.env(format!("ATTUNE_ACTION_{}", key.to_uppercase()), value_str);
}
// Configure stdio
cmd.stdin(Stdio::piped())
.stdout(Stdio::piped())
@@ -102,29 +93,42 @@ impl NativeRuntime {
.spawn()
.map_err(|e| RuntimeError::ExecutionFailed(format!("Failed to spawn binary: {}", e)))?;
// Write secrets to stdin - if this fails, the process has already started
// so we should continue and capture whatever output we can
let stdin_write_error = if !secrets.is_empty() {
if let Some(mut stdin) = child.stdin.take() {
// Write to stdin - parameters (if using stdin delivery) and/or secrets
// If this fails, the process has already started, so we continue and capture output
let stdin_write_error = if let Some(mut stdin) = child.stdin.take() {
let mut error = None;
// Write parameters first if using stdin delivery
if let Some(params_data) = parameters_stdin {
if let Err(e) = stdin.write_all(params_data.as_bytes()).await {
error = Some(format!("Failed to write parameters to stdin: {}", e));
} else if let Err(e) = stdin.write_all(b"\n---ATTUNE_PARAMS_END---\n").await {
error = Some(format!("Failed to write parameter delimiter: {}", e));
}
}
// Write secrets as JSON (always, for backward compatibility)
if error.is_none() && !secrets.is_empty() {
match serde_json::to_string(secrets) {
Ok(secrets_json) => {
if let Err(e) = stdin.write_all(secrets_json.as_bytes()).await {
Some(format!("Failed to write secrets to stdin: {}", e))
} else if let Err(e) = stdin.shutdown().await {
Some(format!("Failed to close stdin: {}", e))
} else {
None
error = Some(format!("Failed to write secrets to stdin: {}", e));
} else if let Err(e) = stdin.write_all(b"\n").await {
error = Some(format!("Failed to write newline to stdin: {}", e));
}
}
Err(e) => Some(format!("Failed to serialize secrets: {}", e)),
Err(e) => error = Some(format!("Failed to serialize secrets: {}", e)),
}
} else {
None
}
// Close stdin
if let Err(e) = stdin.shutdown().await {
if error.is_none() {
error = Some(format!("Failed to close stdin: {}", e));
}
}
error
} else {
if let Some(stdin) = child.stdin.take() {
drop(stdin); // Close stdin if no secrets
}
None
};
@@ -184,8 +188,8 @@ impl NativeRuntime {
let (stdout_writer, stderr_writer) = tokio::join!(stdout_task, stderr_task);
// Wait for process with timeout
let wait_result = if let Some(timeout_secs) = exec_timeout {
match timeout(Duration::from_secs(timeout_secs), child.wait()).await {
let wait_result = if let Some(timeout_secs) = timeout {
match tokio::time::timeout(Duration::from_secs(timeout_secs), child.wait()).await {
Ok(result) => result,
Err(_) => {
warn!(
@@ -317,10 +321,26 @@ impl Runtime for NativeRuntime {
async fn execute(&self, context: ExecutionContext) -> RuntimeResult<ExecutionResult> {
info!(
"Executing native action: {} (execution_id: {})",
context.action_ref, context.execution_id
"Executing native action: {} (execution_id: {}) with parameter delivery: {:?}, format: {:?}",
context.action_ref, context.execution_id, context.parameter_delivery, context.parameter_format
);
// Prepare environment and parameters according to delivery method
let mut env = context.env.clone();
let config = ParameterDeliveryConfig {
delivery: context.parameter_delivery,
format: context.parameter_format,
};
let prepared_params = parameter_passing::prepare_parameters(
&context.parameters,
&mut env,
config,
)?;
// Get stdin content if parameters are delivered via stdin
let parameters_stdin = prepared_params.stdin_content();
// Get the binary path
let binary_path = context.code_path.ok_or_else(|| {
RuntimeError::InvalidAction("Native runtime requires code_path to be set".to_string())
@@ -328,9 +348,9 @@ impl Runtime for NativeRuntime {
self.execute_binary(
binary_path,
&context.parameters,
&context.secrets,
&context.env,
&env,
parameters_stdin,
context.timeout,
context.max_stdout_bytes,
context.max_stderr_bytes,

View File

@@ -0,0 +1,320 @@
//! Parameter Passing Module
//!
//! Provides utilities for formatting and delivering action parameters
//! in different formats (dotenv, JSON, YAML) via different methods
//! (environment variables, stdin, temporary files).
use attune_common::models::{ParameterDelivery, ParameterFormat};
use serde_json::Value as JsonValue;
use std::collections::HashMap;
use std::io::Write;
use std::path::PathBuf;
use tempfile::NamedTempFile;
use tracing::debug;
use super::RuntimeError;
/// Format parameters according to the specified format
pub fn format_parameters(
parameters: &HashMap<String, JsonValue>,
format: ParameterFormat,
) -> Result<String, RuntimeError> {
match format {
ParameterFormat::Dotenv => format_dotenv(parameters),
ParameterFormat::Json => format_json(parameters),
ParameterFormat::Yaml => format_yaml(parameters),
}
}
/// Format parameters as dotenv (key='value')
/// Note: Parameter names are preserved as-is (case-sensitive)
fn format_dotenv(parameters: &HashMap<String, JsonValue>) -> Result<String, RuntimeError> {
let mut lines = Vec::new();
for (key, value) in parameters {
let value_str = value_to_string(value);
// Escape single quotes in value
let escaped_value = value_str.replace('\'', "'\\''");
lines.push(format!("{}='{}'", key, escaped_value));
}
Ok(lines.join("\n"))
}
/// Format parameters as JSON
fn format_json(parameters: &HashMap<String, JsonValue>) -> Result<String, RuntimeError> {
serde_json::to_string_pretty(parameters).map_err(|e| {
RuntimeError::ExecutionFailed(format!(
"Failed to serialize parameters to JSON: {}",
e
))
})
}
/// Format parameters as YAML
fn format_yaml(parameters: &HashMap<String, JsonValue>) -> Result<String, RuntimeError> {
serde_yaml_ng::to_string(parameters).map_err(|e| {
RuntimeError::ExecutionFailed(format!(
"Failed to serialize parameters to YAML: {}",
e
))
})
}
/// Convert JSON value to string representation
fn value_to_string(value: &JsonValue) -> String {
match value {
JsonValue::String(s) => s.clone(),
JsonValue::Number(n) => n.to_string(),
JsonValue::Bool(b) => b.to_string(),
JsonValue::Null => String::new(),
_ => serde_json::to_string(value).unwrap_or_else(|_| String::new()),
}
}
/// Create a temporary file with parameters
pub fn create_parameter_file(
parameters: &HashMap<String, JsonValue>,
format: ParameterFormat,
) -> Result<NamedTempFile, RuntimeError> {
let formatted = format_parameters(parameters, format)?;
let mut temp_file = NamedTempFile::new()
.map_err(|e| RuntimeError::IoError(e))?;
// Set restrictive permissions (owner read-only)
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = temp_file.as_file().metadata()
.map_err(|e| RuntimeError::IoError(e))?
.permissions();
perms.set_mode(0o400); // Read-only for owner
temp_file.as_file().set_permissions(perms)
.map_err(|e| RuntimeError::IoError(e))?;
}
temp_file
.write_all(formatted.as_bytes())
.map_err(|e| RuntimeError::IoError(e))?;
temp_file
.flush()
.map_err(|e| RuntimeError::IoError(e))?;
debug!(
"Created parameter file at {:?} with format {:?}",
temp_file.path(),
format
);
Ok(temp_file)
}
/// Parameter delivery configuration
#[derive(Debug, Clone)]
pub struct ParameterDeliveryConfig {
pub delivery: ParameterDelivery,
pub format: ParameterFormat,
}
/// Prepared parameters ready for execution
#[derive(Debug)]
pub enum PreparedParameters {
/// Parameters are in environment variables
Environment,
/// Parameters will be passed via stdin
Stdin(String),
/// Parameters are in a temporary file
File {
path: PathBuf,
#[allow(dead_code)]
temp_file: NamedTempFile,
},
}
impl PreparedParameters {
/// Get the file path if this is file-based delivery
pub fn file_path(&self) -> Option<&PathBuf> {
match self {
PreparedParameters::File { path, .. } => Some(path),
_ => None,
}
}
/// Get the stdin content if this is stdin-based delivery
pub fn stdin_content(&self) -> Option<&str> {
match self {
PreparedParameters::Stdin(content) => Some(content),
_ => None,
}
}
}
/// Prepare parameters for delivery according to the specified method and format
pub fn prepare_parameters(
parameters: &HashMap<String, JsonValue>,
env: &mut HashMap<String, String>,
config: ParameterDeliveryConfig,
) -> Result<PreparedParameters, RuntimeError> {
match config.delivery {
ParameterDelivery::Stdin => {
// Format parameters for stdin
let formatted = format_parameters(parameters, config.format)?;
// Add environment variables to indicate delivery method
env.insert(
"ATTUNE_PARAMETER_DELIVERY".to_string(),
"stdin".to_string(),
);
env.insert(
"ATTUNE_PARAMETER_FORMAT".to_string(),
config.format.to_string(),
);
Ok(PreparedParameters::Stdin(formatted))
}
ParameterDelivery::File => {
// Create temporary file with parameters
let temp_file = create_parameter_file(parameters, config.format)?;
let path = temp_file.path().to_path_buf();
// Add environment variables to indicate delivery method and file location
env.insert(
"ATTUNE_PARAMETER_DELIVERY".to_string(),
"file".to_string(),
);
env.insert(
"ATTUNE_PARAMETER_FORMAT".to_string(),
config.format.to_string(),
);
env.insert(
"ATTUNE_PARAMETER_FILE".to_string(),
path.to_string_lossy().to_string(),
);
Ok(PreparedParameters::File { path, temp_file })
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_format_dotenv() {
let mut params = HashMap::new();
params.insert("message".to_string(), json!("Hello, World!"));
params.insert("count".to_string(), json!(42));
params.insert("enabled".to_string(), json!(true));
let result = format_dotenv(&params).unwrap();
assert!(result.contains("message='Hello, World!'"));
assert!(result.contains("count='42'"));
assert!(result.contains("enabled='true'"));
}
#[test]
fn test_format_dotenv_escaping() {
let mut params = HashMap::new();
params.insert("message".to_string(), json!("It's a test"));
let result = format_dotenv(&params).unwrap();
assert!(result.contains("message='It'\\''s a test'"));
}
#[test]
fn test_format_json() {
let mut params = HashMap::new();
params.insert("message".to_string(), json!("Hello"));
params.insert("count".to_string(), json!(42));
let result = format_json(&params).unwrap();
let parsed: HashMap<String, JsonValue> = serde_json::from_str(&result).unwrap();
assert_eq!(parsed.get("message"), Some(&json!("Hello")));
assert_eq!(parsed.get("count"), Some(&json!(42)));
}
#[test]
fn test_format_yaml() {
let mut params = HashMap::new();
params.insert("message".to_string(), json!("Hello"));
params.insert("count".to_string(), json!(42));
let result = format_yaml(&params).unwrap();
assert!(result.contains("message:"));
assert!(result.contains("Hello"));
assert!(result.contains("count:"));
assert!(result.contains("42"));
}
#[test]
#[test]
fn test_create_parameter_file() {
let mut params = HashMap::new();
params.insert("key".to_string(), json!("value"));
let temp_file = create_parameter_file(&params, ParameterFormat::Json).unwrap();
let content = std::fs::read_to_string(temp_file.path()).unwrap();
assert!(content.contains("key"));
assert!(content.contains("value"));
}
#[test]
fn test_prepare_parameters_stdin() {
let mut params = HashMap::new();
params.insert("test".to_string(), json!("value"));
let mut env = HashMap::new();
let config = ParameterDeliveryConfig {
delivery: ParameterDelivery::Stdin,
format: ParameterFormat::Json,
};
let result = prepare_parameters(&params, &mut env, config).unwrap();
assert!(matches!(result, PreparedParameters::Stdin(_)));
assert_eq!(
env.get("ATTUNE_PARAMETER_DELIVERY"),
Some(&"stdin".to_string())
);
assert_eq!(
env.get("ATTUNE_PARAMETER_FORMAT"),
Some(&"json".to_string())
);
}
#[test]
fn test_prepare_parameters_file() {
let mut params = HashMap::new();
params.insert("test".to_string(), json!("value"));
let mut env = HashMap::new();
let config = ParameterDeliveryConfig {
delivery: ParameterDelivery::File,
format: ParameterFormat::Yaml,
};
let result = prepare_parameters(&params, &mut env, config).unwrap();
assert!(matches!(result, PreparedParameters::File { .. }));
assert_eq!(
env.get("ATTUNE_PARAMETER_DELIVERY"),
Some(&"file".to_string())
);
assert_eq!(
env.get("ATTUNE_PARAMETER_FORMAT"),
Some(&"yaml".to_string())
);
assert!(env.contains_key("ATTUNE_PARAMETER_FILE"));
}
}

View File

@@ -3,6 +3,7 @@
//! Executes shell scripts and commands using subprocess execution.
use super::{
parameter_passing::{self, ParameterDeliveryConfig},
BoundedLogWriter, ExecutionContext, ExecutionResult, Runtime, RuntimeError, RuntimeResult,
};
use async_trait::async_trait;
@@ -53,6 +54,7 @@ impl ShellRuntime {
&self,
mut cmd: Command,
secrets: &std::collections::HashMap<String, String>,
parameters_stdin: Option<&str>,
timeout_secs: Option<u64>,
max_stdout_bytes: usize,
max_stderr_bytes: usize,
@@ -66,22 +68,36 @@ impl ShellRuntime {
.stderr(Stdio::piped())
.spawn()?;
// Write secrets to stdin - if this fails, the process has already started
// so we should continue and capture whatever output we can
// Write to stdin - parameters (if using stdin delivery) and/or secrets
// If this fails, the process has already started, so we continue and capture output
let stdin_write_error = if let Some(mut stdin) = child.stdin.take() {
match serde_json::to_string(secrets) {
Ok(secrets_json) => {
if let Err(e) = stdin.write_all(secrets_json.as_bytes()).await {
Some(format!("Failed to write secrets to stdin: {}", e))
} else if let Err(e) = stdin.write_all(b"\n").await {
Some(format!("Failed to write newline to stdin: {}", e))
} else {
drop(stdin);
None
}
let mut error = None;
// Write parameters first if using stdin delivery
if let Some(params_data) = parameters_stdin {
if let Err(e) = stdin.write_all(params_data.as_bytes()).await {
error = Some(format!("Failed to write parameters to stdin: {}", e));
} else if let Err(e) = stdin.write_all(b"\n---ATTUNE_PARAMS_END---\n").await {
error = Some(format!("Failed to write parameter delimiter: {}", e));
}
Err(e) => Some(format!("Failed to serialize secrets: {}", e)),
}
// Write secrets as JSON (always, for backward compatibility)
if error.is_none() && !secrets.is_empty() {
match serde_json::to_string(secrets) {
Ok(secrets_json) => {
if let Err(e) = stdin.write_all(secrets_json.as_bytes()).await {
error = Some(format!("Failed to write secrets to stdin: {}", e));
} else if let Err(e) = stdin.write_all(b"\n").await {
error = Some(format!("Failed to write newline to stdin: {}", e));
}
}
Err(e) => error = Some(format!("Failed to serialize secrets: {}", e)),
}
}
drop(stdin);
error
} else {
None
};
@@ -315,9 +331,10 @@ impl ShellRuntime {
/// Execute shell script directly
async fn execute_shell_code(
&self,
script: String,
code: String,
secrets: &std::collections::HashMap<String, String>,
env: &std::collections::HashMap<String, String>,
parameters_stdin: Option<&str>,
timeout_secs: Option<u64>,
max_stdout_bytes: usize,
max_stderr_bytes: usize,
@@ -329,7 +346,7 @@ impl ShellRuntime {
// Build command
let mut cmd = Command::new(&self.shell_path);
cmd.arg("-c").arg(&script);
cmd.arg("-c").arg(&code);
// Add environment variables
for (key, value) in env {
@@ -339,6 +356,7 @@ impl ShellRuntime {
self.execute_with_streaming(
cmd,
secrets,
parameters_stdin,
timeout_secs,
max_stdout_bytes,
max_stderr_bytes,
@@ -349,22 +367,23 @@ impl ShellRuntime {
/// Execute shell script from file
async fn execute_shell_file(
&self,
code_path: PathBuf,
script_path: PathBuf,
secrets: &std::collections::HashMap<String, String>,
env: &std::collections::HashMap<String, String>,
parameters_stdin: Option<&str>,
timeout_secs: Option<u64>,
max_stdout_bytes: usize,
max_stderr_bytes: usize,
) -> RuntimeResult<ExecutionResult> {
debug!(
"Executing shell file: {:?} with {} secrets",
code_path,
script_path,
secrets.len()
);
// Build command
let mut cmd = Command::new(&self.shell_path);
cmd.arg(&code_path);
cmd.arg(&script_path);
// Add environment variables
for (key, value) in env {
@@ -374,6 +393,7 @@ impl ShellRuntime {
self.execute_with_streaming(
cmd,
secrets,
parameters_stdin,
timeout_secs,
max_stdout_bytes,
max_stderr_bytes,
@@ -412,29 +432,49 @@ impl Runtime for ShellRuntime {
async fn execute(&self, context: ExecutionContext) -> RuntimeResult<ExecutionResult> {
info!(
"Executing shell action: {} (execution_id: {})",
context.action_ref, context.execution_id
"Executing shell action: {} (execution_id: {}) with parameter delivery: {:?}, format: {:?}",
context.action_ref, context.execution_id, context.parameter_delivery, context.parameter_format
);
info!(
"Action parameters (count: {}): {:?}",
context.parameters.len(),
context.parameters
);
// Prepare environment and parameters according to delivery method
let mut env = context.env.clone();
let config = ParameterDeliveryConfig {
delivery: context.parameter_delivery,
format: context.parameter_format,
};
let prepared_params = parameter_passing::prepare_parameters(
&context.parameters,
&mut env,
config,
)?;
// Get stdin content if parameters are delivered via stdin
let parameters_stdin = prepared_params.stdin_content();
if let Some(stdin_data) = parameters_stdin {
info!(
"Parameters to be sent via stdin (length: {} bytes):\n{}",
stdin_data.len(),
stdin_data
);
} else {
info!("No parameters will be sent via stdin");
}
// If code_path is provided, execute the file directly
if let Some(code_path) = &context.code_path {
// Merge parameters into environment variables with ATTUNE_ACTION_ prefix
let mut env = context.env.clone();
for (key, value) in &context.parameters {
let value_str = match value {
serde_json::Value::String(s) => s.clone(),
serde_json::Value::Number(n) => n.to_string(),
serde_json::Value::Bool(b) => b.to_string(),
_ => serde_json::to_string(value)?,
};
env.insert(format!("ATTUNE_ACTION_{}", key.to_uppercase()), value_str);
}
return self
.execute_shell_file(
code_path.clone(),
&context.secrets,
&env,
parameters_stdin,
context.timeout,
context.max_stdout_bytes,
context.max_stderr_bytes,
@@ -447,7 +487,8 @@ impl Runtime for ShellRuntime {
self.execute_shell_code(
script,
&context.secrets,
&context.env,
&env,
parameters_stdin,
context.timeout,
context.max_stdout_bytes,
context.max_stderr_bytes,
@@ -534,6 +575,8 @@ mod tests {
runtime_name: Some("shell".to_string()),
max_stdout_bytes: 10 * 1024 * 1024,
max_stderr_bytes: 10 * 1024 * 1024,
parameter_delivery: attune_common::models::ParameterDelivery::default(),
parameter_format: attune_common::models::ParameterFormat::default(),
};
let result = runtime.execute(context).await.unwrap();
@@ -564,6 +607,8 @@ mod tests {
runtime_name: Some("shell".to_string()),
max_stdout_bytes: 10 * 1024 * 1024,
max_stderr_bytes: 10 * 1024 * 1024,
parameter_delivery: attune_common::models::ParameterDelivery::default(),
parameter_format: attune_common::models::ParameterFormat::default(),
};
let result = runtime.execute(context).await.unwrap();
@@ -589,6 +634,8 @@ mod tests {
runtime_name: Some("shell".to_string()),
max_stdout_bytes: 10 * 1024 * 1024,
max_stderr_bytes: 10 * 1024 * 1024,
parameter_delivery: attune_common::models::ParameterDelivery::default(),
parameter_format: attune_common::models::ParameterFormat::default(),
};
let result = runtime.execute(context).await.unwrap();
@@ -616,6 +663,8 @@ mod tests {
runtime_name: Some("shell".to_string()),
max_stdout_bytes: 10 * 1024 * 1024,
max_stderr_bytes: 10 * 1024 * 1024,
parameter_delivery: attune_common::models::ParameterDelivery::default(),
parameter_format: attune_common::models::ParameterFormat::default(),
};
let result = runtime.execute(context).await.unwrap();
@@ -658,6 +707,8 @@ echo "missing=$missing"
runtime_name: Some("shell".to_string()),
max_stdout_bytes: 10 * 1024 * 1024,
max_stderr_bytes: 10 * 1024 * 1024,
parameter_delivery: attune_common::models::ParameterDelivery::default(),
parameter_format: attune_common::models::ParameterFormat::default(),
};
let result = runtime.execute(context).await.unwrap();