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