Configuration
Configuration management with atomic writes, hot reload, conflict detection, and preset overrides.
The config module manages Aleph's configuration system. It supports file-based configuration via TOML, programmatic updates at runtime, and hot-reloading without restarting the server.
Design Philosophy
Configuration in Aleph follows three principles:
- Atomicity — Configuration changes are never partial; either the entire change applies or none of it does
- Safety — Runtime updates are guarded against conflicts and accidental erasure
- Transparency — Changes are logged, auditable, and reversible
The configuration system is built on top of standard Rust file operations with defensive patterns to prevent data loss.
Architecture
┌─────────────────────────────────────────┐
│ ConfigManager │
│ ┌─────────────┐ ┌──────────────────┐ │
│ │ FileStore │ │ ConfigPatcher │ │
│ └─────────────┘ └──────────────────┘ │
└─────────────────────────────────────────┘
│ ▲
│ read/write │ patch
▼ │
┌──────────┐ ┌──────────────┐
│ aleph.toml│ │ Runtime │
│ (~/.aleph/)│ │ Updates │
└──────────┘ └──────────────┘Core Components
ConfigManager
The central configuration coordinator.
pub struct ConfigManager {
store: FileStore,
patcher: ConfigPatcher,
defaults: ConfigDefaults,
}Responsibilities:
- Loads configuration from
~/.aleph/aleph.tomlat startup - Watches the file for changes (via
notifycrate) - Applies runtime patches from the Gateway's admin API
- Persists changes back to disk atomically
FileStore
Handles all file I/O with atomic write semantics.
impl FileStore {
pub async fn save_to_file(&self,
path: &Path,
config: &Config,
) -> Result<(), ConfigError> {
// 1. Write to temp file
// 2. fsync to ensure data hits disk
// 3. Atomic rename (replace old file)
// 4. Clean up temp file on success
}
}Atomic Write Pattern:
- Write new content to a temporary file in the same directory
- Call
fsync()to flush OS buffers to disk - Rename temp file to target filename (atomic on POSIX)
- Delete temp file if rename succeeds
If any step fails, the original configuration file remains untouched.
ConfigPatcher
Applies incremental updates with conflict detection.
pub struct ConfigPatcher {
last_modified: SystemTime,
}
impl ConfigPatcher {
pub async fn apply(
&mut self,
config: &mut Config,
patch: ConfigPatch,
) -> Result<(), PatchError> {
// 1. Check mtime — has file changed since last read?
// 2. If conflict, re-read file and re-apply patch
// 3. Validate patch invariants
// 4. Apply under write lock
}
}Conflict Detection (TOCTOU Mitigation):
- Records the file's modification time before reading
- Before writing, checks if the mtime has changed
- If changed: re-reads the file, merges the patch with current state, then writes
- This prevents lost updates when both file watcher and API try to modify config simultaneously
Configuration Structure
# ~/.aleph/aleph.toml
[server]
host = "127.0.0.1"
port = 18790
[channels.telegram]
enabled = true
token = "${TELEGRAM_BOT_TOKEN}"
[channels.discord]
enabled = true
token = "${DISCORD_BOT_TOKEN}"
[memory]
embedding_model = "text-embedding-3-small"
max_facts = 10000
[tools]
allowed = ["search", "bash_exec", "file_read"]
[[presets]]
name = "fast"
provider = "openai"
model = "gpt-4o-mini"
temperature = 0.7Environment Variable Substitution
Values prefixed with ${} are resolved from environment variables at load time:
token = "${TELEGRAM_BOT_TOKEN}"This keeps secrets out of the configuration file while allowing version-controlled defaults for non-sensitive settings.
Preset Override
Named presets can override default settings:
// Use the "fast" preset for this request
let config = Config::load()
.with_preset("fast")
.build();Presets are useful for:
- Switching between fast/cheap and slow/accurate models
- A/B testing configurations
- Guest-specific settings
Defensive Design
Embedding Provider Guard
Prevents accidental erasure of embedding provider configuration:
if patch.removes("memory.embedding_model") {
// Capture backtrace for debugging
tracing::warn!("Attempted to remove embedding provider");
return Err(PatchError::InvalidEmbeddingChange);
}Validation
All patches are validated before application:
- Port numbers must be in valid range (1-65535)
- File paths must not contain path traversal (
../) - Array lengths must be within bounds
- Enum values must be valid variants
Hot Reload
Configuration changes are detected within 600ms via notify-debouncer-full:
// File watcher setup
let mut watcher = notify::recommended_watcher(move |res| {
match res {
Ok(event) if event.kind.is_modify() => {
// Reload configuration
config_manager.reload().await?;
}
_ => {}
}
})?;
watcher.watch(config_path, RecursiveMode::NonRecursive)?;Limitations:
- Embedding model changes require a server restart (model weights are loaded at startup)
- Port changes require a restart (socket binding happens at startup)
- Channel token changes take effect immediately
Code Location
src/config/mod.rs— ConfigManager and public APIsrc/config/store.rs— FileStore with atomic writessrc/config/patcher.rs— ConfigPatcher with conflict detectionsrc/config/validation.rs— Patch validation rulessrc/config/presets.rs— Preset override logic
See Also
- Getting Started: Configuration — User-facing configuration guide
- Gateway — Admin API for runtime config updates
- Error Handling — How config errors are propagated