1use std::collections::BTreeMap;
2use std::fmt::{Display, Formatter};
3use std::path::PathBuf;
4
5use crate::json::JsonValue;
6use crate::sandbox::SandboxConfig;
7
8pub const CODINEER_SETTINGS_SCHEMA_NAME: &str = "SettingsSchema";
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
11pub enum ConfigSource {
12 User,
13 Project,
14 Local,
15}
16
17impl ConfigSource {
18 pub const fn as_str(self) -> &'static str {
19 match self {
20 Self::User => "user",
21 Self::Project => "project",
22 Self::Local => "local",
23 }
24 }
25}
26
27impl Display for ConfigSource {
28 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
29 f.write_str(self.as_str())
30 }
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub enum ResolvedPermissionMode {
35 ReadOnly,
36 WorkspaceWrite,
37 DangerFullAccess,
38}
39
40#[derive(Debug, Clone, PartialEq, Eq)]
41pub struct ConfigEntry {
42 pub source: ConfigSource,
43 pub path: PathBuf,
44}
45
46#[derive(Debug, Clone, PartialEq, Eq)]
47pub struct RuntimeConfig {
48 merged: BTreeMap<String, JsonValue>,
49 loaded_entries: Vec<ConfigEntry>,
50 feature_config: RuntimeFeatureConfig,
51}
52
53#[derive(Debug, Clone, PartialEq, Eq, Default)]
54pub struct RuntimePluginConfig {
55 pub(crate) enabled_plugins: BTreeMap<String, bool>,
56 pub(crate) external_directories: Vec<String>,
57 pub(crate) install_root: Option<String>,
58 pub(crate) registry_path: Option<String>,
59 pub(crate) bundled_root: Option<String>,
60}
61
62#[derive(Debug, Clone, PartialEq, Eq)]
64pub struct CredentialConfig {
65 pub default_source: Option<String>,
67 pub auto_discover: bool,
69 pub claude_code_enabled: bool,
71}
72
73impl Default for CredentialConfig {
74 fn default() -> Self {
75 Self {
76 default_source: None,
77 auto_discover: true,
78 claude_code_enabled: true,
79 }
80 }
81}
82
83#[derive(Debug, Clone, PartialEq, Eq, Default)]
84pub struct RuntimeFeatureConfig {
85 pub(crate) hooks: RuntimeHookConfig,
86 pub(crate) plugins: RuntimePluginConfig,
87 pub(crate) mcp: McpConfigCollection,
88 pub(crate) oauth: Option<OAuthConfig>,
89 pub(crate) model: Option<String>,
90 pub(crate) fallback_models: Vec<String>,
91 pub(crate) permission_mode: Option<ResolvedPermissionMode>,
92 pub(crate) sandbox: SandboxConfig,
93 pub(crate) providers: BTreeMap<String, CustomProviderConfig>,
94 pub(crate) credentials: CredentialConfig,
95}
96
97#[derive(Debug, Clone, PartialEq, Eq)]
99pub struct CustomProviderConfig {
100 pub base_url: String,
101 pub api_version: Option<String>,
103 pub api_key: Option<String>,
104 pub api_key_env: Option<String>,
105 pub models: Vec<String>,
106 pub default_model: Option<String>,
107}
108
109#[derive(Debug, Clone, PartialEq, Eq, Default)]
110pub struct RuntimeHookConfig {
111 pub(crate) pre_tool_use: Vec<String>,
112 pub(crate) post_tool_use: Vec<String>,
113}
114
115#[derive(Debug, Clone, PartialEq, Eq, Default)]
116pub struct McpConfigCollection {
117 pub(crate) servers: BTreeMap<String, ScopedMcpServerConfig>,
118}
119
120#[derive(Debug, Clone, PartialEq, Eq)]
121pub struct ScopedMcpServerConfig {
122 pub scope: ConfigSource,
123 pub config: McpServerConfig,
124}
125
126#[derive(Debug, Clone, Copy, PartialEq, Eq)]
127pub enum McpTransport {
128 Stdio,
129 Sse,
130 Http,
131 Ws,
132 Sdk,
133 ManagedProxy,
134}
135
136#[derive(Debug, Clone, PartialEq, Eq)]
137pub enum McpServerConfig {
138 Stdio(McpStdioServerConfig),
139 Sse(McpRemoteServerConfig),
140 Http(McpRemoteServerConfig),
141 Ws(McpWebSocketServerConfig),
142 Sdk(McpSdkServerConfig),
143 ManagedProxy(McpManagedProxyServerConfig),
144}
145
146#[derive(Debug, Clone, PartialEq, Eq)]
147pub struct McpStdioServerConfig {
148 pub command: String,
149 pub args: Vec<String>,
150 pub env: BTreeMap<String, String>,
151}
152
153#[derive(Debug, Clone, PartialEq, Eq)]
154pub struct McpRemoteServerConfig {
155 pub url: String,
156 pub headers: BTreeMap<String, String>,
157 pub headers_helper: Option<String>,
158 pub oauth: Option<McpOAuthConfig>,
159}
160
161#[derive(Debug, Clone, PartialEq, Eq)]
162pub struct McpWebSocketServerConfig {
163 pub url: String,
164 pub headers: BTreeMap<String, String>,
165 pub headers_helper: Option<String>,
166}
167
168#[derive(Debug, Clone, PartialEq, Eq)]
169pub struct McpSdkServerConfig {
170 pub name: String,
171}
172
173#[derive(Debug, Clone, PartialEq, Eq)]
174pub struct McpManagedProxyServerConfig {
175 pub url: String,
176 pub id: String,
177}
178
179#[derive(Debug, Clone, PartialEq, Eq)]
180pub struct McpOAuthConfig {
181 pub client_id: Option<String>,
182 pub callback_port: Option<u16>,
183 pub auth_server_metadata_url: Option<String>,
184 pub xaa: Option<bool>,
185}
186
187#[derive(Debug, Clone, PartialEq, Eq)]
188pub struct OAuthConfig {
189 pub client_id: String,
190 pub authorize_url: String,
191 pub token_url: String,
192 pub callback_port: Option<u16>,
193 pub manual_redirect_url: Option<String>,
194 pub scopes: Vec<String>,
195}
196
197#[derive(Debug)]
198pub enum ConfigError {
199 Io(std::io::Error),
200 Parse(String),
201}
202
203impl Display for ConfigError {
204 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
205 match self {
206 Self::Io(error) => write!(f, "{error}"),
207 Self::Parse(error) => write!(f, "{error}"),
208 }
209 }
210}
211
212impl std::error::Error for ConfigError {}
213
214impl From<std::io::Error> for ConfigError {
215 fn from(value: std::io::Error) -> Self {
216 Self::Io(value)
217 }
218}
219
220impl RuntimeConfig {
221 #[must_use]
222 pub fn new(
223 merged: BTreeMap<String, JsonValue>,
224 loaded_entries: Vec<ConfigEntry>,
225 feature_config: RuntimeFeatureConfig,
226 ) -> Self {
227 Self {
228 merged,
229 loaded_entries,
230 feature_config,
231 }
232 }
233
234 #[must_use]
235 pub fn empty() -> Self {
236 Self::new(BTreeMap::new(), Vec::new(), RuntimeFeatureConfig::default())
237 }
238
239 #[must_use]
240 pub fn merged(&self) -> &BTreeMap<String, JsonValue> {
241 &self.merged
242 }
243
244 #[must_use]
245 pub fn loaded_entries(&self) -> &[ConfigEntry] {
246 &self.loaded_entries
247 }
248
249 #[must_use]
250 pub fn get(&self, key: &str) -> Option<&JsonValue> {
251 self.merged.get(key)
252 }
253
254 #[must_use]
255 pub fn as_json(&self) -> JsonValue {
256 JsonValue::Object(self.merged.clone())
257 }
258
259 #[must_use]
260 pub fn feature_config(&self) -> &RuntimeFeatureConfig {
261 &self.feature_config
262 }
263
264 #[must_use]
265 pub fn mcp(&self) -> &McpConfigCollection {
266 &self.feature_config.mcp
267 }
268
269 #[must_use]
270 pub fn hooks(&self) -> &RuntimeHookConfig {
271 &self.feature_config.hooks
272 }
273
274 #[must_use]
275 pub fn plugins(&self) -> &RuntimePluginConfig {
276 &self.feature_config.plugins
277 }
278
279 #[must_use]
280 pub fn oauth(&self) -> Option<&OAuthConfig> {
281 self.feature_config.oauth.as_ref()
282 }
283
284 #[must_use]
285 pub fn model(&self) -> Option<&str> {
286 self.feature_config.model.as_deref()
287 }
288
289 #[must_use]
290 pub fn fallback_models(&self) -> &[String] {
291 &self.feature_config.fallback_models
292 }
293
294 #[must_use]
295 pub fn permission_mode(&self) -> Option<ResolvedPermissionMode> {
296 self.feature_config.permission_mode
297 }
298
299 #[must_use]
300 pub fn sandbox(&self) -> &SandboxConfig {
301 &self.feature_config.sandbox
302 }
303
304 #[must_use]
305 pub fn providers(&self) -> &BTreeMap<String, CustomProviderConfig> {
306 &self.feature_config.providers
307 }
308
309 #[must_use]
310 pub fn credentials(&self) -> &CredentialConfig {
311 &self.feature_config.credentials
312 }
313
314 #[must_use]
317 pub fn env_section(&self) -> Vec<(String, String)> {
318 self.merged
319 .get("env")
320 .and_then(JsonValue::as_object)
321 .map(|obj| {
322 obj.iter()
323 .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
324 .collect()
325 })
326 .unwrap_or_default()
327 }
328}
329
330impl RuntimeFeatureConfig {
331 #[must_use]
332 pub fn with_hooks(mut self, hooks: RuntimeHookConfig) -> Self {
333 self.hooks = hooks;
334 self
335 }
336
337 #[must_use]
338 pub fn with_plugins(mut self, plugins: RuntimePluginConfig) -> Self {
339 self.plugins = plugins;
340 self
341 }
342
343 #[must_use]
344 pub fn hooks(&self) -> &RuntimeHookConfig {
345 &self.hooks
346 }
347
348 #[must_use]
349 pub fn plugins(&self) -> &RuntimePluginConfig {
350 &self.plugins
351 }
352
353 #[must_use]
354 pub fn mcp(&self) -> &McpConfigCollection {
355 &self.mcp
356 }
357
358 #[must_use]
359 pub fn oauth(&self) -> Option<&OAuthConfig> {
360 self.oauth.as_ref()
361 }
362
363 #[must_use]
364 pub fn model(&self) -> Option<&str> {
365 self.model.as_deref()
366 }
367
368 #[must_use]
369 pub fn permission_mode(&self) -> Option<ResolvedPermissionMode> {
370 self.permission_mode
371 }
372
373 #[must_use]
374 pub fn sandbox(&self) -> &SandboxConfig {
375 &self.sandbox
376 }
377
378 #[must_use]
379 pub fn providers(&self) -> &BTreeMap<String, CustomProviderConfig> {
380 &self.providers
381 }
382
383 #[must_use]
384 pub fn credentials(&self) -> &CredentialConfig {
385 &self.credentials
386 }
387
388 pub fn set_providers(&mut self, providers: BTreeMap<String, CustomProviderConfig>) {
390 self.providers = providers;
391 }
392
393 pub fn set_fallback_models(&mut self, fallback_models: Vec<String>) {
394 self.fallback_models = fallback_models;
395 }
396}
397
398impl RuntimePluginConfig {
399 #[must_use]
400 pub fn enabled_plugins(&self) -> &BTreeMap<String, bool> {
401 &self.enabled_plugins
402 }
403
404 #[must_use]
405 pub fn external_directories(&self) -> &[String] {
406 &self.external_directories
407 }
408
409 #[must_use]
410 pub fn install_root(&self) -> Option<&str> {
411 self.install_root.as_deref()
412 }
413
414 #[must_use]
415 pub fn registry_path(&self) -> Option<&str> {
416 self.registry_path.as_deref()
417 }
418
419 #[must_use]
420 pub fn bundled_root(&self) -> Option<&str> {
421 self.bundled_root.as_deref()
422 }
423
424 pub fn set_plugin_state(&mut self, plugin_id: String, enabled: bool) {
425 self.enabled_plugins.insert(plugin_id, enabled);
426 }
427
428 #[must_use]
429 pub fn state_for(&self, plugin_id: &str, default_enabled: bool) -> bool {
430 self.enabled_plugins
431 .get(plugin_id)
432 .copied()
433 .unwrap_or(default_enabled)
434 }
435}
436
437impl RuntimeHookConfig {
438 #[must_use]
439 pub fn new(pre_tool_use: Vec<String>, post_tool_use: Vec<String>) -> Self {
440 Self {
441 pre_tool_use,
442 post_tool_use,
443 }
444 }
445
446 #[must_use]
447 pub fn pre_tool_use(&self) -> &[String] {
448 &self.pre_tool_use
449 }
450
451 #[must_use]
452 pub fn post_tool_use(&self) -> &[String] {
453 &self.post_tool_use
454 }
455
456 #[must_use]
457 pub fn merged(&self, other: &Self) -> Self {
458 let mut merged = self.clone();
459 merged.extend(other);
460 merged
461 }
462
463 pub fn extend(&mut self, other: &Self) {
464 extend_unique(&mut self.pre_tool_use, other.pre_tool_use());
465 extend_unique(&mut self.post_tool_use, other.post_tool_use());
466 }
467}
468
469impl McpConfigCollection {
470 #[must_use]
471 pub fn servers(&self) -> &BTreeMap<String, ScopedMcpServerConfig> {
472 &self.servers
473 }
474
475 #[must_use]
476 pub fn get(&self, name: &str) -> Option<&ScopedMcpServerConfig> {
477 self.servers.get(name)
478 }
479}
480
481impl ScopedMcpServerConfig {
482 #[must_use]
483 pub fn transport(&self) -> McpTransport {
484 self.config.transport()
485 }
486}
487
488impl McpServerConfig {
489 #[must_use]
490 pub fn transport(&self) -> McpTransport {
491 match self {
492 Self::Stdio(_) => McpTransport::Stdio,
493 Self::Sse(_) => McpTransport::Sse,
494 Self::Http(_) => McpTransport::Http,
495 Self::Ws(_) => McpTransport::Ws,
496 Self::Sdk(_) => McpTransport::Sdk,
497 Self::ManagedProxy(_) => McpTransport::ManagedProxy,
498 }
499 }
500}
501
502fn extend_unique(target: &mut Vec<String>, values: &[String]) {
503 for value in values {
504 push_unique(target, value.clone());
505 }
506}
507
508fn push_unique(target: &mut Vec<String>, value: String) {
509 if !target.iter().any(|existing| existing == &value) {
510 target.push(value);
511 }
512}