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