use regex::Regex;
use super::{MigrateError, MigrationResult, insert_after_section, section_header_present};
static TUI_HEADER_RE: std::sync::LazyLock<Regex> = std::sync::LazyLock::new(|| {
Regex::new(r"(?m)^[ \t]*\[tui\][ \t]*(?:#[^\r\n]*)?\r?\n").expect("static pattern")
});
pub fn migrate_tui_delights(toml_src: &str) -> Result<MigrationResult, MigrateError> {
if !section_header_present(toml_src, "tui") {
return Ok(MigrationResult {
output: toml_src.to_owned(),
changed_count: 0,
sections_changed: Vec::new(),
});
}
let already_present = section_header_present(toml_src, "tui.delights")
|| toml_src.lines().any(|l| l.trim() == "# [tui.delights]");
if already_present {
return Ok(MigrationResult {
output: toml_src.to_owned(),
changed_count: 0,
sections_changed: Vec::new(),
});
}
let owned;
let src = if toml_src.ends_with('\n') {
toml_src
} else {
owned = format!("{toml_src}\n");
&owned
};
if !TUI_HEADER_RE.is_match(src) {
return Ok(MigrationResult {
output: toml_src.to_owned(),
changed_count: 0,
sections_changed: Vec::new(),
});
}
let advisory = "\n# [tui.delights] — micro-delight toggles (all default true, #5104).\n\
# motion = off acts as a master kill-switch regardless of individual settings.\n\
# [tui.delights]\n\
# stream_metrics = true # tok/s during streaming + TTFT after turn in status bar\n\
# toasts = true # ephemeral overlay notifications (theme switch, copy, etc.)\n\
# completion_flash = true # accent tint on a finished tool group for ~400 ms\n\
# smooth_scroll = true # eased multi-frame interpolation on page-up / page-down\n\
# splash_shimmer = true # one-shot gradient sweep across the wordmark at startup\n";
let output = TUI_HEADER_RE
.replacen(src, 1, |caps: ®ex::Captures| {
format!("{}{advisory}", &caps[0])
})
.into_owned();
let changed = output != toml_src;
let changed_count = usize::from(changed);
Ok(MigrationResult {
output,
changed_count,
sections_changed: if changed {
vec!["tui.delights".to_owned()]
} else {
Vec::new()
},
})
}
pub fn migrate_tui_mouse(toml_src: &str) -> Result<MigrationResult, MigrateError> {
if !section_header_present(toml_src, "tui") {
return Ok(MigrationResult {
output: toml_src.to_owned(),
changed_count: 0,
sections_changed: Vec::new(),
});
}
let already_present = toml_src.lines().any(|l| {
let t = l.trim().trim_start_matches('#').trim();
t.starts_with("mouse")
});
if already_present {
return Ok(MigrationResult {
output: toml_src.to_owned(),
changed_count: 0,
sections_changed: Vec::new(),
});
}
let owned;
let src = if toml_src.ends_with('\n') {
toml_src
} else {
owned = format!("{toml_src}\n");
&owned
};
if !TUI_HEADER_RE.is_match(src) {
return Ok(MigrationResult {
output: toml_src.to_owned(),
changed_count: 0,
sections_changed: Vec::new(),
});
}
let insert =
"# mouse = false # opt-in mouse capture: wheel scrolls, clicks focus panels (#5103)\n";
let output = TUI_HEADER_RE
.replacen(src, 1, |caps: ®ex::Captures| {
format!("{}{insert}", &caps[0])
})
.into_owned();
let changed = output != toml_src;
let changed_count = usize::from(changed);
Ok(MigrationResult {
output,
changed_count,
sections_changed: if changed {
vec!["tui".to_owned()]
} else {
Vec::new()
},
})
}
pub fn migrate_compression_predictor_config(
toml_src: &str,
) -> Result<MigrationResult, MigrateError> {
let has_active = section_header_present(toml_src, "memory.compression.predictor");
let has_commented = toml_src.contains("# [memory.compression.predictor]");
if !has_active && !has_commented {
return Ok(MigrationResult {
output: toml_src.to_owned(),
changed_count: 0,
sections_changed: Vec::new(),
});
}
let mut output_lines: Vec<&str> = Vec::new();
let mut in_predictor = false;
for line in toml_src.lines() {
let trimmed = line.trim();
if trimmed == "[memory.compression.predictor]"
|| trimmed == "# [memory.compression.predictor]"
{
in_predictor = true;
continue;
}
if in_predictor && trimmed.starts_with('[') && !trimmed.starts_with("# [") {
in_predictor = false;
}
if !in_predictor {
output_lines.push(line);
}
}
let mut output = output_lines.join("\n");
if toml_src.ends_with('\n') {
output.push('\n');
}
Ok(MigrationResult {
output,
changed_count: 1,
sections_changed: vec!["memory.compression.predictor".to_owned()],
})
}
pub fn migrate_microcompact_config(toml_src: &str) -> Result<MigrationResult, MigrateError> {
if section_header_present(toml_src, "memory.microcompact")
|| toml_src.contains("# [memory.microcompact]")
{
return Ok(MigrationResult {
output: toml_src.to_owned(),
changed_count: 0,
sections_changed: Vec::new(),
});
}
let doc = toml_src.parse::<toml_edit::DocumentMut>()?;
if !doc.contains_key("memory") {
return Ok(MigrationResult {
output: toml_src.to_owned(),
changed_count: 0,
sections_changed: Vec::new(),
});
}
let comment = "\n# Time-based microcompact (#2699). Strips stale low-value tool outputs after idle.\n\
# [memory.microcompact]\n\
# enabled = false\n\
# gap_threshold_minutes = 60 # idle gap before clearing stale outputs\n\
# keep_recent = 3 # always keep this many recent outputs intact\n";
let raw = doc.to_string();
let output = format!("{raw}{comment}");
Ok(MigrationResult {
output,
changed_count: 1,
sections_changed: vec!["memory.microcompact".to_owned()],
})
}
pub fn migrate_autodream_config(toml_src: &str) -> Result<MigrationResult, MigrateError> {
if section_header_present(toml_src, "memory.autodream")
|| toml_src.contains("# [memory.autodream]")
{
return Ok(MigrationResult {
output: toml_src.to_owned(),
changed_count: 0,
sections_changed: Vec::new(),
});
}
let doc = toml_src.parse::<toml_edit::DocumentMut>()?;
if !doc.contains_key("memory") {
return Ok(MigrationResult {
output: toml_src.to_owned(),
changed_count: 0,
sections_changed: Vec::new(),
});
}
let comment = "\n# autoDream background memory consolidation (#2697). Disabled by default.\n\
# [memory.autodream]\n\
# enabled = false\n\
# min_sessions = 5 # sessions since last consolidation\n\
# min_hours = 8 # hours since last consolidation\n\
# consolidation_provider = \"\" # provider name from [[llm.providers]]; empty = primary\n\
# max_iterations = 5\n";
let raw = doc.to_string();
let output = format!("{raw}{comment}");
Ok(MigrationResult {
output,
changed_count: 1,
sections_changed: vec!["memory.autodream".to_owned()],
})
}
pub fn migrate_magic_docs_config(toml_src: &str) -> Result<MigrationResult, MigrateError> {
use toml_edit::{Item, Table};
let mut doc = toml_src.parse::<toml_edit::DocumentMut>()?;
let commented_present = toml_src.lines().any(|l| l.trim() == "# [magic_docs]");
if doc.contains_key("magic_docs") || commented_present {
return Ok(MigrationResult {
output: toml_src.to_owned(),
changed_count: 0,
sections_changed: Vec::new(),
});
}
doc.insert("magic_docs", Item::Table(Table::new()));
let comment = "# MagicDocs auto-maintained markdown (#2702). Disabled by default.\n\
# [magic_docs]\n\
# enabled = false\n\
# min_turns_between_updates = 10\n\
# update_provider = \"\" # provider name from [[llm.providers]]; empty = primary\n\
# max_iterations = 3\n";
doc.remove("magic_docs");
let raw = doc.to_string();
let output = format!("{raw}\n{comment}");
Ok(MigrationResult {
output,
changed_count: 1,
sections_changed: vec!["magic_docs".to_owned()],
})
}
pub fn migrate_orchestration_persistence(toml_src: &str) -> Result<MigrationResult, MigrateError> {
if toml_src.contains("persistence_enabled") {
return Ok(MigrationResult {
output: toml_src.to_owned(),
changed_count: 0,
sections_changed: Vec::new(),
});
}
if !section_header_present(toml_src, "orchestration") {
return Ok(MigrationResult {
output: toml_src.to_owned(),
changed_count: 0,
sections_changed: Vec::new(),
});
}
let comment = "\n# persistence_enabled = true \
# persist task graphs to SQLite after each tick; enables `/plan resume <id>` (#3107)\n";
let output = insert_after_section(toml_src, "orchestration", comment);
if output == toml_src {
return Ok(MigrationResult {
output: toml_src.to_owned(),
changed_count: 0,
sections_changed: Vec::new(),
});
}
Ok(MigrationResult {
output,
changed_count: 1,
sections_changed: vec!["orchestration.persistence_enabled".to_owned()],
})
}
pub fn migrate_goals_config(toml_src: &str) -> Result<MigrationResult, MigrateError> {
if section_header_present(toml_src, "goals") || toml_src.contains("# [goals]") {
return Ok(MigrationResult {
output: toml_src.to_owned(),
changed_count: 0,
sections_changed: Vec::new(),
});
}
let comment = "\n# Long-horizon goal lifecycle tracking (#3567).\n\
# [goals]\n\
# enabled = false\n\
# inject_into_system_prompt = true\n\
# max_text_chars = 2000\n\
# max_history = 50\n";
Ok(MigrationResult {
output: format!("{toml_src}{comment}"),
changed_count: 1,
sections_changed: vec!["goals".to_owned()],
})
}
pub fn migrate_caveman_config(toml_src: &str) -> Result<MigrationResult, MigrateError> {
if section_header_present(toml_src, "caveman") || toml_src.contains("# [caveman]") {
return Ok(MigrationResult {
output: toml_src.to_owned(),
changed_count: 0,
sections_changed: Vec::new(),
});
}
let comment = "\n# [caveman] — ultra-compressed telegraphic output mode (#4985).\n\
# Toggle at runtime with /caveman [on|off] or via the bundled caveman skill.\n\
# [caveman]\n\
# default_on = false\n";
let output = format!("{toml_src}{comment}");
Ok(MigrationResult {
output,
changed_count: 1,
sections_changed: vec!["caveman".to_owned()],
})
}
pub fn migrate_deep_link_config(toml_src: &str) -> Result<MigrationResult, MigrateError> {
if section_header_present(toml_src, "deep_link") || toml_src.contains("# [deep_link]") {
return Ok(MigrationResult {
output: toml_src.to_owned(),
changed_count: 0,
sections_changed: Vec::new(),
});
}
let comment = "\n# [deep_link] — zeph:// URI scheme configuration (spec-066, #5011).\n\
# Requires the `deep-link` Cargo feature to be active.\n\
# [deep_link]\n\
# confirm_before_prompt = true # require y/N before injecting prompt (secure default)\n\
# allowed_cwd_roots = [] # restrict cwd to these prefixes; empty = any non-denylisted path\n\
# prefer_acp = \"never\" # v1 only: \"never\"; \"auto\"/\"always\" reserved for v2\n";
let output = format!("{toml_src}{comment}");
Ok(MigrationResult {
output,
changed_count: 1,
sections_changed: vec!["deep_link".to_owned()],
})
}
pub fn migrate_five_signal_config(toml_src: &str) -> Result<MigrationResult, MigrateError> {
if section_header_present(toml_src, "memory.five_signal")
|| toml_src.contains("# [memory.five_signal]")
{
return Ok(MigrationResult {
output: toml_src.to_owned(),
changed_count: 0,
sections_changed: Vec::new(),
});
}
let doc = toml_src.parse::<toml_edit::DocumentMut>()?;
if !doc.contains_key("memory") {
return Ok(MigrationResult {
output: toml_src.to_owned(),
changed_count: 0,
sections_changed: Vec::new(),
});
}
let comment = "\n# Five-signal SYNAPSE retrieval (#4374). Disabled by default.\n\
# [memory.five_signal]\n\
# enabled = false\n\
# w_recency = 0.35\n\
# w_relevance = 0.35\n\
# w_frequency = 0.15\n\
# w_causal = 0.10\n\
# w_novelty = 0.05\n\
# causal_bfs_max_depth = 10\n\
# neutral_causal_distance = 5\n\
# novelty_decay_rate = 0.1\n\
#\n\
# [memory.five_signal.consolidation_daemon]\n\
# enabled = false\n\
# interval_seconds = 7200\n\
# batch_size = 500\n\
# promotion_score_threshold = 0.70\n\
# demotion_score_threshold = 0.20\n\
# top_k_per_run = 500\n";
let raw = doc.to_string();
let output = format!("{raw}{comment}");
Ok(MigrationResult {
output,
changed_count: 1,
sections_changed: vec!["memory.five_signal".to_owned()],
})
}
pub fn migrate_knowledge_config(toml_src: &str) -> Result<MigrationResult, MigrateError> {
if section_header_present(toml_src, "knowledge") || toml_src.contains("# [knowledge]") {
return Ok(MigrationResult {
output: toml_src.to_owned(),
changed_count: 0,
sections_changed: Vec::new(),
});
}
let comment = "\n# Knowledge-ingest subsystem (spec-067, #5017). All defaults shown.\n\
# [knowledge]\n\
# ingest_provider = \"\" # provider from [[llm.providers]]; empty = primary (Phase 2 graph)\n\
# concurrency = 3 # max parallel extract tasks (Phase 2)\n\
# max_documents = 0 # 0 = unlimited; CLI --max-documents overrides\n\
# recall_include_imported = true # include imported rows in semantic recall\n\
# transcript_scope = \"current-project\" # INV-6: only current-project supported in Phase 1\n";
let doc = toml_src.parse::<toml_edit::DocumentMut>()?;
let raw = doc.to_string();
let output = format!("{raw}{comment}");
Ok(MigrationResult {
output,
changed_count: 1,
sections_changed: vec!["knowledge".to_owned()],
})
}
static TUI_THEME_HEADER_RE: std::sync::LazyLock<Regex> = std::sync::LazyLock::new(|| {
Regex::new(r"(?m)^[ \t]*\[tui\.theme\][ \t]*(?:#[^\r\n]*)?\r?\n").expect("static pattern")
});
pub fn migrate_tui_theme_defaults(toml_src: &str) -> Result<MigrationResult, MigrateError> {
let key_in_tui_theme = |key: &str| {
let mut in_tui_theme = false;
toml_src.lines().any(|l| {
let t = l.trim();
if !t.starts_with('#') && t.starts_with('[') {
in_tui_theme = t == "[tui.theme]";
return false;
}
if !in_tui_theme {
return false;
}
let body = t.trim_start_matches('#').trim();
let lhs = body.split('=').next().unwrap_or("").trim();
lhs == key
})
};
let has_name = key_in_tui_theme("name");
let has_color_mode = key_in_tui_theme("color_mode");
let has_section = section_header_present(toml_src, "tui.theme");
if !has_section || (has_name && has_color_mode) {
return Ok(MigrationResult {
output: toml_src.to_owned(),
changed_count: 0,
sections_changed: Vec::new(),
});
}
let owned;
let src = if toml_src.ends_with('\n') {
toml_src
} else {
owned = format!("{toml_src}\n");
&owned
};
if !TUI_THEME_HEADER_RE.is_match(src) {
return Ok(MigrationResult {
output: toml_src.to_owned(),
changed_count: 0,
sections_changed: Vec::new(),
});
}
let mut insert = String::new();
if !has_name {
insert
.push_str("name = \"zephyr\" # built-in preset; see /theme for alternatives\n");
}
if !has_color_mode {
insert.push_str("color_mode = \"auto\" # auto | truecolor | ansi256 | ansi16 | never\n");
}
let output = TUI_THEME_HEADER_RE
.replacen(src, 1, |caps: ®ex::Captures| {
format!("{}{insert}", &caps[0])
})
.into_owned();
let changed = output != toml_src;
let changed_count = usize::from(changed);
Ok(MigrationResult {
output,
changed_count,
sections_changed: if changed {
vec!["tui.theme".to_owned()]
} else {
Vec::new()
},
})
}
pub fn migrate_tui_theme_config(toml_src: &str) -> Result<MigrationResult, MigrateError> {
let in_tui_section = {
let mut in_section = false;
toml_src.lines().any(|l| {
let t = l.trim();
if !t.starts_with('#') && t.starts_with('[') && !t.starts_with("[[") {
in_section = t == "[tui]";
return false;
}
if t.starts_with('#') {
let inner = t.trim_start_matches('#').trim();
if inner.starts_with('[') {
in_section = false;
return false;
}
return in_section && (inner == "[tui.theme]" || inner.starts_with("tui.theme"));
}
in_section && (t == "[tui.theme]" || t.starts_with("tui.theme"))
})
};
if in_tui_section
|| section_header_present(toml_src, "tui.theme")
|| toml_src.lines().any(|l| l.trim() == "# [tui.theme]")
{
return Ok(MigrationResult {
output: toml_src.to_owned(),
changed_count: 0,
sections_changed: Vec::new(),
});
}
let doc = toml_src.parse::<toml_edit::DocumentMut>()?;
let raw = doc.to_string();
let comment = "\n# [tui.theme] — TUI visual theme (Theme System 2.0, #5087).\n\
# name sets the colour palette. Built-in presets: classic, zephyr, zephyr-light,\n\
# high-contrast, catppuccin-mocha, gruvbox-dark, solarized-dark.\n\
# Custom palettes: drop a TOML file in ~/.config/zeph/themes/<name>.toml.\n\
# [tui.theme]\n\
# name = \"zephyr\" # default: zephyr (new default since 2.0; use \"classic\" for legacy look)\n\
# color_mode = \"auto\" # auto | truecolor | ansi256 | ansi16 | never\n";
let output = format!("{raw}{comment}");
Ok(MigrationResult {
output,
changed_count: 1,
sections_changed: vec!["tui.theme".to_owned()],
})
}
pub fn migrate_orchestration_asset_sensitivity(
toml_src: &str,
) -> Result<MigrationResult, MigrateError> {
if toml_src.contains("default_asset_sensitivity") {
return Ok(MigrationResult {
output: toml_src.to_owned(),
changed_count: 0,
sections_changed: Vec::new(),
});
}
if !section_header_present(toml_src, "orchestration") {
return Ok(MigrationResult {
output: toml_src.to_owned(),
changed_count: 0,
sections_changed: Vec::new(),
});
}
let comment = "\n# default_asset_sensitivity = \"public\" \
# advisory asset sensitivity: public | internal | confidential (spec-068, #3934)\n";
let output = insert_after_section(toml_src, "orchestration", comment);
if output == toml_src {
return Ok(MigrationResult {
output: toml_src.to_owned(),
changed_count: 0,
sections_changed: Vec::new(),
});
}
Ok(MigrationResult {
output,
changed_count: 1,
sections_changed: vec!["orchestration.default_asset_sensitivity".to_owned()],
})
}
pub fn migrate_orchestration_idle_timeout(toml_src: &str) -> Result<MigrationResult, MigrateError> {
if toml_src.contains("default_idle_timeout_secs") {
return Ok(MigrationResult {
output: toml_src.to_owned(),
changed_count: 0,
sections_changed: Vec::new(),
});
}
if !section_header_present(toml_src, "orchestration") {
return Ok(MigrationResult {
output: toml_src.to_owned(),
changed_count: 0,
sections_changed: Vec::new(),
});
}
let comment = "\n# default_idle_timeout_secs = 60 \
# kill a task if it emits no progress for this many seconds; must be set above the \
# longest expected single-turn duration (spec-075-orchestration-node-control-parity, #6021)\n";
let output = insert_after_section(toml_src, "orchestration", comment);
if output == toml_src {
return Ok(MigrationResult {
output: toml_src.to_owned(),
changed_count: 0,
sections_changed: Vec::new(),
});
}
Ok(MigrationResult {
output,
changed_count: 1,
sections_changed: vec!["orchestration.default_idle_timeout_secs".to_owned()],
})
}
pub fn migrate_orchestration_ensemble(toml_src: &str) -> Result<MigrationResult, MigrateError> {
if section_header_present(toml_src, "orchestration.ensemble")
|| toml_src.contains("[orchestration.ensemble]")
{
return Ok(MigrationResult {
output: toml_src.to_owned(),
changed_count: 0,
sections_changed: Vec::new(),
});
}
let doc = toml_src.parse::<toml_edit::DocumentMut>()?;
if !doc.contains_key("orchestration") {
return Ok(MigrationResult {
output: toml_src.to_owned(),
changed_count: 0,
sections_changed: Vec::new(),
});
}
let comment = "\n# ORCH-style deterministic verifier ensemble-merge — off by default (spec 073, #6232)\n\
# [orchestration.ensemble]\n\
# enabled = false\n\
# verify = false\n\
# members = [] # odd length, >= 3, no duplicates, from [[llm.providers]]\n\
# ema_alpha = 0.3\n\
# ema_decay = 0.95\n\
# min_observations = 5\n\
# member_timeout_secs = 0 # 0 = fall back to verifier_timeout_secs\n";
let raw = doc.to_string();
let output = format!("{raw}{comment}");
Ok(MigrationResult {
output,
changed_count: 1,
sections_changed: vec!["orchestration.ensemble".to_owned()],
})
}
pub fn migrate_orchestration_command_config(
toml_src: &str,
) -> Result<MigrationResult, MigrateError> {
if section_header_present(toml_src, "orchestration.command")
|| toml_src.contains("[orchestration.command]")
{
return Ok(MigrationResult {
output: toml_src.to_owned(),
changed_count: 0,
sections_changed: Vec::new(),
});
}
let doc = toml_src.parse::<toml_edit::DocumentMut>()?;
if !doc.contains_key("orchestration") {
return Ok(MigrationResult {
output: toml_src.to_owned(),
changed_count: 0,
sections_changed: Vec::new(),
});
}
let comment = "\n# Command-style dynamic task handoff — lets a node's agent route \
execution to a\n\
# named already-planned node at runtime and write into the cross-thread store's \
shared\n\
# state channel. Off by default (spec-080, #6363).\n\
# [orchestration.command]\n\
# enabled = false\n\
# max_handoffs = 16 # per-graph livelock budget, must be > 0\n";
let raw = doc.to_string();
let output = format!("{raw}{comment}");
Ok(MigrationResult {
output,
changed_count: 1,
sections_changed: vec!["orchestration.command".to_owned()],
})
}
pub fn migrate_orchestration_whole_plan_verifier_timeout(
toml_src: &str,
) -> Result<MigrationResult, MigrateError> {
if toml_src.contains("whole_plan_verifier_timeout_secs") {
return Ok(MigrationResult {
output: toml_src.to_owned(),
changed_count: 0,
sections_changed: Vec::new(),
});
}
if !section_header_present(toml_src, "orchestration") {
return Ok(MigrationResult {
output: toml_src.to_owned(),
changed_count: 0,
sections_changed: Vec::new(),
});
}
let comment = "\n# Timeout in seconds for the whole-plan verify_plan() LLM call; 0 = fall \
back to verifier_timeout_secs. Default: 0 (#6379)\n\
# whole_plan_verifier_timeout_secs = 0\n";
let output = insert_after_section(toml_src, "orchestration", comment);
if output == toml_src {
return Ok(MigrationResult {
output: toml_src.to_owned(),
changed_count: 0,
sections_changed: Vec::new(),
});
}
Ok(MigrationResult {
output,
changed_count: 1,
sections_changed: vec!["orchestration.whole_plan_verifier_timeout_secs".to_owned()],
})
}
pub fn migrate_skills_registry(toml_src: &str) -> Result<MigrationResult, MigrateError> {
let commented_present = toml_src.lines().any(|l| l.trim() == "# [skills.registry]");
if section_header_present(toml_src, "skills.registry") || commented_present {
return Ok(MigrationResult {
output: toml_src.to_owned(),
changed_count: 0,
sections_changed: Vec::new(),
});
}
let doc = toml_src.parse::<toml_edit::DocumentMut>()?;
let raw = doc.to_string();
let comment = "\n# External skill/plugin registry discovery (spec-045, #5869). Off by\n\
# default — no network call is made to any registry unless explicitly opted in. See\n\
# `zeph skill search --help` / `zeph plugin search --help`.\n\
# [skills.registry]\n\
# enabled = false\n\
# backend_kind = \"skills-sh\"\n\
# backend_url = \"https://www.skills.sh\"\n\
# auth_vault_key = \"ZEPH_SKILL_REGISTRY_TOKEN\" # set via `zeph vault set <key> <token>`\n\
# registry_timeout_secs = 30\n";
let output = format!("{raw}{comment}");
Ok(MigrationResult {
output,
changed_count: 1,
sections_changed: vec!["skills.registry".to_owned()],
})
}
static SKILLS_TRUST_HEADER_RE: std::sync::LazyLock<Regex> = std::sync::LazyLock::new(|| {
Regex::new(r"(?m)^[ \t]*\[skills\.trust\][ \t]*(?:#[^\r\n]*)?\r?\n").expect("static pattern")
});
pub fn migrate_skill_trust_require_check(toml_src: &str) -> Result<MigrationResult, MigrateError> {
if !section_header_present(toml_src, "skills.trust") {
return Ok(MigrationResult {
output: toml_src.to_owned(),
changed_count: 0,
sections_changed: Vec::new(),
});
}
let already_present = toml_src.lines().any(|l| {
l.trim()
.trim_start_matches('#')
.trim()
.starts_with("require_integrity_check_on_promote")
});
if already_present || !SKILLS_TRUST_HEADER_RE.is_match(toml_src) {
return Ok(MigrationResult {
output: toml_src.to_owned(),
changed_count: 0,
sections_changed: Vec::new(),
});
}
let comment = "# require_integrity_check_on_promote = true # arm per-invocation blake3 \
re-check on promotion to trusted/verified; override with --no-require-check (#6087)\n";
let output = SKILLS_TRUST_HEADER_RE
.replacen(toml_src, 1, |caps: ®ex::Captures| {
format!("{}{comment}", &caps[0])
})
.into_owned();
let changed = output != toml_src;
let changed_count = usize::from(changed);
Ok(MigrationResult {
output,
changed_count,
sections_changed: if changed {
vec!["skills.trust.require_integrity_check_on_promote".to_owned()]
} else {
Vec::new()
},
})
}
pub fn migrate_rate_limit_advisory(toml_src: &str) -> Result<MigrationResult, MigrateError> {
if section_header_present(toml_src, "security.rate_limit")
|| toml_src.contains("# [security.rate_limit]")
{
return Ok(MigrationResult {
output: toml_src.to_owned(),
changed_count: 0,
sections_changed: Vec::new(),
});
}
let comment = "\n# [security.rate_limit] — per-category sliding-window tool rate limiter with \
circuit breaker. Enabled by default; values below match the built-in defaults (issue #6469).\n\
# [security.rate_limit]\n\
# enabled = true\n\
# shell_calls_per_minute = 30\n\
# web_calls_per_minute = 20\n\
# memory_calls_per_minute = 60\n\
# mcp_calls_per_minute = 40\n\
# other_calls_per_minute = 60\n\
# circuit_breaker_cooldown_secs = 30\n";
let output = format!("{toml_src}{comment}");
Ok(MigrationResult {
output,
changed_count: 1,
sections_changed: vec!["security.rate_limit".to_owned()],
})
}
#[cfg(test)]
mod rate_limit_advisory_tests {
use super::*;
#[test]
fn migrate_rate_limit_advisory_appends_block() {
let base = "[agent]\nname = \"zeph\"\n";
let result = migrate_rate_limit_advisory(base).unwrap();
assert_eq!(result.changed_count, 1);
assert!(result.output.contains("# [security.rate_limit]"));
assert!(result.output.contains("# enabled = true"));
assert!(result.output.contains("# shell_calls_per_minute = 30"));
}
#[test]
fn migrate_rate_limit_advisory_idempotent_on_commented_output() {
let base = "[agent]\nname = \"zeph\"\n";
let first = migrate_rate_limit_advisory(base).unwrap();
let second = migrate_rate_limit_advisory(&first.output).unwrap();
assert_eq!(second.changed_count, 0, "second run must not double-append");
assert_eq!(second.output, first.output);
}
#[test]
fn migrate_rate_limit_advisory_noop_when_active_section_present() {
let base = "[security.rate_limit]\nenabled = false\n";
let result = migrate_rate_limit_advisory(base).unwrap();
assert_eq!(result.changed_count, 0);
assert_eq!(result.output, base);
}
}
pub fn migrate_telegram_expandable_blockquote_config(
toml_src: &str,
) -> Result<MigrationResult, MigrateError> {
if toml_src.contains("expandable_blockquote_min_lines") {
return Ok(MigrationResult {
output: toml_src.to_owned(),
changed_count: 0,
sections_changed: Vec::new(),
});
}
if !section_header_present(toml_src, "telegram") {
return Ok(MigrationResult {
output: toml_src.to_owned(),
changed_count: 0,
sections_changed: Vec::new(),
});
}
let comment = "\n# expandable_blockquote_min_lines = 10 \
# blockquotes with this many lines or more render as an expandable \
# (collapsed-by-default) blockquote (Bot API 10.1); 0 disables the expandable form \
# entirely (spec 007-3-telegram-rich-text, issue #6541)\n";
let output = insert_after_section(toml_src, "telegram", comment);
if output == toml_src {
return Ok(MigrationResult {
output: toml_src.to_owned(),
changed_count: 0,
sections_changed: Vec::new(),
});
}
Ok(MigrationResult {
output,
changed_count: 1,
sections_changed: vec!["telegram.expandable_blockquote_min_lines".to_owned()],
})
}
#[cfg(test)]
mod telegram_expandable_blockquote_tests {
use super::*;
#[test]
fn migrate_telegram_expandable_blockquote_appends_advisory_comment() {
let base = "[telegram]\ntoken = \"tok\"\nallowed_users = [\"alice\"]\n";
let result = migrate_telegram_expandable_blockquote_config(base).unwrap();
assert_eq!(result.changed_count, 1);
assert!(
result
.output
.contains("# expandable_blockquote_min_lines = 10")
);
}
#[test]
fn migrate_telegram_expandable_blockquote_noop_without_telegram_section() {
let base = "[agent]\nname = \"zeph\"\n";
let result = migrate_telegram_expandable_blockquote_config(base).unwrap();
assert_eq!(result.changed_count, 0);
assert_eq!(result.output, base);
}
#[test]
fn migrate_telegram_expandable_blockquote_noop_when_key_already_present() {
let base = "[telegram]\ntoken = \"tok\"\nexpandable_blockquote_min_lines = 5\n";
let result = migrate_telegram_expandable_blockquote_config(base).unwrap();
assert_eq!(result.changed_count, 0);
assert_eq!(result.output, base);
}
#[test]
fn migrate_telegram_expandable_blockquote_idempotent() {
let base = "[telegram]\ntoken = \"tok\"\n";
let first = migrate_telegram_expandable_blockquote_config(base).unwrap();
let second = migrate_telegram_expandable_blockquote_config(&first.output).unwrap();
assert_eq!(second.changed_count, 0, "second run must not double-append");
assert_eq!(second.output, first.output);
}
}