Skip to main content

a3s_code_core/config/
loader.rs

1use super::provider::{
2    apply_model_caps, ModelConfig, ModelCost, ModelLimit, ModelModalities, ProviderConfig,
3};
4use super::{AutoDelegationConfig, CodeConfig, OsConfig, StorageBackend};
5use crate::error::{CodeError, Result};
6use crate::llm::LlmConfig;
7use crate::mcp::McpServerConfig;
8use crate::memory::MemoryConfig;
9use crate::queue::SessionQueueConfig;
10use crate::task_scheduler::TaskSchedulerConfig;
11use a3s_memory::{PrunePolicy, RelevanceConfig};
12use serde_json::{Map as JsonMap, Value as JsonValue};
13use std::collections::HashMap;
14use std::path::{Path, PathBuf};
15
16// ============================================================================
17// ACL Parsing Helpers
18// ============================================================================
19
20fn acl_attr<'a>(block: &'a a3s_acl::Block, keys: &[&str]) -> Option<&'a a3s_acl::Value> {
21    keys.iter().find_map(|key| block.attributes.get(*key))
22}
23
24fn acl_string(value: &a3s_acl::Value) -> Option<String> {
25    match value {
26        a3s_acl::Value::String(s) => Some(s.clone()),
27        a3s_acl::Value::Call(name, args) if name == "env" => {
28            let var_name = args.first().and_then(acl_string)?;
29            std::env::var(var_name).ok()
30        }
31        _ => None,
32    }
33}
34
35fn acl_string_attr(block: &a3s_acl::Block, keys: &[&str]) -> Option<String> {
36    acl_attr(block, keys).and_then(acl_string)
37}
38
39fn acl_label_or_attr(block: &a3s_acl::Block, keys: &[&str]) -> Option<String> {
40    block
41        .labels
42        .first()
43        .cloned()
44        .or_else(|| acl_string_attr(block, keys))
45}
46
47fn acl_bool_attr(block: &a3s_acl::Block, keys: &[&str]) -> Option<bool> {
48    match acl_attr(block, keys) {
49        Some(a3s_acl::Value::Bool(value)) => Some(*value),
50        _ => None,
51    }
52}
53
54fn acl_usize_attr(block: &a3s_acl::Block, keys: &[&str]) -> Option<usize> {
55    match acl_attr(block, keys) {
56        Some(a3s_acl::Value::Number(value)) if *value >= 0.0 => Some(*value as usize),
57        _ => None,
58    }
59}
60
61fn acl_f32_attr(block: &a3s_acl::Block, keys: &[&str]) -> Option<f32> {
62    match acl_attr(block, keys) {
63        Some(a3s_acl::Value::Number(value)) => Some(*value as f32),
64        _ => None,
65    }
66}
67
68fn parse_auto_delegation_block(
69    block: &a3s_acl::Block,
70    base: &AutoDelegationConfig,
71) -> AutoDelegationConfig {
72    let mut config = base.clone();
73    if let Some(enabled) = acl_bool_attr(block, &["enabled"]) {
74        config.enabled = enabled;
75    }
76    if let Some(auto_parallel) =
77        acl_bool_attr(block, &["auto_parallel", "autoParallel", "parallel"])
78    {
79        config.auto_parallel = auto_parallel;
80    }
81    if let Some(allow_manual_delegation) = acl_bool_attr(
82        block,
83        &[
84            "allow_manual_delegation",
85            "allowManualDelegation",
86            "manual_delegation",
87            "manualDelegation",
88        ],
89    ) {
90        config.allow_manual_delegation = allow_manual_delegation;
91    }
92    if let Some(min_confidence) = acl_f32_attr(block, &["min_confidence", "minConfidence"]) {
93        config.min_confidence = min_confidence.clamp(0.0, 1.0);
94    }
95    if let Some(max_tasks) = acl_usize_attr(block, &["max_tasks", "maxTasks"]) {
96        config.max_tasks = max_tasks.max(1);
97    }
98    config
99}
100
101fn parse_memory_block(block: &a3s_acl::Block, base: Option<&MemoryConfig>) -> MemoryConfig {
102    let mut config = base.cloned().unwrap_or_default();
103
104    if let Some(max_short_term) = acl_usize_attr(block, &["max_short_term", "maxShortTerm"]) {
105        config.max_short_term = max_short_term;
106    }
107    if let Some(max_working) = acl_usize_attr(block, &["max_working", "maxWorking"]) {
108        config.max_working = max_working;
109    }
110    if let Some(prune_interval_secs) =
111        acl_usize_attr(block, &["prune_interval_secs", "pruneIntervalSecs"])
112    {
113        config.prune_interval_secs = prune_interval_secs as u64;
114    }
115    if let Some(llm_extraction) = acl_bool_attr(block, &["llm_extraction", "llmExtraction"]) {
116        config.llm_extraction = llm_extraction;
117    }
118    if let Some(max_items) = acl_usize_attr(
119        block,
120        &["llm_extraction_max_items", "llmExtractionMaxItems"],
121    ) {
122        config.llm_extraction_max_items = max_items;
123    }
124    if let Some(max_input_chars) = acl_usize_attr(
125        block,
126        &[
127            "llm_extraction_max_input_chars",
128            "llmExtractionMaxInputChars",
129        ],
130    ) {
131        config.llm_extraction_max_input_chars = max_input_chars;
132    }
133
134    if let Some(relevance) = acl_attr(block, &["relevance"]) {
135        config.relevance = parse_relevance_value(relevance, &config.relevance);
136    }
137
138    if let Some(prune_policy) = acl_attr(block, &["prune", "prune_policy", "prunePolicy"]) {
139        config.prune_policy = Some(parse_prune_policy_value(
140            prune_policy,
141            config.prune_policy.as_ref(),
142        ));
143    }
144
145    for child in &block.blocks {
146        let value = a3s_acl::Value::Object(
147            child
148                .attributes
149                .iter()
150                .map(|(key, value)| (key.clone(), value.clone()))
151                .collect(),
152        );
153        match child.name.as_str() {
154            "relevance" => {
155                config.relevance = parse_relevance_value(&value, &config.relevance);
156            }
157            "prune" | "prune_policy" | "prunePolicy" => {
158                config.prune_policy = Some(parse_prune_policy_value(
159                    &value,
160                    config.prune_policy.as_ref(),
161                ));
162            }
163            _ => {}
164        }
165    }
166
167    config
168}
169
170fn parse_relevance_value(value: &a3s_acl::Value, base: &RelevanceConfig) -> RelevanceConfig {
171    let mut config = base.clone();
172    if let Some(decay_days) = acl_object_f32_attr(value, &["decay_days", "decayDays"]) {
173        config.decay_days = decay_days.max(0.1);
174    }
175    if let Some(importance_weight) =
176        acl_object_f32_attr(value, &["importance_weight", "importanceWeight"])
177    {
178        config.importance_weight = importance_weight.max(0.0);
179    }
180    if let Some(recency_weight) = acl_object_f32_attr(value, &["recency_weight", "recencyWeight"]) {
181        config.recency_weight = recency_weight.max(0.0);
182    }
183    config
184}
185
186fn parse_prune_policy_value(value: &a3s_acl::Value, base: Option<&PrunePolicy>) -> PrunePolicy {
187    let mut policy = base.cloned().unwrap_or_default();
188    if let Some(max_age_days) = acl_object_u32_attr(value, &["max_age_days", "maxAgeDays"]) {
189        policy.max_age_days = max_age_days;
190    }
191    if let Some(min_importance) =
192        acl_object_f32_attr(value, &["min_importance_to_keep", "minImportanceToKeep"])
193    {
194        policy.min_importance_to_keep = min_importance.clamp(0.0, 1.0);
195    }
196    if let Some(max_items) = acl_object_usize_attr(value, &["max_items", "maxItems"]) {
197        policy.max_items = max_items;
198    }
199    policy
200}
201
202fn acl_object_attr<'a>(value: &'a a3s_acl::Value, keys: &[&str]) -> Option<&'a a3s_acl::Value> {
203    match value {
204        a3s_acl::Value::Object(pairs) => keys.iter().find_map(|key| {
205            pairs
206                .iter()
207                .find_map(|(candidate, value)| (candidate == key).then_some(value))
208        }),
209        _ => None,
210    }
211}
212
213fn acl_f32(value: &a3s_acl::Value) -> Option<f32> {
214    match value {
215        a3s_acl::Value::Number(value) => Some(*value as f32),
216        _ => None,
217    }
218}
219
220fn acl_usize(value: &a3s_acl::Value) -> Option<usize> {
221    match value {
222        a3s_acl::Value::Number(value) if *value >= 0.0 => Some(*value as usize),
223        _ => None,
224    }
225}
226
227fn acl_u32(value: &a3s_acl::Value) -> Option<u32> {
228    match value {
229        a3s_acl::Value::Number(value) if *value >= 0.0 => {
230            Some((*value as usize).min(u32::MAX as usize) as u32)
231        }
232        _ => None,
233    }
234}
235
236fn acl_object_f32_attr(value: &a3s_acl::Value, keys: &[&str]) -> Option<f32> {
237    acl_object_attr(value, keys).and_then(acl_f32)
238}
239
240fn acl_object_usize_attr(value: &a3s_acl::Value, keys: &[&str]) -> Option<usize> {
241    acl_object_attr(value, keys).and_then(acl_usize)
242}
243
244fn acl_object_u32_attr(value: &a3s_acl::Value, keys: &[&str]) -> Option<u32> {
245    acl_object_attr(value, keys).and_then(acl_u32)
246}
247
248fn acl_path_list_attr(block: &a3s_acl::Block, keys: &[&str]) -> Option<Vec<PathBuf>> {
249    let value = acl_attr(block, keys)?;
250    match value {
251        a3s_acl::Value::List(items) => Some(
252            items
253                .iter()
254                .filter_map(acl_string)
255                .map(PathBuf::from)
256                .collect(),
257        ),
258        _ => acl_string(value).map(|s| vec![PathBuf::from(s)]),
259    }
260}
261
262fn acl_string_list_attr(block: &a3s_acl::Block, keys: &[&str]) -> Option<Vec<String>> {
263    let value = acl_attr(block, keys)?;
264    match value {
265        a3s_acl::Value::List(items) => Some(items.iter().filter_map(acl_string).collect()),
266        _ => acl_string(value).map(|value| vec![value]),
267    }
268}
269
270fn snake_to_camel(value: &str) -> String {
271    let mut output = String::with_capacity(value.len());
272    let mut uppercase_next = false;
273    for ch in value.chars() {
274        if ch == '_' || ch == '-' {
275            uppercase_next = true;
276        } else if uppercase_next {
277            output.extend(ch.to_uppercase());
278            uppercase_next = false;
279        } else {
280            output.push(ch);
281        }
282    }
283    output
284}
285
286fn acl_value_to_json(value: &a3s_acl::Value) -> Option<JsonValue> {
287    match value {
288        a3s_acl::Value::String(value) => Some(JsonValue::String(value.clone())),
289        a3s_acl::Value::Number(value) if value.fract() == 0.0 && *value >= 0.0 => {
290            Some(JsonValue::Number(serde_json::Number::from(*value as u64)))
291        }
292        a3s_acl::Value::Number(value) if value.fract() == 0.0 => {
293            Some(JsonValue::Number(serde_json::Number::from(*value as i64)))
294        }
295        a3s_acl::Value::Number(value) => {
296            serde_json::Number::from_f64(*value).map(JsonValue::Number)
297        }
298        a3s_acl::Value::Bool(value) => Some(JsonValue::Bool(*value)),
299        a3s_acl::Value::List(items) => Some(JsonValue::Array(
300            items.iter().filter_map(acl_value_to_json).collect(),
301        )),
302        a3s_acl::Value::Object(pairs) => {
303            let mut object = JsonMap::new();
304            for (key, value) in pairs {
305                if let Some(value) = acl_value_to_json(value) {
306                    object.insert(key.clone(), value);
307                }
308            }
309            Some(JsonValue::Object(object))
310        }
311        a3s_acl::Value::Null => Some(JsonValue::Null),
312        a3s_acl::Value::Call(name, _) if name == "env" => acl_string(value).map(JsonValue::String),
313        a3s_acl::Value::Call(_, _) => None,
314    }
315}
316
317fn insert_nested_json(object: &mut JsonMap<String, JsonValue>, key: String, value: JsonValue) {
318    match object.remove(&key) {
319        None => {
320            object.insert(key, value);
321        }
322        Some(JsonValue::Array(mut values)) => {
323            values.push(value);
324            object.insert(key, JsonValue::Array(values));
325        }
326        Some(previous) => {
327            object.insert(key, JsonValue::Array(vec![previous, value]));
328        }
329    }
330}
331
332fn acl_block_to_json(block: &a3s_acl::Block) -> JsonValue {
333    let mut object = JsonMap::new();
334    for (key, value) in &block.attributes {
335        if let Some(value) = acl_value_to_json(value) {
336            object.insert(snake_to_camel(key), value);
337        }
338    }
339
340    for child in &block.blocks {
341        let key = snake_to_camel(&child.name);
342        let value = acl_block_to_json(child);
343        if let Some(label) = child.labels.first() {
344            let entry = object
345                .entry(key)
346                .or_insert_with(|| JsonValue::Object(JsonMap::new()));
347            if let JsonValue::Object(entries) = entry {
348                entries.insert(label.clone(), value);
349            }
350        } else {
351            insert_nested_json(&mut object, key, value);
352        }
353    }
354
355    JsonValue::Object(object)
356}
357
358fn normalize_lane_name(value: &str) -> Option<&'static str> {
359    match value.trim().to_ascii_lowercase().as_str() {
360        "control" => Some("Control"),
361        "query" => Some("Query"),
362        "execute" => Some("Execute"),
363        "generate" => Some("Generate"),
364        _ => None,
365    }
366}
367
368fn normalize_lane_map(value: &mut JsonValue, normalize_handler_mode: bool) {
369    let JsonValue::Object(entries) = value else {
370        return;
371    };
372    let previous = std::mem::take(entries);
373    for (name, mut value) in previous {
374        let Some(name) = normalize_lane_name(&name) else {
375            continue;
376        };
377        if normalize_handler_mode {
378            if let JsonValue::Object(handler) = &mut value {
379                rename_json_key(handler, "timeoutMs", "timeout_ms");
380                if let Some(JsonValue::String(mode)) = handler.get_mut("mode") {
381                    *mode = match mode.trim().to_ascii_lowercase().as_str() {
382                        "external" => "External".to_string(),
383                        "hybrid" => "Hybrid".to_string(),
384                        _ => "Internal".to_string(),
385                    };
386                }
387            }
388        }
389        entries.insert(name.to_string(), value);
390    }
391}
392
393fn parse_queue_block(block: &a3s_acl::Block) -> Result<SessionQueueConfig> {
394    let mut value = acl_block_to_json(block);
395    if let Some(lane_handlers) = value.get_mut("laneHandlers") {
396        normalize_lane_map(lane_handlers, true);
397    }
398    if let Some(lane_timeouts) = value.get_mut("laneTimeouts") {
399        normalize_lane_map(lane_timeouts, false);
400    }
401    serde_json::from_value(value)
402        .map_err(|error| CodeError::Config(format!("Invalid queue configuration: {error}")))
403}
404
405fn parse_task_scheduler_block(block: &a3s_acl::Block) -> Result<TaskSchedulerConfig> {
406    let config: TaskSchedulerConfig =
407        serde_json::from_value(acl_block_to_json(block)).map_err(|error| {
408            CodeError::Config(format!("Invalid task scheduler configuration: {error}"))
409        })?;
410    config
411        .validate()
412        .map_err(|error| CodeError::Config(error.to_string()))?;
413    Ok(config)
414}
415
416fn parse_search_block(block: &a3s_acl::Block) -> Result<super::SearchConfig> {
417    serde_json::from_value(acl_block_to_json(block))
418        .map_err(|error| CodeError::Config(format!("Invalid search configuration: {error}")))
419}
420
421fn parse_document_parser_block(block: &a3s_acl::Block) -> Result<super::DocumentParserConfig> {
422    serde_json::from_value(acl_block_to_json(block)).map_err(|error| {
423        CodeError::Config(format!("Invalid document parser configuration: {error}"))
424    })
425}
426
427fn rename_json_key(object: &mut JsonMap<String, JsonValue>, from: &str, to: &str) {
428    if let Some(value) = object.remove(from) {
429        object.insert(to.to_string(), value);
430    }
431}
432
433fn parse_mcp_server_block(block: &a3s_acl::Block) -> Result<McpServerConfig> {
434    let mut value = acl_block_to_json(block);
435    let object = value.as_object_mut().ok_or_else(|| {
436        CodeError::Config("Invalid MCP server configuration: expected an object".to_string())
437    })?;
438    if let Some(label) = block.labels.first() {
439        object.insert("name".to_string(), JsonValue::String(label.clone()));
440    }
441    if let Some(JsonValue::Object(oauth)) = object.get_mut("oauth") {
442        rename_json_key(oauth, "authUrl", "auth_url");
443        rename_json_key(oauth, "tokenUrl", "token_url");
444        rename_json_key(oauth, "clientId", "client_id");
445        rename_json_key(oauth, "clientSecret", "client_secret");
446        rename_json_key(oauth, "redirectUri", "redirect_uri");
447        rename_json_key(oauth, "accessToken", "access_token");
448    }
449    serde_json::from_value(value)
450        .map_err(|error| CodeError::Config(format!("Invalid MCP server configuration: {error}")))
451}
452
453fn acl_string_map(value: &a3s_acl::Value) -> HashMap<String, String> {
454    match value {
455        a3s_acl::Value::Object(pairs) => pairs
456            .iter()
457            .filter_map(|(key, value)| acl_string(value).map(|value| (key.clone(), value)))
458            .collect(),
459        _ => HashMap::new(),
460    }
461}
462
463fn acl_string_list(value: &a3s_acl::Value) -> Vec<String> {
464    match value {
465        a3s_acl::Value::List(items) => items.iter().filter_map(acl_string).collect(),
466        _ => Vec::new(),
467    }
468}
469
470fn acl_object_f64_attr(value: &a3s_acl::Value, keys: &[&str]) -> Option<f64> {
471    match acl_object_attr(value, keys) {
472        Some(a3s_acl::Value::Number(value)) => Some(*value),
473        _ => None,
474    }
475}
476
477// ============================================================================
478// CodeConfig Implementation
479// ============================================================================
480
481impl CodeConfig {
482    /// Create a new empty configuration
483    pub fn new() -> Self {
484        Self::default()
485    }
486
487    /// Load configuration from an ACL-compatible config file.
488    ///
489    /// `.acl` is the only supported config file extension. JSON and legacy
490    /// `.hcl` config files are not supported.
491    pub fn from_file(path: &Path) -> Result<Self> {
492        let content = crate::bounded_io::read_utf8_file_bounded(
493            path,
494            crate::bounded_io::MAX_CONFIG_FILE_BYTES,
495        )
496        .map_err(|e| {
497            CodeError::Config(format!(
498                "Failed to read config file {}: {}",
499                path.display(),
500                e
501            ))
502        })?;
503
504        Self::from_acl(&content).map_err(|e| {
505            CodeError::Config(format!(
506                "Failed to parse ACL config {}: {}",
507                path.display(),
508                e
509            ))
510        })
511    }
512
513    /// Parse configuration from an ACL string.
514    ///
515    /// ACL (Agent Configuration Language) uses labeled blocks like
516    /// `providers "openai" { }`.
517    pub fn from_acl(content: &str) -> Result<Self> {
518        use a3s_acl::parse_acl;
519
520        let doc = parse_acl(content)
521            .map_err(|e| CodeError::Config(format!("Failed to parse ACL: {}", e)))?;
522
523        let mut config = Self::default();
524
525        for block in doc.blocks {
526            match block.name.as_str() {
527                "default_model" => {
528                    // ACL: default_model = "openai/gpt-4" or just "openai/gpt-4" as label
529                    if let Some(default_model) = acl_label_or_attr(&block, &["default_model"]) {
530                        config.default_model = Some(default_model);
531                    }
532                }
533                "storage_backend" => {
534                    if let Some(backend) = acl_string_attr(&block, &["storage_backend"]) {
535                        config.storage_backend = match backend.to_ascii_lowercase().as_str() {
536                            "memory" => StorageBackend::Memory,
537                            "custom" => StorageBackend::Custom,
538                            _ => StorageBackend::File,
539                        };
540                    }
541                }
542                "sessions_dir" => {
543                    if let Some(path) = acl_string_attr(&block, &["sessions_dir"]) {
544                        config.sessions_dir = Some(PathBuf::from(path));
545                    }
546                }
547                "memory_dir" | "memoryDir" => {
548                    if let Some(path) = acl_string_attr(&block, &["memory_dir", "memoryDir"]) {
549                        config.memory_dir = Some(PathBuf::from(path));
550                    }
551                }
552                "memory" => {
553                    config.memory = Some(parse_memory_block(&block, config.memory.as_ref()));
554                }
555                "queue" => {
556                    config.queue = Some(parse_queue_block(&block)?);
557                }
558                "task_scheduler" | "taskScheduler" => {
559                    config.task_scheduler = parse_task_scheduler_block(&block)?;
560                }
561                "search" => {
562                    config.search = Some(parse_search_block(&block)?);
563                }
564                "document_parser" | "documentParser" => {
565                    config.document_parser = Some(parse_document_parser_block(&block)?);
566                }
567                "mcp_servers" | "mcpServers" | "mcp_server" => {
568                    config.mcp_servers.push(parse_mcp_server_block(&block)?);
569                }
570                "storage_url" => {
571                    if let Some(storage_url) = acl_string_attr(&block, &["storage_url"]) {
572                        config.storage_url = Some(storage_url);
573                    }
574                }
575                "skill_dirs" | "skills" => {
576                    if let Some(paths) = acl_path_list_attr(&block, &["skill_dirs", "skills"]) {
577                        config.skill_dirs = paths;
578                    }
579                }
580                "agent_dirs" => {
581                    if let Some(paths) = acl_path_list_attr(&block, &["agent_dirs"]) {
582                        config.agent_dirs = paths;
583                    }
584                }
585                "user_instructions_dir"
586                | "userInstructionsDir"
587                | "global_instructions_dir"
588                | "globalInstructionsDir" => {
589                    if let Some(path) = acl_string_attr(
590                        &block,
591                        &[
592                            "user_instructions_dir",
593                            "userInstructionsDir",
594                            "global_instructions_dir",
595                            "globalInstructionsDir",
596                        ],
597                    ) {
598                        config.user_instructions_dir = Some(PathBuf::from(path));
599                    }
600                }
601                "project_doc_max_bytes" | "projectDocMaxBytes" => {
602                    if let Some(max_bytes) =
603                        acl_usize_attr(&block, &["project_doc_max_bytes", "projectDocMaxBytes"])
604                    {
605                        config.project_doc_max_bytes = Some(max_bytes);
606                    }
607                }
608                "project_doc_fallback_filenames" | "projectDocFallbackFilenames" => {
609                    if let Some(filenames) = acl_string_list_attr(
610                        &block,
611                        &[
612                            "project_doc_fallback_filenames",
613                            "projectDocFallbackFilenames",
614                        ],
615                    ) {
616                        config.project_doc_fallback_filenames = filenames;
617                    }
618                }
619                "max_tool_rounds" => {
620                    if let Some(max_tool_rounds) = acl_usize_attr(&block, &["max_tool_rounds"]) {
621                        config.max_tool_rounds = Some(max_tool_rounds);
622                    }
623                }
624                "max_parallel_tasks" => {
625                    if let Some(max_parallel_tasks) =
626                        acl_usize_attr(&block, &["max_parallel_tasks"])
627                    {
628                        config.max_parallel_tasks = Some(max_parallel_tasks);
629                    }
630                }
631                "auto_parallel" | "auto_parallel_enabled" => {
632                    if let Some(auto_parallel) =
633                        acl_bool_attr(&block, &["auto_parallel", "auto_parallel_enabled"])
634                    {
635                        config.auto_parallel = Some(auto_parallel);
636                    }
637                }
638                "auto_delegation" => {
639                    config.auto_delegation =
640                        parse_auto_delegation_block(&block, &config.auto_delegation);
641                }
642                "thinking_budget" => {
643                    if let Some(thinking_budget) = acl_usize_attr(&block, &["thinking_budget"]) {
644                        config.thinking_budget = Some(thinking_budget);
645                    }
646                }
647                "llm_api_timeout_ms" | "api_timeout_ms" | "model_api_timeout_ms" => {
648                    if let Some(timeout_ms) = acl_usize_attr(
649                        &block,
650                        &[
651                            "llm_api_timeout_ms",
652                            "api_timeout_ms",
653                            "model_api_timeout_ms",
654                        ],
655                    ) {
656                        config.llm_api_timeout_ms = Some(timeout_ms as u64);
657                    }
658                }
659                "os" => {
660                    if let Some(address) =
661                        acl_label_or_attr(&block, &["os", "address", "url", "baseUrl", "base_url"])
662                            .map(|value| value.trim().to_string())
663                            .filter(|value| !value.is_empty())
664                    {
665                        config.os = Some(OsConfig { address });
666                    }
667                }
668                "providers" => {
669                    let provider_name = block.labels.first().cloned().ok_or_else(|| {
670                        CodeError::Config(
671                            "providers block requires a label (e.g., providers \"openai\" { ... })"
672                                .into(),
673                        )
674                    })?;
675
676                    let mut provider = ProviderConfig {
677                        name: provider_name.clone(),
678                        api_key: None,
679                        base_url: None,
680                        headers: HashMap::new(),
681                        session_id_header: None,
682                        models: Vec::new(),
683                    };
684
685                    for (key, value) in &block.attributes {
686                        match key.as_str() {
687                            "apiKey" | "api_key" => {
688                                if let Some(api_key) = acl_string(value) {
689                                    provider.api_key = Some(api_key);
690                                }
691                            }
692                            "baseUrl" | "base_url" => {
693                                if let Some(base_url) = acl_string(value) {
694                                    provider.base_url = Some(base_url);
695                                }
696                            }
697                            "sessionIdHeader" | "session_id_header" => {
698                                if let Some(header) = acl_string(value) {
699                                    provider.session_id_header = Some(header);
700                                }
701                            }
702                            "headers" => {
703                                provider.headers = acl_string_map(value);
704                            }
705                            _ => {}
706                        }
707                    }
708
709                    // Process nested models blocks
710                    for model_block in &block.blocks {
711                        if model_block.name == "models" {
712                            let model_name =
713                                model_block.labels.first().cloned().ok_or_else(|| {
714                                    CodeError::Config(
715                                        "models block requires a label (e.g., models \"gpt-4\" { ... })"
716                                            .into(),
717                                    )
718                                })?;
719
720                            let mut model = ModelConfig {
721                                id: model_name.clone(),
722                                name: model_name.clone(),
723                                family: String::new(),
724                                api_key: None,
725                                base_url: None,
726                                headers: HashMap::new(),
727                                session_id_header: None,
728                                attachment: false,
729                                reasoning: false,
730                                tool_call: true,
731                                temperature: true,
732                                release_date: None,
733                                modalities: ModelModalities::default(),
734                                cost: ModelCost::default(),
735                                limit: ModelLimit::default(),
736                            };
737
738                            for (key, value) in &model_block.attributes {
739                                match key.as_str() {
740                                    "name" => {
741                                        if let Some(s) = acl_string(value) {
742                                            model.name = s;
743                                        }
744                                    }
745                                    "family" => {
746                                        if let Some(s) = acl_string(value) {
747                                            model.family = s;
748                                        }
749                                    }
750                                    "apiKey" | "api_key" => {
751                                        if let Some(api_key) = acl_string(value) {
752                                            model.api_key = Some(api_key);
753                                        }
754                                    }
755                                    "baseUrl" | "base_url" => {
756                                        if let Some(base_url) = acl_string(value) {
757                                            model.base_url = Some(base_url);
758                                        }
759                                    }
760                                    "sessionIdHeader" | "session_id_header" => {
761                                        if let Some(header) = acl_string(value) {
762                                            model.session_id_header = Some(header);
763                                        }
764                                    }
765                                    "headers" => {
766                                        model.headers = acl_string_map(value);
767                                    }
768                                    "attachment" => {
769                                        model.attachment =
770                                            acl_bool_attr(model_block, &["attachment"])
771                                                .unwrap_or(model.attachment);
772                                    }
773                                    "reasoning" => {
774                                        model.reasoning =
775                                            acl_bool_attr(model_block, &["reasoning"])
776                                                .unwrap_or(model.reasoning);
777                                    }
778                                    "toolCall" | "tool_call" => {
779                                        model.tool_call =
780                                            acl_bool_attr(model_block, &["toolCall", "tool_call"])
781                                                .unwrap_or(model.tool_call);
782                                    }
783                                    "temperature" => {
784                                        model.temperature =
785                                            acl_bool_attr(model_block, &["temperature"])
786                                                .unwrap_or(model.temperature);
787                                    }
788                                    "releaseDate" | "release_date" => {
789                                        if let Some(release_date) = acl_string(value) {
790                                            model.release_date = Some(release_date);
791                                        }
792                                    }
793                                    "maxTokens" => {
794                                        tracing::warn!(
795                                            provider = %provider.name,
796                                            model = %model.id,
797                                            field = "maxTokens",
798                                            "Flat ACL model token limit fields are deprecated; use limit = {{ output = ..., context = ... }}"
799                                        );
800                                        if let Some(output) = acl_u32(value) {
801                                            model.limit.output = output;
802                                        }
803                                    }
804                                    "contextTokens" => {
805                                        tracing::warn!(
806                                            provider = %provider.name,
807                                            model = %model.id,
808                                            field = "contextTokens",
809                                            "Flat ACL model token limit fields are deprecated; use limit = {{ output = ..., context = ... }}"
810                                        );
811                                        if let Some(context) = acl_u32(value) {
812                                            model.limit.context = context;
813                                        }
814                                    }
815                                    "limit" => {
816                                        if let Some(output) =
817                                            acl_object_u32_attr(value, &["output"])
818                                        {
819                                            model.limit.output = output;
820                                        }
821                                        if let Some(context) =
822                                            acl_object_u32_attr(value, &["context"])
823                                        {
824                                            model.limit.context = context;
825                                        }
826                                    }
827                                    "modalities" => {
828                                        if let Some(input) = acl_object_attr(value, &["input"]) {
829                                            model.modalities.input = acl_string_list(input);
830                                        }
831                                        if let Some(output) = acl_object_attr(value, &["output"]) {
832                                            model.modalities.output = acl_string_list(output);
833                                        }
834                                    }
835                                    "cost" => {
836                                        if let Some(input) = acl_object_f64_attr(value, &["input"])
837                                        {
838                                            model.cost.input = input;
839                                        }
840                                        if let Some(output) =
841                                            acl_object_f64_attr(value, &["output"])
842                                        {
843                                            model.cost.output = output;
844                                        }
845                                        if let Some(cache_read) =
846                                            acl_object_f64_attr(value, &["cache_read", "cacheRead"])
847                                        {
848                                            model.cost.cache_read = cache_read;
849                                        }
850                                        if let Some(cache_write) = acl_object_f64_attr(
851                                            value,
852                                            &["cache_write", "cacheWrite"],
853                                        ) {
854                                            model.cost.cache_write = cache_write;
855                                        }
856                                    }
857                                    _ => {}
858                                }
859                            }
860
861                            provider.models.push(model);
862                        }
863                    }
864
865                    config.providers.push(provider);
866                }
867                _ => {}
868            }
869        }
870
871        if let Some(auto_parallel) = config.auto_parallel {
872            config.auto_delegation.auto_parallel = auto_parallel;
873        }
874
875        Ok(config)
876    }
877
878    /// Find a provider by name
879    pub fn find_provider(&self, name: &str) -> Option<&ProviderConfig> {
880        self.providers.iter().find(|p| p.name == name)
881    }
882
883    /// Get the default provider configuration (parsed from `default_model` "provider/model" format)
884    pub fn default_provider_config(&self) -> Option<&ProviderConfig> {
885        let default = self.default_model.as_ref()?;
886        let (provider_name, _) = default.split_once('/')?;
887        self.find_provider(provider_name)
888    }
889
890    /// Get the default model configuration (parsed from `default_model` "provider/model" format)
891    pub fn default_model_config(&self) -> Option<(&ProviderConfig, &ModelConfig)> {
892        let default = self.default_model.as_ref()?;
893        let (provider_name, model_id) = default.split_once('/')?;
894        let provider = self.find_provider(provider_name)?;
895        let model = provider.find_model(model_id)?;
896        Some((provider, model))
897    }
898
899    /// Get LlmConfig for the default provider and model
900    ///
901    /// Returns None if default provider/model is not configured or API key is missing.
902    pub fn default_llm_config(&self) -> Option<LlmConfig> {
903        let (provider, model) = self.default_model_config()?;
904        let api_key = provider.get_api_key(model)?;
905        let base_url = provider.get_base_url(model);
906        let headers = provider.get_headers(model);
907        let session_id_header = provider.get_session_id_header(model);
908
909        let mut config = LlmConfig::new(&provider.name, &model.id, api_key);
910        if let Some(url) = base_url {
911            config = config.with_base_url(url);
912        }
913        if !headers.is_empty() {
914            config = config.with_headers(headers);
915        }
916        if let Some(header_name) = session_id_header {
917            config = config.with_session_id_header(header_name);
918        }
919        if let Some(timeout_ms) = self.llm_api_timeout_ms {
920            config = config.with_api_timeout(timeout_ms);
921        }
922        config = apply_model_caps(config, model, self.thinking_budget);
923        Some(config)
924    }
925
926    /// Get LlmConfig for a specific provider and model
927    ///
928    /// Returns None if provider/model is not found or API key is missing.
929    pub fn llm_config(&self, provider_name: &str, model_id: &str) -> Option<LlmConfig> {
930        let provider = self.find_provider(provider_name)?;
931        let model = provider.find_model(model_id)?;
932        let api_key = provider.get_api_key(model)?;
933        let base_url = provider.get_base_url(model);
934        let headers = provider.get_headers(model);
935        let session_id_header = provider.get_session_id_header(model);
936
937        let mut config = LlmConfig::new(&provider.name, &model.id, api_key);
938        if let Some(url) = base_url {
939            config = config.with_base_url(url);
940        }
941        if !headers.is_empty() {
942            config = config.with_headers(headers);
943        }
944        if let Some(header_name) = session_id_header {
945            config = config.with_session_id_header(header_name);
946        }
947        if let Some(timeout_ms) = self.llm_api_timeout_ms {
948            config = config.with_api_timeout(timeout_ms);
949        }
950        config = apply_model_caps(config, model, self.thinking_budget);
951        Some(config)
952    }
953
954    /// List all available models across all providers
955    pub fn list_models(&self) -> Vec<(&ProviderConfig, &ModelConfig)> {
956        self.providers
957            .iter()
958            .flat_map(|p| p.models.iter().map(move |m| (p, m)))
959            .collect()
960    }
961
962    /// Add a skill directory
963    pub fn add_skill_dir(mut self, dir: impl Into<PathBuf>) -> Self {
964        self.skill_dirs.push(dir.into());
965        self
966    }
967
968    /// Add an agent directory
969    pub fn add_agent_dir(mut self, dir: impl Into<PathBuf>) -> Self {
970        self.agent_dirs.push(dir.into());
971        self
972    }
973
974    /// Check if any directories are configured
975    pub fn has_directories(&self) -> bool {
976        !self.skill_dirs.is_empty() || !self.agent_dirs.is_empty()
977    }
978
979    /// Check if provider configuration is available
980    pub fn has_providers(&self) -> bool {
981        !self.providers.is_empty()
982    }
983}