mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-08-28 05:27:41 +00:00
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:
parent
74834c31a0
commit
a74a7b43bb
22 changed files with 395 additions and 439 deletions
|
|
@ -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")
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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::*;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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,
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue