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