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