1use std::collections::HashMap;
9
10use claude_wrapper::types::{Effort, PermissionMode};
11
12use crate::types::{PoolConfig, SlotConfig, TaskOverrides};
13
14#[derive(Debug, Clone)]
18pub struct ResolvedConfig {
19 pub model: Option<String>,
20 pub fallback_model: Option<String>,
21 pub permission_mode: PermissionMode,
22 pub max_turns: Option<u32>,
23 pub system_prompt: Option<String>,
24 pub allowed_tools: Vec<String>,
25 pub disallowed_tools: Vec<String>,
26 pub tools: Vec<String>,
27 pub effort: Option<Effort>,
28 pub mcp_servers: HashMap<String, serde_json::Value>,
31 pub strict_mcp_config: bool,
33 pub json_schema: Option<serde_json::Value>,
35 pub max_budget_usd: Option<f64>,
37}
38
39impl ResolvedConfig {
40 pub fn resolve(global: &PoolConfig, slot: &SlotConfig, task: Option<&TaskOverrides>) -> Self {
46 let model = task
47 .and_then(|t| t.model.clone())
48 .or_else(|| slot.model.clone())
49 .or_else(|| global.model.clone());
50
51 let fallback_model = task
52 .and_then(|t| t.fallback_model.clone())
53 .or_else(|| slot.fallback_model.clone())
54 .or_else(|| global.fallback_model.clone());
55
56 let permission_mode = task
57 .and_then(|t| t.permission_mode)
58 .or(slot.permission_mode)
59 .or(global.permission_mode)
60 .unwrap_or(PermissionMode::Plan);
61
62 let max_turns = task
63 .and_then(|t| t.max_turns)
64 .or(slot.max_turns)
65 .or(global.max_turns);
66
67 let system_prompt = task
68 .and_then(|t| t.system_prompt.clone())
69 .or_else(|| slot.system_prompt.clone())
70 .or_else(|| global.system_prompt.clone());
71
72 let effort = task
73 .and_then(|t| t.effort)
74 .or(slot.effort)
75 .or(global.effort);
76
77 let mut allowed_tools = global.allowed_tools.clone();
79 if let Some(ref wt) = slot.allowed_tools {
80 allowed_tools.extend(wt.iter().cloned());
81 }
82 if let Some(task_cfg) = task
83 && let Some(ref tt) = task_cfg.allowed_tools
84 {
85 allowed_tools.extend(tt.iter().cloned());
86 }
87
88 let disallowed_tools = task
90 .and_then(|t| t.disallowed_tools.as_ref())
91 .cloned()
92 .unwrap_or_default();
93
94 let tools = task
96 .and_then(|t| t.tools.as_ref())
97 .cloned()
98 .unwrap_or_default();
99
100 let mut mcp_servers = global.mcp_servers.clone();
103 if let Some(ref slot_servers) = slot.mcp_servers {
104 mcp_servers.extend(slot_servers.iter().map(|(k, v)| (k.clone(), v.clone())));
105 }
106 if let Some(task_cfg) = task
107 && let Some(ref task_servers) = task_cfg.mcp_servers
108 {
109 mcp_servers.extend(task_servers.iter().map(|(k, v)| (k.clone(), v.clone())));
110 }
111
112 let strict_mcp_config = global.strict_mcp_config;
113
114 let json_schema = task.and_then(|t| t.json_schema.clone());
115 let max_budget_usd = task
116 .and_then(|t| t.max_budget_usd)
117 .or_else(|| global.budget_microdollars.map(|m| m as f64 / 1_000_000.0));
118
119 Self {
120 model,
121 fallback_model,
122 permission_mode,
123 max_turns,
124 system_prompt,
125 allowed_tools,
126 disallowed_tools,
127 tools,
128 effort,
129 mcp_servers,
130 strict_mcp_config,
131 json_schema,
132 max_budget_usd,
133 }
134 }
135}
136
137#[cfg(test)]
138mod tests {
139 use super::*;
140
141 #[test]
142 fn global_defaults() {
143 let global = PoolConfig::default();
144 let slot = SlotConfig::default();
145 let resolved = ResolvedConfig::resolve(&global, &slot, None);
146
147 assert_eq!(resolved.permission_mode, PermissionMode::Plan);
149 assert!(resolved.model.is_none());
150 assert!(resolved.allowed_tools.is_empty());
151 assert!(resolved.mcp_servers.is_empty());
152 assert!(resolved.strict_mcp_config);
153 }
154
155 #[test]
156 fn slot_overrides_global() {
157 let global = PoolConfig {
158 model: Some("haiku".into()),
159 effort: Some(Effort::Low),
160 ..Default::default()
161 };
162 let slot = SlotConfig {
163 model: Some("opus".into()),
164 ..Default::default()
165 };
166 let resolved = ResolvedConfig::resolve(&global, &slot, None);
167
168 assert_eq!(resolved.model.as_deref(), Some("opus"));
169 assert_eq!(resolved.effort, Some(Effort::Low)); }
171
172 #[test]
173 fn task_overrides_slot() {
174 let global = PoolConfig {
175 model: Some("haiku".into()),
176 ..Default::default()
177 };
178 let slot = SlotConfig {
179 model: Some("sonnet".into()),
180 effort: Some(Effort::Medium),
181 ..Default::default()
182 };
183 let task = TaskOverrides {
184 effort: Some(Effort::Max),
185 ..Default::default()
186 };
187 let resolved = ResolvedConfig::resolve(&global, &slot, Some(&task));
188
189 assert_eq!(resolved.model.as_deref(), Some("sonnet")); assert_eq!(resolved.effort, Some(Effort::Max)); }
192
193 #[test]
194 fn allowed_tools_merge() {
195 let global = PoolConfig {
196 allowed_tools: vec!["Bash".into(), "Read".into()],
197 ..Default::default()
198 };
199 let slot = SlotConfig {
200 allowed_tools: Some(vec!["Write".into()]),
201 ..Default::default()
202 };
203 let task = TaskOverrides {
204 allowed_tools: Some(vec!["Edit".into()]),
205 ..Default::default()
206 };
207 let resolved = ResolvedConfig::resolve(&global, &slot, Some(&task));
208
209 assert_eq!(
210 resolved.allowed_tools,
211 vec!["Bash", "Read", "Write", "Edit"]
212 );
213 }
214
215 #[test]
216 fn mcp_servers_merge() {
217 let global = PoolConfig {
218 mcp_servers: [(
219 "hub".to_string(),
220 serde_json::json!({"type": "http", "url": "http://global/"}),
221 )]
222 .into_iter()
223 .collect(),
224 ..Default::default()
225 };
226 let slot = SlotConfig {
227 mcp_servers: Some(
228 [(
229 "tool".to_string(),
230 serde_json::json!({"type": "stdio", "command": "npx"}),
231 )]
232 .into_iter()
233 .collect(),
234 ),
235 ..Default::default()
236 };
237 let resolved = ResolvedConfig::resolve(&global, &slot, None);
238
239 assert_eq!(resolved.mcp_servers.len(), 2);
240 assert!(resolved.mcp_servers.contains_key("hub"));
241 assert!(resolved.mcp_servers.contains_key("tool"));
242 }
243
244 #[test]
245 fn task_mcp_servers_override_slot() {
246 let global = PoolConfig::default();
247 let slot = SlotConfig {
248 mcp_servers: Some(
249 [(
250 "hub".to_string(),
251 serde_json::json!({"type": "http", "url": "http://slot/"}),
252 )]
253 .into_iter()
254 .collect(),
255 ),
256 ..Default::default()
257 };
258 let task = TaskOverrides {
259 mcp_servers: Some(
260 [(
261 "hub".to_string(),
262 serde_json::json!({"type": "http", "url": "http://task/"}),
263 )]
264 .into_iter()
265 .collect(),
266 ),
267 ..Default::default()
268 };
269 let resolved = ResolvedConfig::resolve(&global, &slot, Some(&task));
270
271 assert_eq!(resolved.mcp_servers["hub"]["url"], "http://task/");
272 }
273
274 #[test]
275 fn strict_mcp_config_defaults_true() {
276 let global = PoolConfig::default();
277 let slot = SlotConfig::default();
278 let resolved = ResolvedConfig::resolve(&global, &slot, None);
279 assert!(resolved.strict_mcp_config);
280 }
281
282 #[test]
283 fn strict_mcp_config_can_be_disabled() {
284 let global = PoolConfig {
285 strict_mcp_config: false,
286 ..Default::default()
287 };
288 let slot = SlotConfig::default();
289 let resolved = ResolvedConfig::resolve(&global, &slot, None);
290 assert!(!resolved.strict_mcp_config);
291 }
292}