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