mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-08-28 05:27:41 +00:00
refactor(settings): resolve cli settings
This commit is contained in:
parent
5dd6b88415
commit
a93bf1df9c
18 changed files with 427 additions and 161 deletions
|
|
@ -2,7 +2,7 @@ use std::path::{Path, PathBuf};
|
|||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{Context as _, Result, bail};
|
||||
use fabro_types::settings::SettingsFile;
|
||||
use fabro_types::settings::{CliSettings, SettingsFile};
|
||||
use tokio::sync::OnceCell;
|
||||
|
||||
use crate::args::{ServerConnectionArgs, ServerTargetArgs};
|
||||
|
|
@ -25,6 +25,7 @@ pub(crate) struct CommandContext {
|
|||
cwd: PathBuf,
|
||||
base_config_path: PathBuf,
|
||||
machine_settings: SettingsFile,
|
||||
cli_settings: CliSettings,
|
||||
server_mode: ServerMode,
|
||||
server: OnceCell<Arc<ServerStoreClient>>,
|
||||
}
|
||||
|
|
@ -57,11 +58,13 @@ impl CommandContext {
|
|||
..
|
||||
} => user_config::load_settings_with_storage_dir(storage_dir_override.as_deref())?,
|
||||
};
|
||||
let cli_settings = user_config::resolve_cli_settings(&machine_settings)?;
|
||||
|
||||
Ok(Self {
|
||||
cwd,
|
||||
base_config_path,
|
||||
machine_settings,
|
||||
cli_settings,
|
||||
server_mode,
|
||||
server: OnceCell::new(),
|
||||
})
|
||||
|
|
@ -79,6 +82,10 @@ impl CommandContext {
|
|||
&self.machine_settings
|
||||
}
|
||||
|
||||
pub(crate) fn cli_settings(&self) -> &CliSettings {
|
||||
&self.cli_settings
|
||||
}
|
||||
|
||||
pub(crate) async fn server(&self) -> Result<Arc<ServerStoreClient>> {
|
||||
let server_mode = self.server_mode.clone();
|
||||
let base_config_path = self.base_config_path.clone();
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ use fabro_agent::cli::{OutputFormat, run_with_args, run_with_args_and_client};
|
|||
use fabro_llm::client::Client;
|
||||
use fabro_llm::providers::FabroServerAdapter;
|
||||
use fabro_types::settings::InterpString;
|
||||
use fabro_types::settings::cli::OutputFormat as SettingsOutputFormat;
|
||||
use fabro_types::settings::run::McpEntryLayer;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
|
@ -99,29 +100,35 @@ pub(crate) async fn execute(mut args: ExecArgs, globals: &GlobalArgs) -> Result<
|
|||
use fabro_types::settings::run::AgentPermissions;
|
||||
|
||||
let cli_settings = user_config::load_settings()?;
|
||||
let resolved_cli = user_config::resolve_cli_settings(&cli_settings)?;
|
||||
#[cfg(feature = "sleep_inhibitor")]
|
||||
let _sleep_guard = crate::sleep_inhibitor::guard(cli_settings.prevent_idle_sleep_enabled());
|
||||
let exec_defaults = cli_settings.cli_exec();
|
||||
let exec_model = exec_defaults.and_then(|e| e.model.as_ref());
|
||||
let exec_agent = exec_defaults.and_then(|e| e.agent.as_ref());
|
||||
let provider_str = exec_model
|
||||
.and_then(|m| m.provider.as_ref())
|
||||
let _sleep_guard = crate::sleep_inhibitor::guard(resolved_cli.exec.prevent_idle_sleep);
|
||||
let provider_str = resolved_cli
|
||||
.exec
|
||||
.model
|
||||
.provider
|
||||
.as_ref()
|
||||
.map(InterpString::as_source);
|
||||
let model_str = exec_model
|
||||
.and_then(|m| m.name.as_ref())
|
||||
let model_str = resolved_cli
|
||||
.exec
|
||||
.model
|
||||
.name
|
||||
.as_ref()
|
||||
.map(InterpString::as_source);
|
||||
let permissions = exec_agent
|
||||
.and_then(|agent| agent.permissions)
|
||||
.map(|p| match p {
|
||||
AgentPermissions::ReadOnly => AgentPermissionLevel::ReadOnly,
|
||||
AgentPermissions::ReadWrite => AgentPermissionLevel::ReadWrite,
|
||||
AgentPermissions::Full => AgentPermissionLevel::Full,
|
||||
});
|
||||
let permissions = resolved_cli.exec.agent.permissions.map(|p| match p {
|
||||
AgentPermissions::ReadOnly => AgentPermissionLevel::ReadOnly,
|
||||
AgentPermissions::ReadWrite => AgentPermissionLevel::ReadWrite,
|
||||
AgentPermissions::Full => AgentPermissionLevel::Full,
|
||||
});
|
||||
let output_format = Some(match resolved_cli.output.format {
|
||||
SettingsOutputFormat::Text => OutputFormat::Text,
|
||||
SettingsOutputFormat::Json => OutputFormat::Json,
|
||||
});
|
||||
args.agent.apply_cli_defaults(
|
||||
provider_str.as_deref(),
|
||||
model_str.as_deref(),
|
||||
permissions,
|
||||
None,
|
||||
output_format,
|
||||
);
|
||||
if globals.json {
|
||||
args.agent.output_format = Some(OutputFormat::Json);
|
||||
|
|
@ -130,52 +137,48 @@ pub(crate) async fn execute(mut args: ExecArgs, globals: &GlobalArgs) -> Result<
|
|||
// v2 MCPs live under `cli.exec.agent.mcps` (owner-specific) or
|
||||
// `run.agent.mcps`. For `fabro exec` we use the cli.exec path, falling
|
||||
// back to run.agent.mcps if unset.
|
||||
let mcp_servers: Vec<fabro_mcp::config::McpServerSettings> = if let Some(mcps) = exec_agent
|
||||
.map(|agent| &agent.mcps)
|
||||
.filter(|mcps| !mcps.is_empty())
|
||||
{
|
||||
mcps.iter()
|
||||
.map(|(name, entry)| runtime_mcp_server(name, entry))
|
||||
.collect()
|
||||
} else {
|
||||
fabro_config::resolve_run_from_file(&cli_settings)
|
||||
.map(|settings| {
|
||||
settings
|
||||
.agent
|
||||
.mcps
|
||||
.values()
|
||||
.map(|server| fabro_mcp::config::McpServerSettings {
|
||||
name: server.name.clone(),
|
||||
transport: match &server.transport {
|
||||
fabro_types::settings::run::McpTransport::Stdio { command, env } => {
|
||||
fabro_mcp::config::McpTransport::Stdio {
|
||||
command: command.clone(),
|
||||
env: env.clone(),
|
||||
}
|
||||
}
|
||||
fabro_types::settings::run::McpTransport::Http { url, headers } => {
|
||||
fabro_mcp::config::McpTransport::Http {
|
||||
url: url.clone(),
|
||||
headers: headers.clone(),
|
||||
}
|
||||
}
|
||||
fabro_types::settings::run::McpTransport::Sandbox {
|
||||
command,
|
||||
port,
|
||||
env,
|
||||
} => fabro_mcp::config::McpTransport::Sandbox {
|
||||
command: command.clone(),
|
||||
port: *port,
|
||||
env: env.clone(),
|
||||
},
|
||||
},
|
||||
startup_timeout_secs: server.startup_timeout_secs,
|
||||
tool_timeout_secs: server.tool_timeout_secs,
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
};
|
||||
let mcp_servers: Vec<fabro_mcp::config::McpServerSettings> =
|
||||
if !resolved_cli.exec.agent.mcps.is_empty() {
|
||||
resolved_cli
|
||||
.exec
|
||||
.agent
|
||||
.mcps
|
||||
.values()
|
||||
.map(|server| fabro_mcp::config::McpServerSettings {
|
||||
name: server.name.clone(),
|
||||
transport: server.transport.clone(),
|
||||
startup_timeout_secs: server.startup_timeout_secs,
|
||||
tool_timeout_secs: server.tool_timeout_secs,
|
||||
})
|
||||
.collect()
|
||||
} else if let Some(mcps) = cli_settings
|
||||
.cli
|
||||
.as_ref()
|
||||
.and_then(|cli| cli.exec.as_ref())
|
||||
.and_then(|exec| exec.agent.as_ref())
|
||||
.map(|agent| &agent.mcps)
|
||||
.filter(|mcps| !mcps.is_empty())
|
||||
{
|
||||
mcps.iter()
|
||||
.map(|(name, entry)| runtime_mcp_server(name, entry))
|
||||
.collect()
|
||||
} else {
|
||||
fabro_config::resolve_run_from_file(&cli_settings)
|
||||
.map(|settings| {
|
||||
settings
|
||||
.agent
|
||||
.mcps
|
||||
.values()
|
||||
.map(|server| fabro_mcp::config::McpServerSettings {
|
||||
name: server.name.clone(),
|
||||
transport: server.transport.clone(),
|
||||
startup_timeout_secs: server.startup_timeout_secs,
|
||||
tool_timeout_secs: server.tool_timeout_secs,
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
};
|
||||
if let Some(target) = server_target {
|
||||
tracing::info!(transport = "server", "Agent session starting");
|
||||
let provider_name = args
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
use anyhow::bail;
|
||||
use fabro_config::ConfigLayer;
|
||||
use fabro_types::settings::cli::OutputVerbosity;
|
||||
use fabro_util::terminal::Styles;
|
||||
|
||||
use crate::args::{GlobalArgs, PreflightArgs};
|
||||
|
|
@ -13,7 +14,7 @@ use crate::shared::print_json_pretty;
|
|||
pub(crate) async fn execute(mut args: PreflightArgs, globals: &GlobalArgs) -> anyhow::Result<()> {
|
||||
let styles: &'static Styles = Box::leak(Box::new(Styles::detect_stderr()));
|
||||
let ctx = CommandContext::for_target(&args.target)?;
|
||||
args.verbose = args.verbose || ctx.machine_settings().verbose_enabled();
|
||||
args.verbose = args.verbose || ctx.cli_settings().output.verbosity == OutputVerbosity::Verbose;
|
||||
|
||||
let manifest = build_run_manifest(ManifestBuildInput {
|
||||
workflow: args.workflow.clone(),
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ use fabro_types::{EventBody, RunEvent, RunId};
|
|||
use fabro_api::types;
|
||||
use fabro_interview::{AnswerValue, ConsoleInterviewer, Question, QuestionOption, QuestionType};
|
||||
use fabro_store::EventEnvelope;
|
||||
use fabro_types::settings::cli::OutputVerbosity;
|
||||
use fabro_util::json::normalize_json_value;
|
||||
use fabro_util::terminal::Styles;
|
||||
use fabro_workflow::outcome::StageStatus;
|
||||
|
|
@ -69,10 +70,11 @@ pub(crate) async fn attach_run_with_client(
|
|||
})
|
||||
.unwrap_or(false)
|
||||
});
|
||||
let verbose = state
|
||||
.run
|
||||
.as_ref()
|
||||
.is_some_and(|record| record.settings.verbose_enabled());
|
||||
let verbose = state.run.as_ref().is_some_and(|record| {
|
||||
fabro_config::resolve_cli_from_file(&record.settings)
|
||||
.map(|settings| settings.output.verbosity == OutputVerbosity::Verbose)
|
||||
.unwrap_or(false)
|
||||
});
|
||||
let events = client.list_run_events(run_id, None, None).await?;
|
||||
let replay_events = events.clone();
|
||||
let next_seq = events.last().map_or(1, |event| event.seq.saturating_add(1));
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
use anyhow::Result;
|
||||
use fabro_types::settings::cli::OutputVerbosity;
|
||||
use fabro_util::terminal::Styles;
|
||||
|
||||
use crate::args::{GlobalArgs, RunArgs};
|
||||
|
|
@ -10,10 +11,10 @@ pub(crate) async fn execute(mut args: RunArgs, globals: &GlobalArgs) -> Result<(
|
|||
let styles: &'static Styles = Box::leak(Box::new(Styles::detect_stderr()));
|
||||
let ctx = CommandContext::for_target(&args.target)?;
|
||||
let cli = settings_layer_with_storage_dir(None)?;
|
||||
args.verbose = args.verbose || ctx.machine_settings().verbose_enabled();
|
||||
args.verbose = args.verbose || ctx.cli_settings().output.verbosity == OutputVerbosity::Verbose;
|
||||
|
||||
let quiet = args.detach;
|
||||
let prevent_idle_sleep = ctx.machine_settings().prevent_idle_sleep_enabled();
|
||||
let prevent_idle_sleep = ctx.cli_settings().exec.prevent_idle_sleep;
|
||||
let created_run = Box::pin(super::create::create_run(&ctx, &args, cli, styles, quiet)).await?;
|
||||
|
||||
#[cfg(feature = "sleep_inhibitor")]
|
||||
|
|
|
|||
|
|
@ -97,7 +97,7 @@ pub(crate) async fn dispatch(cmd: RunCommands, globals: &GlobalArgs) -> Result<(
|
|||
#[cfg(feature = "sleep_inhibitor")]
|
||||
let _sleep_guard = {
|
||||
let ctx = CommandContext::for_target(&args.server)?;
|
||||
crate::sleep_inhibitor::guard(ctx.machine_settings().prevent_idle_sleep_enabled())
|
||||
crate::sleep_inhibitor::guard(ctx.cli_settings().exec.prevent_idle_sleep)
|
||||
};
|
||||
resume::resume_command(args, styles, globals).await
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ use args::{Commands, GlobalArgs, LONG_VERSION, RunCommands, ServerCommand, Serve
|
|||
use clap::{CommandFactory, Parser};
|
||||
use fabro_config::user::load_settings_config;
|
||||
use fabro_telemetry::{git, panic as tel_panic, sanitize, sender};
|
||||
use fabro_types::settings::cli::OutputVerbosity;
|
||||
use fabro_util::printer::Printer;
|
||||
use fabro_util::terminal::Styles;
|
||||
use rustls::crypto::ring::default_provider;
|
||||
|
|
@ -145,14 +146,10 @@ async fn main_inner() -> (String, Result<()>) {
|
|||
}
|
||||
} else {
|
||||
match user_config::load_settings() {
|
||||
Ok(cli_settings) => (
|
||||
cli_settings
|
||||
.cli
|
||||
.as_ref()
|
||||
.and_then(|c| c.logging.as_ref())
|
||||
.and_then(|l| l.level.clone()),
|
||||
cli_settings.upgrade_check_enabled(),
|
||||
),
|
||||
Ok(cli_settings) => match user_config::resolve_cli_settings(&cli_settings) {
|
||||
Ok(resolved_cli) => (resolved_cli.logging.level, resolved_cli.updates.check),
|
||||
Err(err) => return (command_name, Err(err)),
|
||||
},
|
||||
Err(err) => return (command_name, Err(err)),
|
||||
}
|
||||
}
|
||||
|
|
@ -207,7 +204,11 @@ async fn main_inner() -> (String, Result<()>) {
|
|||
}
|
||||
Commands::Doctor(args) => {
|
||||
let cli_settings = user_config::load_settings()?;
|
||||
let verbose = args.verbose || cli_settings.verbose_enabled();
|
||||
let verbose = args.verbose
|
||||
|| user_config::resolve_cli_settings(&cli_settings)?
|
||||
.output
|
||||
.verbosity
|
||||
== OutputVerbosity::Verbose;
|
||||
let exit_code = commands::doctor::run_doctor(&args, verbose, &globals).await?;
|
||||
std::process::exit(exit_code);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@ pub(crate) use fabro_config::user::*;
|
|||
|
||||
use anyhow::{Result, bail};
|
||||
use fabro_config::ConfigLayer;
|
||||
use fabro_types::settings::SettingsFile;
|
||||
use fabro_types::settings::cli::CliTargetSettings;
|
||||
use fabro_types::settings::{CliSettings, SettingsFile};
|
||||
use fabro_util::version::FABRO_VERSION;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing::debug;
|
||||
|
|
@ -50,6 +51,21 @@ pub(crate) fn load_settings_with_config_and_storage_dir(
|
|||
Ok(settings_layer_with_config_and_storage_dir(config_path, storage_dir)?.into())
|
||||
}
|
||||
|
||||
fn render_resolve_errors(errors: Vec<fabro_config::ResolveError>) -> anyhow::Error {
|
||||
anyhow::anyhow!(
|
||||
"failed to resolve cli settings:\n{}",
|
||||
errors
|
||||
.into_iter()
|
||||
.map(|error| error.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn resolve_cli_settings(file: &SettingsFile) -> anyhow::Result<CliSettings> {
|
||||
fabro_config::resolve_cli_from_file(file).map_err(render_resolve_errors)
|
||||
}
|
||||
|
||||
pub(crate) fn apply_storage_dir_override(
|
||||
mut layer: ConfigLayer,
|
||||
storage_dir: Option<&Path>,
|
||||
|
|
@ -77,36 +93,29 @@ pub(crate) enum ServerTarget {
|
|||
UnixSocket(PathBuf),
|
||||
}
|
||||
|
||||
/// Pull the CLI target configuration out of the v2 `[cli.target]` stanza.
|
||||
/// Pull the resolved CLI target configuration out of `[cli.target]`.
|
||||
/// Returns `(target_string, tls)` where `target_string` is either an
|
||||
/// http(s) URL or a unix socket path. `tls` is the CLI-side client TLS
|
||||
/// settings extracted from `[cli.target.http.tls]`.
|
||||
fn cli_target_from_v2(settings: &SettingsFile) -> Option<(String, Option<ClientTlsSettings>)> {
|
||||
use fabro_types::settings::cli::CliTargetLayer;
|
||||
use fabro_types::settings::interp::InterpString;
|
||||
|
||||
let target = settings.cli.as_ref()?.target.as_ref()?;
|
||||
/// http(s) URL or a unix socket path.
|
||||
fn cli_target_from_settings(settings: &CliSettings) -> Option<(String, Option<ClientTlsSettings>)> {
|
||||
let target = settings.target.as_ref()?;
|
||||
match target {
|
||||
CliTargetLayer::Http { url, tls } => {
|
||||
let url_str = url.as_ref().map(InterpString::as_source)?;
|
||||
CliTargetSettings::Http { url, tls } => {
|
||||
let tls_settings = tls.as_ref().and_then(|tls| {
|
||||
Some(ClientTlsSettings {
|
||||
cert: PathBuf::from(tls.cert.as_ref().map(InterpString::as_source)?),
|
||||
key: PathBuf::from(tls.key.as_ref().map(InterpString::as_source)?),
|
||||
ca: PathBuf::from(tls.ca.as_ref().map(InterpString::as_source)?),
|
||||
cert: PathBuf::from(tls.cert.as_source()),
|
||||
key: PathBuf::from(tls.key.as_source()),
|
||||
ca: PathBuf::from(tls.ca.as_source()),
|
||||
})
|
||||
});
|
||||
Some((url_str, tls_settings))
|
||||
Some((url.as_source(), tls_settings))
|
||||
}
|
||||
CliTargetLayer::Unix { path } => path
|
||||
.as_ref()
|
||||
.map(InterpString::as_source)
|
||||
.map(|path_str| (path_str, None)),
|
||||
CliTargetSettings::Unix { path } => Some((path.as_source(), None)),
|
||||
}
|
||||
}
|
||||
|
||||
fn configured_server_target(settings: &SettingsFile) -> Result<Option<ServerTarget>> {
|
||||
let Some((value, tls)) = cli_target_from_v2(settings) else {
|
||||
let cli_settings = resolve_cli_settings(settings)?;
|
||||
let Some((value, tls)) = cli_target_from_settings(&cli_settings) else {
|
||||
return Ok(None);
|
||||
};
|
||||
parse_server_target(&value, tls).map(Some)
|
||||
|
|
@ -136,9 +145,13 @@ fn explicit_server_target(
|
|||
args: &ServerTargetArgs,
|
||||
settings: &SettingsFile,
|
||||
) -> Result<Option<ServerTarget>> {
|
||||
let cli_settings = resolve_cli_settings(settings)?;
|
||||
args.as_deref()
|
||||
.map(|value| {
|
||||
parse_server_target(value, cli_target_from_v2(settings).and_then(|(_, tls)| tls))
|
||||
parse_server_target(
|
||||
value,
|
||||
cli_target_from_settings(&cli_settings).and_then(|(_, tls)| tls),
|
||||
)
|
||||
})
|
||||
.transpose()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,6 +35,10 @@ fn parse_settings(stdout: &[u8]) -> SettingsFile {
|
|||
serde_yaml::from_slice(stdout).expect("stdout should be valid YAML SettingsFile")
|
||||
}
|
||||
|
||||
fn resolve_cli(settings: &SettingsFile) -> fabro_types::settings::CliSettings {
|
||||
fabro_config::resolve_cli_from_file(settings).expect("cli settings should resolve")
|
||||
}
|
||||
|
||||
fn server_settings_fixture() -> SettingsFile {
|
||||
ConfigLayer::parse(
|
||||
r#"
|
||||
|
|
@ -582,7 +586,10 @@ name = "legacy-model"
|
|||
.stderr(predicate::str::contains("Rename it to"));
|
||||
|
||||
let cfg = parse_settings(&assert.get_output().stdout);
|
||||
assert!(!cfg.verbose_enabled());
|
||||
assert_eq!(
|
||||
resolve_cli(&cfg).output.verbosity,
|
||||
fabro_types::settings::cli::OutputVerbosity::Normal
|
||||
);
|
||||
assert!(cfg.run_model().is_none());
|
||||
}
|
||||
|
||||
|
|
@ -756,7 +763,10 @@ shared = "cli"
|
|||
cfg.server_storage_root_str().as_deref(),
|
||||
Some("/srv/fabro-server")
|
||||
);
|
||||
assert!(cfg.verbose_enabled());
|
||||
assert_eq!(
|
||||
resolve_cli(&cfg).output.verbosity,
|
||||
fabro_types::settings::cli::OutputVerbosity::Verbose
|
||||
);
|
||||
|
||||
// R22: run.inputs replaces wholesale across layers. Project is the
|
||||
// highest-precedence layer that sets inputs, so project's vars win
|
||||
|
|
|
|||
|
|
@ -353,6 +353,7 @@ fn create_persists_requested_overrides_into_store() {
|
|||
"team": run_record.labels.get("team"),
|
||||
});
|
||||
let settings = &run_record.settings;
|
||||
let cli_settings = fabro_config::resolve_cli_from_file(settings).expect("cli settings");
|
||||
let compact = json!({
|
||||
"workflow_slug": run_record.workflow_slug,
|
||||
"settings": {
|
||||
|
|
@ -360,7 +361,7 @@ fn create_persists_requested_overrides_into_store() {
|
|||
"dry_run": settings.dry_run_enabled(),
|
||||
"auto_approve": settings.auto_approve_enabled(),
|
||||
"no_retro": settings.no_retro_enabled(),
|
||||
"verbose": settings.verbose_enabled(),
|
||||
"verbose": cli_settings.output.verbosity == fabro_types::settings::cli::OutputVerbosity::Verbose,
|
||||
"llm": {
|
||||
"model": settings.run_model_name_str(),
|
||||
"provider": settings.run_model_provider_str(),
|
||||
|
|
|
|||
|
|
@ -15,7 +15,8 @@ pub use config::ConfigLayer;
|
|||
pub use fabro_util::path::expand_tilde;
|
||||
pub use home::Home;
|
||||
pub use resolve::{
|
||||
ResolveError, resolve_run, resolve_run_from_file, resolve_server, resolve_server_from_file,
|
||||
ResolveError, resolve_cli, resolve_cli_from_file, resolve_run, resolve_run_from_file,
|
||||
resolve_server, resolve_server_from_file,
|
||||
};
|
||||
pub use storage::{RunScratch, ServerState, Storage};
|
||||
|
||||
|
|
|
|||
95
lib/crates/fabro-config/src/resolve/cli.rs
Normal file
95
lib/crates/fabro-config/src/resolve/cli.rs
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
use fabro_types::settings::cli::{
|
||||
CliAuthSettings, CliExecAgentSettings, CliExecLayer, CliExecModelSettings, CliExecSettings,
|
||||
CliLayer, CliLoggingSettings, CliOutputSettings, CliSettings, CliTargetLayer,
|
||||
CliTargetSettings, CliTargetTlsSettings, CliUpdatesSettings, OutputFormat, OutputVerbosity,
|
||||
};
|
||||
|
||||
use super::{ResolveError, require_interp};
|
||||
|
||||
pub fn resolve_cli(layer: &CliLayer, errors: &mut Vec<ResolveError>) -> CliSettings {
|
||||
CliSettings {
|
||||
target: resolve_target(layer.target.as_ref(), errors),
|
||||
auth: CliAuthSettings {
|
||||
strategy: layer.auth.as_ref().and_then(|auth| auth.strategy),
|
||||
},
|
||||
exec: resolve_exec(layer.exec.as_ref()),
|
||||
output: CliOutputSettings {
|
||||
format: layer
|
||||
.output
|
||||
.as_ref()
|
||||
.and_then(|output| output.format)
|
||||
.unwrap_or(OutputFormat::Text),
|
||||
verbosity: layer
|
||||
.output
|
||||
.as_ref()
|
||||
.and_then(|output| output.verbosity)
|
||||
.unwrap_or(OutputVerbosity::Normal),
|
||||
},
|
||||
updates: CliUpdatesSettings {
|
||||
check: layer
|
||||
.updates
|
||||
.as_ref()
|
||||
.and_then(|updates| updates.check)
|
||||
.unwrap_or(true),
|
||||
},
|
||||
logging: CliLoggingSettings {
|
||||
level: layer
|
||||
.logging
|
||||
.as_ref()
|
||||
.and_then(|logging| logging.level.clone()),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_target(
|
||||
target: Option<&CliTargetLayer>,
|
||||
errors: &mut Vec<ResolveError>,
|
||||
) -> Option<CliTargetSettings> {
|
||||
match target {
|
||||
Some(CliTargetLayer::Http { url, tls }) => Some(CliTargetSettings::Http {
|
||||
url: require_interp(url.as_ref(), "cli.target.url", errors),
|
||||
tls: tls.as_ref().map(|tls| CliTargetTlsSettings {
|
||||
cert: require_interp(tls.cert.as_ref(), "cli.target.tls.cert", errors),
|
||||
key: require_interp(tls.key.as_ref(), "cli.target.tls.key", errors),
|
||||
ca: require_interp(tls.ca.as_ref(), "cli.target.tls.ca", errors),
|
||||
}),
|
||||
}),
|
||||
Some(CliTargetLayer::Unix { path }) => Some(CliTargetSettings::Unix {
|
||||
path: require_interp(path.as_ref(), "cli.target.path", errors),
|
||||
}),
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_exec(exec: Option<&CliExecLayer>) -> CliExecSettings {
|
||||
let Some(exec) = exec else {
|
||||
return CliExecSettings::default();
|
||||
};
|
||||
|
||||
CliExecSettings {
|
||||
prevent_idle_sleep: exec.prevent_idle_sleep.unwrap_or(false),
|
||||
model: CliExecModelSettings {
|
||||
provider: exec.model.as_ref().and_then(|model| model.provider.clone()),
|
||||
name: exec.model.as_ref().and_then(|model| model.name.clone()),
|
||||
},
|
||||
agent: CliExecAgentSettings {
|
||||
permissions: exec.agent.as_ref().and_then(|agent| agent.permissions),
|
||||
mcps: exec
|
||||
.agent
|
||||
.as_ref()
|
||||
.map(|agent| {
|
||||
agent
|
||||
.mcps
|
||||
.iter()
|
||||
.map(|(name, entry)| {
|
||||
(
|
||||
name.clone(),
|
||||
super::run::resolve_mcp_entry(name.as_str(), entry),
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +1,26 @@
|
|||
mod cli;
|
||||
mod error;
|
||||
mod run;
|
||||
mod server;
|
||||
|
||||
use fabro_types::settings::{RunSettings, ServerSettings, SettingsFile};
|
||||
use fabro_types::settings::{CliSettings, RunSettings, ServerSettings, SettingsFile};
|
||||
|
||||
pub use cli::resolve_cli;
|
||||
pub use error::ResolveError;
|
||||
pub use run::resolve_run;
|
||||
pub use server::resolve_server;
|
||||
|
||||
pub fn resolve_cli_from_file(file: &SettingsFile) -> Result<CliSettings, Vec<ResolveError>> {
|
||||
let mut errors = Vec::new();
|
||||
let layer = file.cli.as_ref().cloned().unwrap_or_default();
|
||||
let resolved = resolve_cli(&layer, &mut errors);
|
||||
if errors.is_empty() {
|
||||
Ok(resolved)
|
||||
} else {
|
||||
Err(errors)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resolve_server_from_file(file: &SettingsFile) -> Result<ServerSettings, Vec<ResolveError>> {
|
||||
let mut errors = Vec::new();
|
||||
let layer = file.server.as_ref().cloned().unwrap_or_default();
|
||||
|
|
|
|||
|
|
@ -269,7 +269,7 @@ fn resolve_agent(agent: Option<&RunAgentLayer>) -> RunAgentSettings {
|
|||
}
|
||||
}
|
||||
|
||||
fn resolve_mcp_entry(name: &str, entry: &McpEntryLayer) -> McpServerSettings {
|
||||
pub(crate) fn resolve_mcp_entry(name: &str, entry: &McpEntryLayer) -> McpServerSettings {
|
||||
let transport = match entry {
|
||||
McpEntryLayer::Stdio {
|
||||
script,
|
||||
|
|
|
|||
93
lib/crates/fabro-config/tests/resolve_cli.rs
Normal file
93
lib/crates/fabro-config/tests/resolve_cli.rs
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
use fabro_config::resolve_cli_from_file;
|
||||
use fabro_types::settings::SettingsFile;
|
||||
use fabro_types::settings::cli::{CliTargetSettings, OutputFormat, OutputVerbosity};
|
||||
use fabro_types::settings::run::AgentPermissions;
|
||||
|
||||
#[test]
|
||||
fn resolves_cli_defaults_from_empty_settings() {
|
||||
let settings = SettingsFile::default();
|
||||
|
||||
let cli = resolve_cli_from_file(&settings).expect("empty settings should resolve");
|
||||
|
||||
assert!(cli.target.is_none());
|
||||
assert_eq!(cli.output.format, OutputFormat::Text);
|
||||
assert_eq!(cli.output.verbosity, OutputVerbosity::Normal);
|
||||
assert!(!cli.exec.prevent_idle_sleep);
|
||||
assert!(cli.updates.check);
|
||||
assert!(cli.logging.level.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolves_cli_target_exec_and_output_settings() {
|
||||
let settings: SettingsFile = fabro_config::ConfigLayer::parse(
|
||||
r#"
|
||||
_version = 1
|
||||
|
||||
[cli.target]
|
||||
type = "http"
|
||||
url = "https://config.example.com"
|
||||
|
||||
[cli.target.tls]
|
||||
cert = "cert.pem"
|
||||
key = "key.pem"
|
||||
ca = "ca.pem"
|
||||
|
||||
[cli.exec]
|
||||
prevent_idle_sleep = true
|
||||
|
||||
[cli.exec.model]
|
||||
provider = "openai"
|
||||
name = "gpt-5"
|
||||
|
||||
[cli.exec.agent]
|
||||
permissions = "read-only"
|
||||
|
||||
[cli.exec.agent.mcps.fs]
|
||||
type = "stdio"
|
||||
command = ["echo", "cli"]
|
||||
|
||||
[cli.output]
|
||||
format = "json"
|
||||
verbosity = "verbose"
|
||||
|
||||
[cli.updates]
|
||||
check = false
|
||||
|
||||
[cli.logging]
|
||||
level = "debug"
|
||||
"#,
|
||||
)
|
||||
.expect("fixture should parse")
|
||||
.into();
|
||||
|
||||
let cli = resolve_cli_from_file(&settings).expect("cli settings should resolve");
|
||||
|
||||
let CliTargetSettings::Http { url, tls } = cli.target.expect("target") else {
|
||||
panic!("expected http target");
|
||||
};
|
||||
assert_eq!(url.as_source(), "https://config.example.com");
|
||||
let tls = tls.expect("tls");
|
||||
assert_eq!(tls.cert.as_source(), "cert.pem");
|
||||
assert_eq!(tls.key.as_source(), "key.pem");
|
||||
assert_eq!(tls.ca.as_source(), "ca.pem");
|
||||
|
||||
assert!(cli.exec.prevent_idle_sleep);
|
||||
assert_eq!(
|
||||
cli.exec
|
||||
.model
|
||||
.provider
|
||||
.as_ref()
|
||||
.map(|value| value.as_source()),
|
||||
Some("openai".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
cli.exec.model.name.as_ref().map(|value| value.as_source()),
|
||||
Some("gpt-5".to_string())
|
||||
);
|
||||
assert_eq!(cli.exec.agent.permissions, Some(AgentPermissions::ReadOnly));
|
||||
assert_eq!(cli.exec.agent.mcps["fs"].name, "fs");
|
||||
assert_eq!(cli.output.format, OutputFormat::Json);
|
||||
assert_eq!(cli.output.verbosity, OutputVerbosity::Verbose);
|
||||
assert!(!cli.updates.check);
|
||||
assert_eq!(cli.logging.level.as_deref(), Some("debug"));
|
||||
}
|
||||
|
|
@ -8,7 +8,6 @@
|
|||
use std::collections::HashMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use super::cli::{CliExecLayer, CliLayer, CliOutputLayer};
|
||||
use super::interp::InterpString;
|
||||
use super::project::ProjectLayer;
|
||||
use super::run::{
|
||||
|
|
@ -284,49 +283,6 @@ impl SettingsFile {
|
|||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
// ---------- cli-scope ----------
|
||||
|
||||
#[must_use]
|
||||
pub fn cli_layer(&self) -> Option<&CliLayer> {
|
||||
self.cli.as_ref()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn cli_exec(&self) -> Option<&CliExecLayer> {
|
||||
self.cli.as_ref().and_then(|c| c.exec.as_ref())
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn cli_output(&self) -> Option<&CliOutputLayer> {
|
||||
self.cli.as_ref().and_then(|c| c.output.as_ref())
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn verbose_enabled(&self) -> bool {
|
||||
use super::cli::OutputVerbosity;
|
||||
matches!(
|
||||
self.cli_output().and_then(|o| o.verbosity),
|
||||
Some(OutputVerbosity::Verbose)
|
||||
)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn prevent_idle_sleep_enabled(&self) -> bool {
|
||||
self.cli_exec()
|
||||
.and_then(|e| e.prevent_idle_sleep)
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Upgrade check defaults to `true` when unset.
|
||||
#[must_use]
|
||||
pub fn upgrade_check_enabled(&self) -> bool {
|
||||
self.cli
|
||||
.as_ref()
|
||||
.and_then(|c| c.updates.as_ref())
|
||||
.and_then(|u| u.check)
|
||||
.unwrap_or(true)
|
||||
}
|
||||
|
||||
// ---------- server-scope ----------
|
||||
|
||||
#[must_use]
|
||||
|
|
@ -611,12 +567,6 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn upgrade_check_defaults_to_true_when_unset() {
|
||||
let file = SettingsFile::default();
|
||||
assert!(file.upgrade_check_enabled());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_labels_merges_project_workflow_run() {
|
||||
use crate::settings::project::ProjectLayer;
|
||||
|
|
|
|||
|
|
@ -9,7 +9,76 @@ use std::collections::HashMap;
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::interp::InterpString;
|
||||
use super::run::{AgentPermissions, McpEntryLayer};
|
||||
use super::run::{AgentPermissions, McpEntryLayer, McpServerSettings};
|
||||
|
||||
/// A structurally resolved `[cli]` view for consumers.
|
||||
#[derive(Debug, Clone, Default, PartialEq)]
|
||||
pub struct CliSettings {
|
||||
pub target: Option<CliTargetSettings>,
|
||||
pub auth: CliAuthSettings,
|
||||
pub exec: CliExecSettings,
|
||||
pub output: CliOutputSettings,
|
||||
pub updates: CliUpdatesSettings,
|
||||
pub logging: CliLoggingSettings,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum CliTargetSettings {
|
||||
Http {
|
||||
url: InterpString,
|
||||
tls: Option<CliTargetTlsSettings>,
|
||||
},
|
||||
Unix {
|
||||
path: InterpString,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct CliTargetTlsSettings {
|
||||
pub cert: InterpString,
|
||||
pub key: InterpString,
|
||||
pub ca: InterpString,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq)]
|
||||
pub struct CliAuthSettings {
|
||||
pub strategy: Option<CliAuthStrategy>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq)]
|
||||
pub struct CliExecSettings {
|
||||
pub prevent_idle_sleep: bool,
|
||||
pub model: CliExecModelSettings,
|
||||
pub agent: CliExecAgentSettings,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq)]
|
||||
pub struct CliExecModelSettings {
|
||||
pub provider: Option<InterpString>,
|
||||
pub name: Option<InterpString>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq)]
|
||||
pub struct CliExecAgentSettings {
|
||||
pub permissions: Option<AgentPermissions>,
|
||||
pub mcps: HashMap<String, McpServerSettings>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq)]
|
||||
pub struct CliOutputSettings {
|
||||
pub format: OutputFormat,
|
||||
pub verbosity: OutputVerbosity,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq)]
|
||||
pub struct CliUpdatesSettings {
|
||||
pub check: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq)]
|
||||
pub struct CliLoggingSettings {
|
||||
pub level: Option<String>,
|
||||
}
|
||||
|
||||
/// A sparse `[cli]` layer as it appears in a single settings file.
|
||||
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
|
||||
|
|
@ -115,17 +184,19 @@ pub struct CliOutputLayer {
|
|||
pub verbosity: Option<OutputVerbosity>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum OutputFormat {
|
||||
#[default]
|
||||
Text,
|
||||
Json,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum OutputVerbosity {
|
||||
Quiet,
|
||||
#[default]
|
||||
Normal,
|
||||
Verbose,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,11 @@ pub mod tree;
|
|||
pub mod version;
|
||||
pub mod workflow;
|
||||
|
||||
pub use cli::CliLayer;
|
||||
pub use cli::{
|
||||
CliAuthSettings, CliExecAgentSettings, CliExecModelSettings, CliExecSettings, CliLayer,
|
||||
CliLoggingSettings, CliOutputSettings, CliSettings, CliTargetSettings, CliTargetTlsSettings,
|
||||
CliUpdatesSettings,
|
||||
};
|
||||
pub use duration::{Duration, ParseDurationError};
|
||||
pub use features::FeaturesLayer;
|
||||
pub use interp::{InterpString, Provenance, ResolveEnvError, Resolved};
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue