Files
zesdex/src/model/settings.rs
T

154 lines
5.6 KiB
Rust
Raw Normal View History

//! User-configurable settings persisted as JSON in the store's base directory.
//!
//! `Settings::load` / `Settings::save` are the only entry points; every field
//! falls back to a hardcoded default via `Default for Settings` when the file
//! is missing or fails to parse.
use serde::{Deserialize, Serialize};
/// Controls how much network access the agent is permitted during a session.
///
/// `Off` disables outbound requests entirely, `ReadOnly` allows fetches but
/// no mutating calls, `Full` permits everything. Defaults to `Off`.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[derive(Default)]
pub enum InternetMode {
#[default]
Off,
ReadOnly,
Full,
}
/// Default per-node timeout for hive-mind nodes: 10 minutes.
fn default_hive_mind_node_timeout_ms() -> u64 {
600_000
}
/// Top-level application settings, serialized to `settings.json` in the store dir.
///
/// Why: a single flat struct rather than nested config so the JSON file stays
/// human-editable; unknown/missing fields on load fall back to `Default`.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Settings {
pub internet_mode: InternetMode,
pub provider: String,
pub model: String,
pub api_keys: std::collections::HashMap<String, String>,
pub max_tokens: Option<u32>,
pub temperature: Option<f32>,
pub review_enabled: bool,
pub review_max_lessons_per_run: usize,
pub adaptive_review_max_skip: u32,
pub verify_command: Option<String>,
pub verify_timeout_ms: u64,
pub workflow_max_concurrency: usize,
pub session_archive_enabled: bool,
pub lsp_auto_provision: bool,
pub lsp_languages: Vec<String>,
/// Wall-clock deadline for a single hive-mind processing node (cycle
/// node or synthesis node). Prevents one stuck node from hanging an
/// entire hive-mind convergence forever.
#[serde(default = "default_hive_mind_node_timeout_ms")]
pub hive_mind_node_timeout_ms: u64,
}
impl Default for Settings {
fn default() -> Self {
Settings {
internet_mode: InternetMode::Off,
provider: "zen".to_string(),
model: "deepseek-v4-flash-free".to_string(),
api_keys: std::collections::HashMap::new(),
max_tokens: None,
temperature: None,
review_enabled: true,
review_max_lessons_per_run: 5,
adaptive_review_max_skip: 3,
verify_command: None,
verify_timeout_ms: 30000,
workflow_max_concurrency: 5,
session_archive_enabled: true,
lsp_auto_provision: true,
lsp_languages: Vec::new(),
hive_mind_node_timeout_ms: default_hive_mind_node_timeout_ms(),
}
}
}
impl Settings {
/// Load settings from `<store_base_dir>/settings.json`.
///
/// Flow: read file → parse JSON → fall back to `Settings::default()` on
/// any failure (missing file, unreadable, malformed JSON).
///
/// Return: always succeeds; never surfaces I/O or parse errors to the caller.
pub fn load() -> Self {
let store = super::store::Store::new();
let path = store.base_dir.join("settings.json");
std::fs::read_to_string(path)
.ok()
.and_then(|s| serde_json::from_str(&s).ok())
.unwrap_or_default()
}
/// Serialize and write settings to `<store_base_dir>/settings.json`,
/// using write-then-rename with fsync for crash safety.
///
/// Flow: ensure base dir exists → pretty-print JSON → write to a temp
/// file → sync to disk → rename over the real path → sync the directory.
///
/// Return: `Err` if the directory can't be created or the write fails.
pub fn save(&self) -> std::io::Result<()> {
let store = super::store::Store::new();
std::fs::create_dir_all(&store.base_dir)?;
let path = store.base_dir.join("settings.json");
let tmp = store.base_dir.join("settings.json.tmp");
let s = serde_json::to_string_pretty(self)?;
std::fs::write(&tmp, s)?;
let f = std::fs::File::open(&tmp)?;
f.sync_all()?;
std::fs::rename(&tmp, path)?;
let _ = std::fs::File::open(&store.base_dir).and_then(|d| d.sync_all());
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_hive_mind_node_timeout_is_ten_minutes() {
let settings = Settings::default();
assert_eq!(settings.hive_mind_node_timeout_ms, 600_000);
}
#[test]
fn missing_hive_mind_node_timeout_field_falls_back_to_default() {
// Simulates loading a settings.json written before this field
// existed — #[serde(default = ...)] must fill it in rather than
// failing the whole parse (which would silently reset every
// other saved setting to default too).
let old_json = r#"{
"internet_mode": "Off",
"provider": "zen",
"model": "deepseek-v4-flash-free",
"api_keys": {},
"max_tokens": null,
"temperature": null,
"review_enabled": true,
"review_max_lessons_per_run": 5,
"adaptive_review_max_skip": 3,
"verify_command": null,
"verify_timeout_ms": 30000,
"workflow_max_concurrency": 5,
"session_archive_enabled": true,
"lsp_auto_provision": true,
"lsp_languages": []
}"#;
let parsed: Settings = serde_json::from_str(old_json)
.expect("must parse even without the new field present");
assert_eq!(parsed.hive_mind_node_timeout_ms, 600_000);
}
}