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