fix(config): recover docker runtime path on save (#3165)
* fix(config): recover docker runtime path on save * fix: update config_path in-memory after save() resolves bare filename Change save() signature from &self to &mut self so it can assign the resolved config_path back to the struct. This ensures downstream reads (proxy_config, model_routing_config) use the correct absolute path instead of a stale bare filename. Add test assertion verifying config.config_path equals resolved path after save(). Update all callers to use mutable bindings. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
b21223a6aa
commit
069b8e0586
@ -6691,11 +6691,41 @@ impl Config {
|
||||
set_runtime_proxy_config(self.proxy.clone());
|
||||
}
|
||||
|
||||
pub async fn save(&self) -> Result<()> {
|
||||
// Encrypt secrets before serialization
|
||||
let mut config_to_save = self.clone();
|
||||
let zeroclaw_dir = self
|
||||
async fn resolve_config_path_for_save(&self) -> Result<PathBuf> {
|
||||
if self
|
||||
.config_path
|
||||
.parent()
|
||||
.is_some_and(|parent| !parent.as_os_str().is_empty())
|
||||
{
|
||||
return Ok(self.config_path.clone());
|
||||
}
|
||||
|
||||
let (default_zeroclaw_dir, default_workspace_dir) = default_config_and_workspace_dirs()?;
|
||||
let (zeroclaw_dir, _workspace_dir, source) =
|
||||
resolve_runtime_config_dirs(&default_zeroclaw_dir, &default_workspace_dir).await?;
|
||||
let file_name = self
|
||||
.config_path
|
||||
.file_name()
|
||||
.filter(|name| !name.is_empty())
|
||||
.unwrap_or_else(|| std::ffi::OsStr::new("config.toml"));
|
||||
let resolved = zeroclaw_dir.join(file_name);
|
||||
tracing::warn!(
|
||||
path = %self.config_path.display(),
|
||||
resolved = %resolved.display(),
|
||||
source = source.as_str(),
|
||||
"Config path missing parent directory; resolving from runtime environment"
|
||||
);
|
||||
Ok(resolved)
|
||||
}
|
||||
|
||||
pub async fn save(&mut self) -> Result<()> {
|
||||
// Encrypt secrets before serialization
|
||||
let config_path = self.resolve_config_path_for_save().await?;
|
||||
// Keep the in-memory config_path in sync so downstream reads
|
||||
// (e.g. proxy_config, model_routing_config) use the resolved path.
|
||||
self.config_path = config_path.clone();
|
||||
let mut config_to_save = self.clone();
|
||||
let zeroclaw_dir = config_path
|
||||
.parent()
|
||||
.context("Config path must have a parent directory")?;
|
||||
let store = crate::security::SecretStore::new(zeroclaw_dir, self.secrets.encrypt);
|
||||
@ -6764,8 +6794,7 @@ impl Config {
|
||||
let toml_str =
|
||||
toml::to_string_pretty(&config_to_save).context("Failed to serialize config")?;
|
||||
|
||||
let parent_dir = self
|
||||
.config_path
|
||||
let parent_dir = config_path
|
||||
.parent()
|
||||
.context("Config path must have a parent directory")?;
|
||||
|
||||
@ -6776,8 +6805,7 @@ impl Config {
|
||||
)
|
||||
})?;
|
||||
|
||||
let file_name = self
|
||||
.config_path
|
||||
let file_name = config_path
|
||||
.file_name()
|
||||
.and_then(|v| v.to_str())
|
||||
.unwrap_or("config.toml");
|
||||
@ -6817,9 +6845,9 @@ impl Config {
|
||||
.context("Failed to fsync temporary config file")?;
|
||||
drop(temp_file);
|
||||
|
||||
let had_existing_config = self.config_path.exists();
|
||||
let had_existing_config = config_path.exists();
|
||||
if had_existing_config {
|
||||
fs::copy(&self.config_path, &backup_path)
|
||||
fs::copy(&config_path, &backup_path)
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!(
|
||||
@ -6829,10 +6857,10 @@ impl Config {
|
||||
})?;
|
||||
}
|
||||
|
||||
if let Err(e) = fs::rename(&temp_path, &self.config_path).await {
|
||||
if let Err(e) = fs::rename(&temp_path, &config_path).await {
|
||||
let _ = fs::remove_file(&temp_path).await;
|
||||
if had_existing_config && backup_path.exists() {
|
||||
fs::copy(&backup_path, &self.config_path)
|
||||
fs::copy(&backup_path, &config_path)
|
||||
.await
|
||||
.context("Failed to restore config backup")?;
|
||||
}
|
||||
@ -6842,12 +6870,12 @@ impl Config {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::{fs::Permissions, os::unix::fs::PermissionsExt};
|
||||
fs::set_permissions(&self.config_path, Permissions::from_mode(0o600))
|
||||
fs::set_permissions(&config_path, Permissions::from_mode(0o600))
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"Failed to enforce secure permissions on config file: {}",
|
||||
self.config_path.display()
|
||||
config_path.display()
|
||||
)
|
||||
})?;
|
||||
}
|
||||
@ -7660,7 +7688,7 @@ tool_dispatcher = "xml"
|
||||
fs::create_dir_all(&dir).await.unwrap();
|
||||
|
||||
let config_path = dir.join("config.toml");
|
||||
let config = Config {
|
||||
let mut config = Config {
|
||||
workspace_dir: dir.join("workspace"),
|
||||
config_path: config_path.clone(),
|
||||
api_key: Some("sk-roundtrip".into()),
|
||||
@ -10486,6 +10514,44 @@ default_model = "legacy-model"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
async fn save_repairs_bare_config_filename_using_runtime_resolution() {
|
||||
let _env_guard = env_override_lock().await;
|
||||
let temp_home =
|
||||
std::env::temp_dir().join(format!("zeroclaw_test_home_{}", uuid::Uuid::new_v4()));
|
||||
let workspace_dir = temp_home.join("workspace");
|
||||
let resolved_config_path = temp_home.join(".zeroclaw").join("config.toml");
|
||||
|
||||
let original_home = std::env::var("HOME").ok();
|
||||
std::env::set_var("HOME", &temp_home);
|
||||
std::env::set_var("ZEROCLAW_WORKSPACE", &workspace_dir);
|
||||
|
||||
let mut config = Config::default();
|
||||
config.workspace_dir = workspace_dir;
|
||||
config.config_path = PathBuf::from("config.toml");
|
||||
config.default_temperature = 0.5;
|
||||
config.save().await.unwrap();
|
||||
|
||||
assert!(resolved_config_path.exists());
|
||||
assert_eq!(
|
||||
config.config_path, resolved_config_path,
|
||||
"save() must update config_path to the resolved path"
|
||||
);
|
||||
let saved = tokio::fs::read_to_string(&resolved_config_path)
|
||||
.await
|
||||
.unwrap();
|
||||
let parsed: Config = toml::from_str(&saved).unwrap();
|
||||
assert_eq!(parsed.default_temperature, 0.5);
|
||||
|
||||
std::env::remove_var("ZEROCLAW_WORKSPACE");
|
||||
if let Some(home) = original_home {
|
||||
std::env::set_var("HOME", home);
|
||||
} else {
|
||||
std::env::remove_var("HOME");
|
||||
}
|
||||
let _ = tokio::fs::remove_dir_all(temp_home).await;
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
async fn save_restricts_existing_world_readable_config_to_owner_only() {
|
||||
|
||||
@ -543,7 +543,7 @@ pub async fn handle_api_config_put(
|
||||
};
|
||||
|
||||
let current_config = state.config.lock().clone();
|
||||
let new_config = hydrate_config_for_save(incoming, ¤t_config);
|
||||
let mut new_config = hydrate_config_for_save(incoming, ¤t_config);
|
||||
|
||||
if let Err(e) = new_config.validate() {
|
||||
return (
|
||||
@ -752,7 +752,7 @@ pub async fn handle_api_integration_credentials_put(
|
||||
}
|
||||
}
|
||||
|
||||
let updated = match apply_integration_credentials_update(¤t, &id, &body.fields) {
|
||||
let mut updated = match apply_integration_credentials_update(¤t, &id, &body.fields) {
|
||||
Ok(config) => config,
|
||||
Err(error) if error.starts_with("Unknown integration id:") => {
|
||||
return (
|
||||
|
||||
@ -128,7 +128,7 @@ pub async fn run_wizard(force: bool) -> Result<Config> {
|
||||
|
||||
// ── Build config ──
|
||||
// Defaults: SQLite memory, supervised autonomy, workspace-scoped, native runtime
|
||||
let config = Config {
|
||||
let mut config = Config {
|
||||
workspace_dir: workspace_dir.clone(),
|
||||
config_path: config_path.clone(),
|
||||
api_key: if api_key.is_empty() {
|
||||
@ -487,7 +487,7 @@ async fn run_quick_setup_with_home(
|
||||
// Create memory config based on backend choice
|
||||
let memory_config = memory_config_defaults_for_backend(&memory_backend_name);
|
||||
|
||||
let config = Config {
|
||||
let mut config = Config {
|
||||
workspace_dir: workspace_dir.clone(),
|
||||
config_path: config_path.clone(),
|
||||
api_key: credential_override.map(|c| {
|
||||
|
||||
@ -921,7 +921,7 @@ mod tests {
|
||||
}
|
||||
|
||||
async fn test_config(tmp: &TempDir) -> Arc<Config> {
|
||||
let config = Config {
|
||||
let mut config = Config {
|
||||
workspace_dir: tmp.path().join("workspace"),
|
||||
config_path: tmp.path().join("config.toml"),
|
||||
..Config::default()
|
||||
|
||||
@ -450,7 +450,7 @@ mod tests {
|
||||
}
|
||||
|
||||
async fn test_config(tmp: &TempDir) -> Arc<Config> {
|
||||
let config = Config {
|
||||
let mut config = Config {
|
||||
workspace_dir: tmp.path().join("workspace"),
|
||||
config_path: tmp.path().join("config.toml"),
|
||||
..Config::default()
|
||||
|
||||
Loading…
Reference in New Issue
Block a user