mod agent;
mod builder;
mod channels;
mod database;
pub(crate) mod embeddings;
mod heartbeat;
pub(crate) mod helpers;
mod hygiene;
pub(crate) mod llm;
pub mod relay;
mod routines;
mod safety;
mod sandbox;
mod search;
mod secrets;
mod skills;
mod transcription;
mod tunnel;
mod wasm;
pub(crate) mod workspace;
use std::collections::HashMap;
use std::sync::{LazyLock, Mutex, Once};
use crate::error::ConfigError;
use crate::settings::Settings;
pub use self::agent::AgentConfig;
pub use self::builder::BuilderModeConfig;
pub use self::channels::{
ChannelsConfig, CliConfig, DEFAULT_GATEWAY_PORT, GatewayConfig, HttpConfig, SignalConfig,
};
pub use self::database::{DatabaseBackend, DatabaseConfig, SslMode, default_libsql_path};
pub use self::embeddings::{DEFAULT_EMBEDDING_CACHE_SIZE, EmbeddingsConfig};
pub use self::heartbeat::HeartbeatConfig;
pub use self::hygiene::HygieneConfig;
pub use self::llm::default_session_path;
pub use self::relay::RelayConfig;
pub use self::routines::RoutineConfig;
pub use self::safety::SafetyConfig;
use self::safety::resolve_safety_config;
pub use self::sandbox::{ClaudeCodeConfig, SandboxModeConfig};
pub use self::search::WorkspaceSearchConfig;
pub use self::secrets::SecretsConfig;
pub use self::skills::SkillsConfig;
pub use self::transcription::TranscriptionConfig;
pub use self::tunnel::TunnelConfig;
pub use self::wasm::WasmConfig;
pub use self::workspace::WorkspaceConfig;
pub use crate::llm::config::{
BedrockConfig, CacheRetention, GeminiOauthConfig, LlmConfig, NearAiConfig, OAUTH_PLACEHOLDER,
OpenAiCodexConfig, RegistryProviderConfig,
};
pub use crate::llm::session::SessionConfig;
pub use self::helpers::{env_or_override, set_runtime_env};
static INJECTED_VARS: LazyLock<Mutex<HashMap<String, String>>> =
LazyLock::new(|| Mutex::new(HashMap::new()));
static WARNED_EXPLICIT_DEFAULT_OWNER_ID: Once = Once::new();
#[derive(Debug, Clone)]
pub struct Config {
pub owner_id: String,
pub database: DatabaseConfig,
pub llm: LlmConfig,
pub embeddings: EmbeddingsConfig,
pub tunnel: TunnelConfig,
pub channels: ChannelsConfig,
pub agent: AgentConfig,
pub safety: SafetyConfig,
pub wasm: WasmConfig,
pub secrets: SecretsConfig,
pub builder: BuilderModeConfig,
pub heartbeat: HeartbeatConfig,
pub hygiene: HygieneConfig,
pub routines: RoutineConfig,
pub sandbox: SandboxModeConfig,
pub claude_code: ClaudeCodeConfig,
pub skills: SkillsConfig,
pub transcription: TranscriptionConfig,
pub search: WorkspaceSearchConfig,
pub workspace: WorkspaceConfig,
pub observability: crate::observability::ObservabilityConfig,
pub relay: Option<RelayConfig>,
}
impl Config {
#[cfg(feature = "libsql")]
pub fn for_testing(
libsql_path: std::path::PathBuf,
skills_dir: std::path::PathBuf,
installed_skills_dir: std::path::PathBuf,
) -> Self {
Self {
owner_id: "default".to_string(),
database: DatabaseConfig {
backend: DatabaseBackend::LibSql,
url: secrecy::SecretString::from("unused://test".to_string()),
pool_size: 1,
ssl_mode: SslMode::Disable,
libsql_path: Some(libsql_path),
libsql_url: None,
libsql_auth_token: None,
},
llm: LlmConfig::for_testing(),
embeddings: EmbeddingsConfig::default(),
tunnel: TunnelConfig::default(),
channels: ChannelsConfig {
cli: CliConfig { enabled: false },
http: None,
gateway: None,
signal: None,
wasm_channels_dir: std::env::temp_dir().join("ironclaw-test-channels"),
wasm_channels_enabled: false,
wasm_channel_owner_ids: HashMap::new(),
},
agent: AgentConfig::for_testing(),
safety: SafetyConfig {
max_output_length: 100_000,
injection_check_enabled: false,
},
wasm: WasmConfig {
enabled: false,
..WasmConfig::default()
},
secrets: SecretsConfig::default(),
builder: BuilderModeConfig {
enabled: false,
..BuilderModeConfig::default()
},
heartbeat: HeartbeatConfig::default(),
hygiene: HygieneConfig::default(),
routines: RoutineConfig {
enabled: false,
..RoutineConfig::default()
},
sandbox: SandboxModeConfig {
enabled: false,
..SandboxModeConfig::default()
},
claude_code: ClaudeCodeConfig::default(),
skills: SkillsConfig {
enabled: true,
local_dir: skills_dir,
installed_dir: installed_skills_dir,
..SkillsConfig::default()
},
transcription: TranscriptionConfig::default(),
search: WorkspaceSearchConfig::default(),
workspace: WorkspaceConfig::default(),
observability: crate::observability::ObservabilityConfig::default(),
relay: None,
}
}
pub async fn from_db(
store: &(dyn crate::db::SettingsStore + Sync),
user_id: &str,
) -> Result<Self, ConfigError> {
Self::from_db_with_toml(store, user_id, None).await
}
pub async fn from_db_with_toml(
store: &(dyn crate::db::SettingsStore + Sync),
user_id: &str,
toml_path: Option<&std::path::Path>,
) -> Result<Self, ConfigError> {
let _ = dotenvy::dotenv();
crate::bootstrap::load_ironclaw_env();
let mut db_settings = match store.get_all_settings(user_id).await {
Ok(map) => Settings::from_db_map(&map),
Err(e) => {
tracing::warn!("Failed to load settings from DB, using defaults: {}", e);
Settings::default()
}
};
Self::apply_toml_overlay(&mut db_settings, toml_path)?;
Self::build(&db_settings).await
}
pub async fn from_env() -> Result<Self, ConfigError> {
Self::from_env_with_toml(None).await
}
pub async fn from_env_with_toml(
toml_path: Option<&std::path::Path>,
) -> Result<Self, ConfigError> {
let settings = load_bootstrap_settings(toml_path)?;
Self::build(&settings).await
}
fn apply_toml_overlay(
settings: &mut Settings,
explicit_path: Option<&std::path::Path>,
) -> Result<(), ConfigError> {
let path = explicit_path
.map(std::path::PathBuf::from)
.unwrap_or_else(Settings::default_toml_path);
match Settings::load_toml(&path) {
Ok(Some(toml_settings)) => {
settings.merge_from(&toml_settings);
tracing::debug!("Loaded TOML config from {}", path.display());
}
Ok(None) => {
if explicit_path.is_some() {
return Err(ConfigError::ParseError(format!(
"Config file not found: {}",
path.display()
)));
}
}
Err(e) => {
if explicit_path.is_some() {
return Err(ConfigError::ParseError(format!(
"Failed to load config file {}: {}",
path.display(),
e
)));
}
tracing::warn!("Failed to load default config file: {}", e);
}
}
Ok(())
}
pub async fn re_resolve_llm(
&mut self,
store: Option<&(dyn crate::db::SettingsStore + Sync)>,
user_id: &str,
toml_path: Option<&std::path::Path>,
) -> Result<(), ConfigError> {
let settings = if let Some(store) = store {
let mut s = match store.get_all_settings(user_id).await {
Ok(map) => Settings::from_db_map(&map),
Err(_) => Settings::default(),
};
Self::apply_toml_overlay(&mut s, toml_path)?;
s
} else {
Settings::default()
};
self.llm = LlmConfig::resolve(&settings)?;
Ok(())
}
async fn build(settings: &Settings) -> Result<Self, ConfigError> {
let owner_id = resolve_owner_id(settings)?;
let tunnel = TunnelConfig::resolve(settings)?;
let channels = ChannelsConfig::resolve(settings, &owner_id)?;
let workspace = WorkspaceConfig::resolve(&owner_id)?;
Ok(Self {
owner_id: owner_id.clone(),
database: DatabaseConfig::resolve()?,
llm: LlmConfig::resolve(settings)?,
embeddings: EmbeddingsConfig::resolve(settings)?,
tunnel,
channels,
agent: AgentConfig::resolve(settings)?,
safety: resolve_safety_config(settings)?,
wasm: WasmConfig::resolve(settings)?,
secrets: SecretsConfig::resolve().await?,
builder: BuilderModeConfig::resolve(settings)?,
heartbeat: HeartbeatConfig::resolve(settings)?,
hygiene: HygieneConfig::resolve()?,
routines: RoutineConfig::resolve()?,
sandbox: SandboxModeConfig::resolve(settings)?,
claude_code: ClaudeCodeConfig::resolve(settings)?,
skills: SkillsConfig::resolve()?,
transcription: TranscriptionConfig::resolve(settings)?,
search: WorkspaceSearchConfig::resolve()?,
workspace,
observability: crate::observability::ObservabilityConfig {
backend: std::env::var("OBSERVABILITY_BACKEND").unwrap_or_else(|_| "none".into()),
},
relay: RelayConfig::from_env(),
})
}
}
pub(crate) fn load_bootstrap_settings(
toml_path: Option<&std::path::Path>,
) -> Result<Settings, ConfigError> {
let _ = dotenvy::dotenv();
crate::bootstrap::load_ironclaw_env();
let mut settings = Settings::load();
Config::apply_toml_overlay(&mut settings, toml_path)?;
Ok(settings)
}
pub(crate) fn resolve_owner_id(settings: &Settings) -> Result<String, ConfigError> {
let env_owner_id = self::helpers::optional_env("IRONCLAW_OWNER_ID")?;
let settings_owner_id = settings.owner_id.clone();
let configured_owner_id = env_owner_id.clone().or(settings_owner_id.clone());
let owner_id = configured_owner_id
.map(|value| value.trim().to_string())
.filter(|value| !value.is_empty())
.unwrap_or_else(|| "default".to_string());
if owner_id == "default"
&& (env_owner_id.is_some()
|| settings_owner_id
.as_deref()
.is_some_and(|value| !value.trim().is_empty()))
{
WARNED_EXPLICIT_DEFAULT_OWNER_ID.call_once(|| {
tracing::warn!(
"IRONCLAW_OWNER_ID resolved to the legacy 'default' scope explicitly; durable state will keep legacy owner behavior"
);
});
}
Ok(owner_id)
}
pub async fn inject_llm_keys_from_secrets(
secrets: &dyn crate::secrets::SecretsStore,
user_id: &str,
) {
let mut mappings: Vec<(&str, &str)> = vec![
("llm_nearai_api_key", "NEARAI_API_KEY"),
("llm_anthropic_oauth_token", "ANTHROPIC_OAUTH_TOKEN"),
];
let registry = crate::llm::ProviderRegistry::load();
let dynamic_mappings: Vec<(String, String)> = registry
.selectable()
.iter()
.filter_map(|def| {
def.api_key_env.as_ref().and_then(|env_var| {
def.setup
.as_ref()
.and_then(|s| s.secret_name())
.map(|secret_name| (secret_name.to_string(), env_var.clone()))
})
})
.collect();
for (secret, env_var) in &dynamic_mappings {
mappings.push((secret, env_var));
}
let mut injected = HashMap::new();
for (secret_name, env_var) in mappings {
match std::env::var(env_var) {
Ok(val) if !val.is_empty() => continue,
_ => {}
}
match secrets.get_decrypted(user_id, secret_name).await {
Ok(decrypted) => {
injected.insert(env_var.to_string(), decrypted.expose().to_string());
tracing::debug!("Loaded secret '{}' for env var '{}'", secret_name, env_var);
}
Err(_) => {
}
}
}
inject_os_credential_store_tokens(&mut injected);
merge_injected_vars(injected);
}
pub fn inject_os_credentials() {
let mut injected = HashMap::new();
inject_os_credential_store_tokens(&mut injected);
merge_injected_vars(injected);
}
fn merge_injected_vars(new_entries: HashMap<String, String>) {
if new_entries.is_empty() {
return;
}
match INJECTED_VARS.lock() {
Ok(mut map) => map.extend(new_entries),
Err(poisoned) => poisoned.into_inner().extend(new_entries),
}
}
pub fn inject_single_var(key: &str, value: &str) {
match INJECTED_VARS.lock() {
Ok(mut map) => {
map.insert(key.to_string(), value.to_string());
}
Err(poisoned) => {
poisoned
.into_inner()
.insert(key.to_string(), value.to_string());
}
}
}
fn inject_os_credential_store_tokens(injected: &mut HashMap<String, String>) {
if let Some(fresh) = crate::config::ClaudeCodeConfig::extract_oauth_token() {
injected.insert("ANTHROPIC_OAUTH_TOKEN".to_string(), fresh);
tracing::debug!("Refreshed ANTHROPIC_OAUTH_TOKEN from OS credential store");
}
}