mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-08-28 05:27:41 +00:00
Refactor config layering into combine plus settings
This commit is contained in:
parent
126ece3d71
commit
8a256bb68e
52 changed files with 1502 additions and 1619 deletions
10
Cargo.lock
generated
10
Cargo.lock
generated
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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<()> {
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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"));
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
14
lib/crates/fabro-config-derive/Cargo.toml
Normal file
14
lib/crates/fabro-config-derive/Cargo.toml
Normal 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"] }
|
||||
52
lib/crates/fabro-config-derive/src/lib.rs
Normal file
52
lib/crates/fabro-config-derive/src/lib.rs
Normal 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()
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
60
lib/crates/fabro-config/src/combine.rs
Normal file
60
lib/crates/fabro-config/src/combine.rs
Normal 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,);
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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::*;
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
225
lib/crates/fabro-config/src/settings.rs
Normal file
225
lib/crates/fabro-config/src/settings.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -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()),
|
||||
|
|
|
|||
|
|
@ -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(".")),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue