refactor(settings): stage 6.3b + 6.5b finish — delete last legacy server types and flatten v2/

**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 <module>::*` 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) <noreply@anthropic.com>
This commit is contained in:
Bryan Helmkamp 2026-04-09 18:32:54 -04:00
parent 74834c31a0
commit a74a7b43bb
22 changed files with 395 additions and 439 deletions

View file

@ -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<ApiAuthStrategy>,
pub tls: Option<TlsSettings>,
}
/// JWT claims for service-to-service authentication.
#[derive(Debug, Deserialize)]
@ -86,8 +117,6 @@ pub fn resolve_auth_mode_with_lookup<F>(
where
F: Fn(&str) -> Option<String>,
{
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")
{

View file

@ -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;

View file

@ -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<SettingsFile> {
/// 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;

View file

@ -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)]

View file

@ -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;

View file

@ -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::*;
}

View file

@ -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<ServerListenLayer>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub api: Option<ServerApiLayer>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub web: Option<ServerWebLayer>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub auth: Option<ServerAuthLayer>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub storage: Option<ServerStorageLayer>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub artifacts: Option<ServerArtifactsLayer>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub slatedb: Option<ServerSlateDbLayer>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub scheduler: Option<ServerSchedulerLayer>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub logging: Option<ServerLoggingLayer>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub integrations: Option<ServerIntegrationsLayer>,
}
/// `[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<InterpString>,
#[serde(default)]
tls: Option<ServerListenTlsLayer>,
},
Unix {
#[serde(default)]
path: Option<InterpString>,
},
}
#[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<InterpString>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub key: Option<InterpString>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ca: Option<InterpString>,
}
/// `[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<InterpString>,
}
/// `[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<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub url: Option<InterpString>,
}
/// `[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<ServerAuthApiLayer>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub web: Option<ServerAuthWebLayer>,
}
/// `[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<ServerAuthApiJwtLayer>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub mtls: Option<ServerAuthApiMtlsLayer>,
}
/// `[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<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub issuer: Option<InterpString>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub audience: Option<InterpString>,
}
/// `[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<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ca: Option<InterpString>,
}
/// `[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<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub providers: Option<ServerAuthWebProvidersLayer>,
}
/// `[server.auth.web.providers.<provider>]` — 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<ServerAuthWebGithubLayer>,
}
/// `[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<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub client_id: Option<InterpString>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub client_secret: Option<InterpString>,
}
/// `[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<InterpString>,
}
/// `[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<ObjectStoreProvider>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub prefix: Option<InterpString>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub local: Option<ObjectStoreLocalLayer>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub s3: Option<ObjectStoreS3Layer>,
}
/// `[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<ObjectStoreProvider>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub prefix: Option<InterpString>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub flush_interval: Option<Duration>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub local: Option<ObjectStoreLocalLayer>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub s3: Option<ObjectStoreS3Layer>,
}
/// 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<InterpString>,
}
#[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<InterpString>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub region: Option<InterpString>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub endpoint: Option<InterpString>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub path_style: Option<bool>,
}
/// `[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<usize>,
}
/// `[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<String>,
}
/// `[server.integrations.<provider>]` — 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<GithubIntegrationLayer>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub slack: Option<SlackIntegrationLayer>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub discord: Option<DiscordIntegrationLayer>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub teams: Option<TeamsIntegrationLayer>,
}
/// `[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<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub app_id: Option<InterpString>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub client_id: Option<InterpString>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub slug: Option<InterpString>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub permissions: HashMap<String, InterpString>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub webhooks: Option<IntegrationWebhooksLayer>,
}
/// `[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<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub default_channel: Option<InterpString>,
}
/// `[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<bool>,
}
/// `[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<bool>,
}
#[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<WebhookStrategy>,
}
#[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<ApiAuthStrategy>,
pub tls: Option<TlsSettings>,
pub enum WebhookStrategy {
TailscaleFunnel,
}

View file

@ -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;

View file

@ -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<ServerListenLayer>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub api: Option<ServerApiLayer>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub web: Option<ServerWebLayer>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub auth: Option<ServerAuthLayer>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub storage: Option<ServerStorageLayer>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub artifacts: Option<ServerArtifactsLayer>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub slatedb: Option<ServerSlateDbLayer>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub scheduler: Option<ServerSchedulerLayer>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub logging: Option<ServerLoggingLayer>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub integrations: Option<ServerIntegrationsLayer>,
}
/// `[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<InterpString>,
#[serde(default)]
tls: Option<ServerListenTlsLayer>,
},
Unix {
#[serde(default)]
path: Option<InterpString>,
},
}
#[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<InterpString>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub key: Option<InterpString>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ca: Option<InterpString>,
}
/// `[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<InterpString>,
}
/// `[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<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub url: Option<InterpString>,
}
/// `[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<ServerAuthApiLayer>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub web: Option<ServerAuthWebLayer>,
}
/// `[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<ServerAuthApiJwtLayer>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub mtls: Option<ServerAuthApiMtlsLayer>,
}
/// `[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<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub issuer: Option<InterpString>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub audience: Option<InterpString>,
}
/// `[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<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ca: Option<InterpString>,
}
/// `[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<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub providers: Option<ServerAuthWebProvidersLayer>,
}
/// `[server.auth.web.providers.<provider>]` — 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<ServerAuthWebGithubLayer>,
}
/// `[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<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub client_id: Option<InterpString>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub client_secret: Option<InterpString>,
}
/// `[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<InterpString>,
}
/// `[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<ObjectStoreProvider>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub prefix: Option<InterpString>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub local: Option<ObjectStoreLocalLayer>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub s3: Option<ObjectStoreS3Layer>,
}
/// `[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<ObjectStoreProvider>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub prefix: Option<InterpString>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub flush_interval: Option<Duration>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub local: Option<ObjectStoreLocalLayer>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub s3: Option<ObjectStoreS3Layer>,
}
/// 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<InterpString>,
}
#[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<InterpString>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub region: Option<InterpString>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub endpoint: Option<InterpString>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub path_style: Option<bool>,
}
/// `[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<usize>,
}
/// `[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<String>,
}
/// `[server.integrations.<provider>]` — 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<GithubIntegrationLayer>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub slack: Option<SlackIntegrationLayer>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub discord: Option<DiscordIntegrationLayer>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub teams: Option<TeamsIntegrationLayer>,
}
/// `[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<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub app_id: Option<InterpString>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub client_id: Option<InterpString>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub slug: Option<InterpString>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub permissions: HashMap<String, InterpString>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub webhooks: Option<IntegrationWebhooksLayer>,
}
/// `[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<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub default_channel: Option<InterpString>,
}
/// `[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<bool>,
}
/// `[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<bool>,
}
#[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<WebhookStrategy>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum WebhookStrategy {
TailscaleFunnel,
}