From 1bd7b7688f8bddd3e5f6d47c5111380f5eda8cb5 Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Thu, 23 Apr 2026 19:10:45 -0400 Subject: [PATCH] lock down sparse settings exports in fabro-types --- Cargo.lock | 1 + lib/crates/fabro-checkpoint/Cargo.toml | 1 + lib/crates/fabro-checkpoint/src/author.rs | 3 +- lib/crates/fabro-config/src/resolve/mod.rs | 1 + lib/crates/fabro-types/src/lib.rs | 1 - lib/crates/fabro-types/src/settings/cli.rs | 18 +++--- .../fabro-types/src/settings/combine.rs | 15 ++++- .../fabro-types/src/settings/features.rs | 2 +- lib/crates/fabro-types/src/settings/layer.rs | 6 +- lib/crates/fabro-types/src/settings/maps.rs | 21 +++++-- lib/crates/fabro-types/src/settings/mod.rs | 31 +++++----- .../fabro-types/src/settings/project.rs | 2 +- lib/crates/fabro-types/src/settings/run.rs | 56 +++++++++---------- lib/crates/fabro-types/src/settings/server.rs | 42 +++++++------- .../fabro-types/src/settings/splice_array.rs | 18 +++--- .../fabro-types/src/settings/workflow.rs | 2 +- .../tests/server_settings_serde.rs | 23 -------- 17 files changed, 118 insertions(+), 125 deletions(-) delete mode 100644 lib/crates/fabro-types/tests/server_settings_serde.rs diff --git a/Cargo.lock b/Cargo.lock index e123d0e23..074221793 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1585,6 +1585,7 @@ name = "fabro-checkpoint" version = "0.212.0-nightly.0" dependencies = [ "chrono", + "fabro-config", "fabro-store", "fabro-types", "git2", diff --git a/lib/crates/fabro-checkpoint/Cargo.toml b/lib/crates/fabro-checkpoint/Cargo.toml index b443ab206..3734d01fb 100644 --- a/lib/crates/fabro-checkpoint/Cargo.toml +++ b/lib/crates/fabro-checkpoint/Cargo.toml @@ -14,6 +14,7 @@ doctest = false workspace = true [dependencies] +fabro-config = { path = "../fabro-config" } fabro-store = { path = "../fabro-store" } fabro-types = { path = "../fabro-types" } git2.workspace = true diff --git a/lib/crates/fabro-checkpoint/src/author.rs b/lib/crates/fabro-checkpoint/src/author.rs index d8e364c69..0b1714615 100644 --- a/lib/crates/fabro-checkpoint/src/author.rs +++ b/lib/crates/fabro-checkpoint/src/author.rs @@ -1,7 +1,8 @@ use std::fmt::Write; +use fabro_config::GitAuthorLayer; use fabro_types::settings::InterpString; -use fabro_types::settings::run::{GitAuthorLayer, GitAuthorSettings}; +use fabro_types::settings::run::GitAuthorSettings; /// Resolved git author identity for checkpoint commits. #[derive(Debug, Clone, PartialEq)] diff --git a/lib/crates/fabro-config/src/resolve/mod.rs b/lib/crates/fabro-config/src/resolve/mod.rs index 19915279b..d6f69a1f0 100644 --- a/lib/crates/fabro-config/src/resolve/mod.rs +++ b/lib/crates/fabro-config/src/resolve/mod.rs @@ -12,6 +12,7 @@ use fabro_types::settings::InterpString; pub use features::resolve_features; pub use project::resolve_project; pub use run::resolve_run; +#[cfg(test)] pub(crate) use server::dev_token_auth_enabled; pub use server::resolve_server; pub use workflow::resolve_workflow; diff --git a/lib/crates/fabro-types/src/lib.rs b/lib/crates/fabro-types/src/lib.rs index 15a787534..c10d44c55 100644 --- a/lib/crates/fabro-types/src/lib.rs +++ b/lib/crates/fabro-types/src/lib.rs @@ -62,7 +62,6 @@ pub use run_id::{RunId, fixtures}; pub use run_projection::{NodeState, PendingInterviewRecord, RunProjection}; pub use run_summary::RunSummary; pub use sandbox_record::SandboxRecord; -pub use settings::Combine; pub use stage_id::{ParallelBranchId, StageId}; pub use start::StartRecord; pub use status::{ diff --git a/lib/crates/fabro-types/src/settings/cli.rs b/lib/crates/fabro-types/src/settings/cli.rs index c3130f510..65f3055a9 100644 --- a/lib/crates/fabro-types/src/settings/cli.rs +++ b/lib/crates/fabro-types/src/settings/cli.rs @@ -74,7 +74,7 @@ pub struct CliLoggingSettings { /// A sparse `[cli]` layer as it appears in a single settings file. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] -pub struct CliLayer { +pub(crate) struct CliLayer { #[serde(default, skip_serializing_if = "Option::is_none")] pub target: Option, #[serde(default, skip_serializing_if = "Option::is_none")] @@ -92,7 +92,7 @@ pub struct CliLayer { /// `[cli.target]` — explicit transport selection. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields, tag = "type", rename_all = "lowercase")] -pub enum CliTargetLayer { +pub(crate) enum CliTargetLayer { Http { #[serde(default)] url: Option, @@ -106,7 +106,7 @@ pub enum CliTargetLayer { /// `[cli.auth]` — explicit auth strategy selection. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] -pub struct CliAuthLayer { +pub(crate) struct CliAuthLayer { /// `none` explicitly disables inherited auth. #[serde(default, skip_serializing_if = "Option::is_none")] pub strategy: Option, @@ -122,7 +122,7 @@ pub enum CliAuthStrategy { /// `[cli.exec]` — `fabro exec` defaults. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] -pub struct CliExecLayer { +pub(crate) struct CliExecLayer { /// Prevent idle sleep on macOS while an exec run is in flight. #[serde(default, skip_serializing_if = "Option::is_none")] pub prevent_idle_sleep: Option, @@ -134,7 +134,7 @@ pub struct CliExecLayer { #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] -pub struct CliExecModelLayer { +pub(crate) struct CliExecModelLayer { #[serde(default, skip_serializing_if = "Option::is_none")] pub provider: Option, #[serde(default, skip_serializing_if = "Option::is_none")] @@ -143,7 +143,7 @@ pub struct CliExecModelLayer { #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] -pub struct CliExecAgentLayer { +pub(crate) struct CliExecAgentLayer { #[serde(default, skip_serializing_if = "Option::is_none")] pub permissions: Option, /// Agent-scoped MCP entries for `fabro exec`. @@ -154,7 +154,7 @@ pub struct CliExecAgentLayer { /// `[cli.output]` — generic CLI output defaults. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] -pub struct CliOutputLayer { +pub(crate) struct CliOutputLayer { #[serde(default, skip_serializing_if = "Option::is_none")] pub format: Option, #[serde(default, skip_serializing_if = "Option::is_none")] @@ -181,7 +181,7 @@ pub enum OutputVerbosity { /// `[cli.updates]` — upgrade check toggle. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] -pub struct CliUpdatesLayer { +pub(crate) struct CliUpdatesLayer { #[serde(default, skip_serializing_if = "Option::is_none")] pub check: Option, } @@ -189,7 +189,7 @@ pub struct CliUpdatesLayer { /// `[cli.logging]` — process-owned logging configuration for the CLI. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] -pub struct CliLoggingLayer { +pub(crate) struct CliLoggingLayer { #[serde(default, skip_serializing_if = "Option::is_none")] pub level: Option, } diff --git a/lib/crates/fabro-types/src/settings/combine.rs b/lib/crates/fabro-types/src/settings/combine.rs index 985f14e66..9a781d955 100644 --- a/lib/crates/fabro-types/src/settings/combine.rs +++ b/lib/crates/fabro-types/src/settings/combine.rs @@ -19,7 +19,7 @@ use super::server::{ }; use super::size::Size; -pub trait Combine { +pub(crate) trait Combine { /// Combine two values, preferring the values in `self`. #[must_use] fn combine(self, other: Self) -> Self; @@ -137,7 +137,7 @@ impl Combine for RunCheckpointLayer { /// An element of a splice-aware sequence: either a regular value or the /// `...` marker that asks the combiner to expand the fallback list inline. -pub trait SpliceMarker { +pub(crate) trait SpliceMarker { fn is_splice(&self) -> bool; } @@ -217,12 +217,21 @@ fn combine_hooks(fallback: &[HookEntry], current: Vec) -> Vec, b: Option, } + impl Combine for FieldMergeLayer { + fn combine(self, other: Self) -> Self { + Self { + a: self.a.combine(other.a), + b: self.b.combine(other.b), + } + } + } + #[derive(Debug, PartialEq)] struct WholeReplaceLayer { a: Option, diff --git a/lib/crates/fabro-types/src/settings/features.rs b/lib/crates/fabro-types/src/settings/features.rs index 79dfc9f59..29df5e09c 100644 --- a/lib/crates/fabro-types/src/settings/features.rs +++ b/lib/crates/fabro-types/src/settings/features.rs @@ -17,7 +17,7 @@ pub struct FeaturesNamespace { /// override a flag without forcing a default that hides an unset value. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] -pub struct FeaturesLayer { +pub(crate) struct FeaturesLayer { #[serde(default, skip_serializing_if = "Option::is_none")] pub session_sandboxes: Option, } diff --git a/lib/crates/fabro-types/src/settings/layer.rs b/lib/crates/fabro-types/src/settings/layer.rs index 188ece46d..4a5146fc9 100644 --- a/lib/crates/fabro-types/src/settings/layer.rs +++ b/lib/crates/fabro-types/src/settings/layer.rs @@ -16,7 +16,7 @@ use super::workflow::WorkflowLayer; /// A sparse settings layer before merge/resolve. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] -pub struct SettingsLayer { +pub(crate) struct SettingsLayer { #[serde(default, rename = "_version", skip_serializing_if = "Option::is_none")] pub version: Option, #[serde(default, skip_serializing_if = "Option::is_none")] @@ -39,7 +39,7 @@ impl SettingsLayer { /// with `["dev-token"]`. Use anywhere a test needs a starter /// `SettingsLayer` that the strict resolver will accept. #[must_use] - pub fn test_default() -> Self { + pub(crate) fn test_default() -> Self { let mut layer = Self::default(); layer.ensure_test_auth_methods(); layer @@ -48,7 +48,7 @@ impl SettingsLayer { /// If `server.auth.methods` is unset, populate it with `["dev-token"]`. /// Existing methods (set by a fixture) are preserved. Use to make a /// parsed-from-TOML layer resolve cleanly without overriding test intent. - pub fn ensure_test_auth_methods(&mut self) { + pub(crate) fn ensure_test_auth_methods(&mut self) { use super::server::{ServerAuthLayer, ServerAuthMethod, ServerLayer as ServerLayerTy}; if self diff --git a/lib/crates/fabro-types/src/settings/maps.rs b/lib/crates/fabro-types/src/settings/maps.rs index f34bdb6a7..21db83484 100644 --- a/lib/crates/fabro-types/src/settings/maps.rs +++ b/lib/crates/fabro-types/src/settings/maps.rs @@ -8,26 +8,26 @@ use super::combine::Combine; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(transparent)] -pub struct ReplaceMap(pub HashMap); +pub(crate) struct ReplaceMap(pub HashMap); #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(transparent)] -pub struct StickyMap(pub HashMap); +pub(crate) struct StickyMap(pub HashMap); #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(transparent)] -pub struct MergeMap(pub HashMap); +pub(crate) struct MergeMap(pub HashMap); macro_rules! impl_map_wrapper { ($name:ident) => { impl $name { #[must_use] - pub fn is_empty(&self) -> bool { + pub(crate) fn is_empty(&self) -> bool { self.0.is_empty() } #[must_use] - pub fn into_inner(self) -> HashMap { + pub(crate) fn into_inner(self) -> HashMap { self.0 } } @@ -107,12 +107,21 @@ impl Combine for MergeMap { mod tests { use super::*; - #[derive(Debug, PartialEq, fabro_macros::Combine)] + #[derive(Debug, PartialEq)] struct ValueLayer { a: Option, b: Option, } + impl Combine for ValueLayer { + fn combine(self, other: Self) -> Self { + Self { + a: self.a.combine(other.a), + b: self.b.combine(other.b), + } + } + } + #[test] fn replace_map_self_wins_when_non_empty() { let this = ReplaceMap(HashMap::from([("a".to_string(), "this".to_string())])); diff --git a/lib/crates/fabro-types/src/settings/mod.rs b/lib/crates/fabro-types/src/settings/mod.rs index 95831e6cd..372f88971 100644 --- a/lib/crates/fabro-types/src/settings/mod.rs +++ b/lib/crates/fabro-types/src/settings/mod.rs @@ -10,51 +10,46 @@ //! exists. pub mod cli; -pub mod combine; +mod combine; pub mod duration; pub mod features; pub mod interp; -pub mod layer; -pub mod maps; +mod layer; +mod maps; pub mod model_ref; pub mod project; pub mod run; pub mod server; pub mod size; -pub mod splice_array; +mod splice_array; pub mod workflow; pub use cli::{ - CliAuthSettings, CliExecAgentSettings, CliExecModelSettings, CliExecSettings, CliLayer, + CliAuthSettings, CliExecAgentSettings, CliExecModelSettings, CliExecSettings, CliLoggingSettings, CliNamespace, CliOutputSettings, CliTargetSettings, CliUpdatesSettings, }; -pub use combine::Combine; pub use duration::{Duration, ParseDurationError}; -pub use features::{FeaturesLayer, FeaturesNamespace}; +pub use features::FeaturesNamespace; pub use interp::{InterpString, Provenance, ResolveEnvError, Resolved}; -pub use layer::SettingsLayer; -pub use maps::{MergeMap, ReplaceMap, StickyMap}; pub use model_ref::{ AmbiguousModelRef, ModelRef, ModelRegistry, ParseModelRefError, ResolvedModelRef, }; -pub use project::{ProjectLayer, ProjectNamespace}; +pub use project::ProjectNamespace; pub use run::{ ArtifactsSettings, DaytonaSettings, DaytonaSnapshotSettings, DockerfileSource, GitAuthorSettings, HookDefinition, HookType, InterviewProviderSettings, McpServerSettings, McpTransport, NotificationProviderSettings, NotificationRouteSettings, PullRequestSettings, RunAgentSettings, RunCheckpointSettings, RunExecutionSettings, RunGitSettings, RunGoal, - RunInterviewsSettings, RunLayer, RunModelSettings, RunNamespace, RunPrepareSettings, - RunSandboxSettings, RunScmSettings, ScmGitHubSettings, TlsMode, + RunInterviewsSettings, RunModelSettings, RunNamespace, RunPrepareSettings, RunSandboxSettings, + RunScmSettings, ScmGitHubSettings, TlsMode, }; pub use server::{ DiscordIntegrationSettings, GithubIntegrationSettings, IntegrationWebhooksSettings, IpAllowEntry, ObjectStoreSettings, ServerApiSettings, ServerArtifactsSettings, ServerAuthGithubSettings, ServerAuthMethod, ServerAuthSettings, ServerIntegrationsSettings, - ServerIpAllowlistLayer, ServerIpAllowlistOverrideLayer, ServerIpAllowlistOverrideSettings, - ServerIpAllowlistSettings, ServerLayer, ServerListenSettings, ServerLoggingSettings, - ServerNamespace, ServerSchedulerSettings, ServerSlateDbSettings, ServerStorageSettings, - ServerWebSettings, SlackIntegrationSettings, TeamsIntegrationSettings, + ServerIpAllowlistOverrideSettings, ServerIpAllowlistSettings, ServerListenSettings, + ServerLoggingSettings, ServerNamespace, ServerSchedulerSettings, ServerSlateDbSettings, + ServerStorageSettings, ServerWebSettings, SlackIntegrationSettings, TeamsIntegrationSettings, }; pub use size::{ParseSizeError, Size}; -pub use splice_array::{SPLICE_MARKER, SpliceArray, SpliceArrayError}; -pub use workflow::{WorkflowLayer, WorkflowNamespace}; +pub use workflow::WorkflowNamespace; diff --git a/lib/crates/fabro-types/src/settings/project.rs b/lib/crates/fabro-types/src/settings/project.rs index eb1a439be..470076b8e 100644 --- a/lib/crates/fabro-types/src/settings/project.rs +++ b/lib/crates/fabro-types/src/settings/project.rs @@ -21,7 +21,7 @@ pub struct ProjectNamespace { /// A sparse `[project]` layer as it appears in a single settings file. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] -pub struct ProjectLayer { +pub(crate) struct ProjectLayer { #[serde(default, skip_serializing_if = "Option::is_none")] pub name: Option, #[serde(default, skip_serializing_if = "Option::is_none")] diff --git a/lib/crates/fabro-types/src/settings/run.rs b/lib/crates/fabro-types/src/settings/run.rs index d0d0a5b01..a1033eb15 100644 --- a/lib/crates/fabro-types/src/settings/run.rs +++ b/lib/crates/fabro-types/src/settings/run.rs @@ -429,7 +429,7 @@ pub struct ArtifactsSettings { /// A sparse `[run]` layer as it appears in a single settings file. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] -pub struct RunLayer { +pub(crate) struct RunLayer { #[serde(default, skip_serializing_if = "Option::is_none")] pub goal: Option, #[serde(default, skip_serializing_if = "Option::is_none")] @@ -491,7 +491,7 @@ pub struct RunLayer { /// effective working directory. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(untagged, deny_unknown_fields)] -pub enum RunGoalLayer { +pub(crate) enum RunGoalLayer { Inline(InterpString), File { file: InterpString }, } @@ -520,7 +520,7 @@ pub enum ResolvedGoalSource { /// `[run.model]` — provider-neutral default model selection. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] -pub struct RunModelLayer { +pub(crate) struct RunModelLayer { #[serde(default, skip_serializing_if = "Option::is_none")] pub provider: Option, #[serde(default, skip_serializing_if = "Option::is_none")] @@ -533,7 +533,7 @@ pub struct RunModelLayer { /// A single `fallbacks` entry: either a parsed `ModelRef` or the splice marker. #[derive(Debug, Clone, PartialEq, Eq)] -pub enum ModelRefOrSplice { +pub(crate) enum ModelRefOrSplice { ModelRef(ModelRef), Splice, } @@ -562,14 +562,14 @@ impl<'de> Deserialize<'de> for ModelRefOrSplice { /// `[run.git]` — local git behavior such as commit author. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] -pub struct RunGitLayer { +pub(crate) struct RunGitLayer { #[serde(default, skip_serializing_if = "Option::is_none")] pub author: Option, } #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] -pub struct GitAuthorLayer { +pub(crate) struct GitAuthorLayer { #[serde(default, skip_serializing_if = "Option::is_none")] pub name: Option, #[serde(default, skip_serializing_if = "Option::is_none")] @@ -580,7 +580,7 @@ pub struct GitAuthorLayer { /// across layers. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] -pub struct RunPrepareLayer { +pub(crate) struct RunPrepareLayer { #[serde(default, skip_serializing_if = "Vec::is_empty")] pub steps: Vec, /// Optional timeout applied to each prepare step. @@ -603,7 +603,7 @@ pub struct PrepareStep { /// `[run.execution]` — run posture knobs. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] -pub struct RunExecutionLayer { +pub(crate) struct RunExecutionLayer { #[serde(default, skip_serializing_if = "Option::is_none")] pub mode: Option, #[serde(default, skip_serializing_if = "Option::is_none")] @@ -630,7 +630,7 @@ pub enum ApprovalMode { /// `[run.checkpoint]` — checkpoint policy. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] -pub struct RunCheckpointLayer { +pub(crate) struct RunCheckpointLayer { #[serde(default, skip_serializing_if = "Vec::is_empty")] pub exclude_globs: Vec, } @@ -638,7 +638,7 @@ pub struct RunCheckpointLayer { /// `[run.sandbox]` — sandbox selection and execution-environment surface. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] -pub struct RunSandboxLayer { +pub(crate) struct RunSandboxLayer { #[serde(default, skip_serializing_if = "Option::is_none")] pub provider: Option, #[serde(default, skip_serializing_if = "Option::is_none")] @@ -656,7 +656,7 @@ pub struct RunSandboxLayer { #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] -pub struct LocalSandboxLayer { +pub(crate) struct LocalSandboxLayer { #[serde(default, skip_serializing_if = "Option::is_none")] pub worktree_mode: Option, } @@ -673,7 +673,7 @@ pub enum WorktreeMode { #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] -pub struct DaytonaSandboxLayer { +pub(crate) struct DaytonaSandboxLayer { #[serde(default, skip_serializing_if = "Option::is_none")] pub auto_stop_interval: Option, /// Sticky merge-by-key (provider-native labels). @@ -689,7 +689,7 @@ pub struct DaytonaSandboxLayer { #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] -pub struct DaytonaSnapshotLayer { +pub(crate) struct DaytonaSnapshotLayer { #[serde(default, skip_serializing_if = "Option::is_none")] pub name: Option, #[serde(default, skip_serializing_if = "Option::is_none")] @@ -704,7 +704,7 @@ pub struct DaytonaSnapshotLayer { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(untagged, deny_unknown_fields)] -pub enum DaytonaDockerfileLayer { +pub(crate) enum DaytonaDockerfileLayer { Inline(String), Path { path: String }, } @@ -720,7 +720,7 @@ pub enum DaytonaNetworkLayer { /// `[run.notifications.]` — a keyed notification route. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] -pub struct NotificationRouteLayer { +pub(crate) struct NotificationRouteLayer { #[serde(default, skip_serializing_if = "Option::is_none")] pub enabled: Option, #[serde(default, skip_serializing_if = "Option::is_none")] @@ -739,7 +739,7 @@ pub struct NotificationRouteLayer { /// A single string array entry that may be the splice marker. #[derive(Debug, Clone, PartialEq, Eq)] -pub enum StringOrSplice { +pub(crate) enum StringOrSplice { Value(String), Splice, } @@ -767,7 +767,7 @@ impl<'de> Deserialize<'de> for StringOrSplice { /// Provider-specific destination fields for a notification route. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] -pub struct NotificationProviderLayer { +pub(crate) struct NotificationProviderLayer { #[serde(default, skip_serializing_if = "Option::is_none")] pub channel: Option, } @@ -775,7 +775,7 @@ pub struct NotificationProviderLayer { /// `[run.interviews]` — external interview delivery. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] -pub struct InterviewsLayer { +pub(crate) struct InterviewsLayer { #[serde(default, skip_serializing_if = "Option::is_none")] pub provider: Option, #[serde(default, skip_serializing_if = "Option::is_none")] @@ -788,7 +788,7 @@ pub struct InterviewsLayer { #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] -pub struct InterviewProviderLayer { +pub(crate) struct InterviewProviderLayer { #[serde(default, skip_serializing_if = "Option::is_none")] pub channel: Option, } @@ -796,7 +796,7 @@ pub struct InterviewProviderLayer { /// `[run.agent]` — agent knobs only (permissions, MCPs). #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] -pub struct RunAgentLayer { +pub(crate) struct RunAgentLayer { #[serde(default, skip_serializing_if = "Option::is_none")] pub permissions: Option, /// Agent-scoped MCP server entries, keyed by name. @@ -817,7 +817,7 @@ pub enum AgentPermissions { /// transports use neither field. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields, tag = "type", rename_all = "snake_case")] -pub enum McpEntryLayer { +pub(crate) enum McpEntryLayer { Http { #[serde(default)] enabled: Option, @@ -865,7 +865,7 @@ pub enum McpEntryLayer { /// used for cross-layer replace-by-id merging. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] -pub struct HookEntry { +pub(crate) struct HookEntry { /// Optional merge identity. Hooks with the same `id` replace in place. #[serde(default, skip_serializing_if = "Option::is_none")] pub id: Option, @@ -906,7 +906,7 @@ pub struct HookEntry { #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] -pub enum HookTlsMode { +pub(crate) enum HookTlsMode { #[default] Verify, NoVerify, @@ -918,7 +918,7 @@ pub enum HookTlsMode { /// struct without a discriminator. #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] -pub enum HookAgentMarker { +pub(crate) enum HookAgentMarker { #[default] Enabled, } @@ -947,7 +947,7 @@ pub enum HookEvent { /// `[run.scm]` — remote SCM host/provider behavior. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] -pub struct RunScmLayer { +pub(crate) struct RunScmLayer { #[serde(default, skip_serializing_if = "Option::is_none")] pub provider: Option, #[serde(default, skip_serializing_if = "Option::is_none")] @@ -964,12 +964,12 @@ pub struct RunScmLayer { /// `run.pull_request` until a concrete use case lands. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] -pub struct ScmGitHubLayer; +pub(crate) struct ScmGitHubLayer; /// `[run.pull_request]` — provider-neutral PR behavior. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] -pub struct RunPullRequestLayer { +pub(crate) struct RunPullRequestLayer { #[serde(default, skip_serializing_if = "Option::is_none")] pub enabled: Option, #[serde(default, skip_serializing_if = "Option::is_none")] @@ -991,7 +991,7 @@ pub enum MergeStrategy { /// `[run.artifacts]` — run artifact collection policy. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] -pub struct RunArtifactsLayer { +pub(crate) struct RunArtifactsLayer { #[serde(default, skip_serializing_if = "Vec::is_empty")] pub include: Vec, } diff --git a/lib/crates/fabro-types/src/settings/server.rs b/lib/crates/fabro-types/src/settings/server.rs index bdce8d470..9e49f0486 100644 --- a/lib/crates/fabro-types/src/settings/server.rs +++ b/lib/crates/fabro-types/src/settings/server.rs @@ -309,7 +309,7 @@ where /// A sparse `[server]` layer as it appears in a single settings file. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] -pub struct ServerLayer { +pub(crate) struct ServerLayer { #[serde(default, skip_serializing_if = "Option::is_none")] pub listen: Option, #[serde(default, skip_serializing_if = "Option::is_none")] @@ -337,7 +337,7 @@ pub struct ServerLayer { /// `[server.listen]` — shared bind transport. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields, tag = "type", rename_all = "lowercase")] -pub enum ServerListenLayer { +pub(crate) enum ServerListenLayer { Tcp { #[serde(default)] address: Option, @@ -353,7 +353,7 @@ pub enum ServerListenLayer { /// `url` is an optional public URL; it is **not** derived from `server.listen`. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] -pub struct ServerApiLayer { +pub(crate) struct ServerApiLayer { #[serde(default, skip_serializing_if = "Option::is_none")] pub url: Option, } @@ -361,7 +361,7 @@ pub struct ServerApiLayer { /// `[server.web]` — web surface settings. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] -pub struct ServerWebLayer { +pub(crate) struct ServerWebLayer { #[serde(default, skip_serializing_if = "Option::is_none")] pub enabled: Option, #[serde(default, skip_serializing_if = "Option::is_none")] @@ -375,7 +375,7 @@ pub struct ServerWebLayer { /// explicitly opt in to insecure configurations. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] -pub struct ServerAuthLayer { +pub(crate) struct ServerAuthLayer { #[serde(default, skip_serializing_if = "Option::is_none")] pub methods: Option>, #[serde(default, skip_serializing_if = "Option::is_none")] @@ -384,14 +384,14 @@ pub struct ServerAuthLayer { #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] -pub struct ServerAuthGithubLayer { +pub(crate) struct ServerAuthGithubLayer { #[serde(default, skip_serializing_if = "Vec::is_empty")] pub allowed_usernames: Vec, } #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] -pub struct ServerIpAllowlistLayer { +pub(crate) struct ServerIpAllowlistLayer { #[serde(default, skip_serializing_if = "Option::is_none")] pub entries: Option>, #[serde(default, skip_serializing_if = "Option::is_none")] @@ -400,7 +400,7 @@ pub struct ServerIpAllowlistLayer { #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] -pub struct ServerIpAllowlistOverrideLayer { +pub(crate) struct ServerIpAllowlistOverrideLayer { #[serde(default, skip_serializing_if = "Option::is_none")] pub entries: Option>, #[serde(default, skip_serializing_if = "Option::is_none")] @@ -410,7 +410,7 @@ pub struct ServerIpAllowlistOverrideLayer { /// `[server.storage]` — single managed local disk root. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] -pub struct ServerStorageLayer { +pub(crate) struct ServerStorageLayer { #[serde(default, skip_serializing_if = "Option::is_none")] pub root: Option, } @@ -418,7 +418,7 @@ pub struct ServerStorageLayer { /// `[server.artifacts]` — object-store-backed artifact storage. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] -pub struct ServerArtifactsLayer { +pub(crate) struct ServerArtifactsLayer { #[serde(default, skip_serializing_if = "Option::is_none")] pub provider: Option, #[serde(default, skip_serializing_if = "Option::is_none")] @@ -432,7 +432,7 @@ pub struct ServerArtifactsLayer { /// `[server.slatedb]` — SlateDB bottomless storage plus tunables. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] -pub struct ServerSlateDbLayer { +pub(crate) struct ServerSlateDbLayer { #[serde(default, skip_serializing_if = "Option::is_none")] pub provider: Option, #[serde(default, skip_serializing_if = "Option::is_none")] @@ -458,7 +458,7 @@ pub enum ObjectStoreProvider { #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] -pub struct ObjectStoreLocalLayer { +pub(crate) struct ObjectStoreLocalLayer { /// Overrides the default root, which otherwise falls back to /// `{server.storage.root}/objects/{domain}`. #[serde(default, skip_serializing_if = "Option::is_none")] @@ -467,7 +467,7 @@ pub struct ObjectStoreLocalLayer { #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] -pub struct ObjectStoreS3Layer { +pub(crate) struct ObjectStoreS3Layer { #[serde(default, skip_serializing_if = "Option::is_none")] pub bucket: Option, #[serde(default, skip_serializing_if = "Option::is_none")] @@ -481,7 +481,7 @@ pub struct ObjectStoreS3Layer { /// `[server.scheduler]` — server-managed execution policy. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] -pub struct ServerSchedulerLayer { +pub(crate) struct ServerSchedulerLayer { #[serde(default, skip_serializing_if = "Option::is_none")] pub max_concurrent_runs: Option, } @@ -489,7 +489,7 @@ pub struct ServerSchedulerLayer { /// `[server.logging]` — process-owned logging configuration for the server. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] -pub struct ServerLoggingLayer { +pub(crate) struct ServerLoggingLayer { #[serde(default, skip_serializing_if = "Option::is_none")] pub level: Option, } @@ -500,7 +500,7 @@ pub struct ServerLoggingLayer { /// shape so strict unknown-field validation still holds. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] -pub struct ServerIntegrationsLayer { +pub(crate) struct ServerIntegrationsLayer { #[serde(default, skip_serializing_if = "Option::is_none")] pub github: Option, #[serde(default, skip_serializing_if = "Option::is_none")] @@ -515,7 +515,7 @@ pub struct ServerIntegrationsLayer { /// webhooks. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] -pub struct GithubIntegrationLayer { +pub(crate) struct GithubIntegrationLayer { #[serde(default, skip_serializing_if = "Option::is_none")] pub enabled: Option, #[serde(default, skip_serializing_if = "Option::is_none")] @@ -535,7 +535,7 @@ pub struct GithubIntegrationLayer { /// `[server.integrations.slack]` — Slack workspace credentials and defaults. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] -pub struct SlackIntegrationLayer { +pub(crate) struct SlackIntegrationLayer { #[serde(default, skip_serializing_if = "Option::is_none")] pub enabled: Option, #[serde(default, skip_serializing_if = "Option::is_none")] @@ -545,7 +545,7 @@ pub struct SlackIntegrationLayer { /// `[server.integrations.discord]` — Discord workspace configuration. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] -pub struct DiscordIntegrationLayer { +pub(crate) struct DiscordIntegrationLayer { #[serde(default, skip_serializing_if = "Option::is_none")] pub enabled: Option, } @@ -553,14 +553,14 @@ pub struct DiscordIntegrationLayer { /// `[server.integrations.teams]` — Microsoft Teams configuration. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] -pub struct TeamsIntegrationLayer { +pub(crate) struct TeamsIntegrationLayer { #[serde(default, skip_serializing_if = "Option::is_none")] pub enabled: Option, } #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] -pub struct IntegrationWebhooksLayer { +pub(crate) struct IntegrationWebhooksLayer { #[serde(default, skip_serializing_if = "Option::is_none")] pub strategy: Option, #[serde(default, skip_serializing_if = "Option::is_none")] diff --git a/lib/crates/fabro-types/src/settings/splice_array.rs b/lib/crates/fabro-types/src/settings/splice_array.rs index 35d312168..ed435be5e 100644 --- a/lib/crates/fabro-types/src/settings/splice_array.rs +++ b/lib/crates/fabro-types/src/settings/splice_array.rs @@ -14,11 +14,11 @@ use serde::de::{self, Visitor}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; /// The reserved literal that marks the splice insertion point. -pub const SPLICE_MARKER: &str = "..."; +pub(crate) const SPLICE_MARKER: &str = "..."; /// A string array that may contain at most one splice marker. #[derive(Debug, Clone, PartialEq, Eq, Default)] -pub struct SpliceArray { +pub(crate) struct SpliceArray { entries: Vec, } @@ -30,7 +30,7 @@ enum Entry { /// An error returned when a splice array fails validation. #[derive(Debug, Clone, PartialEq, Eq)] -pub enum SpliceArrayError { +pub(crate) enum SpliceArrayError { /// The array contained more than one splice marker. MultipleMarkers, } @@ -49,7 +49,7 @@ impl std::error::Error for SpliceArrayError {} impl SpliceArray { /// Build a splice array from a raw `Vec`. - pub fn from_raw(raw: Vec) -> Result { + pub(crate) fn from_raw(raw: Vec) -> Result { let mut entries = Vec::with_capacity(raw.len()); let mut marker_count = 0; for item in raw { @@ -68,7 +68,7 @@ impl SpliceArray { /// Build a splice array with no inherited splice marker. #[must_use] - pub fn from_values(values: impl IntoIterator) -> Self { + pub(crate) fn from_values(values: impl IntoIterator) -> Self { Self { entries: values.into_iter().map(Entry::Value).collect(), } @@ -76,19 +76,19 @@ impl SpliceArray { /// True when the array contains a splice marker. #[must_use] - pub fn has_splice(&self) -> bool { + pub(crate) fn has_splice(&self) -> bool { self.entries.iter().any(|e| matches!(e, Entry::Splice)) } /// The index of the splice marker, if present. #[must_use] - pub fn splice_position(&self) -> Option { + pub(crate) fn splice_position(&self) -> Option { self.entries.iter().position(|e| matches!(e, Entry::Splice)) } /// The non-splice values, in source order. #[must_use] - pub fn values(&self) -> Vec<&str> { + pub(crate) fn values(&self) -> Vec<&str> { self.entries .iter() .filter_map(|e| match e { @@ -105,7 +105,7 @@ impl SpliceArray { /// - If the array has no splice marker, it replaces the inherited list /// wholesale. #[must_use] - pub fn resolve(self, inherited: Vec) -> Vec { + pub(crate) fn resolve(self, inherited: Vec) -> Vec { let Some(pos) = self.splice_position() else { return self .entries diff --git a/lib/crates/fabro-types/src/settings/workflow.rs b/lib/crates/fabro-types/src/settings/workflow.rs index 834ec58ed..4f356d6c3 100644 --- a/lib/crates/fabro-types/src/settings/workflow.rs +++ b/lib/crates/fabro-types/src/settings/workflow.rs @@ -21,7 +21,7 @@ pub struct WorkflowNamespace { /// A sparse `[workflow]` layer as it appears in a single settings file. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] -pub struct WorkflowLayer { +pub(crate) struct WorkflowLayer { #[serde(default, skip_serializing_if = "Option::is_none")] pub name: Option, #[serde(default, skip_serializing_if = "Option::is_none")] diff --git a/lib/crates/fabro-types/tests/server_settings_serde.rs b/lib/crates/fabro-types/tests/server_settings_serde.rs deleted file mode 100644 index fafc9fc4d..000000000 --- a/lib/crates/fabro-types/tests/server_settings_serde.rs +++ /dev/null @@ -1,23 +0,0 @@ -use fabro_types::settings::SettingsLayer; -use serde_json::json; - -#[test] -fn settings_layer_round_trips_github_integration_strategy() { - let source = json!({ - "_version": 1, - "server": { - "integrations": { - "github": { - "strategy": "token", - "app_id": "{{ env.GITHUB_APP_ID }}" - } - } - } - }); - - let settings: SettingsLayer = - serde_json::from_value(source.clone()).expect("settings should deserialize"); - let round_trip = serde_json::to_value(&settings).expect("settings should serialize"); - - assert_eq!(round_trip, source); -}