Skip to main content

codineer_runtime/config/
types.rs

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/// Configuration for a custom OpenAI-compatible provider.
59#[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    /// Return the `"env"` section from merged config as key-value pairs.
264    /// Callers can use this to apply environment variables to the process.
265    #[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}