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