use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::error::{ClawDBError, ClawDBResult};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct ClawDBConfig {
pub data_dir: PathBuf,
pub workspace_id: Uuid,
pub agent_id: Uuid,
pub log_level: String,
pub log_format: String,
pub core: CoreConfig,
pub vector: VectorConfig,
pub branch: BranchConfig,
pub sync: SyncConfig,
pub guard: GuardConfig,
pub reflect: ReflectConfig,
pub server: ServerConfig,
pub plugins: PluginsConfig,
pub telemetry: TelemetryConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct CoreConfig {
pub db_path: PathBuf,
pub max_connections: u32,
pub wal_enabled: bool,
pub cache_size_mb: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct VectorConfig {
pub enabled: bool,
pub db_path: PathBuf,
pub index_dir: PathBuf,
pub embedding_service_url: String,
pub default_dimensions: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct BranchConfig {
pub branches_dir: PathBuf,
pub registry_db_path: PathBuf,
pub max_branches_per_workspace: usize,
pub gc_interval_secs: u64,
pub trunk_branch_name: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct SyncConfig {
pub hub_url: Option<String>,
pub data_dir: PathBuf,
pub db_path: PathBuf,
pub sync_interval_secs: u64,
pub tls_enabled: bool,
pub connect_timeout_secs: u64,
pub request_timeout_secs: u64,
pub max_delta_rows: usize,
pub max_chunk_bytes: usize,
pub max_pull_chunks: u32,
pub max_push_inflight: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct GuardConfig {
pub db_path: String,
pub jwt_secret: String,
pub policy_dir: PathBuf,
pub sensitive_resources: Vec<String>,
pub audit_flush_interval_ms: u64,
pub audit_batch_size: usize,
pub tls_cert_path: PathBuf,
pub tls_key_path: PathBuf,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct ReflectConfig {
#[serde(alias = "service_url")]
pub base_url: Option<String>,
pub api_key: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct ServerConfig {
pub grpc_port: u16,
pub http_port: u16,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct PluginsConfig {
pub plugins_dir: PathBuf,
pub enabled: Vec<String>,
pub sandbox_enabled: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct TelemetryConfig {
pub metrics_port: u16,
pub otel_endpoint: Option<String>,
pub service_name: String,
}
impl ClawDBConfig {
pub fn default_for_dir(data_dir: &Path) -> Self {
let host = host_identity();
let workspace_id = Uuid::new_v5(&Uuid::NAMESPACE_DNS, host.as_bytes());
let agent_seed = format!("{host}:agent");
let agent_id = Uuid::new_v5(&Uuid::NAMESPACE_DNS, agent_seed.as_bytes());
Self {
data_dir: data_dir.to_path_buf(),
workspace_id,
agent_id,
log_level: "info".to_string(),
log_format: "json".to_string(),
core: CoreConfig {
db_path: data_dir.join("claw.db"),
max_connections: 10,
wal_enabled: true,
cache_size_mb: 64,
},
vector: VectorConfig {
enabled: true,
db_path: data_dir.join("claw_vector.db"),
index_dir: data_dir.join("claw_vector_indices"),
embedding_service_url: "http://localhost:50051".to_string(),
default_dimensions: 384,
},
branch: BranchConfig {
branches_dir: data_dir.join("branches"),
registry_db_path: data_dir.join("branches").join("branch_registry.db"),
max_branches_per_workspace: 50,
gc_interval_secs: 3600,
trunk_branch_name: "trunk".to_string(),
},
sync: SyncConfig {
hub_url: None,
data_dir: data_dir.join("sync"),
db_path: data_dir.join("claw.db"),
sync_interval_secs: 30,
tls_enabled: false,
connect_timeout_secs: 10,
request_timeout_secs: 30,
max_delta_rows: 1000,
max_chunk_bytes: 64 * 1024,
max_pull_chunks: 128,
max_push_inflight: 4,
},
guard: GuardConfig {
db_path: data_dir.join("claw_guard.db").display().to_string(),
jwt_secret: "change-me".to_string(),
policy_dir: data_dir.join("policies"),
sensitive_resources: vec!["memory".to_string(), "branch".to_string()],
audit_flush_interval_ms: 100,
audit_batch_size: 500,
tls_cert_path: data_dir.join("certs").join("server.crt"),
tls_key_path: data_dir.join("certs").join("server.key"),
},
reflect: ReflectConfig {
base_url: None,
api_key: None,
},
server: ServerConfig {
grpc_port: 50050,
http_port: 8080,
},
plugins: PluginsConfig {
plugins_dir: data_dir.join("plugins"),
enabled: Vec::new(),
sandbox_enabled: true,
},
telemetry: TelemetryConfig {
metrics_port: 9090,
otel_endpoint: None,
service_name: "clawdb".to_string(),
},
}
}
pub fn from_env() -> ClawDBResult<Self> {
let data_dir = std::env::var("CLAW_DATA_DIR")
.map(PathBuf::from)
.unwrap_or_else(|_| {
dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join(".clawdb")
});
let mut config = Self::default_for_dir(&data_dir);
apply_env_overrides(&mut config)?;
if std::env::var("CLAW_GUARD_JWT_SECRET").is_err() && config.guard.jwt_secret == "change-me"
{
return Err(ClawDBError::Config(
"CLAW_GUARD_JWT_SECRET is required".to_string(),
));
}
Ok(config)
}
pub fn from_file(path: &Path) -> ClawDBResult<Self> {
let raw = std::fs::read_to_string(path)?;
let mut config: Self =
toml::from_str(&raw).map_err(|error| ClawDBError::Config(error.to_string()))?;
if config.data_dir.as_os_str().is_empty() {
config.data_dir = path
.parent()
.unwrap_or_else(|| Path::new("."))
.to_path_buf();
}
apply_env_overrides(&mut config)?;
if config.guard.jwt_secret.trim().is_empty() || config.guard.jwt_secret == "change-me" {
return Err(ClawDBError::Config(
"CLAW_GUARD_JWT_SECRET is required".to_string(),
));
}
Ok(config)
}
pub fn load_or_default(data_dir: &Path) -> ClawDBResult<Self> {
let path = data_dir.join("config.toml");
if path.exists() {
Self::from_file(&path)
} else {
Ok(Self::default_for_dir(data_dir))
}
}
pub fn load(path: &Path) -> ClawDBResult<Self> {
let raw = std::fs::read_to_string(path)?;
toml::from_str(&raw).map_err(|error| ClawDBError::Config(error.to_string()))
}
pub fn save(&self, path: &Path) -> ClawDBResult<()> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
let raw =
toml::to_string_pretty(self).map_err(|error| ClawDBError::Config(error.to_string()))?;
std::fs::write(path, raw)?;
Ok(())
}
}
impl Default for ClawDBConfig {
fn default() -> Self {
Self::default_for_dir(
&dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join(".clawdb"),
)
}
}
impl Default for CoreConfig {
fn default() -> Self {
ClawDBConfig::default().core
}
}
impl Default for VectorConfig {
fn default() -> Self {
ClawDBConfig::default().vector
}
}
impl Default for BranchConfig {
fn default() -> Self {
ClawDBConfig::default().branch
}
}
impl Default for SyncConfig {
fn default() -> Self {
ClawDBConfig::default().sync
}
}
impl Default for GuardConfig {
fn default() -> Self {
ClawDBConfig::default().guard
}
}
impl Default for ReflectConfig {
fn default() -> Self {
ClawDBConfig::default().reflect
}
}
impl Default for ServerConfig {
fn default() -> Self {
ClawDBConfig::default().server
}
}
impl Default for PluginsConfig {
fn default() -> Self {
ClawDBConfig::default().plugins
}
}
impl Default for TelemetryConfig {
fn default() -> Self {
ClawDBConfig::default().telemetry
}
}
fn apply_env_overrides(config: &mut ClawDBConfig) -> ClawDBResult<()> {
if let Ok(value) = std::env::var("CLAW_DATA_DIR") {
config.data_dir = PathBuf::from(value);
}
if let Ok(value) = std::env::var("CLAW_WORKSPACE_ID") {
config.workspace_id = parse_uuid(&value, "CLAW_WORKSPACE_ID")?;
}
if let Ok(value) = std::env::var("CLAW_AGENT_ID") {
config.agent_id = parse_uuid(&value, "CLAW_AGENT_ID")?;
}
if let Ok(value) = std::env::var("CLAW_LOG_LEVEL") {
config.log_level = value;
}
if let Ok(value) = std::env::var("CLAW_LOG_FORMAT") {
config.log_format = value;
}
if let Ok(value) = std::env::var("CLAW_VECTOR_BASE_URL") {
config.vector.embedding_service_url = value;
}
if let Ok(value) = std::env::var("CLAW_VECTOR_ENABLED") {
config.vector.enabled = parse_bool(&value)?;
}
if let Ok(value) = std::env::var("CLAW_SYNC_HUB_URL") {
config.sync.hub_url = Some(value);
}
if let Ok(value) = std::env::var("CLAW_GUARD_JWT_SECRET") {
config.guard.jwt_secret = value;
}
if let Ok(value) = std::env::var("CLAW_GUARD_POLICY_DIR") {
config.guard.policy_dir = PathBuf::from(value);
}
if let Ok(value) = std::env::var("CLAW_REFLECT_BASE_URL") {
config.reflect.base_url = Some(value);
}
if let Ok(value) = std::env::var("CLAW_REFLECT_API_KEY") {
config.reflect.api_key = Some(value);
}
Ok(())
}
fn parse_uuid(value: &str, name: &str) -> ClawDBResult<Uuid> {
Uuid::parse_str(value).map_err(|error| ClawDBError::Config(format!("invalid {name}: {error}")))
}
fn parse_bool(value: &str) -> ClawDBResult<bool> {
match value.trim().to_ascii_lowercase().as_str() {
"1" | "true" | "yes" | "on" => Ok(true),
"0" | "false" | "no" | "off" => Ok(false),
_ => Err(ClawDBError::Config(format!("invalid boolean: {value}"))),
}
}
fn host_identity() -> String {
std::env::var("HOSTNAME")
.or_else(|_| std::env::var("HOST"))
.or_else(|_| std::env::var("USER"))
.unwrap_or_else(|_| "clawdb-local".to_string())
}