use std::collections::HashMap;
use serde::Deserialize;
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub(super) struct ConfigFile {
pub(super) agent: Option<AgentSection>,
#[serde(default)]
pub(super) agent_roles: Vec<AgentRoleRow>,
#[serde(default)]
pub(super) scheduled_agent_task: Vec<ScheduledAgentTaskRow>,
#[serde(default)]
pub(super) tool_registry: Option<ToolRegistrySection>,
}
#[derive(Debug, Deserialize, Clone)]
#[serde(deny_unknown_fields)]
pub(super) struct ScheduledAgentTaskRow {
pub(super) id: String,
pub(super) schedule: String,
pub(super) message: String,
#[serde(default = "scheduled_agent_task_enabled_default")]
pub(super) enabled: bool,
pub(super) conversation_id: Option<String>,
#[serde(default)]
pub(super) new_conversation: bool,
pub(super) agent_role: Option<String>,
}
fn scheduled_agent_task_enabled_default() -> bool {
true
}
#[derive(Debug, Deserialize, Clone, Default)]
#[serde(deny_unknown_fields)]
pub(super) struct ToolRegistrySection {
#[serde(default)]
pub(super) http_fetch_wall_timeout_secs: Option<u64>,
#[serde(default)]
pub(super) http_request_wall_timeout_secs: Option<u64>,
#[serde(default)]
pub(super) parallel_wall_timeout_secs: HashMap<String, u64>,
pub(super) parallel_sync_denied_tools: Option<Vec<String>>,
pub(super) parallel_sync_denied_prefixes: Option<Vec<String>>,
pub(super) sync_default_inline_tools: Option<Vec<String>>,
pub(super) write_effect_tools: Option<Vec<String>>,
#[serde(default)]
pub(super) sub_agent_patch_write_extra_tools: Option<Vec<String>>,
#[serde(default)]
pub(super) sub_agent_test_runner_extra_tools: Option<Vec<String>>,
#[serde(default)]
pub(super) sub_agent_review_readonly_deny_tools: Option<Vec<String>>,
}
#[derive(Debug, Deserialize, Clone)]
#[serde(deny_unknown_fields)]
pub(super) struct AgentRoleRow {
pub(super) id: String,
pub(super) system_prompt: Option<String>,
pub(super) system_prompt_file: Option<String>,
#[serde(default)]
pub(super) allowed_tools: Option<Vec<String>>,
#[serde(default)]
pub(super) prepend_coding_workbench: Option<bool>,
#[serde(default)]
pub(super) default_session_mode: Option<String>,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub(super) struct AgentSection {
pub(super) api_base: Option<String>,
pub(super) model: Option<String>,
pub(super) planner_model: Option<String>,
pub(super) executor_model: Option<String>,
pub(super) llm_http_auth_mode: Option<String>,
pub(super) max_message_history: Option<u64>,
pub(super) command_timeout_secs: Option<u64>,
pub(super) command_max_output_len: Option<u64>,
pub(super) allowed_commands: Option<Vec<String>>,
pub(super) run_command_working_dir: Option<String>,
pub(super) allow_external_path_with_approval: Option<bool>,
pub(super) max_tokens: Option<u64>,
pub(super) llm_context_tokens: Option<u64>,
pub(super) temperature: Option<f64>,
pub(super) llm_seed: Option<i64>,
pub(super) llm_reasoning_split: Option<bool>,
pub(super) llm_bigmodel_thinking: Option<bool>,
pub(super) llm_kimi_thinking_disabled: Option<bool>,
pub(super) api_timeout_secs: Option<u64>,
pub(super) api_max_retries: Option<u64>,
pub(super) api_retry_delay_secs: Option<u64>,
pub(super) weather_timeout_secs: Option<u64>,
pub(super) web_search_provider: Option<String>,
pub(super) web_search_api_key: Option<String>,
pub(super) web_search_timeout_secs: Option<u64>,
pub(super) web_search_max_results: Option<u64>,
pub(super) http_fetch_allowed_prefixes: Option<Vec<String>>,
pub(super) http_fetch_timeout_secs: Option<u64>,
pub(super) http_fetch_max_response_bytes: Option<u64>,
pub(super) reflection_default_max_rounds: Option<u64>,
pub(super) final_plan_requirement: Option<String>,
pub(super) plan_rewrite_max_attempts: Option<u64>,
pub(super) final_plan_require_strict_workflow_node_coverage: Option<bool>,
pub(super) final_plan_semantic_check_enabled: Option<bool>,
pub(super) final_plan_semantic_check_accept_legacy_text: Option<bool>,
pub(super) final_plan_semantic_check_max_non_readonly_tools: Option<u64>,
pub(super) final_plan_semantic_check_max_tokens: Option<u64>,
pub(super) planner_executor_mode: Option<String>,
pub(super) system_prompt: Option<String>,
pub(super) system_prompt_file: Option<String>,
pub(super) default_agent_role: Option<String>,
pub(super) coding_workbench_enabled: Option<bool>,
pub(super) coding_workbench_increment_file: Option<String>,
pub(super) default_session_mode: Option<String>,
pub(super) cursor_rules_enabled: Option<bool>,
pub(super) cursor_rules_dir: Option<String>,
pub(super) cursor_rules_include_agents_md: Option<bool>,
pub(super) cursor_rules_max_chars: Option<u64>,
pub(super) skills_enabled: Option<bool>,
pub(super) skills_dir: Option<String>,
pub(super) skills_user_dir: Option<String>,
pub(super) skills_system_dir: Option<String>,
pub(super) skills_max_chars: Option<u64>,
pub(super) skills_top_k: Option<u64>,
pub(super) tool_message_max_chars: Option<u64>,
pub(super) tool_result_envelope_v1: Option<bool>,
pub(super) sse_tool_call_include_arguments: Option<bool>,
pub(super) agent_tool_stats_enabled: Option<bool>,
pub(super) agent_tool_stats_window_events: Option<u64>,
pub(super) agent_tool_stats_min_samples: Option<u64>,
pub(super) agent_tool_stats_max_chars: Option<u64>,
pub(super) agent_tool_stats_warn_below_success_ratio: Option<f64>,
pub(super) thinking_avoid_echo_system_prompt: Option<bool>,
pub(super) thinking_avoid_echo_appendix: Option<String>,
pub(super) thinking_avoid_echo_appendix_file: Option<String>,
pub(super) context_char_budget: Option<u64>,
pub(super) context_min_messages_after_system: Option<u64>,
pub(super) context_summary_trigger_chars: Option<u64>,
pub(super) context_summary_tail_messages: Option<u64>,
pub(super) context_summary_max_tokens: Option<u64>,
pub(super) context_summary_transcript_max_chars: Option<u64>,
pub(super) context_summary_system_file: Option<String>,
pub(super) context_summary_user_file: Option<String>,
pub(super) health_llm_models_probe: Option<bool>,
pub(super) health_llm_models_probe_cache_secs: Option<u64>,
pub(super) chat_queue_max_concurrent: Option<u64>,
pub(super) chat_queue_max_pending: Option<u64>,
pub(super) parallel_readonly_tools_max: Option<u64>,
pub(super) read_file_turn_cache_max_entries: Option<u64>,
pub(super) readonly_tool_ttl_cache_secs: Option<u64>,
pub(super) readonly_tool_ttl_cache_max_entries: Option<u64>,
pub(super) test_result_cache_enabled: Option<bool>,
pub(super) test_result_cache_max_entries: Option<u64>,
pub(super) session_workspace_changelist_enabled: Option<bool>,
pub(super) session_workspace_changelist_max_chars: Option<u64>,
pub(super) sync_default_tool_sandbox_mode: Option<String>,
pub(super) sync_default_tool_sandbox_docker_image: Option<String>,
pub(super) sync_default_tool_sandbox_docker_network: Option<String>,
pub(super) sync_default_tool_sandbox_docker_timeout_secs: Option<u64>,
pub(super) sync_default_tool_sandbox_docker_user: Option<String>,
pub(super) workspace_allowed_roots: Option<Vec<String>>,
pub(super) web_workspace_pool: Option<String>,
pub(super) web_api_bearer_token: Option<String>,
pub(super) web_api_require_bearer: Option<bool>,
pub(super) web_cors_allowed_origins: Option<Vec<String>>,
pub(super) allow_insecure_no_auth_for_non_loopback: Option<bool>,
pub(super) web_audit_log_write_tools: Option<bool>,
pub(super) web_audit_trust_x_forwarded_for: Option<bool>,
pub(super) conversation_store_sqlite_path: Option<String>,
pub(super) agent_memory_file_enabled: Option<bool>,
pub(super) agent_memory_file: Option<String>,
pub(super) agent_memory_file_max_chars: Option<u64>,
pub(super) living_docs_inject_enabled: Option<bool>,
pub(super) living_docs_relative_dir: Option<String>,
pub(super) living_docs_inject_max_chars: Option<u64>,
pub(super) living_docs_file_max_each_chars: Option<u64>,
pub(super) project_profile_inject_enabled: Option<bool>,
pub(super) project_profile_inject_max_chars: Option<u64>,
pub(super) project_dependency_brief_inject_enabled: Option<bool>,
pub(super) project_dependency_brief_inject_max_chars: Option<u64>,
pub(super) tool_call_explain_enabled: Option<bool>,
pub(super) tool_call_explain_min_chars: Option<u64>,
pub(super) tool_call_explain_max_chars: Option<u64>,
pub(super) long_term_memory_scope_mode: Option<String>,
pub(super) long_term_memory_vector_backend: Option<String>,
pub(super) long_term_memory_enabled: Option<bool>,
pub(super) long_term_memory_max_entries: Option<u64>,
pub(super) long_term_memory_inject_max_chars: Option<u64>,
pub(super) long_term_memory_store_sqlite_path: Option<String>,
pub(super) long_term_memory_top_k: Option<u64>,
pub(super) long_term_memory_max_chars_per_chunk: Option<u64>,
pub(super) long_term_memory_min_chars_to_index: Option<u64>,
pub(super) long_term_memory_async_index: Option<bool>,
pub(super) long_term_memory_auto_index_turns: Option<bool>,
pub(super) long_term_memory_auto_summarize_experience: Option<bool>,
pub(super) long_term_memory_prioritize_experience_recall: Option<bool>,
pub(super) long_term_memory_default_ttl_secs: Option<u64>,
pub(super) mcp_enabled: Option<bool>,
pub(super) mcp_command: Option<String>,
pub(super) mcp_tool_timeout_secs: Option<u64>,
pub(super) codebase_semantic_search_enabled: Option<bool>,
pub(super) codebase_semantic_invalidate_on_workspace_change: Option<bool>,
pub(super) codebase_semantic_index_sqlite_path: Option<String>,
pub(super) codebase_semantic_max_file_bytes: Option<u64>,
pub(super) codebase_semantic_chunk_max_chars: Option<u64>,
pub(super) codebase_semantic_top_k: Option<u64>,
pub(super) codebase_semantic_query_max_chunks: Option<u64>,
pub(super) codebase_semantic_rebuild_max_files: Option<u64>,
pub(super) codebase_semantic_rebuild_incremental: Option<bool>,
pub(super) codebase_semantic_hybrid_alpha: Option<f64>,
pub(super) codebase_semantic_fts_top_n: Option<u64>,
pub(super) codebase_semantic_hybrid_semantic_pool: Option<u64>,
}
pub(super) fn parse_agent_section(s: &str) -> Result<Option<AgentSection>, toml::de::Error> {
Ok(toml::from_str::<ConfigFile>(s)?.agent)
}
pub(super) type ParsedConfigFileRoles = (
Option<AgentSection>,
Vec<AgentRoleRow>,
Option<ToolRegistrySection>,
Vec<ScheduledAgentTaskRow>,
);
pub(super) fn parse_config_file_roles(s: &str) -> Result<ParsedConfigFileRoles, toml::de::Error> {
let f: ConfigFile = toml::from_str(s)?;
Ok((
f.agent,
f.agent_roles,
f.tool_registry,
f.scheduled_agent_task,
))
}
pub(super) fn parse_tools_config_bundle(
s: &str,
) -> Result<(Option<AgentSection>, Option<ToolRegistrySection>), toml::de::Error> {
let f: ConfigFile = toml::from_str(s)?;
Ok((f.agent, f.tool_registry))
}
pub(super) fn parse_bool_like(s: &str) -> Option<bool> {
let v = s.trim().to_ascii_lowercase();
if matches!(v.as_str(), "1" | "true" | "yes" | "on") {
Some(true)
} else if matches!(v.as_str(), "0" | "false" | "no" | "off") {
Some(false)
} else {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_valid_toml_with_agent_section() {
let toml = r#"
[agent]
api_base = "https://api.example.com"
model = "deepseek-chat"
"#;
let result = parse_agent_section(toml).expect("should parse valid TOML");
let agent = result.expect("should have [agent]");
assert_eq!(agent.api_base.as_deref(), Some("https://api.example.com"));
assert_eq!(agent.model.as_deref(), Some("deepseek-chat"));
}
#[test]
fn parse_valid_toml_without_agent_section() {
let toml = "# no tables\n";
let result = parse_agent_section(toml).expect("should parse valid TOML");
assert!(result.is_none(), "no [agent] section should yield None");
}
#[test]
fn parse_rejects_unknown_top_level_table() {
let toml = r#"
[other]
key = "value"
"#;
let err = parse_agent_section(toml).expect_err("unknown top-level table should fail");
assert!(
err.to_string().contains("unknown field") || err.to_string().contains("other"),
"expected unknown field error, got: {err}"
);
}
#[test]
fn parse_empty_toml() {
let result = parse_agent_section("").expect("empty TOML is valid");
assert!(result.is_none());
}
#[test]
fn parse_malformed_toml_returns_error() {
let bad = "[[[ not valid toml !!!";
let result = parse_agent_section(bad);
assert!(result.is_err(), "malformed TOML should return Err");
}
#[test]
fn parse_rejects_unknown_key_in_agent_section() {
let toml = r#"
[agent]
api_base = "https://api.example.com"
model = "m"
typo_unknown_key = 1
"#;
let err = parse_agent_section(toml).expect_err("unknown key should fail");
let msg = err.to_string();
assert!(
msg.contains("unknown field") || msg.contains("unknown"),
"expected serde unknown field error, got: {msg}"
);
}
#[test]
fn parse_rejects_removed_compat_keys_in_agent_section() {
for key in [
"llm_fold_system_into_user = true",
"intent_mode_bias_enabled = false",
"intent_l2_enabled = false",
"intent_l2_min_confidence = 0.7",
"intent_l2_max_tokens = 384",
"intent_execute_low_threshold = 0.2",
"intent_execute_high_threshold = 0.45",
"intent_non_hier_execute_low_threshold = 0.2",
"intent_non_hier_execute_high_threshold = 0.45",
"intent_l0_routing_boost_enabled = true",
"intent_at_turn_start_enabled = false",
"tui_load_session_on_start = false",
"tui_session_max_messages = 400",
"repl_initial_workspace_messages_enabled = false",
] {
let toml = format!(
r#"
[agent]
api_base = "https://api.example.com"
model = "m"
{key}
"#
);
let err = parse_agent_section(&toml)
.expect_err("removed compat key should fail deny_unknown_fields");
let msg = err.to_string();
assert!(
msg.contains("unknown field") || msg.contains("unknown"),
"expected unknown field for `{key}`, got: {msg}"
);
}
}
#[test]
fn parse_bool_like_truthy() {
for s in [
"1", "true", "True", "TRUE", "yes", "YES", "on", "ON", " true ",
] {
assert_eq!(parse_bool_like(s), Some(true), "expected true for {:?}", s);
}
}
#[test]
fn parse_bool_like_falsy() {
for s in [
"0", "false", "False", "FALSE", "no", "NO", "off", "OFF", " false ",
] {
assert_eq!(
parse_bool_like(s),
Some(false),
"expected false for {:?}",
s
);
}
}
#[test]
fn parse_bool_like_invalid() {
assert_eq!(parse_bool_like("maybe"), None);
assert_eq!(parse_bool_like(""), None);
}
}