#[allow(clippy::wildcard_imports)]
use super::*;
impl Config {
pub(super) fn merge_local(&mut self, local_toml: &str, trusted: bool) {
let selected_profile =
super::loader::environment_config_profile().or_else(|| self.config_profile.clone());
let mut local: Config =
match super::loader::parse_config_with_profile(local_toml, selected_profile.as_deref())
{
Ok(c) => c,
Err(e) => {
tracing::warn!("local config parse error: {e}");
eprintln!(
"\x1b[33m[lean-ctx] WARNING: local .lean-ctx.toml parse error: {e}\n \
Local overrides skipped.\x1b[0m"
);
return;
}
};
if !trusted {
let withheld = strip_sensitive_overrides(&mut local);
if !withheld.is_empty() {
tracing::warn!(
"[SECURITY] untrusted workspace: ignoring {} security-sensitive \
.lean-ctx.toml override(s): {} — run `lean-ctx trust` to apply them",
withheld.len(),
withheld.join(", ")
);
}
}
macro_rules! override_if_true {
($($field:tt)+) => {
if local.$($field)+ {
self.$($field)+ = true;
}
};
}
macro_rules! override_if_false {
($($field:tt)+) => {
if !local.$($field)+ {
self.$($field)+ = false;
}
};
}
macro_rules! override_if_ne {
($default:expr, $($field:tt)+) => {
if local.$($field)+ != $default {
self.$($field)+ = local.$($field)+;
}
};
}
macro_rules! override_if_some {
($($field:tt)+) => {
if local.$($field)+.is_some() {
self.$($field)+ = local.$($field)+;
}
};
}
macro_rules! extend_if_nonempty {
($($field:tt)+) => {
if !local.$($field)+.is_empty() {
self.$($field)+.extend(local.$($field)+);
}
};
}
macro_rules! replace_if_nonempty {
($($field:tt)+) => {
if !local.$($field)+.is_empty() {
self.$($field)+ = local.$($field)+;
}
};
}
macro_rules! override_if_key_present {
($key:literal, $($field:tt)+) => {
if local_toml.contains($key) {
self.$($field)+ = local.$($field)+;
}
};
}
let default = Config::default();
override_if_true!(ultra_compact);
override_if_ne!(default.tee_mode, tee_mode);
override_if_ne!(default.recovery_hints, recovery_hints);
override_if_ne!(default.output_density, output_density);
override_if_ne!(default.checkpoint_interval, checkpoint_interval);
extend_if_nonempty!(excluded_commands);
extend_if_nonempty!(passthrough_urls);
extend_if_nonempty!(custom_aliases);
for fmt in local.preserve_compact_formats {
if !self
.preserve_compact_formats
.iter()
.any(|f| f.eq_ignore_ascii_case(&fmt))
{
self.preserve_compact_formats.push(fmt);
}
}
override_if_ne!(default.slow_command_threshold_ms, slow_command_threshold_ms);
override_if_ne!(default.theme, theme);
override_if_false!(buddy_enabled);
override_if_false!(enable_wakeup_ctx);
extend_if_nonempty!(redirect_exclude);
extend_if_nonempty!(disabled_tools);
override_if_true!(prefer_native_editor);
extend_if_nonempty!(extra_ignore_patterns);
extend_if_nonempty!(index.exclude);
replace_if_nonempty!(index.include);
override_if_false!(index.respect_gitignore);
override_if_some!(rules_scope);
override_if_some!(rules_injection);
override_if_some!(permission_inheritance);
override_if_some!(proxy.anthropic_upstream);
override_if_some!(proxy.openai_upstream);
override_if_some!(proxy.chatgpt_upstream);
override_if_some!(proxy.gemini_upstream);
if let Ok(value) = local_toml.parse::<toml::Value>()
&& let Some(table) = value.get("conversation").and_then(toml::Value::as_table)
{
if table.contains_key("compression_enabled") {
self.conversation.compression_enabled = local.conversation.compression_enabled;
}
if table.contains_key("preserve_last_n_turns") {
self.conversation.preserve_last_n_turns = local.conversation.preserve_last_n_turns;
}
if table.contains_key("compression_threshold_tokens") {
self.conversation.compression_threshold_tokens =
local.conversation.compression_threshold_tokens;
}
if table.contains_key("min_score_to_preserve") {
self.conversation.min_score_to_preserve = local.conversation.min_score_to_preserve;
}
if table.contains_key("summarize_score_range") {
self.conversation.summarize_score_range = local.conversation.summarize_score_range;
}
if table.contains_key("drop_score_below") {
self.conversation.drop_score_below = local.conversation.drop_score_below;
}
if table.contains_key("ccr_store_dropped") {
self.conversation.ccr_store_dropped = local.conversation.ccr_store_dropped;
}
}
override_if_key_present!("response_shaping", response_shaping);
override_if_false!(autonomy.enabled);
override_if_false!(autonomy.auto_preload);
override_if_false!(autonomy.auto_dedup);
override_if_false!(autonomy.auto_related);
override_if_false!(autonomy.auto_consolidate);
self.autonomy.silent_preload = local.autonomy.silent_preload;
override_if_ne!(default.autonomy.dedup_threshold, autonomy.dedup_threshold);
override_if_ne!(
default.autonomy.consolidate_every_calls,
autonomy.consolidate_every_calls
);
override_if_ne!(
default.autonomy.consolidate_cooldown_secs,
autonomy.consolidate_cooldown_secs
);
override_if_false!(autonomy.cognition_loop_enabled);
override_if_ne!(
default.autonomy.cognition_loop_interval_secs,
autonomy.cognition_loop_interval_secs
);
override_if_ne!(
default.autonomy.cognition_loop_max_steps,
autonomy.cognition_loop_max_steps
);
override_if_key_present!("compression_level", compression_level);
override_if_key_present!("compression_aggressiveness", compression_aggressiveness);
override_if_key_present!("terse_agent", terse_agent);
override_if_false!(archive.enabled);
override_if_ne!(default.archive.threshold_chars, archive.threshold_chars);
override_if_ne!(default.archive.max_age_hours, archive.max_age_hours);
override_if_ne!(default.archive.max_disk_mb, archive.max_disk_mb);
override_if_false!(archive.ephemeral);
override_if_ne!(
default.archive.ephemeral_min_tokens,
archive.ephemeral_min_tokens
);
override_if_ne!(default.archive.inline_max_bytes, archive.inline_max_bytes);
override_if_ne!(default.archive.raw_commands, archive.raw_commands);
override_if_ne!(
default.memory.knowledge.max_facts,
memory.knowledge.max_facts
);
override_if_ne!(
default.memory.knowledge.max_patterns,
memory.knowledge.max_patterns
);
override_if_ne!(
default.memory.knowledge.max_history,
memory.knowledge.max_history
);
override_if_ne!(
default.memory.knowledge.contradiction_threshold,
memory.knowledge.contradiction_threshold
);
override_if_ne!(
default.memory.episodic.max_episodes,
memory.episodic.max_episodes
);
override_if_ne!(
default.memory.episodic.max_actions_per_episode,
memory.episodic.max_actions_per_episode
);
override_if_ne!(
default.memory.episodic.summary_max_chars,
memory.episodic.summary_max_chars
);
override_if_ne!(
default.memory.procedural.min_repetitions,
memory.procedural.min_repetitions
);
override_if_ne!(
default.memory.procedural.min_sequence_len,
memory.procedural.min_sequence_len
);
override_if_ne!(
default.memory.procedural.max_procedures,
memory.procedural.max_procedures
);
override_if_ne!(
default.memory.procedural.max_window_size,
memory.procedural.max_window_size
);
override_if_ne!(
default.memory.lifecycle.decay_rate,
memory.lifecycle.decay_rate
);
override_if_ne!(
default.memory.lifecycle.low_confidence_threshold,
memory.lifecycle.low_confidence_threshold
);
override_if_ne!(
default.memory.lifecycle.stale_days,
memory.lifecycle.stale_days
);
override_if_ne!(
default.memory.lifecycle.similarity_threshold,
memory.lifecycle.similarity_threshold
);
override_if_ne!(
default.memory.lifecycle.reclaim_headroom_pct,
memory.lifecycle.reclaim_headroom_pct
);
override_if_ne!(
default.memory.lifecycle.reclaim_enabled,
memory.lifecycle.reclaim_enabled
);
override_if_ne!(
default.memory.embeddings.max_facts,
memory.embeddings.max_facts
);
extend_if_nonempty!(allow_paths);
extend_if_nonempty!(write_allow_paths);
extend_if_nonempty!(extra_roots);
extend_if_nonempty!(read_only_roots);
extend_if_nonempty!(allow_symlink_roots);
override_if_true!(minimal_overhead);
override_if_true!(shell_hook_disabled);
override_if_true!(skip_agent_aliases);
override_if_ne!(default.shell_activation, shell_activation);
override_if_ne!(default.read_redirect, read_redirect);
override_if_ne!(default.read_dedup, read_dedup);
override_if_ne!(default.bm25_max_cache_mb, bm25_max_cache_mb);
override_if_ne!(default.memory_profile, memory_profile);
override_if_ne!(default.memory_cleanup, memory_cleanup);
override_if_ne!(default.shell_allowlist, shell_allowlist);
extend_if_nonempty!(shell_allowlist_extra);
replace_if_nonempty!(default_tool_categories);
override_if_some!(tool_profile);
replace_if_nonempty!(tools_enabled);
override_if_true!(no_degrade);
override_if_true!(delta_explicit);
override_if_some!(profile);
override_if_some!(proxy_timeout_ms);
}
}