fix(config): log resolved config path source at startup
This commit is contained in:
parent
e83e017062
commit
9f94ad6db4
2 changed files with 147 additions and 14 deletions
|
|
@ -4,9 +4,15 @@ This is a high-signal reference for common config sections and defaults.
|
||||||
|
|
||||||
Last verified: **February 19, 2026**.
|
Last verified: **February 19, 2026**.
|
||||||
|
|
||||||
Config file path:
|
Config path resolution at startup:
|
||||||
|
|
||||||
- `~/.zeroclaw/config.toml`
|
1. `ZEROCLAW_WORKSPACE` override (if set)
|
||||||
|
2. persisted `~/.zeroclaw/active_workspace.toml` marker (if present)
|
||||||
|
3. default `~/.zeroclaw/config.toml`
|
||||||
|
|
||||||
|
ZeroClaw logs the resolved config on startup at `INFO` level:
|
||||||
|
|
||||||
|
- `Config loaded` with fields: `path`, `workspace`, `source`, `initialized`
|
||||||
|
|
||||||
Schema export command:
|
Schema export command:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2659,6 +2659,60 @@ fn resolve_config_dir_for_workspace(workspace_dir: &Path) -> (PathBuf, PathBuf)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
enum ConfigResolutionSource {
|
||||||
|
EnvWorkspace,
|
||||||
|
ActiveWorkspaceMarker,
|
||||||
|
DefaultConfigDir,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ConfigResolutionSource {
|
||||||
|
const fn as_str(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::EnvWorkspace => "ZEROCLAW_WORKSPACE",
|
||||||
|
Self::ActiveWorkspaceMarker => "active_workspace.toml",
|
||||||
|
Self::DefaultConfigDir => "default",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn resolve_runtime_config_dirs(
|
||||||
|
default_zeroclaw_dir: &Path,
|
||||||
|
default_workspace_dir: &Path,
|
||||||
|
) -> Result<(PathBuf, PathBuf, ConfigResolutionSource)> {
|
||||||
|
// Resolution priority:
|
||||||
|
// 1. ZEROCLAW_WORKSPACE env override
|
||||||
|
// 2. Persisted active workspace marker from onboarding/custom profile
|
||||||
|
// 3. Default ~/.zeroclaw layout
|
||||||
|
if let Ok(custom_workspace) = std::env::var("ZEROCLAW_WORKSPACE") {
|
||||||
|
if !custom_workspace.is_empty() {
|
||||||
|
let (zeroclaw_dir, workspace_dir) =
|
||||||
|
resolve_config_dir_for_workspace(&PathBuf::from(custom_workspace));
|
||||||
|
return Ok((
|
||||||
|
zeroclaw_dir,
|
||||||
|
workspace_dir,
|
||||||
|
ConfigResolutionSource::EnvWorkspace,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some((zeroclaw_dir, workspace_dir)) =
|
||||||
|
load_persisted_workspace_dirs(default_zeroclaw_dir).await?
|
||||||
|
{
|
||||||
|
return Ok((
|
||||||
|
zeroclaw_dir,
|
||||||
|
workspace_dir,
|
||||||
|
ConfigResolutionSource::ActiveWorkspaceMarker,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok((
|
||||||
|
default_zeroclaw_dir.to_path_buf(),
|
||||||
|
default_workspace_dir.to_path_buf(),
|
||||||
|
ConfigResolutionSource::DefaultConfigDir,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
fn decrypt_optional_secret(
|
fn decrypt_optional_secret(
|
||||||
store: &crate::security::SecretStore,
|
store: &crate::security::SecretStore,
|
||||||
value: &mut Option<String>,
|
value: &mut Option<String>,
|
||||||
|
|
@ -2697,18 +2751,8 @@ impl Config {
|
||||||
pub async fn load_or_init() -> Result<Self> {
|
pub async fn load_or_init() -> Result<Self> {
|
||||||
let (default_zeroclaw_dir, default_workspace_dir) = default_config_and_workspace_dirs()?;
|
let (default_zeroclaw_dir, default_workspace_dir) = default_config_and_workspace_dirs()?;
|
||||||
|
|
||||||
// Resolution priority:
|
let (zeroclaw_dir, workspace_dir, resolution_source) =
|
||||||
// 1. ZEROCLAW_WORKSPACE env override
|
resolve_runtime_config_dirs(&default_zeroclaw_dir, &default_workspace_dir).await?;
|
||||||
// 2. Persisted active workspace marker from onboarding/custom profile
|
|
||||||
// 3. Default ~/.zeroclaw layout
|
|
||||||
let (zeroclaw_dir, workspace_dir) = match std::env::var("ZEROCLAW_WORKSPACE") {
|
|
||||||
Ok(custom_workspace) if !custom_workspace.is_empty() => {
|
|
||||||
resolve_config_dir_for_workspace(&PathBuf::from(custom_workspace))
|
|
||||||
}
|
|
||||||
_ => load_persisted_workspace_dirs(&default_zeroclaw_dir)
|
|
||||||
.await?
|
|
||||||
.unwrap_or((default_zeroclaw_dir, default_workspace_dir)),
|
|
||||||
};
|
|
||||||
|
|
||||||
let config_path = zeroclaw_dir.join("config.toml");
|
let config_path = zeroclaw_dir.join("config.toml");
|
||||||
|
|
||||||
|
|
@ -2775,6 +2819,13 @@ impl Config {
|
||||||
decrypt_optional_secret(&store, &mut agent.api_key, "config.agents.*.api_key")?;
|
decrypt_optional_secret(&store, &mut agent.api_key, "config.agents.*.api_key")?;
|
||||||
}
|
}
|
||||||
config.apply_env_overrides();
|
config.apply_env_overrides();
|
||||||
|
tracing::info!(
|
||||||
|
path = %config.config_path.display(),
|
||||||
|
workspace = %config.workspace_dir.display(),
|
||||||
|
source = resolution_source.as_str(),
|
||||||
|
initialized = false,
|
||||||
|
"Config loaded"
|
||||||
|
);
|
||||||
Ok(config)
|
Ok(config)
|
||||||
} else {
|
} else {
|
||||||
let mut config = Config::default();
|
let mut config = Config::default();
|
||||||
|
|
@ -2790,6 +2841,13 @@ impl Config {
|
||||||
}
|
}
|
||||||
|
|
||||||
config.apply_env_overrides();
|
config.apply_env_overrides();
|
||||||
|
tracing::info!(
|
||||||
|
path = %config.config_path.display(),
|
||||||
|
workspace = %config.workspace_dir.display(),
|
||||||
|
source = resolution_source.as_str(),
|
||||||
|
initialized = true,
|
||||||
|
"Config loaded"
|
||||||
|
);
|
||||||
Ok(config)
|
Ok(config)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -4545,6 +4603,75 @@ default_temperature = 0.7
|
||||||
std::env::remove_var("ZEROCLAW_WORKSPACE");
|
std::env::remove_var("ZEROCLAW_WORKSPACE");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
async fn resolve_runtime_config_dirs_uses_env_workspace_first() {
|
||||||
|
let _env_guard = env_override_lock().await;
|
||||||
|
let default_config_dir = std::env::temp_dir().join(uuid::Uuid::new_v4().to_string());
|
||||||
|
let default_workspace_dir = default_config_dir.join("workspace");
|
||||||
|
let workspace_dir = default_config_dir.join("profile-a");
|
||||||
|
|
||||||
|
std::env::set_var("ZEROCLAW_WORKSPACE", &workspace_dir);
|
||||||
|
let (config_dir, resolved_workspace_dir, source) =
|
||||||
|
resolve_runtime_config_dirs(&default_config_dir, &default_workspace_dir)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(source, ConfigResolutionSource::EnvWorkspace);
|
||||||
|
assert_eq!(config_dir, workspace_dir);
|
||||||
|
assert_eq!(resolved_workspace_dir, workspace_dir.join("workspace"));
|
||||||
|
|
||||||
|
std::env::remove_var("ZEROCLAW_WORKSPACE");
|
||||||
|
let _ = fs::remove_dir_all(default_config_dir).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
async fn resolve_runtime_config_dirs_uses_active_workspace_marker() {
|
||||||
|
let _env_guard = env_override_lock().await;
|
||||||
|
let default_config_dir = std::env::temp_dir().join(uuid::Uuid::new_v4().to_string());
|
||||||
|
let default_workspace_dir = default_config_dir.join("workspace");
|
||||||
|
let marker_config_dir = default_config_dir.join("profiles").join("alpha");
|
||||||
|
let state_path = default_config_dir.join(ACTIVE_WORKSPACE_STATE_FILE);
|
||||||
|
|
||||||
|
std::env::remove_var("ZEROCLAW_WORKSPACE");
|
||||||
|
fs::create_dir_all(&default_config_dir).await.unwrap();
|
||||||
|
let state = ActiveWorkspaceState {
|
||||||
|
config_dir: marker_config_dir.to_string_lossy().into_owned(),
|
||||||
|
};
|
||||||
|
fs::write(&state_path, toml::to_string(&state).unwrap())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let (config_dir, resolved_workspace_dir, source) =
|
||||||
|
resolve_runtime_config_dirs(&default_config_dir, &default_workspace_dir)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(source, ConfigResolutionSource::ActiveWorkspaceMarker);
|
||||||
|
assert_eq!(config_dir, marker_config_dir);
|
||||||
|
assert_eq!(resolved_workspace_dir, marker_config_dir.join("workspace"));
|
||||||
|
|
||||||
|
let _ = fs::remove_dir_all(default_config_dir).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
async fn resolve_runtime_config_dirs_falls_back_to_default_layout() {
|
||||||
|
let _env_guard = env_override_lock().await;
|
||||||
|
let default_config_dir = std::env::temp_dir().join(uuid::Uuid::new_v4().to_string());
|
||||||
|
let default_workspace_dir = default_config_dir.join("workspace");
|
||||||
|
|
||||||
|
std::env::remove_var("ZEROCLAW_WORKSPACE");
|
||||||
|
let (config_dir, resolved_workspace_dir, source) =
|
||||||
|
resolve_runtime_config_dirs(&default_config_dir, &default_workspace_dir)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(source, ConfigResolutionSource::DefaultConfigDir);
|
||||||
|
assert_eq!(config_dir, default_config_dir);
|
||||||
|
assert_eq!(resolved_workspace_dir, default_workspace_dir);
|
||||||
|
|
||||||
|
let _ = fs::remove_dir_all(default_config_dir).await;
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
async fn load_or_init_workspace_override_uses_workspace_root_for_config() {
|
async fn load_or_init_workspace_override_uses_workspace_root_for_config() {
|
||||||
let _env_guard = env_override_lock().await;
|
let _env_guard = env_override_lock().await;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue