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