use serde::{Deserialize, Serialize};
use std::fs;
use std::path::PathBuf;
use crate::crypto::compression::{CompressionAlgorithm, CompressionLevel};
use crate::error::{Result, VaultError};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VaultConfig {
pub version: String,
pub vault: VaultSettings,
pub crypto: CryptoSettings,
pub compression: CompressionSettings,
pub storage: StorageSettings,
pub security: SecuritySettings,
pub compliance: ComplianceSettings,
#[serde(default)]
pub telemetry: TelemetrySettings,
#[serde(default)]
pub federation: FederationSettings,
#[serde(skip)]
pub dirs: DirectoryPaths,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VaultSettings {
pub default_vault: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CryptoSettings {
pub algorithm: String,
pub kdf: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CompressionSettings {
pub algorithm: String,
pub level: u8,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StorageSettings {
pub max_versions: u32,
pub auto_cleanup: bool,
pub checkpoint_format: String,
#[serde(default = "default_streaming_threshold")]
pub streaming_threshold: u64,
}
fn default_streaming_threshold() -> u64 {
16 * 1024 * 1024
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SecuritySettings {
pub require_passphrase: bool,
pub session_timeout_seconds: u64,
pub audit_log: bool,
#[serde(default)]
pub blockchain_audit: bool,
#[serde(default = "default_blockchain_block_size")]
pub blockchain_block_size: usize,
}
fn default_blockchain_block_size() -> usize {
1
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ComplianceSettings {
pub fips_mode: bool,
pub cve_scanning: bool,
pub audit_retention_days: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FederationSettings {
#[serde(default)]
pub enabled: bool,
#[serde(default)]
pub node_id: String,
#[serde(default)]
pub node_name: String,
#[serde(default = "default_true")]
pub seal_transfers: bool,
#[serde(default)]
pub peers: Vec<FederationPeerSettings>,
}
impl Default for FederationSettings {
fn default() -> Self {
Self {
enabled: false,
node_id: String::new(),
node_name: String::new(),
seal_transfers: true,
peers: Vec::new(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FederationPeerSettings {
pub node_id: String,
#[serde(default)]
pub name: String,
pub endpoint: String,
#[serde(default)]
pub api_key: Option<String>,
#[serde(default = "default_true")]
pub enabled: bool,
}
fn default_true() -> bool {
true
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TelemetrySettings {
pub enabled: bool,
#[serde(default = "default_device_id")]
pub device_id: String,
}
fn default_device_id() -> String {
uuid::Uuid::new_v4().to_string()
}
impl Default for TelemetrySettings {
fn default() -> Self {
Self {
enabled: true,
device_id: default_device_id(),
}
}
}
pub const ENV_HOME: &str = "aimodelvault_HOME";
pub const ENV_CONFIG: &str = "aimodelvault_CONFIG";
pub const ENV_VAULT: &str = "aimodelvault_VAULT";
static INIT_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
fn create_dir_resilient(dir: &std::path::Path) -> Result<()> {
match fs::create_dir_all(dir) {
Ok(()) => Ok(()),
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => Ok(()),
Err(_) if dir.is_dir() => Ok(()),
Err(first) => match fs::create_dir_all(dir) {
Ok(()) => Ok(()),
Err(_) if dir.is_dir() => Ok(()),
Err(_) => Err(VaultError::IoError(first)),
},
}
}
fn non_empty_env(name: &str) -> Option<String> {
std::env::var(name)
.ok()
.map(|v| v.trim().to_string())
.filter(|v| !v.is_empty())
}
#[derive(Debug, Clone, Default)]
pub struct DirectoryPaths {
pub config_dir: PathBuf,
pub data_dir: PathBuf,
pub cache_dir: PathBuf,
pub vault_dir: PathBuf,
pub log_dir: PathBuf,
pub backends_dir: PathBuf,
pub utilities_dir: PathBuf,
pub databases_dir: PathBuf,
}
impl VaultConfig {
pub fn new() -> Result<Self> {
let dirs = Self::get_project_dirs()?;
Self::ensure_directories(&dirs)?;
let config_file = dirs.config_dir.join("config.yaml");
let mut config = if config_file.exists() {
Self::load_from_file(&config_file, dirs)?
} else {
let config = Self::default_with_dirs(dirs);
config.save()?;
config
};
if let Some(name) = non_empty_env(ENV_VAULT) {
config.vault.default_vault = name;
}
Ok(config)
}
pub fn with_dirs(dirs: DirectoryPaths) -> Result<Self> {
Self::ensure_directories(&dirs)?;
Ok(Self::default_with_dirs(dirs))
}
fn get_project_dirs() -> Result<DirectoryPaths> {
let mut dirs = Self::platform_dirs()?;
if let Some(config_root) = non_empty_env(ENV_CONFIG) {
let config_dir = PathBuf::from(config_root);
dirs.backends_dir = config_dir.join("backends");
dirs.utilities_dir = config_dir.join("utilities");
dirs.databases_dir = config_dir.join("databases");
dirs.config_dir = config_dir;
}
Ok(dirs)
}
fn platform_dirs() -> Result<DirectoryPaths> {
use directories::BaseDirs;
if let Some(root) = non_empty_env(ENV_HOME) {
let root = PathBuf::from(root);
let config_dir = root.join("config");
let data_dir = root.join("data");
return Ok(DirectoryPaths {
cache_dir: root.join("cache"),
vault_dir: data_dir.join("vaults"),
log_dir: data_dir.join("logs"),
backends_dir: config_dir.join("backends"),
utilities_dir: config_dir.join("utilities"),
databases_dir: config_dir.join("databases"),
config_dir,
data_dir,
});
}
let base_dirs = BaseDirs::new().ok_or_else(|| {
VaultError::ConfigError("Failed to determine base directories".to_string())
})?;
let config_base = base_dirs.config_dir().join("ai");
let data_base = base_dirs.data_dir().join("ai");
let cache_base = base_dirs.cache_dir().join("ai");
let config_dir = config_base.join("models");
let data_dir = data_base.join("models");
let cache_dir = cache_base.join("models");
let vault_dir = data_dir.join("vaults");
let log_dir = data_dir.join("logs");
let backends_dir = config_base.join("backends");
let utilities_dir = config_base.join("utilities");
let databases_dir = config_base.join("databases");
Ok(DirectoryPaths {
config_dir,
data_dir,
cache_dir,
vault_dir,
log_dir,
backends_dir,
utilities_dir,
databases_dir,
})
}
fn ensure_directories(dirs: &DirectoryPaths) -> Result<()> {
let all = [
&dirs.config_dir,
&dirs.data_dir,
&dirs.cache_dir,
&dirs.vault_dir,
&dirs.log_dir,
&dirs.backends_dir,
&dirs.utilities_dir,
&dirs.databases_dir,
];
let _guard = INIT_LOCK.lock().unwrap_or_else(|e| e.into_inner());
for dir in all {
if !dir.is_dir() {
create_dir_resilient(dir)?;
crate::permissions::restrict_dir(dir)?;
}
}
Ok(())
}
fn load_from_file(path: &PathBuf, dirs: DirectoryPaths) -> Result<Self> {
let contents = fs::read_to_string(path)?;
let mut config: VaultConfig = serde_yaml_ng::from_str(&contents)?;
config.dirs = dirs;
Ok(config)
}
fn default_with_dirs(dirs: DirectoryPaths) -> Self {
Self {
version: "1.0".to_string(),
vault: VaultSettings {
default_vault: "default".to_string(),
},
crypto: CryptoSettings {
algorithm: "aes-256-gcm".to_string(),
kdf: "argon2id".to_string(),
},
compression: CompressionSettings {
algorithm: "gzip".to_string(),
level: 6,
},
storage: StorageSettings {
max_versions: 10,
auto_cleanup: true,
checkpoint_format: "v{version}_{timestamp}".to_string(),
streaming_threshold: default_streaming_threshold(),
},
security: SecuritySettings {
require_passphrase: true,
session_timeout_seconds: 3600,
audit_log: true,
blockchain_audit: false,
blockchain_block_size: default_blockchain_block_size(),
},
compliance: ComplianceSettings {
fips_mode: true,
cve_scanning: true,
audit_retention_days: 90,
},
telemetry: TelemetrySettings::default(),
federation: FederationSettings::default(),
dirs,
}
}
pub fn save(&self) -> Result<()> {
let config_file = self.dirs.config_dir.join("config.yaml");
let contents = serde_yaml_ng::to_string(self)?;
fs::write(&config_file, contents)?;
crate::permissions::restrict_file(&config_file)?;
Ok(())
}
pub fn get_vault_path(&self, vault_name: Option<&str>) -> PathBuf {
let name = vault_name.unwrap_or(&self.vault.default_vault);
self.dirs.vault_dir.join(name)
}
pub fn get_audit_log_path(&self) -> PathBuf {
self.dirs.log_dir.join("audit.log")
}
pub fn get_audit_chain_dir(&self) -> PathBuf {
self.dirs.log_dir.join("chain")
}
pub fn get_compression_algorithm(&self) -> CompressionAlgorithm {
match self.compression.algorithm.as_str() {
"gzip" => CompressionAlgorithm::Gzip,
"lzma" => CompressionAlgorithm::Lzma,
"none" => CompressionAlgorithm::None,
_ => CompressionAlgorithm::Gzip,
}
}
pub fn get_compression_level(&self) -> CompressionLevel {
match self.compression.level {
0 => CompressionLevel::None,
1 => CompressionLevel::Fast,
9 => CompressionLevel::Maximum,
_ => CompressionLevel::Balanced,
}
}
}
impl Default for VaultConfig {
fn default() -> Self {
Self::new().expect("Failed to create default configuration: home directory unavailable")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_telemetry_settings_default() {
let ts = TelemetrySettings::default();
assert!(ts.enabled);
assert!(!ts.device_id.is_empty());
}
#[test]
fn test_telemetry_is_opt_in_by_default() {
let effective = crate::telemetry::TelemetryConfig::default();
assert!(
!effective.enabled,
"telemetry must be off by default — the README promises opt-in"
);
}
#[test]
fn test_vault_config_with_dirs() {
let temp = tempfile::tempdir().unwrap();
let dirs = DirectoryPaths {
config_dir: temp.path().join("config"),
data_dir: temp.path().join("data"),
cache_dir: temp.path().join("cache"),
vault_dir: temp.path().join("vaults"),
log_dir: temp.path().join("logs"),
backends_dir: temp.path().join("backends"),
utilities_dir: temp.path().join("utils"),
databases_dir: temp.path().join("dbs"),
};
let config = VaultConfig::with_dirs(dirs).unwrap();
assert!(config.dirs.config_dir.starts_with(temp.path()));
}
#[test]
fn test_vault_config_new() {
let config = VaultConfig::new().unwrap();
assert!(!config.dirs.config_dir.as_os_str().is_empty());
}
#[test]
fn test_compression_level_settings() {
let mut config = VaultConfig::new().unwrap();
config.compression.level = 0;
assert!(matches!(
config.get_compression_level(),
CompressionLevel::None
));
config.compression.level = 1;
assert!(matches!(
config.get_compression_level(),
CompressionLevel::Fast
));
config.compression.level = 9;
assert!(matches!(
config.get_compression_level(),
CompressionLevel::Maximum
));
config.compression.level = 5;
assert!(matches!(
config.get_compression_level(),
CompressionLevel::Balanced
));
}
#[test]
fn test_compression_algorithm_variants() {
let mut config = VaultConfig::new().unwrap();
config.compression.algorithm = "gzip".to_string();
assert!(matches!(
config.get_compression_algorithm(),
CompressionAlgorithm::Gzip
));
config.compression.algorithm = "lzma".to_string();
assert!(matches!(
config.get_compression_algorithm(),
CompressionAlgorithm::Lzma
));
config.compression.algorithm = "none".to_string();
assert!(matches!(
config.get_compression_algorithm(),
CompressionAlgorithm::None
));
config.compression.algorithm = "unknown_algo".to_string();
assert!(matches!(
config.get_compression_algorithm(),
CompressionAlgorithm::Gzip
));
}
#[test]
fn test_vault_config_save_and_reload() {
let temp = tempfile::tempdir().unwrap();
let dirs = DirectoryPaths {
config_dir: temp.path().join("config"),
data_dir: temp.path().join("data"),
cache_dir: temp.path().join("cache"),
vault_dir: temp.path().join("vaults"),
log_dir: temp.path().join("logs"),
backends_dir: temp.path().join("backends"),
utilities_dir: temp.path().join("utils"),
databases_dir: temp.path().join("dbs"),
};
let config = VaultConfig::with_dirs(dirs.clone()).unwrap();
config.save().unwrap();
let config_file = dirs.config_dir.join("config.yaml");
assert!(config_file.exists());
let reloaded = VaultConfig::load_from_file(&config_file, dirs).unwrap();
assert_eq!(reloaded.vault.default_vault, "default");
assert_eq!(reloaded.crypto.algorithm, "aes-256-gcm");
}
#[test]
fn test_vault_path_and_audit_log_path() {
let config = VaultConfig::new().unwrap();
let vault_path = config.get_vault_path(None);
assert!(vault_path.ends_with("default"));
let custom_path = config.get_vault_path(Some("my-vault"));
assert!(custom_path.ends_with("my-vault"));
let audit_path = config.get_audit_log_path();
assert!(audit_path.ends_with("audit.log"));
}
#[test]
fn test_vault_config_default_impl() {
let config = VaultConfig::default();
assert_eq!(config.vault.default_vault, "default");
assert_eq!(config.crypto.algorithm, "aes-256-gcm");
}
}