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