Skip to main content

defect_config/
types.rs

1use std::collections::BTreeMap;
2use std::fmt;
3use std::path::PathBuf;
4
5use defect_agent::error::BoxError;
6use defect_agent::session::{
7    BackgroundProgressConfig, SessionCapabilitiesConfig, TurnConfig, WebSearchCapabilityConfig,
8};
9use serde::{Deserialize, Serialize};
10use toml::Value as TomlValue;
11
12pub(crate) const DEFAULT_ANTHROPIC_MODEL: &str = "claude-sonnet-4-5";
13pub(crate) const DEFAULT_OPENAI_MODEL: &str = "gpt-4o-mini";
14pub(crate) const DEFAULT_DEEPSEEK_MODEL: &str = "deepseek-chat";
15pub(crate) const DEFAULT_ECHO_MODEL: &str = "echo";
16pub(crate) const DEFAULT_BASH_TIMEOUT_MS: u64 = 30_000;
17pub(crate) const DEFAULT_BASH_MAX_TIMEOUT_MS: u64 = 600_000;
18pub(crate) const DEFAULT_BASH_OUTPUT_MAX_BYTES: usize = 1024 * 1024;
19pub(crate) const DEFAULT_FS_READ_LIMIT: u32 = 2_000;
20pub(crate) const DEFAULT_FS_READ_MAX_LIMIT: u32 = 5_000;
21
22pub(crate) const USER_CONFIG_RELATIVE: &str = "defect/config.toml";
23pub(crate) const PROJECT_CONFIG_RELATIVE: &str = ".defect/config.toml";
24pub(crate) const PROJECT_LOCAL_CONFIG_RELATIVE: &str = ".defect/config.local.toml";
25
26const PROVIDER_DEFECT: &str = "defect";
27const PROVIDER_ANTHROPIC: &str = "anthropic";
28const PROVIDER_OPENAI: &str = "openai";
29const PROVIDER_DEEPSEEK: &str = "deepseek";
30const PROVIDER_LITELLM: &str = "litellm";
31
32#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
33#[serde(from = "String", into = "String")]
34pub enum ProviderKind {
35    /// Built-in placeholder provider: echoes the user's most recent message back as-is
36    /// (model id `echo`).
37    /// Requires no external credentials; serves as the fallback default for
38    /// `default.provider`.
39    #[default]
40    Defect,
41    Anthropic,
42    Openai,
43    Deepseek,
44    Litellm,
45    Custom(String),
46}
47
48impl ProviderKind {
49    #[must_use]
50    pub fn as_str(&self) -> &str {
51        match self {
52            Self::Defect => PROVIDER_DEFECT,
53            Self::Anthropic => PROVIDER_ANTHROPIC,
54            Self::Openai => PROVIDER_OPENAI,
55            Self::Deepseek => PROVIDER_DEEPSEEK,
56            Self::Litellm => PROVIDER_LITELLM,
57            Self::Custom(value) => value,
58        }
59    }
60}
61
62impl fmt::Display for ProviderKind {
63    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
64        f.write_str(self.as_str())
65    }
66}
67
68impl From<ProviderKind> for String {
69    fn from(value: ProviderKind) -> Self {
70        value.to_string()
71    }
72}
73
74impl From<String> for ProviderKind {
75    fn from(value: String) -> Self {
76        match value.as_str() {
77            PROVIDER_DEFECT => Self::Defect,
78            PROVIDER_ANTHROPIC => Self::Anthropic,
79            PROVIDER_OPENAI => Self::Openai,
80            PROVIDER_DEEPSEEK => Self::Deepseek,
81            PROVIDER_LITELLM => Self::Litellm,
82            _ => Self::Custom(value),
83        }
84    }
85}
86
87impl From<&str> for ProviderKind {
88    fn from(value: &str) -> Self {
89        match value {
90            PROVIDER_DEFECT => Self::Defect,
91            PROVIDER_ANTHROPIC => Self::Anthropic,
92            PROVIDER_OPENAI => Self::Openai,
93            PROVIDER_DEEPSEEK => Self::Deepseek,
94            PROVIDER_LITELLM => Self::Litellm,
95            other => Self::Custom(other.to_string()),
96        }
97    }
98}
99
100#[derive(Debug, Clone, Copy, PartialEq, Eq)]
101pub enum ConfigSource {
102    Defaults,
103    User,
104    Project,
105    ProjectLocal,
106    Cli,
107}
108
109#[derive(Debug, Clone, PartialEq)]
110pub struct ConfigLayerEntry {
111    pub source: ConfigSource,
112    pub path: Option<PathBuf>,
113    pub raw_toml: Option<String>,
114    pub value: TomlValue,
115}
116
117#[derive(Debug, Clone, Default, PartialEq)]
118pub struct ConfigLayerStack {
119    pub layers: Vec<ConfigLayerEntry>,
120}
121
122#[derive(Debug, Clone, PartialEq, Eq)]
123pub enum ConfigWarning {
124    DeprecatedKey {
125        path: PathBuf,
126        old: String,
127        new: String,
128    },
129    /// A configuration section exists in the file but is inactive under the current mode.
130    ///
131    /// See capabilities semantic mapping.
132    /// Typical scenario: a configuration section is written but the corresponding
133    /// capability is disabled (e.g., the legacy `capabilities.search` section has been
134    /// deprecated in favor of `[capabilities.web_search]`, and the old key no longer
135    /// takes effect).
136    InactiveSection {
137        path: PathBuf,
138        section: String,
139        reason: String,
140    },
141    /// Conflicting MCP tools are renamed to `mcp__<server>__<name>` during session
142    /// startup.
143    ///
144    /// See capabilities for MCP tool classification. All MCP tools are renamed
145    /// (regardless of capability mode or tool enabled) to prevent MCP bypass name
146    /// squatting.
147    McpToolRenamed {
148        server: String,
149        original: String,
150        renamed: String,
151    },
152    /// A `.mcp.json` server was shadowed by a same-named TOML `[mcp.servers.<name>]`
153    /// entry. The TOML entry (more explicit, layered source) wins; the `.mcp.json`
154    /// definition is ignored.
155    McpJsonOverridden { path: PathBuf, server: String },
156}
157
158#[non_exhaustive]
159#[derive(Debug, thiserror::Error)]
160pub enum ConfigError {
161    #[error("failed to read config file {path}: {source}")]
162    Io {
163        path: PathBuf,
164        #[source]
165        source: BoxError,
166    },
167
168    #[error("failed to parse config file {path}: {source}")]
169    Parse {
170        path: PathBuf,
171        #[source]
172        source: BoxError,
173    },
174
175    #[error("invalid config at {path}: {message}")]
176    Invalid { path: PathBuf, message: String },
177
178    #[error(transparent)]
179    Source(#[from] BoxError),
180}
181
182#[derive(Debug, Clone, Default)]
183pub struct CliOverrides {
184    pub provider: Option<ProviderKind>,
185    pub model: Option<String>,
186    pub sandbox: Option<SandboxMode>,
187    pub log_format: Option<LogFormat>,
188    pub config_overrides: Vec<(String, TomlValue)>,
189}
190
191#[derive(Debug, Clone, Default)]
192pub struct LoadConfigOptions {
193    pub cwd: PathBuf,
194    pub cli: CliOverrides,
195    pub xdg_config_home: Option<PathBuf>,
196    pub home_dir: Option<PathBuf>,
197    /// `--local` sandbox mode: ignores global/user-level config and user-level
198    /// agents/skills directories,
199    /// only uses the project root `.defect/`. When `true`, user layers are always absent
200    /// (see
201    /// `resolve_user_config_path` / `resolve_user_agents_dir` /
202    /// `resolve_user_skills_dir`).
203    pub local: bool,
204}
205
206#[derive(Debug, Clone)]
207pub struct LoadedConfig {
208    pub layers: ConfigLayerStack,
209    pub effective: EffectiveConfig,
210    pub warnings: Vec<ConfigWarning>,
211}
212
213#[derive(Debug, Clone)]
214pub struct EffectiveConfig {
215    pub cli: CliConfig,
216    pub turn: TurnConfig,
217    pub base_prompt: BasePromptConfigFile,
218    pub prompt: PromptConfigFile,
219    /// Global capability source selection. Overridden by `providers.<p>.capabilities`
220    /// during session startup.
221    pub capabilities: CapabilitiesConfig,
222    pub providers: ProviderConfigs,
223    pub tools: ToolsConfig,
224    pub sandbox: SandboxConfig,
225    pub tracing: TracingConfig,
226    pub mcp: McpConfig,
227    pub http: HttpClientConfig,
228    /// Resolved hook configuration.
229    pub hooks: HooksConfig,
230}
231
232// Hooks
233
234/// Valid configuration for the hook system: pipelines are grouped by step `event_name`
235/// and executed in declaration order within each group.
236///
237/// Bucket keys are the mount point's `event_name` (snake_case, e.g. `before_turn_end`) —
238/// the same set of names as `defect_agent::hooks::step::ALL_EVENT_NAMES`. A map is used
239/// instead of fixed fields so that adding a new mount point requires no changes at the
240/// config layer.
241#[derive(Debug, Clone, Default, PartialEq)]
242pub struct HooksConfig {
243    /// `event_name` → entries declared under that event, in declaration order.
244    pub buckets: std::collections::BTreeMap<String, Vec<HookEntry>>,
245}
246
247impl HooksConfig {
248    /// Whether any hooks have been declared on this config. When `false`, CLI assembly
249    /// can use the noop engine directly.
250    pub fn is_empty(&self) -> bool {
251        self.buckets.values().all(Vec::is_empty)
252    }
253
254    /// Returns the entries for a given event, or an empty slice if none exist.
255    pub fn get(&self, event_name: &str) -> &[HookEntry] {
256        self.buckets
257            .get(event_name)
258            .map(Vec::as_slice)
259            .unwrap_or(&[])
260    }
261
262    /// Appends a hook entry under the given event name.
263    pub fn push(&mut self, event_name: impl Into<String>, entry: HookEntry) {
264        self.buckets
265            .entry(event_name.into())
266            .or_default()
267            .push(entry);
268    }
269}
270
271/// A single hook configuration: matcher + handler + source layer.
272#[derive(Debug, Clone, PartialEq)]
273pub struct HookEntry {
274    /// Optional human-readable name, used only for tracing/observability to identify this
275    /// hook.
276    /// `None` ⇒ falls back to an anonymous label at assembly time (see `defect-cli`'s
277    /// hook assembly).
278    /// Does not participate in deduplication or disable matching (that only uses matcher
279    /// + handler) — purely for display.
280    pub name: Option<String>,
281    pub matcher: HookMatcher,
282    pub handler: HookHandlerSpec,
283    /// The source layer of this hook. Phase G's trust gating uses it to decide whether
284    /// explicit trust is required.
285    pub source: ConfigSource,
286}
287
288/// Event matcher. Empty fields match all triggers under that event; see the hooks trust
289/// model.
290#[derive(Debug, Clone, Default, PartialEq, Eq, Hash)]
291pub struct HookMatcher {
292    /// Exact tool name match (only `*ToolUse*` events).
293    pub tool: Option<String>,
294    /// Tool name glob match (only `*ToolUse*` events).
295    pub tool_glob: Option<String>,
296    /// `SafetyClass` filter (only for `PreToolUse`); any match triggers. Empty vec = no
297    /// filtering.
298    pub safety: Vec<defect_agent::tool::SafetyClass>,
299}
300
301/// Handler spec — three variants.
302#[non_exhaustive]
303#[derive(Debug, Clone, PartialEq, Eq, Hash)]
304pub enum HookHandlerSpec {
305    /// In-process Rust handler, referenced by name via
306    /// `crate::hooks::builtin::registry()`.
307    Builtin { name: String },
308    /// External command (see hooks command handler).
309    Command(HookCommandSpec),
310    /// Calls an LLM (see hooks prompt handler).
311    Prompt(HookPromptSpec),
312}
313
314#[non_exhaustive]
315#[derive(Debug, Clone, PartialEq, Eq, Hash)]
316pub enum HookCommandSpec {
317    /// Spawn `argv` directly, without any shell.
318    Argv {
319        argv: Vec<String>,
320        /// Windows override; `None` falls back to `argv`.
321        argv_windows: Option<Vec<String>>,
322        cwd: Option<PathBuf>,
323        env: BTreeMap<String, String>,
324        timeout_sec: Option<u64>,
325    },
326    /// Explicit shell. The `shell` field is required; the engine no longer auto-selects
327    /// `sh`.
328    Shell {
329        shell: HookShellKind,
330        command: String,
331        cwd: Option<PathBuf>,
332        env: BTreeMap<String, String>,
333        timeout_sec: Option<u64>,
334    },
335}
336
337#[non_exhaustive]
338#[derive(Debug, Clone, PartialEq, Eq, Hash)]
339pub enum HookShellKind {
340    Sh,
341    Bash,
342    Pwsh,
343    Cmd,
344    /// `program` plus passthrough `args` (excluding the command itself).
345    Custom {
346        program: String,
347        args: Vec<String>,
348    },
349}
350
351#[non_exhaustive]
352#[derive(Debug, Clone, PartialEq, Eq, Hash)]
353pub struct HookPromptSpec {
354    /// `None` = use the session's default model.
355    pub model: Option<String>,
356    pub system: String,
357    pub render: HookPromptRender,
358    pub timeout_sec: Option<u64>,
359}
360
361impl HookPromptSpec {
362    /// Cross-crate construction entry point — struct literals are unavailable after
363    /// `#[non_exhaustive]`.
364    #[must_use]
365    pub fn new(
366        model: Option<String>,
367        system: String,
368        render: HookPromptRender,
369        timeout_sec: Option<u64>,
370    ) -> Self {
371        Self {
372            model,
373            system,
374            render,
375            timeout_sec,
376        }
377    }
378}
379
380#[non_exhaustive]
381#[derive(Debug, Clone, PartialEq, Eq, Hash)]
382pub enum HookPromptRender {
383    /// Feed the JSON-serialized `HookEvent` directly.
384    Json,
385    /// Renders a handlebars template using fields from the event.
386    Template { template: String },
387}
388
389/// Top-level capability configuration entry point.
390///
391/// Structurally equivalent to [`SessionCapabilitiesConfig`]; a separate type on
392/// `EffectiveConfig` is kept so that future non-session-level capabilities can be
393/// added without touching the agent crate. Currently P1 only has `web_search`
394/// (hosted-only); local grep/glob tools are not part of the capability layer and
395/// are managed separately by `[tools.search]`.
396#[non_exhaustive]
397#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
398pub struct CapabilitiesConfig {
399    pub web_search: WebSearchCapabilityConfig,
400}
401
402impl CapabilitiesConfig {
403    /// Construct with a single [`WebSearchCapabilityConfig`]. Cross-crate callers need
404    /// this entry point because the struct is `#[non_exhaustive]` and cannot be built
405    /// with a struct literal directly.
406    #[must_use]
407    pub const fn with_web_search(web_search: WebSearchCapabilityConfig) -> Self {
408        Self { web_search }
409    }
410
411    /// Converts to the agent-side [`SessionCapabilitiesConfig`], for direct consumption
412    /// by
413    /// `DefaultAgentCoreBuilder::capabilities`.
414    #[must_use]
415    pub fn to_session_capabilities(self) -> SessionCapabilitiesConfig {
416        SessionCapabilitiesConfig::with_web_search(self.web_search)
417    }
418}
419
420/// Overrides for global capabilities under a single provider.
421///
422/// A `None` field means "follow the global setting".
423#[non_exhaustive]
424#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
425pub struct ProviderCapabilityOverrides {
426    pub web_search: Option<WebSearchCapabilityConfig>,
427}
428
429impl ProviderCapabilityOverrides {
430    /// Construct with a single `web_search` override. `None` means follow the global
431    /// setting.
432    #[must_use]
433    pub const fn with_web_search(web_search: Option<WebSearchCapabilityConfig>) -> Self {
434        Self { web_search }
435    }
436
437    /// Merges the global [`CapabilitiesConfig`] with this provider's overrides.
438    /// Unset fields fall back to the global values.
439    #[must_use]
440    pub fn merge_into(&self, base: CapabilitiesConfig) -> CapabilitiesConfig {
441        CapabilitiesConfig::with_web_search(self.web_search.unwrap_or(base.web_search))
442    }
443}
444
445#[derive(Debug, Clone, PartialEq, Eq)]
446pub struct CliConfig {
447    pub provider: ProviderKind,
448    pub model: String,
449}
450
451#[derive(Debug, Clone, PartialEq, Eq, Default)]
452pub struct BasePromptConfigFile {
453    pub file: Option<PathBuf>,
454    pub text: Option<String>,
455}
456
457#[derive(Debug, Clone, PartialEq, Eq, Default)]
458pub struct PromptConfigFile {
459    pub file: String,
460    pub text: Option<String>,
461    pub provider_overlays: BTreeMap<String, String>,
462    pub model_overlays: BTreeMap<String, String>,
463}
464
465#[derive(Debug, Clone, PartialEq, Default)]
466pub struct ProviderConfigs {
467    pub anthropic: ProviderConfigFile,
468    pub openai: ProviderConfigFile,
469    pub deepseek: ProviderConfigFile,
470    pub litellm: ProviderConfigFile,
471    pub custom: BTreeMap<String, ProviderConfigFile>,
472}
473
474impl ProviderConfigs {
475    #[must_use]
476    pub fn get(&self, provider: &ProviderKind) -> Option<&ProviderConfigFile> {
477        match provider {
478            ProviderKind::Defect => None,
479            ProviderKind::Anthropic => Some(&self.anthropic),
480            ProviderKind::Openai => Some(&self.openai),
481            ProviderKind::Deepseek => Some(&self.deepseek),
482            ProviderKind::Litellm => Some(&self.litellm),
483            ProviderKind::Custom(name) => self.custom.get(name),
484        }
485    }
486}
487
488#[derive(Debug, Clone, PartialEq, Eq, Default)]
489pub struct ToolsConfig {
490    pub bash: BashToolConfig,
491    pub fs: FsToolConfig,
492    /// The `[tools.fetch]` section. Reserved — P1 only lands the schema; the tool
493    /// implementation will follow in a later PR.
494    pub fetch: FetchToolConfig,
495    /// The `[tools.search]` section. Parameters for the local `search` tool (grep/glob).
496    /// Independent of `[capabilities.web_search]`; registration is controlled separately
497    /// by `enabled`.
498    /// See search tool config.
499    pub search: SearchToolConfig,
500    /// `[tools.background]` section. Configuration for the background subagent progress
501    /// view (progress ring capacity / per-block text character limit) — the "last N
502    /// blocks" that the main agent sees via `inspect_background_task`. The source of
503    /// truth is on the agent side ([`BackgroundProgressConfig`]); this is a direct reuse.
504    pub background: BackgroundProgressConfig,
505}
506
507/// Configuration for the local fetch tool.
508#[non_exhaustive]
509#[derive(Debug, Clone, PartialEq, Eq)]
510pub struct FetchToolConfig {
511    pub enabled: bool,
512    pub default_timeout_secs: u32,
513    pub max_timeout_secs: u32,
514    pub max_response_bytes: u64,
515    pub default_format: FetchFormat,
516    pub html_to_markdown: bool,
517    pub follow_redirects: bool,
518}
519
520impl Default for FetchToolConfig {
521    fn default() -> Self {
522        Self {
523            enabled: true,
524            default_timeout_secs: 30,
525            max_timeout_secs: 120,
526            max_response_bytes: 5 * 1024 * 1024,
527            default_format: FetchFormat::Markdown,
528            html_to_markdown: true,
529            follow_redirects: true,
530        }
531    }
532}
533
534#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
535#[serde(rename_all = "snake_case")]
536pub enum FetchFormat {
537    #[default]
538    Markdown,
539    Html,
540    Text,
541}
542
543#[derive(Debug, Clone, PartialEq, Eq)]
544pub struct BashToolConfig {
545    pub default_timeout_ms: u64,
546    pub max_timeout_ms: u64,
547    /// Maximum bytes of merged stdout/stderr captured per command; output beyond this is
548    /// dropped and reported as truncated. Applies to the local shell backend.
549    pub output_max_bytes: usize,
550}
551
552impl Default for BashToolConfig {
553    fn default() -> Self {
554        Self {
555            default_timeout_ms: DEFAULT_BASH_TIMEOUT_MS,
556            max_timeout_ms: DEFAULT_BASH_MAX_TIMEOUT_MS,
557            output_max_bytes: DEFAULT_BASH_OUTPUT_MAX_BYTES,
558        }
559    }
560}
561
562#[derive(Debug, Clone, PartialEq, Eq)]
563pub struct FsToolConfig {
564    pub read_default_limit: u32,
565    pub read_max_limit: u32,
566}
567
568impl Default for FsToolConfig {
569    fn default() -> Self {
570        Self {
571            read_default_limit: DEFAULT_FS_READ_LIMIT,
572            read_max_limit: DEFAULT_FS_READ_MAX_LIMIT,
573        }
574    }
575}
576
577/// Configuration for the local search tool.
578#[non_exhaustive]
579#[derive(Debug, Clone, PartialEq, Eq)]
580pub struct SearchToolConfig {
581    pub enabled: bool,
582    pub default_head_limit: u32,
583    pub max_head_limit: u32,
584    pub max_file_size_bytes: u64,
585    pub max_result_bytes: u64,
586    pub max_walk_files: u64,
587    pub respect_gitignore_default: bool,
588}
589
590impl Default for SearchToolConfig {
591    fn default() -> Self {
592        Self {
593            enabled: true,
594            default_head_limit: 100,
595            max_head_limit: 1000,
596            max_file_size_bytes: 16 * 1024 * 1024,
597            max_result_bytes: 256 * 1024,
598            max_walk_files: 100_000,
599            respect_gitignore_default: true,
600        }
601    }
602}
603
604#[derive(Debug, Clone, PartialEq, Eq)]
605pub struct SandboxConfig {
606    pub mode: SandboxMode,
607}
608
609impl Default for SandboxConfig {
610    fn default() -> Self {
611        Self {
612            mode: SandboxMode::AskWrites,
613        }
614    }
615}
616
617#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
618#[serde(rename_all = "kebab-case")]
619pub enum SandboxMode {
620    ReadOnly,
621    #[default]
622    AskWrites,
623    Open,
624    DenyAll,
625}
626
627impl SandboxMode {
628    pub fn as_str(self) -> &'static str {
629        match self {
630            Self::ReadOnly => "read-only",
631            Self::AskWrites => "ask-writes",
632            Self::Open => "open",
633            Self::DenyAll => "deny-all",
634        }
635    }
636}
637
638pub type AnthropicConfigFile = ProviderConfigFile;
639pub type OpenAiConfigFile = ProviderConfigFile;
640pub type DeepSeekConfigFile = ProviderConfigFile;
641pub type LiteLlmConfigFile = ProviderConfigFile;
642
643#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
644#[serde(rename_all = "kebab-case")]
645pub enum ProviderProtocol {
646    AnthropicMessages,
647    OpenaiChat,
648}
649
650/// A model candidate configuration declaration.
651///
652/// TOML supports two forms (accepted via [`Deserialize`] with `untagged`):
653/// - Plain string `"gpt-5.5"`: only the id is given; the UI display name falls back to
654///   the id.
655/// - Table `{ id = "...", name = "Opus 4.8" }`: pairs a long id with a short display
656///   name.
657///
658/// `name` is mapped to [`defect_agent::llm::ModelInfo::display_name`]; the ACP uses it as
659/// the label for model selector options. When `None`, the wire layer falls back to the
660/// id.
661///
662/// `context_window` / `max_output_tokens` let the user declare model metadata that the
663/// provider cannot discover at runtime — most importantly for **Bedrock**, whose SDK does
664/// not return model limits, so without this the compaction watermarks have no window to key
665/// off and the context can grow unbounded. Both are optional; when absent the provider's
666/// own value (if any) or the compaction fallback applies.
667#[derive(Debug, Clone, PartialEq, Deserialize)]
668#[serde(untagged)]
669pub enum ModelEntry {
670    /// A plain ID (legacy format).
671    Id(String),
672    /// A detailed entry with a display name and optional limits.
673    Detailed {
674        id: String,
675        #[serde(default)]
676        name: Option<String>,
677        #[serde(default)]
678        context_window: Option<u64>,
679        #[serde(default)]
680        max_output_tokens: Option<u64>,
681        /// How extended thinking is serialized on the Anthropic Messages wire for this
682        /// model. Defaults to [`ThinkingFormat::Adaptive`] — the format newer models
683        /// (Opus 4.6+, Sonnet 4.6, Fable) require. Set to [`ThinkingFormat::Legacy`] for
684        /// older models (Sonnet 4.5, Haiku 4.5, …) that only accept the
685        /// `thinking.type=enabled` + `budget_tokens` shape and reject `adaptive` /
686        /// `output_config.effort`. Ignored by the OpenAI-compatible protocol.
687        #[serde(default)]
688        thinking_format: Option<ThinkingFormat>,
689    },
690}
691
692/// Anthropic Messages thinking wire format for a model.
693///
694/// The codec layer has its own `defect_llm` enum; this config-layer enum exists so
695/// `defect-config` need not depend on `defect-llm` (one-way crate dependency), mirroring
696/// the [`ReasoningEffort`] split.
697#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
698#[serde(rename_all = "snake_case")]
699pub enum ThinkingFormat {
700    /// `thinking.type=adaptive` + `output_config.effort` (Opus 4.6+, Sonnet 4.6, Fable).
701    Adaptive,
702    /// `thinking.type=enabled` + `budget_tokens` (Sonnet 4.5, Haiku 4.5, older models).
703    Legacy,
704}
705
706impl ModelEntry {
707    /// The model ID (present in both variants).
708    #[must_use]
709    pub fn id(&self) -> &str {
710        match self {
711            Self::Id(id) => id,
712            Self::Detailed { id, .. } => id,
713        }
714    }
715
716    /// Optional display name (only available in table form).
717    #[must_use]
718    pub fn name(&self) -> Option<&str> {
719        match self {
720            Self::Id(_) => None,
721            Self::Detailed { name, .. } => name.as_deref(),
722        }
723    }
724
725    /// Optional context-window size (tokens), only from the table form.
726    #[must_use]
727    pub fn context_window(&self) -> Option<u64> {
728        match self {
729            Self::Id(_) => None,
730            Self::Detailed { context_window, .. } => *context_window,
731        }
732    }
733
734    /// Optional max output tokens, only from the table form.
735    #[must_use]
736    pub fn max_output_tokens(&self) -> Option<u64> {
737        match self {
738            Self::Id(_) => None,
739            Self::Detailed {
740                max_output_tokens, ..
741            } => *max_output_tokens,
742        }
743    }
744
745    /// Optional thinking wire format override, only from the table form.
746    #[must_use]
747    pub fn thinking_format(&self) -> Option<ThinkingFormat> {
748        match self {
749            Self::Id(_) => None,
750            Self::Detailed {
751                thinking_format, ..
752            } => *thinking_format,
753        }
754    }
755}
756
757#[derive(Debug, Clone, PartialEq, Default)]
758pub struct ProviderConfigFile {
759    pub protocol: Option<ProviderProtocol>,
760    pub base_url: Option<String>,
761    pub default_model: Option<String>,
762    pub models: Option<Vec<ModelEntry>>,
763    pub display_name: Option<String>,
764    pub api_key_env: Option<String>,
765    pub organization: Option<String>,
766    pub project: Option<String>,
767    pub aws: Option<ProviderAwsConfigFile>,
768    pub headers: BTreeMap<String, String>,
769    /// Overrides the authentication header name for the Anthropic Messages protocol
770    /// (`x-api-key` by default). Some gateways fronting the protocol expect `api-key`
771    /// instead. Ignored by the OpenAI-compatible protocol (which uses bearer auth via
772    /// `Authorization`).
773    pub auth_header: Option<String>,
774    pub capabilities: ProviderCapabilityOverrides,
775    /// `reasoning_effort` wire parameter. `None` = do not send, use provider default.
776    pub reasoning_effort: Option<ReasoningEffort>,
777}
778
779#[derive(Debug, Clone, PartialEq, Eq, Default, Deserialize)]
780#[serde(deny_unknown_fields)]
781pub struct ProviderAwsConfigFile {
782    pub profile: Option<String>,
783    pub region: Option<String>,
784    /// `anthropic_beta` flags injected into the Bedrock request body (Anthropic Messages
785    /// `anthropic_beta` array). Default (absent) sends nothing. Some newer models (e.g. Opus
786    /// 4.8) reject the default data retention mode and require `["no-data-retention-v1"]`;
787    /// set it explicitly. The flag is shared by every model under this provider, and Bedrock
788    /// 400s a model that does not support a given flag — so only enable it on a
789    /// provider whose models all accept the flag.
790    pub anthropic_beta: Option<Vec<String>>,
791}
792
793/// Values for the `reasoning_effort` parameter in the OpenAI-compatible protocol.
794///
795/// 1:1 mapping with the official OpenAI wire enum: `xhigh` is only supported after
796/// `gpt-5.1-codex-max`, and `none` is only supported after `gpt-5.1`. The configuration
797/// layer does not distinguish between models; values are passed through as-is and
798/// validated upstream.
799#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
800#[serde(rename_all = "snake_case")]
801pub enum ReasoningEffort {
802    None,
803    Minimal,
804    Low,
805    Medium,
806    High,
807    Xhigh,
808}
809
810/// Typed configuration for the HTTP client stack.
811///
812/// Only captures user intent (`None` always means "use the HTTP stack layer default");
813/// the CLI entry point converts it to `defect_http::HttpStackConfig` when assembling the
814/// provider.
815///
816/// Not sharing a type directly with `defect_http::HttpStackConfig` preserves a one-way
817/// crate dependency: `defect-config` does not depend on `defect-http`, preventing
818/// downstream consumers like fetch tool from creating a reverse dependency.
819#[derive(Debug, Clone, PartialEq, Eq, Default)]
820pub struct HttpClientConfig {
821    /// Total timeout for a single request; `None` means use the HTTP stack default
822    /// (600s).
823    pub total_timeout_ms: Option<u64>,
824    /// Maximum number of transport error retries (excluding the first attempt); `None`
825    /// means default 2, `Some(0)` disables retries.
826    pub transport_retries: Option<u8>,
827    /// Initial backoff for retries; `None` = default 200ms.
828    pub initial_backoff_ms: Option<u64>,
829    /// Override the `User-Agent` header; `None` uses the compile-time default
830    /// `defect-http/{version} ({git_sha})`.
831    pub user_agent: Option<String>,
832    /// Proxy sub-configuration. `mode` defaults to `FromEnv` (reads `HTTP_PROXY` etc.).
833    pub proxy: HttpProxyConfig,
834}
835
836#[derive(Debug, Clone, PartialEq, Eq)]
837pub struct HttpProxyConfig {
838    pub mode: HttpProxyMode,
839    /// Explicit proxy; only effective when `mode = Explicit`.
840    pub explicit: HttpProxySettings,
841}
842
843impl Default for HttpProxyConfig {
844    fn default() -> Self {
845        Self {
846            mode: HttpProxyMode::FromEnv,
847            explicit: HttpProxySettings::default(),
848        }
849    }
850}
851
852#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
853#[serde(rename_all = "kebab-case")]
854pub enum HttpProxyMode {
855    #[default]
856    FromEnv,
857    Disabled,
858    Explicit,
859}
860
861impl HttpProxyMode {
862    pub fn as_str(self) -> &'static str {
863        match self {
864            Self::FromEnv => "from-env",
865            Self::Disabled => "disabled",
866            Self::Explicit => "explicit",
867        }
868    }
869}
870
871#[derive(Debug, Clone, PartialEq, Eq, Default)]
872pub struct HttpProxySettings {
873    pub http_proxy: Option<String>,
874    pub https_proxy: Option<String>,
875    pub no_proxy: Vec<String>,
876}
877
878/// Output format for the tracing-subscriber log sink (stderr).
879///
880/// Independent of `--format`, which controls the agent's stdout event stream.
881#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
882#[serde(rename_all = "kebab-case")]
883pub enum LogFormat {
884    /// Human-readable text, ANSI colors when stderr is a terminal.
885    #[default]
886    Text,
887    /// One JSON object per log line (JSONL/NDJSON).
888    Jsonl,
889}
890
891impl LogFormat {
892    pub fn as_str(self) -> &'static str {
893        match self {
894            Self::Text => "text",
895            Self::Jsonl => "jsonl",
896        }
897    }
898}
899
900#[derive(Debug, Clone, PartialEq, Default)]
901pub struct TracingConfig {
902    pub filter: Option<String>,
903    pub format: LogFormat,
904    pub otlp: Option<OtlpTracingConfig>,
905    pub langfuse: Option<LangfuseConfig>,
906}
907
908#[derive(Debug, Clone, PartialEq, Default)]
909pub struct OtlpTracingConfig {
910    pub endpoint: Option<String>,
911}
912
913/// Langfuse upload configuration.
914///
915/// Disabled by default; if `enabled = true` but keys are missing, the assembly layer
916/// warns and disables it (no silent success).
917#[derive(Debug, Clone, PartialEq, Eq, Default)]
918pub struct LangfuseConfig {
919    pub enabled: bool,
920    /// Langfuse host, e.g. `https://cloud.langfuse.com`. `None` uses the assembly layer
921    /// default.
922    pub host: Option<String>,
923    pub public_key: Option<String>,
924    pub secret_key: Option<String>,
925    /// Flush interval in milliseconds. `None` uses the assembly layer default.
926    pub flush_interval_ms: Option<u64>,
927    /// Maximum number of events per batch. `None` uses the assembly layer default.
928    pub max_batch: Option<usize>,
929}
930
931#[derive(Debug, Clone, PartialEq, Eq, Default)]
932pub struct McpConfig {
933    pub enabled_servers: Vec<String>,
934    pub servers: BTreeMap<String, McpServerConfig>,
935}
936
937#[derive(Debug, Clone, PartialEq, Eq)]
938pub enum McpServerConfig {
939    Stdio(McpStdioServerConfig),
940    Http(McpRemoteServerConfig),
941    Sse(McpRemoteServerConfig),
942}
943
944#[derive(Debug, Clone, PartialEq, Eq)]
945pub struct McpStdioServerConfig {
946    pub command: String,
947    pub args: Vec<String>,
948    pub env: BTreeMap<String, String>,
949}
950
951#[derive(Debug, Clone, PartialEq, Eq)]
952pub struct McpRemoteServerConfig {
953    pub url: String,
954    pub headers: BTreeMap<String, String>,
955}
956
957#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
958#[serde(rename_all = "snake_case")]
959pub(crate) enum McpTransportKind {
960    Stdio,
961    Http,
962    Sse,
963}
964
965#[derive(Debug, Clone, Default, Deserialize)]
966#[serde(deny_unknown_fields)]
967pub(crate) struct ConfigToml {
968    #[serde(default)]
969    pub(crate) default: DefaultSection,
970    #[serde(default)]
971    pub(crate) base_prompt: BasePromptSection,
972    #[serde(default)]
973    pub(crate) prompt: PromptSection,
974    #[serde(default)]
975    pub(crate) turn: TurnSection,
976    #[serde(default)]
977    pub(crate) capabilities: CapabilitiesSection,
978    #[serde(default)]
979    pub(crate) providers: ProvidersSection,
980    #[serde(default)]
981    pub(crate) tools: ToolsSection,
982    #[serde(default)]
983    pub(crate) sandbox: SandboxSection,
984    #[serde(default)]
985    pub(crate) tracing: TracingSection,
986    #[serde(default)]
987    pub(crate) mcp: McpSection,
988    #[serde(default)]
989    pub(crate) http: HttpSection,
990    /// The `[hooks]` section is not processed by `ConfigToml::try_into` (its array
991    /// semantics are append+dedupe; see `crate::hooks`). We absorb it here as
992    /// `toml::Value` to prevent `deny_unknown_fields` from misidentifying `[[hooks.*]]`
993    /// as an unknown section; hooks' own parser performs schema validation.
994    #[serde(default)]
995    #[allow(dead_code)]
996    pub(crate) hooks: Option<TomlValue>,
997}
998
999#[derive(Debug, Clone, Default, Deserialize)]
1000#[serde(deny_unknown_fields)]
1001pub(crate) struct CapabilitiesSection {
1002    pub(crate) web_search: Option<WebSearchCapabilitySection>,
1003}
1004
1005#[derive(Debug, Clone, Default, Deserialize)]
1006#[serde(deny_unknown_fields)]
1007pub(crate) struct WebSearchCapabilitySection {
1008    pub(crate) mode: Option<defect_agent::session::WebSearchCapabilityMode>,
1009}
1010
1011#[derive(Debug, Clone, Default, Deserialize)]
1012#[serde(deny_unknown_fields)]
1013pub(crate) struct ProviderCapabilitiesSection {
1014    pub(crate) web_search: Option<WebSearchCapabilitySection>,
1015}
1016
1017#[derive(Debug, Clone, Default, Deserialize)]
1018#[serde(deny_unknown_fields)]
1019pub(crate) struct DefaultSection {
1020    pub(crate) provider: Option<ProviderKind>,
1021    pub(crate) model: Option<String>,
1022}
1023
1024#[derive(Debug, Clone, Default, Deserialize)]
1025#[serde(deny_unknown_fields)]
1026pub(crate) struct BasePromptSection {
1027    pub(crate) file: Option<String>,
1028    pub(crate) text: Option<String>,
1029}
1030
1031/// How `[turn] request_limit` is interpreted. The numeric `request_limit` supplies `N`;
1032/// this key selects the strategy. Defaults to `Adaptive` when omitted, so a bare
1033/// `request_limit = N` keeps its historical self-expanding behavior.
1034#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
1035#[serde(rename_all = "snake_case")]
1036pub(crate) enum RequestLimitMode {
1037    /// Hard cap at `N` LLM calls; never expands.
1038    Fixed,
1039    /// Start at `N`; each executed tool raises the cap by one (the default).
1040    Adaptive,
1041    /// No cap at all; `N` is ignored.
1042    Unbounded,
1043}
1044
1045#[derive(Debug, Clone, Default, Deserialize)]
1046#[serde(deny_unknown_fields)]
1047pub(crate) struct TurnSection {
1048    pub(crate) system_prompt: Option<String>,
1049    /// `[turn.sampling]` — per-call generation parameters for the **main agent**
1050    /// (`max_tokens` / `temperature` / `top_p` / `top_k`). Mirrors the subagent profile's
1051    /// `[sampling]` section. Omitted fields fall back to the provider default; in
1052    /// particular, a missing `max_tokens` lets the Anthropic protocol layer apply its own
1053    /// fallback. `reasoning_effort` is *not* here — it is a provider-level concern wired in
1054    /// `cli/providers.rs` and switchable per-session via ACP.
1055    pub(crate) sampling: Option<SamplingSection>,
1056    pub(crate) request_limit: Option<u32>,
1057    /// Strategy for `request_limit`. `None` ⇒ `Adaptive` (back-compatible).
1058    pub(crate) request_limit_mode: Option<RequestLimitMode>,
1059    pub(crate) compact_threshold_tokens: Option<u64>,
1060    pub(crate) compact_ratio: Option<f64>,
1061    /// Enables background full compaction (asynchronous summarization when the soft
1062    /// watermark is exceeded, without blocking the current turn).
1063    pub(crate) background_compact_enabled: Option<bool>,
1064    /// Background compaction soft watermark as a fraction of `context_window` (default
1065    /// 0.7).
1066    pub(crate) compact_soft_ratio: Option<f64>,
1067    /// Enables micro-compaction: cleans oversized `tool_result` entries from older turns
1068    /// without invoking the LLM.
1069    pub(crate) microcompact_enabled: Option<bool>,
1070    /// Micro‑compact watermark as a fraction of `context_window` (default 0.6).
1071    pub(crate) microcompact_ratio: Option<f64>,
1072    pub(crate) max_llm_retries: Option<u32>,
1073    pub(crate) max_concurrent_tools: Option<usize>,
1074    /// Hard upper limit on forced continues from the `before turn-end` hook. `None` ⇒ use
1075    /// the agent-side default (3).
1076    pub(crate) max_hook_continues: Option<u32>,
1077    /// Maximum subagent vertical recursion depth. `None` ⇒ use the agent-side default
1078    /// (4). `0` ⇒ disallow dispatching any subagent (the top-level tool set does not
1079    /// contain `spawn_agent`).
1080    pub(crate) subagent_max_depth: Option<u32>,
1081}
1082
1083/// `[turn.sampling]` — main-agent generation parameters. Each field is independently
1084/// optional and overrides the corresponding [`SamplingParams`](defect_agent::llm::SamplingParams)
1085/// default only when present. Mirrors the subagent profile's `[sampling]` section.
1086#[derive(Debug, Clone, Default, Deserialize)]
1087#[serde(deny_unknown_fields)]
1088pub(crate) struct SamplingSection {
1089    /// Maximum tokens the model may generate in a single response. Omitted ⇒ the protocol
1090    /// layer's own fallback applies (e.g. Anthropic's `DEFAULT_MAX_TOKENS`).
1091    pub(crate) max_tokens: Option<u32>,
1092    pub(crate) temperature: Option<f32>,
1093    pub(crate) top_p: Option<f32>,
1094    pub(crate) top_k: Option<u32>,
1095}
1096
1097#[derive(Debug, Clone, Default, Deserialize)]
1098#[serde(deny_unknown_fields)]
1099pub(crate) struct PromptSection {
1100    pub(crate) file: Option<String>,
1101    pub(crate) text: Option<String>,
1102    pub(crate) providers: Option<BTreeMap<String, PromptOverlaySection>>,
1103    pub(crate) models: Option<BTreeMap<String, String>>,
1104}
1105
1106#[derive(Debug, Clone, Default, Deserialize)]
1107#[serde(deny_unknown_fields)]
1108pub(crate) struct PromptOverlaySection {
1109    pub(crate) text: Option<String>,
1110}
1111
1112#[derive(Debug, Clone, Default, Deserialize)]
1113pub(crate) struct ProvidersSection {
1114    pub(crate) anthropic: Option<AnthropicProviderSection>,
1115    pub(crate) openai: Option<OpenAiProviderSection>,
1116    pub(crate) deepseek: Option<DeepSeekProviderSection>,
1117    pub(crate) litellm: Option<LiteLlmProviderSection>,
1118    #[serde(flatten)]
1119    pub(crate) custom: BTreeMap<String, ProviderSection>,
1120}
1121
1122pub(crate) type AnthropicProviderSection = ProviderSection;
1123pub(crate) type OpenAiProviderSection = ProviderSection;
1124pub(crate) type DeepSeekProviderSection = ProviderSection;
1125pub(crate) type LiteLlmProviderSection = ProviderSection;
1126
1127#[derive(Debug, Clone, Default, Deserialize)]
1128#[serde(deny_unknown_fields)]
1129pub(crate) struct ProviderSection {
1130    pub(crate) protocol: Option<ProviderProtocol>,
1131    pub(crate) base_url: Option<String>,
1132    pub(crate) default_model: Option<String>,
1133    pub(crate) models: Option<Vec<ModelEntry>>,
1134    pub(crate) display_name: Option<String>,
1135    pub(crate) api_key_env: Option<String>,
1136    pub(crate) organization: Option<String>,
1137    pub(crate) project: Option<String>,
1138    pub(crate) aws: Option<ProviderAwsConfigFile>,
1139    pub(crate) headers: Option<BTreeMap<String, String>>,
1140    pub(crate) auth_header: Option<String>,
1141    pub(crate) capabilities: Option<ProviderCapabilitiesSection>,
1142    pub(crate) reasoning_effort: Option<ReasoningEffort>,
1143}
1144
1145#[derive(Debug, Clone, Default, Deserialize)]
1146#[serde(deny_unknown_fields)]
1147pub(crate) struct ToolsSection {
1148    pub(crate) bash: Option<BashToolSection>,
1149    pub(crate) fs: Option<FsToolSection>,
1150    pub(crate) fetch: Option<FetchToolSection>,
1151    /// `[tools.search]`: parameters for the local `search` tool (grep/glob). Registration
1152    /// depends solely on `enabled` and is completely independent of
1153    /// `[capabilities.web_search]`.
1154    pub(crate) search: Option<SearchToolSection>,
1155    /// `[tools.background]`: background subagent progress view (ring capacity / text
1156    /// limit).
1157    pub(crate) background: Option<BackgroundToolSection>,
1158}
1159
1160#[derive(Debug, Clone, Default, Deserialize)]
1161#[serde(deny_unknown_fields)]
1162pub(crate) struct BackgroundToolSection {
1163    /// Default number of recent message blocks returned when `inspect` is called without
1164    /// `recent_blocks`. Defaults to 10.
1165    pub(crate) default_recent_blocks: Option<usize>,
1166    /// Character limit for free-form body text in a single block (assistant/thought/tool
1167    /// result). Default 0 = keep only summary/metadata.
1168    pub(crate) block_text_limit: Option<usize>,
1169    /// How many finished background-task entries to retain in the task table before the
1170    /// oldest are evicted. Default 64. Bounds memory for long sessions with many tasks.
1171    pub(crate) finished_tasks_cap: Option<usize>,
1172}
1173
1174#[derive(Debug, Clone, Default, Deserialize)]
1175#[serde(deny_unknown_fields)]
1176pub(crate) struct SearchToolSection {
1177    pub(crate) enabled: Option<bool>,
1178    pub(crate) default_head_limit: Option<u32>,
1179    pub(crate) max_head_limit: Option<u32>,
1180    pub(crate) max_file_size_bytes: Option<u64>,
1181    pub(crate) max_result_bytes: Option<u64>,
1182    pub(crate) max_walk_files: Option<u64>,
1183    pub(crate) respect_gitignore_default: Option<bool>,
1184}
1185
1186#[derive(Debug, Clone, Default, Deserialize)]
1187#[serde(deny_unknown_fields)]
1188pub(crate) struct FetchToolSection {
1189    pub(crate) enabled: Option<bool>,
1190    pub(crate) default_timeout_secs: Option<u32>,
1191    pub(crate) max_timeout_secs: Option<u32>,
1192    pub(crate) max_response_bytes: Option<u64>,
1193    pub(crate) default_format: Option<FetchFormat>,
1194    pub(crate) html_to_markdown: Option<bool>,
1195    pub(crate) follow_redirects: Option<bool>,
1196}
1197
1198#[derive(Debug, Clone, Default, Deserialize)]
1199#[serde(deny_unknown_fields)]
1200pub(crate) struct BashToolSection {
1201    pub(crate) default_timeout_ms: Option<u64>,
1202    pub(crate) max_timeout_ms: Option<u64>,
1203    pub(crate) output_max_bytes: Option<usize>,
1204}
1205
1206#[derive(Debug, Clone, Default, Deserialize)]
1207#[serde(deny_unknown_fields)]
1208pub(crate) struct FsToolSection {
1209    pub(crate) read_default_limit: Option<u32>,
1210    pub(crate) read_max_limit: Option<u32>,
1211}
1212
1213#[derive(Debug, Clone, Default, Deserialize)]
1214#[serde(deny_unknown_fields)]
1215pub(crate) struct SandboxSection {
1216    pub(crate) mode: Option<SandboxMode>,
1217}
1218
1219#[derive(Debug, Clone, Default, Deserialize)]
1220#[serde(deny_unknown_fields)]
1221pub(crate) struct TracingSection {
1222    pub(crate) filter: Option<String>,
1223    pub(crate) format: Option<LogFormat>,
1224    pub(crate) otlp: Option<OtlpTracingSection>,
1225    pub(crate) langfuse: Option<LangfuseSection>,
1226}
1227
1228#[derive(Debug, Clone, Default, Deserialize)]
1229#[serde(deny_unknown_fields)]
1230pub(crate) struct OtlpTracingSection {
1231    pub(crate) endpoint: Option<String>,
1232}
1233
1234#[derive(Debug, Clone, Default, Deserialize)]
1235#[serde(rename_all = "snake_case", deny_unknown_fields)]
1236pub(crate) struct LangfuseSection {
1237    pub(crate) enabled: Option<bool>,
1238    pub(crate) host: Option<String>,
1239    pub(crate) public_key: Option<String>,
1240    pub(crate) secret_key: Option<String>,
1241    pub(crate) flush_interval_ms: Option<u64>,
1242    pub(crate) max_batch: Option<usize>,
1243}
1244
1245#[derive(Debug, Clone, Default, Deserialize)]
1246#[serde(deny_unknown_fields)]
1247pub(crate) struct McpSection {
1248    pub(crate) enabled_servers: Option<Vec<String>>,
1249    pub(crate) servers: Option<BTreeMap<String, McpServerSection>>,
1250}
1251
1252#[derive(Debug, Clone, Default, Deserialize)]
1253#[serde(deny_unknown_fields)]
1254pub(crate) struct HttpSection {
1255    pub(crate) total_timeout_ms: Option<u64>,
1256    pub(crate) transport_retries: Option<u8>,
1257    pub(crate) initial_backoff_ms: Option<u64>,
1258    pub(crate) user_agent: Option<String>,
1259    pub(crate) proxy: Option<HttpProxySection>,
1260}
1261
1262#[derive(Debug, Clone, Default, Deserialize)]
1263#[serde(deny_unknown_fields)]
1264pub(crate) struct HttpProxySection {
1265    pub(crate) mode: Option<HttpProxyMode>,
1266    pub(crate) http_proxy: Option<String>,
1267    pub(crate) https_proxy: Option<String>,
1268    pub(crate) no_proxy: Option<Vec<String>>,
1269}
1270
1271#[derive(Debug, Clone, Default, Deserialize)]
1272#[serde(deny_unknown_fields)]
1273pub(crate) struct McpServerSection {
1274    pub(crate) transport: Option<McpTransportKind>,
1275    pub(crate) command: Option<String>,
1276    pub(crate) args: Option<Vec<String>>,
1277    pub(crate) env: Option<BTreeMap<String, String>>,
1278    pub(crate) url: Option<String>,
1279    pub(crate) headers: Option<BTreeMap<String, String>>,
1280}