#[cfg(test)]
mod profiling_support;
use crate::{
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 {
view_image: super::view_image::ViewImageState,
settings: crate::config::ToolSettings,
cwd: PathBuf,
cwd_canonical: PathBuf,
paths: Option<crate::config::McPaths>,
checkpoint_context: Arc<Mutex<Option<crate::checkpoints::SnapshotContext>>>,
bash: super::bash::BashState,
bash_jobs: super::bash_jobs::BashJobs,
humanize_protection: Option<crate::protection::humanize::HumanizeProtection>,
prompt_injection_protection:
Option<crate::protection::prompt_injection::PromptInjectionProtection>,
subdir_discovery: bool,
subagent_depth: usize,
mutation_locks: Arc<Mutex<HashMap<PathBuf, Arc<Mutex<()>>>>>,
path_search: Arc<find::FindSearchService>,
fs_cache: Arc<fs_cache::FsCache>,
hashline_snapshots: Arc<Mutex<HashlineSnapshotStore>>,
pending_read_snapshots: Option<Arc<Mutex<Vec<super::fs::ReadSnapshot>>>>,
workspace_walker: Arc<workspace::WorkspaceWalker>,
skills: Arc<BTreeMap<String, crate::skills::DiscoveredSkill>>,
subagents: Option<Arc<super::subagents::SubagentsRunner>>,
web_cache: Arc<Mutex<WebCache>>,
mcp: Option<Arc<Mutex<crate::mcp::manager::McpManager>>>,
mcp_definitions: Arc<Vec<(crate::mcp::QualifiedMcpToolName, crate::mcp::Tool)>>,
disabled_tools: Arc<Mutex<HashSet<String>>>,
restricted_tools: HashSet<String>,
inspection: bool,
task_scope: Option<Arc<Mutex<crate::sessions::task_scope::TaskScope>>>,
client_tools: Option<Arc<super::client::ClientToolRun>>,
client_task: Option<(String, String)>,
client_ceiling: Option<BTreeMap<String, String>>,
}
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.settings.read.absolute_paths)
.field(
"view_image_absolute_paths",
&self.settings.view_image.absolute_paths,
)
.field(
"view_image_max_image_bytes",
&self.settings.view_image.max_image_bytes,
)
.field(
"view_image_vision_model",
&self
.settings
.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.paths.is_some())
.field(
"hashline_absolute_paths",
&self.settings.hash_edit.absolute_paths,
)
.field("write_absolute_paths", &self.settings.write.absolute_paths)
.field("grep_absolute_paths", &self.settings.grep.absolute_paths)
.field("find_absolute_paths", &self.settings.find.absolute_paths)
.field(
"list_files_absolute_paths",
&self.settings.list_files.absolute_paths,
)
.field(
"ast_grep_absolute_paths",
&self.settings.ast_grep.absolute_paths,
)
.field("bash_absolute_paths", &self.settings.bash.absolute_paths)
.field("bash_shell_expansion", &self.settings.bash.shell_expansion)
.field(
"subagents_absolute_paths",
&self.settings.subagents.absolute_paths,
)
.field("subagents_max_depth", &self.settings.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("disabled_tools", &self.disabled_tool_names().ok())
.finish_non_exhaustive()
}
}
impl ToolRuntime {
fn new_with_settings_and_disabled_tools(
cwd: impl Into<PathBuf>,
mut settings: crate::config::ToolSettings,
disabled_tools: Arc<Mutex<HashSet<String>>>,
) -> anyhow::Result<Self> {
let cwd = cwd.into();
let cwd_canonical = cwd.canonicalize()?;
settings.subagents.max_depth =
crate::config::clamp_subagent_max_depth(settings.subagents.max_depth);
Ok(Self {
settings,
cwd: cwd_canonical.clone(),
cwd_canonical: cwd_canonical.clone(),
view_image: super::view_image::ViewImageState {
custom_providers: BTreeMap::new(),
text_verbosity: None,
codex_text_verbosity: crate::config::TextVerbosity::Low,
},
paths: None,
checkpoint_context: Arc::new(Mutex::new(None)),
bash: super::bash::BashState {
protection: None,
file_tracking: None,
},
bash_jobs: super::bash_jobs::BashJobs::default(),
humanize_protection: None,
prompt_injection_protection: None,
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())),
pending_read_snapshots: None,
workspace_walker: Arc::new(workspace::WorkspaceWalker::default()),
skills: Arc::new(BTreeMap::new()),
subagents: None,
web_cache: Arc::new(Mutex::new(WebCache::default())),
mcp: None,
mcp_definitions: Arc::new(Vec::new()),
disabled_tools,
restricted_tools: if crate::config::subagents_disabled() {
HashSet::from([super::contract::tool_name::SUBAGENTS.to_string()])
} else {
HashSet::new()
},
inspection: false,
task_scope: None,
client_tools: None,
client_task: None,
client_ceiling: 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 shared_text_verbosity = settings.openai_responses.text_verbosity;
let codex_text_verbosity = settings
.text_verbosity_for(crate::provider_ids::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 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),
),
)));
}
if let Some(manager) = &runtime.mcp {
runtime.mcp_definitions = Arc::new(
manager
.lock()
.map_err(|_| anyhow::anyhow!("MCP manager lock poisoned"))?
.list_tool_definitions(),
);
}
runtime.view_image.custom_providers = custom_providers;
runtime.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)
}
}
impl ToolRuntime {
pub(crate) fn canonical_disabled_key(name: &str) -> anyhow::Result<String> {
super::names::canonical_disabled_key(name)
}
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"))?;
if disabled.contains(super::contract::tool_name::BASH)
|| self
.restricted_tools
.contains(super::contract::tool_name::BASH)
{
disabled.insert(super::contract::tool_name::BASH_JOB.to_string());
}
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 name == super::contract::tool_name::BASH_JOB
&& self.is_tool_disabled(super::contract::tool_name::BASH)
{
return true;
}
if self.inspection && !super::ceiling::inspection_allows(name) {
return true;
}
if name.starts_with(super::client::PREFIX)
&& let Some(ceiling) = &self.client_ceiling
&& self
.client_tools
.as_ref()
.and_then(|run| run.registry.fingerprint(name))
!= ceiling.get(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
.settings
.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)
}
}
impl ToolRuntime {
pub fn clone_for_cwd_with_subagent_depth(
&self,
cwd: &Path,
depth: usize,
) -> anyhow::Result<Self> {
let cwd_canonical = cwd.canonicalize()?;
Ok(Self {
settings: self.settings.clone(),
cwd: cwd_canonical.clone(),
cwd_canonical,
view_image: self.view_image.clone(),
paths: self.paths.clone(),
checkpoint_context: Arc::new(Mutex::new(None)),
bash: self.bash.clone(),
bash_jobs: self.bash_jobs.child()?,
humanize_protection: self.humanize_protection.clone(),
prompt_injection_protection: self.prompt_injection_protection.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),
pending_read_snapshots: None,
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(),
mcp_definitions: Arc::clone(&self.mcp_definitions),
disabled_tools: Arc::clone(&self.disabled_tools),
restricted_tools: self.restricted_tools.clone(),
inspection: self.inspection,
task_scope: None,
client_tools: self.client_tools.clone(),
client_task: self.client_task.clone(),
client_ceiling: self.client_ceiling.clone(),
})
}
pub(crate) fn subagents_absolute_paths(&self) -> bool {
self.settings.subagents.absolute_paths
}
pub(crate) fn subagents_max_depth(&self) -> usize {
self.settings.subagents.max_depth
}
pub(crate) fn subagents_schema_enabled(&self) -> bool {
self.subagent_depth < self.settings.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.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 cwd_canonical(&self) -> &Path {
&self.cwd_canonical
}
pub(crate) fn dynamic_tool_output_schemas(&self) -> std::collections::HashMap<String, Value> {
self.mcp_definitions
.iter()
.filter_map(|(name, tool)| {
tool.output_schema
.clone()
.map(|schema| (name.to_string(), schema))
})
.collect()
}
pub(crate) fn dynamic_provider_tool_definitions(&self) -> Vec<Value> {
if self.inspection {
return Vec::new();
}
let mut definitions: Vec<Value> = self
.mcp_definitions
.iter()
.map(|(name, tool)| crate::mcp::tool_schema::provider_tool_definition(name, tool))
.collect();
if let Some(run) = &self.client_tools {
definitions.extend(run.registry.definitions());
}
definitions.retain(|definition| {
definition
.get("name")
.and_then(Value::as_str)
.is_some_and(|name| !self.is_tool_disabled(name))
});
definitions
}
}
impl ToolRuntime {
pub(super) fn list_files_context(&self) -> super::list_files::ListFilesContext<'_> {
super::list_files::ListFilesContext {
paths: self.path_context(),
settings: &self.settings.list_files,
fs_cache: &self.fs_cache,
}
}
pub(super) fn grep_context(&self) -> super::grep::GrepContext<'_> {
super::grep::GrepContext {
paths: self.path_context(),
settings: &self.settings.grep,
workspace_walker: &self.workspace_walker,
}
}
pub(super) fn find_context(&self) -> super::find::FindContext<'_> {
super::find::FindContext {
paths: self.path_context(),
settings: &self.settings.find,
path_search: &self.path_search,
workspace_walker: &self.workspace_walker,
}
}
pub(super) fn ast_grep_context(&self) -> super::ast_grep::AstGrepContext<'_> {
super::ast_grep::AstGrepContext {
paths: self.path_context(),
settings: &self.settings.ast_grep,
workspace_walker: &self.workspace_walker,
}
}
pub(super) fn bash_context(&self) -> super::bash::BashContext<'_> {
super::bash::BashContext {
paths: self.path_context(),
settings: &self.settings.bash,
bash_file_tracking: &self.bash.file_tracking,
jobs: &self.bash_jobs,
}
}
pub(crate) fn with_bash_jobs(mut self, jobs: super::bash_jobs::BashJobs) -> Self {
self.bash_jobs = jobs;
self
}
pub(crate) fn bash_jobs_cleanup_guard(&self) -> super::bash_jobs::BashJobsCleanupGuard {
self.bash_jobs.cleanup_guard()
}
pub(super) fn bash_job(
&self,
args: super::args::BashJobArgs,
cancellation: &crate::cancellation::AgentCancellation,
) -> anyhow::Result<ToolResult> {
self.bash_jobs.call(args, cancellation)
}
pub(super) fn view_image_context(&self) -> super::view_image::ViewImageContext<'_> {
super::view_image::ViewImageContext {
paths: self.path_context(),
settings: &self.settings.view_image,
view_image_custom_providers: &self.view_image.custom_providers,
credential_paths: &self.paths,
view_image_text_verbosity: self.view_image.text_verbosity,
view_image_codex_text_verbosity: self.view_image.codex_text_verbosity,
}
}
pub(super) fn exa_context(&self) -> super::exa::WebContext<'_> {
super::exa::WebContext {
paths: &self.paths,
web_cache: &self.web_cache,
}
}
pub(super) fn skill_context(&self) -> super::skill::SkillContext<'_> {
super::skill::SkillContext {
skills: &self.skills,
}
}
pub(super) fn hash_edit_context(&self) -> super::hash_edit::tool::HashEditContext<'_> {
super::hash_edit::tool::HashEditContext {
paths: self.path_context(),
mutation: self.mutation_context(),
settings: &self.settings.hash_edit,
hashline_snapshots: &self.hashline_snapshots,
humanize_protection: &self.humanize_protection,
}
}
pub(super) fn path_context(&self) -> super::fs::PathContext<'_> {
super::fs::PathContext {
cwd: &self.cwd,
cwd_canonical: &self.cwd_canonical,
}
}
fn mutation_context(&self) -> super::fs::MutationContext<'_> {
super::fs::MutationContext {
paths: self.path_context(),
mutation_locks: &self.mutation_locks,
checkpoint_context: &self.checkpoint_context,
}
}
pub(crate) fn for_parallel_call(&self) -> Self {
let mut runtime = self.clone();
runtime.pending_read_snapshots = Some(Arc::new(Mutex::new(Vec::new())));
runtime
}
pub(crate) fn record_parallel_read_snapshots(&self, source: &Self) -> anyhow::Result<()> {
let Some(pending) = &source.pending_read_snapshots else {
return Ok(());
};
let mut pending = pending
.lock()
.map_err(|_| anyhow::anyhow!("pending read snapshot mutex poisoned"))?;
if pending.is_empty() {
return Ok(());
}
let mut store = self
.hashline_snapshots
.lock()
.map_err(|_| anyhow::anyhow!("hashline snapshot store mutex poisoned"))?;
for snapshot in pending.drain(..) {
store.record(snapshot.path, snapshot.text, snapshot.seen_lines);
}
Ok(())
}
pub(super) fn read_context(&self) -> super::fs::ReadContext<'_> {
super::fs::ReadContext {
paths: self.path_context(),
settings: &self.settings.read,
hashline_snapshots: &self.hashline_snapshots,
pending_snapshots: self.pending_read_snapshots.as_deref(),
skills: self.skill_context(),
}
}
pub(super) fn write_context(&self) -> super::fs::WriteContext<'_> {
super::fs::WriteContext {
paths: self.path_context(),
settings: &self.settings.write,
mutation: self.mutation_context(),
humanize_protection: &self.humanize_protection,
}
}
}
impl ToolRuntime {
pub(super) fn capture_tool_ceiling(&self) -> anyhow::Result<super::ceiling::ToolCeiling> {
Ok(super::ceiling::ToolCeiling {
cwd: self.cwd_canonical.clone(),
inspection: self.inspection,
enabled_builtins: super::MVP_TOOL_CAPABILITIES
.iter()
.filter(|tool| !self.is_tool_disabled(tool.canonical_name()))
.map(|tool| tool.canonical_name().to_string())
.collect(),
disabled_tools: self.disabled_tool_names()?.into_iter().collect(),
code_mode_disabled_tools: Some(
self.for_code_mode()
.disabled_tool_names()?
.into_iter()
.collect(),
),
client_tools: self
.client_tools
.as_ref()
.map(|run| {
run.registry
.fingerprints()
.into_iter()
.filter(|(name, _)| {
!self.is_tool_disabled(name)
|| (crate::code_mode::catalog::code_mode_available(self)
&& !self.for_code_mode().is_tool_disabled(name))
})
.collect()
})
.unwrap_or_default(),
read_absolute_paths: self.settings.read.absolute_paths,
view_image_absolute_paths: self.settings.view_image.absolute_paths,
hashline_absolute_paths: self.settings.hash_edit.absolute_paths,
write_absolute_paths: self.settings.write.absolute_paths,
grep_absolute_paths: self.settings.grep.absolute_paths,
find_absolute_paths: self.settings.find.absolute_paths,
list_files_absolute_paths: self.settings.list_files.absolute_paths,
ast_grep_absolute_paths: self.settings.ast_grep.absolute_paths,
bash_absolute_paths: self.settings.bash.absolute_paths,
bash_shell_expansion: self.settings.bash.shell_expansion,
subagents_absolute_paths: self.settings.subagents.absolute_paths,
remaining_depth: self
.settings
.subagents
.max_depth
.saturating_sub(self.subagent_depth),
})
}
pub(super) fn apply_tool_ceiling(
&mut self,
ceiling: &super::ceiling::ToolCeiling,
) -> anyhow::Result<()> {
ceiling.validate()?;
anyhow::ensure!(
self.cwd_canonical == ceiling.cwd,
"delegated task cwd differs from its saved capability ceiling; start a separately authorized task"
);
self.restrict_to_inspection(ceiling.inspection);
let mut client_ceiling = ceiling.client_tools.clone();
if let Some(inherited) = &self.client_ceiling {
client_ceiling.retain(|name, fingerprint| inherited.get(name) == Some(fingerprint));
}
self.client_ceiling = Some(client_ceiling);
let mut disabled = self.disabled_tool_names()?;
disabled.extend(ceiling.disabled_tools.iter().cloned());
disabled.extend(
super::MVP_TOOL_CAPABILITIES
.iter()
.filter(|tool| !ceiling.enabled_builtins.contains(tool.canonical_name()))
.map(|tool| tool.canonical_name().to_string()),
);
self.settings.code_mode.disabled.extend(
ceiling
.code_mode_disabled_tools
.as_ref()
.map(|names| names.iter().cloned().collect::<Vec<_>>())
.unwrap_or_else(|| disabled.iter().cloned().collect()),
);
self.disabled_tools = std::sync::Arc::new(std::sync::Mutex::new(disabled));
self.settings.read.absolute_paths &= ceiling.read_absolute_paths;
self.settings.view_image.absolute_paths &= ceiling.view_image_absolute_paths;
self.settings.hash_edit.absolute_paths &= ceiling.hashline_absolute_paths;
self.settings.write.absolute_paths &= ceiling.write_absolute_paths;
self.settings.grep.absolute_paths &= ceiling.grep_absolute_paths;
self.settings.find.absolute_paths &= ceiling.find_absolute_paths;
self.settings.list_files.absolute_paths &= ceiling.list_files_absolute_paths;
self.settings.ast_grep.absolute_paths &= ceiling.ast_grep_absolute_paths;
self.settings.bash.absolute_paths &= ceiling.bash_absolute_paths;
self.settings.bash.shell_expansion &= ceiling.bash_shell_expansion;
self.settings.subagents.absolute_paths &= ceiling.subagents_absolute_paths;
self.settings.subagents.max_depth = self
.settings
.subagents
.max_depth
.min(self.subagent_depth.saturating_add(ceiling.remaining_depth));
Ok(())
}
}
impl ToolRuntime {
pub(crate) fn code_mode_settings(&self) -> &crate::config::CodeModeToolSettings {
&self.settings.code_mode
}
pub(crate) fn task_scope(&self) -> Option<&Arc<Mutex<crate::sessions::task_scope::TaskScope>>> {
self.task_scope.as_ref()
}
pub(crate) fn set_client_tools(&mut self, tools: Option<Arc<super::client::ClientToolRun>>) {
self.client_tools = tools;
}
pub(crate) fn client_tools(&self) -> Option<&Arc<super::client::ClientToolRun>> {
self.client_tools.as_ref()
}
pub(crate) fn set_client_task(&mut self, task: Option<(String, String)>) {
self.client_task = task;
}
pub(super) fn client_task(&self) -> Option<&(String, String)> {
self.client_task.as_ref()
}
pub(super) fn inspection_enabled(&self) -> bool {
self.inspection
}
pub(super) fn mcp_manager(&self) -> Option<&Arc<Mutex<crate::mcp::manager::McpManager>>> {
self.mcp.as_ref()
}
pub(super) fn subagents_context(&self) -> super::subagents::SubagentsContext<'_> {
super::subagents::SubagentsContext {
runner: self.subagents.as_deref(),
}
}
pub(super) fn magi_control_context(&self) -> super::control::MagiControlContext<'_> {
super::control::MagiControlContext {
compact_enabled: &self.settings.magi_control.compact,
}
}
}