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