This commit is contained in:
2026-03-02 19:27:52 -06:00
parent 42a9f1d31a
commit 5da940639a
40 changed files with 3931 additions and 2785 deletions

View File

@@ -33,5 +33,6 @@ aes-gcm = { workspace = true }
sha2 = { workspace = true }
base64 = { workspace = true }
tempfile = { workspace = true }
jsonwebtoken = { workspace = true }
[dev-dependencies]

View File

@@ -13,6 +13,7 @@
//! so the `ProcessRuntime` uses version-specific interpreter binaries,
//! environment commands, etc.
use attune_common::auth::jwt::{generate_execution_token, JwtConfig};
use attune_common::error::{Error, Result};
use attune_common::models::runtime::RuntimeExecutionConfig;
use attune_common::models::{runtime::Runtime as RuntimeModel, Action, Execution, ExecutionStatus};
@@ -42,6 +43,18 @@ pub struct ActionExecutor {
max_stderr_bytes: usize,
packs_base_dir: PathBuf,
api_url: String,
jwt_config: JwtConfig,
}
/// Normalize a server bind address into a connectable URL.
///
/// When the server binds to `0.0.0.0` (all interfaces) or `::` (IPv6 any),
/// we substitute `127.0.0.1` so that actions running on the same host can
/// reach the API.
fn normalize_api_url(raw_url: &str) -> String {
raw_url
.replace("://0.0.0.0", "://127.0.0.1")
.replace("://[::]", "://127.0.0.1")
}
impl ActionExecutor {
@@ -55,7 +68,9 @@ impl ActionExecutor {
max_stderr_bytes: usize,
packs_base_dir: PathBuf,
api_url: String,
jwt_config: JwtConfig,
) -> Self {
let api_url = normalize_api_url(&api_url);
Self {
pool,
runtime_registry,
@@ -65,6 +80,7 @@ impl ActionExecutor {
max_stderr_bytes,
packs_base_dir,
api_url,
jwt_config,
}
}
@@ -276,9 +292,34 @@ impl ActionExecutor {
env.insert("ATTUNE_ACTION".to_string(), execution.action_ref.clone());
env.insert("ATTUNE_API_URL".to_string(), self.api_url.clone());
// TODO: Generate execution-scoped API token
// For now, set placeholder to maintain interface compatibility
env.insert("ATTUNE_API_TOKEN".to_string(), "".to_string());
// Generate execution-scoped API token.
// The identity that triggered the execution is derived from the `sub` claim
// of the original token; for rule-triggered executions we use identity 1
// (the system identity) as a reasonable default.
let identity_id: i64 = 1; // System identity fallback
// Default timeout is 300s; add 60s grace period for cleanup.
// The actual `timeout` variable is computed later in this function,
// but the token TTL just needs a reasonable upper bound.
let token_ttl = Some(360_i64);
match generate_execution_token(
identity_id,
execution.id,
&execution.action_ref,
&self.jwt_config,
token_ttl,
) {
Ok(token) => {
env.insert("ATTUNE_API_TOKEN".to_string(), token);
}
Err(e) => {
warn!(
"Failed to generate execution token for execution {}: {}. \
Actions that call back to the API will not authenticate.",
execution.id, e
);
env.insert("ATTUNE_API_TOKEN".to_string(), String::new());
}
}
// Add rule and trigger context if execution was triggered by enforcement
if let Some(enforcement_id) = execution.enforcement {

View File

@@ -285,6 +285,17 @@ impl WorkerService {
let api_url = std::env::var("ATTUNE_API_URL")
.unwrap_or_else(|_| format!("http://{}:{}", config.server.host, config.server.port));
// Build JWT config for generating execution-scoped tokens
let jwt_config = attune_common::auth::jwt::JwtConfig {
secret: config
.security
.jwt_secret
.clone()
.unwrap_or_else(|| "insecure_default_secret_change_in_production".to_string()),
access_token_expiration: config.security.jwt_access_expiration as i64,
refresh_token_expiration: config.security.jwt_refresh_expiration as i64,
};
let executor = Arc::new(ActionExecutor::new(
pool.clone(),
runtime_registry,
@@ -294,6 +305,7 @@ impl WorkerService {
max_stderr_bytes,
packs_base_dir.clone(),
api_url,
jwt_config,
));
// Initialize heartbeat manager