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