#[cfg(test)]
mod profiling_support;
use crate::{
lsp::LspManager,
output::ToolDispatchContext,
skills::SkillDiscovery,
tools::{
ToolResult, exa::WebCache, find, fs_cache, hash_edit::HashlineSnapshotStore, workspace,
},
};
use serde_json::Value;
use std::{
collections::{BTreeMap, HashMap, HashSet},
path::{Path, PathBuf},
sync::{Arc, Mutex},
};
#[derive(Clone)]
pub struct ToolRuntime {
pub(super) cwd: PathBuf,
pub(super) cwd_canonical: PathBuf,
pub(super) read_absolute_paths: bool,
pub(super) view_image_absolute_paths: bool,
pub(super) view_image_max_image_bytes: u64,
pub(super) view_image_vision_model: Option<crate::config::ViewImageVisionModelSettings>,
pub(super) view_image_custom_providers: BTreeMap<String, crate::config::CustomProviderConfig>,
pub(super) view_image_paths: Option<crate::config::McPaths>,
pub(super) view_image_text_verbosity: Option<crate::config::TextVerbosity>,
pub(super) view_image_codex_text_verbosity: crate::config::TextVerbosity,
pub(super) checkpoint_context: Arc<Mutex<Option<crate::checkpoints::SnapshotContext>>>,
pub(super) hashline_absolute_paths: bool,
pub(super) write_absolute_paths: bool,
pub(super) grep_absolute_paths: bool,
pub(super) find_absolute_paths: bool,
pub(super) list_files_absolute_paths: bool,
pub(super) ast_grep_absolute_paths: bool,
pub(super) bash_absolute_paths: bool,
pub(super) bash_shell_expansion: bool,
pub(super) bash_protection: Option<crate::protection::bash::BashProtection>,
pub(super) bash_file_tracking: Option<crate::typesafe::TypeSafeClient>,
pub(super) humanize_protection: Option<crate::protection::humanize::HumanizeProtection>,
pub(super) prompt_injection_protection:
Option<crate::protection::prompt_injection::PromptInjectionProtection>,
pub(super) subagents_absolute_paths: bool,
pub(super) subagents_max_depth: usize,
pub(super) magi_control_compact: bool,
pub(crate) code_mode: crate::config::CodeModeToolSettings,
pub(super) subdir_discovery: bool,
pub(super) subagent_depth: usize,
pub(super) mutation_locks: Arc<Mutex<HashMap<PathBuf, Arc<Mutex<()>>>>>,
pub(super) path_search: Arc<find::FindSearchService>,
pub(super) fs_cache: Arc<fs_cache::FsCache>,
pub(super) hashline_snapshots: Arc<Mutex<HashlineSnapshotStore>>,
pub(super) workspace_walker: Arc<workspace::WorkspaceWalker>,
pub(super) skills: Arc<BTreeMap<String, crate::skills::DiscoveredSkill>>,
pub(super) subagents:
Option<Arc<dyn Fn(Value, ToolDispatchContext) -> ToolResult + Send + Sync>>,
pub(super) web_cache: Arc<Mutex<WebCache>>,
pub(super) mcp: Option<Arc<Mutex<crate::mcp::manager::McpManager>>>,
lsp_manager: Option<Arc<LspManager>>,
pub(super) disabled_tools: Arc<Mutex<HashSet<String>>>,
pub(super) restricted_tools: HashSet<String>,
pub(super) inspection: bool,
pub(crate) task_scope: Option<Arc<Mutex<crate::sessions::task_scope::TaskScope>>>,
}
impl std::fmt::Debug for ToolRuntime {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("ToolRuntime")
.field("cwd", &self.cwd)
.field("cwd_canonical", &self.cwd_canonical)
.field("read_absolute_paths", &self.read_absolute_paths)
.field("view_image_absolute_paths", &self.view_image_absolute_paths)
.field(
"view_image_max_image_bytes",
&self.view_image_max_image_bytes,
)
.field(
"view_image_vision_model",
&self
.view_image_vision_model
.as_ref()
.map(|_| "<configured>"),
)
.field(
"view_image_custom_providers",
&self.view_image_custom_providers.keys().collect::<Vec<_>>(),
)
.field("view_image_has_paths", &self.view_image_paths.is_some())
.field("hashline_absolute_paths", &self.hashline_absolute_paths)
.field("write_absolute_paths", &self.write_absolute_paths)
.field("grep_absolute_paths", &self.grep_absolute_paths)
.field("find_absolute_paths", &self.find_absolute_paths)
.field("list_files_absolute_paths", &self.list_files_absolute_paths)
.field("ast_grep_absolute_paths", &self.ast_grep_absolute_paths)
.field("bash_absolute_paths", &self.bash_absolute_paths)
.field("bash_shell_expansion", &self.bash_shell_expansion)
.field("subagents_absolute_paths", &self.subagents_absolute_paths)
.field("subagents_max_depth", &self.subagents_max_depth)
.field("subdir_discovery", &self.subdir_discovery)
.field("subagent_depth", &self.subagent_depth)
.field("path_index_count", &self.path_search.index_count())
.field("fs_cache_entries", &self.fs_cache.len().ok())
.field(
"hashline_snapshot_count",
&self.hashline_snapshots.lock().map(|store| store.len()).ok(),
)
.field("skills_count", &self.skills.len())
.field("has_subagents", &self.subagents.is_some())
.field("has_mcp", &self.mcp.is_some())
.field("has_lsp", &self.lsp_manager.is_some())
.field("disabled_tools", &self.disabled_tool_names().ok())
.finish_non_exhaustive()
}
}
impl ToolRuntime {
fn new_with_settings_and_disabled_tools(
cwd: impl Into<PathBuf>,
settings: crate::config::ToolSettings,
disabled_tools: Arc<Mutex<HashSet<String>>>,
) -> anyhow::Result<Self> {
let cwd = cwd.into();
let cwd_canonical = cwd.canonicalize()?;
Ok(Self {
cwd: cwd_canonical.clone(),
cwd_canonical: cwd_canonical.clone(),
read_absolute_paths: settings.read.absolute_paths,
view_image_absolute_paths: settings.view_image.absolute_paths,
view_image_max_image_bytes: settings.view_image.max_image_bytes,
view_image_vision_model: settings.view_image.vision_model.clone(),
view_image_custom_providers: BTreeMap::new(),
view_image_paths: None,
view_image_text_verbosity: None,
view_image_codex_text_verbosity: crate::config::TextVerbosity::Low,
checkpoint_context: Arc::new(Mutex::new(None)),
hashline_absolute_paths: settings.hash_edit.absolute_paths,
write_absolute_paths: settings.write.absolute_paths,
grep_absolute_paths: settings.grep.absolute_paths,
find_absolute_paths: settings.find.absolute_paths,
list_files_absolute_paths: settings.list_files.absolute_paths,
ast_grep_absolute_paths: settings.ast_grep.absolute_paths,
bash_absolute_paths: settings.bash.absolute_paths,
bash_shell_expansion: settings.bash.shell_expansion,
bash_protection: None,
bash_file_tracking: None,
humanize_protection: None,
prompt_injection_protection: None,
subagents_absolute_paths: settings.subagents.absolute_paths,
subagents_max_depth: crate::config::clamp_subagent_max_depth(
settings.subagents.max_depth,
),
magi_control_compact: settings.magi_control.compact,
code_mode: settings.code_mode,
subdir_discovery: false,
subagent_depth: 0,
mutation_locks: Arc::new(Mutex::new(HashMap::new())),
path_search: Arc::new(find::FindSearchService),
fs_cache: Arc::new(fs_cache::FsCache::default()),
hashline_snapshots: Arc::new(Mutex::new(HashlineSnapshotStore::default())),
workspace_walker: Arc::new(workspace::WorkspaceWalker::default()),
skills: Arc::new(BTreeMap::new()),
subagents: None,
web_cache: Arc::new(Mutex::new(WebCache::default())),
mcp: None,
disabled_tools,
restricted_tools: HashSet::new(),
lsp_manager: None,
inspection: false,
task_scope: None,
})
}
pub(crate) fn new_with_full_settings_and_mcp_with_disabled_tools(
cwd: impl Into<PathBuf>,
paths: crate::config::McPaths,
settings: crate::config::Settings,
mcp: Option<Arc<Mutex<crate::mcp::manager::McpManager>>>,
disabled_tools: Arc<Mutex<HashSet<String>>>,
) -> anyhow::Result<Self> {
let mcp_servers = settings.mcp_servers.clone();
let custom_providers = settings.custom_providers.clone();
let lsp_settings = settings.lsp.clone();
let shared_text_verbosity = settings.openai_responses.text_verbosity;
let codex_text_verbosity = settings
.text_verbosity_for(crate::providers::OPENAI_CODEX_PROVIDER)
.expect("Codex text verbosity always resolves");
let mut runtime =
Self::new_with_settings_and_disabled_tools(cwd, settings.tools, disabled_tools)?;
if settings.jev.enabled && settings.jev.bash_file_tracking.enabled {
runtime.bash_file_tracking =
Some(crate::typesafe::TypeSafeClient::for_background_inference(
super::bash_file_tracking::ASSESSMENT_TIMEOUT,
)?);
}
if settings.jev.enabled && settings.jev.bash_protection.enabled {
runtime.bash_protection = Some(crate::protection::bash::BashProtection::from_settings(
settings.jev.bash_protection.clone(),
)?);
}
if settings.jev.enabled && settings.jev.humanize_protection.enabled {
runtime.humanize_protection = Some(
crate::protection::humanize::HumanizeProtection::from_settings(
settings.jev.humanize_protection.clone(),
)?,
);
}
if settings.jev.enabled && settings.jev.prompt_injection_protection.enabled {
runtime.prompt_injection_protection = Some(
crate::protection::prompt_injection::PromptInjectionProtection::from_settings(
settings.jev.prompt_injection_protection.clone(),
)?,
);
}
runtime.view_image_text_verbosity = shared_text_verbosity;
runtime.view_image_codex_text_verbosity = codex_text_verbosity;
if lsp_settings.enabled {
runtime.lsp_manager = Some(Arc::new(LspManager::new(
lsp_settings,
runtime.cwd_canonical.clone(),
)));
}
if let Some(mcp) = mcp {
runtime.mcp = Some(mcp);
} else if !mcp_servers.is_empty() {
runtime.mcp = Some(Arc::new(Mutex::new(
crate::mcp::manager::McpManager::from_settings_with_paths(
&mcp_servers,
Some(&paths.root),
),
)));
}
runtime.view_image_custom_providers = custom_providers;
runtime.view_image_paths = Some(paths);
runtime.subdir_discovery = settings.instructions.subdir_discovery;
Ok(runtime)
}
pub fn with_skills(mut self, skills: &SkillDiscovery) -> Self {
self.skills = Arc::new(skills.skills.clone());
self
}
pub fn with_subagents(
mut self,
runner: impl Fn(Value, ToolDispatchContext) -> ToolResult + Send + Sync + 'static,
) -> Self {
self.subagents = Some(Arc::new(runner));
self
}
pub(crate) fn clone_for_cwd_with_subagent_depth_and_additional_disabled_tools(
&self,
cwd: &Path,
depth: usize,
additional_disabled_tools: &HashSet<String>,
) -> anyhow::Result<Self> {
let mut disabled_tools = self.disabled_tool_names()?;
for name in additional_disabled_tools {
disabled_tools.insert(Self::canonical_disabled_key(name)?);
}
let mut cloned = self.clone_for_cwd_with_subagent_depth(cwd, depth)?;
cloned.disabled_tools = Arc::new(Mutex::new(disabled_tools));
cloned.restricted_tools.extend(
additional_disabled_tools
.iter()
.map(|name| Self::canonical_disabled_key(name))
.collect::<anyhow::Result<HashSet<_>>>()?,
);
Ok(cloned)
}
pub(crate) fn canonical_disabled_key(name: &str) -> anyhow::Result<String> {
if name == "jev" {
return Ok(name.to_string());
}
if let Some(tool) = crate::tools::ToolCapability::from_dispatch_name(name) {
return Ok(tool.canonical_name().to_string());
}
crate::mcp::QualifiedMcpToolName::parse(name)
.map(|qualified| qualified.to_string())
.map_err(|error| anyhow::anyhow!(error))
}
pub(crate) fn disabled_tool_names(&self) -> anyhow::Result<HashSet<String>> {
let mut disabled = self
.disabled_tools
.lock()
.map(|disabled| disabled.clone())
.map_err(|_| anyhow::anyhow!("disabled tools lock poisoned"))?;
disabled.extend(self.restricted_tools.iter().cloned());
if self.inspection {
disabled.extend(
super::MVP_TOOL_CAPABILITIES
.iter()
.filter(|tool| !super::ceiling::inspection_allows(tool.canonical_name()))
.map(|tool| tool.canonical_name().to_string()),
);
}
Ok(disabled)
}
pub(crate) fn is_tool_disabled(&self, name: &str) -> bool {
if self.inspection && !super::ceiling::inspection_allows(name) {
return true;
}
let Ok(key) = Self::canonical_disabled_key(name) else {
return false;
};
if self.restricted_tools.contains(&key) {
return true;
}
self.disabled_tools
.lock()
.map(|disabled| disabled.contains(&key))
.unwrap_or(true)
}
pub(crate) fn for_code_mode(&self) -> Self {
let mut runtime = self.clone();
let disabled = self
.code_mode
.disabled
.iter()
.filter_map(|name| Self::canonical_disabled_key(name).ok())
.collect();
runtime.disabled_tools = Arc::new(Mutex::new(disabled));
runtime
}
pub(crate) fn assess_bash_command(
&self,
command: &str,
) -> anyhow::Result<Option<crate::protection::bash::BashRiskAssessment>> {
self.bash_protection
.as_ref()
.map(|protection| protection.assess(command, &self.cwd, &self.cwd_canonical))
.transpose()
}
pub(crate) fn bash_protection_failure_policy(
&self,
) -> Option<crate::config::BashProtectionFailurePolicy> {
self.bash_protection
.as_ref()
.map(crate::protection::bash::BashProtection::failure_policy)
}
pub(crate) fn bash_protection_level(&self) -> Option<crate::config::BashProtectionLevel> {
self.bash_protection
.as_ref()
.map(crate::protection::bash::BashProtection::protection_level)
}
pub(crate) fn prompt_injection_enforcement(&self, tool_name: &str) -> Option<bool> {
self.prompt_injection_protection
.as_ref()
.filter(|protection| protection.applies_to(tool_name))
.map(|protection| protection.enforcement_enabled())
}
pub(crate) fn assess_tool_result_for_prompt_injection(
&self,
tool_name: &str,
content: &str,
context: &serde_json::Value,
) -> Option<(
crate::protection::prompt_injection::PromptInjectionAssessment,
bool,
)> {
self.prompt_injection_protection
.as_ref()
.filter(|protection| protection.applies_to(tool_name))
.map(|protection| {
(
protection.assess(tool_name, content, context),
protection.enforcement_enabled(),
)
})
}
pub(crate) fn restrict_to_inspection(&mut self, inspect: bool) {
self.inspection |= inspect;
}
pub(crate) fn with_task_scope(
mut self,
scope: crate::sessions::task_scope::TaskScope,
) -> anyhow::Result<Self> {
scope.apply(&mut self)?;
self.task_scope = Some(Arc::new(Mutex::new(scope)));
Ok(self)
}
pub fn clone_for_cwd_with_subagent_depth(
&self,
cwd: &Path,
depth: usize,
) -> anyhow::Result<Self> {
let cwd_canonical = cwd.canonicalize()?;
let lsp_manager = if cwd_canonical == self.cwd_canonical {
self.lsp_manager.clone()
} else {
None
};
Ok(Self {
cwd: cwd_canonical.clone(),
cwd_canonical,
read_absolute_paths: self.read_absolute_paths,
view_image_absolute_paths: self.view_image_absolute_paths,
view_image_max_image_bytes: self.view_image_max_image_bytes,
view_image_vision_model: self.view_image_vision_model.clone(),
view_image_custom_providers: self.view_image_custom_providers.clone(),
view_image_paths: self.view_image_paths.clone(),
view_image_text_verbosity: self.view_image_text_verbosity,
view_image_codex_text_verbosity: self.view_image_codex_text_verbosity,
checkpoint_context: Arc::new(Mutex::new(None)),
hashline_absolute_paths: self.hashline_absolute_paths,
write_absolute_paths: self.write_absolute_paths,
grep_absolute_paths: self.grep_absolute_paths,
find_absolute_paths: self.find_absolute_paths,
list_files_absolute_paths: self.list_files_absolute_paths,
ast_grep_absolute_paths: self.ast_grep_absolute_paths,
bash_absolute_paths: self.bash_absolute_paths,
bash_shell_expansion: self.bash_shell_expansion,
bash_protection: self.bash_protection.clone(),
bash_file_tracking: self.bash_file_tracking.clone(),
humanize_protection: self.humanize_protection.clone(),
prompt_injection_protection: self.prompt_injection_protection.clone(),
subagents_absolute_paths: self.subagents_absolute_paths,
subagents_max_depth: self.subagents_max_depth,
magi_control_compact: self.magi_control_compact,
code_mode: self.code_mode.clone(),
subdir_discovery: self.subdir_discovery,
subagent_depth: depth,
mutation_locks: Arc::clone(&self.mutation_locks),
path_search: Arc::clone(&self.path_search),
fs_cache: Arc::clone(&self.fs_cache),
hashline_snapshots: Arc::clone(&self.hashline_snapshots),
workspace_walker: Arc::clone(&self.workspace_walker),
skills: Arc::clone(&self.skills),
subagents: None,
web_cache: Arc::clone(&self.web_cache),
mcp: self.mcp.clone(),
disabled_tools: Arc::clone(&self.disabled_tools),
restricted_tools: self.restricted_tools.clone(),
lsp_manager,
inspection: self.inspection,
task_scope: None,
})
}
pub(crate) fn subagents_absolute_paths(&self) -> bool {
self.subagents_absolute_paths
}
pub(crate) fn subagents_max_depth(&self) -> usize {
self.subagents_max_depth
}
pub(crate) fn subagents_schema_enabled(&self) -> bool {
self.subagent_depth < self.subagents_max_depth
&& !self.is_tool_disabled(crate::tools::contract::tool_name::SUBAGENTS)
}
pub(crate) fn subagents_available_on_any_surface(&self) -> bool {
self.subagents_schema_enabled()
|| (crate::code_mode::catalog::code_mode_available(self)
&& self.for_code_mode().subagents_schema_enabled())
}
pub(crate) fn is_subagent(&self) -> bool {
self.subagent_depth > 0
}
pub(crate) fn subdir_discovery_enabled(&self) -> bool {
self.subdir_discovery
}
pub(crate) fn checkpoint_paths(&self) -> Option<crate::config::McPaths> {
self.view_image_paths.clone()
}
pub(crate) fn set_checkpoint_context(
&self,
context: Option<crate::checkpoints::SnapshotContext>,
) -> anyhow::Result<()> {
let mut slot = self
.checkpoint_context
.lock()
.map_err(|_| anyhow::anyhow!("checkpoint context lock poisoned"))?;
*slot = context;
Ok(())
}
pub(crate) fn checkpoint_context(&self) -> Option<crate::checkpoints::SnapshotContext> {
self.checkpoint_context
.lock()
.ok()
.and_then(|slot| slot.clone())
}
pub(crate) fn cwd_canonical(&self) -> &Path {
&self.cwd_canonical
}
pub(crate) fn lsp_manager(&self) -> Option<&LspManager> {
self.lsp_manager.as_deref()
}
pub(crate) fn dynamic_tool_output_schemas(&self) -> std::collections::HashMap<String, Value> {
self.mcp
.as_ref()
.and_then(|manager| manager.lock().ok())
.map(|manager| {
manager
.list_tool_definitions()
.into_iter()
.filter_map(|(name, tool)| {
tool.output_schema.map(|schema| (name.to_string(), schema))
})
.collect()
})
.unwrap_or_default()
}
pub(crate) fn dynamic_provider_tool_definitions(&self) -> Vec<Value> {
if self.inspection {
return Vec::new();
}
self.mcp
.as_ref()
.and_then(|manager| {
manager.lock().ok().map(|manager| {
manager
.provider_tool_definitions()
.into_iter()
.filter(|definition| {
definition
.get("name")
.and_then(Value::as_str)
.is_some_and(|name| !self.is_tool_disabled(name))
})
.collect()
})
})
.unwrap_or_default()
}
}
#[cfg(test)]
impl ToolRuntime {
pub fn new_with_settings(
cwd: impl Into<PathBuf>,
settings: crate::config::ToolSettings,
) -> anyhow::Result<Self> {
let disabled_tools = Arc::new(Mutex::new(Self::settings_disabled_tool_names(&settings)));
Self::new_with_settings_and_disabled_tools(cwd, settings, disabled_tools)
}
}