tuitbot_core/config/
mod.rs1pub mod capability;
11mod defaults;
12mod enrichment;
13mod env_overrides;
14pub mod merge;
15mod types;
16mod types_policy;
17mod validation;
18
19#[cfg(test)]
20mod tests;
21#[cfg(test)]
22mod tests_backend;
23
24pub use capability::{compute_tier, CapabilityTier};
25pub use enrichment::{EnrichmentStage, ProfileCompleteness};
26pub use merge::{
27 effective_config, merge_overrides, split_patch_by_scope, validate_override_keys,
28 EffectiveConfigResult, ACCOUNT_SCOPED_KEYS,
29};
30pub use types::{
31 AuthConfig, BusinessProfile, ConnectorConfig, ContentSourceEntry, ContentSourcesConfig,
32 DeploymentCapabilities, DeploymentMode, EmbeddingConfig, GoogleDriveConnectorConfig,
33 IntervalsConfig, LimitsConfig, LlmConfig, LoggingConfig, ScoringConfig, ServerConfig,
34 StorageConfig, TargetsConfig, XApiConfig,
35};
36pub use types_policy::{CircuitBreakerConfig, McpPolicyConfig, ScheduleConfig};
37
38use crate::error::ConfigError;
39use serde::{Deserialize, Serialize};
40use std::env;
41use std::path::PathBuf;
42
43fn default_approval_mode() -> bool {
44 true
45}
46
47fn default_max_batch_approve() -> usize {
48 25
49}
50
51#[derive(Debug, Clone, Default, Deserialize, Serialize, PartialEq, Eq)]
58#[serde(rename_all = "lowercase")]
59pub enum OperatingMode {
60 #[default]
62 Autopilot,
63 Composer,
65}
66
67impl std::fmt::Display for OperatingMode {
68 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
69 match self {
70 OperatingMode::Autopilot => write!(f, "autopilot"),
71 OperatingMode::Composer => write!(f, "composer"),
72 }
73 }
74}
75
76#[derive(Debug, Clone, Default, Deserialize, Serialize)]
78pub struct Config {
79 #[serde(default)]
81 pub mode: OperatingMode,
82
83 #[serde(default)]
85 pub x_api: XApiConfig,
86
87 #[serde(default)]
89 pub auth: AuthConfig,
90
91 #[serde(default)]
93 pub business: BusinessProfile,
94
95 #[serde(default)]
97 pub scoring: ScoringConfig,
98
99 #[serde(default)]
101 pub limits: LimitsConfig,
102
103 #[serde(default)]
105 pub intervals: IntervalsConfig,
106
107 #[serde(default)]
109 pub llm: LlmConfig,
110
111 #[serde(default)]
113 pub targets: TargetsConfig,
114
115 #[serde(default = "default_approval_mode")]
117 pub approval_mode: bool,
118
119 #[serde(default = "default_max_batch_approve")]
121 pub max_batch_approve: usize,
122
123 #[serde(default)]
125 pub server: ServerConfig,
126
127 #[serde(default)]
129 pub storage: StorageConfig,
130
131 #[serde(default)]
133 pub logging: LoggingConfig,
134
135 #[serde(default)]
137 pub schedule: ScheduleConfig,
138
139 #[serde(default)]
141 pub mcp_policy: McpPolicyConfig,
142
143 #[serde(default)]
145 pub circuit_breaker: CircuitBreakerConfig,
146
147 #[serde(default)]
149 pub content_sources: ContentSourcesConfig,
150
151 #[serde(default)]
154 pub deployment_mode: DeploymentMode,
155
156 #[serde(default)]
158 pub connectors: ConnectorConfig,
159
160 #[serde(default)]
163 pub embedding: Option<EmbeddingConfig>,
164}
165
166impl Config {
167 pub fn load(config_path: Option<&str>) -> Result<Config, ConfigError> {
174 let (path, explicit) = Self::resolve_config_path(config_path);
175
176 let mut config = match std::fs::read_to_string(&path) {
177 Ok(contents) => toml::from_str::<Config>(&contents)
178 .map_err(|e| ConfigError::ParseError { source: e })?,
179 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
180 if explicit {
181 return Err(ConfigError::FileNotFound {
182 path: path.display().to_string(),
183 });
184 }
185 Config::default()
186 }
187 Err(_) => {
188 return Err(ConfigError::FileNotFound {
189 path: path.display().to_string(),
190 });
191 }
192 };
193
194 config.apply_env_overrides()?;
195
196 Ok(config)
197 }
198
199 pub fn load_and_validate(config_path: Option<&str>) -> Result<Config, Vec<ConfigError>> {
201 let config = Config::load(config_path).map_err(|e| vec![e])?;
202 config.validate()?;
203 Ok(config)
204 }
205
206 pub fn effective_approval_mode(&self) -> bool {
214 self.approval_mode || self.mode == OperatingMode::Composer
215 }
216
217 pub fn is_composer_mode(&self) -> bool {
219 self.mode == OperatingMode::Composer
220 }
221
222 fn resolve_config_path(config_path: Option<&str>) -> (PathBuf, bool) {
227 if let Some(path) = config_path {
228 return (expand_tilde(path), true);
229 }
230
231 if let Ok(env_path) = env::var("TUITBOT_CONFIG") {
232 return (expand_tilde(&env_path), true);
233 }
234
235 (expand_tilde("~/.tuitbot/config.toml"), false)
236 }
237}
238
239fn expand_tilde(path: &str) -> PathBuf {
241 if let Some(rest) = path.strip_prefix("~/") {
242 if let Some(home) = dirs::home_dir() {
243 return home.join(rest);
244 }
245 } else if path == "~" {
246 if let Some(home) = dirs::home_dir() {
247 return home;
248 }
249 }
250 PathBuf::from(path)
251}