From 635d3d588acd5d8a845027ab9806f3fde39ef158 Mon Sep 17 00:00:00 2001 From: fabro-bot Date: Mon, 4 May 2026 18:23:31 +0000 Subject: [PATCH] feat(dev): add bootstrap_catalog reference allowlist policy test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per the settings-driven LLM catalog plan: the install/API-key flow needs a built-in catalog before any project settings are loaded, so fabro_model::bootstrap_catalog is the explicit hatch for that. Once the bootstrap module lands, request-serving paths must use the resolved Arc threaded through their state — never the bootstrap default. This policy test scans every .rs file under lib/ for non-comment references to 'bootstrap_catalog' and fails unless every reference sits in an allowlisted path fragment (the bootstrap module itself, install flows, test_support modules, and tests/policy.rs). Today the symbol does not yet exist, so the scan is clean and the test passes; the moment the symbol is introduced and used outside the allowlist, this test fails and forces an explicit allowlist update — fulfilling the workspace-policy requirement in the plan. Also lands rustfmt + clippy hygiene on this slice and drops the unused Result wrapper from infallible adapter factories so the factory registry remains plain function pointers. --- .../fabro-cli/src/commands/run/overrides.rs | 2 +- lib/crates/fabro-config/src/builders.rs | 2 +- lib/crates/fabro-config/src/layers/llm.rs | 84 ++++++-------- lib/crates/fabro-config/src/layers/mod.rs | 2 +- lib/crates/fabro-config/src/layers/run.rs | 2 +- .../fabro-config/src/layers/settings.rs | 2 +- lib/crates/fabro-config/src/lib.rs | 12 +- lib/crates/fabro-config/src/parse.rs | 12 +- lib/crates/fabro-config/src/resolve/run.rs | 10 +- .../fabro-config/src/tests/resolve_run.rs | 2 +- lib/crates/fabro-dev/tests/it/main.rs | 3 +- lib/crates/fabro-dev/tests/it/policy.rs | 106 ++++++++++++++++++ lib/crates/fabro-llm/src/adapter_registry.rs | 51 +++++---- lib/crates/fabro-llm/src/lib.rs | 2 +- lib/crates/fabro-model/src/adapter.rs | 11 +- lib/crates/fabro-model/src/billing.rs | 1 + lib/crates/fabro-model/src/ids.rs | 7 +- lib/crates/fabro-model/src/reasoning.rs | 15 ++- lib/crates/fabro-server/src/run_manifest.rs | 2 +- lib/crates/fabro-types/src/settings/mod.rs | 2 +- lib/crates/fabro-types/src/settings/run.rs | 2 +- 21 files changed, 221 insertions(+), 111 deletions(-) create mode 100644 lib/crates/fabro-dev/tests/it/policy.rs diff --git a/lib/crates/fabro-cli/src/commands/run/overrides.rs b/lib/crates/fabro-cli/src/commands/run/overrides.rs index 20de26666..7746d486c 100644 --- a/lib/crates/fabro-cli/src/commands/run/overrides.rs +++ b/lib/crates/fabro-cli/src/commands/run/overrides.rs @@ -230,4 +230,4 @@ mod tests { .is_none() ); } -} \ No newline at end of file +} diff --git a/lib/crates/fabro-config/src/builders.rs b/lib/crates/fabro-config/src/builders.rs index bd0892726..38d2f2cd7 100644 --- a/lib/crates/fabro-config/src/builders.rs +++ b/lib/crates/fabro-config/src/builders.rs @@ -542,4 +542,4 @@ command = ["demo-mcp"] assert_eq!(settings.run.execution.approval, ApprovalMode::Auto); assert!(!settings.run.execution.retros); } -} \ No newline at end of file +} diff --git a/lib/crates/fabro-config/src/layers/llm.rs b/lib/crates/fabro-config/src/layers/llm.rs index f19a528d4..343e65a5e 100644 --- a/lib/crates/fabro-config/src/layers/llm.rs +++ b/lib/crates/fabro-config/src/layers/llm.rs @@ -184,11 +184,12 @@ where D: Deserializer<'de>, { use serde::de::Error; + use toml::value::Datetime; #[derive(Deserialize)] #[serde(untagged)] enum Either { - Toml(toml::value::Datetime), + Toml(Datetime), Str(String), } @@ -310,8 +311,9 @@ impl TryFrom for CredentialRef { impl<'de> Deserialize<'de> for CredentialRef { fn deserialize>(deserializer: D) -> Result { + use serde::de::Error; let s = String::deserialize(deserializer)?; - s.parse().map_err(serde::de::Error::custom) + s.parse().map_err(D::Error::custom) } } @@ -428,6 +430,10 @@ mod tests { // CredentialRef. The error bubbles up as a TOML deserialization // failure. #[derive(Deserialize)] + #[expect( + dead_code, + reason = "field exists only to drive the deserializer; we assert on the parse error" + )] struct Wrap { v: Vec, } @@ -453,20 +459,14 @@ aliases = ["moonshot"] let kimi = layer.providers.get("kimi").unwrap(); assert_eq!(kimi.display_name.as_deref(), Some("Kimi")); assert_eq!(kimi.adapter.as_deref(), Some("openai_compatible")); - assert_eq!( - kimi.base_url.as_deref(), - Some("https://api.moonshot.ai/v1") - ); + assert_eq!(kimi.base_url.as_deref(), Some("https://api.moonshot.ai/v1")); assert_eq!(kimi.priority, Some(60)); assert_eq!(kimi.enabled, Some(true)); assert_eq!(kimi.aliases.as_deref(), Some(&["moonshot".to_string()][..])); - assert_eq!( - kimi.credentials.as_ref().unwrap(), - &vec![ - CredentialRef::Credential("kimi".to_string()), - CredentialRef::Env("KIMI_API_KEY".to_string()), - ] - ); + assert_eq!(kimi.credentials.as_ref().unwrap(), &vec![ + CredentialRef::Credential("kimi".to_string()), + CredentialRef::Env("KIMI_API_KEY".to_string()), + ]); } #[test] @@ -548,18 +548,9 @@ cache_input_cost_per_mtok = 9.0 let controls = m.controls.as_ref().unwrap(); assert_eq!( controls.reasoning_effort.as_deref(), - Some( - &[ - "low".to_string(), - "medium".to_string(), - "high".to_string() - ][..] - ) - ); - assert_eq!( - controls.speed.as_deref(), - Some(&["fast".to_string()][..]) + Some(&["low".to_string(), "medium".to_string(), "high".to_string()][..]) ); + assert_eq!(controls.speed.as_deref(), Some(&["fast".to_string()][..])); let costs = m.costs.as_ref().unwrap(); let fast = costs.speed.as_ref().unwrap().get("fast").unwrap(); @@ -629,10 +620,9 @@ mystery = 1 ..ProviderSettings::default() }; let merged = high.combine(low); - assert_eq!( - merged.credentials.unwrap(), - vec![CredentialRef::Env("FOO".to_string())] - ); + assert_eq!(merged.credentials.unwrap(), vec![CredentialRef::Env( + "FOO".to_string() + )]); } #[test] @@ -643,35 +633,28 @@ mystery = 1 ..ProviderSettings::default() }; let merged = high.combine(low); - assert_eq!( - merged.credentials.unwrap(), - vec![CredentialRef::Env("FOO".to_string())] - ); + assert_eq!(merged.credentials.unwrap(), vec![CredentialRef::Env( + "FOO".to_string() + )]); } #[test] fn merge_map_field_merges_per_provider_id() { let mut high_map: std::collections::HashMap = std::collections::HashMap::new(); - high_map.insert( - "kimi".to_string(), - ProviderSettings { - base_url: Some("https://override".to_string()), - ..ProviderSettings::default() - }, - ); + high_map.insert("kimi".to_string(), ProviderSettings { + base_url: Some("https://override".to_string()), + ..ProviderSettings::default() + }); let high: MergeMap = MergeMap::from(high_map); let mut low_map: std::collections::HashMap = std::collections::HashMap::new(); - low_map.insert( - "kimi".to_string(), - ProviderSettings { - adapter: Some("openai_compatible".to_string()), - base_url: Some("https://defaults".to_string()), - ..ProviderSettings::default() - }, - ); + low_map.insert("kimi".to_string(), ProviderSettings { + adapter: Some("openai_compatible".to_string()), + base_url: Some("https://defaults".to_string()), + ..ProviderSettings::default() + }); let low: MergeMap = MergeMap::from(low_map); let merged = high.combine(low); @@ -693,7 +676,10 @@ mystery = 1 speed: Some(vec!["fast".to_string()]), }; let merged = high.combine(low); - assert_eq!(merged.reasoning_effort.as_deref(), Some(&["high".to_string()][..])); + assert_eq!( + merged.reasoning_effort.as_deref(), + Some(&["high".to_string()][..]) + ); assert_eq!(merged.speed.as_deref(), Some(&["fast".to_string()][..])); } -} \ No newline at end of file +} diff --git a/lib/crates/fabro-config/src/layers/mod.rs b/lib/crates/fabro-config/src/layers/mod.rs index 97b951920..37d2e7106 100644 --- a/lib/crates/fabro-config/src/layers/mod.rs +++ b/lib/crates/fabro-config/src/layers/mod.rs @@ -43,4 +43,4 @@ pub use server::{ SlackIntegrationLayer, TeamsIntegrationLayer, }; pub(crate) use settings::SettingsLayer; -pub use workflow::WorkflowLayer; \ No newline at end of file +pub use workflow::WorkflowLayer; diff --git a/lib/crates/fabro-config/src/layers/run.rs b/lib/crates/fabro-config/src/layers/run.rs index 593807f24..40b0cc55d 100644 --- a/lib/crates/fabro-config/src/layers/run.rs +++ b/lib/crates/fabro-config/src/layers/run.rs @@ -602,4 +602,4 @@ pub struct RunPullRequestLayer { pub struct RunArtifactsLayer { #[serde(default, skip_serializing_if = "Vec::is_empty")] pub include: Vec, -} \ No newline at end of file +} diff --git a/lib/crates/fabro-config/src/layers/settings.rs b/lib/crates/fabro-config/src/layers/settings.rs index 17785b53b..273470ac7 100644 --- a/lib/crates/fabro-config/src/layers/settings.rs +++ b/lib/crates/fabro-config/src/layers/settings.rs @@ -134,4 +134,4 @@ impl SettingsLayer { let auth = server.auth.get_or_insert_with(ServerAuthLayer::default); auth.methods = Some(vec![ServerAuthMethod::DevToken]); } -} \ No newline at end of file +} diff --git a/lib/crates/fabro-config/src/lib.rs b/lib/crates/fabro-config/src/lib.rs index 3df4af086..90fc5211f 100644 --- a/lib/crates/fabro-config/src/lib.rs +++ b/lib/crates/fabro-config/src/lib.rs @@ -48,11 +48,11 @@ pub use layers::{ ReplaceMap, RunAgentLayer, RunArtifactsLayer, RunCheckpointLayer, RunExecutionLayer, RunGitLayer, RunGoalLayer, RunLayer, RunModelControlsLayer, RunModelLayer, RunPrepareLayer, RunPullRequestLayer, RunSandboxLayer, RunScmLayer, ScmGitHubLayer, ServerApiLayer, - ServerArtifactsLayer, - ServerAuthGithubLayer, ServerAuthLayer, ServerIntegrationsLayer, ServerIpAllowlistLayer, - ServerIpAllowlistOverrideLayer, ServerLayer, ServerListenLayer, ServerLoggingLayer, - ServerSchedulerLayer, ServerSlateDbLayer, ServerStorageLayer, ServerWebLayer, - SlackIntegrationLayer, StickyMap, StringOrSplice, TeamsIntegrationLayer, WorkflowLayer, + ServerArtifactsLayer, ServerAuthGithubLayer, ServerAuthLayer, ServerIntegrationsLayer, + ServerIpAllowlistLayer, ServerIpAllowlistOverrideLayer, ServerLayer, ServerListenLayer, + ServerLoggingLayer, ServerSchedulerLayer, ServerSlateDbLayer, ServerStorageLayer, + ServerWebLayer, SlackIntegrationLayer, StickyMap, StringOrSplice, TeamsIntegrationLayer, + WorkflowLayer, }; pub(crate) use layers::{Combine, SettingsLayer}; pub use logging::{resolve_log_destination, resolve_log_destination_with_env}; @@ -88,4 +88,4 @@ where Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(T::default()), Err(e) => Err(Error::read_file(&default_path, e)), } -} \ No newline at end of file +} diff --git a/lib/crates/fabro-config/src/parse.rs b/lib/crates/fabro-config/src/parse.rs index e109b64bb..f3519958a 100644 --- a/lib/crates/fabro-config/src/parse.rs +++ b/lib/crates/fabro-config/src/parse.rs @@ -216,7 +216,10 @@ mod tests { .parse::() .unwrap_err(); let text = err.to_string(); - assert!(text.contains("run.model") || text.contains("llm"), "got: {text}"); + assert!( + text.contains("run.model") || text.contains("llm"), + "got: {text}" + ); } #[test] @@ -225,6 +228,9 @@ mod tests { .parse::() .unwrap_err(); let text = err.to_string(); - assert!(text.contains("run.model") || text.contains("llm"), "got: {text}"); + assert!( + text.contains("run.model") || text.contains("llm"), + "got: {text}" + ); } -} \ No newline at end of file +} diff --git a/lib/crates/fabro-config/src/resolve/run.rs b/lib/crates/fabro-config/src/resolve/run.rs index c557845cd..2dcb5e67d 100644 --- a/lib/crates/fabro-config/src/resolve/run.rs +++ b/lib/crates/fabro-config/src/resolve/run.rs @@ -4,9 +4,9 @@ use fabro_types::settings::run::{ GitAuthorSettings, HookDefinition, HookType, InterviewProviderSettings, LocalSandboxSettings, McpServerSettings, McpTransport, MergeStrategy, NotificationProviderSettings, NotificationRouteSettings, PullRequestSettings, RunAgentSettings, RunCheckpointSettings, - RunExecutionSettings, RunGitSettings, RunGoal, RunInterviewsSettings, RunModelSettings, - RunNamespace, RunPrepareSettings, RunSandboxSettings, RunScmSettings, ScmGitHubSettings, - TlsMode, + RunExecutionSettings, RunGitSettings, RunGoal, RunInterviewsSettings, RunModelControls, + RunModelSettings, RunNamespace, RunPrepareSettings, RunSandboxSettings, RunScmSettings, + ScmGitHubSettings, TlsMode, }; use super::ResolveError; @@ -75,7 +75,7 @@ fn resolve_model(model: Option<&RunModelLayer>) -> RunModelSettings { controls: model .controls .as_ref() - .map(|c| fabro_types::settings::RunModelControls { + .map(|c| RunModelControls { reasoning_effort: c.reasoning_effort.clone(), speed: c.speed.clone(), }) @@ -505,4 +505,4 @@ fn resolve_artifacts(artifacts: Option<&RunArtifactsLayer>) -> ArtifactsSettings fn size_to_gb_i32(bytes: u64) -> i32 { let gb = bytes / 1_000_000_000; i32::try_from(gb).unwrap_or(i32::MAX) -} \ No newline at end of file +} diff --git a/lib/crates/fabro-config/src/tests/resolve_run.rs b/lib/crates/fabro-config/src/tests/resolve_run.rs index e2cfbaf17..fe2d56dc9 100644 --- a/lib/crates/fabro-config/src/tests/resolve_run.rs +++ b/lib/crates/fabro-config/src/tests/resolve_run.rs @@ -110,4 +110,4 @@ name = "sonnet" Some(InterpString::parse("anthropic")) ); assert_eq!(settings.model.name, Some(InterpString::parse("sonnet"))); -} \ No newline at end of file +} diff --git a/lib/crates/fabro-dev/tests/it/main.rs b/lib/crates/fabro-dev/tests/it/main.rs index fd6afc5f5..ad662e9ab 100644 --- a/lib/crates/fabro-dev/tests/it/main.rs +++ b/lib/crates/fabro-dev/tests/it/main.rs @@ -4,6 +4,7 @@ mod docker_build; #[cfg(unix)] mod docker_entrypoint; mod docs; +mod policy; mod release; mod spa; @@ -11,7 +12,7 @@ fn fabro_dev() -> assert_cmd::Command { assert_cmd::cargo::cargo_bin_cmd!("fabro-dev") } -fn workspace_root() -> PathBuf { +pub(crate) fn workspace_root() -> PathBuf { let mut root = PathBuf::from(env!("CARGO_MANIFEST_DIR")); root.pop(); root.pop(); diff --git a/lib/crates/fabro-dev/tests/it/policy.rs b/lib/crates/fabro-dev/tests/it/policy.rs new file mode 100644 index 000000000..37678b353 --- /dev/null +++ b/lib/crates/fabro-dev/tests/it/policy.rs @@ -0,0 +1,106 @@ +//! Workspace policy tests. +//! +//! These tests scan the source tree for references that violate +//! product-level invariants. They run as part of `cargo nextest` and are +//! cheap (text scans only). + +use std::path::{Path, PathBuf}; + +use crate::workspace_root; + +/// `fabro_model::bootstrap_catalog` (and its module) is the install/API-key +/// validation hatch from the settings-driven LLM catalog plan. It must +/// **not** appear in request-serving paths — server handlers, workflow +/// operations, agent runtime, hooks, or completion handlers — because those +/// must use the resolved `Arc` threaded through their state. +/// +/// The allowed-callers list below is the policy boundary. Adding a new +/// caller is intentional and requires updating this list. +const BOOTSTRAP_CATALOG_ALLOWED_PATH_FRAGMENTS: &[&str] = &[ + // The bootstrap module itself. + "lib/crates/fabro-model/src/bootstrap_catalog", + // Install / first-run / API-key validation flows that legitimately need + // a built-in catalog before any project settings have been loaded. + "lib/crates/fabro-install/", + "lib/crates/fabro-cli/src/commands/install/", + "lib/crates/fabro-cli/src/shared/install_", + "lib/crates/fabro-cli/src/shared/api_key_validation", + // Test support modules. + "tests/", + "test_support", + "/tests/it/", + "/tests/policy.rs", + // Documentation files referencing the policy. + "docs/", + "CLAUDE.md", + "AGENTS.md", +]; + +#[test] +fn bootstrap_catalog_references_stay_in_allowlist() { + let root = workspace_root(); + let mut violations: Vec<(PathBuf, usize, String)> = Vec::new(); + walk_rust_sources(&root, &mut |path, contents| { + for (idx, line) in contents.lines().enumerate() { + if !line.contains("bootstrap_catalog") { + continue; + } + // Skip comments referencing the symbol in prose. + let trimmed = line.trim_start(); + if trimmed.starts_with("//") || trimmed.starts_with("/*") || trimmed.starts_with('*') { + continue; + } + let rel = path.strip_prefix(&root).unwrap_or(path); + let rel_str = rel.to_string_lossy().replace('\\', "/"); + if BOOTSTRAP_CATALOG_ALLOWED_PATH_FRAGMENTS + .iter() + .any(|frag| rel_str.contains(frag)) + { + continue; + } + violations.push((rel.to_path_buf(), idx + 1, line.to_string())); + } + }); + + assert!( + violations.is_empty(), + "bootstrap_catalog (install-only) referenced from non-allowlisted source files:\n{}\n\nIf this is intentional, add the path fragment to BOOTSTRAP_CATALOG_ALLOWED_PATH_FRAGMENTS in lib/crates/fabro-dev/tests/it/policy.rs.", + violations + .into_iter() + .map(|(p, l, s)| format!(" {}:{}: {}", p.display(), l, s.trim())) + .collect::>() + .join("\n"), + ); +} + +#[expect( + clippy::disallowed_methods, + reason = "policy test reads source files synchronously with std::fs" +)] +fn walk_rust_sources(root: &Path, on_file: &mut dyn FnMut(&Path, &str)) { + let mut stack: Vec = vec![root.join("lib")]; + while let Some(dir) = stack.pop() { + let Ok(entries) = std::fs::read_dir(&dir) else { + continue; + }; + for entry in entries.flatten() { + let path = entry.path(); + let name = entry.file_name(); + let name_str = name.to_string_lossy(); + // Skip generated/output directories. + if matches!( + name_str.as_ref(), + "target" | ".git" | "node_modules" | "dist" | "build" + ) { + continue; + } + if path.is_dir() { + stack.push(path); + } else if path.extension().is_some_and(|ext| ext == "rs") { + if let Ok(contents) = std::fs::read_to_string(&path) { + on_file(&path, &contents); + } + } + } + } +} diff --git a/lib/crates/fabro-llm/src/adapter_registry.rs b/lib/crates/fabro-llm/src/adapter_registry.rs index d24a7e5a9..5722e5582 100644 --- a/lib/crates/fabro-llm/src/adapter_registry.rs +++ b/lib/crates/fabro-llm/src/adapter_registry.rs @@ -18,7 +18,6 @@ use std::sync::Arc; use fabro_auth::ApiKeyHeader; use fabro_model::adapter::{self as model_adapter, AdapterMetadata}; -use crate::error::Error; use crate::provider::ProviderAdapter; use crate::providers; @@ -50,13 +49,13 @@ impl AdapterConfig { /// Construct a minimal config with just provider ID and auth header. pub fn new(provider_id: impl Into, auth_header: ApiKeyHeader) -> Self { Self { - provider_id: provider_id.into(), + provider_id: provider_id.into(), auth_header, - base_url: None, + base_url: None, extra_headers: HashMap::new(), - codex_mode: false, - org_id: None, - project_id: None, + codex_mode: false, + org_id: None, + project_id: None, } } } @@ -69,11 +68,15 @@ fn auth_value(header: &ApiKeyHeader) -> String { /// Factory function signature. Takes a fully-resolved [`AdapterConfig`] and /// returns a registered-ready [`ProviderAdapter`]. -pub type AdapterFactory = fn(&AdapterConfig) -> Result, Error>; +/// +/// Adapter constructors are infallible today; if a future adapter needs to +/// fail at construction time, add a separate fallible factory variant +/// rather than re-shaping every existing factory. +pub type AdapterFactory = fn(&AdapterConfig) -> Arc; const KIMI_BASE_URL: &str = "https://api.moonshot.ai/v1"; -fn build_anthropic(config: &AdapterConfig) -> Result, Error> { +fn build_anthropic(config: &AdapterConfig) -> Arc { let mut adapter = providers::AnthropicAdapter::new(auth_value(&config.auth_header)); if let Some(base_url) = config.base_url.clone() { adapter = adapter.with_base_url(base_url); @@ -81,10 +84,10 @@ fn build_anthropic(config: &AdapterConfig) -> Result, E if !config.extra_headers.is_empty() { adapter = adapter.with_default_headers(config.extra_headers.clone()); } - Ok(Arc::new(adapter)) + Arc::new(adapter) } -fn build_openai(config: &AdapterConfig) -> Result, Error> { +fn build_openai(config: &AdapterConfig) -> Arc { let mut adapter = providers::OpenAiAdapter::new(auth_value(&config.auth_header)); if let Some(base_url) = config.base_url.clone() { adapter = adapter.with_base_url(base_url); @@ -101,10 +104,10 @@ fn build_openai(config: &AdapterConfig) -> Result, Erro if let Some(project_id) = config.project_id.clone() { adapter = adapter.with_project_id(project_id); } - Ok(Arc::new(adapter)) + Arc::new(adapter) } -fn build_gemini(config: &AdapterConfig) -> Result, Error> { +fn build_gemini(config: &AdapterConfig) -> Arc { let mut adapter = providers::GeminiAdapter::new(auth_value(&config.auth_header)); if let Some(base_url) = config.base_url.clone() { adapter = adapter.with_base_url(base_url); @@ -112,10 +115,10 @@ fn build_gemini(config: &AdapterConfig) -> Result, Erro if !config.extra_headers.is_empty() { adapter = adapter.with_default_headers(config.extra_headers.clone()); } - Ok(Arc::new(adapter)) + Arc::new(adapter) } -fn build_openai_compatible(config: &AdapterConfig) -> Result, Error> { +fn build_openai_compatible(config: &AdapterConfig) -> Arc { let base_url = config .base_url .clone() @@ -126,7 +129,7 @@ fn build_openai_compatible(config: &AdapterConfig) -> Result = - model_adapter::keys().collect(); + let metadata: std::collections::BTreeSet<&str> = model_adapter::keys().collect(); let factories: std::collections::BTreeSet<&str> = registered_keys().collect(); assert_eq!(metadata, factories); } @@ -195,14 +197,11 @@ mod tests { #[test] fn anthropic_factory_builds_anthropic_adapter() { - let config = AdapterConfig::new( - "anthropic", - ApiKeyHeader::Custom { - name: "x-api-key".to_string(), - value: "test-key".to_string(), - }, - ); - let adapter = factory_for("anthropic").unwrap()(&config).unwrap(); + let config = AdapterConfig::new("anthropic", ApiKeyHeader::Custom { + name: "x-api-key".to_string(), + value: "test-key".to_string(), + }); + let adapter = factory_for("anthropic").unwrap()(&config); assert_eq!(adapter.name(), "anthropic"); } @@ -217,7 +216,7 @@ mod tests { org_id: None, project_id: None, }; - let adapter = factory_for("openai_compatible").unwrap()(&config).unwrap(); + let adapter = factory_for("openai_compatible").unwrap()(&config); assert_eq!(adapter.name(), "kimi"); } } diff --git a/lib/crates/fabro-llm/src/lib.rs b/lib/crates/fabro-llm/src/lib.rs index a3d275310..45b847011 100644 --- a/lib/crates/fabro-llm/src/lib.rs +++ b/lib/crates/fabro-llm/src/lib.rs @@ -11,4 +11,4 @@ pub mod tools; pub mod types; pub use error::{Error, ProviderErrorDetail, ProviderErrorKind, Result}; -pub use fabro_model::{ModelHandle, Provider}; \ No newline at end of file +pub use fabro_model::{ModelHandle, Provider}; diff --git a/lib/crates/fabro-model/src/adapter.rs b/lib/crates/fabro-model/src/adapter.rs index c1edaa443..f9168b625 100644 --- a/lib/crates/fabro-model/src/adapter.rs +++ b/lib/crates/fabro-model/src/adapter.rs @@ -8,8 +8,8 @@ //! (in `fabro-model`) and the LLM factory registry (in `fabro-llm`) must agree //! on the same set of adapter keys; the parity is enforced by tests. -use crate::reasoning::ReasoningEffort; use crate::Speed; +use crate::reasoning::ReasoningEffort; /// Internal dispatch key that `fabro-agent` maps to a concrete agent profile. /// @@ -100,7 +100,9 @@ pub const OPENAI: AdapterMetadata = AdapterMetadata { pub const GEMINI: AdapterMetadata = AdapterMetadata { key: "gemini", default_profile: AgentProfileKind::Gemini, - api_key_header: ApiKeyHeaderPolicy::Custom { name: "x-goog-api-key" }, + api_key_header: ApiKeyHeaderPolicy::Custom { + name: "x-goog-api-key", + }, controls: AdapterControlCapabilities { native_reasoning_effort: FULL_REASONING_EFFORTS, additional_speeds: &[], @@ -184,10 +186,7 @@ mod tests { #[test] fn openai_compatible_uses_openai_profile() { - assert_eq!( - OPENAI_COMPATIBLE.default_profile, - AgentProfileKind::OpenAi - ); + assert_eq!(OPENAI_COMPATIBLE.default_profile, AgentProfileKind::OpenAi); } #[test] diff --git a/lib/crates/fabro-model/src/billing.rs b/lib/crates/fabro-model/src/billing.rs index f793473de..dcbb92bdf 100644 --- a/lib/crates/fabro-model/src/billing.rs +++ b/lib/crates/fabro-model/src/billing.rs @@ -107,6 +107,7 @@ impl PricePerMTok { Display, EnumString, IntoStaticStr, + strum::VariantArray, )] #[serde(rename_all = "snake_case")] #[strum(serialize_all = "snake_case")] diff --git a/lib/crates/fabro-model/src/ids.rs b/lib/crates/fabro-model/src/ids.rs index 68f8d9817..0d10da414 100644 --- a/lib/crates/fabro-model/src/ids.rs +++ b/lib/crates/fabro-model/src/ids.rs @@ -133,12 +133,15 @@ mod tests { #[test] fn display_writes_inner_string() { assert_eq!(ProviderId::new("anthropic").to_string(), "anthropic"); - assert_eq!(ModelId::new("claude-opus-4-7").to_string(), "claude-opus-4-7"); + assert_eq!( + ModelId::new("claude-opus-4-7").to_string(), + "claude-opus-4-7" + ); } #[test] fn ord_is_lexicographic() { - let mut v = vec![ProviderId::new("zai"), ProviderId::new("anthropic")]; + let mut v = [ProviderId::new("zai"), ProviderId::new("anthropic")]; v.sort(); assert_eq!(v[0].as_str(), "anthropic"); assert_eq!(v[1].as_str(), "zai"); diff --git a/lib/crates/fabro-model/src/reasoning.rs b/lib/crates/fabro-model/src/reasoning.rs index 2ea61e8e4..f99d3b9bb 100644 --- a/lib/crates/fabro-model/src/reasoning.rs +++ b/lib/crates/fabro-model/src/reasoning.rs @@ -42,17 +42,26 @@ mod tests { #[test] fn parses_canonical_lowercase_strings() { - assert_eq!(ReasoningEffort::from_str("low").unwrap(), ReasoningEffort::Low); + assert_eq!( + ReasoningEffort::from_str("low").unwrap(), + ReasoningEffort::Low + ); assert_eq!( ReasoningEffort::from_str("medium").unwrap(), ReasoningEffort::Medium ); - assert_eq!(ReasoningEffort::from_str("high").unwrap(), ReasoningEffort::High); + assert_eq!( + ReasoningEffort::from_str("high").unwrap(), + ReasoningEffort::High + ); assert_eq!( ReasoningEffort::from_str("xhigh").unwrap(), ReasoningEffort::XHigh ); - assert_eq!(ReasoningEffort::from_str("max").unwrap(), ReasoningEffort::Max); + assert_eq!( + ReasoningEffort::from_str("max").unwrap(), + ReasoningEffort::Max + ); } #[test] diff --git a/lib/crates/fabro-server/src/run_manifest.rs b/lib/crates/fabro-server/src/run_manifest.rs index c52d96657..16268b985 100644 --- a/lib/crates/fabro-server/src/run_manifest.rs +++ b/lib/crates/fabro-server/src/run_manifest.rs @@ -1981,4 +1981,4 @@ digraph Demo { ); assert!(response_mock.calls_async().await >= 1); } -} \ No newline at end of file +} diff --git a/lib/crates/fabro-types/src/settings/mod.rs b/lib/crates/fabro-types/src/settings/mod.rs index a17643882..b6608261a 100644 --- a/lib/crates/fabro-types/src/settings/mod.rs +++ b/lib/crates/fabro-types/src/settings/mod.rs @@ -52,4 +52,4 @@ pub use server::{ ServerStorageSettings, ServerWebSettings, SlackIntegrationSettings, TeamsIntegrationSettings, }; pub use size::{ParseSizeError, Size}; -pub use workflow::WorkflowNamespace; \ No newline at end of file +pub use workflow::WorkflowNamespace; diff --git a/lib/crates/fabro-types/src/settings/run.rs b/lib/crates/fabro-types/src/settings/run.rs index 9e8409e94..337c200cf 100644 --- a/lib/crates/fabro-types/src/settings/run.rs +++ b/lib/crates/fabro-types/src/settings/run.rs @@ -562,4 +562,4 @@ pub enum MergeStrategy { Merge, Squash, Rebase, -} \ No newline at end of file +}