agent workers

This commit is contained in:
2026-03-21 10:05:02 -05:00
parent 4d5a3b1bf5
commit d4c6240485
10 changed files with 280 additions and 152 deletions

1
Cargo.lock generated
View File

@@ -490,6 +490,7 @@ dependencies = [
"sha1", "sha1",
"sha2", "sha2",
"sqlx", "sqlx",
"subtle",
"tar", "tar",
"tempfile", "tempfile",
"thiserror 2.0.18", "thiserror 2.0.18",

View File

@@ -89,7 +89,9 @@ spec:
- name: ATTUNE_WORKER_TYPE - name: ATTUNE_WORKER_TYPE
value: container value: container
- name: ATTUNE_WORKER_NAME - name: ATTUNE_WORKER_NAME
value: agent-worker-{{ .name }}-01 valueFrom:
fieldRef:
fieldPath: metadata.name
- name: ATTUNE_API_URL - name: ATTUNE_API_URL
value: http://{{ include "attune.apiServiceName" $ }}:{{ $.Values.api.service.port }} value: http://{{ include "attune.apiServiceName" $ }}:{{ $.Values.api.service.port }}
- name: RUST_LOG - name: RUST_LOG

View File

@@ -89,6 +89,7 @@ hmac = "0.12"
sha1 = "0.10" sha1 = "0.10"
sha2 = { workspace = true } sha2 = { workspace = true }
hex = "0.4" hex = "0.4"
subtle = "2.6"
# OpenAPI/Swagger # OpenAPI/Swagger
utoipa = { workspace = true, features = ["axum_extras"] } utoipa = { workspace = true, features = ["axum_extras"] }

View File

@@ -14,6 +14,7 @@ use axum::{
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::sync::Arc; use std::sync::Arc;
use subtle::ConstantTimeEq;
use tokio::fs; use tokio::fs;
use tokio_util::io::ReaderStream; use tokio_util::io::ReaderStream;
use utoipa::{IntoParams, ToSchema}; use utoipa::{IntoParams, ToSchema};
@@ -83,7 +84,18 @@ fn validate_token(
let expected_token = match expected_token { let expected_token = match expected_token {
Some(t) => t, Some(t) => t,
None => return Ok(()), // No token configured, allow access None => {
use std::sync::Once;
static WARN_ONCE: Once = Once::new();
WARN_ONCE.call_once(|| {
tracing::warn!(
"Agent binary download endpoint has no bootstrap_token configured. \
Anyone with network access to the API can download the agent binary. \
Set agent.bootstrap_token in config to restrict access."
);
});
return Ok(());
}
}; };
// Check X-Agent-Token header first, then query param // Check X-Agent-Token header first, then query param
@@ -94,7 +106,7 @@ fn validate_token(
.or_else(|| query_token.clone()); .or_else(|| query_token.clone());
match provided_token { match provided_token {
Some(ref t) if t == expected_token => Ok(()), Some(ref t) if bool::from(t.as_bytes().ct_eq(expected_token.as_bytes())) => Ok(()),
Some(_) => Err(( Some(_) => Err((
StatusCode::UNAUTHORIZED, StatusCode::UNAUTHORIZED,
Json(serde_json::json!({ Json(serde_json::json!({
@@ -152,15 +164,19 @@ pub async fn download_agent_binary(
let binary_dir = std::path::Path::new(&agent_config.binary_dir); let binary_dir = std::path::Path::new(&agent_config.binary_dir);
// Try arch-specific binary first, then fall back to generic name // Try arch-specific binary first, then fall back to generic name.
// IMPORTANT: The generic `attune-agent` binary is only safe to serve for
// x86_64 requests, because the current build pipeline produces an
// x86_64-unknown-linux-musl binary. Serving it for aarch64/arm64 would
// give the caller an incompatible executable (exec format error).
let arch_specific = binary_dir.join(format!("attune-agent-{}", arch)); let arch_specific = binary_dir.join(format!("attune-agent-{}", arch));
let generic = binary_dir.join("attune-agent"); let generic = binary_dir.join("attune-agent");
let binary_path = if arch_specific.exists() { let binary_path = if arch_specific.exists() {
arch_specific arch_specific
} else if generic.exists() { } else if arch == "x86_64" && generic.exists() {
tracing::debug!( tracing::debug!(
"Arch-specific binary not found at {:?}, falling back to {:?}", "Arch-specific binary not found at {:?}, falling back to generic {:?} (safe for x86_64)",
arch_specific, arch_specific,
generic generic
); );
@@ -269,12 +285,14 @@ pub async fn agent_info(
let arch_specific = binary_dir.join(format!("attune-agent-{}", arch)); let arch_specific = binary_dir.join(format!("attune-agent-{}", arch));
let generic = binary_dir.join("attune-agent"); let generic = binary_dir.join("attune-agent");
// Only fall back to the generic binary for x86_64, since the build
// pipeline currently produces x86_64-only generic binaries.
let (available, size_bytes) = if arch_specific.exists() { let (available, size_bytes) = if arch_specific.exists() {
match fs::metadata(&arch_specific).await { match fs::metadata(&arch_specific).await {
Ok(m) => (true, m.len()), Ok(m) => (true, m.len()),
Err(_) => (false, 0), Err(_) => (false, 0),
} }
} else if generic.exists() { } else if *arch == "x86_64" && generic.exists() {
match fs::metadata(&generic).await { match fs::metadata(&generic).await {
Ok(m) => (true, m.len()), Ok(m) => (true, m.len()),
Err(_) => (false, 0), Err(_) => (false, 0),

View File

@@ -34,21 +34,22 @@ use tracing::{debug, info, warn};
/// use attune_common::runtime_detection::normalize_runtime_name; /// use attune_common::runtime_detection::normalize_runtime_name;
/// assert_eq!(normalize_runtime_name("node.js"), "node"); /// assert_eq!(normalize_runtime_name("node.js"), "node");
/// assert_eq!(normalize_runtime_name("nodejs"), "node"); /// assert_eq!(normalize_runtime_name("nodejs"), "node");
/// assert_eq!(normalize_runtime_name("python3"), "python"); /// assert_eq!(normalize_runtime_name("Python3"), "python");
/// assert_eq!(normalize_runtime_name("shell"), "shell"); /// assert_eq!(normalize_runtime_name("Shell"), "shell");
/// ``` /// ```
pub fn normalize_runtime_name(name: &str) -> &str { pub fn normalize_runtime_name(name: &str) -> String {
match name { let lower = name.to_ascii_lowercase();
"node" | "nodejs" | "node.js" => "node", match lower.as_str() {
"python" | "python3" => "python", "node" | "nodejs" | "node.js" => "node".to_string(),
"bash" | "sh" | "shell" => "shell", "python" | "python3" => "python".to_string(),
"native" | "builtin" | "standalone" => "native", "bash" | "sh" | "shell" => "shell".to_string(),
"ruby" | "rb" => "ruby", "native" | "builtin" | "standalone" => "native".to_string(),
"go" | "golang" => "go", "ruby" | "rb" => "ruby".to_string(),
"java" | "jdk" | "openjdk" => "java", "go" | "golang" => "go".to_string(),
"perl" | "perl5" => "perl", "java" | "jdk" | "openjdk" => "java".to_string(),
"r" | "rscript" => "r", "perl" | "perl5" => "perl".to_string(),
other => other, "r" | "rscript" => "r".to_string(),
_ => lower,
} }
} }
@@ -57,9 +58,7 @@ pub fn normalize_runtime_name(name: &str) -> &str {
/// Both sides are lowercased and then normalized before comparison so that, /// Both sides are lowercased and then normalized before comparison so that,
/// e.g., a filter value of `"node"` matches a database runtime name `"Node.js"`. /// e.g., a filter value of `"node"` matches a database runtime name `"Node.js"`.
pub fn runtime_matches_filter(rt_name: &str, filter_entry: &str) -> bool { pub fn runtime_matches_filter(rt_name: &str, filter_entry: &str) -> bool {
let rt_lower = rt_name.to_ascii_lowercase(); normalize_runtime_name(rt_name) == normalize_runtime_name(filter_entry)
let filter_lower = filter_entry.to_ascii_lowercase();
normalize_runtime_name(&rt_lower) == normalize_runtime_name(&filter_lower)
} }
/// Check if a runtime name matches any entry in a filter list. /// Check if a runtime name matches any entry in a filter list.
@@ -398,6 +397,25 @@ mod tests {
assert_eq!(normalize_runtime_name("custom_runtime"), "custom_runtime"); assert_eq!(normalize_runtime_name("custom_runtime"), "custom_runtime");
} }
#[test]
fn test_normalize_runtime_name_case_insensitive() {
assert_eq!(normalize_runtime_name("Node"), "node");
assert_eq!(normalize_runtime_name("NodeJS"), "node");
assert_eq!(normalize_runtime_name("Node.js"), "node");
assert_eq!(normalize_runtime_name("Python"), "python");
assert_eq!(normalize_runtime_name("Python3"), "python");
assert_eq!(normalize_runtime_name("Shell"), "shell");
assert_eq!(normalize_runtime_name("BASH"), "shell");
assert_eq!(normalize_runtime_name("Ruby"), "ruby");
assert_eq!(normalize_runtime_name("Go"), "go");
assert_eq!(normalize_runtime_name("GoLang"), "go");
assert_eq!(normalize_runtime_name("Java"), "java");
assert_eq!(normalize_runtime_name("JDK"), "java");
assert_eq!(normalize_runtime_name("Perl"), "perl");
assert_eq!(normalize_runtime_name("R"), "r");
assert_eq!(normalize_runtime_name("Custom_Runtime"), "custom_runtime");
}
#[test] #[test]
fn test_runtime_matches_filter() { fn test_runtime_matches_filter() {
// Node.js DB name lowercased vs worker filter "node" // Node.js DB name lowercased vs worker filter "node"

View File

@@ -60,8 +60,7 @@ struct Args {
detect_only: bool, detect_only: bool,
} }
#[tokio::main] fn main() -> Result<()> {
async fn main() -> Result<()> {
// Install HMAC-only JWT crypto provider (must be before any token operations) // Install HMAC-only JWT crypto provider (must be before any token operations)
attune_common::auth::install_crypto_provider(); attune_common::auth::install_crypto_provider();
@@ -75,7 +74,11 @@ async fn main() -> Result<()> {
info!("Starting Attune Universal Worker Agent"); info!("Starting Attune Universal Worker Agent");
// --- Phase 1: Runtime auto-detection --- // --- Phase 1: Runtime auto-detection (synchronous, before tokio runtime) ---
//
// All std::env::set_var calls MUST happen here, before we create the tokio
// runtime, to avoid undefined behavior from mutating the process environment
// while other threads are running.
// //
// Check if the user has explicitly set ATTUNE_WORKER_RUNTIMES. If so, skip // Check if the user has explicitly set ATTUNE_WORKER_RUNTIMES. If so, skip
// auto-detection and respect their override. Otherwise, probe the system for // auto-detection and respect their override. Otherwise, probe the system for
@@ -83,15 +86,57 @@ async fn main() -> Result<()> {
let runtimes_override = std::env::var("ATTUNE_WORKER_RUNTIMES").ok(); let runtimes_override = std::env::var("ATTUNE_WORKER_RUNTIMES").ok();
// Holds the detected runtimes so we can pass them to WorkerService later. // Holds the detected runtimes so we can pass them to WorkerService later.
// Populated only when auto-detection actually runs (no env var override). // Populated in both branches: auto-detection and override (filtered to
// match the override list).
let mut agent_detected_runtimes: Option<Vec<attune_worker::runtime_detect::DetectedRuntime>> = let mut agent_detected_runtimes: Option<Vec<attune_worker::runtime_detect::DetectedRuntime>> =
None; None;
if let Some(ref override_value) = runtimes_override { if let Some(ref override_value) = runtimes_override {
info!( info!(
"ATTUNE_WORKER_RUNTIMES already set (override), skipping auto-detection: {}", "ATTUNE_WORKER_RUNTIMES already set (override): {}",
override_value override_value
); );
// Even with an explicit override, run detection so we can register
// the overridden runtimes in the database and advertise accurate
// capability metadata (binary paths, versions). Without this, the
// worker would accept work for runtimes that were never registered
// locally — e.g. ruby/go on a fresh deployment.
info!("Running auto-detection for override-specified runtimes...");
let detected = detect_runtimes();
// Filter detected runtimes to only those matching the override list,
// so we don't register runtimes the user explicitly excluded.
let override_names: Vec<&str> = override_value.split(',').map(|s| s.trim()).collect();
let filtered: Vec<_> = detected
.into_iter()
.filter(|rt| {
let normalized = attune_common::runtime_detection::normalize_runtime_name(&rt.name);
override_names.iter().any(|ov| {
attune_common::runtime_detection::normalize_runtime_name(ov) == normalized
})
})
.collect();
if filtered.is_empty() {
warn!(
"None of the override runtimes ({}) were found on this system! \
The agent may not be able to execute any actions.",
override_value
);
} else {
info!(
"Matched {} override runtime(s) to detected interpreters:",
filtered.len()
);
for rt in &filtered {
match &rt.version {
Some(ver) => info!(" ✓ {} — {} ({})", rt.name, rt.path, ver),
None => info!(" ✓ {} — {}", rt.name, rt.path),
}
}
agent_detected_runtimes = Some(filtered);
}
} else { } else {
info!("No ATTUNE_WORKER_RUNTIMES override — running auto-detection..."); info!("No ATTUNE_WORKER_RUNTIMES override — running auto-detection...");
@@ -113,10 +158,7 @@ async fn main() -> Result<()> {
let runtime_list: Vec<&str> = detected.iter().map(|r| r.name.as_str()).collect(); let runtime_list: Vec<&str> = detected.iter().map(|r| r.name.as_str()).collect();
let runtime_csv = runtime_list.join(","); let runtime_csv = runtime_list.join(",");
info!("Setting ATTUNE_WORKER_RUNTIMES={}", runtime_csv); info!("Setting ATTUNE_WORKER_RUNTIMES={}", runtime_csv);
// SAFETY: std::env::set_var is safe in Rust 2021 edition. If upgrading // Safe: no other threads are running yet (tokio runtime not started).
// to edition 2024+, this call will need to be wrapped in `unsafe {}`.
// It's sound here because detection runs single-threaded before tokio
// starts any worker tasks.
std::env::set_var("ATTUNE_WORKER_RUNTIMES", &runtime_csv); std::env::set_var("ATTUNE_WORKER_RUNTIMES", &runtime_csv);
// Stash for Phase 2: pass to WorkerService for rich capability registration // Stash for Phase 2: pass to WorkerService for rich capability registration
@@ -124,7 +166,7 @@ async fn main() -> Result<()> {
} }
} }
// --- Handle --detect-only --- // --- Handle --detect-only (synchronous, no async runtime needed) ---
if args.detect_only { if args.detect_only {
if runtimes_override.is_some() { if runtimes_override.is_some() {
// User set an override, but --detect-only should show what's actually // User set an override, but --detect-only should show what's actually
@@ -147,12 +189,24 @@ async fn main() -> Result<()> {
return Ok(()); return Ok(());
} }
// --- Phase 2: Load configuration --- // --- Set config path env var (synchronous, before tokio runtime) ---
if let Some(config_path) = args.config { if let Some(ref config_path) = args.config {
// SAFETY: std::env::set_var is safe in Rust 2021 edition. See note above. // Safe: no other threads are running yet (tokio runtime not started).
std::env::set_var("ATTUNE_CONFIG", config_path); std::env::set_var("ATTUNE_CONFIG", config_path);
} }
// --- Build the tokio runtime and run the async portion ---
let runtime = tokio::runtime::Runtime::new()?;
runtime.block_on(async_main(args, agent_detected_runtimes))
}
/// The async portion of the agent entrypoint. Called from `main()` via
/// `runtime.block_on()` after all environment variable mutations are complete.
async fn async_main(
args: Args,
agent_detected_runtimes: Option<Vec<attune_worker::runtime_detect::DetectedRuntime>>,
) -> Result<()> {
// --- Phase 2: Load configuration ---
let mut config = Config::load()?; let mut config = Config::load()?;
config.validate()?; config.validate()?;

View File

@@ -84,10 +84,10 @@ pub async fn auto_register_detected_runtimes(
// Check if a runtime with a matching name already exists in the DB. // Check if a runtime with a matching name already exists in the DB.
// We normalize both sides for alias-aware comparison. // We normalize both sides for alias-aware comparison.
let already_exists = existing_runtimes.iter().any(|r| { // normalize_runtime_name lowercases internally, so no need to pre-lowercase.
let db_name = r.name.to_ascii_lowercase(); let already_exists = existing_runtimes
normalize_runtime_name(&db_name) == canonical_name .iter()
}); .any(|r| normalize_runtime_name(&r.name) == canonical_name);
if already_exists { if already_exists {
debug!( debug!(
@@ -194,7 +194,7 @@ pub async fn auto_register_detected_runtimes(
"Auto-detected {} runtime at {}", "Auto-detected {} runtime at {}",
detected_rt.name, detected_rt.path detected_rt.name, detected_rt.path
)), )),
name: capitalize_runtime_name(canonical_name), name: capitalize_runtime_name(&canonical_name),
distributions: build_minimal_distributions(detected_rt), distributions: build_minimal_distributions(detected_rt),
installation: None, installation: None,
execution_config, execution_config,
@@ -286,7 +286,7 @@ fn build_execution_config_from_template(
/// interpreter directly, without environment or dependency management. /// interpreter directly, without environment or dependency management.
fn build_minimal_execution_config(detected: &DetectedRuntime) -> serde_json::Value { fn build_minimal_execution_config(detected: &DetectedRuntime) -> serde_json::Value {
let canonical = normalize_runtime_name(&detected.name); let canonical = normalize_runtime_name(&detected.name);
let file_ext = default_file_extension(canonical); let file_ext = default_file_extension(&canonical);
let mut config = json!({ let mut config = json!({
"interpreter": { "interpreter": {

View File

@@ -24,9 +24,27 @@ use attune_common::models::runtime::{
}; };
use std::collections::HashMap; use std::collections::HashMap;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex as StdMutex, OnceLock};
use tokio::process::Command; use tokio::process::Command;
use tracing::{debug, error, info, warn}; use tracing::{debug, error, info, warn};
/// Per-directory locks for lazy environment setup to prevent concurrent
/// setup of the same environment from corrupting it. When two executions
/// for the same pack arrive concurrently (e.g. in agent mode), both may
/// see `!env_dir.exists()` and race to run `setup_pack_environment`.
/// This map provides a per-directory async mutex so that only one setup
/// runs at a time for each env_dir path.
static ENV_SETUP_LOCKS: OnceLock<StdMutex<HashMap<PathBuf, Arc<tokio::sync::Mutex<()>>>>> =
OnceLock::new();
fn get_env_setup_lock(env_dir: &Path) -> Arc<tokio::sync::Mutex<()>> {
let locks = ENV_SETUP_LOCKS.get_or_init(|| StdMutex::new(HashMap::new()));
let mut map = locks.lock().unwrap();
map.entry(env_dir.to_path_buf())
.or_insert_with(|| Arc::new(tokio::sync::Mutex::new(())))
.clone()
}
fn bash_single_quote_escape(s: &str) -> String { fn bash_single_quote_escape(s: &str) -> String {
s.replace('\'', "'\\''") s.replace('\'', "'\\''")
} }
@@ -620,111 +638,122 @@ impl Runtime for ProcessRuntime {
// create it on-demand. This is the primary code path for agent mode where // create it on-demand. This is the primary code path for agent mode where
// proactive startup setup is skipped, but it also serves as a safety net // proactive startup setup is skipped, but it also serves as a safety net
// for standard workers if the environment was somehow missed. // for standard workers if the environment was somehow missed.
if effective_config.environment.is_some() && pack_dir.exists() && !env_dir.exists() { // Acquire a per-directory async lock to serialize environment setup.
info!( // This prevents concurrent executions for the same pack from racing
"Runtime environment for pack '{}' not found at {}. \ // to create or repair the environment simultaneously.
Creating on first use (lazy setup).", if effective_config.environment.is_some() && pack_dir.exists() {
context.action_ref, let env_lock = get_env_setup_lock(&env_dir);
env_dir.display(), let _guard = env_lock.lock().await;
);
let setup_runtime = ProcessRuntime::new( // --- Lazy environment creation (double-checked after lock) ---
self.runtime_name.clone(), if !env_dir.exists() {
effective_config.clone(), info!(
self.packs_base_dir.clone(), "Runtime environment for pack '{}' not found at {}. \
self.runtime_envs_dir.clone(), Creating on first use (lazy setup).",
); context.action_ref,
match setup_runtime env_dir.display(),
.setup_pack_environment(&pack_dir, &env_dir) );
.await
{ let setup_runtime = ProcessRuntime::new(
Ok(()) => { self.runtime_name.clone(),
info!( effective_config.clone(),
"Successfully created environment for pack '{}' at {} (lazy setup)", self.packs_base_dir.clone(),
context.action_ref, self.runtime_envs_dir.clone(),
env_dir.display(), );
); match setup_runtime
} .setup_pack_environment(&pack_dir, &env_dir)
Err(e) => { .await
warn!( {
"Failed to create environment for pack '{}' at {}: {}. \ Ok(()) => {
Proceeding with system interpreter as fallback.", info!(
context.action_ref, "Successfully created environment for pack '{}' at {} (lazy setup)",
env_dir.display(), context.action_ref,
e, env_dir.display(),
); );
}
Err(e) => {
warn!(
"Failed to create environment for pack '{}' at {}: {}. \
Proceeding with system interpreter as fallback.",
context.action_ref,
env_dir.display(),
e,
);
}
} }
} }
}
// If the environment directory exists but contains a broken interpreter // --- Broken-symlink repair (also under the per-directory lock) ---
// (e.g. broken symlinks from a venv created in a different container), // If the environment directory exists but contains a broken interpreter
// attempt to recreate it before resolving the interpreter. // (e.g. broken symlinks from a venv created in a different container),
if effective_config.environment.is_some() && env_dir.exists() && pack_dir.exists() { // attempt to recreate it before resolving the interpreter.
if let Some(ref env_cfg) = effective_config.environment { if env_dir.exists() {
if let Some(ref interp_template) = env_cfg.interpreter_path { if let Some(ref env_cfg) = effective_config.environment {
let mut vars = std::collections::HashMap::new(); if let Some(ref interp_template) = env_cfg.interpreter_path {
vars.insert("env_dir", env_dir.to_string_lossy().to_string()); let mut vars = std::collections::HashMap::new();
vars.insert("pack_dir", pack_dir.to_string_lossy().to_string()); vars.insert("env_dir", env_dir.to_string_lossy().to_string());
let resolved = RuntimeExecutionConfig::resolve_template(interp_template, &vars); vars.insert("pack_dir", pack_dir.to_string_lossy().to_string());
let resolved_path = std::path::PathBuf::from(&resolved); let resolved =
RuntimeExecutionConfig::resolve_template(interp_template, &vars);
let resolved_path = std::path::PathBuf::from(&resolved);
// Check for a broken symlink: symlink_metadata succeeds for // Check for a broken symlink: symlink_metadata succeeds for
// the link itself even when its target is missing, while // the link itself even when its target is missing, while
// exists() (which follows symlinks) returns false. // exists() (which follows symlinks) returns false.
let is_broken_symlink = !resolved_path.exists() let is_broken_symlink = !resolved_path.exists()
&& std::fs::symlink_metadata(&resolved_path) && std::fs::symlink_metadata(&resolved_path)
.map(|m| m.file_type().is_symlink()) .map(|m| m.file_type().is_symlink())
.unwrap_or(false); .unwrap_or(false);
if is_broken_symlink { if is_broken_symlink {
let target = std::fs::read_link(&resolved_path) let target = std::fs::read_link(&resolved_path)
.map(|t| t.display().to_string()) .map(|t| t.display().to_string())
.unwrap_or_else(|_| "<unreadable>".to_string()); .unwrap_or_else(|_| "<unreadable>".to_string());
warn!(
"Detected broken symlink at '{}' -> '{}' in venv for pack '{}'. \
Removing broken environment and recreating...",
resolved_path.display(),
target,
context.action_ref,
);
// Remove the broken environment directory
if let Err(e) = std::fs::remove_dir_all(&env_dir) {
warn!( warn!(
"Failed to remove broken environment at {}: {}. \ "Detected broken symlink at '{}' -> '{}' in venv for pack '{}'. \
Will proceed with system interpreter.", Removing broken environment and recreating...",
env_dir.display(), resolved_path.display(),
e, target,
context.action_ref,
); );
} else {
// Recreate the environment using a temporary ProcessRuntime // Remove the broken environment directory
// with the effective (possibly version-specific) config. if let Err(e) = std::fs::remove_dir_all(&env_dir) {
let setup_runtime = ProcessRuntime::new( warn!(
self.runtime_name.clone(), "Failed to remove broken environment at {}: {}. \
effective_config.clone(), Will proceed with system interpreter.",
self.packs_base_dir.clone(), env_dir.display(),
self.runtime_envs_dir.clone(), e,
); );
match setup_runtime } else {
.setup_pack_environment(&pack_dir, &env_dir) // Recreate the environment using a temporary ProcessRuntime
.await // with the effective (possibly version-specific) config.
{ let setup_runtime = ProcessRuntime::new(
Ok(()) => { self.runtime_name.clone(),
info!( effective_config.clone(),
"Successfully recreated environment for pack '{}' at {}", self.packs_base_dir.clone(),
context.action_ref, self.runtime_envs_dir.clone(),
env_dir.display(), );
); match setup_runtime
} .setup_pack_environment(&pack_dir, &env_dir)
Err(e) => { .await
warn!( {
"Failed to recreate environment for pack '{}' at {}: {}. \ Ok(()) => {
Will proceed with system interpreter.", info!(
context.action_ref, "Successfully recreated environment for pack '{}' at {}",
env_dir.display(), context.action_ref,
e, env_dir.display(),
); );
}
Err(e) => {
warn!(
"Failed to recreate environment for pack '{}' at {}: {}. \
Will proceed with system interpreter.",
context.action_ref,
env_dir.display(),
e,
);
}
} }
} }
} }

View File

@@ -223,8 +223,6 @@ services:
- api_logs:/opt/attune/logs - api_logs:/opt/attune/logs
- agent_bin:/opt/attune/agent:ro - agent_bin:/opt/attune/agent:ro
depends_on: depends_on:
init-agent:
condition: service_completed_successfully
init-packs: init-packs:
condition: service_completed_successfully condition: service_completed_successfully
init-user: init-user:

View File

@@ -26,6 +26,9 @@ set -e
AGENT_DIR="${ATTUNE_AGENT_DIR:-/opt/attune/agent}" AGENT_DIR="${ATTUNE_AGENT_DIR:-/opt/attune/agent}"
AGENT_BIN="$AGENT_DIR/attune-agent" AGENT_BIN="$AGENT_DIR/attune-agent"
AGENT_URL="${ATTUNE_AGENT_URL:-http://attune-api:8080/api/v1/agent/binary}" AGENT_URL="${ATTUNE_AGENT_URL:-http://attune-api:8080/api/v1/agent/binary}"
# SECURITY: The default URL uses plain HTTP, which is fine for internal Docker
# networking. For cross-network or production deployments, set ATTUNE_AGENT_URL
# to an HTTPS endpoint and consider setting ATTUNE_AGENT_TOKEN to authenticate.
AGENT_TOKEN="${ATTUNE_AGENT_TOKEN:-}" AGENT_TOKEN="${ATTUNE_AGENT_TOKEN:-}"
# Auto-detect architecture if not specified # Auto-detect architecture if not specified
@@ -70,20 +73,24 @@ while [ $ATTEMPT -lt $MAX_RETRIES ]; do
ATTEMPT=$((ATTEMPT + 1)) ATTEMPT=$((ATTEMPT + 1))
if command -v curl >/dev/null 2>&1; then if command -v curl >/dev/null 2>&1; then
CURL_ARGS="-fsSL --retry 3 --retry-delay 2 -o $AGENT_BIN"
if [ -n "$AUTH_HEADER" ]; then if [ -n "$AUTH_HEADER" ]; then
CURL_ARGS="$CURL_ARGS -H \"$AUTH_HEADER\"" if curl -fsSL --retry 3 --retry-delay 2 -o "$AGENT_BIN" -H "$AUTH_HEADER" "$DOWNLOAD_URL" 2>/dev/null; then
fi break
if eval curl $CURL_ARGS "$DOWNLOAD_URL" 2>/dev/null; then fi
break else
if curl -fsSL --retry 3 --retry-delay 2 -o "$AGENT_BIN" "$DOWNLOAD_URL" 2>/dev/null; then
break
fi
fi fi
elif command -v wget >/dev/null 2>&1; then elif command -v wget >/dev/null 2>&1; then
WGET_ARGS="-q -O $AGENT_BIN"
if [ -n "$AUTH_HEADER" ]; then if [ -n "$AUTH_HEADER" ]; then
WGET_ARGS="$WGET_ARGS --header=\"$AUTH_HEADER\"" if wget -q -O "$AGENT_BIN" --header="$AUTH_HEADER" "$DOWNLOAD_URL" 2>/dev/null; then
fi break
if eval wget $WGET_ARGS "$DOWNLOAD_URL" 2>/dev/null; then fi
break else
if wget -q -O "$AGENT_BIN" "$DOWNLOAD_URL" 2>/dev/null; then
break
fi
fi fi
else else
echo "[attune] ERROR: Neither curl nor wget available. Cannot download agent." >&2 echo "[attune] ERROR: Neither curl nor wget available. Cannot download agent." >&2