mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-08-25 14:53:34 +00:00
feat: add IRONCLAW_BASE_DIR env var with LazyLock caching (#397)
This commit is contained in:
+181
-7
@@ -7,13 +7,75 @@
|
||||
//! File: `~/.ironclaw/.env` (standard dotenvy format)
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
const IRONCLAW_BASE_DIR_ENV: &str = "IRONCLAW_BASE_DIR";
|
||||
|
||||
/// Lazily computed IronClaw base directory, cached for the lifetime of the process.
|
||||
static IRONCLAW_BASE_DIR: LazyLock<PathBuf> = LazyLock::new(compute_ironclaw_base_dir);
|
||||
|
||||
/// Compute the IronClaw base directory from environment.
|
||||
///
|
||||
/// This is the underlying implementation used by both the public
|
||||
/// `ironclaw_base_dir()` function (which caches the result) and tests
|
||||
/// (which need to verify different configurations).
|
||||
pub fn compute_ironclaw_base_dir() -> PathBuf {
|
||||
std::env::var(IRONCLAW_BASE_DIR_ENV)
|
||||
.map(PathBuf::from)
|
||||
.map(|path| {
|
||||
if path.as_os_str().is_empty() {
|
||||
default_base_dir()
|
||||
} else if !path.is_absolute() {
|
||||
eprintln!(
|
||||
"Warning: IRONCLAW_BASE_DIR is a relative path '{}', resolved against current directory",
|
||||
path.display()
|
||||
);
|
||||
path
|
||||
} else {
|
||||
path
|
||||
}
|
||||
})
|
||||
.unwrap_or_else(|_| default_base_dir())
|
||||
}
|
||||
|
||||
/// Get the default IronClaw base directory (~/.ironclaw).
|
||||
///
|
||||
/// Logs a warning if the home directory cannot be determined and falls back to
|
||||
/// the current directory.
|
||||
fn default_base_dir() -> PathBuf {
|
||||
if let Some(home) = dirs::home_dir() {
|
||||
home.join(".ironclaw")
|
||||
} else {
|
||||
eprintln!("Warning: Could not determine home directory, using current directory");
|
||||
std::env::current_dir()
|
||||
.unwrap_or_else(|_| PathBuf::from("/tmp"))
|
||||
.join(".ironclaw")
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the IronClaw base directory.
|
||||
///
|
||||
/// Override with `IRONCLAW_BASE_DIR` environment variable.
|
||||
/// Defaults to `~/.ironclaw` (or `./.ironclaw` if home directory cannot be determined).
|
||||
///
|
||||
/// Thread-safe: the value is computed once and cached in a `LazyLock`.
|
||||
///
|
||||
/// # Environment Variable Behavior
|
||||
/// - If `IRONCLAW_BASE_DIR` is set to a non-empty path, that path is used.
|
||||
/// - If `IRONCLAW_BASE_DIR` is set to an empty string, it is treated as unset.
|
||||
/// - If `IRONCLAW_BASE_DIR` contains null bytes, a warning is printed and the default is used.
|
||||
/// - If the home directory cannot be determined, a warning is printed and the current directory is used.
|
||||
///
|
||||
/// # Returns
|
||||
/// A `PathBuf` pointing to the base directory. The path is not validated
|
||||
/// for existence.
|
||||
pub fn ironclaw_base_dir() -> PathBuf {
|
||||
IRONCLAW_BASE_DIR.clone()
|
||||
}
|
||||
|
||||
/// Path to the IronClaw-specific `.env` file: `~/.ironclaw/.env`.
|
||||
pub fn ironclaw_env_path() -> PathBuf {
|
||||
dirs::home_dir()
|
||||
.unwrap_or_else(|| PathBuf::from("."))
|
||||
.join(".ironclaw")
|
||||
.join(".env")
|
||||
ironclaw_base_dir().join(".env")
|
||||
}
|
||||
|
||||
/// Load env vars from `~/.ironclaw/.env` (in addition to the standard `.env`).
|
||||
@@ -200,9 +262,7 @@ pub async fn migrate_disk_to_db(
|
||||
store: &dyn crate::db::Database,
|
||||
user_id: &str,
|
||||
) -> Result<(), MigrationError> {
|
||||
let ironclaw_dir = dirs::home_dir()
|
||||
.unwrap_or_else(|| PathBuf::from("."))
|
||||
.join(".ironclaw");
|
||||
let ironclaw_dir = ironclaw_base_dir();
|
||||
let legacy_settings_path = ironclaw_dir.join("settings.json");
|
||||
|
||||
if !legacy_settings_path.exists() {
|
||||
@@ -336,8 +396,11 @@ pub enum MigrationError {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::sync::Mutex;
|
||||
use tempfile::tempdir;
|
||||
|
||||
static ENV_MUTEX: Mutex<()> = Mutex::new(());
|
||||
|
||||
#[test]
|
||||
fn test_save_and_load_database_url() {
|
||||
let dir = tempdir().unwrap();
|
||||
@@ -727,4 +790,115 @@ INJECTED="pwned"#;
|
||||
assert_eq!(&found.unwrap().1, value, "{key} value mismatch");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ironclaw_base_dir_default() {
|
||||
// This test must run first (or in isolation) before the LazyLock is initialized.
|
||||
// It verifies that when IRONCLAW_BASE_DIR is not set, the default path is used.
|
||||
let _guard = ENV_MUTEX.lock().unwrap();
|
||||
let old_val = std::env::var("IRONCLAW_BASE_DIR").ok();
|
||||
// SAFETY: ENV_MUTEX ensures single-threaded access to env vars in tests
|
||||
unsafe { std::env::remove_var("IRONCLAW_BASE_DIR") };
|
||||
|
||||
// Force re-evaluation by calling the computation function directly
|
||||
let path = compute_ironclaw_base_dir();
|
||||
let home = dirs::home_dir().unwrap_or_else(|| std::path::PathBuf::from("."));
|
||||
assert_eq!(path, home.join(".ironclaw"));
|
||||
|
||||
if let Some(val) = old_val {
|
||||
// SAFETY: ENV_MUTEX ensures single-threaded access to env vars in tests
|
||||
unsafe { std::env::set_var("IRONCLAW_BASE_DIR", val) };
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ironclaw_base_dir_env_override() {
|
||||
// This test verifies that when IRONCLAW_BASE_DIR is set,
|
||||
// the custom path is used. Must run before LazyLock is initialized.
|
||||
let _guard = ENV_MUTEX.lock().unwrap();
|
||||
let old_val = std::env::var("IRONCLAW_BASE_DIR").ok();
|
||||
// SAFETY: ENV_MUTEX ensures single-threaded access to env vars in tests
|
||||
unsafe { std::env::set_var("IRONCLAW_BASE_DIR", "/custom/ironclaw/path") };
|
||||
|
||||
// Force re-evaluation by calling the computation function directly
|
||||
let path = compute_ironclaw_base_dir();
|
||||
assert_eq!(path, std::path::PathBuf::from("/custom/ironclaw/path"));
|
||||
|
||||
if let Some(val) = old_val {
|
||||
// SAFETY: ENV_MUTEX ensures single-threaded access to env vars in tests
|
||||
unsafe { std::env::set_var("IRONCLAW_BASE_DIR", val) };
|
||||
} else {
|
||||
// SAFETY: ENV_MUTEX ensures single-threaded access to env vars in tests
|
||||
unsafe { std::env::remove_var("IRONCLAW_BASE_DIR") };
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_compute_base_dir_env_path_join() {
|
||||
// Verifies that ironclaw_env_path correctly joins .env to the base dir.
|
||||
// Uses compute_ironclaw_base_dir directly to avoid LazyLock caching.
|
||||
let _guard = ENV_MUTEX.lock().unwrap();
|
||||
let old_val = std::env::var("IRONCLAW_BASE_DIR").ok();
|
||||
// SAFETY: ENV_MUTEX ensures single-threaded access to env vars in tests
|
||||
unsafe { std::env::set_var("IRONCLAW_BASE_DIR", "/my/custom/dir") };
|
||||
|
||||
// Test the path construction logic directly
|
||||
let base_path = compute_ironclaw_base_dir();
|
||||
let env_path = base_path.join(".env");
|
||||
assert_eq!(env_path, std::path::PathBuf::from("/my/custom/dir/.env"));
|
||||
|
||||
if let Some(val) = old_val {
|
||||
// SAFETY: ENV_MUTEX ensures single-threaded access to env vars in tests
|
||||
unsafe { std::env::set_var("IRONCLAW_BASE_DIR", val) };
|
||||
} else {
|
||||
// SAFETY: ENV_MUTEX ensures single-threaded access to env vars in tests
|
||||
unsafe { std::env::remove_var("IRONCLAW_BASE_DIR") };
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ironclaw_base_dir_empty_env() {
|
||||
// Verifies that empty IRONCLAW_BASE_DIR falls back to default.
|
||||
let _guard = ENV_MUTEX.lock().unwrap();
|
||||
let old_val = std::env::var("IRONCLAW_BASE_DIR").ok();
|
||||
// SAFETY: ENV_MUTEX ensures single-threaded access to env vars in tests
|
||||
unsafe { std::env::set_var("IRONCLAW_BASE_DIR", "") };
|
||||
|
||||
// Force re-evaluation by calling the computation function directly
|
||||
let path = compute_ironclaw_base_dir();
|
||||
let home = dirs::home_dir().unwrap_or_else(|| std::path::PathBuf::from("."));
|
||||
assert_eq!(path, home.join(".ironclaw"));
|
||||
|
||||
if let Some(val) = old_val {
|
||||
// SAFETY: ENV_MUTEX ensures single-threaded access to env vars in tests
|
||||
unsafe { std::env::set_var("IRONCLAW_BASE_DIR", val) };
|
||||
} else {
|
||||
// SAFETY: ENV_MUTEX ensures single-threaded access to env vars in tests
|
||||
unsafe { std::env::remove_var("IRONCLAW_BASE_DIR") };
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ironclaw_base_dir_special_chars() {
|
||||
// Verifies that paths with special characters are handled correctly.
|
||||
let _guard = ENV_MUTEX.lock().unwrap();
|
||||
let old_val = std::env::var("IRONCLAW_BASE_DIR").ok();
|
||||
// SAFETY: ENV_MUTEX ensures single-threaded access to env vars in tests
|
||||
unsafe { std::env::set_var("IRONCLAW_BASE_DIR", "/tmp/test_with-special.chars") };
|
||||
|
||||
// Force re-evaluation by calling the computation function directly
|
||||
let path = compute_ironclaw_base_dir();
|
||||
assert_eq!(
|
||||
path,
|
||||
std::path::PathBuf::from("/tmp/test_with-special.chars")
|
||||
);
|
||||
|
||||
if let Some(val) = old_val {
|
||||
// SAFETY: ENV_MUTEX ensures single-threaded access to env vars in tests
|
||||
unsafe { std::env::set_var("IRONCLAW_BASE_DIR", val) };
|
||||
} else {
|
||||
// SAFETY: ENV_MUTEX ensures single-threaded access to env vars in tests
|
||||
unsafe { std::env::remove_var("IRONCLAW_BASE_DIR") };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ use tokio::sync::mpsc;
|
||||
use tokio_stream::wrappers::ReceiverStream;
|
||||
|
||||
use crate::agent::truncate_for_preview;
|
||||
use crate::bootstrap::ironclaw_base_dir;
|
||||
use crate::channels::{Channel, IncomingMessage, MessageStream, OutgoingResponse, StatusUpdate};
|
||||
use crate::error::ChannelError;
|
||||
|
||||
@@ -279,10 +280,7 @@ fn print_help() {
|
||||
|
||||
/// Get the history file path (~/.ironclaw/history).
|
||||
fn history_path() -> std::path::PathBuf {
|
||||
dirs::home_dir()
|
||||
.unwrap_or_else(|| std::path::PathBuf::from("."))
|
||||
.join(".ironclaw")
|
||||
.join("history")
|
||||
ironclaw_base_dir().join("history")
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
|
||||
@@ -17,6 +17,7 @@ use serde::Deserialize;
|
||||
use tokio::sync::RwLock;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::bootstrap::ironclaw_base_dir;
|
||||
use crate::channels::{Channel, IncomingMessage, MessageStream, OutgoingResponse, StatusUpdate};
|
||||
use crate::config::SignalConfig;
|
||||
use crate::error::ChannelError;
|
||||
@@ -557,9 +558,7 @@ impl SignalChannel {
|
||||
/// - All paths are within ~/.ironclaw/ sandbox
|
||||
fn validate_attachment_paths(paths: &[String]) -> Result<(), ChannelError> {
|
||||
// Get the sandbox base directory (same as MessageTool uses)
|
||||
let base_dir = dirs::home_dir()
|
||||
.unwrap_or_else(|| std::path::PathBuf::from("."))
|
||||
.join(".ironclaw");
|
||||
let base_dir = ironclaw_base_dir();
|
||||
|
||||
for path in paths {
|
||||
crate::tools::builtin::path_utils::validate_path(path, Some(&base_dir)).map_err(
|
||||
@@ -2671,9 +2670,7 @@ mod tests {
|
||||
use std::fs;
|
||||
|
||||
// Create test files in sandbox
|
||||
let base_dir = dirs::home_dir()
|
||||
.unwrap_or_else(|| std::path::PathBuf::from("."))
|
||||
.join(".ironclaw");
|
||||
let base_dir = crate::bootstrap::ironclaw_base_dir();
|
||||
|
||||
// Create sandbox directory if it doesn't exist (needed for CI)
|
||||
let _ = fs::create_dir_all(&base_dir);
|
||||
|
||||
@@ -11,6 +11,7 @@ use std::sync::Arc;
|
||||
|
||||
use tokio::fs;
|
||||
|
||||
use crate::bootstrap::ironclaw_base_dir;
|
||||
use crate::channels::wasm::capabilities::ChannelCapabilities;
|
||||
use crate::channels::wasm::error::WasmChannelError;
|
||||
use crate::channels::wasm::runtime::WasmChannelRuntime;
|
||||
@@ -356,10 +357,7 @@ pub struct DiscoveredChannel {
|
||||
/// Returns ~/.ironclaw/channels/
|
||||
#[allow(dead_code)]
|
||||
pub fn default_channels_dir() -> PathBuf {
|
||||
dirs::home_dir()
|
||||
.unwrap_or_else(|| PathBuf::from("."))
|
||||
.join(".ironclaw")
|
||||
.join("channels")
|
||||
ironclaw_base_dir().join("channels")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -6,6 +6,7 @@ use axum::{
|
||||
response::{Html, IntoResponse},
|
||||
};
|
||||
|
||||
use crate::bootstrap::ironclaw_base_dir;
|
||||
use crate::channels::web::types::*;
|
||||
|
||||
// --- Static file handlers ---
|
||||
@@ -71,11 +72,7 @@ async fn serve_project_file(project_id: &str, path: &str) -> axum::response::Res
|
||||
return (StatusCode::BAD_REQUEST, "Invalid project ID").into_response();
|
||||
}
|
||||
|
||||
let base = dirs::home_dir()
|
||||
.unwrap_or_else(|| std::path::PathBuf::from("."))
|
||||
.join(".ironclaw")
|
||||
.join("projects")
|
||||
.join(project_id);
|
||||
let base = ironclaw_base_dir().join("projects").join(project_id);
|
||||
|
||||
let file_path = base.join(path);
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ use tower_http::set_header::SetResponseHeaderLayer;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::agent::SessionManager;
|
||||
use crate::bootstrap::ironclaw_base_dir;
|
||||
use crate::channels::IncomingMessage;
|
||||
use crate::channels::web::auth::{AuthState, auth_middleware};
|
||||
use crate::channels::web::handlers::skills::{
|
||||
@@ -1921,11 +1922,7 @@ async fn serve_project_file(project_id: &str, path: &str) -> axum::response::Res
|
||||
return (StatusCode::BAD_REQUEST, "Invalid project ID").into_response();
|
||||
}
|
||||
|
||||
let base = dirs::home_dir()
|
||||
.unwrap_or_else(|| std::path::PathBuf::from("."))
|
||||
.join(".ironclaw")
|
||||
.join("projects")
|
||||
.join(project_id);
|
||||
let base = ironclaw_base_dir().join("projects").join(project_id);
|
||||
|
||||
let file_path = base.join(path);
|
||||
|
||||
|
||||
+3
-3
@@ -6,6 +6,8 @@
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::bootstrap::ironclaw_base_dir;
|
||||
|
||||
/// Run all diagnostic checks and print results.
|
||||
pub async fn run_doctor_command() -> anyhow::Result<()> {
|
||||
println!("IronClaw Doctor");
|
||||
@@ -195,9 +197,7 @@ async fn try_pg_connect() -> Result<(), String> {
|
||||
}
|
||||
|
||||
fn check_workspace_dir() -> CheckResult {
|
||||
let dir = dirs::home_dir()
|
||||
.unwrap_or_else(|| PathBuf::from("."))
|
||||
.join(".ironclaw");
|
||||
let dir = ironclaw_base_dir();
|
||||
|
||||
if dir.exists() {
|
||||
if dir.is_dir() {
|
||||
|
||||
+3
-8
@@ -5,6 +5,7 @@
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::bootstrap::ironclaw_base_dir;
|
||||
use crate::settings::Settings;
|
||||
|
||||
/// Run the status command, printing system health info.
|
||||
@@ -206,15 +207,9 @@ fn count_wasm_files(dir: &std::path::Path) -> usize {
|
||||
}
|
||||
|
||||
fn default_tools_dir() -> PathBuf {
|
||||
dirs::home_dir()
|
||||
.unwrap_or_else(|| PathBuf::from("."))
|
||||
.join(".ironclaw")
|
||||
.join("tools")
|
||||
ironclaw_base_dir().join("tools")
|
||||
}
|
||||
|
||||
fn default_channels_dir() -> PathBuf {
|
||||
dirs::home_dir()
|
||||
.unwrap_or_else(|| PathBuf::from("."))
|
||||
.join(".ironclaw")
|
||||
.join("channels")
|
||||
ironclaw_base_dir().join("channels")
|
||||
}
|
||||
|
||||
+2
-3
@@ -9,6 +9,7 @@ use std::sync::Arc;
|
||||
use clap::Subcommand;
|
||||
use tokio::fs;
|
||||
|
||||
use crate::bootstrap::ironclaw_base_dir;
|
||||
use crate::config::Config;
|
||||
#[allow(unused_imports)]
|
||||
use crate::db::Database;
|
||||
@@ -19,9 +20,7 @@ use crate::tools::wasm::{CapabilitiesFile, compute_binary_hash};
|
||||
|
||||
/// Default tools directory.
|
||||
fn default_tools_dir() -> PathBuf {
|
||||
dirs::home_dir()
|
||||
.map(|h| h.join(".ironclaw").join("tools"))
|
||||
.unwrap_or_else(|| PathBuf::from(".ironclaw/tools"))
|
||||
ironclaw_base_dir().join("tools")
|
||||
}
|
||||
|
||||
#[derive(Subcommand, Debug, Clone)]
|
||||
|
||||
@@ -2,6 +2,7 @@ use std::path::PathBuf;
|
||||
|
||||
use secrecy::SecretString;
|
||||
|
||||
use crate::bootstrap::ironclaw_base_dir;
|
||||
use crate::config::helpers::{optional_env, parse_bool_env, parse_optional_env};
|
||||
use crate::error::ConfigError;
|
||||
use crate::settings::Settings;
|
||||
@@ -193,8 +194,5 @@ impl ChannelsConfig {
|
||||
|
||||
/// Get the default channels directory (~/.ironclaw/channels/).
|
||||
fn default_channels_dir() -> PathBuf {
|
||||
dirs::home_dir()
|
||||
.unwrap_or_else(|| PathBuf::from("."))
|
||||
.join(".ironclaw")
|
||||
.join("channels")
|
||||
ironclaw_base_dir().join("channels")
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ use std::path::PathBuf;
|
||||
|
||||
use secrecy::{ExposeSecret, SecretString};
|
||||
|
||||
use crate::bootstrap::ironclaw_base_dir;
|
||||
use crate::config::helpers::{optional_env, parse_optional_env};
|
||||
use crate::error::ConfigError;
|
||||
|
||||
@@ -123,8 +124,5 @@ impl DatabaseConfig {
|
||||
|
||||
/// Default libSQL database path (~/.ironclaw/ironclaw.db).
|
||||
pub fn default_libsql_path() -> PathBuf {
|
||||
dirs::home_dir()
|
||||
.unwrap_or_else(|| PathBuf::from("."))
|
||||
.join(".ironclaw")
|
||||
.join("ironclaw.db")
|
||||
ironclaw_base_dir().join("ironclaw.db")
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::bootstrap::ironclaw_base_dir;
|
||||
use crate::config::helpers::{parse_bool_env, parse_optional_env};
|
||||
use crate::error::ConfigError;
|
||||
|
||||
@@ -41,9 +42,7 @@ impl HygieneConfig {
|
||||
enabled: self.enabled,
|
||||
retention_days: self.retention_days,
|
||||
cadence_hours: self.cadence_hours,
|
||||
state_dir: dirs::home_dir()
|
||||
.unwrap_or_else(|| std::path::PathBuf::from("."))
|
||||
.join(".ironclaw"),
|
||||
state_dir: ironclaw_base_dir(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-4
@@ -2,6 +2,7 @@ use std::path::PathBuf;
|
||||
|
||||
use secrecy::SecretString;
|
||||
|
||||
use crate::bootstrap::ironclaw_base_dir;
|
||||
use crate::config::helpers::{optional_env, parse_optional_env};
|
||||
use crate::error::ConfigError;
|
||||
use crate::settings::Settings;
|
||||
@@ -373,10 +374,7 @@ fn parse_extra_headers(val: &str) -> Result<Vec<(String, String)>, ConfigError>
|
||||
|
||||
/// Get the default session file path (~/.ironclaw/session.json).
|
||||
fn default_session_path() -> PathBuf {
|
||||
dirs::home_dir()
|
||||
.unwrap_or_else(|| PathBuf::from("."))
|
||||
.join(".ironclaw")
|
||||
.join("session.json")
|
||||
ironclaw_base_dir().join("session.json")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::bootstrap::ironclaw_base_dir;
|
||||
use crate::config::helpers::{optional_env, parse_bool_env, parse_optional_env};
|
||||
use crate::error::ConfigError;
|
||||
|
||||
@@ -34,18 +35,12 @@ impl Default for SkillsConfig {
|
||||
|
||||
/// Get the default user skills directory (~/.ironclaw/skills/).
|
||||
fn default_skills_dir() -> PathBuf {
|
||||
dirs::home_dir()
|
||||
.unwrap_or_else(|| PathBuf::from("."))
|
||||
.join(".ironclaw")
|
||||
.join("skills")
|
||||
ironclaw_base_dir().join("skills")
|
||||
}
|
||||
|
||||
/// Get the default installed skills directory (~/.ironclaw/installed_skills/).
|
||||
fn default_installed_skills_dir() -> PathBuf {
|
||||
dirs::home_dir()
|
||||
.unwrap_or_else(|| PathBuf::from("."))
|
||||
.join(".ironclaw")
|
||||
.join("installed_skills")
|
||||
ironclaw_base_dir().join("installed_skills")
|
||||
}
|
||||
|
||||
impl SkillsConfig {
|
||||
|
||||
+2
-4
@@ -1,6 +1,7 @@
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::bootstrap::ironclaw_base_dir;
|
||||
use crate::config::helpers::{optional_env, parse_bool_env, parse_optional_env};
|
||||
use crate::error::ConfigError;
|
||||
|
||||
@@ -39,10 +40,7 @@ impl Default for WasmConfig {
|
||||
|
||||
/// Get the default tools directory (~/.ironclaw/tools/).
|
||||
fn default_tools_dir() -> PathBuf {
|
||||
dirs::home_dir()
|
||||
.unwrap_or_else(|| PathBuf::from("."))
|
||||
.join(".ironclaw")
|
||||
.join("tools")
|
||||
ironclaw_base_dir().join("tools")
|
||||
}
|
||||
|
||||
impl WasmConfig {
|
||||
|
||||
+2
-4
@@ -7,6 +7,7 @@
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::bootstrap::ironclaw_base_dir;
|
||||
use crate::cli::oauth_defaults::OAUTH_CALLBACK_PORT;
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
@@ -46,10 +47,7 @@ impl Default for SessionConfig {
|
||||
|
||||
/// Get the default session file path (~/.ironclaw/session.json).
|
||||
pub fn default_session_path() -> PathBuf {
|
||||
dirs::home_dir()
|
||||
.unwrap_or_else(|| PathBuf::from("."))
|
||||
.join(".ironclaw")
|
||||
.join("session.json")
|
||||
ironclaw_base_dir().join("session.json")
|
||||
}
|
||||
|
||||
/// Manages NEAR AI session tokens with persistence and automatic renewal.
|
||||
|
||||
@@ -11,6 +11,7 @@ use chrono::{DateTime, Utc};
|
||||
use tokio::sync::RwLock;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::bootstrap::ironclaw_base_dir;
|
||||
use crate::error::OrchestratorError;
|
||||
use crate::orchestrator::auth::{CredentialGrant, TokenStore};
|
||||
use crate::sandbox::connect_docker;
|
||||
@@ -158,11 +159,14 @@ fn validate_bind_mount_path(
|
||||
),
|
||||
})?;
|
||||
|
||||
let home = dirs::home_dir().ok_or_else(|| OrchestratorError::ContainerCreationFailed {
|
||||
job_id,
|
||||
reason: "could not determine home directory for path validation".to_string(),
|
||||
})?;
|
||||
let projects_base = home.join(".ironclaw").join("projects");
|
||||
let projects_base = ironclaw_base_dir().join("projects");
|
||||
|
||||
if !projects_base.is_absolute() {
|
||||
return Err(OrchestratorError::ContainerCreationFailed {
|
||||
job_id,
|
||||
reason: "base directory is not absolute; cannot safely validate bind mounts".into(),
|
||||
});
|
||||
}
|
||||
|
||||
// Ensure the base exists so canonicalize always succeeds.
|
||||
std::fs::create_dir_all(&projects_base).map_err(|e| {
|
||||
@@ -617,7 +621,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_validate_bind_mount_valid_path() {
|
||||
let base = dirs::home_dir().unwrap().join(".ironclaw").join("projects");
|
||||
let base = crate::bootstrap::compute_ironclaw_base_dir().join("projects");
|
||||
std::fs::create_dir_all(&base).unwrap();
|
||||
|
||||
let test_dir = base.join("test_validate_bind");
|
||||
|
||||
@@ -12,6 +12,8 @@ use fs4::FileExt;
|
||||
use rand::Rng;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::bootstrap::ironclaw_base_dir;
|
||||
|
||||
const PAIRING_CODE_LENGTH: usize = 8;
|
||||
const PAIRING_ALPHABET: &[u8] = b"ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
|
||||
/// TTL for pending pairing requests (minutes, not hours — reduces brute-force window).
|
||||
@@ -70,9 +72,7 @@ struct AllowFromStoreFile {
|
||||
}
|
||||
|
||||
fn default_pairing_dir() -> PathBuf {
|
||||
dirs::home_dir()
|
||||
.unwrap_or_else(|| PathBuf::from("."))
|
||||
.join(".ironclaw")
|
||||
ironclaw_base_dir()
|
||||
}
|
||||
|
||||
fn safe_channel_key(channel: &str) -> Result<String, PairingStoreError> {
|
||||
|
||||
@@ -5,6 +5,7 @@ use std::path::{Component, Path, PathBuf};
|
||||
|
||||
use tokio::fs;
|
||||
|
||||
use crate::bootstrap::ironclaw_base_dir;
|
||||
use crate::registry::catalog::RegistryError;
|
||||
use crate::registry::manifest::{BundleDefinition, ExtensionManifest, ManifestKind};
|
||||
|
||||
@@ -179,11 +180,11 @@ impl RegistryInstaller {
|
||||
|
||||
/// Default installer using standard paths.
|
||||
pub fn with_defaults(repo_root: PathBuf) -> Self {
|
||||
let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
|
||||
let base_dir = ironclaw_base_dir();
|
||||
Self {
|
||||
repo_root,
|
||||
tools_dir: home.join(".ironclaw").join("tools"),
|
||||
channels_dir: home.join(".ironclaw").join("channels"),
|
||||
tools_dir: base_dir.join("tools"),
|
||||
channels_dir: base_dir.join("channels"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+6
-5
@@ -12,6 +12,8 @@ use std::process::Command;
|
||||
|
||||
use anyhow::{Context, Result, bail};
|
||||
|
||||
use crate::bootstrap::ironclaw_base_dir;
|
||||
|
||||
const SERVICE_LABEL: &str = "com.ironclaw.daemon";
|
||||
const SYSTEMD_UNIT: &str = "ironclaw.service";
|
||||
|
||||
@@ -57,7 +59,7 @@ fn install_macos() -> Result<()> {
|
||||
}
|
||||
|
||||
let exe = std::env::current_exe().context("failed to resolve current executable")?;
|
||||
let logs_dir = ironclaw_logs_dir()?;
|
||||
let logs_dir = ironclaw_logs_dir();
|
||||
std::fs::create_dir_all(&logs_dir)?;
|
||||
|
||||
let stdout = logs_dir.join("daemon.stdout.log");
|
||||
@@ -250,9 +252,8 @@ fn linux_unit_path() -> Result<PathBuf> {
|
||||
.join(SYSTEMD_UNIT))
|
||||
}
|
||||
|
||||
fn ironclaw_logs_dir() -> Result<PathBuf> {
|
||||
let home = dirs::home_dir().context("could not find home directory")?;
|
||||
Ok(home.join(".ironclaw").join("logs"))
|
||||
fn ironclaw_logs_dir() -> PathBuf {
|
||||
ironclaw_base_dir().join("logs")
|
||||
}
|
||||
|
||||
// ── Shell helpers ───────────────────────────────────────────────
|
||||
@@ -350,7 +351,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn logs_dir_under_ironclaw() {
|
||||
let path = ironclaw_logs_dir().unwrap();
|
||||
let path = ironclaw_logs_dir();
|
||||
let s = path.to_string_lossy();
|
||||
assert!(s.ends_with(".ironclaw/logs"), "unexpected path: {s}");
|
||||
}
|
||||
|
||||
+4
-8
@@ -7,6 +7,8 @@ use std::path::PathBuf;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::bootstrap::ironclaw_base_dir;
|
||||
|
||||
/// User settings persisted to disk.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct Settings {
|
||||
@@ -656,10 +658,7 @@ impl Settings {
|
||||
|
||||
/// Get the default settings file path (~/.ironclaw/settings.json).
|
||||
pub fn default_path() -> std::path::PathBuf {
|
||||
dirs::home_dir()
|
||||
.unwrap_or_else(|| std::path::PathBuf::from("."))
|
||||
.join(".ironclaw")
|
||||
.join("settings.json")
|
||||
ironclaw_base_dir().join("settings.json")
|
||||
}
|
||||
|
||||
/// Load settings from disk, returning default if not found.
|
||||
@@ -677,10 +676,7 @@ impl Settings {
|
||||
|
||||
/// Default TOML config file path (~/.ironclaw/config.toml).
|
||||
pub fn default_toml_path() -> PathBuf {
|
||||
dirs::home_dir()
|
||||
.unwrap_or_else(|| PathBuf::from("."))
|
||||
.join(".ironclaw")
|
||||
.join("config.toml")
|
||||
ironclaw_base_dir().join("config.toml")
|
||||
}
|
||||
|
||||
/// Load settings from a TOML file.
|
||||
|
||||
+5
-10
@@ -20,6 +20,7 @@ use secrecy::{ExposeSecret, SecretString};
|
||||
#[cfg(feature = "postgres")]
|
||||
use tokio_postgres::NoTls;
|
||||
|
||||
use crate::bootstrap::ironclaw_base_dir;
|
||||
use crate::channels::wasm::{
|
||||
ChannelCapabilitiesFile, available_channel_names, install_bundled_channel,
|
||||
};
|
||||
@@ -1465,9 +1466,7 @@ impl SetupWizard {
|
||||
println!();
|
||||
|
||||
// Discover available WASM channels
|
||||
let channels_dir = dirs::home_dir()
|
||||
.ok_or_else(|| SetupError::Config("Could not determine home directory".into()))?
|
||||
.join(".ironclaw/channels");
|
||||
let channels_dir = ironclaw_base_dir().join("channels");
|
||||
|
||||
let mut discovered_channels = discover_wasm_channels(&channels_dir).await;
|
||||
let installed_names: HashSet<String> = discovered_channels
|
||||
@@ -1697,9 +1696,7 @@ impl SetupWizard {
|
||||
println!();
|
||||
|
||||
// Check which tools are already installed
|
||||
let tools_dir = dirs::home_dir()
|
||||
.ok_or_else(|| SetupError::Config("Could not determine home directory".into()))?
|
||||
.join(".ironclaw/tools");
|
||||
let tools_dir = ironclaw_base_dir().join("tools");
|
||||
|
||||
let installed_tools = discover_installed_tools(&tools_dir).await;
|
||||
|
||||
@@ -1739,9 +1736,7 @@ impl SetupWizard {
|
||||
let installer = crate::registry::installer::RegistryInstaller::new(
|
||||
repo_root.to_path_buf(),
|
||||
tools_dir.clone(),
|
||||
dirs::home_dir()
|
||||
.unwrap_or_default()
|
||||
.join(".ironclaw/channels"),
|
||||
ironclaw_base_dir().join("channels"),
|
||||
);
|
||||
|
||||
let mut installed_count = 0;
|
||||
@@ -2795,7 +2790,7 @@ async fn install_selected_registry_channels(
|
||||
|
||||
let installer = crate::registry::installer::RegistryInstaller::new(
|
||||
repo_root.clone(),
|
||||
dirs::home_dir().unwrap_or_default().join(".ironclaw/tools"),
|
||||
ironclaw_base_dir().join("tools"),
|
||||
channels_dir.to_path_buf(),
|
||||
);
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ use async_trait::async_trait;
|
||||
use chrono::Utc;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::bootstrap::ironclaw_base_dir;
|
||||
use crate::channels::IncomingMessage;
|
||||
use crate::channels::web::types::SseEvent;
|
||||
use crate::context::{ContextManager, JobContext, JobState};
|
||||
@@ -554,10 +555,7 @@ fn validate_env_var_name(name: &str) -> Result<(), ToolError> {
|
||||
}
|
||||
|
||||
fn projects_base() -> PathBuf {
|
||||
dirs::home_dir()
|
||||
.unwrap_or_else(|| PathBuf::from("."))
|
||||
.join(".ironclaw")
|
||||
.join("projects")
|
||||
ironclaw_base_dir().join("projects")
|
||||
}
|
||||
|
||||
/// Resolve the project directory, creating it if it doesn't exist.
|
||||
|
||||
@@ -8,6 +8,7 @@ use std::sync::Arc;
|
||||
use async_trait::async_trait;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use crate::bootstrap::ironclaw_base_dir;
|
||||
use crate::channels::{ChannelManager, OutgoingResponse};
|
||||
use crate::context::JobContext;
|
||||
use crate::tools::tool::{
|
||||
@@ -27,9 +28,7 @@ pub struct MessageTool {
|
||||
|
||||
impl MessageTool {
|
||||
pub fn new(channel_manager: Arc<ChannelManager>) -> Self {
|
||||
let base_dir = dirs::home_dir()
|
||||
.unwrap_or_else(|| std::path::PathBuf::from("."))
|
||||
.join(".ironclaw");
|
||||
let base_dir = ironclaw_base_dir();
|
||||
|
||||
Self {
|
||||
channel_manager,
|
||||
|
||||
@@ -9,6 +9,7 @@ use std::path::{Path, PathBuf};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::fs;
|
||||
|
||||
use crate::bootstrap::ironclaw_base_dir;
|
||||
use crate::tools::tool::ToolError;
|
||||
|
||||
/// Configuration for connecting to a remote MCP server.
|
||||
@@ -251,10 +252,7 @@ impl From<ConfigError> for ToolError {
|
||||
|
||||
/// Get the default MCP servers configuration path.
|
||||
pub fn default_config_path() -> PathBuf {
|
||||
dirs::home_dir()
|
||||
.unwrap_or_else(|| PathBuf::from("."))
|
||||
.join(".ironclaw")
|
||||
.join("mcp-servers.json")
|
||||
ironclaw_base_dir().join("mcp-servers.json")
|
||||
}
|
||||
|
||||
/// Load MCP server configurations from the default location.
|
||||
|
||||
@@ -20,6 +20,7 @@ use std::path::PathBuf;
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::bootstrap::ironclaw_base_dir;
|
||||
use crate::workspace::Workspace;
|
||||
|
||||
/// Configuration for workspace hygiene.
|
||||
@@ -37,15 +38,11 @@ pub struct HygieneConfig {
|
||||
|
||||
impl Default for HygieneConfig {
|
||||
fn default() -> Self {
|
||||
let state_dir = dirs::home_dir()
|
||||
.unwrap_or_else(|| PathBuf::from("."))
|
||||
.join(".ironclaw");
|
||||
|
||||
Self {
|
||||
enabled: true,
|
||||
retention_days: 30,
|
||||
cadence_hours: 12,
|
||||
state_dir,
|
||||
state_dir: ironclaw_base_dir(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user