Refactor config layering into combine plus settings

This commit is contained in:
Bryan Helmkamp 2026-03-27 14:52:11 -04:00
parent 126ece3d71
commit 8a256bb68e
No known key found for this signature in database
52 changed files with 1502 additions and 1619 deletions

10
Cargo.lock generated
View file

@ -1395,6 +1395,7 @@ dependencies = [
"anyhow",
"clap",
"dirs",
"fabro-config-derive",
"fabro-util",
"serde",
"serde_json",
@ -1404,6 +1405,15 @@ dependencies = [
"tracing",
]
[[package]]
name = "fabro-config-derive"
version = "0.176.2"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "fabro-core"
version = "0.176.2"

View file

@ -1277,21 +1277,21 @@ mod runs {
}
pub fn configuration() -> serde_json::Value {
serde_json::to_value(fabro_config::FabroConfig {
serde_json::to_value(fabro_config::FabroSettings {
version: Some(1),
goal: Some("Add rate limiting to auth endpoints".into()),
graph: Some("implement.fabro".into()),
work_dir: Some("/workspace/api-server".into()),
llm: Some(fabro_config::run::LlmConfig {
llm: Some(fabro_config::run::LlmSettings {
model: Some("claude-opus-4-6".into()),
provider: Some("anthropic".into()),
fallbacks: None,
}),
setup: Some(fabro_config::run::SetupConfig {
setup: Some(fabro_config::run::SetupSettings {
commands: vec!["bun install".into(), "bun run typecheck".into()],
timeout_ms: Some(120_000),
}),
sandbox: Some(fabro_config::sandbox::SandboxConfig {
sandbox: Some(fabro_config::sandbox::SandboxSettings {
provider: Some("daytona".into()),
preserve: None,
devcontainer: None,
@ -1431,7 +1431,7 @@ mod workflows {
]
}
fn run_config_to_api(cfg: fabro_config::FabroConfig) -> RunConfiguration {
fn run_config_to_api(cfg: fabro_config::FabroSettings) -> RunConfiguration {
fn strip_nulls(val: serde_json::Value) -> serde_json::Value {
match val {
serde_json::Value::Object(map) => serde_json::Value::Object(
@ -1455,18 +1455,18 @@ mod workflows {
WorkflowDetail {
name: "Fix Build".into(), slug: "fix_build".into(), filename: "fix_build.fabro".into(),
description: "Automatically diagnoses and fixes CI build failures by analyzing error logs, identifying root causes, and applying targeted code changes.".into(),
config: run_config_to_api(fabro_config::FabroConfig {
config: run_config_to_api(fabro_config::FabroSettings {
version: Some(1),
goal: Some("Diagnose and fix CI build failures".into()),
graph: Some("fix_build.fabro".into()),
work_dir: None,
llm: Some(fabro_config::run::LlmConfig {
llm: Some(fabro_config::run::LlmSettings {
model: Some("claude-sonnet".into()),
provider: None,
fallbacks: None,
}),
setup: None,
sandbox: Some(fabro_config::sandbox::SandboxConfig {
sandbox: Some(fabro_config::sandbox::SandboxSettings {
provider: Some("daytona".into()),
preserve: None,
devcontainer: None,
@ -1526,21 +1526,21 @@ mod workflows {
WorkflowDetail {
name: "Implement Feature".into(), slug: "implement".into(), filename: "implement.fabro".into(),
description: "Generates production-ready code from a technical blueprint, including tests, documentation, and a pull request ready for review.".into(),
config: run_config_to_api(fabro_config::FabroConfig {
config: run_config_to_api(fabro_config::FabroSettings {
version: Some(1),
goal: Some("Implement feature from technical blueprint".into()),
graph: Some("implement.fabro".into()),
work_dir: None,
llm: Some(fabro_config::run::LlmConfig {
llm: Some(fabro_config::run::LlmSettings {
model: Some("claude-sonnet".into()),
provider: None,
fallbacks: None,
}),
setup: Some(fabro_config::run::SetupConfig {
setup: Some(fabro_config::run::SetupSettings {
commands: vec!["bun install".into(), "bun run typecheck".into()],
timeout_ms: Some(120_000),
}),
sandbox: Some(fabro_config::sandbox::SandboxConfig {
sandbox: Some(fabro_config::sandbox::SandboxSettings {
provider: Some("daytona".into()),
preserve: None,
devcontainer: None,
@ -1615,18 +1615,18 @@ mod workflows {
WorkflowDetail {
name: "Sync Drift".into(), slug: "sync_drift".into(), filename: "sync_drift.fabro".into(),
description: "Detects configuration and code drift between environments, then generates reconciliation patches to bring everything back in sync.".into(),
config: run_config_to_api(fabro_config::FabroConfig {
config: run_config_to_api(fabro_config::FabroSettings {
version: Some(1),
goal: Some("Detect and reconcile configuration drift across environments".into()),
graph: Some("sync_drift.fabro".into()),
work_dir: None,
llm: Some(fabro_config::run::LlmConfig {
llm: Some(fabro_config::run::LlmSettings {
model: Some("claude-sonnet".into()),
provider: None,
fallbacks: None,
}),
setup: None,
sandbox: Some(fabro_config::sandbox::SandboxConfig {
sandbox: Some(fabro_config::sandbox::SandboxSettings {
provider: Some("daytona".into()),
preserve: None,
devcontainer: None,
@ -1692,18 +1692,18 @@ mod workflows {
WorkflowDetail {
name: "Expand Product".into(), slug: "expand".into(), filename: "expand.fabro".into(),
description: "Evolves the product by analyzing usage patterns and specifications to propose and implement incremental improvements.".into(),
config: run_config_to_api(fabro_config::FabroConfig {
config: run_config_to_api(fabro_config::FabroSettings {
version: Some(1),
goal: Some("Propose and implement incremental product improvements".into()),
graph: Some("expand.fabro".into()),
work_dir: None,
llm: Some(fabro_config::run::LlmConfig {
llm: Some(fabro_config::run::LlmSettings {
model: Some("claude-sonnet".into()),
provider: None,
fallbacks: None,
}),
setup: None,
sandbox: Some(fabro_config::sandbox::SandboxConfig {
sandbox: Some(fabro_config::sandbox::SandboxSettings {
provider: Some("daytona".into()),
preserve: None,
devcontainer: None,
@ -3242,25 +3242,25 @@ mod insights {
mod settings {
use fabro_config::server::*;
use fabro_config::FabroConfig;
use fabro_config::FabroSettings;
pub fn server_config() -> serde_json::Value {
serde_json::to_value(FabroConfig {
serde_json::to_value(FabroSettings {
storage_dir: Some("/home/fabro/.fabro".into()),
max_concurrent_runs: Some(10),
web: Some(WebConfig {
web: Some(WebSettings {
url: "https://arc.example.com".into(),
auth: AuthConfig {
auth: AuthSettings {
provider: AuthProvider::Github,
allowed_usernames: vec!["brynary".into(), "alice".into()],
},
}),
api: Some(ApiConfig {
api: Some(ApiSettings {
base_url: "https://api.fabro.example.com".into(),
authentication_strategies: vec![ApiAuthStrategy::Jwt],
tls: None,
}),
git: Some(GitConfig {
git: Some(GitSettings {
provider: GitProvider::Github,
app_id: Some("12345".into()),
client_id: Some("Iv1.abc123".into()),
@ -3268,18 +3268,18 @@ mod settings {
author: Default::default(),
webhooks: None,
}),
features: Some(Features {
features: Some(FeaturesSettings {
session_sandboxes: false,
retros: false,
}),
log: Default::default(),
llm: Some(fabro_config::run::LlmConfig {
llm: Some(fabro_config::run::LlmSettings {
model: Some("claude-sonnet".into()),
provider: Some("anthropic".into()),
fallbacks: None,
}),
setup: None,
sandbox: Some(fabro_config::sandbox::SandboxConfig {
sandbox: Some(fabro_config::sandbox::SandboxSettings {
provider: Some("daytona".into()),
preserve: None,
devcontainer: None,

View file

@ -68,7 +68,7 @@ pub fn decode_pem_env(name: &str, value: &str) -> String {
/// Call this once at startup before serving requests. Panics if the
/// configuration is invalid (JWT strategy but no public key, or mTLS without TLS config).
pub fn resolve_auth_mode(
api_config: &fabro_config::server::ApiConfig,
api_config: &fabro_config::server::ApiSettings,
allowed_usernames: Vec<String>,
) -> AuthMode {
use fabro_config::server::ApiAuthStrategy;

View file

@ -6,7 +6,7 @@ pub mod serve;
pub mod server;
pub mod server_config {
pub use fabro_config::server::*;
pub use fabro_config::FabroConfig;
pub use fabro_config::FabroSettings;
}
pub mod sessions;
pub mod tls;

View file

@ -9,7 +9,7 @@ use tracing::{error, info, warn};
use clap::Args;
use fabro_config::FabroConfig;
use fabro_config::FabroSettings;
use crate::jwt_auth::{AuthMode, AuthStrategy};
use crate::server::build_router;
@ -83,7 +83,8 @@ pub async fn serve_command(args: ServeArgs, styles: &'static Styles) -> anyhow::
// Initialize data directory and SQLite database
let config_path = args.config;
let server_config = fabro_config::server::load_server_config(config_path.as_deref())?;
let server_config: FabroSettings =
fabro_config::server::load_server_config(config_path.as_deref())?.try_into()?;
let data_dir = fabro_config::server::resolve_storage_dir(&server_config);
// Shared config for live reloading
@ -216,6 +217,10 @@ pub async fn serve_command(args: ServeArgs, styles: &'static Styles) -> anyhow::
interval.tick().await;
match fabro_config::server::load_server_config(config_path_for_poll.as_deref()) {
Ok(new_config) => {
let Ok(new_config) = FabroSettings::try_from(new_config) else {
warn!("Failed to finalize reloaded server config");
continue;
};
let changed = {
let cfg = config_for_poll.read().expect("config lock poisoned");
*cfg != new_config
@ -263,7 +268,7 @@ pub async fn serve_command(args: ServeArgs, styles: &'static Styles) -> anyhow::
/// Resolve model and provider from shared config, with CLI overrides taking precedence.
fn resolve_model_provider(
shared_config: &RwLock<FabroConfig>,
shared_config: &RwLock<FabroSettings>,
cli_model: Option<&str>,
cli_provider: Option<&str>,
) -> (String, Provider) {

View file

@ -520,10 +520,10 @@ async fn start_run(
let run_id = ulid::Ulid::new().to_string();
info!(run_id = %run_id, "Run queued");
let run_dir = std::env::temp_dir().join(format!("fabro-{}", uuid::Uuid::new_v4()));
let config = fabro_config::config::FabroConfig {
let config = fabro_config::FabroSettings {
dry_run: Some(state.dry_run),
hooks: state.hooks.clone(),
sandbox: Some(fabro_config::sandbox::SandboxConfig {
sandbox: Some(fabro_config::sandbox::SandboxSettings {
provider: Some("local".to_string()),
..Default::default()
}),

View file

@ -7,7 +7,7 @@ use rustls_pki_types::{CertificateDer, PrivateKeyDer};
use tokio::net::TcpListener;
use tracing::error;
use fabro_config::server::TlsConfig;
use fabro_config::server::TlsSettings;
use crate::jwt_auth::PeerCertificates;
@ -22,7 +22,7 @@ pub enum ClientAuth {
}
/// Build a rustls `ServerConfig` from the `[api.tls]` configuration.
pub fn build_rustls_config(tls_config: &TlsConfig, client_auth: ClientAuth) -> Arc<ServerConfig> {
pub fn build_rustls_config(tls_config: &TlsSettings, client_auth: ClientAuth) -> Arc<ServerConfig> {
let certs = load_certs(&tls_config.cert);
let key = load_private_key(&tls_config.key);

View file

@ -10,7 +10,7 @@ mod mtls_e2e {
use fabro_api::jwt_auth::{AuthMode, AuthStrategy};
use fabro_api::server::{build_router, create_app_state};
use fabro_api::server_config::TlsConfig;
use fabro_api::server_config::TlsSettings;
use fabro_api::tls::{build_rustls_config, ClientAuth};
use fabro_workflows::pipeline::LlmSpec;
use tokio::net::TcpListener;
@ -181,7 +181,7 @@ mod mtls_e2e {
/// Start a TLS server on a random port, returning the bound address.
async fn start_tls_server(
tls_config: &TlsConfig,
tls_config: &TlsSettings,
client_auth: ClientAuth,
auth_mode: AuthMode,
) -> std::net::SocketAddr {
@ -236,7 +236,7 @@ mod mtls_e2e {
let dir = tempfile::tempdir().unwrap();
let pki = generate_pki(dir.path(), "TestCA", "localhost", "testuser");
let tls_config = TlsConfig {
let tls_config = TlsSettings {
cert: pki.server_cert.clone(),
key: pki.server_key.clone(),
ca: pki.ca_cert.clone(),
@ -262,7 +262,7 @@ mod mtls_e2e {
let dir = tempfile::tempdir().unwrap();
let pki = generate_pki(dir.path(), "TestCA", "localhost", "testuser");
let tls_config = TlsConfig {
let tls_config = TlsSettings {
cert: pki.server_cert.clone(),
key: pki.server_key.clone(),
ca: pki.ca_cert.clone(),
@ -303,7 +303,7 @@ mod mtls_e2e {
let dir = tempfile::tempdir().unwrap();
let pki = generate_pki(dir.path(), "TestCA", "localhost", "testuser");
let tls_config = TlsConfig {
let tls_config = TlsSettings {
cert: pki.server_cert.clone(),
key: pki.server_key.clone(),
ca: pki.ca_cert.clone(),
@ -378,7 +378,7 @@ mod mtls_e2e {
let dir = tempfile::tempdir().unwrap();
let pki = generate_pki(dir.path(), "TestCA", "localhost", "testuser");
let tls_config = TlsConfig {
let tls_config = TlsSettings {
cert: pki.server_cert.clone(),
key: pki.server_key.clone(),
ca: pki.ca_cert.clone(),

View file

@ -8,7 +8,7 @@ use fabro_api::jwt_auth::AuthMode;
use fabro_api::server::{build_router, create_app_state};
use fabro_api::server_config::*;
use fabro_config::run::*;
use fabro_config::sandbox::SandboxConfig;
use fabro_config::sandbox::SandboxSettings;
use fabro_hooks::*;
use fabro_sandbox::daytona::*;
use fabro_workflows::pipeline::LlmSpec;
@ -235,59 +235,59 @@ fn compare_schema(
}
}
/// Build a FabroConfig with every Option set to Some so all keys appear
/// Build a FabroSettings with every Option set to Some so all keys appear
/// in the serialized JSON.
fn fully_populated_server_config() -> FabroConfig {
FabroConfig {
fn fully_populated_server_config() -> FabroSettings {
FabroSettings {
storage_dir: Some("/data".into()),
max_concurrent_runs: Some(10),
web: Some(WebConfig {
web: Some(WebSettings {
url: "https://example.com".into(),
auth: AuthConfig {
auth: AuthSettings {
provider: AuthProvider::Github,
allowed_usernames: vec!["user".into()],
},
}),
api: Some(ApiConfig {
api: Some(ApiSettings {
base_url: "https://api.example.com".into(),
authentication_strategies: vec![ApiAuthStrategy::Jwt],
tls: Some(TlsConfig {
tls: Some(TlsSettings {
cert: "c".into(),
key: "k".into(),
ca: "ca".into(),
}),
}),
git: Some(GitConfig {
git: Some(GitSettings {
provider: GitProvider::Github,
app_id: Some("123".into()),
client_id: Some("456".into()),
slug: Some("fabro".into()),
author: GitAuthorConfig {
author: GitAuthorSettings {
name: Some("bot".into()),
email: Some("bot@x".into()),
},
webhooks: Some(WebhookConfig {
webhooks: Some(WebhookSettings {
strategy: WebhookStrategy::TailscaleFunnel,
}),
}),
features: Some(Features {
features: Some(FeaturesSettings {
session_sandboxes: true,
retros: false,
}),
log: Some(LogConfig {
log: Some(LogSettings {
level: Some("debug".into()),
}),
work_dir: Some("/work".into()),
llm: Some(LlmConfig {
llm: Some(LlmSettings {
model: Some("m".into()),
provider: Some("p".into()),
fallbacks: Some(Default::default()),
}),
setup: Some(SetupConfig {
setup: Some(SetupSettings {
commands: vec!["echo hi".into()],
timeout_ms: Some(5000),
}),
sandbox: Some(SandboxConfig {
sandbox: Some(SandboxSettings {
provider: Some("daytona".into()),
preserve: Some(true),
devcontainer: None,
@ -310,16 +310,16 @@ fn fully_populated_server_config() -> FabroConfig {
env: Some(Default::default()),
}),
vars: Some(Default::default()),
checkpoint: CheckpointConfig {
checkpoint: CheckpointSettings {
exclude_globs: vec!["**/node_modules/**".into()],
},
pull_request: Some(PullRequestConfig {
pull_request: Some(PullRequestSettings {
enabled: true,
draft: false,
auto_merge: false,
merge_strategy: MergeStrategy::Squash,
}),
assets: Some(AssetsConfig {
assets: Some(AssetsSettings {
include: vec!["test-results/**".into()],
}),
// One hook per HookType variant so the key union covers all fields.
@ -388,7 +388,7 @@ fn fully_populated_server_config() -> FabroConfig {
tool_timeout_secs: fabro_config::mcp::default_tool_timeout_secs(),
},
)]),
github: Some(GitHubConfig {
github: Some(GitHubSettings {
permissions: std::collections::HashMap::from([("contents".into(), "read".into())]),
}),
..Default::default()

View file

@ -938,7 +938,7 @@ pub(crate) struct ConfigNamespace {
#[derive(Subcommand)]
pub(crate) enum ConfigCommand {
/// Print the merged FabroConfig as YAML
/// Print the merged FabroSettings as YAML
Show(ConfigShowArgs),
}

View file

@ -1,16 +1,23 @@
#[allow(unused_imports)]
pub use fabro_config::cli::*;
#[cfg(feature = "server")]
use fabro_config::FabroConfig;
use std::path::Path;
use fabro_config::FabroSettings;
#[cfg(feature = "server")]
use tracing::debug;
pub fn load_cli_config(path: Option<&Path>) -> anyhow::Result<FabroSettings> {
fabro_config::cli::load_cli_config(path)?.try_into()
}
#[cfg(feature = "server")]
#[derive(Debug, PartialEq)]
pub struct ResolvedMode {
pub mode: ExecutionMode,
pub server_base_url: String,
pub tls: Option<ClientTlsConfig>,
pub tls: Option<ClientTlsSettings>,
}
#[cfg(feature = "server")]
@ -20,7 +27,7 @@ const DEFAULT_SERVER_URL: &str = "http://localhost:3000";
pub fn resolve_mode(
cli_mode: Option<ExecutionMode>,
cli_server_url: Option<&str>,
config: &FabroConfig,
config: &FabroSettings,
) -> ResolvedMode {
let mode = cli_mode.or_else(|| config.mode.clone()).unwrap_or_default();
@ -43,7 +50,7 @@ pub fn resolve_mode(
}
#[cfg(feature = "server")]
pub fn build_server_client(tls: Option<&ClientTlsConfig>) -> anyhow::Result<reqwest::Client> {
pub fn build_server_client(tls: Option<&ClientTlsSettings>) -> anyhow::Result<reqwest::Client> {
let Some(tls) = tls else {
return Ok(reqwest::Client::new());
};
@ -82,7 +89,7 @@ mod tests {
#[test]
fn resolve_mode_defaults_to_standalone() {
let config = FabroConfig::default();
let config = FabroSettings::default();
let resolved = resolve_mode(None, None, &config);
assert_eq!(resolved.mode, ExecutionMode::Standalone);
assert_eq!(resolved.server_base_url, DEFAULT_SERVER_URL);
@ -91,13 +98,13 @@ mod tests {
#[test]
fn resolve_mode_config_overrides_default() {
let config = FabroConfig {
let config = FabroSettings {
mode: Some(ExecutionMode::Server),
server: Some(ServerDefaults {
server: Some(ServerSettings {
base_url: Some("https://config.example.com".to_string()),
tls: None,
}),
..FabroConfig::default()
..FabroSettings::default()
};
let resolved = resolve_mode(None, None, &config);
assert_eq!(resolved.mode, ExecutionMode::Server);
@ -106,13 +113,13 @@ mod tests {
#[test]
fn resolve_mode_cli_overrides_config() {
let config = FabroConfig {
let config = FabroSettings {
mode: Some(ExecutionMode::Standalone),
server: Some(ServerDefaults {
server: Some(ServerSettings {
base_url: Some("https://config.example.com".to_string()),
tls: None,
}),
..FabroConfig::default()
..FabroSettings::default()
};
let resolved = resolve_mode(
Some(ExecutionMode::Server),
@ -125,12 +132,12 @@ mod tests {
#[test]
fn resolve_mode_cli_url_overrides_config_url() {
let config = FabroConfig {
server: Some(ServerDefaults {
let config = FabroSettings {
server: Some(ServerSettings {
base_url: Some("https://config.example.com".to_string()),
tls: None,
}),
..FabroConfig::default()
..FabroSettings::default()
};
let resolved = resolve_mode(None, Some("https://cli.example.com"), &config);
assert_eq!(resolved.server_base_url, "https://cli.example.com");
@ -138,17 +145,17 @@ mod tests {
#[test]
fn resolve_mode_tls_from_config() {
let tls = ClientTlsConfig {
let tls = ClientTlsSettings {
cert: PathBuf::from("cert.pem"),
key: PathBuf::from("key.pem"),
ca: PathBuf::from("ca.pem"),
};
let config = FabroConfig {
server: Some(ServerDefaults {
let config = FabroSettings {
server: Some(ServerSettings {
base_url: None,
tls: Some(tls.clone()),
}),
..FabroConfig::default()
..FabroSettings::default()
};
let resolved = resolve_mode(None, None, &config);
assert_eq!(resolved.tls, Some(tls));

View file

@ -1,11 +1,9 @@
use std::io::Write;
use std::path::Path;
use anyhow::bail;
use fabro_config::FabroConfig;
use crate::args::{ConfigCommand, ConfigNamespace, ConfigShowArgs};
use crate::cli_config;
use anyhow::bail;
use fabro_config::FabroSettings;
pub fn dispatch(ns: ConfigNamespace) -> anyhow::Result<()> {
match ns.command {
@ -13,14 +11,14 @@ pub fn dispatch(ns: ConfigNamespace) -> anyhow::Result<()> {
}
}
fn merged_config(workflow: Option<&Path>) -> anyhow::Result<FabroConfig> {
let mut config = cli_config::load_cli_config(None)?;
fn merged_config(workflow: Option<&Path>) -> anyhow::Result<FabroSettings> {
let mut config = fabro_config::cli::load_cli_config(None)?;
let cwd = std::env::current_dir()?;
if let Some((_config_path, project_config)) =
fabro_config::project::discover_project_config(&cwd)?
{
config.merge_overlay(project_config);
config = config.combine(project_config);
}
if let Some(workflow) = workflow {
@ -28,13 +26,13 @@ fn merged_config(workflow: Option<&Path>) -> anyhow::Result<FabroConfig> {
crate::commands::run::execute::resolve_workflow_source(workflow)?;
if let Some(run_config) = run_config {
config.merge_overlay(run_config);
config = config.combine(run_config);
} else if !resolved_path.is_file() {
bail!("Workflow not found: {}", resolved_path.display());
}
}
Ok(config)
config.try_into()
}
pub fn show_command(args: &ConfigShowArgs) -> anyhow::Result<()> {

View file

@ -928,7 +928,7 @@ pub async fn run_doctor(verbose: bool, live: bool) -> i32 {
spinner.enable_steady_tick(std::time::Duration::from_millis(80));
// Gather state
let cli_config = fabro_config::cli::load_cli_config(None).unwrap_or_default();
let cli_config = crate::cli_config::load_cli_config(None).unwrap_or_default();
let config_path = dirs::home_dir().map(|h| h.join(".fabro").join("cli.toml"));
let config_exists = config_path.as_ref().is_some_and(|p| p.exists());
@ -943,7 +943,9 @@ pub async fn run_doctor(verbose: bool, live: bool) -> i32 {
let daytona_configured = std::env::var("DAYTONA_API_KEY").is_ok();
#[cfg(feature = "server")]
let server_config = fabro_config::server::load_server_config(None).unwrap_or_default();
let server_config = fabro_config::server::load_server_config(None)
.and_then(fabro_config::FabroSettings::try_from)
.unwrap_or_default();
#[cfg(feature = "server")]
let api_status = {

View file

@ -931,7 +931,7 @@ mod tests {
#[cfg(feature = "server")]
fn config_toml_roundtrips() {
let toml_str = format_config_toml("brynary");
let config: fabro_config::FabroConfig =
let config: fabro_config::FabroSettings =
toml::from_str(&toml_str).expect("config should parse");
assert_eq!(config.web.unwrap().auth.allowed_usernames, vec!["brynary"]);
}
@ -940,7 +940,7 @@ mod tests {
#[cfg(feature = "server")]
fn config_toml_has_auth_strategies() {
let toml_str = format_config_toml("alice");
let config: fabro_config::FabroConfig = toml::from_str(&toml_str).unwrap();
let config: fabro_config::FabroSettings = toml::from_str(&toml_str).unwrap();
assert_eq!(
config.api.unwrap().authentication_strategies,
vec![
@ -955,7 +955,7 @@ mod tests {
fn config_toml_has_tls_paths() {
use std::path::PathBuf;
let toml_str = format_config_toml("bob");
let config: fabro_config::FabroConfig = toml::from_str(&toml_str).unwrap();
let config: fabro_config::FabroSettings = toml::from_str(&toml_str).unwrap();
let tls = config.api.unwrap().tls.expect("tls should be set");
assert_eq!(tls.cert, PathBuf::from("~/.fabro/certs/server.crt"));
assert_eq!(tls.key, PathBuf::from("~/.fabro/certs/server.key"));

View file

@ -1,11 +1,11 @@
use anyhow::Result;
use fabro_config::FabroConfig;
use fabro_config::FabroSettings;
use crate::args::GlobalArgs;
pub async fn execute(
mut args: fabro_llm::cli::ChatArgs,
cli_config: &FabroConfig,
cli_config: &FabroSettings,
globals: &GlobalArgs,
) -> Result<()> {
let llm_defaults = cli_config.llm.as_ref();

View file

@ -1,11 +1,11 @@
use anyhow::Result;
use fabro_config::FabroConfig;
use fabro_config::FabroSettings;
use crate::args::GlobalArgs;
pub async fn execute(
mut args: fabro_llm::cli::PromptArgs,
cli_config: &FabroConfig,
cli_config: &FabroSettings,
globals: &GlobalArgs,
) -> Result<()> {
let llm_defaults = cli_config.llm.as_ref();

View file

@ -1,32 +1,25 @@
use std::path::Path;
use anyhow::bail;
use fabro_config::{FabroConfig, FabroSettings};
use fabro_util::terminal::Styles;
use crate::args::PreflightArgs;
use crate::cli_config;
use super::run::execute::{
apply_execution_overrides, load_workflow_source_input, print_workflow_report,
resolve_sandbox_provider, run_preflight, ExecutionOverrides,
load_workflow_source_input, print_workflow_report, resolve_sandbox_provider, run_preflight,
};
use crate::args::PreflightArgs;
pub async fn execute(mut args: PreflightArgs) -> anyhow::Result<()> {
let styles: &'static Styles = Box::leak(Box::new(Styles::detect_stderr()));
let cli_config = cli_config::load_cli_config(None)?;
let cli_defaults = fabro_config::cli::load_cli_config(None)?;
let cli_config: FabroSettings = cli_defaults.clone().try_into()?;
args.verbose = args.verbose || cli_config.verbose_enabled();
let github_app = crate::shared::github::build_github_app_credentials(cli_config.app_id());
let cli_args_config = FabroConfig::try_from(&args)?;
let run_defaults = cli_config;
let source_input = load_workflow_source_input(
&args.workflow,
args.goal.as_deref(),
args.goal_file.as_deref(),
run_defaults,
true,
)?;
let source_input =
load_workflow_source_input(&args.workflow, cli_args_config, cli_defaults, true)?;
let original_cwd = std::env::current_dir()?;
let (origin_url, detected_base_branch) =
@ -42,22 +35,6 @@ pub async fn execute(mut args: PreflightArgs) -> anyhow::Result<()> {
&source_input.run_defaults,
)?;
let mut config = source_input.config.clone();
apply_execution_overrides(
&mut config,
&ExecutionOverrides {
dry_run: false,
auto_approve: false,
no_retro: false,
verbose: args.verbose,
preserve_sandbox: false,
model: args.model.as_deref(),
provider: args.provider.as_deref(),
sandbox_provider,
storage_dir: None,
},
);
let validated = fabro_workflows::operations::validate(
&source_input.raw_source,
fabro_workflows::operations::ValidateOptions {
@ -68,7 +45,7 @@ pub async fn execute(mut args: PreflightArgs) -> anyhow::Result<()> {
.unwrap_or(Path::new("."))
.to_path_buf(),
),
config: Some(config.clone()),
config: Some(source_input.config.clone()),
goal_override: source_input.goal_override.clone(),
..Default::default()
},
@ -80,7 +57,7 @@ pub async fn execute(mut args: PreflightArgs) -> anyhow::Result<()> {
run_preflight(
validated.graph(),
&Some(config),
&Some(source_input.config),
args.model.as_deref(),
args.provider.as_deref(),
&source_input.run_defaults,

View file

@ -1,14 +1,12 @@
use std::path::PathBuf;
use fabro_config::config::FabroConfig;
use fabro_sandbox::SandboxProvider;
use crate::args::RunArgs;
use fabro_config::FabroConfig;
use super::execute::{
apply_execution_overrides, cached_graph_path, default_run_dir, load_workflow_source_input,
make_run_dir, parse_labels, print_diagnostics_from_error, print_workflow_report_from_persisted,
resolve_sandbox_provider, write_run_config_snapshot, ExecutionOverrides,
cached_graph_path, default_run_dir, load_workflow_source_input, make_run_dir, parse_labels,
print_diagnostics_from_error, print_workflow_report_from_persisted, resolve_sandbox_provider,
write_run_config_snapshot,
};
use fabro_util::terminal::Styles;
@ -17,7 +15,7 @@ use fabro_util::terminal::Styles;
/// This does NOT execute the workflow — it only prepares the run directory.
pub async fn create_run(
args: &RunArgs,
run_defaults: FabroConfig,
cli_defaults: FabroConfig,
styles: &Styles,
quiet: bool,
) -> anyhow::Result<(String, PathBuf)> {
@ -25,18 +23,18 @@ pub async fn create_run(
.workflow
.as_ref()
.ok_or_else(|| anyhow::anyhow!("--workflow is required"))?;
let source_input = load_workflow_source_input(
workflow_path,
args.goal.as_deref(),
args.goal_file.as_deref(),
run_defaults,
true,
)?;
let cli_args_config = FabroConfig::try_from(args)?;
let source_input =
load_workflow_source_input(workflow_path, cli_args_config, cli_defaults, true)?;
let run_id = args
.run_id
.clone()
.unwrap_or_else(|| ulid::Ulid::new().to_string());
let run_dir = match &args.storage_dir {
let run_dir = match args
.storage_dir
.clone()
.or_else(|| source_input.config.storage_dir.clone())
{
Some(sd) => make_run_dir(&sd.join("runs"), &run_id, args.dry_run),
None => default_run_dir(&run_id, args.dry_run),
};
@ -44,31 +42,15 @@ pub async fn create_run(
let base_branch = fabro_sandbox::daytona::detect_repo_info(&working_directory)
.ok()
.and_then(|(_, branch)| branch);
let sandbox_provider = if args.dry_run {
SandboxProvider::Local
} else {
resolve_sandbox_provider(
if !args.dry_run {
let _ = resolve_sandbox_provider(
args.sandbox.map(Into::into),
Some(&source_input.config),
&source_input.run_defaults,
)?
};
)?;
}
let mut config = source_input.config.clone();
apply_execution_overrides(
&mut config,
&ExecutionOverrides {
dry_run: args.dry_run,
auto_approve: args.auto_approve,
no_retro: args.no_retro,
verbose: args.verbose,
preserve_sandbox: args.preserve_sandbox,
model: args.model.as_deref(),
provider: args.provider.as_deref(),
sandbox_provider,
storage_dir: args.storage_dir.as_deref(),
},
);
let config = source_input.config.clone();
let persisted = match fabro_workflows::operations::create(
&source_input.raw_source,

View file

@ -7,8 +7,8 @@ use std::time::Instant;
use anyhow::{bail, Context};
use chrono::Local;
use fabro_agent::{DockerSandbox, DockerSandboxConfig, LocalSandbox, Sandbox};
use fabro_config::config::FabroConfig;
use fabro_config::{project as project_config, run as run_config, sandbox as sandbox_config};
use fabro_config::{FabroConfig, FabroSettings};
use fabro_interview::{AutoApproveInterviewer, ConsoleInterviewer, FileInterviewer, Interviewer};
use fabro_model::{Catalog, FallbackTarget, Provider};
use fabro_sandbox::SandboxProvider;
@ -32,8 +32,7 @@ use tracing::debug;
use super::detached::{DetachedRunBootstrapGuard, DetachedRunCompletionGuard};
use super::run_progress;
use crate::args::{CliSandboxProvider, GlobalArgs, RunArgs};
use crate::cli_config;
use crate::args::{CliSandboxProvider, GlobalArgs, PreflightArgs, RunArgs};
use crate::shared::{
format_tokens_human, print_diagnostics, read_workflow_file, relative_path, tilde_path,
};
@ -58,6 +57,80 @@ pub(crate) fn resolve_cli_goal(
pub(crate) use fabro_workflows::operations::{default_run_dir, make_run_dir};
fn sparse_flag(value: bool) -> Option<bool> {
value.then_some(true)
}
impl TryFrom<&RunArgs> for FabroConfig {
type Error = anyhow::Error;
fn try_from(args: &RunArgs) -> Result<Self, Self::Error> {
let goal = resolve_cli_goal(args.goal.as_deref(), args.goal_file.as_deref())?;
let llm = if args.model.is_some() || args.provider.is_some() {
Some(fabro_config::run::LlmConfig {
model: args.model.clone(),
provider: args.provider.clone(),
fallbacks: None,
})
} else {
None
};
let sandbox = if args.sandbox.is_some() || args.preserve_sandbox {
Some(sandbox_config::SandboxConfig {
provider: args
.sandbox
.map(Into::into)
.map(|provider: SandboxProvider| provider.to_string()),
preserve: sparse_flag(args.preserve_sandbox),
..Default::default()
})
} else {
None
};
Ok(Self {
goal,
llm,
sandbox,
verbose: sparse_flag(args.verbose),
dry_run: sparse_flag(args.dry_run),
auto_approve: sparse_flag(args.auto_approve),
no_retro: sparse_flag(args.no_retro),
storage_dir: args.storage_dir.clone(),
..Default::default()
})
}
}
impl TryFrom<&PreflightArgs> for FabroConfig {
type Error = anyhow::Error;
fn try_from(args: &PreflightArgs) -> Result<Self, Self::Error> {
let goal = resolve_cli_goal(args.goal.as_deref(), args.goal_file.as_deref())?;
let llm = if args.model.is_some() || args.provider.is_some() {
Some(fabro_config::run::LlmConfig {
model: args.model.clone(),
provider: args.provider.clone(),
fallbacks: None,
})
} else {
None
};
let sandbox = args.sandbox.map(|sandbox| sandbox_config::SandboxConfig {
provider: Some(SandboxProvider::from(sandbox).to_string()),
..Default::default()
});
Ok(Self {
goal,
llm,
sandbox,
verbose: sparse_flag(args.verbose),
..Default::default()
})
}
}
pub(crate) fn workflow_slug_from_path(workflow_path: &Path) -> Option<String> {
let file_name = workflow_path.file_name()?.to_string_lossy();
if workflow_path.extension().is_none() {
@ -89,8 +162,8 @@ fn is_cached_run_restart(workflow_path: &Path, run_dir: &Path) -> bool {
pub(crate) fn resolve_model_provider(
cli_model: Option<&str>,
cli_provider: Option<&str>,
run_cfg: Option<&FabroConfig>,
run_defaults: &FabroConfig,
run_cfg: Option<&FabroSettings>,
run_defaults: &FabroSettings,
graph: &fabro_graphviz::graph::Graph,
) -> (String, Option<String>) {
let toml_model = run_cfg
@ -139,7 +212,7 @@ pub(crate) fn resolve_model_provider(
/// Parse sandbox provider from an optional `SandboxConfig`.
pub(crate) fn parse_sandbox_provider(
sandbox: Option<&sandbox_config::SandboxConfig>,
sandbox: Option<&sandbox_config::SandboxSettings>,
) -> anyhow::Result<Option<SandboxProvider>> {
sandbox
.and_then(|s| s.provider.as_deref())
@ -151,8 +224,8 @@ pub(crate) fn parse_sandbox_provider(
/// Resolve sandbox provider: CLI flag > TOML config > run defaults > default.
pub(crate) fn resolve_sandbox_provider(
cli: Option<SandboxProvider>,
run_cfg: Option<&FabroConfig>,
run_defaults: &FabroConfig,
run_cfg: Option<&FabroSettings>,
run_defaults: &FabroSettings,
) -> anyhow::Result<SandboxProvider> {
let toml = parse_sandbox_provider(run_cfg.and_then(|c| c.sandbox.as_ref()))?;
let defaults = parse_sandbox_provider(run_defaults.sandbox.as_ref())?;
@ -162,8 +235,8 @@ pub(crate) fn resolve_sandbox_provider(
/// Resolve preserve-sandbox: CLI flag > TOML config > run defaults > false.
pub(crate) fn resolve_preserve_sandbox(
cli: bool,
run_cfg: Option<&FabroConfig>,
run_defaults: &FabroConfig,
run_cfg: Option<&FabroSettings>,
run_defaults: &FabroSettings,
) -> bool {
if cli {
return true;
@ -177,8 +250,8 @@ pub(crate) fn resolve_preserve_sandbox(
/// Resolve worktree mode: TOML config > run defaults > Clean.
fn resolve_worktree_mode(
run_cfg: Option<&FabroConfig>,
run_defaults: &FabroConfig,
run_cfg: Option<&FabroSettings>,
run_defaults: &FabroSettings,
) -> sandbox_config::WorktreeMode {
run_cfg
.and_then(|c| c.sandbox.as_ref())
@ -196,8 +269,8 @@ fn resolve_worktree_mode(
/// Resolve daytona config: TOML config > run defaults.
pub(crate) fn resolve_daytona_config(
run_cfg: Option<&FabroConfig>,
run_defaults: &FabroConfig,
run_cfg: Option<&FabroSettings>,
run_defaults: &FabroSettings,
) -> Option<fabro_sandbox::daytona::DaytonaConfig> {
run_cfg
.and_then(|c| c.sandbox.as_ref())
@ -213,8 +286,8 @@ pub(crate) fn resolve_daytona_config(
#[cfg(feature = "exedev")]
/// Resolve exe.dev config: TOML config > run defaults.
pub(crate) fn resolve_exe_config(
run_cfg: Option<&FabroConfig>,
run_defaults: &FabroConfig,
run_cfg: Option<&FabroSettings>,
run_defaults: &FabroSettings,
) -> Option<fabro_sandbox::exe::ExeConfig> {
run_cfg
.and_then(|c| c.sandbox.as_ref())
@ -243,8 +316,8 @@ pub(crate) fn resolve_exe_clone_params(
/// Resolve SSH sandbox config: TOML config > run defaults.
pub(crate) fn resolve_ssh_config(
run_cfg: Option<&FabroConfig>,
run_defaults: &FabroConfig,
run_cfg: Option<&FabroSettings>,
run_defaults: &FabroSettings,
) -> Option<fabro_sandbox::ssh::SshConfig> {
run_cfg
.and_then(|c| c.sandbox.as_ref())
@ -270,14 +343,11 @@ pub(crate) fn resolve_ssh_clone_params(
Some(fabro_sandbox::ssh::GitCloneParams { url, branch })
}
/// Resolve the fallback chain from config.
///
/// `merge_overlay` must be called before this — it merges
/// `run_defaults.llm.fallbacks` into `run_cfg.llm.fallbacks` already.
/// Resolve the fallback chain from the effective settings.
pub(crate) fn resolve_fallback_chain(
provider: Provider,
model: &str,
run_cfg: Option<&FabroConfig>,
run_cfg: Option<&FabroSettings>,
) -> Vec<FallbackTarget> {
let fallbacks = run_cfg
.and_then(|c| c.llm.as_ref())
@ -386,41 +456,6 @@ pub(crate) fn resolve_workflow_source(
}
}
pub(crate) struct ExecutionOverrides<'a> {
pub dry_run: bool,
pub auto_approve: bool,
pub no_retro: bool,
pub verbose: bool,
pub preserve_sandbox: bool,
pub model: Option<&'a str>,
pub provider: Option<&'a str>,
pub sandbox_provider: SandboxProvider,
pub storage_dir: Option<&'a Path>,
}
pub(crate) fn apply_execution_overrides(config: &mut FabroConfig, overrides: &ExecutionOverrides) {
config.dry_run = Some(overrides.dry_run);
config.auto_approve = Some(overrides.auto_approve);
config.no_retro = Some(overrides.no_retro);
config.verbose = Some(overrides.verbose);
if let Some(model) = overrides.model {
config.llm.get_or_insert_default().model = Some(model.to_string());
}
if let Some(provider) = overrides.provider {
config.llm.get_or_insert_default().provider = Some(provider.to_string());
}
config.sandbox.get_or_insert_default().provider = Some(overrides.sandbox_provider.to_string());
if overrides.preserve_sandbox {
config.sandbox.get_or_insert_default().preserve = Some(true);
}
if let Some(storage_dir) = overrides.storage_dir {
config.storage_dir = Some(storage_dir.to_path_buf());
}
}
pub(crate) fn parse_labels(labels: &[String]) -> HashMap<String, String> {
labels
.iter()
@ -481,9 +516,9 @@ pub(crate) fn print_diagnostics_from_error(
pub(crate) struct WorkflowSourceInput {
pub raw_source: String,
pub config: FabroConfig,
pub config: FabroSettings,
pub workflow_slug: Option<String>,
pub run_defaults: FabroConfig,
pub run_defaults: FabroSettings,
pub workflow_toml_path: Option<PathBuf>,
pub dot_path: PathBuf,
pub goal_override: Option<String>,
@ -497,33 +532,23 @@ enum WorkflowState {
pub(crate) fn load_workflow_source_input(
workflow: &Path,
goal: Option<&str>,
goal_file: Option<&Path>,
mut run_defaults: FabroConfig,
cli_args_config: FabroConfig,
cli_defaults: FabroConfig,
apply_project_config: bool,
) -> anyhow::Result<WorkflowSourceInput> {
if apply_project_config {
// Apply project-level config overrides (fabro.toml) on top of CLI defaults.
if let Ok(Some((_config_path, project_config))) =
project_config::discover_project_config(&std::env::current_dir().unwrap_or_default())
{
tracing::debug!("Applying run defaults from fabro.toml");
run_defaults.merge_overlay(project_config);
}
}
// Resolve workflow arg, load run config if TOML, merge with defaults.
let (resolved_workflow_path, dot_path, config) = {
let (resolved, dot, cfg) = resolve_workflow_source(workflow)?;
match cfg {
Some(cfg) => {
let mut merged = run_defaults.clone();
merged.merge_overlay(cfg);
(resolved, dot, merged)
}
None => (resolved, dot, run_defaults.clone()),
}
let project_config = if apply_project_config {
project_config::discover_project_config(&std::env::current_dir().unwrap_or_default())?
.map(|(_, config)| config)
} else {
None
};
let (resolved_workflow_path, dot_path, workflow_config) = resolve_workflow_source(workflow)?;
let config = cli_args_config
.combine(workflow_config.unwrap_or_default())
.combine(project_config.unwrap_or_default())
.combine(cli_defaults);
let config: FabroSettings = config.try_into()?;
let workflow_slug = workflow_slug_from_path(&resolved_workflow_path);
if let Some(dir) = config.work_dir.as_deref() {
@ -532,8 +557,7 @@ pub(crate) fn load_workflow_source_input(
}
let raw_source = read_workflow_file(&dot_path)?;
let cli_goal = resolve_cli_goal(goal, goal_file)?;
let goal_override = cli_goal.or_else(|| config.goal.clone()).or_else(|| {
let goal_override = config.goal.clone().or_else(|| {
config
.goal_file
.as_ref()
@ -551,9 +575,9 @@ pub(crate) fn load_workflow_source_input(
Ok(WorkflowSourceInput {
raw_source,
config,
config: config.clone(),
workflow_slug,
run_defaults,
run_defaults: config,
workflow_toml_path,
dot_path,
goal_override,
@ -563,7 +587,7 @@ pub(crate) fn load_workflow_source_input(
/// Pre-prepared run state, used to skip workflow preparation in `run_command_impl`.
struct RecordBasedRun {
workflow: WorkflowState,
run_defaults: FabroConfig,
run_defaults: FabroSettings,
}
/// Execute a workflow run from a saved RunRecord, bypassing workflow preparation.
@ -572,7 +596,7 @@ struct RecordBasedRun {
pub async fn run_from_record(
persisted: Persisted,
_run_dir: PathBuf,
run_defaults: FabroConfig,
run_defaults: FabroSettings,
styles: &'static Styles,
github_app: Option<fabro_github::GitHubAppCredentials>,
git_author: fabro_workflows::git::GitAuthor,
@ -647,7 +671,7 @@ pub async fn run_from_record(
pub async fn resume_from_record(
persisted: Persisted,
_run_dir: PathBuf,
run_defaults: FabroConfig,
run_defaults: FabroSettings,
styles: &'static Styles,
github_app: Option<fabro_github::GitHubAppCredentials>,
git_author: fabro_workflows::git::GitAuthor,
@ -743,12 +767,13 @@ fn ensure_resume_target_is_not_already_successful(run_dir: &Path) -> anyhow::Res
pub async fn execute(mut args: RunArgs, _globals: &GlobalArgs) -> anyhow::Result<()> {
let styles: &'static fabro_util::terminal::Styles =
Box::leak(Box::new(fabro_util::terminal::Styles::detect_stderr()));
let cli_config = cli_config::load_cli_config(None)?;
let cli_defaults = fabro_config::cli::load_cli_config(None)?;
let cli_config: FabroSettings = cli_defaults.clone().try_into()?;
args.verbose = args.verbose || cli_config.verbose_enabled();
let quiet = args.detach;
let _prevent_idle_sleep = cli_config.prevent_idle_sleep_enabled();
let (run_id, run_dir) = super::create::create_run(&args, cli_config, styles, quiet).await?;
let (run_id, run_dir) = super::create::create_run(&args, cli_defaults, styles, quiet).await?;
#[cfg(feature = "sleep_inhibitor")]
let _sleep_guard = fabro_beastie::guard(_prevent_idle_sleep);
@ -797,7 +822,6 @@ async fn run_command_impl(
let auto_approve_flag = args.auto_approve;
let no_retro_flag = args.no_retro;
let verbose_flag = args.verbose;
let preserve_sandbox_flag = args.preserve_sandbox;
let label_vec = args.label.clone();
let run_id = args
.run_id
@ -829,30 +853,7 @@ async fn run_command_impl(
source_input.workflow_toml_path,
),
WorkflowState::Source(source_input) => {
let mut config = source_input.config.clone();
let sandbox_provider = if dry_run_flag {
SandboxProvider::Local
} else {
resolve_sandbox_provider(
args.sandbox.map(Into::into),
Some(&config),
&source_input.run_defaults,
)?
};
apply_execution_overrides(
&mut config,
&ExecutionOverrides {
dry_run: dry_run_flag,
auto_approve: auto_approve_flag,
no_retro: no_retro_flag,
verbose: verbose_flag,
preserve_sandbox: preserve_sandbox_flag,
model: args.model.as_deref(),
provider: args.provider.as_deref(),
sandbox_provider,
storage_dir: args.storage_dir.as_deref(),
},
);
let config = source_input.config.clone();
match fabro_workflows::operations::create(
&source_input.raw_source,
@ -942,7 +943,13 @@ async fn run_command_impl(
// Now resolve ${env.VARNAME} references for runtime use.
if let Some(ref mut cfg) = run_cfg {
run_config::resolve_sandbox_env(cfg)?;
if let Some(env) = cfg
.sandbox
.as_mut()
.and_then(|sandbox| sandbox.env.as_mut())
{
run_config::resolve_env_refs(env)?;
}
}
// Create progress UI (used for both normal and verbose modes)
@ -1448,10 +1455,10 @@ pub(crate) fn print_assets(run_dir: &std::path::Path, styles: &Styles) {
#[allow(clippy::too_many_arguments)]
pub(crate) async fn run_preflight(
graph: &fabro_graphviz::graph::Graph,
run_cfg: &Option<FabroConfig>,
run_cfg: &Option<FabroSettings>,
cli_model: Option<&str>,
cli_provider: Option<&str>,
run_defaults: &FabroConfig,
run_defaults: &FabroSettings,
git_status: GitSyncStatus,
sandbox_provider: SandboxProvider,
styles: &'static Styles,
@ -1927,9 +1934,13 @@ include = ["*.md"]
let workflow_path = dir.path().join("workflow.toml");
let source_input =
load_workflow_source_input(&workflow_path, None, None, FabroConfig::default(), false)
.unwrap();
let source_input = load_workflow_source_input(
&workflow_path,
FabroConfig::default(),
FabroConfig::default(),
false,
)
.unwrap();
let validated = fabro_workflows::operations::validate(
&source_input.raw_source,
fabro_workflows::operations::ValidateOptions {
@ -2007,7 +2018,7 @@ include = ["*.md"]
#[test]
fn resolve_model_provider_defaults() {
let graph = fabro_graphviz::graph::Graph::new("test");
let defaults = FabroConfig::default();
let defaults = FabroSettings::default();
let (model, provider) = resolve_model_provider(None, None, None, &defaults, &graph);
assert_eq!(model, "claude-sonnet-4-6");
// Catalog resolves anthropic as the provider for claude-sonnet-4-6
@ -2017,12 +2028,12 @@ include = ["*.md"]
#[test]
fn resolve_model_provider_cli_overrides_toml() {
let graph = fabro_graphviz::graph::Graph::new("test");
let defaults = FabroConfig::default();
let cfg = FabroConfig {
let defaults = FabroSettings::default();
let cfg = FabroSettings {
version: Some(1),
goal: Some("test".to_string()),
graph: Some("test.fabro".to_string()),
llm: Some(run_config::LlmConfig {
llm: Some(run_config::LlmSettings {
model: Some("toml-model".to_string()),
provider: Some("openai".to_string()),
fallbacks: None,
@ -2053,12 +2064,12 @@ include = ["*.md"]
AttrValue::String("gemini".to_string()),
);
let defaults = FabroConfig::default();
let cfg = FabroConfig {
let defaults = FabroSettings::default();
let cfg = FabroSettings {
version: Some(1),
goal: Some("test".to_string()),
graph: Some("test.fabro".to_string()),
llm: Some(run_config::LlmConfig {
llm: Some(run_config::LlmSettings {
model: Some("toml-model".to_string()),
provider: Some("openai".to_string()),
fallbacks: None,
@ -2083,7 +2094,7 @@ include = ["*.md"]
AttrValue::String("openai".to_string()),
);
let defaults = FabroConfig::default();
let defaults = FabroSettings::default();
let (model, provider) = resolve_model_provider(None, None, None, &defaults, &graph);
assert_eq!(model, "gpt-5.2");
assert_eq!(provider, Some("openai".to_string()));
@ -2092,7 +2103,7 @@ include = ["*.md"]
#[test]
fn resolve_model_provider_alias_expansion() {
let graph = fabro_graphviz::graph::Graph::new("test");
let defaults = FabroConfig::default();
let defaults = FabroSettings::default();
let (model, provider) = resolve_model_provider(Some("opus"), None, None, &defaults, &graph);
assert_eq!(model, "claude-opus-4-6");
assert_eq!(provider, Some("anthropic".to_string()));
@ -2101,13 +2112,13 @@ include = ["*.md"]
#[test]
fn resolve_model_provider_run_defaults_used() {
let graph = fabro_graphviz::graph::Graph::new("test");
let defaults = FabroConfig {
llm: Some(run_config::LlmConfig {
let defaults = FabroSettings {
llm: Some(run_config::LlmSettings {
model: Some("default-model".to_string()),
provider: Some("openai".to_string()),
fallbacks: None,
}),
..FabroConfig::default()
..FabroSettings::default()
};
let (model, provider) = resolve_model_provider(None, None, None, &defaults, &graph);
assert_eq!(model, "default-model");
@ -2117,19 +2128,19 @@ include = ["*.md"]
#[test]
fn resolve_model_provider_toml_overrides_run_defaults() {
let graph = fabro_graphviz::graph::Graph::new("test");
let defaults = FabroConfig {
llm: Some(run_config::LlmConfig {
let defaults = FabroSettings {
llm: Some(run_config::LlmSettings {
model: Some("default-model".to_string()),
provider: Some("anthropic".to_string()),
fallbacks: None,
}),
..FabroConfig::default()
..FabroSettings::default()
};
let cfg = FabroConfig {
let cfg = FabroSettings {
version: Some(1),
goal: Some("test".to_string()),
graph: Some("test.fabro".to_string()),
llm: Some(run_config::LlmConfig {
llm: Some(run_config::LlmSettings {
model: Some("toml-model".to_string()),
provider: Some("openai".to_string()),
fallbacks: None,
@ -2143,61 +2154,61 @@ include = ["*.md"]
#[test]
fn resolve_preserve_sandbox_cli_wins() {
let cfg = FabroConfig {
sandbox: Some(sandbox_config::SandboxConfig {
let cfg = FabroSettings {
sandbox: Some(sandbox_config::SandboxSettings {
provider: None,
preserve: Some(false),
..Default::default()
}),
..Default::default()
};
let defaults = FabroConfig::default();
let defaults = FabroSettings::default();
assert!(resolve_preserve_sandbox(true, Some(&cfg), &defaults));
}
#[test]
fn resolve_preserve_sandbox_toml_wins_over_defaults() {
let cfg = FabroConfig {
sandbox: Some(sandbox_config::SandboxConfig {
let cfg = FabroSettings {
sandbox: Some(sandbox_config::SandboxSettings {
provider: None,
preserve: Some(true),
..Default::default()
}),
..Default::default()
};
let defaults = FabroConfig {
sandbox: Some(sandbox_config::SandboxConfig {
let defaults = FabroSettings {
sandbox: Some(sandbox_config::SandboxSettings {
provider: None,
preserve: Some(false),
..Default::default()
}),
..FabroConfig::default()
..FabroSettings::default()
};
assert!(resolve_preserve_sandbox(false, Some(&cfg), &defaults));
}
#[test]
fn resolve_preserve_sandbox_defaults_used() {
let defaults = FabroConfig {
sandbox: Some(sandbox_config::SandboxConfig {
let defaults = FabroSettings {
sandbox: Some(sandbox_config::SandboxSettings {
provider: None,
preserve: Some(true),
..Default::default()
}),
..FabroConfig::default()
..FabroSettings::default()
};
assert!(resolve_preserve_sandbox(false, None, &defaults));
}
#[test]
fn resolve_preserve_sandbox_defaults_to_false() {
let defaults = FabroConfig::default();
let defaults = FabroSettings::default();
assert!(!resolve_preserve_sandbox(false, None, &defaults));
}
#[test]
fn resolve_worktree_mode_defaults_to_clean() {
let defaults = FabroConfig::default();
let defaults = FabroSettings::default();
assert_eq!(
resolve_worktree_mode(None, &defaults),
sandbox_config::WorktreeMode::Clean
@ -2206,16 +2217,16 @@ include = ["*.md"]
#[test]
fn resolve_worktree_mode_from_toml() {
let cfg = FabroConfig {
sandbox: Some(sandbox_config::SandboxConfig {
local: Some(sandbox_config::LocalSandboxConfig {
let cfg = FabroSettings {
sandbox: Some(sandbox_config::SandboxSettings {
local: Some(sandbox_config::LocalSandboxSettings {
worktree_mode: sandbox_config::WorktreeMode::Always,
}),
..Default::default()
}),
..Default::default()
};
let defaults = FabroConfig::default();
let defaults = FabroSettings::default();
assert_eq!(
resolve_worktree_mode(Some(&cfg), &defaults),
sandbox_config::WorktreeMode::Always
@ -2224,17 +2235,17 @@ include = ["*.md"]
#[test]
fn resolve_worktree_mode_from_defaults() {
let defaults = FabroConfig {
sandbox: Some(sandbox_config::SandboxConfig {
let defaults = FabroSettings {
sandbox: Some(sandbox_config::SandboxSettings {
provider: None,
preserve: None,
devcontainer: None,
local: Some(sandbox_config::LocalSandboxConfig {
local: Some(sandbox_config::LocalSandboxSettings {
worktree_mode: sandbox_config::WorktreeMode::Dirty,
}),
..Default::default()
}),
..FabroConfig::default()
..FabroSettings::default()
};
assert_eq!(
resolve_worktree_mode(None, &defaults),
@ -2244,26 +2255,26 @@ include = ["*.md"]
#[test]
fn resolve_worktree_mode_toml_overrides_defaults() {
let cfg = FabroConfig {
sandbox: Some(sandbox_config::SandboxConfig {
local: Some(sandbox_config::LocalSandboxConfig {
let cfg = FabroSettings {
sandbox: Some(sandbox_config::SandboxSettings {
local: Some(sandbox_config::LocalSandboxSettings {
worktree_mode: sandbox_config::WorktreeMode::Never,
}),
..Default::default()
}),
..Default::default()
};
let defaults = FabroConfig {
sandbox: Some(sandbox_config::SandboxConfig {
let defaults = FabroSettings {
sandbox: Some(sandbox_config::SandboxSettings {
provider: None,
preserve: None,
devcontainer: None,
local: Some(sandbox_config::LocalSandboxConfig {
local: Some(sandbox_config::LocalSandboxSettings {
worktree_mode: sandbox_config::WorktreeMode::Dirty,
}),
..Default::default()
}),
..FabroConfig::default()
..FabroSettings::default()
};
assert_eq!(
resolve_worktree_mode(Some(&cfg), &defaults),

View file

@ -24,7 +24,7 @@ pub async fn dispatch(cmd: RunCommands, globals: &GlobalArgs) -> Result<()> {
RunCommands::Create(args) => {
let styles: &'static fabro_util::terminal::Styles =
Box::leak(Box::new(fabro_util::terminal::Styles::detect_stderr()));
let cli_config = crate::cli_config::load_cli_config(None)?;
let cli_config = fabro_config::cli::load_cli_config(None)?;
let (run_id, _run_dir) = create::create_run(&args, cli_config, styles, true).await?;
println!("{run_id}");
Ok(())

View file

@ -119,7 +119,7 @@ fn kill_child_best_effort(child: &mut std::process::Child) {
mod tests {
use super::*;
use chrono::Utc;
use fabro_config::config::FabroConfig;
use fabro_config::FabroSettings;
use fabro_graphviz::graph::Graph;
use fabro_workflows::records::RunRecord;
use fabro_workflows::run_status::{write_run_status, RunStatus, RunStatusRecord, StatusReason};
@ -130,7 +130,7 @@ mod tests {
RunRecord {
run_id: "run-test123".to_string(),
created_at: Utc::now(),
config: FabroConfig::default(),
config: FabroSettings::default(),
graph: Graph {
name: "test".to_string(),
..Default::default()

View file

@ -98,14 +98,18 @@ async fn main_inner() -> (String, Result<()>) {
{
if let Commands::Serve(args) = command.as_ref() {
match fabro_config::server::load_server_config(args.config.as_deref()) {
Ok(server_config) => (
server_config.log.as_ref().and_then(|l| l.level.clone()),
false,
),
Ok(server_config) => match fabro_config::FabroSettings::try_from(server_config)
{
Ok(server_settings) => (
server_settings.log.as_ref().and_then(|l| l.level.clone()),
false,
),
Err(err) => return (command_name, Err(err)),
},
Err(err) => return (command_name, Err(err)),
}
} else {
match fabro_config::cli::load_cli_config(None) {
match crate::cli_config::load_cli_config(None) {
Ok(cli_config) => (
cli_config.log.as_ref().and_then(|l| l.level.clone()),
cli_config.upgrade_check_enabled(),
@ -116,7 +120,7 @@ async fn main_inner() -> (String, Result<()>) {
}
#[cfg(not(feature = "server"))]
{
match fabro_config::cli::load_cli_config(None) {
match crate::cli_config::load_cli_config(None) {
Ok(cli_config) => (
cli_config.log.as_ref().and_then(|l| l.level.clone()),
cli_config.upgrade_check_enabled(),

View file

@ -1,6 +1,6 @@
use assert_cmd::Command;
use fabro_config::mcp::McpTransport;
use fabro_config::FabroConfig;
use fabro_config::FabroSettings;
use predicates::prelude::*;
#[allow(deprecated)]
@ -10,8 +10,8 @@ fn arc() -> Command {
cmd
}
fn parse_config_show(stdout: &[u8]) -> FabroConfig {
serde_yaml::from_slice(stdout).expect("stdout should be valid YAML FabroConfig")
fn parse_config_show(stdout: &[u8]) -> FabroSettings {
serde_yaml::from_slice(stdout).expect("stdout should be valid YAML FabroSettings")
}
fn setup_config_show_fixture() -> (tempfile::TempDir, tempfile::TempDir) {

View file

@ -0,0 +1,14 @@
[package]
name = "fabro-config-derive"
edition.workspace = true
version.workspace = true
license.workspace = true
description = "Derive macros for fabro-config"
[lib]
proc-macro = true
[dependencies]
proc-macro2 = "1"
quote = "1"
syn = { version = "2", features = ["full"] }

View file

@ -0,0 +1,52 @@
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, Data, DeriveInput, Fields};
#[proc_macro_derive(Combine)]
pub fn derive_combine(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let ident = input.ident;
let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
let body = match input.data {
Data::Struct(data) => match data.fields {
Fields::Named(fields) => {
let combined = fields.named.into_iter().map(|field| {
let ident = field.ident.expect("named field");
quote! {
#ident: ::fabro_config::combine::Combine::combine(self.#ident, other.#ident)
}
});
quote! {
Self {
#(#combined,)*
}
}
}
Fields::Unnamed(fields) => {
let combined = fields.unnamed.iter().enumerate().map(|(index, _)| {
let index = syn::Index::from(index);
quote! {
::fabro_config::combine::Combine::combine(self.#index, other.#index)
}
});
quote! {
Self(#(#combined),*)
}
}
Fields::Unit => quote!(Self),
},
Data::Enum(_) | Data::Union(_) => {
quote!(self)
}
};
quote! {
impl #impl_generics ::fabro_config::combine::Combine for #ident #ty_generics #where_clause {
fn combine(self, other: Self) -> Self {
#body
}
}
}
.into()
}

View file

@ -16,6 +16,7 @@ clap = ["dep:clap"]
[dependencies]
anyhow.workspace = true
clap = { workspace = true, optional = true }
fabro-config-derive = { path = "../fabro-config-derive" }
fabro-util = { path = "../fabro-util" }
dirs.workspace = true
serde.workspace = true

View file

@ -1,8 +1,9 @@
use std::path::{Path, PathBuf};
use anyhow::anyhow;
use serde::{Deserialize, Serialize};
#[derive(Clone, Copy, Debug, PartialEq, Deserialize, Serialize)]
#[derive(Clone, Copy, Debug, PartialEq, Deserialize, Serialize, crate::Combine)]
#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
#[serde(rename_all = "kebab-case")]
pub enum OutputFormat {
@ -10,7 +11,7 @@ pub enum OutputFormat {
Json,
}
#[derive(Clone, Copy, Debug, PartialEq, Deserialize, Serialize)]
#[derive(Clone, Copy, Debug, PartialEq, Deserialize, Serialize, crate::Combine)]
#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
#[serde(rename_all = "kebab-case")]
pub enum PermissionLevel {
@ -19,7 +20,7 @@ pub enum PermissionLevel {
Full,
}
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, crate::Combine)]
#[serde(rename_all = "lowercase")]
pub enum ExecutionMode {
#[default]
@ -27,406 +28,90 @@ pub enum ExecutionMode {
Server,
}
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, crate::Combine)]
pub struct ClientTlsConfig {
pub cert: Option<PathBuf>,
pub key: Option<PathBuf>,
pub ca: Option<PathBuf>,
}
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
pub struct ClientTlsSettings {
pub cert: PathBuf,
pub key: PathBuf,
pub ca: PathBuf,
}
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
pub struct ServerDefaults {
impl TryFrom<ClientTlsConfig> for ClientTlsSettings {
type Error = anyhow::Error;
fn try_from(value: ClientTlsConfig) -> Result<Self, Self::Error> {
Ok(Self {
cert: value.cert.ok_or_else(|| {
anyhow!("server.tls.cert is required when server.tls is configured")
})?,
key: value.key.ok_or_else(|| {
anyhow!("server.tls.key is required when server.tls is configured")
})?,
ca: value.ca.ok_or_else(|| {
anyhow!("server.tls.ca is required when server.tls is configured")
})?,
})
}
}
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, crate::Combine)]
pub struct ServerConfig {
pub base_url: Option<String>,
pub tls: Option<ClientTlsConfig>,
}
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
pub struct ExecDefaults {
pub struct ServerSettings {
pub base_url: Option<String>,
pub tls: Option<ClientTlsSettings>,
}
impl TryFrom<ServerConfig> for ServerSettings {
type Error = anyhow::Error;
fn try_from(value: ServerConfig) -> Result<Self, Self::Error> {
Ok(Self {
base_url: value.base_url,
tls: value.tls.map(TryInto::try_into).transpose()?,
})
}
}
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, crate::Combine)]
pub struct ExecConfig {
pub provider: Option<String>,
pub model: Option<String>,
pub permissions: Option<PermissionLevel>,
pub output_format: Option<OutputFormat>,
}
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
pub struct ExecSettings {
pub provider: Option<String>,
pub model: Option<String>,
pub permissions: Option<PermissionLevel>,
pub output_format: Option<OutputFormat>,
}
impl From<ExecConfig> for ExecSettings {
fn from(value: ExecConfig) -> Self {
Self {
provider: value.provider,
model: value.model,
permissions: value.permissions,
output_format: value.output_format,
}
}
}
/// Load CLI config from an explicit path or `~/.fabro/cli.toml`, returning defaults if the
/// default file doesn't exist. An explicit path that doesn't exist is an error.
pub fn load_cli_config(path: Option<&Path>) -> anyhow::Result<crate::config::FabroConfig> {
crate::load_config_file(path, "cli.toml")
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::FabroConfig;
use crate::mcp::{McpServerEntry, McpTransport};
use std::collections::HashMap;
#[test]
fn parse_empty_config_defaults() {
let config: FabroConfig = toml::from_str("").unwrap();
assert_eq!(config, FabroConfig::default());
}
#[test]
fn parse_full_config() {
let toml = r#"
[exec]
provider = "anthropic"
model = "claude-opus-4-6"
permissions = "read-write"
output_format = "text"
[llm]
model = "claude-sonnet-4-5"
"#;
let config: FabroConfig = toml::from_str(toml).unwrap();
let exec = config.exec.unwrap();
assert_eq!(exec.provider.as_deref(), Some("anthropic"));
assert_eq!(exec.model.as_deref(), Some("claude-opus-4-6"));
assert_eq!(exec.permissions, Some(PermissionLevel::ReadWrite));
assert_eq!(exec.output_format, Some(OutputFormat::Text));
let llm = config.llm.unwrap();
assert_eq!(llm.model.as_deref(), Some("claude-sonnet-4-5"));
}
#[test]
fn parse_partial_exec_config() {
let toml = r#"
[exec]
provider = "openai"
"#;
let config: FabroConfig = toml::from_str(toml).unwrap();
let exec = config.exec.unwrap();
assert_eq!(exec.provider.as_deref(), Some("openai"));
assert_eq!(exec.model, None);
assert_eq!(exec.permissions, None);
assert_eq!(exec.output_format, None);
assert_eq!(config.llm, None);
}
#[test]
fn load_cli_config_from_explicit_path() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("custom.toml");
std::fs::write(
&path,
r#"
[exec]
provider = "gemini"
model = "gemini-pro"
"#,
)
.unwrap();
let config = load_cli_config(Some(&path)).unwrap();
let exec = config.exec.unwrap();
assert_eq!(exec.provider.as_deref(), Some("gemini"));
assert_eq!(exec.model.as_deref(), Some("gemini-pro"));
}
#[test]
fn load_cli_config_explicit_path_missing_is_error() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("nonexistent.toml");
let result = load_cli_config(Some(&path));
assert!(result.is_err());
}
#[test]
fn parse_mode_server() {
let toml = r#"mode = "server""#;
let config: FabroConfig = toml::from_str(toml).unwrap();
assert_eq!(config.mode, Some(ExecutionMode::Server));
}
#[test]
fn parse_mode_standalone() {
let toml = r#"mode = "standalone""#;
let config: FabroConfig = toml::from_str(toml).unwrap();
assert_eq!(config.mode, Some(ExecutionMode::Standalone));
}
#[test]
fn parse_mode_absent() {
let config: FabroConfig = toml::from_str("").unwrap();
assert_eq!(config.mode, None);
}
#[test]
fn parse_server_base_url() {
let toml = r#"
[server]
base_url = "https://arc.example.com:3000"
"#;
let config: FabroConfig = toml::from_str(toml).unwrap();
let server = config.server.unwrap();
assert_eq!(
server.base_url.as_deref(),
Some("https://arc.example.com:3000")
);
assert_eq!(server.tls, None);
}
#[test]
fn parse_server_tls() {
let toml = r#"
[server]
base_url = "https://arc.example.com:3000"
[server.tls]
cert = "~/.fabro/tls/client.crt"
key = "~/.fabro/tls/client.key"
ca = "~/.fabro/tls/ca.crt"
"#;
let config: FabroConfig = toml::from_str(toml).unwrap();
let tls = config.server.unwrap().tls.unwrap();
assert_eq!(tls.cert, PathBuf::from("~/.fabro/tls/client.crt"));
assert_eq!(tls.key, PathBuf::from("~/.fabro/tls/client.key"));
assert_eq!(tls.ca, PathBuf::from("~/.fabro/tls/ca.crt"));
}
#[test]
fn parse_git_author_config() {
let toml = r#"
[git.author]
name = "my-arc"
email = "me@local"
"#;
let config: FabroConfig = toml::from_str(toml).unwrap();
let git = config.git.unwrap();
assert_eq!(git.author.name.as_deref(), Some("my-arc"));
assert_eq!(git.author.email.as_deref(), Some("me@local"));
}
#[test]
fn parse_git_author_absent() {
let config: FabroConfig = toml::from_str("").unwrap();
assert_eq!(config.git, None);
}
#[test]
fn parse_prevent_idle_sleep_true() {
let config: FabroConfig = toml::from_str("prevent_idle_sleep = true").unwrap();
assert_eq!(config.prevent_idle_sleep, Some(true));
}
#[test]
fn parse_prevent_idle_sleep_defaults_to_none() {
let config: FabroConfig = toml::from_str("").unwrap();
assert_eq!(config.prevent_idle_sleep, None);
}
#[test]
fn parse_verbose_true() {
let config: FabroConfig = toml::from_str("verbose = true").unwrap();
assert_eq!(config.verbose, Some(true));
}
#[test]
fn parse_log_level() {
let toml = "[log]\nlevel = \"debug\"";
let config: FabroConfig = toml::from_str(toml).unwrap();
assert_eq!(
config.log.as_ref().and_then(|l| l.level.as_deref()),
Some("debug")
);
}
#[test]
fn parse_pull_request_enabled() {
let toml = r#"
[pull_request]
enabled = true
"#;
let config: FabroConfig = toml::from_str(toml).unwrap();
let pr = config.pull_request.unwrap();
assert!(pr.enabled);
}
#[test]
fn parse_pull_request_absent() {
let config: FabroConfig = toml::from_str("").unwrap();
assert_eq!(config.pull_request, None);
}
#[test]
fn parse_git_config_with_app_id() {
let toml = r#"
[git]
app_id = "12345"
slug = "my-app"
[git.author]
name = "fabro-bot"
email = "arc@test.com"
"#;
let config: FabroConfig = toml::from_str(toml).unwrap();
let git = config.git.unwrap();
assert_eq!(git.app_id.as_deref(), Some("12345"));
assert_eq!(git.slug.as_deref(), Some("my-app"));
assert_eq!(git.author.name.as_deref(), Some("fabro-bot"));
}
#[test]
fn parse_git_config_with_client_id() {
let toml = r#"
[git]
app_id = "12345"
slug = "my-app"
client_id = "Iv1.abc123"
"#;
let config: FabroConfig = toml::from_str(toml).unwrap();
assert_eq!(config.client_id(), Some("Iv1.abc123"));
let git = config.git.unwrap();
assert_eq!(git.client_id.as_deref(), Some("Iv1.abc123"));
}
#[test]
fn parse_llm_with_provider_and_fallbacks() {
let toml = r#"
[llm]
model = "claude-sonnet-4-5"
provider = "anthropic"
"#;
let config: FabroConfig = toml::from_str(toml).unwrap();
let llm = config.llm.unwrap();
assert_eq!(llm.model.as_deref(), Some("claude-sonnet-4-5"));
assert_eq!(llm.provider.as_deref(), Some("anthropic"));
}
#[test]
fn parse_sandbox_config() {
let toml = r#"
[sandbox]
provider = "daytona"
"#;
let config: FabroConfig = toml::from_str(toml).unwrap();
let sandbox = config.sandbox.unwrap();
assert_eq!(sandbox.provider.as_deref(), Some("daytona"));
}
#[test]
fn parse_mcp_stdio_server_with_env_and_timeouts() {
let toml = r#"
[mcp_servers.filesystem]
type = "stdio"
command = ["npx", "-y", "@modelcontextprotocol/server-filesystem", "/workspace"]
startup_timeout_secs = 15
tool_timeout_secs = 90
[mcp_servers.filesystem.env]
NODE_ENV = "production"
"#;
let config: FabroConfig = toml::from_str(toml).unwrap();
assert_eq!(config.mcp_servers.len(), 1);
let entry = &config.mcp_servers["filesystem"];
assert_eq!(entry.startup_timeout_secs, 15);
assert_eq!(entry.tool_timeout_secs, 90);
match &entry.transport {
McpTransport::Stdio { command, env } => {
assert_eq!(
command,
&[
"npx",
"-y",
"@modelcontextprotocol/server-filesystem",
"/workspace"
]
);
assert_eq!(env.get("NODE_ENV").unwrap(), "production");
}
_ => panic!("expected Stdio transport"),
}
}
#[test]
fn parse_mcp_http_server_with_headers() {
let toml = r#"
[mcp_servers.sentry]
type = "http"
url = "https://mcp.sentry.dev/mcp"
[mcp_servers.sentry.headers]
Authorization = "Bearer sk-xxx"
"#;
let config: FabroConfig = toml::from_str(toml).unwrap();
assert_eq!(config.mcp_servers.len(), 1);
let entry = &config.mcp_servers["sentry"];
match &entry.transport {
McpTransport::Http { url, headers } => {
assert_eq!(url, "https://mcp.sentry.dev/mcp");
assert_eq!(headers.get("Authorization").unwrap(), "Bearer sk-xxx");
}
_ => panic!("expected Http transport"),
}
}
#[test]
fn parse_mcp_empty_backward_compat() {
let config: FabroConfig = toml::from_str("").unwrap();
assert!(config.mcp_servers.is_empty());
}
#[test]
fn parse_mcp_both_transports() {
let toml = r#"
[mcp_servers.local]
type = "stdio"
command = ["python3", "server.py"]
[mcp_servers.remote]
type = "http"
url = "https://mcp.example.com"
"#;
let config: FabroConfig = toml::from_str(toml).unwrap();
assert_eq!(config.mcp_servers.len(), 2);
assert!(matches!(
config.mcp_servers["local"].transport,
McpTransport::Stdio { .. }
));
assert!(matches!(
config.mcp_servers["remote"].transport,
McpTransport::Http { .. }
));
}
#[test]
fn parse_mcp_defaults_applied_when_timeouts_omitted() {
let toml = r#"
[mcp_servers.minimal]
type = "stdio"
command = ["echo"]
"#;
let config: FabroConfig = toml::from_str(toml).unwrap();
let entry = &config.mcp_servers["minimal"];
assert_eq!(entry.startup_timeout_secs, 10);
assert_eq!(entry.tool_timeout_secs, 60);
}
#[test]
fn mcp_server_entry_into_config() {
let entry = McpServerEntry {
transport: McpTransport::Stdio {
command: vec!["node".into(), "server.js".into()],
env: HashMap::new(),
},
startup_timeout_secs: 15,
tool_timeout_secs: 90,
};
let config = entry.into_config("my-server".into());
assert_eq!(config.name, "my-server");
assert_eq!(config.startup_timeout_secs, 15);
assert_eq!(config.tool_timeout_secs, 90);
}
#[test]
fn parse_upgrade_check_false() {
let config: FabroConfig = toml::from_str("upgrade_check = false").unwrap();
assert_eq!(config.upgrade_check, Some(false));
}
#[test]
fn parse_upgrade_check_default_none() {
let config: FabroConfig = toml::from_str("").unwrap();
assert_eq!(config.upgrade_check, None);
}
}

View file

@ -0,0 +1,60 @@
use std::collections::HashMap;
use std::hash::Hash;
use std::path::PathBuf;
pub trait Combine {
fn combine(self, other: Self) -> Self;
}
impl<T: Combine> Combine for Option<T> {
fn combine(self, other: Self) -> Self {
match (self, other) {
(Some(this), Some(other)) => Some(this.combine(other)),
(Some(this), None) => Some(this),
(None, Some(other)) => Some(other),
(None, None) => None,
}
}
}
impl<T> Combine for Vec<T> {
fn combine(mut self, other: Self) -> Self {
self.extend(other);
self
}
}
impl<K, V> Combine for HashMap<K, V>
where
K: Eq + Hash,
V: Combine,
{
fn combine(mut self, other: Self) -> Self {
for (key, value) in other {
match self.remove(&key) {
Some(existing) => {
self.insert(key, existing.combine(value));
}
None => {
self.insert(key, value);
}
}
}
self
}
}
macro_rules! impl_left_wins {
($($ty:ty),* $(,)?) => {
$(
impl Combine for $ty {
fn combine(self, _other: Self) -> Self {
self
}
}
)*
};
}
impl_left_wins!(bool, i32, u16, u32, u64, usize, String, PathBuf,);

View file

@ -3,7 +3,8 @@ use std::path::PathBuf;
use serde::{Deserialize, Serialize};
use crate::cli::{ExecDefaults, ExecutionMode, ServerDefaults};
use crate::cli::{ExecConfig, ExecutionMode, ServerConfig};
use crate::combine::Combine;
use crate::hook::{HookConfig, HookDefinition};
use crate::mcp::McpServerEntry;
use crate::project::ProjectFabroConfig;
@ -12,16 +13,17 @@ use crate::run::{
};
use crate::sandbox::SandboxConfig;
use crate::server::{ApiConfig, Features, GitConfig, LogConfig, WebConfig};
use crate::settings::FabroSettings;
fn is_default_checkpoint(c: &CheckpointConfig) -> bool {
c.exclude_globs.is_empty()
}
/// Unified configuration type for all Fabro config sources.
/// Unified sparse configuration type for all Fabro config sources.
///
/// Loading functions (`load_cli_config`, `load_server_config`, `load_run_config`,
/// `parse_project_config`) all return this type. Fields irrelevant to a
/// particular source are left at their defaults (None / empty).
/// particular source are left unset (`None` / empty).
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
pub struct FabroConfig {
// --- Workflow run config fields ---
@ -79,10 +81,10 @@ pub struct FabroConfig {
pub mode: Option<ExecutionMode>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub server: Option<ServerDefaults>,
pub server: Option<ServerConfig>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub exec: Option<ExecDefaults>,
pub exec: Option<ExecConfig>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub prevent_idle_sleep: Option<bool>,
@ -130,268 +132,62 @@ pub struct FabroConfig {
pub fabro: Option<ProjectFabroConfig>,
}
impl FabroConfig {
// --- Convenience methods (ported from CliConfig) ---
impl Combine for FabroConfig {
fn combine(self, other: Self) -> Self {
let hooks = if self.hooks.is_empty() {
other.hooks
} else if other.hooks.is_empty() {
self.hooks
} else {
HookConfig { hooks: self.hooks }
.merge(HookConfig { hooks: other.hooks })
.hooks
};
/// Resolve the storage directory: config value > default `~/.fabro`.
pub fn storage_dir(&self) -> PathBuf {
self.storage_dir.clone().unwrap_or_else(|| {
dirs::home_dir()
.expect("could not determine home directory")
.join(".fabro")
})
}
pub fn app_id(&self) -> Option<&str> {
self.git.as_ref().and_then(|g| g.app_id.as_deref())
}
pub fn slug(&self) -> Option<&str> {
self.git.as_ref().and_then(|g| g.slug.as_deref())
}
pub fn client_id(&self) -> Option<&str> {
self.git.as_ref().and_then(|g| g.client_id.as_deref())
}
pub fn git_author(&self) -> Option<&crate::server::GitAuthorConfig> {
self.git.as_ref().map(|g| &g.author)
}
pub fn verbose_enabled(&self) -> bool {
self.verbose.unwrap_or(false)
}
pub fn prevent_idle_sleep_enabled(&self) -> bool {
self.prevent_idle_sleep.unwrap_or(false)
}
pub fn upgrade_check_enabled(&self) -> bool {
self.upgrade_check.unwrap_or(true)
}
pub fn dry_run_enabled(&self) -> bool {
self.dry_run.unwrap_or(false)
}
pub fn auto_approve_enabled(&self) -> bool {
self.auto_approve.unwrap_or(false)
}
pub fn no_retro_enabled(&self) -> bool {
self.no_retro.unwrap_or(false)
}
/// Merge an overlay on top of this base. The overlay takes precedence
/// for simple fields; compound fields (vars, hooks, mcp_servers) are
/// deep-merged with the overlay winning on collision.
pub fn merge_overlay(&mut self, overlay: FabroConfig) {
// --- Workflow run config fields ---
if overlay.version.is_some() {
self.version = overlay.version;
}
if overlay.goal.is_some() {
self.goal = overlay.goal;
}
if overlay.goal_file.is_some() {
self.goal_file = overlay.goal_file;
}
if overlay.graph.is_some() {
self.graph = overlay.graph;
}
if !overlay.labels.is_empty() {
let mut merged = std::mem::take(&mut self.labels);
merged.extend(overlay.labels);
self.labels = merged;
}
// --- Run defaults fields ---
if overlay.work_dir.is_some() {
self.work_dir = overlay.work_dir;
}
match (&mut self.llm, overlay.llm) {
(Some(base), Some(over)) => {
if over.model.is_some() {
base.model = over.model;
}
if over.provider.is_some() {
base.provider = over.provider;
}
if over.fallbacks.is_some() {
base.fallbacks = over.fallbacks;
}
}
(None, Some(over)) => self.llm = Some(over),
_ => {}
}
match (&mut self.setup, overlay.setup) {
(Some(base), Some(over)) => {
if over.timeout_ms.is_some() {
base.timeout_ms = over.timeout_ms;
}
}
(None, Some(over)) => self.setup = Some(over),
_ => {}
}
match (&mut self.sandbox, overlay.sandbox) {
(Some(base), Some(over)) => {
if over.provider.is_some() {
base.provider = over.provider;
}
if over.preserve.is_some() {
base.preserve = over.preserve;
}
if over.devcontainer.is_some() {
base.devcontainer = over.devcontainer;
}
if over.local.is_some() {
base.local = over.local;
}
match (&mut base.daytona, over.daytona) {
(Some(base_d), Some(over_d)) => {
if over_d.auto_stop_interval.is_some() {
base_d.auto_stop_interval = over_d.auto_stop_interval;
}
if over_d.snapshot.is_some() {
base_d.snapshot = over_d.snapshot;
}
if let Some(over_labels) = over_d.labels {
let mut merged = base_d.labels.take().unwrap_or_default();
merged.extend(over_labels);
base_d.labels = Some(merged);
}
if over_d.network.is_some() {
base_d.network = over_d.network;
}
}
(None, Some(over_d)) => base.daytona = Some(over_d),
_ => {}
}
#[cfg(feature = "exedev")]
match (&mut base.exe, over.exe) {
(Some(base_e), Some(over_e)) => {
if over_e.image.is_some() {
base_e.image = over_e.image;
}
}
(None, Some(over_e)) => base.exe = Some(over_e),
_ => {}
}
if over.ssh.is_some() {
base.ssh = over.ssh;
}
if let Some(over_env) = over.env {
let mut merged = base.env.take().unwrap_or_default();
merged.extend(over_env);
base.env = Some(merged);
}
}
(None, Some(over)) => self.sandbox = Some(over),
_ => {}
}
if let Some(overlay_vars) = overlay.vars {
let mut merged = self.vars.take().unwrap_or_default();
merged.extend(overlay_vars);
self.vars = Some(merged);
}
if !overlay.checkpoint.exclude_globs.is_empty() {
self.checkpoint
.exclude_globs
.extend(overlay.checkpoint.exclude_globs);
self.checkpoint.exclude_globs.sort();
self.checkpoint.exclude_globs.dedup();
}
if overlay.pull_request.is_some() {
self.pull_request = overlay.pull_request;
}
if overlay.assets.is_some() {
self.assets = overlay.assets;
}
if !overlay.hooks.is_empty() {
let base = HookConfig {
hooks: std::mem::take(&mut self.hooks),
};
let over = HookConfig {
hooks: overlay.hooks,
};
self.hooks = base.merge(over).hooks;
}
if !overlay.mcp_servers.is_empty() {
let mut merged = std::mem::take(&mut self.mcp_servers);
merged.extend(overlay.mcp_servers);
self.mcp_servers = merged;
}
if overlay.github.is_some() {
self.github = overlay.github;
}
// --- CLI config fields ---
if overlay.mode.is_some() {
self.mode = overlay.mode;
}
if overlay.server.is_some() {
self.server = overlay.server;
}
if overlay.exec.is_some() {
self.exec = overlay.exec;
}
if overlay.prevent_idle_sleep.is_some() {
self.prevent_idle_sleep = overlay.prevent_idle_sleep;
}
if overlay.verbose.is_some() {
self.verbose = overlay.verbose;
}
if overlay.upgrade_check.is_some() {
self.upgrade_check = overlay.upgrade_check;
}
if overlay.dry_run.is_some() {
self.dry_run = overlay.dry_run;
}
if overlay.auto_approve.is_some() {
self.auto_approve = overlay.auto_approve;
}
if overlay.no_retro.is_some() {
self.no_retro = overlay.no_retro;
}
// --- Server config fields ---
if overlay.storage_dir.is_some() {
self.storage_dir = overlay.storage_dir;
}
if overlay.max_concurrent_runs.is_some() {
self.max_concurrent_runs = overlay.max_concurrent_runs;
}
if overlay.web.is_some() {
self.web = overlay.web;
}
if overlay.api.is_some() {
self.api = overlay.api;
}
if overlay.features.is_some() {
self.features = overlay.features;
}
// --- Shared fields ---
if overlay.log.is_some() {
self.log = overlay.log;
}
if overlay.git.is_some() {
self.git = overlay.git;
}
// --- Project config fields ---
if overlay.fabro.is_some() {
self.fabro = overlay.fabro;
Self {
version: self.version.combine(other.version),
goal: self.goal.combine(other.goal),
goal_file: self.goal_file.combine(other.goal_file),
graph: self.graph.combine(other.graph),
labels: self.labels.combine(other.labels),
work_dir: self.work_dir.combine(other.work_dir),
llm: self.llm.combine(other.llm),
setup: self.setup.combine(other.setup),
sandbox: self.sandbox.combine(other.sandbox),
vars: self.vars.combine(other.vars),
checkpoint: self.checkpoint.combine(other.checkpoint),
pull_request: self.pull_request.combine(other.pull_request),
assets: self.assets.combine(other.assets),
hooks,
mcp_servers: self.mcp_servers.combine(other.mcp_servers),
github: self.github.combine(other.github),
mode: self.mode.combine(other.mode),
server: self.server.combine(other.server),
exec: self.exec.combine(other.exec),
prevent_idle_sleep: self.prevent_idle_sleep.combine(other.prevent_idle_sleep),
verbose: self.verbose.combine(other.verbose),
upgrade_check: self.upgrade_check.combine(other.upgrade_check),
dry_run: self.dry_run.combine(other.dry_run),
auto_approve: self.auto_approve.combine(other.auto_approve),
no_retro: self.no_retro.combine(other.no_retro),
storage_dir: self.storage_dir.combine(other.storage_dir),
max_concurrent_runs: self.max_concurrent_runs.combine(other.max_concurrent_runs),
web: self.web.combine(other.web),
api: self.api.combine(other.api),
features: self.features.combine(other.features),
log: self.log.combine(other.log),
git: self.git.combine(other.git),
fabro: self.fabro.combine(other.fabro),
}
}
}
impl FabroConfig {
pub fn combine(self, other: Self) -> Self {
Combine::combine(self, other)
}
pub fn try_into_settings(self) -> anyhow::Result<FabroSettings> {
self.try_into()
}
}

View file

@ -1,4 +1,7 @@
extern crate self as fabro_config;
pub mod cli;
pub mod combine;
pub mod config;
pub mod dotenv;
pub mod hook;
@ -7,9 +10,12 @@ pub mod project;
pub mod run;
pub mod sandbox;
pub mod server;
pub mod settings;
pub use config::FabroConfig;
pub use fabro_config_derive::Combine;
pub use fabro_util::path::expand_tilde;
pub use settings::FabroSettings;
use std::path::Path;

View file

@ -3,6 +3,8 @@ use std::time::Duration;
use serde::{Deserialize, Serialize};
use crate::combine::Combine;
pub fn default_startup_timeout_secs() -> u64 {
10
}
@ -57,6 +59,12 @@ pub enum McpTransport {
},
}
impl Combine for McpTransport {
fn combine(self, _other: Self) -> Self {
self
}
}
/// MCP server entry as it appears in TOML config files (without a `name` field).
///
/// Converted to [`McpServerConfig`] via [`McpServerEntry::into_config`].
@ -81,6 +89,12 @@ impl McpServerEntry {
}
}
impl Combine for McpServerEntry {
fn combine(self, _other: Self) -> Self {
self
}
}
#[cfg(test)]
mod tests {
use super::*;

View file

@ -8,17 +8,22 @@ use crate::config::FabroConfig;
const CONFIG_FILENAME: &str = "fabro.toml";
const SUPPORTED_VERSION: u32 = 1;
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, crate::Combine)]
pub struct ProjectFabroConfig {
#[serde(default = "default_root")]
pub root: String,
pub root: Option<String>,
}
fn default_root() -> String {
".".to_string()
}
impl Default for ProjectFabroConfig {
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct ProjectFabroSettings {
#[serde(default = "default_root")]
pub root: String,
}
impl Default for ProjectFabroSettings {
fn default() -> Self {
Self {
root: default_root(),
@ -26,6 +31,14 @@ impl Default for ProjectFabroConfig {
}
}
impl From<ProjectFabroConfig> for ProjectFabroSettings {
fn from(value: ProjectFabroConfig) -> Self {
Self {
root: value.root.unwrap_or_else(default_root),
}
}
}
/// Parse a project config from a TOML string.
pub fn parse_project_config(content: &str) -> anyhow::Result<FabroConfig> {
let config: FabroConfig = toml::from_str(content).context("Failed to parse project config")?;
@ -46,7 +59,7 @@ pub fn load_project_config(path: &Path) -> anyhow::Result<FabroConfig> {
let root = config
.fabro
.as_ref()
.map(|f| f.root.as_str())
.and_then(|f| f.root.as_deref())
.unwrap_or(".");
tracing::debug!(path = %path.display(), root = %root, "Loaded project config");
Ok(config)
@ -295,7 +308,11 @@ fn resolve_workflow_from(
pub fn is_retro_enabled() -> bool {
let start = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
match discover_project_config(&start) {
Ok(Some((_path, config))) => config.features.as_ref().map(|f| f.retros).unwrap_or(false),
Ok(Some((_path, config))) => config
.features
.as_ref()
.and_then(|f| f.retros)
.unwrap_or(false),
_ => false,
}
}
@ -309,7 +326,7 @@ pub fn resolve_fabro_root(config_path: &Path, config: &FabroConfig) -> PathBuf {
let root = config
.fabro
.as_ref()
.map(|f| f.root.as_str())
.and_then(|f| f.root.as_deref())
.unwrap_or(".");
project_dir.join(root)
}
@ -317,7 +334,6 @@ pub fn resolve_fabro_root(config_path: &Path, config: &FabroConfig) -> PathBuf {
#[cfg(test)]
mod tests {
use super::*;
use crate::run::MergeStrategy;
use std::fs;
use tempfile::TempDir;
@ -331,22 +347,26 @@ mod tests {
#[test]
fn parse_full_config() {
let config = parse_project_config("version = 1\n[fabro]\nroot = \"fabro/\"\n").unwrap();
assert_eq!(config.fabro.unwrap().root, "fabro/");
assert_eq!(config.fabro.unwrap().root.as_deref(), Some("fabro/"));
}
#[test]
fn parse_retros_default_false() {
let config = parse_project_config("version = 1\n").unwrap();
assert_eq!(
config.features.as_ref().map(|f| f.retros).unwrap_or(false),
false
config
.features
.as_ref()
.and_then(|f| f.retros)
.unwrap_or(false),
false,
);
}
#[test]
fn parse_retros_enabled() {
let config = parse_project_config("version = 1\n[features]\nretros = true\n").unwrap();
assert!(config.features.unwrap().retros);
assert_eq!(config.features.unwrap().retros, Some(true));
}
#[test]
@ -366,10 +386,10 @@ mod tests {
assert_eq!(
config.pull_request,
Some(crate::run::PullRequestConfig {
enabled: true,
draft: false,
auto_merge: false,
merge_strategy: MergeStrategy::Squash,
enabled: Some(true),
draft: Some(false),
auto_merge: None,
merge_strategy: None,
})
);
}
@ -389,7 +409,7 @@ memory = 8
let sandbox = config.sandbox.unwrap();
assert_eq!(sandbox.provider.as_deref(), Some("daytona"));
let snap = sandbox.daytona.unwrap().snapshot.unwrap();
assert_eq!(snap.name, "my-snapshot");
assert_eq!(snap.name.as_deref(), Some("my-snapshot"));
assert_eq!(snap.cpu, Some(4));
assert_eq!(snap.memory, Some(8));
}
@ -461,7 +481,7 @@ model = "claude-sonnet-4-6"
let config = FabroConfig {
version: Some(1),
fabro: Some(ProjectFabroConfig {
root: "fabro/".to_string(),
root: Some("fabro/".to_string()),
..Default::default()
}),
..Default::default()
@ -478,7 +498,7 @@ model = "claude-sonnet-4-6"
let config = FabroConfig {
version: Some(1),
fabro: Some(ProjectFabroConfig {
root: ".".to_string(),
root: Some(".".to_string()),
..Default::default()
}),
..Default::default()

View file

@ -3,11 +3,10 @@ use std::path::{Path, PathBuf};
use anyhow::{bail, Context};
use serde::{Deserialize, Serialize};
use tracing::debug;
use crate::combine::Combine;
use crate::config::FabroConfig;
use crate::sandbox::DockerfileSource;
const SUPPORTED_VERSION: u32 = 1;
@ -17,12 +16,44 @@ pub struct CheckpointConfig {
pub exclude_globs: Vec<String>,
}
impl Combine for CheckpointConfig {
fn combine(mut self, other: Self) -> Self {
self.exclude_globs.extend(other.exclude_globs);
self.exclude_globs.sort();
self.exclude_globs.dedup();
self
}
}
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
pub struct CheckpointSettings {
#[serde(default)]
pub exclude_globs: Vec<String>,
}
impl From<CheckpointConfig> for CheckpointSettings {
fn from(value: CheckpointConfig) -> Self {
let mut exclude_globs = value.exclude_globs;
exclude_globs.sort();
exclude_globs.dedup();
Self { exclude_globs }
}
}
fn default_true() -> bool {
true
}
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, crate::Combine)]
pub struct PullRequestConfig {
pub enabled: Option<bool>,
pub draft: Option<bool>,
pub auto_merge: Option<bool>,
pub merge_strategy: Option<MergeStrategy>,
}
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
pub struct PullRequestSettings {
#[serde(default)]
pub enabled: bool,
#[serde(default = "default_true")]
@ -33,7 +64,18 @@ pub struct PullRequestConfig {
pub merge_strategy: MergeStrategy,
}
#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Serialize)]
impl From<PullRequestConfig> for PullRequestSettings {
fn from(value: PullRequestConfig) -> Self {
Self {
enabled: value.enabled.unwrap_or(false),
draft: value.draft.unwrap_or(true),
auto_merge: value.auto_merge.unwrap_or(false),
merge_strategy: value.merge_strategy.unwrap_or_default(),
}
}
}
#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Serialize, crate::Combine)]
#[serde(rename_all = "lowercase")]
pub enum MergeStrategy {
#[default]
@ -42,19 +84,47 @@ pub enum MergeStrategy {
Rebase,
}
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, crate::Combine)]
pub struct AssetsConfig {
#[serde(default)]
pub include: Vec<String>,
}
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
pub struct AssetsSettings {
#[serde(default)]
pub include: Vec<String>,
}
impl From<AssetsConfig> for AssetsSettings {
fn from(value: AssetsConfig) -> Self {
Self {
include: value.include,
}
}
}
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, crate::Combine)]
pub struct GitHubConfig {
#[serde(default)]
pub permissions: HashMap<String, String>,
}
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
pub struct GitHubSettings {
#[serde(default)]
pub permissions: HashMap<String, String>,
}
impl From<GitHubConfig> for GitHubSettings {
fn from(value: GitHubConfig) -> Self {
Self {
permissions: value.permissions,
}
}
}
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, crate::Combine)]
pub struct LlmConfig {
pub model: Option<String>,
pub provider: Option<String>,
@ -62,12 +132,47 @@ pub struct LlmConfig {
pub fallbacks: Option<HashMap<String, Vec<String>>>,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
pub struct LlmSettings {
pub model: Option<String>,
pub provider: Option<String>,
#[serde(default)]
pub fallbacks: Option<HashMap<String, Vec<String>>>,
}
impl From<LlmConfig> for LlmSettings {
fn from(value: LlmConfig) -> Self {
Self {
model: value.model,
provider: value.provider,
fallbacks: value.fallbacks,
}
}
}
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, crate::Combine)]
pub struct SetupConfig {
#[serde(default)]
pub commands: Vec<String>,
pub timeout_ms: Option<u64>,
}
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
pub struct SetupSettings {
#[serde(default)]
pub commands: Vec<String>,
pub timeout_ms: Option<u64>,
}
impl From<SetupConfig> for SetupSettings {
fn from(value: SetupConfig) -> Self {
Self {
commands: value.commands,
timeout_ms: value.timeout_ms,
}
}
}
/// Load and validate a run config from a TOML file.
///
/// The `graph` path in the returned config is resolved relative to the
@ -128,12 +233,12 @@ fn resolve_dockerfile(config: &mut FabroConfig, config_dir: &Path) -> anyhow::Re
.and_then(|d| d.snapshot.as_mut())
.and_then(|snap| snap.dockerfile.as_mut());
if let Some(DockerfileSource::Path { path: ref rel }) = source {
if let Some(crate::sandbox::DockerfileSource::Path { path: ref rel }) = source {
let path = config_dir.join(rel);
let contents = std::fs::read_to_string(&path)
.with_context(|| format!("Failed to read dockerfile at {}", path.display()))?;
debug!(path = %path.display(), "Resolved dockerfile from path");
*source.unwrap() = DockerfileSource::Inline(contents);
*source.unwrap() = crate::sandbox::DockerfileSource::Inline(contents);
}
Ok(())
@ -156,7 +261,6 @@ pub fn parse_run_config(contents: &str) -> anyhow::Result<FabroConfig> {
let mut config: FabroConfig =
toml::from_str(contents).context("Failed to parse run config TOML")?;
// Apply default graph if not specified
if config.graph.is_none() {
config.graph = Some("workflow.fabro".to_string());
}

View file

@ -1,22 +1,44 @@
use std::collections::HashMap;
use anyhow::anyhow;
use serde::de::{self, MapAccess, Visitor};
use serde::{Deserialize, Serialize};
/// Configuration for a Daytona cloud sandbox.
///
/// Doubles as the TOML deserialization target for `[sandbox.daytona]`.
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, crate::Combine)]
pub struct DaytonaConfig {
pub auto_stop_interval: Option<i32>,
pub labels: Option<HashMap<String, String>>,
pub snapshot: Option<DaytonaSnapshotConfig>,
pub network: Option<DaytonaNetwork>,
/// Skip git repo detection and cloning during initialization.
pub skip_clone: Option<bool>,
}
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
pub struct DaytonaSettings {
pub auto_stop_interval: Option<i32>,
pub labels: Option<HashMap<String, String>>,
pub snapshot: Option<DaytonaSnapshotSettings>,
pub network: Option<DaytonaNetwork>,
/// Skip git repo detection and cloning during initialization.
#[serde(default)]
pub skip_clone: bool,
}
impl TryFrom<DaytonaConfig> for DaytonaSettings {
type Error = anyhow::Error;
fn try_from(value: DaytonaConfig) -> Result<Self, Self::Error> {
Ok(Self {
auto_stop_interval: value.auto_stop_interval,
labels: value.labels,
snapshot: value.snapshot.map(TryInto::try_into).transpose()?,
network: value.network,
skip_clone: value.skip_clone.unwrap_or(false),
})
}
}
/// Network access mode for a Daytona sandbox.
///
/// TOML syntax:
@ -25,7 +47,7 @@ pub struct DaytonaConfig {
/// network = "allow_all" # full access (default)
/// network = { allow_list = ["208.80.154.232/32"] } # CIDR allowlist
/// ```
#[derive(Clone, Debug, PartialEq)]
#[derive(Clone, Debug, PartialEq, crate::Combine)]
pub enum DaytonaNetwork {
Block,
AllowAll,
@ -121,7 +143,7 @@ impl<'de> Deserialize<'de> for DaytonaNetwork {
/// `Path` variants are resolved to `Inline` during config loading
/// (see `run_config::resolve_dockerfile`), so downstream consumers
/// should only ever see `Inline`.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, crate::Combine)]
#[serde(untagged)]
pub enum DockerfileSource {
Inline(String),
@ -130,8 +152,17 @@ pub enum DockerfileSource {
/// Snapshot configuration: when present, the sandbox is created from a snapshot
/// instead of a bare Docker image.
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, crate::Combine)]
pub struct DaytonaSnapshotConfig {
pub name: Option<String>,
pub cpu: Option<i32>,
pub memory: Option<i32>,
pub disk: Option<i32>,
pub dockerfile: Option<DockerfileSource>,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct DaytonaSnapshotSettings {
pub name: String,
pub cpu: Option<i32>,
pub memory: Option<i32>,
@ -139,16 +170,58 @@ pub struct DaytonaSnapshotConfig {
pub dockerfile: Option<DockerfileSource>,
}
impl TryFrom<DaytonaSnapshotConfig> for DaytonaSnapshotSettings {
type Error = anyhow::Error;
fn try_from(value: DaytonaSnapshotConfig) -> Result<Self, Self::Error> {
Ok(Self {
name: value
.name
.ok_or_else(|| anyhow!("sandbox.daytona.snapshot.name is required"))?,
cpu: value.cpu,
memory: value.memory,
disk: value.disk,
dockerfile: value.dockerfile,
})
}
}
/// Configuration for an exe.dev sandbox (TOML target for `[sandbox.exe]`).
#[cfg(feature = "exedev")]
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, crate::Combine)]
pub struct ExeConfig {
pub image: Option<String>,
}
#[cfg(feature = "exedev")]
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
pub struct ExeSettings {
pub image: Option<String>,
}
#[cfg(feature = "exedev")]
impl From<ExeConfig> for ExeSettings {
fn from(value: ExeConfig) -> Self {
Self { image: value.image }
}
}
/// Configuration for an SSH sandbox (TOML target for `[sandbox.ssh]`).
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, crate::Combine)]
pub struct SshConfig {
/// SSH destination (e.g. `user@host` or an SSH alias).
pub destination: Option<String>,
/// Remote working directory.
pub working_directory: Option<String>,
/// Optional path to a custom SSH config file.
pub config_file: Option<String>,
/// Base URL for port previews (e.g. `"http://beast"`).
/// When set, `get_preview_url(port)` returns `"{preview_url_base}:{port}"`.
pub preview_url_base: Option<String>,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct SshSettings {
/// SSH destination (e.g. `user@host` or an SSH alias).
pub destination: String,
/// Remote working directory.
@ -160,7 +233,24 @@ pub struct SshConfig {
pub preview_url_base: Option<String>,
}
#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Serialize)]
impl TryFrom<SshConfig> for SshSettings {
type Error = anyhow::Error;
fn try_from(value: SshConfig) -> Result<Self, Self::Error> {
Ok(Self {
destination: value
.destination
.ok_or_else(|| anyhow!("sandbox.ssh.destination is required"))?,
working_directory: value
.working_directory
.ok_or_else(|| anyhow!("sandbox.ssh.working_directory is required"))?,
config_file: value.config_file,
preview_url_base: value.preview_url_base,
})
}
}
#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Serialize, crate::Combine)]
#[serde(rename_all = "snake_case")]
pub enum WorktreeMode {
Always,
@ -170,17 +260,29 @@ pub enum WorktreeMode {
Never,
}
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, crate::Combine)]
pub struct LocalSandboxConfig {
pub worktree_mode: Option<WorktreeMode>,
}
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
pub struct LocalSandboxSettings {
#[serde(default)]
pub worktree_mode: WorktreeMode,
}
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
impl From<LocalSandboxConfig> for LocalSandboxSettings {
fn from(value: LocalSandboxConfig) -> Self {
Self {
worktree_mode: value.worktree_mode.unwrap_or_default(),
}
}
}
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, crate::Combine)]
pub struct SandboxConfig {
pub provider: Option<String>,
pub preserve: Option<bool>,
#[serde(default)]
pub devcontainer: Option<bool>,
pub local: Option<LocalSandboxConfig>,
pub daytona: Option<DaytonaConfig>,
@ -189,3 +291,34 @@ pub struct SandboxConfig {
pub ssh: Option<SshConfig>,
pub env: Option<HashMap<String, String>>,
}
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
pub struct SandboxSettings {
pub provider: Option<String>,
pub preserve: Option<bool>,
pub devcontainer: Option<bool>,
pub local: Option<LocalSandboxSettings>,
pub daytona: Option<DaytonaSettings>,
#[cfg(feature = "exedev")]
pub exe: Option<ExeSettings>,
pub ssh: Option<SshSettings>,
pub env: Option<HashMap<String, String>>,
}
impl TryFrom<SandboxConfig> for SandboxSettings {
type Error = anyhow::Error;
fn try_from(value: SandboxConfig) -> Result<Self, Self::Error> {
Ok(Self {
provider: value.provider,
preserve: value.preserve,
devcontainer: value.devcontainer,
local: value.local.map(Into::into),
daytona: value.daytona.map(TryInto::try_into).transpose()?,
#[cfg(feature = "exedev")]
exe: value.exe.map(Into::into),
ssh: value.ssh.map(TryInto::try_into).transpose()?,
env: value.env,
})
}
}

View file

@ -1,10 +1,12 @@
use std::path::{Path, PathBuf};
use anyhow::anyhow;
use serde::{Deserialize, Serialize};
use crate::config::FabroConfig;
use crate::settings::FabroSettings;
#[derive(Debug, Clone, Default, Deserialize, PartialEq, Serialize)]
#[derive(Debug, Clone, Default, Deserialize, PartialEq, Serialize, crate::Combine)]
#[serde(rename_all = "snake_case")]
pub enum AuthProvider {
#[default]
@ -12,42 +14,91 @@ pub enum AuthProvider {
InsecureDisabled,
}
#[derive(Debug, Clone, Default, Deserialize, PartialEq, Serialize)]
#[derive(Debug, Clone, Default, Deserialize, PartialEq, Serialize, crate::Combine)]
pub struct AuthConfig {
pub provider: Option<AuthProvider>,
#[serde(default)]
pub allowed_usernames: Vec<String>,
}
#[derive(Debug, Clone, Default, Deserialize, PartialEq, Serialize)]
pub struct AuthSettings {
#[serde(default)]
pub provider: AuthProvider,
#[serde(default)]
pub allowed_usernames: Vec<String>,
}
#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)]
impl From<AuthConfig> for AuthSettings {
fn from(value: AuthConfig) -> Self {
Self {
provider: value.provider.unwrap_or_default(),
allowed_usernames: value.allowed_usernames,
}
}
}
#[derive(Debug, Clone, Deserialize, PartialEq, Serialize, crate::Combine)]
#[serde(rename_all = "snake_case")]
pub enum ApiAuthStrategy {
Jwt,
Mtls,
}
#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)]
#[derive(Debug, Clone, Default, Deserialize, PartialEq, Serialize, crate::Combine)]
pub struct TlsConfig {
pub cert: Option<PathBuf>,
pub key: Option<PathBuf>,
pub ca: Option<PathBuf>,
}
#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)]
pub struct TlsSettings {
pub cert: PathBuf,
pub key: PathBuf,
pub ca: PathBuf,
}
#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)]
impl TryFrom<TlsConfig> for TlsSettings {
type Error = anyhow::Error;
fn try_from(value: TlsConfig) -> Result<Self, Self::Error> {
Ok(Self {
cert: value
.cert
.ok_or_else(|| anyhow!("tls.cert is required when tls is configured"))?,
key: value
.key
.ok_or_else(|| anyhow!("tls.key is required when tls is configured"))?,
ca: value
.ca
.ok_or_else(|| anyhow!("tls.ca is required when tls is configured"))?,
})
}
}
#[derive(Debug, Clone, Default, Deserialize, PartialEq, Serialize, crate::Combine)]
pub struct ApiConfig {
pub base_url: Option<String>,
#[serde(default)]
pub authentication_strategies: Vec<ApiAuthStrategy>,
pub tls: Option<TlsConfig>,
}
#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)]
pub struct ApiSettings {
#[serde(default = "default_base_url")]
pub base_url: String,
#[serde(default)]
pub authentication_strategies: Vec<ApiAuthStrategy>,
pub tls: Option<TlsConfig>,
pub tls: Option<TlsSettings>,
}
fn default_base_url() -> String {
"http://localhost:3000".to_string()
}
impl Default for ApiConfig {
impl Default for ApiSettings {
fn default() -> Self {
Self {
base_url: default_base_url(),
@ -57,65 +108,156 @@ impl Default for ApiConfig {
}
}
#[derive(Debug, Clone, Default, Deserialize, PartialEq, Serialize)]
impl TryFrom<ApiConfig> for ApiSettings {
type Error = anyhow::Error;
fn try_from(value: ApiConfig) -> Result<Self, Self::Error> {
Ok(Self {
base_url: value.base_url.unwrap_or_else(default_base_url),
authentication_strategies: value.authentication_strategies,
tls: value.tls.map(TryInto::try_into).transpose()?,
})
}
}
#[derive(Debug, Clone, Default, Deserialize, PartialEq, Serialize, crate::Combine)]
#[serde(rename_all = "snake_case")]
pub enum GitProvider {
#[default]
Github,
}
#[derive(Debug, Clone, Default, Deserialize, PartialEq, Serialize)]
#[derive(Debug, Clone, Default, Deserialize, PartialEq, Serialize, crate::Combine)]
pub struct GitAuthorConfig {
pub name: Option<String>,
pub email: Option<String>,
}
#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)]
#[derive(Debug, Clone, Default, Deserialize, PartialEq, Serialize)]
pub struct GitAuthorSettings {
pub name: Option<String>,
pub email: Option<String>,
}
impl From<GitAuthorConfig> for GitAuthorSettings {
fn from(value: GitAuthorConfig) -> Self {
Self {
name: value.name,
email: value.email,
}
}
}
#[derive(Debug, Clone, Deserialize, PartialEq, Serialize, crate::Combine)]
#[serde(rename_all = "snake_case")]
pub enum WebhookStrategy {
TailscaleFunnel,
}
#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)]
#[derive(Debug, Clone, Default, Deserialize, PartialEq, Serialize, crate::Combine)]
pub struct WebhookConfig {
pub strategy: Option<WebhookStrategy>,
}
#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)]
pub struct WebhookSettings {
pub strategy: WebhookStrategy,
}
#[derive(Debug, Clone, Default, Deserialize, PartialEq, Serialize)]
impl TryFrom<WebhookConfig> for WebhookSettings {
type Error = anyhow::Error;
fn try_from(value: WebhookConfig) -> Result<Self, Self::Error> {
Ok(Self {
strategy: value
.strategy
.ok_or_else(|| anyhow!("git.webhooks.strategy is required"))?,
})
}
}
#[derive(Debug, Clone, Default, Deserialize, PartialEq, Serialize, crate::Combine)]
pub struct GitConfig {
pub provider: Option<GitProvider>,
pub app_id: Option<String>,
pub client_id: Option<String>,
pub slug: Option<String>,
pub author: Option<GitAuthorConfig>,
pub webhooks: Option<WebhookConfig>,
}
#[derive(Debug, Clone, Default, Deserialize, PartialEq, Serialize)]
pub struct GitSettings {
#[serde(default)]
pub provider: GitProvider,
pub app_id: Option<String>,
pub client_id: Option<String>,
pub slug: Option<String>,
#[serde(default)]
pub author: GitAuthorConfig,
pub webhooks: Option<WebhookConfig>,
pub author: GitAuthorSettings,
pub webhooks: Option<WebhookSettings>,
}
impl TryFrom<GitConfig> for GitSettings {
type Error = anyhow::Error;
fn try_from(value: GitConfig) -> Result<Self, Self::Error> {
Ok(Self {
provider: value.provider.unwrap_or_default(),
app_id: value.app_id,
client_id: value.client_id,
slug: value.slug,
author: value.author.map(Into::into).unwrap_or_default(),
webhooks: value.webhooks.map(TryInto::try_into).transpose()?,
})
}
}
#[derive(Debug, Clone, Default, Deserialize, PartialEq, Serialize, crate::Combine)]
pub struct WebConfig {
pub url: Option<String>,
pub auth: Option<AuthConfig>,
}
#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)]
pub struct WebConfig {
pub struct WebSettings {
#[serde(default = "default_web_url")]
pub url: String,
#[serde(default)]
pub auth: AuthConfig,
pub auth: AuthSettings,
}
fn default_web_url() -> String {
"http://localhost:5173".to_string()
}
impl Default for WebConfig {
impl Default for WebSettings {
fn default() -> Self {
Self {
url: default_web_url(),
auth: AuthConfig::default(),
auth: AuthSettings::default(),
}
}
}
#[derive(Debug, Clone, Default, Deserialize, PartialEq, Serialize)]
impl From<WebConfig> for WebSettings {
fn from(value: WebConfig) -> Self {
Self {
url: value.url.unwrap_or_else(default_web_url),
auth: value.auth.map(Into::into).unwrap_or_default(),
}
}
}
#[derive(Debug, Clone, Default, Deserialize, PartialEq, Serialize, crate::Combine)]
pub struct Features {
pub session_sandboxes: Option<bool>,
/// Experimental: enable automatic retro generation after workflow runs.
pub retros: Option<bool>,
}
#[derive(Debug, Clone, Default, Deserialize, PartialEq, Serialize)]
pub struct FeaturesSettings {
#[serde(default)]
pub session_sandboxes: bool,
/// Experimental: enable automatic retro generation after workflow runs.
@ -123,11 +265,31 @@ pub struct Features {
pub retros: bool,
}
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
impl From<Features> for FeaturesSettings {
fn from(value: Features) -> Self {
Self {
session_sandboxes: value.session_sandboxes.unwrap_or(false),
retros: value.retros.unwrap_or(false),
}
}
}
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, crate::Combine)]
pub struct LogConfig {
pub level: Option<String>,
}
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
pub struct LogSettings {
pub level: Option<String>,
}
impl From<LogConfig> for LogSettings {
fn from(value: LogConfig) -> Self {
Self { level: value.level }
}
}
/// Load server config from an explicit path or `~/.fabro/server.toml`, returning defaults if the
/// default file doesn't exist. An explicit path that doesn't exist is an error.
pub fn load_server_config(path: Option<&Path>) -> anyhow::Result<FabroConfig> {
@ -135,392 +297,6 @@ pub fn load_server_config(path: Option<&Path>) -> anyhow::Result<FabroConfig> {
}
/// Resolve the storage directory: config value > default `~/.fabro`.
pub fn resolve_storage_dir(config: &FabroConfig) -> PathBuf {
pub fn resolve_storage_dir(config: &FabroSettings) -> PathBuf {
config.storage_dir()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_config_with_storage_dir() {
let toml = r#"storage_dir = "/custom/path""#;
let config: FabroConfig = toml::from_str(toml).unwrap();
assert_eq!(config.storage_dir, Some(PathBuf::from("/custom/path")));
}
#[test]
fn parse_config_with_data_dir_alias() {
let toml = r#"data_dir = "/custom/path""#;
let config: FabroConfig = toml::from_str(toml).unwrap();
assert_eq!(config.storage_dir, Some(PathBuf::from("/custom/path")));
}
#[test]
fn parse_empty_config_defaults() {
let toml = "";
let config: FabroConfig = toml::from_str(toml).unwrap();
assert_eq!(config.storage_dir, None);
}
#[test]
fn resolve_storage_dir_uses_config_value() {
let config = FabroConfig {
storage_dir: Some(PathBuf::from("/my/data")),
..FabroConfig::default()
};
assert_eq!(resolve_storage_dir(&config), PathBuf::from("/my/data"));
}
#[test]
fn resolve_storage_dir_defaults_to_home() {
let config = FabroConfig::default();
let dir = resolve_storage_dir(&config);
// Should end with .fabro
assert!(
dir.ends_with(".fabro"),
"expected path ending with .fabro, got: {}",
dir.display()
);
}
#[test]
fn parse_full_config() {
let toml = r#"
[web]
url = "https://arc.example.com"
[web.auth]
provider = "github"
allowed_usernames = ["brynary", "alice"]
[api]
base_url = "http://example.com:8080"
authentication_strategies = ["jwt"]
[git]
provider = "github"
app_id = "12345"
client_id = "Iv1.abc123"
"#;
let config: FabroConfig = toml::from_str(toml).unwrap();
let web = config.web.unwrap();
assert_eq!(web.url, "https://arc.example.com");
assert_eq!(web.auth.provider, AuthProvider::Github);
assert_eq!(web.auth.allowed_usernames, vec!["brynary", "alice"]);
let api = config.api.unwrap();
assert_eq!(api.base_url, "http://example.com:8080");
assert_eq!(api.authentication_strategies, vec![ApiAuthStrategy::Jwt]);
let git = config.git.unwrap();
assert_eq!(git.provider, GitProvider::Github);
assert_eq!(git.app_id.as_deref(), Some("12345"));
assert_eq!(git.client_id.as_deref(), Some("Iv1.abc123"));
}
#[test]
fn parse_web_defaults() {
let toml = "[web]\n";
let config: FabroConfig = toml::from_str(toml).unwrap();
let web = config.web.unwrap();
assert_eq!(web.url, "http://localhost:5173");
assert_eq!(web.auth.provider, AuthProvider::Github);
assert!(web.auth.allowed_usernames.is_empty());
}
#[test]
fn parse_api_defaults() {
let toml = "[api]\n";
let config: FabroConfig = toml::from_str(toml).unwrap();
let api = config.api.unwrap();
assert_eq!(api.base_url, "http://localhost:3000");
assert!(api.authentication_strategies.is_empty());
assert!(api.tls.is_none());
}
#[test]
fn parse_git_config() {
let toml = r#"
[git]
provider = "github"
app_id = "12345"
client_id = "Iv1.abc123"
"#;
let config: FabroConfig = toml::from_str(toml).unwrap();
let git = config.git.unwrap();
assert_eq!(git.provider, GitProvider::Github);
assert_eq!(git.app_id.as_deref(), Some("12345"));
assert_eq!(git.client_id.as_deref(), Some("Iv1.abc123"));
}
#[test]
fn parse_git_defaults() {
let toml = "[git]\n";
let config: FabroConfig = toml::from_str(toml).unwrap();
let git = config.git.unwrap();
assert_eq!(git.provider, GitProvider::Github);
assert_eq!(git.app_id, None);
assert_eq!(git.client_id, None);
assert_eq!(git.author.name, None);
assert_eq!(git.author.email, None);
}
#[test]
fn parse_git_author_config() {
let toml = r#"
[git.author]
name = "fabro-bot"
email = "fabro-bot@company.com"
"#;
let config: FabroConfig = toml::from_str(toml).unwrap();
let git = config.git.unwrap();
assert_eq!(git.author.name.as_deref(), Some("fabro-bot"));
assert_eq!(git.author.email.as_deref(), Some("fabro-bot@company.com"));
}
#[test]
fn parse_git_author_partial() {
let toml = r#"
[git.author]
name = "custom-name"
"#;
let config: FabroConfig = toml::from_str(toml).unwrap();
let git = config.git.unwrap();
assert_eq!(git.author.name.as_deref(), Some("custom-name"));
assert_eq!(git.author.email, None);
}
#[test]
fn parse_config_with_run_defaults() {
let toml = r#"
[llm]
model = "claude-haiku"
provider = "anthropic"
[sandbox]
provider = "daytona"
[vars]
repo_url = "https://github.com/org/repo"
"#;
let config: FabroConfig = toml::from_str(toml).unwrap();
let llm = config.llm.unwrap();
assert_eq!(llm.model.as_deref(), Some("claude-haiku"));
assert_eq!(llm.provider.as_deref(), Some("anthropic"));
let sandbox = config.sandbox.unwrap();
assert_eq!(sandbox.provider.as_deref(), Some("daytona"));
let vars = config.vars.unwrap();
assert_eq!(vars["repo_url"], "https://github.com/org/repo");
}
#[test]
fn parse_config_server_and_run_defaults_together() {
let toml = r#"
[web.auth]
provider = "github"
[git]
provider = "github"
app_id = "123"
[llm]
model = "gpt-4"
"#;
let config: FabroConfig = toml::from_str(toml).unwrap();
let web = config.web.unwrap();
assert_eq!(web.auth.provider, AuthProvider::Github);
let git = config.git.unwrap();
assert_eq!(git.app_id.as_deref(), Some("123"));
let llm = config.llm.unwrap();
assert_eq!(llm.model.as_deref(), Some("gpt-4"));
}
#[test]
fn parse_insecure_disabled_auth_provider() {
let toml = r#"
[web.auth]
provider = "insecure_disabled"
"#;
let config: FabroConfig = toml::from_str(toml).unwrap();
let web = config.web.unwrap();
assert_eq!(web.auth.provider, AuthProvider::InsecureDisabled);
}
#[test]
fn parse_jwt_and_mtls_strategies() {
let toml = r#"
[api]
authentication_strategies = ["jwt", "mtls"]
[api.tls]
cert = "~/.fabro/certs/server.crt"
key = "~/.fabro/certs/server.key"
ca = "~/.fabro/certs/ca.crt"
"#;
let config: FabroConfig = toml::from_str(toml).unwrap();
let api = config.api.unwrap();
assert_eq!(
api.authentication_strategies,
vec![ApiAuthStrategy::Jwt, ApiAuthStrategy::Mtls]
);
let tls = api.tls.unwrap();
assert_eq!(tls.cert, PathBuf::from("~/.fabro/certs/server.crt"));
assert_eq!(tls.key, PathBuf::from("~/.fabro/certs/server.key"));
assert_eq!(tls.ca, PathBuf::from("~/.fabro/certs/ca.crt"));
}
#[test]
fn parse_max_concurrent_runs() {
let toml = r#"max_concurrent_runs = 8"#;
let config: FabroConfig = toml::from_str(toml).unwrap();
assert_eq!(config.max_concurrent_runs, Some(8));
}
#[test]
fn parse_max_concurrent_runs_defaults_to_none() {
let toml = "";
let config: FabroConfig = toml::from_str(toml).unwrap();
assert_eq!(config.max_concurrent_runs, None);
}
#[test]
fn parse_empty_strategies() {
let toml = r#"
[api]
authentication_strategies = []
"#;
let config: FabroConfig = toml::from_str(toml).unwrap();
let api = config.api.unwrap();
assert!(api.authentication_strategies.is_empty());
}
#[test]
fn parse_jwt_only_strategy() {
let toml = r#"
[api]
authentication_strategies = ["jwt"]
"#;
let config: FabroConfig = toml::from_str(toml).unwrap();
let api = config.api.unwrap();
assert_eq!(api.authentication_strategies, vec![ApiAuthStrategy::Jwt]);
assert!(api.tls.is_none());
}
#[test]
fn load_server_config_from_explicit_path() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("custom.toml");
std::fs::write(&path, r#"max_concurrent_runs = 42"#).unwrap();
let config = load_server_config(Some(&path)).unwrap();
assert_eq!(config.max_concurrent_runs, Some(42));
}
#[test]
fn load_server_config_explicit_path_missing_is_error() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("nonexistent.toml");
let result = load_server_config(Some(&path));
assert!(result.is_err());
}
#[test]
fn parse_config_with_hooks() {
let toml = r#"
[[hooks]]
event = "run_start"
command = "echo 'run starting'"
[[hooks]]
event = "stage_complete"
command = "echo 'stage done'"
matcher = "agent_loop"
"#;
let config: FabroConfig = toml::from_str(toml).unwrap();
assert_eq!(config.hooks.len(), 2);
assert_eq!(config.hooks[0].event, crate::hook::HookEvent::RunStart);
assert_eq!(
config.hooks[0].command.as_deref(),
Some("echo 'run starting'")
);
assert_eq!(config.hooks[1].event, crate::hook::HookEvent::StageComplete);
assert_eq!(config.hooks[1].matcher.as_deref(), Some("agent_loop"));
}
#[test]
fn parse_features() {
let toml = "[features]\nsession_sandboxes = true";
let config: FabroConfig = toml::from_str(toml).unwrap();
let features = config.features.unwrap();
assert!(features.session_sandboxes);
}
#[test]
fn parse_features_defaults() {
let toml = "";
let config: FabroConfig = toml::from_str(toml).unwrap();
assert_eq!(config.features, None);
}
#[test]
fn parse_config_without_hooks_defaults_empty() {
let toml = "";
let config: FabroConfig = toml::from_str(toml).unwrap();
assert!(config.hooks.is_empty());
}
#[test]
fn parse_config_with_checkpoint_exclude_globs() {
let toml = r#"
[checkpoint]
exclude_globs = ["**/node_modules/**", "**/.cache/**"]
"#;
let config: FabroConfig = toml::from_str(toml).unwrap();
assert_eq!(
config.checkpoint.exclude_globs,
vec!["**/node_modules/**", "**/.cache/**"]
);
}
#[test]
fn parse_config_checkpoint_defaults_empty() {
let toml = "";
let config: FabroConfig = toml::from_str(toml).unwrap();
assert!(config.checkpoint.exclude_globs.is_empty());
}
#[test]
fn parse_git_webhooks_config() {
let toml = r#"
[git]
provider = "github"
app_id = "2993730"
[git.webhooks]
strategy = "tailscale_funnel"
"#;
let config: FabroConfig = toml::from_str(toml).unwrap();
let webhooks = config.git.unwrap().webhooks.unwrap();
assert_eq!(webhooks.strategy, WebhookStrategy::TailscaleFunnel);
}
#[test]
fn parse_git_webhooks_missing_is_none() {
let toml = r#"
[git]
provider = "github"
app_id = "123"
"#;
let config: FabroConfig = toml::from_str(toml).unwrap();
assert!(config.git.unwrap().webhooks.is_none());
}
#[test]
fn parse_log_config() {
let toml = "[log]\nlevel = \"trace\"";
let config: FabroConfig = toml::from_str(toml).unwrap();
assert_eq!(
config.log.as_ref().and_then(|l| l.level.as_deref()),
Some("trace")
);
}
}

View file

@ -0,0 +1,225 @@
use std::collections::HashMap;
use std::path::PathBuf;
use serde::{Deserialize, Serialize};
use crate::cli::{ExecSettings, ExecutionMode, ServerSettings};
use crate::config::FabroConfig;
use crate::hook::HookDefinition;
use crate::mcp::McpServerEntry;
use crate::project::ProjectFabroSettings;
use crate::run::{
AssetsSettings, CheckpointSettings, GitHubSettings, LlmSettings, PullRequestSettings,
SetupSettings,
};
use crate::sandbox::SandboxSettings;
use crate::server::{
ApiSettings, FeaturesSettings, GitAuthorSettings, GitSettings, LogSettings, WebSettings,
};
fn is_default_checkpoint(c: &CheckpointSettings) -> bool {
c.exclude_globs.is_empty()
}
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
pub struct FabroSettings {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub version: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub goal: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub goal_file: Option<PathBuf>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub graph: Option<String>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub labels: HashMap<String, String>,
#[serde(default, alias = "directory", skip_serializing_if = "Option::is_none")]
pub work_dir: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub llm: Option<LlmSettings>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub setup: Option<SetupSettings>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub sandbox: Option<SandboxSettings>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub vars: Option<HashMap<String, String>>,
#[serde(default, skip_serializing_if = "is_default_checkpoint")]
pub checkpoint: CheckpointSettings,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub pull_request: Option<PullRequestSettings>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub assets: Option<AssetsSettings>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub hooks: Vec<HookDefinition>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub mcp_servers: HashMap<String, McpServerEntry>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub github: Option<GitHubSettings>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub mode: Option<ExecutionMode>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub server: Option<ServerSettings>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub exec: Option<ExecSettings>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub prevent_idle_sleep: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub verbose: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub upgrade_check: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub dry_run: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub auto_approve: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub no_retro: Option<bool>,
#[serde(default, alias = "data_dir", skip_serializing_if = "Option::is_none")]
pub storage_dir: Option<PathBuf>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_concurrent_runs: Option<usize>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub web: Option<WebSettings>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub api: Option<ApiSettings>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub features: Option<FeaturesSettings>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub log: Option<LogSettings>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub git: Option<GitSettings>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub fabro: Option<ProjectFabroSettings>,
}
impl TryFrom<FabroConfig> for FabroSettings {
type Error = anyhow::Error;
fn try_from(value: FabroConfig) -> Result<Self, Self::Error> {
Ok(Self {
version: value.version,
goal: value.goal,
goal_file: value.goal_file,
graph: value.graph,
labels: value.labels,
work_dir: value.work_dir,
llm: value.llm.map(Into::into),
setup: value.setup.map(Into::into),
sandbox: value.sandbox.map(TryInto::try_into).transpose()?,
vars: value.vars,
checkpoint: value.checkpoint.into(),
pull_request: value.pull_request.map(Into::into),
assets: value.assets.map(Into::into),
hooks: value.hooks,
mcp_servers: value.mcp_servers,
github: value.github.map(Into::into),
mode: value.mode,
server: value.server.map(TryInto::try_into).transpose()?,
exec: value.exec.map(Into::into),
prevent_idle_sleep: value.prevent_idle_sleep,
verbose: value.verbose,
upgrade_check: value.upgrade_check,
dry_run: value.dry_run,
auto_approve: value.auto_approve,
no_retro: value.no_retro,
storage_dir: value.storage_dir,
max_concurrent_runs: value.max_concurrent_runs,
web: value.web.map(Into::into),
api: value.api.map(TryInto::try_into).transpose()?,
features: value.features.map(Into::into),
log: value.log.map(Into::into),
git: value.git.map(TryInto::try_into).transpose()?,
fabro: value.fabro.map(Into::into),
})
}
}
impl TryFrom<&FabroConfig> for FabroSettings {
type Error = anyhow::Error;
fn try_from(value: &FabroConfig) -> Result<Self, Self::Error> {
value.clone().try_into()
}
}
impl FabroSettings {
/// Resolve the storage directory: config value > default `~/.fabro`.
pub fn storage_dir(&self) -> PathBuf {
self.storage_dir.clone().unwrap_or_else(|| {
dirs::home_dir()
.expect("could not determine home directory")
.join(".fabro")
})
}
pub fn app_id(&self) -> Option<&str> {
self.git.as_ref().and_then(|g| g.app_id.as_deref())
}
pub fn slug(&self) -> Option<&str> {
self.git.as_ref().and_then(|g| g.slug.as_deref())
}
pub fn client_id(&self) -> Option<&str> {
self.git.as_ref().and_then(|g| g.client_id.as_deref())
}
pub fn git_author(&self) -> Option<&GitAuthorSettings> {
self.git.as_ref().map(|g| &g.author)
}
pub fn verbose_enabled(&self) -> bool {
self.verbose.unwrap_or(false)
}
pub fn prevent_idle_sleep_enabled(&self) -> bool {
self.prevent_idle_sleep.unwrap_or(false)
}
pub fn upgrade_check_enabled(&self) -> bool {
self.upgrade_check.unwrap_or(true)
}
pub fn dry_run_enabled(&self) -> bool {
self.dry_run.unwrap_or(false)
}
pub fn auto_approve_enabled(&self) -> bool {
self.auto_approve.unwrap_or(false)
}
pub fn no_retro_enabled(&self) -> bool {
self.no_retro.unwrap_or(false)
}
}

View file

@ -15,7 +15,8 @@ const WORKING_DIRECTORY: &str = "/home/daytona/workspace";
const DEFAULT_SNAPSHOT: &str = "daytona-medium";
pub use fabro_config::sandbox::{
DaytonaConfig, DaytonaNetwork, DaytonaSnapshotConfig, DockerfileSource,
DaytonaNetwork, DaytonaSettings as DaytonaConfig,
DaytonaSnapshotSettings as DaytonaSnapshotConfig, DockerfileSource,
};
/// Sandbox that runs all operations inside a Daytona cloud sandbox.

View file

@ -16,7 +16,7 @@ use tokio_util::sync::CancellationToken;
pub use crate::ssh_common::{GitCloneParams, SshOutput, SshRunner};
pub use openssh_runner::OpensshRunner;
pub use fabro_config::sandbox::ExeConfig;
pub use fabro_config::sandbox::ExeSettings as ExeConfig;
const WORKING_DIRECTORY: &str = "/home/exedev";
const PROVIDER: &str = "exe";

View file

@ -16,7 +16,7 @@ use tokio_util::sync::CancellationToken;
pub use crate::ssh_common::{GitCloneParams, SshOutput, SshRunner};
pub use openssh_runner::OpensshRunner;
pub use fabro_config::sandbox::SshConfig;
pub use fabro_config::sandbox::SshSettings as SshConfig;
const PROVIDER: &str = "ssh";

View file

@ -144,7 +144,7 @@ impl Handler for SubWorkflowHandler {
let git_state = services.git_state();
let child_run_options = RunOptions {
config: fabro_config::FabroConfig::default(),
config: fabro_config::FabroSettings::default(),
run_dir: child_logs,
cancel_token: Some(cancel_token),
dry_run: services.dry_run,

View file

@ -2,7 +2,7 @@ use std::collections::HashMap;
use std::path::{Path, PathBuf};
use chrono::{Local, Utc};
use fabro_config::config::FabroConfig;
use fabro_config::FabroSettings;
use fabro_graphviz::graph::{AttrValue, Graph};
use fabro_model::{Catalog, Provider};
@ -16,12 +16,12 @@ use crate::transforms::{expand_vars, Transform};
pub struct ValidateOptions {
pub base_dir: Option<PathBuf>,
pub custom_transforms: Vec<Box<dyn Transform>>,
pub config: Option<FabroConfig>,
pub config: Option<FabroSettings>,
pub goal_override: Option<String>,
}
pub struct RunCreateOptions {
pub config: FabroConfig,
pub config: FabroSettings,
pub run_dir: Option<PathBuf>,
pub run_id: Option<String>,
pub workflow_slug: Option<String>,
@ -96,7 +96,7 @@ fn preprocess_and_validate(
dot_source: &str,
base_dir: Option<PathBuf>,
custom_transforms: Vec<Box<dyn Transform>>,
config: Option<&FabroConfig>,
config: Option<&FabroSettings>,
goal_override: Option<&str>,
) -> Result<Validated, FabroError> {
let source = match config.and_then(|cfg| cfg.vars.as_ref()) {
@ -149,7 +149,7 @@ fn persist_validated(
base_dir: _,
} = options;
finalize_config(&mut config, validated.graph());
finalize_settings(&mut config, validated.graph());
let run_id = run_id.unwrap_or_else(|| ulid::Ulid::new().to_string());
let run_dir = run_dir.unwrap_or_else(|| default_run_dir(&run_id, config.dry_run_enabled()));
@ -177,7 +177,7 @@ fn persist_validated(
)
}
pub(crate) fn finalize_config(config: &mut FabroConfig, graph: &Graph) {
pub(crate) fn finalize_settings(config: &mut FabroSettings, graph: &Graph) {
let llm_config = config.llm.as_ref();
let configured_model = llm_config.and_then(|l| l.model.as_deref());
let configured_provider = llm_config.and_then(|l| l.provider.as_deref());
@ -308,7 +308,7 @@ mod tests {
let validated = validate(
dot,
ValidateOptions {
config: Some(FabroConfig {
config: Some(FabroSettings {
vars: Some(HashMap::from([("who".to_string(), "agent".to_string())])),
..Default::default()
}),
@ -407,7 +407,7 @@ mod tests {
let err = create(
dot,
RunCreateOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: None,
run_id: None,
workflow_slug: None,
@ -435,13 +435,13 @@ mod tests {
let persisted = create(
MINIMAL_DOT,
RunCreateOptions {
config: FabroConfig {
llm: Some(fabro_config::run::LlmConfig {
config: FabroSettings {
llm: Some(fabro_config::run::LlmSettings {
model: Some("sonnet".to_string()),
provider: None,
fallbacks: None,
}),
pull_request: Some(fabro_config::run::PullRequestConfig {
pull_request: Some(fabro_config::run::PullRequestSettings {
enabled: false,
..Default::default()
}),

View file

@ -23,7 +23,7 @@ pub struct StartFinalizeOptions {
}
pub struct StartPullRequestConfig {
pub pr_config: Option<fabro_config::run::PullRequestConfig>,
pub pr_config: Option<fabro_config::run::PullRequestSettings>,
pub github_app: Option<fabro_github::GitHubAppCredentials>,
pub origin_url: Option<String>,
pub model: String,
@ -252,7 +252,7 @@ mod tests {
use chrono::Utc;
use fabro_agent::{LocalSandbox, Sandbox};
use fabro_config::config::FabroConfig;
use fabro_config::FabroSettings;
use super::*;
use crate::context::Context;
@ -274,7 +274,7 @@ mod tests {
crate::operations::create(
dot,
crate::operations::RunCreateOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: Some(run_dir.to_path_buf()),
run_id: Some("run-test".to_string()),
workflow_slug: Some("test".to_string()),

View file

@ -7,7 +7,7 @@ use std::time::Duration;
use async_trait::async_trait;
use chrono::Utc;
use fabro_agent::Sandbox;
use fabro_config::config::FabroConfig;
use fabro_config::FabroSettings;
use fabro_graphviz::graph::{AttrValue, Edge, Graph, Node};
use fabro_hooks::HookConfig;
use fabro_interview::AutoApproveInterviewer;
@ -71,7 +71,7 @@ fn test_run_options(run_dir: &Path, run_id: &str) -> RunOptions {
cancel_token: None,
dry_run: false,
run_id: run_id.into(),
config: FabroConfig::default(),
config: FabroSettings::default(),
git: None,
host_repo_path: None,
labels: HashMap::new(),
@ -115,7 +115,7 @@ fn persisted_workflow(graph: Graph, source: String, run_dir: &Path, run_id: &str
RunRecord {
run_id: run_id.to_string(),
created_at: Utc::now(),
config: FabroConfig::default(),
config: FabroSettings::default(),
graph,
workflow_slug: Some("test".to_string()),
working_directory: std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),

View file

@ -302,7 +302,7 @@ mod tests {
use std::collections::HashMap;
use std::sync::Arc;
use fabro_config::config::FabroConfig;
use fabro_config::FabroSettings;
use fabro_graphviz::graph::Graph;
use super::*;
@ -311,7 +311,7 @@ mod tests {
fn test_run_options(run_dir: &std::path::Path) -> RunOptions {
RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: run_dir.to_path_buf(),
cancel_token: None,
dry_run: true,

View file

@ -865,7 +865,7 @@ mod tests {
use std::sync::Arc;
use chrono::Utc;
use fabro_config::config::FabroConfig;
use fabro_config::FabroSettings;
use fabro_graphviz::graph::{AttrValue, Edge, Graph, Node};
use fabro_interview::AutoApproveInterviewer;
@ -900,7 +900,7 @@ mod tests {
fn test_settings(run_dir: &std::path::Path) -> RunOptions {
RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: run_dir.to_path_buf(),
cancel_token: None,
dry_run: false,
@ -925,7 +925,7 @@ mod tests {
RunRecord {
run_id: "run-test".to_string(),
created_at: Utc::now(),
config: FabroConfig::default(),
config: FabroSettings::default(),
graph,
workflow_slug: Some("test".to_string()),
working_directory: std::env::current_dir().unwrap(),

View file

@ -56,7 +56,7 @@ mod tests {
use std::path::PathBuf;
use chrono::Utc;
use fabro_config::config::FabroConfig;
use fabro_config::FabroSettings;
use fabro_graphviz::graph::{AttrValue, Edge, Graph, Node};
use super::*;
@ -110,7 +110,7 @@ mod tests {
RunRecord {
run_id: "run-123".to_string(),
created_at: Utc::now(),
config: FabroConfig {
config: FabroSettings {
dry_run: Some(true),
verbose: Some(true),
..Default::default()

View file

@ -152,7 +152,7 @@ mod tests {
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use fabro_config::config::FabroConfig;
use fabro_config::FabroSettings;
use fabro_graphviz::graph::Graph;
use super::*;
@ -183,7 +183,7 @@ mod tests {
fn test_run_options(run_dir: &std::path::Path) -> RunOptions {
RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: run_dir.to_path_buf(),
cancel_token: None,
dry_run: true,

View file

@ -374,7 +374,7 @@ pub struct FinalizeOptions {
/// Options for the PULL_REQUEST phase.
pub struct PullRequestOptions {
pub run_dir: PathBuf,
pub pr_config: Option<fabro_config::run::PullRequestConfig>,
pub pr_config: Option<fabro_config::run::PullRequestSettings>,
pub github_app: Option<fabro_github::GitHubAppCredentials>,
pub origin_url: Option<String>,
pub model: String,

View file

@ -2,7 +2,7 @@ use std::collections::HashMap;
use std::path::{Path, PathBuf};
use chrono::{DateTime, Utc};
use fabro_config::config::FabroConfig;
use fabro_config::FabroSettings;
use fabro_graphviz::graph::Graph;
use serde::{Deserialize, Serialize};
@ -12,7 +12,7 @@ const FILE_NAME: &str = "run.json";
pub struct RunRecord {
pub run_id: String,
pub created_at: DateTime<Utc>,
pub config: FabroConfig,
pub config: FabroSettings,
pub graph: Graph,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub workflow_slug: Option<String>,
@ -73,7 +73,7 @@ mod tests {
RunRecord {
run_id: "run-abc123".to_string(),
created_at: Utc::now(),
config: FabroConfig::default(),
config: FabroSettings::default(),
graph,
workflow_slug: Some("smoke".to_string()),
working_directory: PathBuf::from("/home/user/project"),

View file

@ -3,8 +3,8 @@ use std::path::PathBuf;
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
use fabro_config::config::FabroConfig;
use fabro_config::run::PullRequestConfig;
use fabro_config::run::PullRequestSettings;
use fabro_config::FabroSettings;
use crate::git::GitAuthor;
@ -19,7 +19,7 @@ pub struct GitCheckpointOptions {
/// Options for a workflow run.
#[derive(Clone)]
pub struct RunOptions {
pub config: FabroConfig,
pub config: FabroSettings,
pub run_dir: PathBuf,
pub cancel_token: Option<Arc<AtomicBool>>,
pub dry_run: bool,
@ -49,7 +49,7 @@ impl RunOptions {
}
/// PR config (already normalized — disabled entries stripped at construction).
pub fn pull_request(&self) -> Option<&PullRequestConfig> {
pub fn pull_request(&self) -> Option<&PullRequestSettings> {
self.config.pull_request.as_ref()
}

View file

@ -8,7 +8,7 @@ use std::path::Path;
use std::sync::Arc;
use fabro_agent::Sandbox;
use fabro_config::FabroConfig;
use fabro_config::FabroSettings;
use fabro_graphviz::graph::{AttrValue, Edge, Graph, Node};
use fabro_llm::provider::Provider;
use fabro_sandbox::daytona::{DaytonaConfig, DaytonaSandbox, DaytonaSnapshotConfig};
@ -389,7 +389,7 @@ async fn daytona_pipeline_artifact_offload_and_sync() {
let engine = WorkflowRunner::new(registry, Arc::new(EventEmitter::new()), env.clone());
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -581,7 +581,7 @@ async fn daytona_git_checkpoint_remote_emits_events() {
let engine = WorkflowRunner::new(registry, Arc::new(emitter), env.clone());
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -768,7 +768,7 @@ async fn daytona_parallel_git_branching_e2e() {
let engine = WorkflowRunner::new(registry, Arc::new(emitter), Arc::clone(&env));
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: run_tmp.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -1145,7 +1145,7 @@ async fn daytona_git_checkpoint_with_shadow_branch() {
let meta_branch = MetadataStore::branch_name(&run_id);
let engine = WorkflowRunner::new(registry, Arc::new(EventEmitter::new()), env.clone());
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -1286,11 +1286,11 @@ async fn daytona_asset_collection() {
graph.edges.push(Edge::new("create_assets", "exit"));
let run_options = RunOptions {
config: FabroConfig {
assets: Some(fabro_config::run::AssetsConfig {
config: FabroSettings {
assets: Some(fabro_config::run::AssetsSettings {
include: vec!["test-results/**".to_string()],
}),
..FabroConfig::default()
..FabroSettings::default()
},
run_dir: dir.path().to_path_buf(),
cancel_token: None,
@ -1543,7 +1543,7 @@ async fn daytona_git_push_run_branch_to_origin() {
let engine = WorkflowRunner::new(registry, Arc::new(EventEmitter::new()), env.clone());
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,

View file

@ -3,7 +3,7 @@ use std::path::Path;
use std::sync::Arc;
use std::time::Duration;
use fabro_config::FabroConfig;
use fabro_config::FabroSettings;
use fabro_graphviz::graph::{AttrValue, Edge, Graph, Node};
use fabro_graphviz::parser::parse;
use fabro_interview::{
@ -194,7 +194,7 @@ async fn end_to_end_linear_pipeline() {
local_env(),
);
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -334,7 +334,7 @@ async fn end_to_end_branching_pipeline() {
let engine = WorkflowRunner::new(registry, Arc::new(EventEmitter::new()), local_env());
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -455,7 +455,7 @@ async fn end_to_end_human_gate_pipeline() {
let engine = WorkflowRunner::new(registry, Arc::new(EventEmitter::new()), local_env());
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -552,7 +552,7 @@ async fn human_gate_aborted_input_fails_closed_without_fail_route() {
let engine = WorkflowRunner::new(registry, Arc::new(EventEmitter::new()), local_env());
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -664,7 +664,7 @@ async fn human_gate_aborted_input_routes_via_outcome_fail_condition() {
let engine = WorkflowRunner::new(registry, Arc::new(EventEmitter::new()), local_env());
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -778,7 +778,7 @@ async fn goal_gate_routes_to_retry_target_on_failure() {
let engine = WorkflowRunner::new(registry, Arc::new(EventEmitter::new()), local_env());
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -900,7 +900,7 @@ async fn goal_gate_routes_to_retry_target_when_present() {
let engine = WorkflowRunner::new(registry, Arc::new(EventEmitter::new()), local_env());
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -1213,7 +1213,7 @@ async fn retry_on_failure_then_succeed() {
let engine = WorkflowRunner::new(registry, Arc::new(EventEmitter::new()), local_env());
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -1289,7 +1289,7 @@ async fn pipeline_with_many_nodes() {
local_env(),
);
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -1612,7 +1612,7 @@ async fn smoke_test_with_mock_codergen_backend() {
let engine = WorkflowRunner::new(registry, Arc::new(EventEmitter::new()), local_env());
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -1714,7 +1714,7 @@ async fn end_to_end_parallel_fan_out_fan_in() {
let engine = WorkflowRunner::new(registry, Arc::new(EventEmitter::new()), local_env());
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -1828,7 +1828,7 @@ async fn resume_from_checkpoint_completes_pipeline() {
let engine = WorkflowRunner::new(registry, Arc::new(EventEmitter::new()), local_env());
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -1928,7 +1928,7 @@ async fn resume_from_checkpoint_preserves_goal_gate_outcomes() {
let engine = WorkflowRunner::new(registry, Arc::new(EventEmitter::new()), local_env());
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -1972,7 +1972,7 @@ async fn graph_goal_in_context() {
local_env(),
);
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -2009,7 +2009,7 @@ async fn event_streaming_lifecycle() {
let events = collect_events(&emitter);
let engine = WorkflowRunner::new(make_linear_registry(), Arc::new(emitter), local_env());
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -2090,7 +2090,7 @@ async fn context_flow_between_stages() {
local_env(),
);
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -2144,7 +2144,7 @@ async fn tool_handler_e2e() {
local_env(),
);
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -2215,7 +2215,7 @@ async fn auto_approve_interviewer_e2e() {
local_env(),
);
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -2253,7 +2253,7 @@ async fn codergen_without_backend_simulated() {
local_env(),
);
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -2359,7 +2359,7 @@ async fn branching_loop_back_on_failure() {
);
let engine = WorkflowRunner::new(registry, Arc::new(EventEmitter::new()), local_env());
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -2443,7 +2443,7 @@ async fn human_gate_loops_back() {
registry.register("human", Box::new(HumanHandler::new(interviewer)));
let engine = WorkflowRunner::new(registry, Arc::new(EventEmitter::new()), local_env());
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -2502,7 +2502,7 @@ async fn scenario_ship_a_feature() {
local_env(),
);
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -2589,7 +2589,7 @@ async fn scenario_parallel_expert_review() {
let engine = WorkflowRunner::new(registry, Arc::new(EventEmitter::new()), local_env());
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -2674,7 +2674,7 @@ async fn scenario_node_retries_on_retry_status() {
);
let engine = WorkflowRunner::new(registry, Arc::new(EventEmitter::new()), local_env());
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -2737,7 +2737,7 @@ async fn scenario_loop_restart_resets_context() {
);
let engine = WorkflowRunner::new(registry, Arc::new(EventEmitter::new()), local_env());
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -2806,7 +2806,7 @@ async fn scenario_bug_triage_router() {
registry.register("conditional", Box::new(ConditionalHandler));
let engine = WorkflowRunner::new(registry, Arc::new(EventEmitter::new()), local_env());
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -2866,7 +2866,7 @@ async fn scenario_crash_recovery() {
registry.register("exit", Box::new(ExitHandler));
let engine = WorkflowRunner::new(registry, Arc::new(EventEmitter::new()), local_env());
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -2976,7 +2976,7 @@ async fn manager_loop_stop_condition_satisfied_e2e() {
registry.register("stack.manager_loop", Box::new(SubWorkflowHandler));
let engine = WorkflowRunner::new(registry, Arc::new(EventEmitter::new()), local_env());
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -3054,7 +3054,7 @@ async fn manager_loop_max_cycles_exceeded_e2e() {
registry.register("stack.manager_loop", Box::new(SubWorkflowHandler));
let engine = WorkflowRunner::new(registry, Arc::new(EventEmitter::new()), local_env());
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -3191,7 +3191,7 @@ async fn conditional_branching_success_fail_paths() {
registry.register("always_fail", Box::new(AlwaysFailHandler));
let engine = WorkflowRunner::new(registry, Arc::new(EventEmitter::new()), local_env());
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -3245,7 +3245,7 @@ async fn edge_selection_condition_match_wins_over_weight() {
registry.register("exit", Box::new(ExitHandler));
let engine = WorkflowRunner::new(registry, Arc::new(EventEmitter::new()), local_env());
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -3293,7 +3293,7 @@ async fn edge_selection_weight_breaks_ties() {
registry.register("exit", Box::new(ExitHandler));
let engine = WorkflowRunner::new(registry, Arc::new(EventEmitter::new()), local_env());
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -3333,7 +3333,7 @@ async fn edge_selection_lexical_tiebreak() {
registry.register("exit", Box::new(ExitHandler));
let engine = WorkflowRunner::new(registry, Arc::new(EventEmitter::new()), local_env());
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -3392,7 +3392,7 @@ async fn context_updates_visible_across_nodes() {
registry.register("context_setter", Box::new(ContextSetterHandler));
let engine = WorkflowRunner::new(registry, Arc::new(EventEmitter::new()), local_env());
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -3437,7 +3437,7 @@ async fn stylesheet_applies_model_override() {
local_env(),
);
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -3494,7 +3494,7 @@ async fn custom_handler_registration_and_execution() {
registry.register("my_custom", Box::new(CustomHandler));
let engine = WorkflowRunner::new(registry, Arc::new(EventEmitter::new()), local_env());
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -3566,7 +3566,7 @@ async fn integration_smoke_plan_implement_review_done() {
local_env(),
);
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -3671,7 +3671,7 @@ async fn manager_loop_runs_child_engine_e2e() {
let engine = WorkflowRunner::new(registry, Arc::new(EventEmitter::new()), local_env());
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -3806,7 +3806,7 @@ async fn manager_loop_context_flows_e2e() {
let engine = WorkflowRunner::new(registry, Arc::new(EventEmitter::new()), local_env());
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -3880,7 +3880,7 @@ async fn manager_loop_child_dotfile_e2e() {
let engine = WorkflowRunner::new(registry, Arc::new(EventEmitter::new()), local_env());
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -3994,7 +3994,7 @@ async fn graph_merge_e2e_through_engine() {
local_env(),
);
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -4145,7 +4145,7 @@ async fn fidelity_default_is_compact() {
let engine = WorkflowRunner::new(registry, Arc::new(EventEmitter::new()), local_env());
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -4203,7 +4203,7 @@ async fn fidelity_graph_default_applied() {
let engine = WorkflowRunner::new(registry, Arc::new(EventEmitter::new()), local_env());
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -4257,7 +4257,7 @@ async fn fidelity_node_overrides_graph_default() {
let engine = WorkflowRunner::new(registry, Arc::new(EventEmitter::new()), local_env());
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -4317,7 +4317,7 @@ async fn fidelity_edge_overrides_node_and_graph() {
let engine = WorkflowRunner::new(registry, Arc::new(EventEmitter::new()), local_env());
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -4367,7 +4367,7 @@ async fn fidelity_full_produces_empty_preamble() {
let engine = WorkflowRunner::new(registry, Arc::new(EventEmitter::new()), local_env());
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -4427,7 +4427,7 @@ async fn fidelity_truncate_preamble_minimal() {
let engine = WorkflowRunner::new(registry, Arc::new(EventEmitter::new()), local_env());
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -4500,7 +4500,7 @@ async fn fidelity_summary_low_mode() {
let engine = WorkflowRunner::new(registry, Arc::new(EventEmitter::new()), local_env());
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -4568,7 +4568,7 @@ async fn fidelity_summary_medium_mode() {
let engine = WorkflowRunner::new(registry, Arc::new(EventEmitter::new()), local_env());
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -4636,7 +4636,7 @@ async fn fidelity_summary_high_mode() {
let engine = WorkflowRunner::new(registry, Arc::new(EventEmitter::new()), local_env());
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -4697,7 +4697,7 @@ async fn fidelity_full_sets_thread_id_in_context() {
let engine = WorkflowRunner::new(registry, Arc::new(EventEmitter::new()), local_env());
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -4769,7 +4769,7 @@ async fn fidelity_full_nodes_share_thread_id() {
let engine = WorkflowRunner::new(registry, Arc::new(EventEmitter::new()), local_env());
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -4851,7 +4851,7 @@ async fn fidelity_resume_degrades_full_to_summary_high() {
let engine = WorkflowRunner::new(registry, Arc::new(EventEmitter::new()), local_env());
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -4949,7 +4949,7 @@ async fn fidelity_resume_degrade_only_affects_first_hop() {
let engine = WorkflowRunner::new(registry, Arc::new(EventEmitter::new()), local_env());
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -5034,7 +5034,7 @@ async fn fidelity_resume_no_degrade_when_not_full() {
let engine = WorkflowRunner::new(registry, Arc::new(EventEmitter::new()), local_env());
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -5077,7 +5077,7 @@ async fn fidelity_stored_in_checkpoint_context() {
let engine = WorkflowRunner::new(registry, Arc::new(EventEmitter::new()), local_env());
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -5164,7 +5164,7 @@ async fn fidelity_precedence_multi_node_pipeline() {
let engine = WorkflowRunner::new(registry, Arc::new(EventEmitter::new()), local_env());
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -5233,7 +5233,7 @@ async fn fidelity_compact_preamble_includes_completed_stages_and_context() {
let engine = WorkflowRunner::new(registry, Arc::new(EventEmitter::new()), local_env());
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -5309,7 +5309,7 @@ async fn fidelity_summary_low_excludes_context_values_in_pipeline() {
);
let engine_low = WorkflowRunner::new(registry_low, Arc::new(EventEmitter::new()), local_env());
let run_options_low = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: dir_low.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -5377,7 +5377,7 @@ async fn fidelity_summary_low_excludes_context_values_in_pipeline() {
);
let engine_med = WorkflowRunner::new(registry_med, Arc::new(EventEmitter::new()), local_env());
let run_options_med = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: dir_med.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -5449,7 +5449,7 @@ async fn fidelity_thread_id_fallback_to_previous_node_in_pipeline() {
let engine = WorkflowRunner::new(registry, Arc::new(EventEmitter::new()), local_env());
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -5504,7 +5504,7 @@ async fn fidelity_thread_id_from_node_class_in_pipeline() {
let engine = WorkflowRunner::new(registry, Arc::new(EventEmitter::new()), local_env());
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -5562,7 +5562,7 @@ async fn fidelity_edge_thread_id_override_in_pipeline() {
let engine = WorkflowRunner::new(registry, Arc::new(EventEmitter::new()), local_env());
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -5621,7 +5621,7 @@ async fn fidelity_full_without_explicit_thread_id_uses_previous_node() {
let engine = WorkflowRunner::new(registry, Arc::new(EventEmitter::new()), local_env());
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -5690,7 +5690,7 @@ async fn fidelity_from_parsed_dot_pipeline() {
let engine = WorkflowRunner::new(registry, Arc::new(EventEmitter::new()), local_env());
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -5739,7 +5739,7 @@ async fn fidelity_checkpoint_roundtrip_preserves_fidelity() {
let engine = WorkflowRunner::new(registry, Arc::new(EventEmitter::new()), local_env());
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -5810,7 +5810,7 @@ async fn fidelity_node_thread_id_overrides_edge_thread_id_in_pipeline() {
let engine = WorkflowRunner::new(registry, Arc::new(EventEmitter::new()), local_env());
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -5898,7 +5898,7 @@ async fn fidelity_resume_preserves_context_values_across_checkpoint() {
let engine = WorkflowRunner::new(registry, Arc::new(EventEmitter::new()), local_env());
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -5941,7 +5941,7 @@ mod real_llm {
use async_trait::async_trait;
use fabro_config::FabroConfig;
use fabro_config::FabroSettings;
use fabro_graphviz::graph::Node;
use fabro_workflows::context::Context;
use fabro_workflows::error::FabroError;
@ -6114,7 +6114,7 @@ mod real_llm {
let engine = WorkflowRunner::new(registry, Arc::new(EventEmitter::new()), local_env());
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -6229,7 +6229,7 @@ mod real_llm {
let engine = WorkflowRunner::new(registry, Arc::new(EventEmitter::new()), local_env());
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -6369,7 +6369,7 @@ mod real_llm {
let engine = WorkflowRunner::new(registry, Arc::new(EventEmitter::new()), local_env());
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -6477,7 +6477,7 @@ mod real_llm {
let engine = WorkflowRunner::new(registry, Arc::new(EventEmitter::new()), local_env());
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -6574,7 +6574,7 @@ async fn human_gate_freeform_only_routes_text() {
let engine = WorkflowRunner::new(registry, Arc::new(EventEmitter::new()), local_env());
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -6705,7 +6705,7 @@ async fn human_gate_freeform_with_fixed_choice_match() {
let engine = WorkflowRunner::new(registry, Arc::new(EventEmitter::new()), local_env());
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -6821,7 +6821,7 @@ async fn human_gate_freeform_fallback_on_unmatched_text() {
let engine = WorkflowRunner::new(registry, Arc::new(EventEmitter::new()), local_env());
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -6950,7 +6950,7 @@ async fn human_gate_freeform_sets_allow_freeform_on_question() {
let engine = WorkflowRunner::new(registry, Arc::new(EventEmitter::new()), local_env());
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -7059,7 +7059,7 @@ async fn human_gate_without_freeform_sets_allow_freeform_false() {
let engine = WorkflowRunner::new(registry, Arc::new(EventEmitter::new()), local_env());
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -7341,7 +7341,7 @@ fn engine_with_hooks_and_events(
fn make_run_options(dir: &std::path::Path) -> RunOptions {
RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: dir.to_path_buf(),
cancel_token: None,
dry_run: false,
@ -8025,7 +8025,7 @@ event = "run_complete"
command = "echo done"
"#;
let cfg: FabroConfig = toml::from_str(toml).unwrap();
let cfg: FabroSettings = toml::from_str(toml).unwrap();
assert_eq!(cfg.hooks.len(), 2);
assert_eq!(cfg.hooks[0].event, fabro_hooks::HookEvent::StageStart);
assert_eq!(cfg.hooks[0].matcher.as_deref(), Some("agent_loop"));
@ -8205,7 +8205,7 @@ max_tool_rounds = 10
timeout_ms = 120000
"#;
let cfg: FabroConfig = toml::from_str(toml).unwrap();
let cfg: FabroSettings = toml::from_str(toml).unwrap();
assert_eq!(cfg.hooks.len(), 2);
// Prompt hook
@ -8446,7 +8446,7 @@ async fn arc_e2e_with_real_llm() {
let run_dir = tempfile::tempdir().unwrap();
let engine = WorkflowRunner::new(registry, Arc::new(EventEmitter::new()), local_env());
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: run_dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -8575,7 +8575,7 @@ async fn run_fidelity_prompt_pipeline(fidelity: &str) -> String {
let engine = WorkflowRunner::new(registry, Arc::new(EventEmitter::new()), local_env());
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -8775,7 +8775,7 @@ async fn large_context_values_are_offloaded_to_artifact_store() {
let events = collect_events(&emitter);
let engine = WorkflowRunner::new(registry, Arc::new(emitter), local_env());
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -8995,7 +8995,7 @@ async fn artifact_pointers_rewritten_for_remote_sandbox() {
let remote_env = Arc::new(RemoteMockEnv::new("/sandbox"));
let engine = WorkflowRunner::new(registry, Arc::new(EventEmitter::new()), remote_env.clone());
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -9126,7 +9126,7 @@ async fn node_dir_uses_visit_count_on_revisit() {
let engine = WorkflowRunner::new(registry, Arc::new(EventEmitter::new()), local_env());
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -10099,7 +10099,7 @@ async fn full_pipeline_with_cli_backend_node() {
let dir = tempfile::tempdir().unwrap();
let engine = WorkflowRunner::new(registry, Arc::new(EventEmitter::new()), env);
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -10231,7 +10231,7 @@ async fn stylesheet_backend_property_routes_to_cli() {
let dir = tempfile::tempdir().unwrap();
let engine = WorkflowRunner::new(registry, Arc::new(EventEmitter::new()), env);
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -10511,7 +10511,7 @@ async fn git_checkpoint_host_emits_events_and_diff_patch() {
let engine = WorkflowRunner::new(registry, Arc::new(emitter), env);
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: run_dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -10715,7 +10715,7 @@ async fn git_checkpoint_host_writes_shadow_branch() {
let meta_branch = MetadataStore::branch_name(run_id);
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: run_dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -10914,7 +10914,7 @@ async fn parallel_git_branching_host_e2e() {
let engine = WorkflowRunner::new(registry, Arc::new(emitter), env);
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: run_dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -11178,7 +11178,7 @@ async fn git_checkpoint_host_skips_empty_diff_patch() {
let engine = WorkflowRunner::new(registry, Arc::new(emitter), env);
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: run_dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -11561,7 +11561,7 @@ async fn e2e_circuit_breaker_deterministic_self_loop() {
let engine = WorkflowRunner::new(registry, Arc::new(EventEmitter::new()), local_env());
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -11609,7 +11609,7 @@ async fn e2e_circuit_breaker_custom_limit() {
let engine = WorkflowRunner::new(registry, Arc::new(EventEmitter::new()), local_env());
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -11650,7 +11650,7 @@ async fn e2e_circuit_breaker_ignores_transient_failures() {
let engine = WorkflowRunner::new(registry, Arc::new(EventEmitter::new()), local_env());
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -11698,7 +11698,7 @@ async fn e2e_circuit_breaker_different_reasons_separate_counters() {
let engine = WorkflowRunner::new(registry, Arc::new(EventEmitter::new()), local_env());
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -11739,7 +11739,7 @@ async fn e2e_circuit_breaker_loop_restart() {
let engine = WorkflowRunner::new(registry, Arc::new(EventEmitter::new()), local_env());
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -11802,7 +11802,7 @@ async fn e2e_failure_signature_persisted_in_context() {
let engine = WorkflowRunner::new(registry, Arc::new(EventEmitter::new()), local_env());
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -11867,7 +11867,7 @@ async fn e2e_failure_signature_hint_overrides_reason_in_context() {
let engine = WorkflowRunner::new(registry, Arc::new(EventEmitter::new()), local_env());
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -11924,7 +11924,7 @@ async fn e2e_signature_maps_persist_in_checkpoint() {
let engine = WorkflowRunner::new(registry, Arc::new(EventEmitter::new()), local_env());
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -12052,7 +12052,7 @@ async fn e2e_circuit_breaker_emits_events_before_abort() {
let engine = WorkflowRunner::new(registry, Arc::new(emitter), local_env());
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -12120,7 +12120,7 @@ async fn e2e_circuit_breaker_does_not_fire_below_limit() {
let engine = WorkflowRunner::new(registry, Arc::new(EventEmitter::new()), local_env());
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -12217,7 +12217,7 @@ async fn e2e_circuit_breaker_multi_stage_impl_verify_cycle() {
let engine = WorkflowRunner::new(registry, Arc::new(EventEmitter::new()), local_env());
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -12315,7 +12315,7 @@ async fn e2e_loop_restart_blocked_for_deterministic_failure() {
let engine = WorkflowRunner::new(registry, Arc::new(EventEmitter::new()), local_env());
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -12356,7 +12356,7 @@ async fn e2e_loop_restart_blocked_for_structural_failure() {
let engine = WorkflowRunner::new(registry, Arc::new(EventEmitter::new()), local_env());
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -12397,7 +12397,7 @@ async fn e2e_loop_restart_blocked_for_budget_exhausted_failure() {
let engine = WorkflowRunner::new(registry, Arc::new(EventEmitter::new()), local_env());
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -12438,7 +12438,7 @@ async fn e2e_loop_restart_blocked_for_canceled_failure() {
let engine = WorkflowRunner::new(registry, Arc::new(EventEmitter::new()), local_env());
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -12476,7 +12476,7 @@ async fn e2e_loop_restart_blocked_for_compilation_loop_failure() {
let engine = WorkflowRunner::new(registry, Arc::new(EventEmitter::new()), local_env());
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -12518,7 +12518,7 @@ async fn e2e_loop_restart_allowed_for_transient_infra() {
let engine = WorkflowRunner::new(registry, Arc::new(EventEmitter::new()), local_env());
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -12623,7 +12623,7 @@ async fn e2e_stall_watchdog_triggers_from_dot_parsed_pipeline() {
let engine = WorkflowRunner::new(registry, Arc::new(emitter), local_env());
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -12680,7 +12680,7 @@ async fn e2e_stall_watchdog_kept_alive_by_handler_events() {
let engine = WorkflowRunner::new(registry, Arc::new(EventEmitter::new()), local_env());
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -12727,7 +12727,7 @@ async fn e2e_stall_watchdog_disabled_with_zero_timeout() {
let engine = WorkflowRunner::new(registry, Arc::new(EventEmitter::new()), local_env());
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -12793,7 +12793,7 @@ async fn e2e_stall_watchdog_with_explicit_timeout_override() {
let engine = WorkflowRunner::new(registry, Arc::new(EventEmitter::new()), local_env());
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,
@ -12925,11 +12925,11 @@ async fn asset_collection_local_sandbox_success() {
graph.edges.push(Edge::new("create_assets", "exit"));
let run_options = RunOptions {
config: FabroConfig {
assets: Some(fabro_config::run::AssetsConfig {
config: FabroSettings {
assets: Some(fabro_config::run::AssetsSettings {
include: vec!["test-results/**".to_string()],
}),
..FabroConfig::default()
..FabroSettings::default()
},
run_dir: run_dir.path().to_path_buf(),
cancel_token: None,
@ -13040,11 +13040,11 @@ async fn asset_collection_local_sandbox_on_failure() {
graph.edges.push(Edge::new("create_assets", "exit"));
let run_options = RunOptions {
config: FabroConfig {
assets: Some(fabro_config::run::AssetsConfig {
config: FabroSettings {
assets: Some(fabro_config::run::AssetsSettings {
include: vec!["test-results/**".to_string()],
}),
..FabroConfig::default()
..FabroSettings::default()
},
run_dir: run_dir.path().to_path_buf(),
cancel_token: None,
@ -13138,11 +13138,11 @@ async fn asset_collection_docker_sandbox() {
graph.edges.push(Edge::new("create_assets", "exit"));
let run_options = RunOptions {
config: FabroConfig {
assets: Some(fabro_config::run::AssetsConfig {
config: FabroSettings {
assets: Some(fabro_config::run::AssetsSettings {
include: vec!["test-results/**".to_string()],
}),
..FabroConfig::default()
..FabroSettings::default()
},
run_dir: run_dir.path().to_path_buf(),
cancel_token: None,
@ -13213,7 +13213,7 @@ async fn wait_timer_e2e() {
local_env(),
);
let run_options = RunOptions {
config: FabroConfig::default(),
config: FabroSettings::default(),
run_dir: dir.path().to_path_buf(),
cancel_token: None,
dry_run: false,