Skip to main content

magi_code/config/settings/
tools.rs

1use schemars::JsonSchema;
2use serde::{Deserialize, Serialize};
3
4use crate::subagents::{DEFAULT_SUBAGENT_MAX_DEPTH, MAX_SUBAGENT_MAX_DEPTH};
5
6use super::helpers::default_true;
7
8#[derive(Debug, Clone, Default, PartialEq, Eq)]
9pub struct ToolSettings {
10    pub read: ReadToolSettings,
11    pub view_image: ViewImageToolSettings,
12    pub hash_edit: HashEditToolSettings,
13    pub write: WriteToolSettings,
14    pub grep: GrepToolSettings,
15    pub find: FindToolSettings,
16    pub list_files: ListFilesToolSettings,
17    pub ast_grep: AstGrepToolSettings,
18    pub bash: BashToolSettings,
19    pub subagents: SubagentsToolSettings,
20    pub disabled: Vec<String>,
21}
22
23#[derive(Serialize, Deserialize, JsonSchema)]
24struct ToolSettingsWire {
25    #[serde(default, skip_serializing_if = "ReadToolSettings::is_default")]
26    #[schemars(!skip_serializing_if)]
27    read: ReadToolSettings,
28    #[serde(default, skip_serializing_if = "ViewImageToolSettings::is_default")]
29    #[schemars(!skip_serializing_if)]
30    view_image: ViewImageToolSettings,
31    #[serde(default, skip_serializing_if = "HashEditToolSettings::is_default")]
32    #[schemars(!skip_serializing_if)]
33    hash_edit: HashEditToolSettings,
34    #[serde(default, skip_serializing_if = "WriteToolSettings::is_default")]
35    #[schemars(!skip_serializing_if)]
36    write: WriteToolSettings,
37    #[serde(default, skip_serializing_if = "GrepToolSettings::is_default")]
38    #[schemars(!skip_serializing_if)]
39    grep: GrepToolSettings,
40    #[serde(default, skip_serializing_if = "FindToolSettings::is_default")]
41    #[schemars(!skip_serializing_if)]
42    find: FindToolSettings,
43    #[serde(default, skip_serializing_if = "ListFilesToolSettings::is_default")]
44    #[schemars(!skip_serializing_if)]
45    list_files: ListFilesToolSettings,
46    #[serde(default, skip_serializing_if = "AstGrepToolSettings::is_default")]
47    #[schemars(!skip_serializing_if)]
48    ast_grep: AstGrepToolSettings,
49    #[serde(default, skip_serializing_if = "BashToolSettings::is_default")]
50    #[schemars(!skip_serializing_if)]
51    bash: BashToolSettings,
52    #[serde(default, skip_serializing_if = "SubagentsToolSettings::is_default")]
53    subagents: SubagentsToolSettings,
54    #[serde(default, skip_serializing_if = "Vec::is_empty")]
55    #[schemars(!skip_serializing_if)]
56    disabled: Vec<String>,
57}
58
59impl From<ToolSettingsWire> for ToolSettings {
60    fn from(wire: ToolSettingsWire) -> Self {
61        Self {
62            read: wire.read,
63            view_image: wire.view_image,
64            hash_edit: wire.hash_edit,
65            write: wire.write,
66            grep: wire.grep,
67            find: wire.find,
68            list_files: wire.list_files,
69            ast_grep: wire.ast_grep,
70            bash: wire.bash,
71            subagents: wire.subagents,
72            disabled: wire.disabled,
73        }
74    }
75}
76
77impl From<&ToolSettings> for ToolSettingsWire {
78    fn from(settings: &ToolSettings) -> Self {
79        Self {
80            read: settings.read.clone(),
81            view_image: settings.view_image.clone(),
82            hash_edit: settings.hash_edit.clone(),
83            write: settings.write.clone(),
84            grep: settings.grep.clone(),
85            find: settings.find.clone(),
86            list_files: settings.list_files.clone(),
87            ast_grep: settings.ast_grep.clone(),
88            bash: settings.bash.clone(),
89            subagents: settings.subagents.clone(),
90            disabled: settings.disabled.to_vec(),
91        }
92    }
93}
94
95impl Serialize for ToolSettings {
96    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
97    where
98        S: serde::Serializer,
99    {
100        ToolSettingsWire::from(self).serialize(serializer)
101    }
102}
103
104impl<'de> Deserialize<'de> for ToolSettings {
105    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
106    where
107        D: serde::Deserializer<'de>,
108    {
109        ToolSettingsWire::deserialize(deserializer).map(Into::into)
110    }
111}
112
113impl JsonSchema for ToolSettings {
114    fn schema_name() -> std::borrow::Cow<'static, str> {
115        "ToolSettings".into()
116    }
117
118    fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
119        ToolSettingsWire::json_schema(generator)
120    }
121}
122
123macro_rules! absolute_path_tool_settings {
124    ($($name:ident),+ $(,)?) => {
125        $(
126            #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
127            pub struct $name {
128                #[serde(default = "default_true")]
129                pub absolute_paths: bool,
130            }
131
132            impl Default for $name {
133                fn default() -> Self {
134                    Self {
135                        absolute_paths: true,
136                    }
137                }
138            }
139            impl $name {
140                pub(crate) fn is_default(&self) -> bool {
141                    self == &Self::default()
142                }
143            }
144        )+
145    };
146}
147
148absolute_path_tool_settings!(
149    ReadToolSettings,
150    HashEditToolSettings,
151    WriteToolSettings,
152    GrepToolSettings,
153    FindToolSettings,
154    ListFilesToolSettings,
155    AstGrepToolSettings,
156);
157
158#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
159pub struct SubagentsToolSettings {
160    #[serde(default = "default_true")]
161    pub absolute_paths: bool,
162    #[serde(default = "default_subagent_max_depth")]
163    pub max_depth: usize,
164}
165
166impl Default for SubagentsToolSettings {
167    fn default() -> Self {
168        Self {
169            absolute_paths: true,
170            max_depth: DEFAULT_SUBAGENT_MAX_DEPTH,
171        }
172    }
173}
174
175impl SubagentsToolSettings {
176    pub(crate) fn is_default(&self) -> bool {
177        self == &Self::default()
178    }
179}
180
181fn default_subagent_max_depth() -> usize {
182    DEFAULT_SUBAGENT_MAX_DEPTH
183}
184
185#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
186pub struct BashToolSettings {
187    #[serde(default = "default_true")]
188    pub absolute_paths: bool,
189    #[serde(default = "default_true")]
190    pub shell_expansion: bool,
191}
192
193impl Default for BashToolSettings {
194    fn default() -> Self {
195        Self {
196            absolute_paths: true,
197            shell_expansion: true,
198        }
199    }
200}
201
202impl BashToolSettings {
203    pub(crate) fn is_default(&self) -> bool {
204        self == &Self::default()
205    }
206}
207
208pub const DEFAULT_VIEW_IMAGE_MAX_IMAGE_BYTES: u64 = 5 * 1024 * 1024;
209pub const MAX_VIEW_IMAGE_MAX_IMAGE_BYTES: u64 = 20 * 1024 * 1024;
210
211#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
212pub struct ViewImageToolSettings {
213    #[serde(default, skip_serializing_if = "Option::is_none")]
214    pub vision_model: Option<ViewImageVisionModelSettings>,
215    #[serde(default = "default_true")]
216    pub absolute_paths: bool,
217    #[serde(default = "default_view_image_max_image_bytes")]
218    pub max_image_bytes: u64,
219}
220
221#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
222pub struct ViewImageVisionModelSettings {
223    pub provider: String,
224    pub model: String,
225}
226
227impl ViewImageToolSettings {
228    pub(crate) fn is_default(&self) -> bool {
229        self == &Self::default()
230    }
231}
232
233impl Default for ViewImageToolSettings {
234    fn default() -> Self {
235        Self {
236            vision_model: None,
237            absolute_paths: true,
238            max_image_bytes: DEFAULT_VIEW_IMAGE_MAX_IMAGE_BYTES,
239        }
240    }
241}
242
243pub(crate) fn default_view_image_max_image_bytes() -> u64 {
244    DEFAULT_VIEW_IMAGE_MAX_IMAGE_BYTES
245}
246
247pub(crate) fn validate_view_image_identifier(field: &str, value: &str) -> anyhow::Result<String> {
248    validate_model_identifier(&format!("tools.view_image.vision_model.{field}"), value)
249}
250
251pub(super) fn validate_model_identifier(field: &str, value: &str) -> anyhow::Result<String> {
252    let trimmed = value.trim();
253    if trimmed.is_empty() {
254        anyhow::bail!("{field} must not be empty");
255    }
256    if trimmed
257        .chars()
258        .any(|ch| ch.is_ascii_control() || ch.is_ascii_whitespace())
259    {
260        anyhow::bail!("{field} must not contain ASCII whitespace or control characters");
261    }
262    if crate::config::looks_like_secret_value(trimmed) {
263        anyhow::bail!("{field} must not look like a secret value");
264    }
265    Ok(trimmed.to_string())
266}
267
268pub(crate) fn validate_view_image_max_image_bytes(value: u64) -> anyhow::Result<u64> {
269    if !(1..=MAX_VIEW_IMAGE_MAX_IMAGE_BYTES).contains(&value) {
270        anyhow::bail!(
271            "tools.view_image.max_image_bytes must be between 1 and {MAX_VIEW_IMAGE_MAX_IMAGE_BYTES}"
272        );
273    }
274    Ok(value)
275}
276
277pub(crate) fn clamp_subagent_max_depth(max_depth: usize) -> usize {
278    max_depth.clamp(1, MAX_SUBAGENT_MAX_DEPTH)
279}