Skip to main content

magi_code/config/settings/
core.rs

1use crate::config::{CustomProviderConfig, HookSettings};
2use schemars::JsonSchema;
3use serde::{Deserialize, Serialize};
4use std::collections::BTreeMap;
5
6use super::{
7    agent::{
8        CompactionSettings, InstructionsSettings, IntegrationsSettings, ModelsSettings,
9        SessionTitleSettings, SkillsSettings, SubagentsSettings, TuiSettings,
10    },
11    helpers::is_false,
12    providers::{ProviderStreamSettings, SelectedModelSettings},
13    services::{LspSettings, McpServersSettings},
14    tools::ToolSettings,
15};
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum SettingsScope {
19    Global,
20    Project,
21}
22
23impl SettingsScope {
24    pub(crate) fn label(self) -> &'static str {
25        match self {
26            Self::Global => "Global",
27            Self::Project => "Project",
28        }
29    }
30
31    pub(crate) fn toggle(self) -> Self {
32        match self {
33            Self::Global => Self::Project,
34            Self::Project => Self::Global,
35        }
36    }
37}
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub enum SettingsListKind {
41    Skills,
42    Tools,
43    Subagents,
44    Models,
45}
46
47#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
48#[serde(rename_all = "lowercase")]
49pub enum TextVerbosity {
50    #[default]
51    Low,
52    Medium,
53    High,
54}
55
56impl TextVerbosity {
57    pub(crate) fn as_api_str(self) -> &'static str {
58        match self {
59            Self::Low => "low",
60            Self::Medium => "medium",
61            Self::High => "high",
62        }
63    }
64}
65
66#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
67pub struct FastSettings {
68    #[serde(default, skip_serializing_if = "is_false")]
69    #[schemars(!skip_serializing_if, default)]
70    pub enabled: bool,
71    #[serde(default, skip_serializing_if = "is_false")]
72    #[schemars(!skip_serializing_if, default)]
73    pub ultrafast: bool,
74}
75
76impl FastSettings {
77    pub(crate) fn mode_name(&self) -> &'static str {
78        if self.ultrafast { "Ultrafast" } else { "Fast" }
79    }
80
81    pub(crate) fn is_default(&self) -> bool {
82        self == &Self::default()
83    }
84}
85
86#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
87pub struct OpenAiResponsesSettings {
88    #[serde(default, skip_serializing_if = "Option::is_none")]
89    pub text_verbosity: Option<TextVerbosity>,
90}
91
92impl OpenAiResponsesSettings {
93    pub(crate) fn is_default(&self) -> bool {
94        self == &Self::default()
95    }
96}
97
98#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
99pub struct OpenAiCodexSettings {
100    #[serde(default)]
101    pub text_verbosity: TextVerbosity,
102    /// Unverified Codex support; disabled unless explicitly requested.
103    #[serde(default, skip_serializing_if = "is_false")]
104    pub experimental_reasoning_updates: bool,
105}
106
107impl OpenAiCodexSettings {
108    pub(crate) fn is_default(&self) -> bool {
109        self == &Self::default()
110    }
111}
112
113#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
114pub enum AnthropicCacheTtl {
115    #[default]
116    #[serde(rename = "5m")]
117    FiveMinutes,
118    #[serde(rename = "1h")]
119    OneHour,
120}
121
122pub const DEFAULT_SESSION_RETENTION_DAYS: u64 = 30;
123
124fn default_session_retention_days() -> u64 {
125    DEFAULT_SESSION_RETENTION_DAYS
126}
127
128#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
129pub struct SessionSettings {
130    #[serde(default = "default_session_retention_days")]
131    pub retention_days: u64,
132}
133
134impl Default for SessionSettings {
135    fn default() -> Self {
136        Self {
137            retention_days: DEFAULT_SESSION_RETENTION_DAYS,
138        }
139    }
140}
141
142#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
143pub(crate) struct AppearanceSettings {
144    #[serde(
145        default = "default_appearance_theme",
146        skip_serializing_if = "is_default_appearance_theme"
147    )]
148    pub(crate) theme: String,
149    #[serde(default, skip_serializing_if = "is_false")]
150    pub(crate) reduced_motion: bool,
151}
152
153impl Default for AppearanceSettings {
154    fn default() -> Self {
155        Self {
156            theme: crate::appearance::DEFAULT_THEME_ID.to_string(),
157            reduced_motion: false,
158        }
159    }
160}
161
162fn default_appearance_theme() -> String {
163    crate::appearance::DEFAULT_THEME_ID.to_string()
164}
165
166fn is_default_appearance_theme(theme: &String) -> bool {
167    theme == crate::appearance::DEFAULT_THEME_ID
168}
169
170#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
171#[serde(rename_all = "snake_case")]
172pub enum BashProtectionFailurePolicy {
173    #[default]
174    AutoApprove,
175    AutoDeny,
176}
177
178impl BashProtectionFailurePolicy {
179    pub(crate) fn allows_execution(self) -> bool {
180        matches!(self, Self::AutoApprove)
181    }
182
183    pub(crate) const fn as_str(self) -> &'static str {
184        match self {
185            Self::AutoApprove => "auto_approve",
186            Self::AutoDeny => "auto_deny",
187        }
188    }
189}
190#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
191#[serde(rename_all = "snake_case")]
192pub enum BashProtectionLevel {
193    Permissive,
194    #[default]
195    Relaxed,
196    Balanced,
197    Cautious,
198    Strict,
199}
200
201impl BashProtectionLevel {
202    pub(crate) fn thresholds(self) -> (f64, f64) {
203        match self {
204            Self::Permissive => (2.0, 0.0),
205            Self::Relaxed => (1.8, 0.2),
206            Self::Balanced => (1.5, 0.35),
207            Self::Cautious => (1.0, 0.5),
208            Self::Strict => (0.5, 0.7),
209        }
210    }
211
212    pub(crate) const fn as_str(self) -> &'static str {
213        match self {
214            Self::Permissive => "permissive",
215            Self::Relaxed => "relaxed",
216            Self::Balanced => "balanced",
217            Self::Cautious => "cautious",
218            Self::Strict => "strict",
219        }
220    }
221}
222
223#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq)]
224pub struct BashProtectionSettings {
225    #[serde(default, skip_serializing_if = "is_false")]
226    pub enabled: bool,
227    #[serde(default)]
228    pub protection_level: BashProtectionLevel,
229    #[serde(default)]
230    pub failure_policy: BashProtectionFailurePolicy,
231}
232
233#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
234#[serde(rename_all = "snake_case")]
235pub enum HumanizeProtectionFailurePolicy {
236    AutoApprove,
237    #[default]
238    AutoDeny,
239}
240
241impl HumanizeProtectionFailurePolicy {
242    pub(crate) fn allows_execution(self) -> bool {
243        matches!(self, Self::AutoApprove)
244    }
245}
246
247fn default_content_threshold() -> f64 {
248    0.7
249}
250
251fn default_humanized_threshold() -> f64 {
252    0.65
253}
254
255#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
256pub struct HumanizeProtectionSettings {
257    #[serde(default, skip_serializing_if = "is_false")]
258    pub enabled: bool,
259    #[serde(default = "default_content_threshold")]
260    pub content_threshold: f64,
261    #[serde(default = "default_humanized_threshold")]
262    pub humanized_threshold: f64,
263    #[serde(default)]
264    pub failure_policy: HumanizeProtectionFailurePolicy,
265}
266
267impl Default for HumanizeProtectionSettings {
268    fn default() -> Self {
269        Self {
270            enabled: false,
271            content_threshold: default_content_threshold(),
272            humanized_threshold: default_humanized_threshold(),
273            failure_policy: HumanizeProtectionFailurePolicy::default(),
274        }
275    }
276}
277
278#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
279#[serde(rename_all = "snake_case")]
280pub enum PromptInjectionFailurePolicy {
281    Annotate,
282    #[default]
283    Quarantine,
284}
285
286#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
287#[serde(rename_all = "snake_case")]
288pub enum PromptInjectionProtectionLevel {
289    Permissive,
290    Relaxed,
291    #[default]
292    Balanced,
293    Cautious,
294    Strict,
295}
296
297impl PromptInjectionProtectionLevel {
298    pub(crate) fn thresholds(self) -> (f64, f64, f64, f64) {
299        match self {
300            Self::Permissive => (0.60, 0.90, 1.80, 0.30),
301            Self::Relaxed => (0.50, 0.80, 1.70, 0.40),
302            Self::Balanced => (0.35, 0.70, 1.50, 0.55),
303            Self::Cautious => (0.25, 0.60, 1.20, 0.65),
304            Self::Strict => (0.15, 0.50, 0.80, 0.75),
305        }
306    }
307}
308fn default_prompt_injection_max_assessed_bytes() -> usize {
309    32 * 1024
310}
311
312fn default_prompt_injection_tools() -> Vec<String> {
313    vec!["web".to_string()]
314}
315
316#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
317pub struct PromptInjectionProtectionSettings {
318    #[serde(default, skip_serializing_if = "is_false")]
319    pub enabled: bool,
320    #[serde(default, skip_serializing_if = "is_false")]
321    pub enforcement_enabled: bool,
322    #[serde(default)]
323    pub protection_level: PromptInjectionProtectionLevel,
324    #[serde(default = "default_prompt_injection_max_assessed_bytes")]
325    pub max_assessed_bytes: usize,
326    #[serde(default)]
327    pub failure_policy: PromptInjectionFailurePolicy,
328    #[serde(default = "default_prompt_injection_tools")]
329    pub tools: Vec<String>,
330}
331
332impl Default for PromptInjectionProtectionSettings {
333    fn default() -> Self {
334        Self {
335            enabled: false,
336            enforcement_enabled: false,
337            protection_level: PromptInjectionProtectionLevel::default(),
338            max_assessed_bytes: default_prompt_injection_max_assessed_bytes(),
339            failure_policy: PromptInjectionFailurePolicy::default(),
340            tools: default_prompt_injection_tools(),
341        }
342    }
343}
344
345#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
346#[serde(rename_all = "snake_case")]
347pub enum CompletionVerificationFailurePolicy {
348    #[default]
349    AcceptWithWarning,
350    RejectWithWarning,
351}
352
353fn default_completion_verification_threshold() -> f64 {
354    0.7
355}
356fn default_completion_verification_max_state_bytes() -> usize {
357    32 * 1024
358}
359fn default_completion_verification_max_continuations() -> u8 {
360    1
361}
362
363#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
364pub struct CompletionVerificationSettings {
365    #[serde(default, skip_serializing_if = "is_false")]
366    pub enabled: bool,
367    #[serde(default, skip_serializing_if = "is_false")]
368    pub enforcement_enabled: bool,
369    #[serde(default = "default_completion_verification_threshold")]
370    pub threshold: f64,
371    #[serde(default = "default_completion_verification_max_state_bytes")]
372    pub max_state_bytes: usize,
373    #[serde(default = "default_completion_verification_max_continuations")]
374    pub max_continuations: u8,
375    #[serde(default)]
376    pub failure_policy: CompletionVerificationFailurePolicy,
377}
378
379impl Default for CompletionVerificationSettings {
380    fn default() -> Self {
381        Self {
382            enabled: false,
383            enforcement_enabled: false,
384            threshold: default_completion_verification_threshold(),
385            max_state_bytes: default_completion_verification_max_state_bytes(),
386            max_continuations: default_completion_verification_max_continuations(),
387            failure_policy: CompletionVerificationFailurePolicy::default(),
388        }
389    }
390}
391
392fn default_skill_suggestion_gate_threshold() -> f64 {
393    0.3
394}
395fn default_skill_suggestion_fit_threshold() -> f64 {
396    0.3
397}
398
399#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
400pub struct SkillSuggestionSettings {
401    #[serde(default, skip_serializing_if = "is_false")]
402    pub enabled: bool,
403    #[serde(default = "default_skill_suggestion_gate_threshold")]
404    pub gate_threshold: f64,
405    #[serde(default = "default_skill_suggestion_fit_threshold")]
406    pub fit_threshold: f64,
407}
408
409impl Default for SkillSuggestionSettings {
410    fn default() -> Self {
411        Self {
412            enabled: false,
413            gate_threshold: default_skill_suggestion_gate_threshold(),
414            fit_threshold: default_skill_suggestion_fit_threshold(),
415        }
416    }
417}
418
419fn default_compaction_timing_soft_threshold_percent() -> u8 {
420    60
421}
422fn default_compaction_timing_threshold() -> f64 {
423    0.7
424}
425
426#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
427pub struct CompactionTimingSettings {
428    #[serde(default, skip_serializing_if = "is_false")]
429    pub enabled: bool,
430    #[serde(default = "default_compaction_timing_soft_threshold_percent")]
431    pub soft_threshold_percent: u8,
432    #[serde(default = "default_compaction_timing_threshold")]
433    pub threshold: f64,
434}
435
436impl Default for CompactionTimingSettings {
437    fn default() -> Self {
438        Self {
439            enabled: false,
440            soft_threshold_percent: default_compaction_timing_soft_threshold_percent(),
441            threshold: default_compaction_timing_threshold(),
442        }
443    }
444}
445
446#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
447pub struct BashFileTrackingSettings {
448    #[serde(default, skip_serializing_if = "is_false")]
449    pub enabled: bool,
450}
451
452#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq)]
453pub struct JevSettings {
454    #[serde(default, skip_serializing_if = "is_false")]
455    pub enabled: bool,
456    #[serde(default, skip_serializing_if = "is_false")]
457    pub show_debug_messages: bool,
458    #[serde(default)]
459    pub bash_protection: BashProtectionSettings,
460    #[serde(default)]
461    pub bash_file_tracking: BashFileTrackingSettings,
462    #[serde(default)]
463    pub humanize_protection: HumanizeProtectionSettings,
464    #[serde(default)]
465    pub prompt_injection_protection: PromptInjectionProtectionSettings,
466    #[serde(default)]
467    pub completion_verification: CompletionVerificationSettings,
468    #[serde(default)]
469    pub skill_suggestion: SkillSuggestionSettings,
470    #[serde(default)]
471    pub compaction_timing: CompactionTimingSettings,
472}
473
474impl JevSettings {
475    pub(crate) fn is_default(&self) -> bool {
476        self == &Self::default()
477    }
478}
479
480#[derive(Debug, Clone, PartialEq)]
481pub struct Settings {
482    pub selected_model: SelectedModelSettings,
483    pub fast: FastSettings,
484    pub openai_codex: OpenAiCodexSettings,
485    pub openai_responses: OpenAiResponsesSettings,
486    pub no_color: Option<bool>,
487    pub anthropic_cache_ttl: Option<AnthropicCacheTtl>,
488    pub file_autocomplete_respects_gitignore: bool,
489    pub context: Option<crate::context::ContextBudget>,
490    pub session_titles: SessionTitleSettings,
491    pub summarizer: super::agent::SummarizerSettings,
492    pub side: super::agent::SideAgentSettings,
493    pub compaction: CompactionSettings,
494    pub tools: ToolSettings,
495    pub subagents: SubagentsSettings,
496    pub models: ModelsSettings,
497    pub hooks: HookSettings,
498    pub instructions: InstructionsSettings,
499    pub skills: SkillsSettings,
500    pub custom_providers: BTreeMap<String, CustomProviderConfig>,
501    pub mcp_servers: McpServersSettings,
502    /// Global-only approvals keyed by canonical .mcp.json source path, then server name.
503    pub mcp_approvals: BTreeMap<String, BTreeMap<String, bool>>,
504    pub lsp: LspSettings,
505    pub integrations: IntegrationsSettings,
506    pub tui: TuiSettings,
507    pub provider_stream: ProviderStreamSettings,
508    pub selected_primary_agent: Option<String>,
509    pub jev: JevSettings,
510}
511
512impl Default for Settings {
513    fn default() -> Self {
514        Self {
515            fast: FastSettings::default(),
516
517            openai_responses: OpenAiResponsesSettings::default(),
518            selected_model: SelectedModelSettings::default(),
519            openai_codex: OpenAiCodexSettings::default(),
520            no_color: None,
521            file_autocomplete_respects_gitignore: true,
522            anthropic_cache_ttl: None,
523            context: None,
524            session_titles: SessionTitleSettings::default(),
525            summarizer: super::agent::SummarizerSettings::default(),
526            side: super::agent::SideAgentSettings::default(),
527            compaction: CompactionSettings::default(),
528            tools: ToolSettings::default(),
529            subagents: SubagentsSettings::default(),
530            models: ModelsSettings::default(),
531            hooks: HookSettings::default(),
532            instructions: InstructionsSettings::default(),
533            skills: SkillsSettings::default(),
534            custom_providers: BTreeMap::new(),
535            mcp_servers: BTreeMap::new(),
536            mcp_approvals: BTreeMap::new(),
537            lsp: LspSettings::default(),
538            integrations: IntegrationsSettings::default(),
539            tui: TuiSettings::default(),
540            provider_stream: ProviderStreamSettings::default(),
541            selected_primary_agent: None,
542            jev: JevSettings::default(),
543        }
544    }
545}
546
547impl Settings {
548    pub(crate) fn text_verbosity_for(&self, provider: &str) -> Option<TextVerbosity> {
549        if provider == crate::providers::OPENAI_CODEX_PROVIDER {
550            return Some(
551                self.openai_responses
552                    .text_verbosity
553                    .unwrap_or(self.openai_codex.text_verbosity),
554            );
555        }
556        self.custom_providers.get(provider).and_then(|custom| {
557            (custom.use_responses_endpoint && custom.supports_text_verbosity)
558                .then_some(self.openai_responses.text_verbosity)
559                .flatten()
560        })
561    }
562}