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