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 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}