use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ExposureProfile {
#[default]
Full,
Auto,
Minimal,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default)]
pub struct ToolExposureConfig {
pub profile: ExposureProfile,
#[serde(default = "default_schema_budget_pct")]
pub schema_budget_pct: u16,
#[serde(default = "default_max_initial_tools")]
pub max_initial_tools: usize,
#[serde(default = "default_supports_dynamic_catalog")]
pub supports_dynamic_catalog: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub allow_list: Option<Vec<String>>,
#[serde(default)]
pub read_only: bool,
}
impl Default for ToolExposureConfig {
fn default() -> Self {
Self {
profile: ExposureProfile::default(),
schema_budget_pct: default_schema_budget_pct(),
max_initial_tools: default_max_initial_tools(),
supports_dynamic_catalog: default_supports_dynamic_catalog(),
allow_list: None,
read_only: false,
}
}
}
pub const PI_PRESET_TOOLS: &[&str] = &["read_file", "write_file", "edit_file", "run_command"];
impl ToolExposureConfig {
pub fn is_pi_preset(&self) -> bool {
self.allow_list.as_deref().is_some_and(|list| {
list.len() == PI_PRESET_TOOLS.len()
&& PI_PRESET_TOOLS.iter().all(|t| list.iter().any(|n| n == t))
})
}
pub fn status_label(&self) -> Option<String> {
let list = match &self.allow_list {
None => None,
Some(_) if self.is_pi_preset() => Some("pi".to_string()),
Some(l) if l.is_empty() => Some("no tools".to_string()),
Some(l) => Some(format!("{} tools", l.len())),
};
match (list, self.read_only) {
(None, false) => None,
(None, true) => Some("read-only".to_string()),
(Some(l), false) => Some(l),
(Some(l), true) => Some(format!("{l} · read-only")),
}
}
}
pub(crate) fn default_schema_budget_pct() -> u16 {
15
}
pub(crate) fn default_max_initial_tools() -> usize {
0
}
pub(crate) fn default_supports_dynamic_catalog() -> bool {
true
}
#[cfg(test)]
mod tests {
use super::*;
fn with(list: Option<&[&str]>, read_only: bool) -> ToolExposureConfig {
ToolExposureConfig {
allow_list: list.map(|l| l.iter().map(|s| s.to_string()).collect()),
read_only,
..Default::default()
}
}
#[test]
fn status_label_names_the_preset_the_toggle_and_the_count() {
assert_eq!(with(None, false).status_label(), None);
assert_eq!(
with(None, true).status_label().as_deref(),
Some("read-only")
);
let pi: Vec<&str> = PI_PRESET_TOOLS.iter().rev().copied().collect();
assert!(with(Some(&pi), false).is_pi_preset(), "order-insensitive");
assert_eq!(with(Some(&pi), false).status_label().as_deref(), Some("pi"));
assert_eq!(
with(Some(&pi), true).status_label().as_deref(),
Some("pi · read-only")
);
assert_eq!(
with(Some(&["read_file", "find", "git"]), false)
.status_label()
.as_deref(),
Some("3 tools")
);
assert_eq!(
with(Some(&[]), false).status_label().as_deref(),
Some("no tools")
);
assert!(!with(Some(&["read_file"]), false).is_pi_preset());
}
}