Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 104 additions & 0 deletions crates/convergio-executor/src/defaults.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use convergio_runner::{PermissionProfile, RunnerKind};
use serde::{Deserialize, Serialize};
use std::str::FromStr;

/// Template the executor uses for tasks that opt out of runner-based
/// dispatch. ADR-0034 introduced per-task `runner_kind` / `profile`
Expand Down Expand Up @@ -53,3 +54,106 @@ impl Default for RunnerDefaults {
}
}
}

impl RunnerDefaults {
/// Read defaults from the environment, falling back to
/// [`RunnerDefaults::default`] for any value not provided.
///
/// Recognised variables:
/// - `CONVERGIO_RUNNER_DEFAULT` — wire format `<vendor>:<model>`.
/// Examples: `claude:sonnet`, `claude:opus`, `copilot:gpt-5.2`,
/// `copilot:claude-opus`. Invalid strings log a warning and the
/// compiled-in default is used.
/// - `CONVERGIO_RUNNER_PROFILE` — `standard` (default),
/// `restricted`, `unrestricted`.
/// - `CONVERGIO_DAEMON_URL` — base URL the spawned agent calls
/// back to; defaults to `http://127.0.0.1:8420`.
pub fn from_env() -> Self {
let mut out = Self::default();
if let Ok(raw) = std::env::var("CONVERGIO_RUNNER_DEFAULT") {
match RunnerKind::from_str(raw.trim()) {
Ok(k) => out.kind = k,
Err(e) => tracing::warn!(
raw = %raw,
error = %e,
"CONVERGIO_RUNNER_DEFAULT not parseable; using compiled-in default"
),
}
}
if let Ok(raw) = std::env::var("CONVERGIO_RUNNER_PROFILE") {
match PermissionProfile::from_str(raw.trim()) {
Ok(p) => out.profile = p,
Err(e) => tracing::warn!(
raw = %raw,
error = %e,
"CONVERGIO_RUNNER_PROFILE not parseable; using compiled-in default"
),
}
}
if let Ok(url) = std::env::var("CONVERGIO_DAEMON_URL") {
if !url.trim().is_empty() {
out.daemon_url = url;
}
}
out
}
}

#[cfg(test)]
mod tests {
use super::*;
use std::sync::Mutex;

// Env vars are process-global; serialise the cases that touch
// them so they don't race when `cargo test` parallelises.
static ENV_LOCK: Mutex<()> = Mutex::new(());

fn clear_env() {
for k in [
"CONVERGIO_RUNNER_DEFAULT",
"CONVERGIO_RUNNER_PROFILE",
"CONVERGIO_DAEMON_URL",
] {
std::env::remove_var(k);
}
}

#[test]
fn from_env_falls_back_to_default_when_unset() {
let _g = ENV_LOCK.lock().unwrap();
clear_env();
let d = RunnerDefaults::from_env();
assert_eq!(d.kind.to_string(), "claude:sonnet");
assert!(matches!(d.profile, PermissionProfile::Standard));
}

#[test]
fn from_env_picks_copilot_when_requested() {
let _g = ENV_LOCK.lock().unwrap();
clear_env();
std::env::set_var("CONVERGIO_RUNNER_DEFAULT", "copilot:gpt-5.2");
let d = RunnerDefaults::from_env();
clear_env();
assert_eq!(d.kind.to_string(), "copilot:gpt-5.2");
}

#[test]
fn from_env_invalid_runner_keeps_default() {
let _g = ENV_LOCK.lock().unwrap();
clear_env();
std::env::set_var("CONVERGIO_RUNNER_DEFAULT", "garbage-no-colon");
let d = RunnerDefaults::from_env();
clear_env();
assert_eq!(d.kind.to_string(), "claude:sonnet");
}

#[test]
fn from_env_overrides_daemon_url() {
let _g = ENV_LOCK.lock().unwrap();
clear_env();
std::env::set_var("CONVERGIO_DAEMON_URL", "http://10.0.0.1:9000");
let d = RunnerDefaults::from_env();
clear_env();
assert_eq!(d.daemon_url, "http://10.0.0.1:9000");
}
}
23 changes: 17 additions & 6 deletions crates/convergio-server/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ use convergio_db::Pool;
use convergio_durability::reaper::{self, ReaperConfig};
use convergio_durability::telemetry_collector::{self, CollectorConfig};
use convergio_durability::{init as init_durability, Durability};
use convergio_executor::{spawn_loop as executor_spawn_loop, Executor, SpawnTemplate};
use convergio_executor::{
spawn_loop as executor_spawn_loop, Executor, RunnerDefaults, SpawnTemplate,
};
use convergio_lifecycle::watcher::{self, WatcherConfig};
use convergio_lifecycle::Supervisor;
use convergio_server::{router, AppState};
Expand Down Expand Up @@ -117,11 +119,20 @@ async fn start(
};
let _watcher = watcher::spawn((*supervisor).clone(), watcher_config);

let executor = Arc::new(Executor::new(
(*durability).clone(),
(*supervisor).clone(),
SpawnTemplate::default(),
));
let runner_defaults = RunnerDefaults::from_env();
tracing::info!(
runner_kind = %runner_defaults.kind,
runner_profile = ?runner_defaults.profile,
"executor runner defaults"
);
let executor = Arc::new(
Executor::new(
(*durability).clone(),
(*supervisor).clone(),
SpawnTemplate::default(),
)
.with_defaults(runner_defaults),
);
let executor_tick = Duration::seconds(parse_env_i64("CONVERGIO_EXECUTOR_TICK_SECS", 30));
let _executor_loop = executor_spawn_loop(executor, executor_tick);

Expand Down
5 changes: 3 additions & 2 deletions crates/convergio-server/src/routes/dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use crate::error::ApiError;
use axum::extract::State;
use axum::routing::post;
use axum::{Json, Router};
use convergio_executor::{Executor, SpawnTemplate};
use convergio_executor::{Executor, RunnerDefaults, SpawnTemplate};
use serde_json::{json, Value};

/// Mount the dispatch route.
Expand All @@ -21,7 +21,8 @@ async fn dispatch(State(state): State<AppState>) -> Result<Json<Value>, ApiError
(*state.durability).clone(),
(*state.supervisor).clone(),
SpawnTemplate::default(),
);
)
.with_defaults(RunnerDefaults::from_env());
let n = exec.tick().await.map_err(map_exec)?;
Ok(Json(json!({"dispatched": n})))
}
Expand Down
Loading