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