Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 98 additions & 4 deletions src/agentsight/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -534,19 +534,45 @@ pub fn ensure_default_agents_config(path: &Path) -> anyhow::Result<()> {
return Ok(());
}

// Shallow-merge: start from the embedded default, overlay all top-level
// keys the user has set (except schema_version itself), then bump version.
// This preserves user customizations (cmdline rules, https rules,
// codex_offsets, feature overrides) while adding any NEW sections from the
// default that the old config was missing. Fixes #1496 — the previous
// implementation did a destructive overwrite that silently lost user data.
let mut base: serde_json::Value = serde_json::from_str(DEFAULT_AGENTS_JSON)
.expect("embedded DEFAULT_AGENTS_JSON must be valid");
let user: serde_json::Value =
serde_json::from_str(&content).expect("JSON validity already confirmed above");
Comment thread
chengshuyi marked this conversation as resolved.

if let (Some(base_obj), Some(user_obj)) = (base.as_object_mut(), user.as_object()) {
for (key, value) in user_obj {
Comment thread
chengshuyi marked this conversation as resolved.
if key == "schema_version" {
continue;
}
base_obj.insert(key.clone(), value.clone());
}
base_obj.insert(
"schema_version".to_string(),
serde_json::Value::Number(CURRENT_SCHEMA_VERSION.into()),
);
}

let merged = serde_json::to_string_pretty(&base).expect("merged config must serialize");

let backup = {
let ts = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
path.with_extension(format!("json.bak.{ts}"))
};
std::fs::rename(path, &backup)
std::fs::copy(path, &backup)
Comment thread
chengshuyi marked this conversation as resolved.
.with_context(|| format!("Failed to back up {path:?} to {backup:?}"))?;
std::fs::write(path, DEFAULT_AGENTS_JSON)
.with_context(|| format!("Failed to write updated agents config to {path:?}"))?;
std::fs::write(path, &merged)
.with_context(|| format!("Failed to write merged config to {path:?}"))?;
log::info!(
"Config schema_version {:?} < {}, overwrote {path:?} (backup at {backup:?})",
"Config schema_version {:?} < {}, merged user config with defaults at {path:?} (backup at {backup:?})",
on_disk_version,
CURRENT_SCHEMA_VERSION
);
Expand Down Expand Up @@ -2141,4 +2167,72 @@ mod tests {
.collect();
assert!(backups.is_empty());
}

#[test]
fn ensure_default_agents_config_preserves_user_cmdline_rules_on_merge() {
// Regression for #1496: when migrating a valid v1 config to v2, user
// customizations (cmdline rules, etc.) must survive — not be overwritten
// with the embedded default. Discriminating: reverting to the old
// overwrite logic would lose "MyCustomAgent" and fail this test.
let dir = unique_temp_dir();
let path = dir.join("agentsight.json");
let user_config = r#"{
"schema_version": 1,
"cmdline": {
"allow": [
{"rule": ["*mycustomagent*"], "agent_name": "MyCustomAgent"}
],
"deny": [{"rule": ["*noisy*"]}]
},
"https": [{"rule": ["custom.api.example.com"]}]
}"#;
std::fs::write(&path, user_config).unwrap();
ensure_default_agents_config(&path).unwrap();
let result = std::fs::read_to_string(&path).unwrap();
// User's custom agent rule must survive migration
assert!(
result.contains("MyCustomAgent"),
"user cmdline rule lost during migration: {result}"
);
assert!(
result.contains("custom.api.example.com"),
"user https rule lost during migration: {result}"
);
assert!(
result.contains("*noisy*"),
"user deny rule lost during migration: {result}"
);
// schema_version must be bumped to current
let migrated: serde_json::Value = serde_json::from_str(&result).unwrap();
assert_eq!(
migrated["schema_version"].as_u64().unwrap() as u32,
CURRENT_SCHEMA_VERSION
);
}

#[test]
fn ensure_default_agents_config_adds_missing_sections_on_merge() {
// A v1 config that only has cmdline should gain features/runtime_limits
// from the default after migration — verifies additive merge.
let dir = unique_temp_dir();
let path = dir.join("agentsight.json");
let minimal_v1 = r#"{
"cmdline": {"allow": [{"rule": ["*test*"], "agent_name": "Test"}]}
}"#;
std::fs::write(&path, minimal_v1).unwrap();
ensure_default_agents_config(&path).unwrap();
let result = std::fs::read_to_string(&path).unwrap();
let migrated: serde_json::Value = serde_json::from_str(&result).unwrap();
// New sections from default must be present
assert!(
migrated.get("features").is_some(),
"features section not added from default"
);
assert!(
migrated.get("runtime_limits").is_some(),
"runtime_limits section not added from default"
);
// User's original rule must still be there
assert!(result.contains("Test"), "user rule lost");
}
}
Loading