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