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    /// Appended as query on `.../chat/completions` when the server requires a version parameter (e.g. `api-version=...`).
102    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    /// Extra HTTP headers sent with every request to this provider.
108    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    /// Return the `"env"` section from merged config as key-value pairs.
317    /// Callers can use this to apply environment variables to the process.
318    #[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    /// Set the custom providers map (useful in tests and programmatic construction).
391    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}