use super::{MVP_TOOL_CAPABILITIES, ToolCapability, ToolRuntime};
use serde::{Deserialize, Serialize};
use std::collections::BTreeSet;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub(crate) struct ToolCeiling {
cwd: std::path::PathBuf,
inspection: bool,
enabled_builtins: BTreeSet<String>,
disabled_tools: BTreeSet<String>,
#[serde(default)]
code_mode_disabled_tools: Option<BTreeSet<String>>,
read_absolute_paths: bool,
view_image_absolute_paths: bool,
hashline_absolute_paths: bool,
write_absolute_paths: bool,
grep_absolute_paths: bool,
find_absolute_paths: bool,
list_files_absolute_paths: bool,
ast_grep_absolute_paths: bool,
bash_absolute_paths: bool,
bash_shell_expansion: bool,
subagents_absolute_paths: bool,
remaining_depth: usize,
}
pub(super) fn inspection_allows(name: &str) -> bool {
matches!(
ToolCapability::from_dispatch_name(name),
Some(
ToolCapability::Read
| ToolCapability::Find
| ToolCapability::ListFiles
| ToolCapability::Grep
| ToolCapability::AstGrep
)
)
}
impl ToolCeiling {
pub(crate) fn capture(tools: &ToolRuntime) -> anyhow::Result<Self> {
Ok(Self {
cwd: tools.cwd_canonical.clone(),
inspection: tools.inspection,
enabled_builtins: MVP_TOOL_CAPABILITIES
.iter()
.filter(|tool| !tools.is_tool_disabled(tool.canonical_name()))
.map(|tool| tool.canonical_name().to_string())
.collect(),
disabled_tools: tools.disabled_tool_names()?.into_iter().collect(),
code_mode_disabled_tools: Some(
tools
.for_code_mode()
.disabled_tool_names()?
.into_iter()
.collect(),
),
read_absolute_paths: tools.read_absolute_paths,
view_image_absolute_paths: tools.view_image_absolute_paths,
hashline_absolute_paths: tools.hashline_absolute_paths,
write_absolute_paths: tools.write_absolute_paths,
grep_absolute_paths: tools.grep_absolute_paths,
find_absolute_paths: tools.find_absolute_paths,
list_files_absolute_paths: tools.list_files_absolute_paths,
ast_grep_absolute_paths: tools.ast_grep_absolute_paths,
bash_absolute_paths: tools.bash_absolute_paths,
bash_shell_expansion: tools.bash_shell_expansion,
subagents_absolute_paths: tools.subagents_absolute_paths,
remaining_depth: tools
.subagents_max_depth
.saturating_sub(tools.subagent_depth),
})
}
pub(crate) fn validate(&self) -> anyhow::Result<()> {
anyhow::ensure!(
self.remaining_depth <= crate::subagents::MAX_SUBAGENT_MAX_DEPTH,
"invalid task delegation ceiling"
);
anyhow::ensure!(
self.disabled_tools.len() <= 1024
&& self.enabled_builtins.len()
<= MVP_TOOL_CAPABILITIES.len()
+ usize::from(self.enabled_builtins.contains("jev")),
"oversized task tool ceiling"
);
for name in self.enabled_builtins.iter().chain(&self.disabled_tools) {
anyhow::ensure!(
name.len() <= 512 && ToolRuntime::canonical_disabled_key(name)? == *name,
"invalid task tool ceiling"
);
}
if let Some(names) = &self.code_mode_disabled_tools {
anyhow::ensure!(names.len() <= 1024, "oversized Code Mode tool ceiling");
for name in names {
anyhow::ensure!(
name.len() <= 512 && ToolRuntime::canonical_disabled_key(name)? == *name,
"invalid Code Mode tool ceiling"
);
}
}
Ok(())
}
pub(crate) fn is_inspection(&self) -> bool {
self.inspection
}
pub(crate) fn apply(&self, tools: &mut ToolRuntime) -> anyhow::Result<()> {
self.validate()?;
anyhow::ensure!(
tools.cwd_canonical == self.cwd,
"delegated task cwd differs from its saved capability ceiling; start a separately authorized task"
);
tools.restrict_to_inspection(self.inspection);
let mut disabled = tools.disabled_tool_names()?;
disabled.extend(self.disabled_tools.iter().cloned());
disabled.extend(
MVP_TOOL_CAPABILITIES
.iter()
.filter(|tool| !self.enabled_builtins.contains(tool.canonical_name()))
.map(|tool| tool.canonical_name().to_string()),
);
tools.code_mode.disabled.extend(
self.code_mode_disabled_tools
.as_ref()
.map(|names| names.iter().cloned().collect::<Vec<_>>())
.unwrap_or_else(|| disabled.iter().cloned().collect()),
);
tools.disabled_tools = std::sync::Arc::new(std::sync::Mutex::new(disabled));
tools.read_absolute_paths &= self.read_absolute_paths;
tools.view_image_absolute_paths &= self.view_image_absolute_paths;
tools.hashline_absolute_paths &= self.hashline_absolute_paths;
tools.write_absolute_paths &= self.write_absolute_paths;
tools.grep_absolute_paths &= self.grep_absolute_paths;
tools.find_absolute_paths &= self.find_absolute_paths;
tools.list_files_absolute_paths &= self.list_files_absolute_paths;
tools.ast_grep_absolute_paths &= self.ast_grep_absolute_paths;
tools.bash_absolute_paths &= self.bash_absolute_paths;
tools.bash_shell_expansion &= self.bash_shell_expansion;
tools.subagents_absolute_paths &= self.subagents_absolute_paths;
tools.subagents_max_depth = tools
.subagents_max_depth
.min(tools.subagent_depth.saturating_add(self.remaining_depth));
Ok(())
}
}