use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::subagents::{DEFAULT_SUBAGENT_MAX_DEPTH, MAX_SUBAGENT_MAX_DEPTH};
use super::helpers::default_true;
#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
pub struct ToolOutputCompressionSettings {
#[serde(default)]
pub enabled: bool,
}
impl ToolOutputCompressionSettings {
pub(crate) fn is_default(&self) -> bool {
self == &Self::default()
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ToolSettings {
pub read: ReadToolSettings,
pub view_image: ViewImageToolSettings,
pub hash_edit: HashEditToolSettings,
pub write: WriteToolSettings,
pub grep: GrepToolSettings,
pub find: FindToolSettings,
pub list_files: ListFilesToolSettings,
pub ast_grep: AstGrepToolSettings,
pub bash: BashToolSettings,
pub subagents: SubagentsToolSettings,
pub output_compression: ToolOutputCompressionSettings,
pub disabled: Vec<String>,
}
#[derive(Serialize, Deserialize, JsonSchema)]
struct ToolSettingsWire {
#[serde(default, skip_serializing_if = "ReadToolSettings::is_default")]
#[schemars(!skip_serializing_if)]
read: ReadToolSettings,
#[serde(default, skip_serializing_if = "ViewImageToolSettings::is_default")]
#[schemars(!skip_serializing_if)]
view_image: ViewImageToolSettings,
#[serde(default, skip_serializing_if = "HashEditToolSettings::is_default")]
#[schemars(!skip_serializing_if)]
hash_edit: HashEditToolSettings,
#[serde(default, skip_serializing_if = "WriteToolSettings::is_default")]
#[schemars(!skip_serializing_if)]
write: WriteToolSettings,
#[serde(
default,
alias = "ffgrep",
skip_serializing_if = "GrepToolSettings::is_default"
)]
#[schemars(!skip_serializing_if)]
grep: GrepToolSettings,
#[serde(
default,
alias = "fffind",
skip_serializing_if = "FindToolSettings::is_default"
)]
#[schemars(!skip_serializing_if)]
find: FindToolSettings,
#[serde(default, skip_serializing_if = "ListFilesToolSettings::is_default")]
#[schemars(!skip_serializing_if)]
list_files: ListFilesToolSettings,
#[serde(default, skip_serializing_if = "AstGrepToolSettings::is_default")]
#[schemars(!skip_serializing_if)]
ast_grep: AstGrepToolSettings,
#[serde(default, skip_serializing_if = "BashToolSettings::is_default")]
#[schemars(!skip_serializing_if)]
bash: BashToolSettings,
#[serde(
default,
alias = "parallel_subagents",
skip_serializing_if = "SubagentsToolSettings::is_default"
)]
subagents: SubagentsToolSettings,
#[serde(
default,
skip_serializing_if = "ToolOutputCompressionSettings::is_default"
)]
#[schemars(!skip_serializing_if)]
output_compression: ToolOutputCompressionSettings,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[schemars(!skip_serializing_if)]
disabled: Vec<String>,
}
impl From<ToolSettingsWire> for ToolSettings {
fn from(wire: ToolSettingsWire) -> Self {
Self {
read: wire.read,
view_image: wire.view_image,
hash_edit: wire.hash_edit,
write: wire.write,
grep: wire.grep,
find: wire.find,
list_files: wire.list_files,
ast_grep: wire.ast_grep,
bash: wire.bash,
subagents: wire.subagents,
output_compression: wire.output_compression,
disabled: wire.disabled,
}
}
}
impl From<&ToolSettings> for ToolSettingsWire {
fn from(settings: &ToolSettings) -> Self {
Self {
read: settings.read.clone(),
view_image: settings.view_image.clone(),
hash_edit: settings.hash_edit.clone(),
write: settings.write.clone(),
grep: settings.grep.clone(),
find: settings.find.clone(),
list_files: settings.list_files.clone(),
ast_grep: settings.ast_grep.clone(),
bash: settings.bash.clone(),
subagents: settings.subagents.clone(),
output_compression: settings.output_compression.clone(),
disabled: settings.disabled.to_vec(),
}
}
}
impl Serialize for ToolSettings {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
ToolSettingsWire::from(self).serialize(serializer)
}
}
impl<'de> Deserialize<'de> for ToolSettings {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
ToolSettingsWire::deserialize(deserializer).map(Into::into)
}
}
impl JsonSchema for ToolSettings {
fn schema_name() -> std::borrow::Cow<'static, str> {
"ToolSettings".into()
}
fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
ToolSettingsWire::json_schema(generator)
}
}
macro_rules! absolute_path_tool_settings {
($($name:ident),+ $(,)?) => {
$(
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
pub struct $name {
#[serde(default = "default_true")]
pub absolute_paths: bool,
}
impl Default for $name {
fn default() -> Self {
Self {
absolute_paths: true,
}
}
}
impl $name {
pub(crate) fn is_default(&self) -> bool {
self == &Self::default()
}
}
)+
};
}
absolute_path_tool_settings!(
ReadToolSettings,
HashEditToolSettings,
WriteToolSettings,
GrepToolSettings,
FindToolSettings,
ListFilesToolSettings,
AstGrepToolSettings,
);
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
pub struct SubagentsToolSettings {
#[serde(default = "default_true")]
pub absolute_paths: bool,
#[serde(default = "default_subagent_max_depth")]
pub max_depth: usize,
}
impl Default for SubagentsToolSettings {
fn default() -> Self {
Self {
absolute_paths: true,
max_depth: DEFAULT_SUBAGENT_MAX_DEPTH,
}
}
}
impl SubagentsToolSettings {
pub(crate) fn is_default(&self) -> bool {
self == &Self::default()
}
}
fn default_subagent_max_depth() -> usize {
DEFAULT_SUBAGENT_MAX_DEPTH
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
pub struct BashToolSettings {
#[serde(default = "default_true")]
pub absolute_paths: bool,
#[serde(default = "default_true")]
pub shell_expansion: bool,
}
impl Default for BashToolSettings {
fn default() -> Self {
Self {
absolute_paths: true,
shell_expansion: true,
}
}
}
impl BashToolSettings {
pub(crate) fn is_default(&self) -> bool {
self == &Self::default()
}
}
pub const DEFAULT_VIEW_IMAGE_MAX_IMAGE_BYTES: u64 = 5 * 1024 * 1024;
pub const MAX_VIEW_IMAGE_MAX_IMAGE_BYTES: u64 = 20 * 1024 * 1024;
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
pub struct ViewImageToolSettings {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub vision_model: Option<ViewImageVisionModelSettings>,
#[serde(default = "default_true")]
pub absolute_paths: bool,
#[serde(default = "default_view_image_max_image_bytes")]
pub max_image_bytes: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
pub struct ViewImageVisionModelSettings {
pub provider: String,
pub model: String,
}
impl ViewImageToolSettings {
pub(crate) fn is_default(&self) -> bool {
self == &Self::default()
}
}
impl Default for ViewImageToolSettings {
fn default() -> Self {
Self {
vision_model: None,
absolute_paths: true,
max_image_bytes: DEFAULT_VIEW_IMAGE_MAX_IMAGE_BYTES,
}
}
}
pub(crate) fn default_view_image_max_image_bytes() -> u64 {
DEFAULT_VIEW_IMAGE_MAX_IMAGE_BYTES
}
pub(crate) fn validate_view_image_identifier(field: &str, value: &str) -> anyhow::Result<String> {
validate_model_identifier(&format!("tools.view_image.vision_model.{field}"), value)
}
pub(super) fn validate_model_identifier(field: &str, value: &str) -> anyhow::Result<String> {
let trimmed = value.trim();
if trimmed.is_empty() {
anyhow::bail!("{field} must not be empty");
}
if trimmed
.chars()
.any(|ch| ch.is_ascii_control() || ch.is_ascii_whitespace())
{
anyhow::bail!("{field} must not contain ASCII whitespace or control characters");
}
if crate::config::looks_like_secret_value(trimmed) {
anyhow::bail!("{field} must not look like a secret value");
}
Ok(trimmed.to_string())
}
pub(crate) fn validate_view_image_max_image_bytes(value: u64) -> anyhow::Result<u64> {
if !(1..=MAX_VIEW_IMAGE_MAX_IMAGE_BYTES).contains(&value) {
anyhow::bail!(
"tools.view_image.max_image_bytes must be between 1 and {MAX_VIEW_IMAGE_MAX_IMAGE_BYTES}"
);
}
Ok(value)
}
pub(crate) fn clamp_subagent_max_depth(max_depth: usize) -> usize {
max_depth.clamp(1, MAX_SUBAGENT_MAX_DEPTH)
}