From a74a7b43bb5953330dd7674acd02ddd80bb498f9 Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Thu, 9 Apr 2026 18:32:54 -0400 Subject: [PATCH] =?UTF-8?q?refactor(settings):=20stage=206.3b=20+=206.5b?= =?UTF-8?q?=20finish=20=E2=80=94=20delete=20last=20legacy=20server=20types?= =?UTF-8?q?=20and=20flatten=20v2/?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **6.3b finishing touch:** relocates the last three transitional server runtime types (`ApiAuthStrategy`, `TlsSettings`, `ApiSettings`) out of `fabro-types` into `fabro-server/src/jwt_auth.rs` — the only crate that consumes them. `serve.rs`, `tls.rs`, and the mTLS integration test now import from `crate::jwt_auth` / `fabro_server::jwt_auth` instead of `fabro_types::settings::server`. `lib/crates/fabro-types/src/settings/server.rs` (the legacy one) and the `pub mod server_config { pub use fabro_types::settings::server::*; }` block in `fabro-server/src/lib.rs` are both deleted. The legacy runtime type module tree under `fabro-types/src/settings/{hook, mcp, project, run, sandbox, server, user}.rs` is now fully gone — nothing left to promote. **6.5b flatten:** `git mv` the fourteen v2 modules up one directory: - `settings/v2/accessors.rs` → `settings/accessors.rs` - `settings/v2/cli.rs` → `settings/cli.rs` - `settings/v2/duration.rs` → `settings/duration.rs` - `settings/v2/features.rs` → `settings/features.rs` - `settings/v2/interp.rs` → `settings/interp.rs` - `settings/v2/model_ref.rs` → `settings/model_ref.rs` - `settings/v2/project.rs` → `settings/project.rs` - `settings/v2/run.rs` → `settings/run.rs` - `settings/v2/server.rs` → `settings/server.rs` (name no longer collides with the deleted legacy `server.rs`) - `settings/v2/size.rs` → `settings/size.rs` - `settings/v2/splice_array.rs` → `settings/splice_array.rs` - `settings/v2/tree.rs` → `settings/tree.rs` - `settings/v2/version.rs` → `settings/version.rs` - `settings/v2/workflow.rs` → `settings/workflow.rs` - `settings/v2/mod.rs` — deleted (its `pub mod` / `pub use` block moved into `settings/mod.rs`). `settings/mod.rs` picks up those `pub mod` declarations and the accompanying `pub use ::*` re-exports, plus a transitional `pub mod v2 { pub use super::*; }` alias so that existing `fabro_types::settings::v2::*` import paths across the workspace keep compiling. A follow-up sweep will drop the `::v2::` prefix from every consumer and then the alias can go away. 3,758 workspace tests pass. `cargo fmt --check --all` and `cargo clippy --workspace -- -D warnings` are clean. Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/crates/fabro-server/src/jwt_auth.rs | 37 +- lib/crates/fabro-server/src/lib.rs | 3 - lib/crates/fabro-server/src/serve.rs | 4 +- lib/crates/fabro-server/src/tls.rs | 4 +- lib/crates/fabro-server/tests/it/api/mtls.rs | 3 +- .../src/settings/{v2 => }/accessors.rs | 0 .../fabro-types/src/settings/{v2 => }/cli.rs | 0 .../src/settings/{v2 => }/duration.rs | 0 .../src/settings/{v2 => }/features.rs | 0 .../src/settings/{v2 => }/interp.rs | 0 lib/crates/fabro-types/src/settings/mod.rs | 76 ++-- .../src/settings/{v2 => }/model_ref.rs | 0 .../src/settings/{v2 => }/project.rs | 0 .../fabro-types/src/settings/{v2 => }/run.rs | 0 lib/crates/fabro-types/src/settings/server.rs | 346 ++++++++++++++++-- .../fabro-types/src/settings/{v2 => }/size.rs | 0 .../src/settings/{v2 => }/splice_array.rs | 0 .../fabro-types/src/settings/{v2 => }/tree.rs | 0 lib/crates/fabro-types/src/settings/v2/mod.rs | 38 -- .../fabro-types/src/settings/v2/server.rs | 323 ---------------- .../src/settings/{v2 => }/version.rs | 0 .../src/settings/{v2 => }/workflow.rs | 0 22 files changed, 395 insertions(+), 439 deletions(-) rename lib/crates/fabro-types/src/settings/{v2 => }/accessors.rs (100%) rename lib/crates/fabro-types/src/settings/{v2 => }/cli.rs (100%) rename lib/crates/fabro-types/src/settings/{v2 => }/duration.rs (100%) rename lib/crates/fabro-types/src/settings/{v2 => }/features.rs (100%) rename lib/crates/fabro-types/src/settings/{v2 => }/interp.rs (100%) rename lib/crates/fabro-types/src/settings/{v2 => }/model_ref.rs (100%) rename lib/crates/fabro-types/src/settings/{v2 => }/project.rs (100%) rename lib/crates/fabro-types/src/settings/{v2 => }/run.rs (100%) rename lib/crates/fabro-types/src/settings/{v2 => }/size.rs (100%) rename lib/crates/fabro-types/src/settings/{v2 => }/splice_array.rs (100%) rename lib/crates/fabro-types/src/settings/{v2 => }/tree.rs (100%) delete mode 100644 lib/crates/fabro-types/src/settings/v2/mod.rs delete mode 100644 lib/crates/fabro-types/src/settings/v2/server.rs rename lib/crates/fabro-types/src/settings/{v2 => }/version.rs (100%) rename lib/crates/fabro-types/src/settings/{v2 => }/workflow.rs (100%) diff --git a/lib/crates/fabro-server/src/jwt_auth.rs b/lib/crates/fabro-server/src/jwt_auth.rs index 698b1a06d..f1589f311 100644 --- a/lib/crates/fabro-server/src/jwt_auth.rs +++ b/lib/crates/fabro-server/src/jwt_auth.rs @@ -1,3 +1,4 @@ +use std::path::PathBuf; use std::sync::Arc; use axum::extract::FromRequestParts; @@ -5,13 +6,43 @@ use axum::http::request::Parts; use base64::engine::general_purpose::STANDARD as BASE64_STANDARD; use jsonwebtoken::{Algorithm, DecodingKey, Validation}; use rustls_pki_types::CertificateDer; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; use tracing::warn; use crate::error::ApiError; use crate::web_auth::SessionCookie; use fabro_types::RunAuthMethod; -use fabro_types::settings::server::ApiSettings; + +/// Authentication strategy flag consumed by `resolve_auth_mode_with_lookup`. +/// +/// Projected out of the v2 `server.auth.api.{jwt,mtls}` subtree by +/// `serve::build_legacy_api_settings`. Stage 6.6g will delete this shim +/// and walk the v2 tree directly in the auth resolver. +#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum ApiAuthStrategy { + Jwt, + Mtls, +} + +/// mTLS material loaded from `[server.listen.tls]`. Consumed by the +/// `tls.rs` rustls config builder. +#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)] +pub struct TlsSettings { + pub cert: PathBuf, + pub key: PathBuf, + pub ca: PathBuf, +} + +/// Shim `ApiSettings` that `serve::build_legacy_api_settings` projects out +/// of the v2 tree so the pre-v2 [`resolve_auth_mode_with_lookup`] signature +/// keeps working until Stage 6.6g rewrites it. +#[derive(Debug, Clone, Default, Deserialize, PartialEq, Serialize)] +pub struct ApiSettings { + #[serde(default)] + pub authentication_strategies: Vec, + pub tls: Option, +} /// JWT claims for service-to-service authentication. #[derive(Debug, Deserialize)] @@ -86,8 +117,6 @@ pub fn resolve_auth_mode_with_lookup( where F: Fn(&str) -> Option, { - use fabro_types::settings::server::ApiAuthStrategy; - if api_settings.authentication_strategies.is_empty() && std::env::var("FABRO_LOCAL_NO_AUTH").ok().as_deref() == Some("1") { diff --git a/lib/crates/fabro-server/src/lib.rs b/lib/crates/fabro-server/src/lib.rs index 4a7da2c01..507c77f2a 100644 --- a/lib/crates/fabro-server/src/lib.rs +++ b/lib/crates/fabro-server/src/lib.rs @@ -16,8 +16,5 @@ pub mod serve; pub mod server; mod settings_view; pub mod static_files; -pub mod server_config { - pub use fabro_types::settings::server::*; -} pub mod tls; pub mod web_auth; diff --git a/lib/crates/fabro-server/src/serve.rs b/lib/crates/fabro-server/src/serve.rs index 99806c727..65904f1e1 100644 --- a/lib/crates/fabro-server/src/serve.rs +++ b/lib/crates/fabro-server/src/serve.rs @@ -2,10 +2,10 @@ use std::path::{Path, PathBuf}; use std::sync::{Arc, RwLock}; use std::time::Duration; +use crate::jwt_auth::ApiSettings; use fabro_config::Storage; use fabro_config::resolve_storage_dir; use fabro_config::user::{active_settings_path, load_settings_config}; -use fabro_types::settings::server::ApiSettings; use fabro_util::terminal::Styles; use object_store::ObjectStore; use object_store::aws::AmazonS3Builder; @@ -90,7 +90,7 @@ fn load_settings(path: Option<&Path>) -> anyhow::Result { /// v2 tree. Stage 6.6 replaces this with a v2-aware auth resolver and drops /// the legacy `ApiSettings` type entirely. fn build_legacy_api_settings(file: &SettingsFile) -> ApiSettings { - use fabro_types::settings::server::{ApiAuthStrategy, TlsSettings}; + use crate::jwt_auth::{ApiAuthStrategy, TlsSettings}; use fabro_types::settings::v2::interp::InterpString; use fabro_types::settings::v2::server::ServerListenLayer; diff --git a/lib/crates/fabro-server/src/tls.rs b/lib/crates/fabro-server/src/tls.rs index 0009253ca..ff908b3fd 100644 --- a/lib/crates/fabro-server/src/tls.rs +++ b/lib/crates/fabro-server/src/tls.rs @@ -8,9 +8,7 @@ use rustls_pki_types::{CertificateDer, PrivateKeyDer}; use tokio::net::TcpListener; use tracing::error; -use fabro_types::settings::server::TlsSettings; - -use crate::jwt_auth::PeerCertificates; +use crate::jwt_auth::{PeerCertificates, TlsSettings}; /// How client certificates should be verified. #[derive(Clone, Copy)] diff --git a/lib/crates/fabro-server/tests/it/api/mtls.rs b/lib/crates/fabro-server/tests/it/api/mtls.rs index 9ae4ade9f..232cc545c 100644 --- a/lib/crates/fabro-server/tests/it/api/mtls.rs +++ b/lib/crates/fabro-server/tests/it/api/mtls.rs @@ -4,9 +4,8 @@ use crate::helpers::api; use std::path::{Path, PathBuf}; use std::sync::Arc; -use fabro_server::jwt_auth::{AuthMode, AuthStrategy}; +use fabro_server::jwt_auth::{AuthMode, AuthStrategy, TlsSettings}; use fabro_server::server::{build_router, create_app_state}; -use fabro_server::server_config::TlsSettings; use fabro_server::tls::{ClientAuth, build_rustls_config}; use tokio::net::TcpListener; diff --git a/lib/crates/fabro-types/src/settings/v2/accessors.rs b/lib/crates/fabro-types/src/settings/accessors.rs similarity index 100% rename from lib/crates/fabro-types/src/settings/v2/accessors.rs rename to lib/crates/fabro-types/src/settings/accessors.rs diff --git a/lib/crates/fabro-types/src/settings/v2/cli.rs b/lib/crates/fabro-types/src/settings/cli.rs similarity index 100% rename from lib/crates/fabro-types/src/settings/v2/cli.rs rename to lib/crates/fabro-types/src/settings/cli.rs diff --git a/lib/crates/fabro-types/src/settings/v2/duration.rs b/lib/crates/fabro-types/src/settings/duration.rs similarity index 100% rename from lib/crates/fabro-types/src/settings/v2/duration.rs rename to lib/crates/fabro-types/src/settings/duration.rs diff --git a/lib/crates/fabro-types/src/settings/v2/features.rs b/lib/crates/fabro-types/src/settings/features.rs similarity index 100% rename from lib/crates/fabro-types/src/settings/v2/features.rs rename to lib/crates/fabro-types/src/settings/features.rs diff --git a/lib/crates/fabro-types/src/settings/v2/interp.rs b/lib/crates/fabro-types/src/settings/interp.rs similarity index 100% rename from lib/crates/fabro-types/src/settings/v2/interp.rs rename to lib/crates/fabro-types/src/settings/interp.rs diff --git a/lib/crates/fabro-types/src/settings/mod.rs b/lib/crates/fabro-types/src/settings/mod.rs index 38003a3ad..016922ca8 100644 --- a/lib/crates/fabro-types/src/settings/mod.rs +++ b/lib/crates/fabro-types/src/settings/mod.rs @@ -1,40 +1,48 @@ -//! v2 namespaced config schema plus transitional runtime shapes. +//! Namespaced settings schema. //! -//! The authoritative config schema lives in [`v2`] — it is the namespaced -//! parse tree that `_version = 1` TOML files decode into. Value-language -//! helpers, the merge matrix, and strict unknown-key validation all live -//! there. +//! Top-level schema is strictly namespaced with `_version`, `[project]`, +//! `[workflow]`, `[run]`, `[cli]`, `[server]`, and `[features]`. +//! Value-language helpers live alongside the tree: durations, byte sizes, +//! model references, env interpolation, and splice-capable arrays. //! -//! The submodules `hook`, `mcp`, `project`, `run`, `sandbox`, `server`, -//! and `user` still hold **runtime shapes** that downstream crates -//! (fabro-workflow, fabro-sandbox, fabro-mcp, fabro-hooks) consume at -//! execution time. Stage 6.1 deleted the flat `Settings` parse path; -//! Stage 6.2 deleted the `bridge_to_old` catch-all converter; Stage 6.3b -//! deleted the legacy flat `Settings` struct itself, its inherent -//! helpers, and its `Combine`-driven layering. Narrow v2→runtime helpers -//! live in [`v2::to_runtime`] and build these runtime shapes from -//! specific v2 subtrees on demand. -//! -//! A follow-up pass will either promote these runtime shapes into their -//! owning consumer crates or replace their call sites with v2-native -//! accessors, at which point this module goes away. +//! Stage 6.5b promoted these modules up out of the transitional +//! `settings/v2/` subdirectory, so the `::v2::` path prefix no longer +//! exists. +pub mod accessors; +pub mod cli; +pub mod duration; +pub mod features; +pub mod interp; +pub mod model_ref; +pub mod project; +pub mod run; pub mod server; -pub mod v2; +pub mod size; +pub mod splice_array; +pub mod tree; +pub mod version; +pub mod workflow; -pub use server::{ApiAuthStrategy, ApiSettings, TlsSettings}; -// v2 top-level re-exports. Stage 6.5 of the settings TOML redesign -// promoted the v2 namespaced parse tree to be the primary API surface; -// consumers can now write `fabro_types::settings::SettingsFile` / -// `fabro_types::settings::InterpString` / `fabro_types::settings::Duration` -// without the `::v2::` prefix. The `v2` module itself stays until the -// remaining legacy files under `settings/{project,run,server,...}.rs` -// are deleted in a follow-up pass, because the v2 submodules and the -// legacy submodules share those file names. -pub use v2::{ - CURRENT_VERSION, CliLayer, Duration, FeaturesLayer, InterpString, ModelRef, ParseDurationError, - ParseError, ParseModelRefError, ParseSizeError, ProjectLayer, Provenance, ResolveEnvError, - Resolved, ResolvedModelRef, RunLayer, SchemaVersion, ServerLayer, SettingsFile, Size, - SpliceArray, SpliceArrayError, VersionError, WorkflowLayer, parse_settings_file, - validate_version, +pub use cli::CliLayer; +pub use duration::{Duration, ParseDurationError}; +pub use features::FeaturesLayer; +pub use interp::{InterpString, Provenance, ResolveEnvError, Resolved}; +pub use model_ref::{ + AmbiguousModelRef, ModelRef, ModelRegistry, ParseModelRefError, ResolvedModelRef, }; +pub use project::ProjectLayer; +pub use run::RunLayer; +pub use server::ServerLayer; +pub use size::{ParseSizeError, Size}; +pub use splice_array::{SPLICE_MARKER, SpliceArray, SpliceArrayError}; +pub use tree::{ParseError, SettingsFile, parse_settings_file}; +pub use version::{CURRENT_VERSION, SchemaVersion, VersionError, validate_version}; +pub use workflow::WorkflowLayer; + +/// Transitional alias for code still using `fabro_types::settings::v2::*` +/// paths. The whole `v2` namespace is scheduled for removal once the +/// workspace-wide sweep (Stage 6.5b follow-up) is done. +pub mod v2 { + pub use super::*; +} diff --git a/lib/crates/fabro-types/src/settings/v2/model_ref.rs b/lib/crates/fabro-types/src/settings/model_ref.rs similarity index 100% rename from lib/crates/fabro-types/src/settings/v2/model_ref.rs rename to lib/crates/fabro-types/src/settings/model_ref.rs diff --git a/lib/crates/fabro-types/src/settings/v2/project.rs b/lib/crates/fabro-types/src/settings/project.rs similarity index 100% rename from lib/crates/fabro-types/src/settings/v2/project.rs rename to lib/crates/fabro-types/src/settings/project.rs diff --git a/lib/crates/fabro-types/src/settings/v2/run.rs b/lib/crates/fabro-types/src/settings/run.rs similarity index 100% rename from lib/crates/fabro-types/src/settings/v2/run.rs rename to lib/crates/fabro-types/src/settings/run.rs diff --git a/lib/crates/fabro-types/src/settings/server.rs b/lib/crates/fabro-types/src/settings/server.rs index 7562290ff..c1931c620 100644 --- a/lib/crates/fabro-types/src/settings/server.rs +++ b/lib/crates/fabro-types/src/settings/server.rs @@ -1,37 +1,323 @@ -//! Transitional server-domain runtime types. +//! Server domain. //! -//! Only the three types that the auth resolver (`jwt_auth.rs`) and the -//! TLS loader (`tls.rs`) still consume remain here. Stage 6.6g will -//! rewrite those consumers to walk the v2 `server.auth.api` / `server.listen.tls` -//! subtrees directly, at which point this file goes away and the -//! legacy runtime type module tree is fully gone. +//! `[server]` is a namespace container; actual settings live in named +//! subdomains (listen, api, web, auth, storage, artifacts, slatedb, +//! scheduler, logging, integrations). Same-host and split-host deployments +//! use the same schema. -use std::path::PathBuf; +use std::collections::HashMap; use serde::{Deserialize, Serialize}; -/// Authentication strategy flag consumed by `resolve_auth_mode_with_lookup`. -#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)] +use super::duration::Duration; +use super::interp::InterpString; + +/// 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 { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub listen: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub api: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub web: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub auth: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub storage: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub artifacts: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub slatedb: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub scheduler: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub logging: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub integrations: Option, +} + +/// `[server.listen]` — shared bind transport. TLS lives under `[server.listen.tls]`. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields, tag = "type", rename_all = "lowercase")] +pub enum ServerListenLayer { + Tcp { + #[serde(default)] + address: Option, + #[serde(default)] + tls: Option, + }, + Unix { + #[serde(default)] + path: Option, + }, +} + +#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct ServerListenTlsLayer { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub cert: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub key: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub ca: Option, +} + +/// `[server.api]` — API surface settings. +/// +/// `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 { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub url: Option, +} + +/// `[server.web]` — web surface settings. +#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct ServerWebLayer { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub enabled: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub url: Option, +} + +/// `[server.auth]` — cohesive server auth surface. +/// +/// When absent or resolved to no enabled API or web auth configuration, the +/// default server startup posture is fail-closed. Demo and test helpers may +/// explicitly opt in to insecure configurations. +#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct ServerAuthLayer { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub api: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub web: Option, +} + +/// `[server.auth.api]` — supports multiple strategies concurrently. Each +/// strategy is a named subtable: `[server.auth.api.jwt]`, `[server.auth.api.mtls]`. +#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct ServerAuthApiLayer { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub jwt: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub mtls: Option, +} + +/// `[server.auth.api.jwt]` — JWT auth strategy fields. +#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct ServerAuthApiJwtLayer { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub enabled: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub issuer: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub audience: Option, +} + +/// `[server.auth.api.mtls]` — mutual TLS auth strategy fields. +#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct ServerAuthApiMtlsLayer { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub enabled: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub ca: Option, +} + +/// `[server.auth.web]` — provider-neutral access rules plus keyed providers. +#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct ServerAuthWebLayer { + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub allowed_usernames: Vec, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub providers: Option, +} + +/// `[server.auth.web.providers.]` — web auth providers keyed by +/// provider name. First-pass providers cover GitHub. +#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct ServerAuthWebProvidersLayer { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub github: Option, +} + +/// `[server.auth.web.providers.github]` — GitHub OAuth configuration fields. +#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct ServerAuthWebGithubLayer { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub enabled: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub client_id: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub client_secret: Option, +} + +/// `[server.storage]` — single managed local disk root. +#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct ServerStorageLayer { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub root: Option, +} + +/// `[server.artifacts]` — object-store-backed artifact storage. +#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct ServerArtifactsLayer { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub provider: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub prefix: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub local: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub s3: Option, +} + +/// `[server.slatedb]` — SlateDB bottomless storage plus tunables. +#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct ServerSlateDbLayer { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub provider: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub prefix: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub flush_interval: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub local: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub s3: Option, +} + +/// Closed enum of object-store providers. Unknown providers hard-fail +/// against the schema rather than passing through as opaque strings. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum ObjectStoreProvider { + Local, + S3, +} + +#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct ObjectStoreLocalLayer { + /// Overrides the default root, which otherwise falls back to + /// `server.storage.root`. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub root: Option, +} + +#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct ObjectStoreS3Layer { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub bucket: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub region: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub endpoint: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub path_style: Option, +} + +/// `[server.scheduler]` — server-managed execution policy. +#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct ServerSchedulerLayer { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub max_concurrent_runs: Option, +} + +/// `[server.logging]` — process-owned logging configuration for the server. +#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct ServerLoggingLayer { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub level: Option, +} + +/// `[server.integrations.]` — cohesive integration surface for chat +/// platforms and git providers (GitHub App, webhooks, etc.). First-pass +/// integrations enumerate known providers rather than using a flatten-HashMap +/// shape so strict unknown-field validation still holds. +#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct ServerIntegrationsLayer { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub github: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub slack: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub discord: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub teams: Option, +} + +/// `[server.integrations.github]` — GitHub App, credentials, and inbound webhooks. +#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct GithubIntegrationLayer { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub enabled: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub app_id: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub client_id: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub slug: Option, + #[serde(default, skip_serializing_if = "HashMap::is_empty")] + pub permissions: HashMap, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub webhooks: Option, +} + +/// `[server.integrations.slack]` — Slack workspace credentials and defaults. +#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct SlackIntegrationLayer { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub enabled: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub default_channel: Option, +} + +/// `[server.integrations.discord]` — Discord workspace configuration. +#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct DiscordIntegrationLayer { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub enabled: Option, +} + +/// `[server.integrations.teams]` — Microsoft Teams configuration. +#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub 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 { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub strategy: Option, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] -pub enum ApiAuthStrategy { - Jwt, - Mtls, -} - -/// mTLS material loaded by `fabro_server::tls::*`. -#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)] -pub struct TlsSettings { - pub cert: PathBuf, - pub key: PathBuf, - pub ca: PathBuf, -} - -/// Shim `ApiSettings` that `fabro_server::serve::build_legacy_api_settings` -/// projects out of the v2 tree so the pre-v2 `resolve_auth_mode_with_lookup` -/// signature keeps working until Stage 6.6g rewrites it. -#[derive(Debug, Clone, Default, Deserialize, PartialEq, Serialize)] -pub struct ApiSettings { - #[serde(default)] - pub authentication_strategies: Vec, - pub tls: Option, +pub enum WebhookStrategy { + TailscaleFunnel, } diff --git a/lib/crates/fabro-types/src/settings/v2/size.rs b/lib/crates/fabro-types/src/settings/size.rs similarity index 100% rename from lib/crates/fabro-types/src/settings/v2/size.rs rename to lib/crates/fabro-types/src/settings/size.rs diff --git a/lib/crates/fabro-types/src/settings/v2/splice_array.rs b/lib/crates/fabro-types/src/settings/splice_array.rs similarity index 100% rename from lib/crates/fabro-types/src/settings/v2/splice_array.rs rename to lib/crates/fabro-types/src/settings/splice_array.rs diff --git a/lib/crates/fabro-types/src/settings/v2/tree.rs b/lib/crates/fabro-types/src/settings/tree.rs similarity index 100% rename from lib/crates/fabro-types/src/settings/v2/tree.rs rename to lib/crates/fabro-types/src/settings/tree.rs diff --git a/lib/crates/fabro-types/src/settings/v2/mod.rs b/lib/crates/fabro-types/src/settings/v2/mod.rs deleted file mode 100644 index 8e2e9ddce..000000000 --- a/lib/crates/fabro-types/src/settings/v2/mod.rs +++ /dev/null @@ -1,38 +0,0 @@ -//! Namespaced settings schema (v2). -//! -//! This module is the hard-cut replacement for the flat [`super::Settings`] -//! shape. The top-level schema is strictly namespaced with `_version`, -//! `[project]`, `[workflow]`, `[run]`, `[cli]`, `[server]`, and `[features]`. -//! Value-language helpers live alongside the tree: durations, byte sizes, -//! model references, env interpolation, and splice-capable arrays. - -pub mod accessors; -pub mod cli; -pub mod duration; -pub mod features; -pub mod interp; -pub mod model_ref; -pub mod project; -pub mod run; -pub mod server; -pub mod size; -pub mod splice_array; -pub mod tree; -pub mod version; -pub mod workflow; - -pub use cli::CliLayer; -pub use duration::{Duration, ParseDurationError}; -pub use features::FeaturesLayer; -pub use interp::{InterpString, Provenance, ResolveEnvError, Resolved}; -pub use model_ref::{ - AmbiguousModelRef, ModelRef, ModelRegistry, ParseModelRefError, ResolvedModelRef, -}; -pub use project::ProjectLayer; -pub use run::RunLayer; -pub use server::ServerLayer; -pub use size::{ParseSizeError, Size}; -pub use splice_array::{SPLICE_MARKER, SpliceArray, SpliceArrayError}; -pub use tree::{ParseError, SettingsFile, parse_settings_file}; -pub use version::{CURRENT_VERSION, SchemaVersion, VersionError, validate_version}; -pub use workflow::WorkflowLayer; diff --git a/lib/crates/fabro-types/src/settings/v2/server.rs b/lib/crates/fabro-types/src/settings/v2/server.rs deleted file mode 100644 index c1931c620..000000000 --- a/lib/crates/fabro-types/src/settings/v2/server.rs +++ /dev/null @@ -1,323 +0,0 @@ -//! Server domain. -//! -//! `[server]` is a namespace container; actual settings live in named -//! subdomains (listen, api, web, auth, storage, artifacts, slatedb, -//! scheduler, logging, integrations). Same-host and split-host deployments -//! use the same schema. - -use std::collections::HashMap; - -use serde::{Deserialize, Serialize}; - -use super::duration::Duration; -use super::interp::InterpString; - -/// 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 { - #[serde(default, skip_serializing_if = "Option::is_none")] - pub listen: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub api: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub web: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub auth: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub storage: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub artifacts: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub slatedb: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub scheduler: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub logging: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub integrations: Option, -} - -/// `[server.listen]` — shared bind transport. TLS lives under `[server.listen.tls]`. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(deny_unknown_fields, tag = "type", rename_all = "lowercase")] -pub enum ServerListenLayer { - Tcp { - #[serde(default)] - address: Option, - #[serde(default)] - tls: Option, - }, - Unix { - #[serde(default)] - path: Option, - }, -} - -#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct ServerListenTlsLayer { - #[serde(default, skip_serializing_if = "Option::is_none")] - pub cert: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub key: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub ca: Option, -} - -/// `[server.api]` — API surface settings. -/// -/// `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 { - #[serde(default, skip_serializing_if = "Option::is_none")] - pub url: Option, -} - -/// `[server.web]` — web surface settings. -#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct ServerWebLayer { - #[serde(default, skip_serializing_if = "Option::is_none")] - pub enabled: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub url: Option, -} - -/// `[server.auth]` — cohesive server auth surface. -/// -/// When absent or resolved to no enabled API or web auth configuration, the -/// default server startup posture is fail-closed. Demo and test helpers may -/// explicitly opt in to insecure configurations. -#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct ServerAuthLayer { - #[serde(default, skip_serializing_if = "Option::is_none")] - pub api: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub web: Option, -} - -/// `[server.auth.api]` — supports multiple strategies concurrently. Each -/// strategy is a named subtable: `[server.auth.api.jwt]`, `[server.auth.api.mtls]`. -#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct ServerAuthApiLayer { - #[serde(default, skip_serializing_if = "Option::is_none")] - pub jwt: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub mtls: Option, -} - -/// `[server.auth.api.jwt]` — JWT auth strategy fields. -#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct ServerAuthApiJwtLayer { - #[serde(default, skip_serializing_if = "Option::is_none")] - pub enabled: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub issuer: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub audience: Option, -} - -/// `[server.auth.api.mtls]` — mutual TLS auth strategy fields. -#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct ServerAuthApiMtlsLayer { - #[serde(default, skip_serializing_if = "Option::is_none")] - pub enabled: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub ca: Option, -} - -/// `[server.auth.web]` — provider-neutral access rules plus keyed providers. -#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct ServerAuthWebLayer { - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub allowed_usernames: Vec, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub providers: Option, -} - -/// `[server.auth.web.providers.]` — web auth providers keyed by -/// provider name. First-pass providers cover GitHub. -#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct ServerAuthWebProvidersLayer { - #[serde(default, skip_serializing_if = "Option::is_none")] - pub github: Option, -} - -/// `[server.auth.web.providers.github]` — GitHub OAuth configuration fields. -#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct ServerAuthWebGithubLayer { - #[serde(default, skip_serializing_if = "Option::is_none")] - pub enabled: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub client_id: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub client_secret: Option, -} - -/// `[server.storage]` — single managed local disk root. -#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct ServerStorageLayer { - #[serde(default, skip_serializing_if = "Option::is_none")] - pub root: Option, -} - -/// `[server.artifacts]` — object-store-backed artifact storage. -#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct ServerArtifactsLayer { - #[serde(default, skip_serializing_if = "Option::is_none")] - pub provider: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub prefix: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub local: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub s3: Option, -} - -/// `[server.slatedb]` — SlateDB bottomless storage plus tunables. -#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct ServerSlateDbLayer { - #[serde(default, skip_serializing_if = "Option::is_none")] - pub provider: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub prefix: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub flush_interval: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub local: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub s3: Option, -} - -/// Closed enum of object-store providers. Unknown providers hard-fail -/// against the schema rather than passing through as opaque strings. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "lowercase")] -pub enum ObjectStoreProvider { - Local, - S3, -} - -#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct ObjectStoreLocalLayer { - /// Overrides the default root, which otherwise falls back to - /// `server.storage.root`. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub root: Option, -} - -#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct ObjectStoreS3Layer { - #[serde(default, skip_serializing_if = "Option::is_none")] - pub bucket: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub region: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub endpoint: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub path_style: Option, -} - -/// `[server.scheduler]` — server-managed execution policy. -#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct ServerSchedulerLayer { - #[serde(default, skip_serializing_if = "Option::is_none")] - pub max_concurrent_runs: Option, -} - -/// `[server.logging]` — process-owned logging configuration for the server. -#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct ServerLoggingLayer { - #[serde(default, skip_serializing_if = "Option::is_none")] - pub level: Option, -} - -/// `[server.integrations.]` — cohesive integration surface for chat -/// platforms and git providers (GitHub App, webhooks, etc.). First-pass -/// integrations enumerate known providers rather than using a flatten-HashMap -/// shape so strict unknown-field validation still holds. -#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct ServerIntegrationsLayer { - #[serde(default, skip_serializing_if = "Option::is_none")] - pub github: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub slack: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub discord: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub teams: Option, -} - -/// `[server.integrations.github]` — GitHub App, credentials, and inbound webhooks. -#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct GithubIntegrationLayer { - #[serde(default, skip_serializing_if = "Option::is_none")] - pub enabled: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub app_id: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub client_id: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub slug: Option, - #[serde(default, skip_serializing_if = "HashMap::is_empty")] - pub permissions: HashMap, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub webhooks: Option, -} - -/// `[server.integrations.slack]` — Slack workspace credentials and defaults. -#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct SlackIntegrationLayer { - #[serde(default, skip_serializing_if = "Option::is_none")] - pub enabled: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub default_channel: Option, -} - -/// `[server.integrations.discord]` — Discord workspace configuration. -#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct DiscordIntegrationLayer { - #[serde(default, skip_serializing_if = "Option::is_none")] - pub enabled: Option, -} - -/// `[server.integrations.teams]` — Microsoft Teams configuration. -#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -pub 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 { - #[serde(default, skip_serializing_if = "Option::is_none")] - pub strategy: Option, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum WebhookStrategy { - TailscaleFunnel, -} diff --git a/lib/crates/fabro-types/src/settings/v2/version.rs b/lib/crates/fabro-types/src/settings/version.rs similarity index 100% rename from lib/crates/fabro-types/src/settings/v2/version.rs rename to lib/crates/fabro-types/src/settings/version.rs diff --git a/lib/crates/fabro-types/src/settings/v2/workflow.rs b/lib/crates/fabro-types/src/settings/workflow.rs similarity index 100% rename from lib/crates/fabro-types/src/settings/v2/workflow.rs rename to lib/crates/fabro-types/src/settings/workflow.rs