magi_code/config/settings/
tools.rs1use 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}